// Parallel to the Type enum in Bookmark.h
// FIXME: Find a more appropriate place for this enum. Possibly in a utilities file.
var BookmarkType = {
    Leaf: 0,
    Folder: 1,
    Proxy: 2,
};

var NodeType = {
    Leaf: 0,
    Container: 1,
};

var GridSetups = {
    // FIXME: Make the title strings localized.
    // FIXME: Add proper width values, possibly make them be proportions instead of pixels widths.
    Standard: { "title": { title: "Bookmark", disclosure: true, sortable: false, width: 100},
                    "URLString": { title: "Address", disclosure: false, sortable: false} },
    BookmarksBar: { "title": { title: "Bookmark", disclosure: true, sortable: false, width: 50 },
                    "auto-click": { title: "Auto-Click", disclosure: false, sortable: false, width: 50},
                    "URLString": { title: "Address", disclosure: false, sortable: false } },
    Bonjour: { "title": { title: "Bookmark", disclosure: false, sortable: false } },
    AllRSS: { "title": { title: "Bookmark", disclosure: false, sortable: false, width: 50 },
              "parent": { title: "Parent", disclosure: false, sortable: false, width: 50},
               "URLString": { title: "Address", disclosure: false, sortable: false } },
};

BookmarksDataGridNode = function(data, nodeType, iconURLs, bookmark)
{
    BookmarksDataGridNode.baseConstructor.call(this, data, nodeType, iconURLs);
    // FIXME: Implement in this wrapper some of the custom behavior for the
    // bookmarks view, including custom listeners for selecting, dragging, etc.
    
    this.bookmark = bookmark;
}

JSClass.inherit(BookmarksDataGridNode, DataGridNode);

// @param columnIdentifier (string) The identifier associated with the column
//      from the GridSetups.
// @return (bool) If the columnIdentifier is specified, whether or not the
//      cell in the corresponding column is receiving user input. If the
//      columnIdentifier is undefined, returns whether or not any cell in the
//      node is currently receiving user input.
BookmarksDataGridNode.prototype.isEditing = function(columnIdentifier) {
    var cells = this.element.cells;
    for (var i = 0; i < cells.length; i++) {
        if (this.textNodeInCell(cells[i]).contentEditable === "true") {
            if (!columnIdentifier || (columnIdentifier && cells[i].columnIdentifier === columnIdentifier))
                return true;
        }
    }
    return false;
}

// Ends editing for all the cells and their content and saves any changes that
// have been made back to the bookmark.
BookmarksDataGridNode.prototype.endEditAndSave = function() {
    var cells = this.element.cells;
    for (var i = 0; i < cells.length; i++) {
        var editableElement = this.textNodeInCell(cells[i]);
        if (editableElement.contentEditable === "true") {
            editableElement.removeStyleClass("isBeingEdited");
            editableElement.contentEditable = "false";
            
            // Workaround for https://bugs.webkit.org/show_bug.cgi?id=26690
            cells[i].removeChild(editableElement);
            cells[i].appendChild(editableElement);
        }
    }
    this.saveChanges();
}

// Saves the changes made by the user during an edit back to the bookmark
// associated with this node.
BookmarksDataGridNode.prototype.saveChanges = function() {
    var cells = this.element.cells;
    for (var i = 0; i < cells.length; i++) {
        var cell = cells[i];
        if (typeof this.bookmark[cell.columnIdentifier] !== "undefined")
            this.bookmark[cell.columnIdentifier] = this.textNodeInCell(cell).textContent;
    }       
}

BookmarksDataGridNode.prototype.textNodeInCell = function(cell)
{
    return cell.getElementsByTagName("div")[0];
}

// Sets the content of the cell in the column specified by the given columnIdentifier
// to be editable (able to receive user input).
//
// @param columnIdentifier (string) The identifier associated with the column
//      from the GridSetups.
BookmarksDataGridNode.prototype.setEditing = function(columnIdentifier) {
    var cells = this.element.cells;
    var elementToEdit = null;
    for (var i = 0; i < cells.length; i++) {
        if (cells[i].columnIdentifier === columnIdentifier) {
            elementToEdit = this.textNodeInCell(cells[i]);
            // If the element is already editable, return early.
            if (elementToEdit.contentEditable === "true")
                return;
        }
    }
    if (!elementToEdit)
        return;
  
    elementToEdit.addStyleClass("isBeingEdited");
    elementToEdit.contentEditable = "true";
    elementToEdit.focus();
}

