// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package collide.client.treeview;

import org.waveprotocol.wave.client.common.util.SignalEvent;
import org.waveprotocol.wave.client.common.util.SignalEventImpl;

import collide.client.util.CssUtils;
import collide.client.util.Elements;

import com.google.collide.client.util.AnimationController;
import com.google.collide.client.util.dom.MouseGestureListener;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.mvp.CompositeView;
import com.google.collide.mvp.UiComponent;
import com.google.collide.shared.util.JsonCollections;
import com.google.gwt.core.client.Duration;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.user.client.Timer;

import elemental.dom.Element;
import elemental.events.Event;
import elemental.events.EventListener;
import elemental.events.MouseEvent;
import elemental.js.dom.JsElement;
import elemental.js.html.JsDragEvent;

/**
 * A tree widget that is capable of rendering any tree data structure whose node
 * data type is specified in the class parameterization.
 *
 * Users of this widget must specify an appropriate
 * {@link collide.client.treeview.NodeDataAdapter} and
 * {@link collide.client.treeview.NodeRenderer}.
 *
 *  The DOM structure for a tree is a recursive structure of the following form
 * (note that class names will be obfuscated at runtime, and are just specified
 * in human readable form for documentation purposes):
 *
 * <pre>
 *
 *  <ul class="treeRoot childrenContainer">
 *    <li class="treeNode">
 *      <div class="treeNodeBody">
 *       <div class="expandControl"></div>
 *       <span class="treeNodeContents"></span>
 *      </div>
 *      <ul class="childrenContainer">
 *       ...
 *       ...
 *      </ul>
 *    </li>
 *  </ul>
 *
 * </pre>
 */
public class Tree<D> extends UiComponent<Tree.View<D>> {

  /**
   * Static factory method for obtaining an instance of the Tree.
   */
  public static <NodeData> Tree<NodeData> create(View<NodeData> view,
      NodeDataAdapter<NodeData> dataAdapter, NodeRenderer<NodeData> nodeRenderer,
      Resources resources) {
    Model<NodeData> model = new Model<NodeData>(dataAdapter, nodeRenderer, resources);
    return new Tree<NodeData>(view, model);
  }

  /**
   * BaseCss selectors applied to DOM elements in the tree.
   */
  public interface Css extends CssResource {
    String active();

    String childrenContainer();

    String closedIcon();

    String expandControl();

    String isDropTarget();

    String leafIcon();

    String openedIcon();

    String selected();

    String treeNode();

    String treeNodeBody();

    String treeNodeLabel();

    String treeRoot();
  }

  /**
   * Listener interface for being notified about tree events.
   */
  public interface Listener<D> {

    void onNodeAction(TreeNodeElement<D> node);

    void onNodeClosed(TreeNodeElement<D> node);

    void onNodeContextMenu(int mouseX, int mouseY, TreeNodeElement<D> node);

    void onNodeDragStart(TreeNodeElement<D> node, JsDragEvent event);

    void onNodeDragDrop(TreeNodeElement<D> node, JsDragEvent event);

    void onNodeExpanded(TreeNodeElement<D> node);

    void onRootContextMenu(int mouseX, int mouseY);

    void onRootDragDrop(JsDragEvent event);
  }

  /**
   * A visitor interface to visit nodes of the tree.
   */
  public interface Visitor<D> {

    /**
     * @return whether to visit a given node. This is useful to prune a subtree
     *         from being visited
     */
    boolean shouldVisit(D node);

    /**
     * Called for nodes that pass the {@link #shouldVisit} check.
     *
     * @param node the node being iterated
     * @param willVisitChildren true if the given node has a child that will be
     *        (or has been) visited
     */
    void visit(D node, boolean willVisitChildren);
  }

  /**
   * Instance state for the Tree.
   */
  public static class Model<D> {
    private final NodeDataAdapter<D> dataAdapter;
    private Listener<D> externalEventDelegate;
    private final NodeRenderer<D> nodeRenderer;
    private D root;
    private final SelectionModel<D> selectionModel;
    private final Resources resources;
    private final AnimationController animator;

