// 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,
}

// SidebarNode ----------------------------------------------------------------

// Represents an item in the Sidebar outline.
//
// @param draggable (SidebarNode.Draggable enum) Whether or not this node can
//      be dragged.
// @param canReceiveDrop (Sidebarnode.CanReceiveDrop enum) Whether or not this
//      node can receive a drop onto it.
SidebarNode = function(bookmark, draggable, canReceiveDrop)
{
    this.bookmark = bookmark;
    this.id = bookmark.UUID;
    this.draggable = draggable;
    this.canReceiveDrop = canReceiveDrop;
    this.parentOutline = null;
    this.htmlElement = null;
    this._selected = false;
}

SidebarNode.Draggable = {
    False: 0,
    True: 1,
}

SidebarNode.CanReceiveDrop = {
    False: 0,
    True: 1,
}

SidebarNode.prototype = {

    get editable()
    {
        return this.titleNode.contentEditable === "true";
    },
    
    set editable(value)
    {
        var editableElement = this.titleNode;
        var currentEditStatus = editableElement.contentEditable === "true";
        if (currentEditStatus == value)
            return;
        if (value) {
            editableElement.addStyleClass("editing");
            editableElement.contentEditable = "true";
            // Set the focus on this element so the user can tell they can edit it.
            editableElement.focus();
        } else {
            editableElement.removeStyleClass("editing");
            editableElement.contentEditable = "false";
            
            // Workaround for https://bugs.webkit.org/show_bug.cgi?id=26690
            this.htmlElement.removeChild(editableElement);
            this.htmlElement.appendChild(editableElement);    
        }   
    },
    
    get selected()
    {
        return this._selected;
    },
    
    set selected(value)
    {
        if (this._selected == value)
            return;
        this._selected = value;
        if (this._selected)
            this.htmlElement.addStyleClass("selected");
        else
            this.htmlElement.removeStyleClass("selected");
    },
    
    get text()
    {
        if (!this.titleNode)
            return "";
        return this.titleNode.textContent;
    }
}

// Creates an HTML element for this node and inserts it into the HTML children
// of the parent.
//
// @param indexFromParent (integer) If undefined, will append to the end of the
//      HTML children of the parent.
SidebarNode.prototype.createHTMLElement = function(indexFromParent)
{
    var parentHTMLElement = this.parentOutline.htmlElement;
    // Remove the old htmlElement if it exists.
    if (this.htmlElement)
        parentHTMLElement.removeChild(this.htmlElement);
    
    this.htmlElement = document.createElement("li");
    
    this.fillContent();
    
    this.htmlElement.addStyleClass("sidebarItem");
    this.htmlElement.sidebarNode = this;
    
    // FIXME: Add event listeners (and corresponding handlers) for selected,
    // drag, drop, etc.
    this.htmlElement.addEventListener("click", this.select.bind(this), false);
    this.htmlElement.addEventListener("dblclick", this.dblClick.bind(this), false);
    this.htmlElement.addEventListener("dragstart", this.dragStart.bind(this), false);
    this.htmlElement.addEventListener("dragenter", this.dragEnter.bind(this), false);
    this.htmlElement.addEventListener("dragover", this.dragOver.bind(this), false);
    this.htmlElement.addEventListener("dragleave", this.dragLeave.bind(this), false);
    this.htmlElement.addEventListener("drop", this.drop.bind(this), false);

    var siblings = parentHTMLElement.children;
    if (typeof indexFromParent === "undefined" || !siblings.length || siblings.length <= indexFromParent)
        parentHTMLElement.appendChild(this.htmlElement);
    else
        parentHTMLElement.insertBefore(this.htmlElement, siblings[index]);
}

SidebarNode.prototype.fillContent = function()
{
    this.htmlElement.removeChildren();

    var favicon = document.createElement("img");
    favicon.addStyleClass("icon");
    favicon.src = this.bookmark.iconURL;
    this.htmlElement.appendChild(favicon);
    
    var nodeText = document.createElement("div");
    nodeText.addStyleClass("sidebarItemContent");
    nodeText.textContent = this.bookmark.title;
    this.htmlElement.appendChild(nodeText);
    this.titleNode = nodeText;
}

