// 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.filetree;

import collide.client.common.CanRunApplication;
import collide.client.filetree.FileTreeContextMenuController.ContextMenuMode;
import collide.client.treeview.Tree;
import collide.client.treeview.TreeNodeElement;
import collide.client.treeview.TreeNodeLabelRenamer;

import com.google.collide.client.code.FileSelectedPlace;
import com.google.collide.client.communication.ResourceUriUtils;
import com.google.collide.client.history.Place;
import com.google.collide.client.ui.dropdown.DropdownWidgets;
import com.google.collide.client.util.PathUtil;
import com.google.collide.json.shared.JsonArray;

import elemental.html.Location;
import elemental.js.html.JsDragEvent;

/**
 * Listens for changes to the model via callbacks from {@link FileTreeModel} and updates the Tree.
 * Similarly, this controller takes events reported from the {@link Tree} (like clicks and
 * selections) and handles them.
 */
public class FileTreeUiController implements FileTreeModel.TreeModelChangeListener {

  /**
   * Static factory method for obtaining an instance of a FileTreeUiController.
   */
  public static FileTreeUiController create(Place place,
      FileTreeModel fileTreeModel,
      Tree<FileTreeNode> tree,
      FileTreeController<?> controller,
      CanRunApplication applicationRunner) {

    // Set the initial root node for the tree. This will simply be null on first
    // load. But if it isn't we should probably still render what ever was in
    // the FileTreeModel.
    tree.getModel().setRoot(fileTreeModel.getWorkspaceRoot());
    TreeNodeLabelRenamer<FileTreeNode> nodeLabelMutator = new TreeNodeLabelRenamer<FileTreeNode>(
        tree.getModel().getNodeRenderer(), tree.getModel().getDataAdapter(),
        controller.getResources().workspaceNavigationFileTreeNodeRendererCss());
    FileTreeUiController treeUiController = new FileTreeUiController(place,
        controller.getResources(),
        fileTreeModel,
        tree,
        nodeLabelMutator,
        controller,
        applicationRunner);
    fileTreeModel.addModelChangeListener(treeUiController);
    treeUiController.attachEventHandlers();
    return treeUiController;
  }

  /** Listener for drag-and-drop events on nodes in the file tree. */
  public interface DragDropListener {
    void onDragStart(FileTreeNode node, JsDragEvent event);

    void onDragDrop(FileTreeNode node, JsDragEvent event);
  }

  private final FileTreeContextMenuController contextMenuController;
  private DragDropListener uploadDragDropListener;
  private DragDropListener treeNodeMoveListener;
  private final FileTreeModel fileTreeModel;
  private final Tree<FileTreeNode> tree;
  private final Place currentPlace;

  FileTreeUiController(Place place,
      DropdownWidgets.Resources res,
      FileTreeModel fileTreeModel,
      Tree<FileTreeNode> tree,
      TreeNodeLabelRenamer<FileTreeNode> nodeLabelMutator,
      FileTreeController<?> controller,
      CanRunApplication applicationRunner) {
    this.currentPlace = place;
    this.fileTreeModel = fileTreeModel;
    this.tree = tree;
    this.contextMenuController = FileTreeContextMenuController.create(place,
        res,
        this,
        fileTreeModel,
        nodeLabelMutator,
        controller,
        applicationRunner);
  }

  /**
   * Programmatically selects a node in the tree. This will cause any external handlers of the
   * {@link Tree} to have their {@link collide.client.treeview.Tree.Listener#onNodeAction(TreeNodeElement)}
   * method get invoked.
   */
  public void autoExpandAndSelectNode(FileTreeNode nodeToSelect, boolean dispatchNodeAction) {
    tree.autoExpandAndSelectNode(nodeToSelect, dispatchNodeAction);
  }

  public void expandNode(TreeNodeElement<FileTreeNode> parentTreeNode) {
    tree.expandNode(parentTreeNode);
  }

  public void clearSelectedNodes() {
    tree.getSelectionModel().clearSelections();
  }