// Sets the focus to be on the content of the cell being editing currently
// if such a cell exists.
BookmarksDataGridNode.prototype.focusOnEditableElement = function() {
    var cells = this.element.cells;
    for (var i = 0; i < cells.length; i++) {
        var elementToEdit = this.textNodeInCell(cells[i]);
        if (elementToEdit.contentEditable === "true") {
            elementToEdit.focus();
            return;
        }
    }
}

BookmarksDataGrid = function(columns, bookmark, controller)
{
    BookmarksDataGrid.baseConstructor.call(this, columns);
    // FIXME: Implement in this wrapper some of the custom behavior for the
    // bookmarks view, including mass selection, deletion, and dragging.
    this.bookmark = bookmark;
    this._controller = controller;
    
    this.element.removeEventListener("keydown", this._keyDownFunction, false);
    // FIXME: Rename the addEventListener, removeEventListener associated with
    // the Listeners class so that they are not easily confused with the DOM ones.
    this.element.addEventListener("keydown", this._controller.handleKeyEvent.bind(this._controller), this._controller);
    
    // Override the default values in the DataGrid for how deep to indent in
    // the tree.
    this.startingIndent = 10;
    this.indentWidth = 20;
}

JSClass.inherit(BookmarksDataGrid, DataGrid);

BookmarksDataGrid.prototype.addChildNode = function(childNode, parent)
{
    parent.appendChild(childNode);
    childNode.addEventListener("populate", this._controller.populate.bind(this._controller, childNode), this._controller);
    childNode.addEventListener("collapsed", this._collapsed.bind(this, childNode), this);
    childNode.addEventListener("expanded", this._controller.folderExpanded.bind(this._controller, childNode), this._controller);
    childNode.addEventListener("selected", this._controller.selected.bind(this._controller, childNode), this._controller);
    childNode.addEventListener("deselected", this._controller.deselected.bind(this._controller, childNode), this._controller);
    childNode.addEventListener("dblclicked", this._controller.dblClicked.bind(this._controller, childNode), this._controller);
    this._controller.registerNode(childNode);
    // FIXME: Add in ability to show favicons. This change will be needed to be made
    // either here or in the DataGridNode.
}

BookmarksDataGrid.prototype.removeChildNode = function(childNode)
{
    var parent = childNode.parent;
    this._controller.unregisterNode(childNode);
    parent.removeChild(childNode);
}

BookmarksDataGrid.prototype._collapsed = function(node)
{
    // For now, on collapse remove all the children from the DOM. In the
    // future, if this proves to be too slow, consider just hiding all the children.
    // Removing all the children this way hopefully will mean using less memory.
    while (node.children.length > 0) {
        var childNode = node.children[0];
        node.removeChild(childNode);
        this._controller.unregisterNode(childNode);
    }
    
    // Because the DataGridNode.removeChild function does not consider the
    // possibility that the children are being removed to hide them but not actually
    // remove them, it is necessary to tell the node that it still does have children
    // so that it still considers itself as able to expand. Instead of duplicating
    // all the code of removeChild with just the next two lines added, it makes more
    // sense to just set these values to true.
    node.hasChildren = true;
    node._shouldRefreshChildren = true;
    this._controller.folderCollapsed(node);
}

// Controls access to the data grid in the list view, managing the handling of
// notifications of changes to bookmarks within.
// 
// @param dataGridContainer (HTML element) The element that will contain the
//      DataGrid.
// @param flowViewController (FlowViewController) The controller of what is
//      displayed in the flow view.
// @param activateBookmarkFunction (function) The function to be called to
//      activate a Bookmark.
BookmarksDataGridController = function(dataGridContainer, flowViewController, activateBookmarkFunction)
{
    this.dataGridContainer = dataGridContainer;
    this.flowViewController = flowViewController;
    this._activateBookmark = activateBookmarkFunction;
    this.dataGrid = null;
    this.bookmark = null;
    
    // Keep track of what callback notifications are expected from the C++ confirming that
    // the changes made via the JS + HTML have taken.
    this._awaitingAddNewNotification = false;
}

BookmarksDataGridController.prototype.registerNode = function(node)
{
    this._idToNodeMap[node.bookmark.UUID] = node;
    if (!this._urlToNodesMap[node.bookmark.URLString])
        this._urlToNodesMap[node.bookmark.URLString] = [];
    this._urlToNodesMap[node.bookmark.URLString].push(node);
}