SidebarNode.getSidebarNode = function(obj)
{
    while (obj && !obj.sidebarNode)
        obj = obj.parentNode;
    return obj ? obj.sidebarNode : null;
}

SidebarNode.prototype.select = function(event)
{
    // Bubble this up for the outline to deal with.
    this.parentOutline.nodeSelected(this, event);
}

SidebarNode.prototype.dblClick = function(event)
{
    // Bubble this up for the outline to deal with.
    this.parentOutline.nodeDblClicked(this, event);
}

SidebarNode.prototype.dragStart = function(event)
{     
    this.select();  
    if (!this.draggable) {
        event.preventDefault();
        return;
    }
    var x = event.clientX - totalLeftOffset(this.htmlElement);
    var y = event.clientY - totalTopOffset(this.htmlElement);
    event.dataTransfer.setDragImage(this.htmlElement, x, y);
    event.dataTransfer.setData("Text", this.bookmark.UUID);
    
    return true; 
}

SidebarNode.prototype.dragEnter = function(event)
{
    if (this.canReceiveDrop && this.parentOutline.checkIfCanBeDropped(event))
        this.htmlElement.addStyleClass("dragOver");
    
    event.preventDefault();
}

SidebarNode.prototype.dragOver = function(event)
{
    if (this.canReceiveDrop && this.parentOutline.checkIfCanBeDropped(event) && !this.htmlElement.hasStyleClass("dragOver"))
        this.htmlElement.addStyleClass("dragOver");

    event.preventDefault();
}

SidebarNode.prototype.dragLeave = function(event)
{
    if (this.canReceiveDrop && this.parentOutline.checkIfCanBeDropped(event))
        this.htmlElement.removeStyleClass("dragOver");
    
    event.preventDefault();
}

SidebarNode.prototype.drop = function(event)
{   
    // FIXME: There will need to be some way to drop a bookmark in between
    // nodes. With some coordination with dragEnter, dragLeave and dragEnd
    // (which is called after the drop method), this should be possible.

    if (this.canReceiveDrop && this.parentOutline.checkIfCanBeDropped(event)) {
        this.htmlElement.removeStyleClass("dragOver");
        this.parentOutline.itemDropped(this, event);
    }
    
    event.preventDefault();
}

// SidebarOutline -------------------------------------------------------------

// Represents a flat outline of the Bookmarks in a section in the Sidebar.
//
// @param rootBookmark (JSBookmark) The root bookmark folder whose children
//      will be in this SidebarOutline.
// @param element (HTMLUListElement) The HTML element which is the root of the
//      represented section.
// @param element (SidebarController) The controller to handle user events on
//      the outline and to control adding, removing, and changing the sidebar.
SidebarOutline = function(rootBookmark, element, controller)
{
    SidebarOutline.baseConstructor.call(this, rootBookmark);
    this.htmlElement = element;
    this.controller = controller;
}

JSClass.inherit(SidebarOutline, SidebarNode);

SidebarOutline.prototype = {

    get visible()
    {
        return this.htmlElement.style.visibility === "visible";
    },
    
    set visible(value)
    {
        this.htmlElement.style.visibility = value ? "visible" : "hidden";
    },
    
    get numChildren()
    {
        return this.htmlElement.children.length;
    }   
}

SidebarOutline.prototype.insertChildNode = function(node, index)
{
    node.parentOutline = this;
    node.createHTMLElement(index);
    this.controller.registerNode(node);
}

SidebarOutline.prototype.removeChildNode = function(node)
{
    if (node.parentOutline != this)
        return;

    this.htmlElement.removeChild(node.htmlElement);
    node.parentOutline = null;
    this.controller.unregisterNode(node);
}