  public FileTreeContextMenuController getContextMenuController() {
    return contextMenuController;
  }

  public Tree<FileTreeNode> getTree() {
    return tree;
  }

  public void setUploadDragDropListener(DragDropListener listener) {
    uploadDragDropListener = listener;
  }

  public void setFileTreeNodeMoveListener(DragDropListener listener) {
    treeNodeMoveListener = listener;
  }

  public void nodeWillBeAdded() {
  }

  /**
   * Called when a node is added to the model. This will re-render the subtree rooted at this nodes
   * parent iff the parent node is already rendered.
   */
  @Override
  public void onNodeAdded(PathUtil parentDirPath, FileTreeNode newNode) {
    FileTreeNode rootNode = tree.getModel().getRoot();
    if (rootNode == null) {
      return;
    }

    if (PathUtil.WORKSPACE_ROOT.getPathString().equals(parentDirPath.getPathString())) {

      // This means that we are adding to the base of the tree and should reRender.
      reRenderSubTree(rootNode);
      return;
    }

    FileTreeNode parentDir = rootNode.findChildNode(parentDirPath);

    if (parentDir != null && parentDir.isComplete()) {

      // Add the node.
      TreeNodeElement<FileTreeNode> parentDirTreeNode = tree.getNode(parentDir);

      if (parentDirTreeNode != null) {
        reRenderSubTree(parentDir);
      }
    }
  }

  @Override
  public void onNodeMoved(
      PathUtil oldPath, FileTreeNode node, PathUtil newPath, FileTreeNode newNode) {
    FileTreeNode rootNode = tree.getModel().getRoot();
    if (rootNode == null) {
      return;
    }

    if (node != null) {
      FileTreeNode oldParent = rootNode.findChildNode(PathUtil.createExcludingLastN(oldPath, 1));
      onNodeRemoved(oldParent, node.getRenderedTreeNode());
      // do not kill the back reference (as in onNodeRemoved)!
    }

    if (newNode != null) {
      PathUtil parentDirPath = PathUtil.createExcludingLastN(newPath, 1);
      onNodeAdded(parentDirPath, newNode);
    }
  }

  @Override
  public void onNodesRemoved(JsonArray<FileTreeNode> oldNodes) {

    FileTreeNode rootNode = tree.getModel().getRoot();
    if (rootNode == null) {
      return;
    }

    for (int i = 0; i < oldNodes.size(); i++) {
      FileTreeNode oldNode = oldNodes.get(i);
      // If we found a node at the specified path, then remove it.
      if (oldNode != null) {
        onNodeRemoved(oldNode.getParent(), oldNode.getRenderedTreeNode());

        // Kill the back reference so we don't leak.
        oldNode.setRenderedTreeNode(null);
      }
    }
  }

  @Override
  public void onNodeReplaced(FileTreeNode oldNode, FileTreeNode newNode) {

    if (!newNode.isDirectory()) {
      // We don't need to do anything with files being replaced
      return;
    }

    if (PathUtil.WORKSPACE_ROOT.getPathString().equals(newNode.getNodePath().getPathString())) {

      // Install the workspace root for the tree and render it. Expansion state
      // should be restored by this method.
      tree.replaceSubtree(tree.getModel().getRoot(), newNode, false);
    } else if (oldNode != null) {
      TreeNodeElement<FileTreeNode> oldRenderedElement = oldNode.getRenderedTreeNode();

      // If the node that we just replaced had a rendered tree node, we need
      // to re-render.
      if (oldRenderedElement != null) {
        // If the node was loading, animate it open now that the children are available.
        tree.replaceSubtree(oldNode, newNode, oldNode.isLoading());

        // Kill the back reference so we don't leak.
        oldNode.setRenderedTreeNode(null);
      }
    }
  }

  /**
   * Re-renders the subtree rooted at the specified {@link FileTreeNode}, ensuring that its direct
   * children are sorted.
   */
  /* TODO : restore selection state */
  public void reRenderSubTree(FileTreeNode parentDir) {
    parentDir.invalidateUnifiedChildrenCache();
    tree.replaceSubtree(parentDir, parentDir, false);
  }