    public Model(
        NodeDataAdapter<D> dataAdapter, NodeRenderer<D> nodeRenderer, Resources resources) {
      this.dataAdapter = dataAdapter;
      this.nodeRenderer = nodeRenderer;
      this.resources = resources;
      this.selectionModel = new SelectionModel<D>(dataAdapter, resources.treeCss());
      this.animator = new AnimationController.Builder().setCollapse(true).setFade(true).build();
    }

    public NodeDataAdapter<D> getDataAdapter() {
      return dataAdapter;
    }

    public NodeRenderer<D> getNodeRenderer() {
      return nodeRenderer;
    }

    public D getRoot() {
      return root;
    }

    public void setRoot(D root) {
      this.root = root;
    }
  }

  /**
   * Images and BaseCss resources used by the Tree.
   *
   * In order to theme the Tree, you extend this interface and override
   * {@link Resources#treeCss()}.
   */
  public interface Resources extends ClientBundle {

    @Source("closed-folder-icon.png")
    ImageResource closedFolderIcon();

    @Source("css-icon.png")
    ImageResource cssIcon();

    @Source("empty-folder-icon.png")
    ImageResource emptyFolderIcon();

    @Source("expansionIcon.png")
    ImageResource expansionIcon();

    @Source("html-icon.png")
    ImageResource htmlIcon();

    @Source("jar-icon.png")
    ImageResource jarIcon();

    @Source("java-icon.png")
    ImageResource javaIcon();

    @Source("javascript-icon.png")
    ImageResource javascriptIcon();

    @Source("open-folder-icon.png")
    ImageResource openFolderIcon();

    @Source("package-icon.png")
    ImageResource packageIcon();

    @Source("empty-package-icon.png")
    ImageResource emptyPackageIcon();

    @Source("xml-icon.png")
    ImageResource xmlIcon();


    // Default Stylesheet.
    @Source({"collide/client/common/constants.css",
        "Tree.css"})
    Css treeCss();
  }

  /**
   * The view for a Tree is simply a thin wrapper around a ULElement.
   */
  public static class View<D> extends CompositeView<ViewEvents<D>> {

    /**
     * Base event listener for DOM events fired by elements in the view.
     */
    private abstract class TreeNodeEventListener implements EventListener {

      private final boolean primaryMouseButtonOnly;

      /** The {@link Duration#currentTimeMillis()} of the most recent click, or 0 */
      private double previousClickMs;
      private Element previousClickTreeNodeBody;

      TreeNodeEventListener(boolean primaryMouseButtonOnly) {
        this.primaryMouseButtonOnly = primaryMouseButtonOnly;
      }

      @Override
      public void handleEvent(Event evt) {
        // Don't even bother to do anything unless we have someone ready to
        // handle events.
        if (getDelegate() == null || (primaryMouseButtonOnly
            && ((MouseEvent) evt).getButton() != MouseEvent.Button.PRIMARY)) {
          return;
        }

        Element eventTarget = (Element) evt.getTarget();

        if (CssUtils.containsClassName(eventTarget, css.expandControl())) {
          onExpansionControlEvent(evt, eventTarget);
        } else {
          Element treeNodeBody =
              CssUtils.getAncestorOrSelfWithClassName(eventTarget, css.treeNodeBody());
          if (treeNodeBody != null) {

            if (Event.CLICK.equals(evt.getType())) {
              double currentClickMs = Duration.currentTimeMillis();
              if (currentClickMs - previousClickMs < MouseGestureListener.MAX_CLICK_TIMEOUT_MS
                  && treeNodeBody.equals(previousClickTreeNodeBody)) {
                // Swallow double, triple, etc. clicks on an item's label
                return;
              } else {
                this.previousClickMs = currentClickMs;
                this.previousClickTreeNodeBody = treeNodeBody;
              }
            }

            onTreeNodeBodyChildEvent(evt, treeNodeBody);
          } else {
            onOtherEvent(evt);
          }
        }
      }