BookmarksDataGridController.prototype.unregisterNode = function(node)
{
    delete this._idToNodeMap[node.bookmark.UUID];
    var nodesMap = this._urlToNodesMap[node.bookmark.URLString];
    console.assert(nodesMap, "URL '%s' of node passed to unregisterNode not present in this._urlToNodesMap in the Data Grid", node.bookmark.URLString);
    nodesMap.remove(node);
    if (!nodesMap.length)
        delete this._urlToNodesMap[node.bookmark.URLString];
}

// @return (BookmarksDataGridNode) An unattached node corresponding to the
//      given bookmark.
BookmarksDataGridController.prototype.createDataGrid = function(bookmark)
{
    /* The HTML of the list view looks like:
    <div id="listViewContainer" class="container">
        <div class="data-grid"> < - this is DataGrid.element
            <table class="header"> ... </table>
            <div class="data-container"> ... </div>
        </div>
    </div>
    */

    // Do not re-create the data grid unneccessarily.
    if (this.currentBookmark == bookmark)
        return;
    
    this.bookmark = bookmark;
    
    // FIXME: Correctly determine the type of bookmark once that is possible.
    
    // Based on the type of bookmark, create the correct DataGrid type.
    var type = this.bookmark.type;
    if (type == BookmarkType.Folder)
        this.dataGrid = new BookmarksDataGrid(GridSetups.Standard, this.bookmark, this);
    else if (type == BookmarkType.Leaf)
        this.dataGrid = new BookmarksDataGrid(GridSetups.Standard, this.bookmark, this);
    else
        this.dataGrid = new BookmarksDataGrid(GridSetups.BookmarksBar, this.bookmark, this);
    
    if (this.dataGridContainer.children[0])
        this.dataGridContainer.removeChild(this.dataGridContainer.children[0]);
    this.dataGridContainer.appendChild(this.dataGrid.element);
    this.dataGrid.updateWidths();
    
    // Reset the mapping between ids and nodes.
    this._idToNodeMap = {};
    this._idToNodeMap[this.bookmark.UUID] = this.dataGrid;
    
    // Keeps track of the URLs so that when an icon is updated for a URL, all
    // the corresponding bookmarks can have their icons updated.
    this._urlToNodesMap = {};
        
    this.populateTopLevel();
    
    if (this.dataGrid.children[0])
        this.dataGrid.children[0].select();
}

BookmarksDataGridController.prototype.populateTopLevel = function()
{
    this.dataGrid.removeChildren();
    
    if (this.bookmark.type == BookmarkType.Leaf)
        this.dataGrid.addChildNode(this._createBookmarkDataGridNode(this.bookmark), this.dataGrid);
    else {
        var children = this.bookmark.children;
        for (var i = 0; i < children.length; i++)
            this.dataGrid.addChildNode(this._createBookmarkDataGridNode(children[i]), this.dataGrid);
    }
    this.dataGrid.expand();   
}

BookmarksDataGridController.prototype.populate = function(node)
{
    node.removeChildren();

    if (node.bookmark.type == BookmarkType.Leaf)
        return;
    
    var children = node.bookmark.children;
    for (var i = 0; i < children.length; i++)
        this.dataGrid.addChildNode(this._createBookmarkDataGridNode(children[i]), node);
}

BookmarksDataGridController.prototype._createBookmarkDataGridNode = function(bookmark)
{
    var nodeType = bookmark.type == BookmarkType.Leaf ? NodeType.Leaf : NodeType.Container;
    return new BookmarksDataGridNode(this._createDataContent(bookmark), nodeType, { title: bookmark.iconURL }, bookmark);
}

// @return (Object) The data object that can be used by a DataGridNode to fill
//      in the content of the HTML element of the node.
BookmarksDataGridController.prototype._createDataContent = function(bookmark)
{
    var data = {};
    data.title = bookmark.title;
    
    // FIXME: Once it is possible to correctly determine the type of bookmark
    // (e.g. History Item, RSS Feed, Normal Folder, Normal Bookmark), use the
    // corresponding column setup.
    if (bookmark.type == BookmarkType.Folder) {
        // FIXME: Localization needed for "item" and "items" and the placement
        // of the number.
        data.URLString = bookmark.children.length == 1 ? "1 item" : bookmark.children.length + " items";
    } else if (bookmark.type == BookmarkType.Leaf)
        data.URLString = bookmark.URLString;
    else {
        // FIXME: For now, just use the URLString and "address".
        data.URLString = bookmark.URLString;
    }
    
    return data;
}