SidebarOutline.prototype.lastChildNode = function()
{
    var children = this.htmlElement.children;
    if (!children.length)
        return null;
    return SidebarNode.getSidebarNode(children[children.length - 1]);
}

SidebarOutline.prototype.firstChildNode = function()
{
    var children = this.htmlElement.children;
    if (!children.length)
        return null;
    return SidebarNode.getSidebarNode(children[0]);
}

// @return (SidebarNode) The node above the given node in the outline. Null if
// such a node does not exist or if the node is not a child of this outline.
SidebarOutline.prototype.nodeSiblingAbove = function(node)
{
    if (node.parentOutline != this)
        return null;
        
    if (!node.htmlElement.previousElementSibling)
        return null;
    return SidebarNode.getSidebarNode(node.htmlElement.previousElementSibling);
}

// @return (SidebarNode) The node below the given node in the outline. Null if
// such a node does not exist or if the node is not a child of this outline.
SidebarOutline.prototype.nodeSiblingBelow = function(node)
{
    if (node.parentOutline != this)
        return null;

    if (!node.htmlElement.nextElementSibling)
        return null;
    return SidebarNode.getSidebarNode(node.htmlElement.nextElementSibling);
}

SidebarOutline.prototype.nodeSelected = function(node, event)
{
    // Bubble this up for the controller to deal with.
    this.controller.nodeSelected(node, event);
}

SidebarOutline.prototype.nodeDblClicked = function(node, event)
{
    // Bubble this up for the controller to deal with.
    this.controller.nodeDblClicked(node, event);
}

// Returns true only if the item to be dropped that is associated with the drop
// event can be dropped onto a node in the sidebar.
//
// @param event (HTML drop event) The event associated with the drop.
SidebarOutline.prototype.checkIfCanBeDropped = function(event)
{
    return this.controller.checkIfCanBeDropped(event);
}

// @param ontoNode (SidebarNode) The node onto which the item was dropped.
// @param event (HTML drop event) The event associated with the drop.
SidebarOutline.prototype.itemDropped = function(ontoNode, event)
{
    this.controller.itemDropped(ontoNode, event);
}

// SidebarController ----------------------------------------------------------

// Manages the outlines in the sidebar, controlling the addition, removal, and
// changing of bookmarks, along with user events on the bookmarks.
//
// @param rootBookmark (JSBookmark) The top-most bookmark.
// @param listViewController (BookmarksDataGridController) The controller of
//      what is displayed in the list view.
// @param flowViewController (FlowViewController) The controller of what is
//      displayed in the flow view.
// @param setPageTitle (function) A function that can be used to set the title of
//      the page.
SidebarController = function(rootBookmark, listViewController, flowViewController, setPageTitle)
{
    this.rootBookmark = rootBookmark;
    this.listViewController = listViewController;
    this.flowViewController = flowViewController;
    this._setPageTitle = setPageTitle;
    
    this.sidebarElement = document.getElementById("sidebar");
    this.sidebarElement.addEventListener("keydown", this.handleKeyEvent.bind(this), true);
    this.collectionsSection = document.getElementById("collectionsSidebarSection");
    this.bookmarksSection = document.getElementById("bookmarksSidebarSection");
    
    this.collectionsOutline = new SidebarOutline(rootBookmark, this.collectionsSection, this);
    this.bookmarksOutline = new SidebarOutline(rootBookmark, this.bookmarksSection, this);
    
    this._idToNodeMap = {};
    // 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 = {};
    
    // 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;
    this._awaitingRemoveNotification = new Object();
    this._awaitingDidChangeNotification = new Object();
    
    this.populateOutlines();
    
    this.currentlySelectedNode = null;
    // Default to selecting the first of the rootBookmark's
    // children.
    this._idToNodeMap[this.rootBookmark.children[0].UUID].select();
}