      /**
       * Catch-all that is called if the target element was not matched to an
       * element rooted at the TreeNodeBody.
       */
      protected void onOtherEvent(Event evt) {
      }

      /**
       * If an event was dispatched by the TreeNodeBody, or one of its children.
       *
       *  IMPORTANT: However, if the event target is the expansion control, do
       * not call this method.
       */
      protected void onTreeNodeBodyChildEvent(Event evt, Element treeNodeBody) {
      }

      /**
       * If an event was dispatched by the ExpansionControl.
       */
      protected void onExpansionControlEvent(Event evt, Element expansionControl) {
      }
    }

    private final Css css;
    private final Resources resources;

    public View(Resources resources) {
      super(Elements.createElement("ul"));
      this.resources = resources;
      this.css = resources.treeCss();
      getElement().setClassName(resources.treeCss().treeRoot());
      attachEventListeners();
    }

    void attachEventListeners() {

      // There used to be a MOUSEDOWN handler with stopPropagation() and
      // preventDefault() actions, but this badly affected the inline editing
      // experience inside the Tree (e.g. debugger's RemoteObjectTree).

      getElement().addEventListener(Event.CLICK, new TreeNodeEventListener(true) {
        @Override
        protected void onTreeNodeBodyChildEvent(Event evt, Element treeNodeBody) {
          SignalEvent signalEvent =
              SignalEventImpl.create((com.google.gwt.user.client.Event) evt, true);

          // Select the node.
          dispatchNodeSelectedEvent(treeNodeBody, signalEvent, css);

          // Don't dispatch a node action if there is a modifier key depressed.
          if (!(signalEvent.getCommandKey() || signalEvent.getShiftKey())) {
            dispatchNodeActionEvent(treeNodeBody, css);

            TreeNodeElement<D> node = getTreeNodeFromTreeNodeBody(treeNodeBody, css);
            if (node.hasChildrenContainer()) {
              dispatchExpansionEvent(node, css);
            }
          }
        }

        @Override
        protected void onExpansionControlEvent(Event evt, Element expansionControl) {
          if (!CssUtils.containsClassName(expansionControl, css.leafIcon())) {
            /*
             * they've clicked on the expand control of a tree node that is a
             * directory (so expand it)
             */
            TreeNodeElement<D> treeNode =
                ((JsElement) expansionControl.getParentElement().getParentElement()).<
                    TreeNodeElement<D>>cast();
            dispatchExpansionEvent(treeNode, css);
          }
        }
      }, false);

      getElement().addEventListener(Event.CONTEXTMENU, new TreeNodeEventListener(false) {
        @Override
        public void handleEvent(Event evt) {
          super.handleEvent(evt);
          evt.stopPropagation();
          evt.preventDefault();
        }

        @Override
        protected void onOtherEvent(Event evt) {
          MouseEvent mouseEvt = (MouseEvent) evt;

          // This is a click on the root.
          dispatchOnRootContextMenuEvent(mouseEvt.getClientX(), mouseEvt.getClientY());
        }

        @Override
        protected void onTreeNodeBodyChildEvent(Event evt, Element treeNodeBody) {
          MouseEvent mouseEvt = (MouseEvent) evt;

          // Dispatch if eventTarget is the treeNodeBody, or if it is a child
          // of a treeNodeBody.
          dispatchContextMenuEvent(
              mouseEvt.getClientX(), mouseEvt.getClientY(), treeNodeBody, css);
        }
      }, false);

      EventListener dragDropEventListener = new EventListener() {
        @Override
        public void handleEvent(Event event) {
          if (getDelegate() != null) {
            getDelegate().onDragDropEvent((JsDragEvent) event);
          }
        }
      };
      getElement().addEventListener(Event.DROP, dragDropEventListener, false);
      getElement().addEventListener(Event.DRAGOVER, dragDropEventListener, false);
      getElement().addEventListener(Event.DRAGENTER, dragDropEventListener, false);
      getElement().addEventListener(Event.DRAGLEAVE, dragDropEventListener, false);
      getElement().addEventListener(Event.DRAGSTART, dragDropEventListener, false);
    }