// @param index (integer) The index into the bookmarks in the currently
//      selected folder at which to insert the new folder. If undefined,
//      then the new folder is appended to the end of the children in the
//      currently selected folder.
BookmarksDataGridController.prototype.addNewFolder = function(index)
{
    // Make sure adding folders is allowed for this type of DataGrid.
    if (this.bookmark.type != BookmarkType.Folder)
        return;
        
    // FIXME: Once it is possible to determine type information (such as
    // whether or not this is a History bookmark, decide based on that
    // whether it is possible to add a folder.   

    // Find a container that this new folder can be added as a child of.
    var selectedContainer =  this.dataGrid.selectedNode;
    if (!selectedContainer) {
        // There is never a time where a populated DataGrid should not have
        // a selected node, so the DataGrid must not have any nodes in it.
        selectedContainer = this.dataGrid;
    } else if (selectedContainer.bookmark.type != BookmarkType.Folder)
            selectedContainer = selectedContainer.parent;
   
    var parentBookmark = selectedContainer.bookmark;
    
     // Add a new bookmark, but wait for the callback notification from the
    // native code before actually adding it to the DataGrid.
    this._awaitingAddNewNotification = true;
    
    var indexToInsert = typeof index == "undefined" ? parentBookmark.children.length : index;
    
    // FIXME: Localize the "untitled folder" string.
    parentBookmark.insertNewChildAtIndex("untitled folder", BookmarkType.Folder, indexToInsert);
}

BookmarksDataGridController.prototype.removeBookmark = function(bookmark)
{
    var node = this._idToNodeMap[bookmark.UUID];
    if (!node)
        return;
    
    // FIXME: Based on the type of the Bookmark, make sure it is allowed to be
    // removed (e.g. Bonjour Bookmarks shouldn't be allowed to be removed).
        
    // Remove the bookmark, but wait for the callback notification from the
    // native code before actually removing it from the DataGrid.
    node.bookmark.parent.removeChild(node.bookmark);
}

BookmarksDataGridController.prototype.selectBookmark = function(bookmark)
{
    var node = this._idToNodeMap[bookmark.UUID];
    if (!node)
        return;
    
    node.select();
}

BookmarksDataGridController.prototype.handleKeyEvent = function(event)
{
    // FIXME: Add in logic to check if the currently selected node is being
    // edited, in which case arrow keys, enter, and delete will have different
    // behavior.
    var selectedNode = this.dataGrid.selectedNode;
    if (selectedNode && selectedNode.isEditing()) {
        // The user is editing, so don't respond to any key events besides "Enter"
        // with custom handling.
        if (event.keyIdentifier === "Enter") {
            selectedNode.endEditAndSave();
            this.dataGrid.element.focus();
        }
        return;
    }
        
    if (selectedNode && (event.keyIdentifier === "U+0008" && !event.altKey)) {
        // Delete.
       
        // FIXME: If there are multiple bookmarks selected, all of them will
        // need to be selected and deleted. The selection logic in DataGrid or
        // BookmarksDataGrid will need to be modified to take multiple bookmark
        // selection into account.
        
        var bookmarkToRemove = selectedNode.bookmark;
        
        var nodeToSelect = selectedNode.nextSibling ? selectedNode.nextSibling : selectedNode.previousSibling;
        if (nodeToSelect) {
            nodeToSelect.reveal();
            nodeToSelect.select();
        }
        
        this.removeBookmark(bookmarkToRemove);
        return;
    }

    // Let the DataGrid handle the rest of the key events.
    this.dataGrid.handleKeyEvent(event);   
}

BookmarksDataGridController.prototype.handleBookmarkAddedNotification = function(bookmark)
{
    var parent = this._idToNodeMap[bookmark.parent.UUID];
    if (!parent)
        return;
        
    // Update the item count of the parent.
    if (parent != this.dataGrid) {
        parent.data = this._createDataContent(parent.bookmark);
        parent.iconURLs = { title: parent.bookmark.iconURL };
    }
    
    var childNode = null;
    if (!parent.expanded && this._awaitingAddNewNotification) {
        // Expanding the parent will cause the child to be created during
        // population.
        parent.expand();
        childNode = this._idToNodeMap[bookmark.UUID];
    } else {
        childNode = this._createBookmarkDataGridNode(bookmark);
        this.dataGrid.addChildNode(childNode, parent);
    }
    
    if (this._awaitingAddNewNotification) {
        // Because this was newly created, its title will need to be made editable
        // once it is actually added on the callback notification from the native
        // code.
        this._awaitingAddNewNotification = false;
        childNode.select()
        childNode.setEditing("title");
    }  
}