SidebarController.prototype.populateOutlines = function()
{
    // FIXME: Clear the children of the outlines first.
    var rootBookmarkChildren = this.rootBookmark.children;
    for (var i = 0; i < rootBookmarkChildren.length; i++)
    {
        var childBookmark = rootBookmarkChildren[i];
        // FIXME: Once we know if it could be a History, Bonjour, etc collection,
        // adjust whether it is draggable and can receive drops.
        var canReceiveDrop = SidebarNode.CanReceiveDrop.True;
        if (childBookmark.type == BookmarkType.Leaf)
            canReceiveDrop = SidebarNode.CanReceiveDrop.False;
            
        var sidebarNode = new SidebarNode(childBookmark, SidebarNode.Draggable.True, canReceiveDrop);
        // FIXME: Find a better way to determine what children belong in which
        // section.
        if (childBookmark.type === BookmarkType.Proxy || childBookmark.isBookmarksBar || childBookmark.isBookmarksMenu)
            this.collectionsOutline.insertChildNode(sidebarNode);
        else
            this.bookmarksOutline.insertChildNode(sidebarNode);
    }
    
    // If there are no bookmarks in the bookmarks section (the one child is the
    // text description), make the entire section invisible.
    if (!this.bookmarksOutline.numChildren)
        this.bookmarksOutline.visible = false;
    else
        this.bookmarksOutline.visible = true;
}

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

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

SidebarController.prototype.focus = function()
{
    this.currentlySelectedNode.htmlElement.focus();
}

// Method for adding an entirely new folder, primarily in the case where the
// '+' button in the lower left of the page is used to add a new folder to the
// sidebar.
//
// @param index (integer) The index into the bookmarks in the sidebar at which
//      to insert the new folder. If undefined, then the new folder is appended
//      to the end of the children in the section it is destined to be in. 
SidebarController.prototype.addNewFolder = function(index)
{
    // Add a new bookmark, but wait for the callback notification from the
    // native code before actually adding it to the outline.
    this._awaitingAddNewNotification = true;
            
    // FIXME: The index to insert might need to depend on the section the bookmark is in.
    // Once it is possible to determine the correct section, fix this.
    var indexToInsert = typeof index === "undefined" ? this.rootBookmark.children.length : index;
    
    // FIXME: Localize the "untitled folder" string.
    this.rootBookmark.insertNewChildAtIndex("untitled folder", BookmarkType.Folder, indexToInsert);
}

// Method for adding an existing bookmark. Useful in the case where a bookmark
// that was not in the sidebar is dragged into the sidebar (e.g. from the list view) or in
// the case where a drag and drop has changed the index of a bookmark.
//
// @param index (integer) The index into the bookmarks in the sidebar at which
//      to insert the bookmark. If undefined, then the bookmark is appended to
//      the end of the children in the section it is destined to be in. 
SidebarController.prototype.addExistingBookmark = function(bookmark, index)
{
    if (!bookmark)
        return;
        
    // FIXME: The index to insert might need to depend on the section the bookmark is in.
    // Once it is possible to determine the correct section, fix this.
    var indexToInsert = typeof index === "undefined" ? this.rootBookmark.children.length : index;
    this.rootBookmark.insertExistingChildAtIndex(bookmark, index);
}

SidebarController.prototype.handleBookmarkAddedNotification = function(bookmark)
{
    if (!bookmark || bookmark.parent != this.rootBookmark)
        return;
    
    // FIXME: Once we know if it could be a History, Bonjour, etc collection,
    // adjust whether it is draggable and can receive drops.
    var sidebarNode = new SidebarNode(bookmark, SidebarNode.Draggable.True, SidebarNode.CanReceiveDrop.False);

    // FIXME: Make sure to add the bookmark to the correct list.
    this.bookmarksOutline.insertChildNode(sidebarNode);
    // The bookmarksOutline has at least one child now, so make it visible.    
    this.bookmarksOutline.visible = true;

    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;
        sidebarNode.editable = true;
        sidebarNode.select();
    }
}