  private void attachEventHandlers() {
    tree.setTreeEventHandler(new Tree.Listener<FileTreeNode>() {

      @Override
      public void onNodeAction(TreeNodeElement<FileTreeNode> node) {
        if (node.getData().isFile()
            && getContextMenuController().getMode() == ContextMenuMode.READY) {
          currentPlace.fireChildPlaceNavigation(
              FileSelectedPlace.PLACE.createNavigationEvent(node.getData().getNodePath()));
        }
      }

      @Override
      public void onNodeClosed(TreeNodeElement<FileTreeNode> node) {}

      @Override
      public void onNodeContextMenu(int mouseX, int mouseY, TreeNodeElement<FileTreeNode> node) {
        getContextMenuController().show(mouseX, mouseY, node);
      }

      @Override
      public void onNodeDragDrop(TreeNodeElement<FileTreeNode> node, JsDragEvent event) {
        if (treeNodeMoveListener != null) {
          treeNodeMoveListener.onDragDrop(node.getData(), event);
        }

        if (uploadDragDropListener != null) {
          uploadDragDropListener.onDragDrop(node.getData(), event);
        }
      }

      @Override
      public void onRootDragDrop(JsDragEvent event) {
        if (treeNodeMoveListener != null) {
          treeNodeMoveListener.onDragDrop(fileTreeModel.getWorkspaceRoot(), event);
        }

        if (uploadDragDropListener != null) {
          uploadDragDropListener.onDragDrop(fileTreeModel.getWorkspaceRoot(), event);
        }
      }

      @Override
      public void onNodeDragStart(TreeNodeElement<FileTreeNode> node, JsDragEvent event) {
        // When drag starts in tree, we do not know if users want to drop it
        // outside tree or in tree. So, we prepare for both cases.
        prepareForDraggingToOutside(node, event);
        if (treeNodeMoveListener != null) {
          treeNodeMoveListener.onDragStart(node.getData(), event);
        }

        if (uploadDragDropListener != null) {
          uploadDragDropListener.onDragStart(node.getData(), event);
        }
      }

      @Override
      public void onNodeExpanded(TreeNodeElement<FileTreeNode> node) {
        if (!node.getData().isComplete() && !node.getData().isLoading()) {
          // Mark the node as loading.
          node.getData().setLoading(true);
          tree.getModel().getNodeRenderer().updateNodeContents(node);

          // Load the children of the directory.
          fileTreeModel.requestDirectoryChildren(node.getData(), null);
        }
      }

      @Override
      public void onRootContextMenu(int mouseX, int mouseY) {
        getContextMenuController().show(mouseX, mouseY, null);
      }

      private void prepareForDraggingToOutside(
          TreeNodeElement<FileTreeNode> node, JsDragEvent event) {
        FileTreeNode fileTreeNode = node.getData();
        PathUtil nodePath = fileTreeNode.getNodePath();

        String downloadFileName = fileTreeNode.isDirectory() ? nodePath.getBaseName() + ".zip"
            : nodePath.getBaseName();

        Location location = elemental.client.Browser.getWindow().getLocation();
        String urlHttpHostPort = location.getProtocol() + "//" + location.getHost();

        String downloadUrl = "application/octet-stream:" + downloadFileName + ":"
            + ResourceUriUtils.getAbsoluteResourceUri(nodePath)
            + (fileTreeNode.isDirectory() ? "?rt=zip" : "");
        event.getDataTransfer().setData("DownloadURL", downloadUrl);
      }
    });
  }

  private void onNodeRemoved(FileTreeNode oldParent, TreeNodeElement<FileTreeNode> oldNode) {
    tree.removeNode(oldNode);

    // Make sure to remove expansion controls from any directory that might
    // now be empty
    if (oldParent != null && oldParent.getRenderedTreeNode() != null) {
      if (oldParent.getUnifiedChildren().isEmpty()) {
        oldParent.getRenderedTreeNode().makeLeafNode(tree.getResources().treeCss());
      }
    }
  }
}