    private void dispatchContextMenuEvent(int mouseX, int mouseY, Element treeNodeBody, Css css) {

      // We assume the click happened on a TreeNodeBody. We walk up one level
      // to grab the treeNode element.
      @SuppressWarnings("unchecked")
      TreeNodeElement<D> treeNode =
          (TreeNodeElement<D>) treeNodeBody.getParentElement();

      assert (CssUtils.containsClassName(
          treeNode, css.treeNode())) : "Parent of an expandControl wasn't a TreeNode!";

      getDelegate().onNodeContextMenu(mouseX, mouseY, treeNode);
    }

    @SuppressWarnings("unchecked")
    private void dispatchExpansionEvent(TreeNodeElement<D> treeNode, Css css) {

      // Is the node opened or closed?
      if (treeNode.isOpen()) {
        getDelegate().onNodeClosed(treeNode);
      } else {

        // We might have set the CSS to say it is closed, but the animation
        // takes a little while. As such, we check to make sure the children
        // container is set to display:none before trying to dispatch an open.
        // Otherwise we can get into an inconsistent state if we click really
        // fast.
        Element childrenContainer = treeNode.getChildrenContainer();
        if (childrenContainer != null && !CssUtils.isVisible(childrenContainer)) {
          getDelegate().onNodeExpanded(treeNode);
        }
      }
    }

    private void dispatchNodeActionEvent(Element treeNodeBody, Css css) {
      getDelegate().onNodeAction(getTreeNodeFromTreeNodeBody(treeNodeBody, css));
    }

    private void dispatchNodeSelectedEvent(Element treeNodeBody, SignalEvent evt, Css css) {
      getDelegate().onNodeSelected(getTreeNodeFromTreeNodeBody(treeNodeBody, css), evt);
    }

    private void dispatchOnRootContextMenuEvent(int mouseX, int mouseY) {
      getDelegate().onRootContextMenu(mouseX, mouseY);
    }

    private TreeNodeElement<D> getTreeNodeFromTreeNodeBody(Element treeNodeBody, Css css) {
      TreeNodeElement<D> treeNode =
          ((JsElement) treeNodeBody.getParentElement()).<
              TreeNodeElement<D>>cast();

      assert (CssUtils.containsClassName(treeNode, css.treeNode())) :
          "Unexpected element when looking for tree node: " + treeNode.toString();

      return treeNode;
    }
  }

  /**
   * Logical events sourced by the Tree's View. Note that these events get
   * dispatched synchronously in our DOM event handlers.
   */
  private interface ViewEvents<D> {
    public void onNodeAction(TreeNodeElement<D> node);

    public void onNodeClosed(TreeNodeElement<D> node);

    public void onNodeContextMenu(int mouseX, int mouseY, TreeNodeElement<D> node);

    public void onDragDropEvent(JsDragEvent event);

    public void onNodeExpanded(TreeNodeElement<D> node);

    public void onNodeSelected(TreeNodeElement<D> node, SignalEvent event);

    public void onRootContextMenu(int mouseX, int mouseY);

    public void onRootDragDrop(JsDragEvent event);
  }

  private class DragDropController {
    private TreeNodeElement<D> targetNode;
    private boolean hadDragEnterEvent;

    private final ScheduledCommand hadDragEnterEventResetter = new ScheduledCommand() {
      @Override
      public void execute() {
        hadDragEnterEvent = false;
      }
    };

    private final Timer hoverToExpandTimer = new Timer() {
      @Override
      public void run() {
        expandNode(targetNode, true, true);
      }
    };