BookmarksDataGridController.prototype.handleBookmarkChangedNotification = function(bookmark)
{
    var node = this._idToNodeMap[bookmark.UUID];
    if (!node)
        return;
        
    node.data = this._createDataContent(bookmark);
    node.iconURLs = { title: bookmark.iconURL };
}

BookmarksDataGridController.prototype.handleBookmarkRemovedNotification = function(bookmark)
{
    // Might be a notification for a bookmark removed in another instance of bookmarksview.
    var node = this._idToNodeMap[bookmark.UUID];
   
    // Make sure the bookmark currently exists in the DataGrid.
    if (!node)
        return;
    
    this.dataGrid.removeChildNode(node);
}

BookmarksDataGridController.prototype.handleBookmarkIconUpdatedNotification = function(url)
{
    var nodes = this._urlToNodesMap[url];
    if (!nodes) {
        // There is no Bookmark in the Data Grid with that url.
        return;
    }
    for (var i = 0; i < nodes.length; i++)
        nodes[i].iconURLs = { title: nodes[i].bookmark.iconURL };
}

BookmarksDataGridController.prototype.selected = function(node)
{
    if (node.isEditing())
        node.focusOnEditableElement();
    this.flowViewController.showSelected(node.bookmark);
}

BookmarksDataGridController.prototype.deselected = function(node)
{
    // End any edits on the node.
    if (node.isEditing())
        node.endEditAndSave();
}

BookmarksDataGridController.prototype.dblClicked = function(node)
{
    var bookmark = node.bookmark;
    if (!bookmark)
        return;
        
    if (bookmark.type != BookmarkType.Folder) {
        this._activateBookmark(bookmark);
        return;
    }
    
    if (node.expanded)
        node.collapse();
    else
        node.expand();
}

BookmarksDataGridController.prototype.folderExpanded = function(node)
{
    this.flowViewController.showFolderContents(node.bookmark);
}

BookmarksDataGridController.prototype.folderCollapsed = function(node)
{
    this.flowViewController.hideFolderContents(node.bookmark);
}

BookmarksDataGridController.prototype.focus = function()
{
    // Return the focus to the node that should be selected.
    if (this.dataGrid.nodeSelected)
        this.dataGrid.nodeSelected.element.focus();
}

// @return (integer) The index of the row in the DataGrid displaying the
//      bookmark. -1 if the bookmark does not have a corresponding row in the
//      DataGrid.
BookmarksDataGridController.prototype.rowIndexForBookmark = function(bookmark)
{
    var node = this._idToNodeMap[bookmark.UUID];
    if (!node)
        return -1;
    return node.element.rowIndex;   
}

// @return (integer) The bookmark associated with row with the given index in
//      the DataGrid. null if there is no row at the given index.
BookmarksDataGridController.prototype.bookmarkForRowIndex = function(rowIndex)
{
    var row = this.dataGrid.dataTableBody.rows[rowIndex];
    if (!row)
        return null;
    console.assert(row._dataGridNode.bookmark, "Data grid row %i has no bookmark", rowIndex);
    return row._dataGridNode.bookmark;
}

// @return (integer) The index at which it would be possible to find this
//      bookmark already existing in the tree if all the visible Bookmarks,
//      not including Folders, were taken and put into a list based on their
//      row indices.
BookmarksDataGridController.prototype.flowViewIndexForRowIndex = function(rowIndex)
{
    var count = 0;
    var rows = this.dataGrid.dataTableBody.rows;
    for (var i = 0; i < rowIndex; i++) {
        if (rows[i]._dataGridNode.bookmark.type != BookmarkType.Folder)
            count++;
    }
    return count;
}

BookmarksDataGridController.prototype.rowIndexForFlowViewIndex = function(flowViewIndex)
{
    var count = 0;
    var rows = this.dataGrid.dataTableBody.rows;
    for (var i = 0; i < rows.length; i++) {
        if (count > flowViewIndex)
            return -1;
        if (rows[i]._dataGridNode.bookmark.type == BookmarkType.Folder) {
            // The only Bookmarks that could be showing up in the Flow View are
            // the ones that are not Folders.
            continue;
        }
        if (count == flowViewIndex)
            return i;
        count++;
    }
    return -1;
}

BookmarksDataGridController.prototype.currentlySelectedBookmark = function()
{
    var selectedNode = this.dataGrid.selectedNode;
    if (!selectedNode)
        return null;
    return selectedNode.bookmark;
}