SidebarController.prototype.removeBookmark = function(bookmark)
{   
    var node = this._idToNodeMap[bookmark.UUID];
    
    if (!node) {
        // The bookmark does not exist in the sidebar.
        return;
    }
   
    // If the bookmark was in the Collections outline, don't remove it
    if (node.parentOutline == this.collectionsOutline)
        return;
        
    // Remove the bookmark, but wait for the callback notification from the
    // native code before actually removing it from the outline.
    this._awaitingRemoveNotification[bookmark] = node;
    this.rootBookmark.removeChild(node.bookmark);
}

SidebarController.prototype.handleBookmarkRemovedNotification = function(bookmark)
{
    // Might be a notification for a bookmark removed in another instance of bookmarksview.
    var node = this._awaitingRemoveNotification[bookmark] ? this._awaitingRemoveNotification[bookmark] : this._idToNodeMap[bookmark.UUID];
   
    // Make sure the bookmark currently exists in the sidebar.
    if (!node)
        return;
    
    // Move the selection away from the node before it is removed. This really
    // isn't an update so much as a pre-emptive move before it is no longer
    // known where the selection was (which will happen once the node is
    // actually deleted).
    if (!this.moveSelection("Down"))
        this.moveSelection("Up");
        
    node.parentOutline.removeChildNode(node);
        
    if (this._awaitingRemoveNotification[bookmark])
        delete this._awaitingRemoveNotification[bookmark];
    
    // If there are no bookmarks in the bookmarks outline, hide it.
    if (!this.bookmarksOutline.numChildren)
        this.bookmarksOutline.visible = false;
}

SidebarController.prototype.handleBookmarkIconUpdatedNotification = function(url)
{
    var nodes = this._urlToNodesMap[url];
    if (!nodes) {
        // There is no Bookmark in the Sidebar with that url.
        return;
    }
    for (var i = 0; i < nodes.length; i++)
        this.bookmarkChanged(nodes[i].bookmark);
}

SidebarController.prototype.hasBookmark = function(bookmark)
{
    return !(!this._idToNodeMap[bookmark.UUID]);
}

SidebarController.prototype.bookmarkSelected = function(bookmark)
{
    this.flowViewController.showSelected(bookmark);
    this.listViewController.createDataGrid(bookmark);
}

SidebarController.prototype.bookmarkChanged = function(bookmark)
{
    if (this._idToNodeMap[bookmark.UUID])
        this._idToNodeMap[bookmark.UUID].fillContent();
}

// @param node (SidebarNode) The node that was selected.
// @param event (optional JS event) The event that caused the selection to
//      occur.
SidebarController.prototype.nodeSelected = function(node, event)
{
    // FIXME: Grab additional information from the event if it exists in order
    // to determine where on the list item the event occurred so it is possible
    // to end editing when the user clicks to the left of the list item.
    
    var currentNode = this.currentlySelectedNode;
    if (currentNode && currentNode == node)
        return;
    if (currentNode) {
        // If the user was editing the title of the this bookmark, they are
        // now done. Register this bookmark to wait for the callback notification.
        if (currentNode.editable) {
            // FIXME: Don't allow the titles of the Bookmarks to ever be changed to an
            // empty or invalid string.
            currentNode.bookmark.title = this.currentlySelectedNode.text;
            currentNode.editable = false;
        }
        currentNode.selected = false;
    }
    this.currentlySelectedNode = node;
    this.currentlySelectedNode.selected = true;
    
    this.bookmarkSelected(this.currentlySelectedNode.bookmark);
    this._setPageTitle(this.currentlySelectedBookmark().title);
}

SidebarController.prototype.currentlySelectedBookmark = function()
{
    return this.currentlySelectedNode.bookmark;
}

SidebarController.prototype.nodeDblClicked = function(node, event)
{
    // Select the node and allow the user to edit the title.
    node.select();
    node.editable = true;
}