    void handleDragDropEvent(JsDragEvent evt) {
      final D rootData = getModel().root;
      final NodeDataAdapter<D> dataAdapter = getModel().getDataAdapter();
      final Css css = getModel().resources.treeCss();

      @SuppressWarnings("unchecked")
      TreeNodeElement<D> node =
          (TreeNodeElement<D>) CssUtils.getAncestorOrSelfWithClassName((Element) evt.getTarget(),
              css.treeNode());
      D newTargetData = node != null ? dataAdapter.getDragDropTarget(node.getData()) : rootData;
      TreeNodeElement<D> newTargetNode = dataAdapter.getRenderedTreeNode(newTargetData);

      String type = evt.getType();

      if (Event.DRAGSTART.equals(type)) {
        if (getModel().externalEventDelegate != null) {
          D sourceData = node != null ? node.getData() : rootData;
          // TODO support multiple folder selection.
          // We do not support dragging without any folder/file selection.
          if (sourceData != rootData) {
            TreeNodeElement<D> sourceNode = dataAdapter.getRenderedTreeNode(sourceData);
            getModel().externalEventDelegate.onNodeDragStart(sourceNode, evt);
          }
        }
        return;
      }

      if (Event.DROP.equals(type)) {
        if (getModel().externalEventDelegate != null) {
          if (newTargetData == rootData) {
            getModel().externalEventDelegate.onRootDragDrop(evt);
          } else {
            getModel().externalEventDelegate.onNodeDragDrop(newTargetNode, evt);
          }
        }

        clearDropTarget();

      } else if (Event.DRAGOVER.equals(type)) {
        if (newTargetNode != targetNode) {
          clearDropTarget();

          if (newTargetNode != null) {
            // Highlight the node by setting its drop target property
            targetNode = newTargetNode;
            targetNode.setIsDropTarget(true, css);

            if (dataAdapter.hasChildren(newTargetData) && !targetNode.isOpen()) {
              hoverToExpandTimer.schedule(HOVER_TO_EXPAND_DELAY_MS);
            }
          }
        }

      } else if (Event.DRAGLEAVE.equals(type)) {
        if (!hadDragEnterEvent) {
          // This wasn't part of a DRAGENTER-DRAGLEAVE pair (see below)
          clearDropTarget();
        }

      } else if (Event.DRAGENTER.equals(type)) {
        /*
         * DRAGENTER comes before DRAGLEAVE, and a deferred command scheduled
         * here will execute after the DRAGLEAVE. We use hadDragEnter to track a
         * paired DRAGENTER-DRAGLEAVE so that we can cleanup when we get an
         * unpaired DRAGLEAVE.
         */
        hadDragEnterEvent = true;
        Scheduler.get().scheduleDeferred(hadDragEnterEventResetter);
      }

      evt.preventDefault();
      evt.stopPropagation();
    }

    private void clearDropTarget() {
      hoverToExpandTimer.cancel();

      if (targetNode != null) {
        targetNode.setIsDropTarget(false, getModel().resources.treeCss());
        targetNode = null;
      }
    }
  }

  private final DragDropController dragDropController = new DragDropController();

  /**
   * Handles logical events sourced by the View.
   */
  private final ViewEvents<D> viewEventHandler = new ViewEvents<D>() {
    @Override
    public void onNodeAction(final TreeNodeElement<D> node) {
      selectSingleNode(node, true);
    }

    @Override
    public void onNodeClosed(TreeNodeElement<D> node) {
      closeNode(node, true);
    }

    @Override
    public void onNodeContextMenu(int mouseX, int mouseY, TreeNodeElement<D> node) {

      // We want to select the node if it isn't already selected.
      getModel().selectionModel.contextSelect(node.getData());

      if (getModel().externalEventDelegate != null) {
        getModel().externalEventDelegate.onNodeContextMenu(mouseX, mouseY, node);
      }
    }

    @Override
    public void onDragDropEvent(JsDragEvent event) {
      dragDropController.handleDragDropEvent(event);
    }

    @Override
    public void onNodeExpanded(TreeNodeElement<D> node) {
      expandNode(node, true, true);
    }

    @Override
    public void onNodeSelected(TreeNodeElement<D> node, SignalEvent event) {
      getModel().selectionModel.selectNode(node.getData(), event);
    }

    @Override
    public void onRootContextMenu(int mouseX, int mouseY) {
      if (getModel().externalEventDelegate != null) {
        getModel().externalEventDelegate.onRootContextMenu(mouseX, mouseY);
      }
    }

    @Override
    public void onRootDragDrop(JsDragEvent event) {
      if (getModel().externalEventDelegate != null) {
        getModel().externalEventDelegate.onRootDragDrop(event);
      }
    }
  };

  private static final int HOVER_TO_EXPAND_DELAY_MS = 500;

  private final Model<D> treeModel;

  /**
   * Constructor.
   */
  public Tree(View<D> view, Model<D> model) {
    super(view);
    this.treeModel = model;
    getView().setDelegate(viewEventHandler);
  }

  public Model<D> getModel() {
    return treeModel;
  }

  /**
   * Selects a node in the tree and auto expands the tree to this node.
   *
   * @param nodeData the node we want to select and expand to.
   * @param dispatchNodeAction whether or not to notify listeners of the node
   *        action for the selected node.
   */
  public void autoExpandAndSelectNode(D nodeData, boolean dispatchNodeAction) {

    // Expand the tree to the selected element.
    expandPathRecursive(getModel().root, getModel().dataAdapter.getNodePath(nodeData), false);
    // By now the node should have a rendered element.
    TreeNodeElement<D> renderedNode = getModel().dataAdapter.getRenderedTreeNode(nodeData);

    assert (renderedNode != null) : "Expanded selection has a null rendered node!";

    selectSingleNode(renderedNode, dispatchNodeAction);
  }

  private void selectSingleNode(TreeNodeElement<D> renderedNode, boolean dispatchNodeAction) {
    getModel().selectionModel.selectSingleNode(renderedNode.getData());
    maybeNotifyNodeActionExternal(renderedNode, dispatchNodeAction);
  }

  /**
   * Creates a {@link TreeNodeElement}. This does NOT attach said node to the
   * tree. You have to do that manually with {@link TreeNodeElement#addChild}.
   */
  public TreeNodeElement<D> createNode(D nodeData) {
    return TreeNodeElement.create(
        nodeData, getModel().dataAdapter, getModel().nodeRenderer, getModel().resources.treeCss());
  }

  /**
   * @see: {@link #expandNode(TreeNodeElement, boolean, boolean)}.
   */
  public void expandNode(TreeNodeElement<D> treeNode) {
    expandNode(treeNode, false, false);
  }

  /**
   * Expands a {@link TreeNodeElement} and renders its children if it "needs
   * to". "Needs to" is defined as whether or not the children have never been
   * rendered before, or if size of the set of rendered children differs from
   * the size of children in the underlying model.
   *
   * @param treeNode the {@link TreeNodeElement} we are expanding.
   * @param shouldAnimate whether to animate the expansion
   * @param dispatchNodeExpanded whether or not to notify listeners of the node
   *        expansion
   */
  private void expandNode(TreeNodeElement<D> treeNode, boolean shouldAnimate,
      boolean dispatchNodeExpanded) {
    // This is most likely because someone tried to expand root. Ignore it.
    if (treeNode == null) {
      return;
    }

    NodeDataAdapter<D> dataAdapter = getModel().dataAdapter;

    // Nothing to do here.
    if (!dataAdapter.hasChildren(treeNode.getData())) {
      return;
    }

    // Ensure that the node's children container is birthed.
    treeNode.ensureChildrenContainer(dataAdapter, getModel().resources.treeCss());

    JsonArray<D> children = dataAdapter.getChildren(treeNode.getData());

    // Maybe render it's children if they aren't already rendered.
    if (treeNode.getChildrenContainer().getChildren().getLength() != children.size()) {

      // Then the model has not been correctly reflected in the UI.
      // Blank the children and render a single level for each.
      treeNode.getChildrenContainer().setInnerHTML("");
      for (int i = 0, n = children.size(); i < n; i++) {
        renderRecursive(treeNode.getChildrenContainer(), children.get(i), 0);
      }
    }

    // Render the node as being opened after the children have been added, so that
    // AnimationController can correctly measure the height of the child container.
    treeNode.openNode(
        dataAdapter, getModel().resources.treeCss(), getModel().animator, shouldAnimate);

    // Notify listeners of the event.
    if (dispatchNodeExpanded && getModel().externalEventDelegate != null) {
      getModel().externalEventDelegate.onNodeExpanded(treeNode);
    }
  }