// Returns true only if the item to be dropped that is associated with the drop
// event can be dropped onto a node in the sidebar.
//
// @param event (HTML drop event) The event associated with the drop.
SidebarController.prototype.checkIfCanBeDropped = function(event)
{
    // FIXME: Eventually this will need to deal with items from the address bar,
    // search field, and bookmarks bar being dropped into the sidebar, which may
    // involve interaction with native code and therefore different types of events
    // or notifications.
    
    if (!event || !event.dataTransfer || !event.dataTransfer.getData('Text'))
        return false;
        
    // FIXME: Once items can be dragged from the list view, this will need to
    // be modified to accept those drops.
    
    return !(!this._idToNodeMap[event.dataTransfer.getData('Text')]);
}

// @param ontoNode (SidebarNode) The node onto which the item was dropped.
// @param event (HTML drop event) The event associated with the drop.
SidebarController.prototype.itemDropped = function(ontoNode, event)
{
    if (!this.checkIfCanBeDropped(event))
        return;
        
    // FIXME: Eventually this will need to deal with items from the address bar,
    // search field, and bookmarks bar being dropped into the sidebar, which may
    // involve interaction with native code and therefore different types of events
    // or notifications.
    
    // FIXME: Once items can be dragged from the list view, this will need to
    // be modified to accept those drops.
    
    var droppedNode = this._idToNodeMap[event.dataTransfer.getData('Text')];

    if (droppedNode == ontoNode)
        return;
        
    ontoNode.bookmark.insertExistingChildAtIndex(droppedNode.bookmark, ontoNode.bookmark.children.length);
    ontoNode.select();
}

// @param direction (string) The direction to move in (either "Up" or "Down").
//      If the direction is undefined, does nothing.
// @return (bool) Whether or not the selection was able to be moved in the given
//      direction.
SidebarController.prototype.moveSelection = function(direction)
{
    if (!direction)
        return false;
        
    var moved = false;
    if (direction === "Down") {
        var siblingBelow = this.currentlySelectedNode.parentOutline.nodeSiblingBelow(this.currentlySelectedNode);
        if (!siblingBelow && this.currentlySelectedNode.parentOutline === this.collectionsOutline) {
            // Move to the bookmarks outline, if it is visible.
            if (this.bookmarksOutline.visible) {
                var bookmarksFirstChild = this.bookmarksOutline.firstChildNode();
                if (bookmarksFirstChild) {
                    this.bookmarksOutline.firstChildNode().select();
                    moved = true;
                }
            }
        } else if (siblingBelow) {
            siblingBelow.select();
            moved = true;
        }
        // Otherwise, we are at the bottom of the bookmarks outline, so don't do anything.
    }

    if (direction === "Up") {
        var siblingAbove = this.currentlySelectedNode.parentOutline.nodeSiblingAbove(this.currentlySelectedNode);
        if (!siblingAbove && this.currentlySelectedNode.parentOutline === this.bookmarksOutline) {
            // Move to the collections outline.
            // FIXME: Eventually it will make no sense for the collections
            // outline to have no children. Right now, because there is no
            // good way to figure out what the type of the bookmark is, it
            // is probable that the collections outline is empty.
            var collectionsLastChild = this.collectionsOutline.lastChildNode();
            if (collectionsLastChild) {
                this.collectionsOutline.lastChildNode().select();
                moved = true;
            }
        } else if (siblingAbove) {
            siblingAbove.select();
            moved = true;
        }
        // Otherwise, we are at the top of the collections outline, so don't do anything.
    }
    
    return moved;
}

SidebarController.prototype.handleKeyEvent = function(event)
{
    if ((event.keyIdentifier === "Up" || event.keyIdentifier === "Down") && !event.altKey) {
        this.moveSelection(event.keyIdentifier);
    } else if (event.keyIdentifier === "U+0008" && !event.altKey) {
        // Delete.
        if (this.currentlySelectedNode.editable) {
            // The user just hit delete to remove a character, don't delete the
            // bookmark.
            return;
        }
        this.removeBookmark(this.currentlySelectedNode.bookmark);
    }
}