  public void closeNode(TreeNodeElement<D> treeNode) {
    closeNode(treeNode, false);
  }

  private void closeNode(TreeNodeElement<D> treeNode, boolean dispatchNodeClosed) {
    if (!treeNode.isOpen()) {
      return;
    }

    treeNode.closeNode(
        getModel().dataAdapter, getModel().resources.treeCss(), getModel().animator, true);
    if (dispatchNodeClosed && getModel().externalEventDelegate != null) {
      getModel().externalEventDelegate.onNodeClosed(treeNode);
    }
  }

  /**
   * Takes in a list of paths relative to the root, that correspond to nodes in
   * the tree that need to be expanded.
   *
   * <p>This will try to expand all the given paths recursively, and return the
   * array of paths that could not be fully expanded, i.e. when the leaf that
   * the path points to was not found in the tree. In these cases all the middle
   * nodes that were found in the tree will be expanded though.
   *
   * <p>The returned array of not expanded paths may be used to save and restore
   * the expansion history.
   *
   * @param paths array of paths to expand
   * @param dispatchNodeExpanded whether to dispatch the NodeExpanded event
   * @return array of paths that were not expanded, or were partially expanded
   */
  public JsonArray<JsonArray<String>> expandPaths(JsonArray<JsonArray<String>> paths,
      boolean dispatchNodeExpanded) {
    JsonArray<JsonArray<String>> notExpanded = JsonCollections.createArray();
    for (int i = 0, n = paths.size(); i < n; i++) {
      if (!expandPathRecursive(getModel().root, paths.get(i), dispatchNodeExpanded)) {
        notExpanded.add(paths.get(i));
      }
    }
    return notExpanded;
  }

  /**
   * Gets the associated {@link TreeNodeElement} for a given nodeData.
   *
   * If there is no such node rendered in the tree, then {@code null} is
   * returned.
   */
  public TreeNodeElement<D> getNode(D nodeData) {
    return getModel().getDataAdapter().getRenderedTreeNode(nodeData);
  }

  public Resources getResources() {
    return getModel().resources;
  }

  public SelectionModel<D> getSelectionModel() {
    return getModel().selectionModel;
  }

  /**
   * Removes a node from the DOM. Does not mutate the the underlying model. That
   * should be already done before calling this method.
   */
  public void removeNode(TreeNodeElement<D> node) {
    if (node == null) {
      return;
    }

    // Remove from the DOM
    node.removeFromTree();

    // Notify the selection model in case it was selected.
    getModel().selectionModel.removeNode(node.getData());
  }

  /**
   * Renders the entire tree starting with the root node.
   */
  public void renderTree() {
    renderTree(-1);
  }

  /**
   * Renders the tree starting with the root node up until the specified depth.
   *
   *  This will NOT restore any expansions. If you want to re-render the tree
   * obeying previous expansions then,
   *
   * @see: {@link #replaceSubtree(Object, Object)}.
   *
   * @param depth integer indicating how deep we should auto-expand. -1 means
   *        render the entire tree.
   */
  public void renderTree(int depth) {
    // Clear the current view.
    Element rootElement = getView().getElement();
    rootElement.setInnerHTML("");

    // If the root is not set, we have nothing to render.
    D root = getModel().root;
    if (root == null) {
      return;
    }

    // Root is special in that we don't render a directory for it. Only its
    // children.
    JsonArray<D> children = getModel().dataAdapter.getChildren(root);
    for (int i = 0, n = children.size(); i < n; i++) {
      renderRecursive(rootElement, children.get(i), depth);
    }
  }

  /**
   * Replaces the old node in the tree with data representing the subtree rooted
   * where the old node used to be if the old node was rendered.
   *
   * <p>{@code oldSubtreeData} and {@code incomingSubtreeData} are allowed to be
   * the same node (it will simply get re-rendered).
   *
   * <p>This methods also tries to preserve the original expansion state. Any
   * path that was expanded before executing this method but could not be
   * expanded after replacing the subtree, will be returned in the result array,
   * so that it could be expanded later using the {@link #expandPaths} method,
   * if needed (for example, if children of the tree are getting populated
   * asynchronously).
   *
   * @param shouldAnimate if true, the subtree will animate open if it is still open
   * @return array paths that could not be expanded in the new subtree
   */
  public JsonArray<JsonArray<String>> replaceSubtree(D oldSubtreeData, D incomingSubtreeData,
      boolean shouldAnimate) {

    // Gather paths that were expanded in this subtree so that we can restore
    // them later after rendering.
    JsonArray<JsonArray<String>> expandedPaths = gatherExpandedPaths(oldSubtreeData);

    boolean wasRoot = (oldSubtreeData == getModel().root);
    TreeNodeElement<D> oldRenderedNode = null;
    TreeNodeElement<D> newRenderedNode = null;

    if (wasRoot) {

      // We are rendering root! Just render it from the top. We will restore the
      // expansion later.
      getModel().setRoot(incomingSubtreeData);
      renderTree(0);
    } else {
      oldRenderedNode = getModel().dataAdapter.getRenderedTreeNode(oldSubtreeData);

      // If the node does not have a rendered node, then we have nothing to do.
      if (oldRenderedNode == null) {
        expandedPaths.clear();
        return expandedPaths;
      }

      JsElement parentElem = oldRenderedNode.getParentElement();

      // The old node may have been moved from a rendered to a non-rendered
      // state (e.g., into a collapsed folder). In that case, it doesn't have a
      // parent, and we're done here.
      if (parentElem == null) {
        expandedPaths.clear();
        return expandedPaths;
      }

      // Make a new tree node.
      newRenderedNode = createNode(incomingSubtreeData);
      parentElem.insertBefore(newRenderedNode, oldRenderedNode);

      // Remove the old rendered node from the tree.
      oldRenderedNode.removeFromParent();
    }

    // If the old node was the root, or if it and its parents were expanded, then we should
    // attempt to restore expansion.
    boolean shouldExpand = wasRoot;
    if (!wasRoot && oldRenderedNode != null) {
      shouldExpand = true;
      TreeNodeElement<D> curNode = oldRenderedNode;
      while (curNode != null) {
        if (!curNode.isOpen()) {
          // One of the parents is closed, so we should not expand all paths.
          shouldExpand = false;
          break;
        }

        D parentData = getModel().dataAdapter.getParent(curNode.getData());
        curNode =
            (parentData == null) ? null : getModel().dataAdapter.getRenderedTreeNode(parentData);
      }
    }
    if (shouldExpand) {
      // Animate the top node if it was open. If we should not animate, the newRenderedNode will
      // still be expanded by the call to expandPaths() below.
      if (shouldAnimate && newRenderedNode != null) {
        expandNode(newRenderedNode, true, true);
      }

      // But if it is open, we need to attempt to restore the expansion.
      expandedPaths = expandPaths(expandedPaths, true);
    } else {
      expandedPaths.clear();
    }

    // TODO: Be more surgical about restoring the selection model. We
    // are currently recomputing all selected nodes.
