// 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 java.util.ArrayList;
import java.util.List;

import collide.client.common.CanRunApplication;
import collide.client.filetree.FileTreeModel.NodeRequestCallback;
import collide.client.treeview.SelectionModel;
import collide.client.treeview.Tree;
import collide.client.treeview.TreeNodeElement;
import collide.client.treeview.TreeNodeLabelRenamer;
import collide.client.treeview.TreeNodeLabelRenamer.LabelRenamerCallback;
import collide.client.util.BrowserUtils;
import collide.client.util.Elements;

import com.google.collide.client.bootstrap.BootstrapSession;
import com.google.collide.client.communication.FrontendApi.ApiCallback;
import com.google.collide.client.communication.ResourceUriUtils;
import com.google.collide.client.history.Place;
import com.google.collide.client.status.StatusMessage;
import com.google.collide.client.status.StatusMessage.MessageType;
import com.google.collide.client.testing.DebugAttributeSetter;
import com.google.collide.client.ui.dropdown.DropdownController;
import com.google.collide.client.ui.dropdown.DropdownController.DropdownPositionerBuilder;
import com.google.collide.client.ui.dropdown.DropdownWidgets;
import com.google.collide.client.ui.list.SimpleList.ListItemRenderer;
import com.google.collide.client.ui.menu.PositionController;
import com.google.collide.client.ui.menu.PositionController.HorizontalAlign;
import com.google.collide.client.ui.menu.PositionController.Positioner;
import com.google.collide.client.ui.tooltip.Tooltip;
import com.google.collide.client.util.PathUtil;
import com.google.collide.client.workspace.UploadClickedEvent;
import com.google.collide.client.workspace.UploadClickedEvent.UploadType;
import com.google.collide.client.workspace.WorkspaceUtils;
import com.google.collide.dto.DirInfo;
import com.google.collide.dto.EmptyMessage;
import com.google.collide.dto.FileInfo;
import com.google.collide.dto.Mutation;
import com.google.collide.dto.ServerError.FailureReason;
import com.google.collide.dto.TreeNodeInfo;
import com.google.collide.dto.WorkspaceTreeUpdate;
import com.google.collide.dto.client.DtoClientImpls.DirInfoImpl;
import com.google.collide.dto.client.DtoClientImpls.FileInfoImpl;
import com.google.collide.dto.client.DtoClientImpls.TreeNodeInfoImpl;
import com.google.collide.json.client.JsoArray;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.FrontendConstants;
import com.google.collide.shared.util.JsonCollections;
import com.google.gwt.user.client.Window;

import elemental.dom.Element;
import elemental.events.Event;
import elemental.events.EventListener;
import elemental.html.IFrameElement;

/**
 * Handles File tree context menu actions.
 */
public class FileTreeContextMenuController {

  /**
   * The data for a menu item in the context menu.
   */
  abstract class FileTreeMenuItem {
    abstract void onClicked(TreeNodeElement<FileTreeNode> node);

    boolean isDisabled() {
      return false;
    }

    @Override
    public abstract String toString();
  }

  class FileTreeItemRenderer extends ListItemRenderer<FileTreeMenuItem> {
    final String DISABLED_COLOR = "#ccc";

    @Override
    public void render(Element listItemBase, FileTreeMenuItem item) {
      if (item.isDisabled()) {
        listItemBase.getStyle().setColor(DISABLED_COLOR);
      }
      new DebugAttributeSetter().add("disabled", Boolean.toString(item.isDisabled()))
          .on(listItemBase);
      listItemBase.setTextContent(item.toString());
    }
  }

  /**
   * Specifies the current mode of the context menu. Some modes may indicate that the context menu
   * retains control of the cursor, for example in RENAME mode. When the mode is READY, the context
   * menu will not capture the cursor.
   */
  public enum ContextMenuMode {
    /** the cursor will not be captured by the context menu */
    READY,

    /** the cursor will be captured and used for placing the caret in the rename text field */
    RENAME
  }

  private static final String NEW_FILE_NAME = "untitled";

  /**
   * The parameter used to enable file tree cut.
   */
  private static final String FILE_TREE_CUT_URL_PARAM = "fileTreeCutEnabled";

  public static final String DOWNLOAD_FRAME_ID = "download";

  /**
   * Static factory method for obtaining an instance of FileTreeContextMenuController.
   */
  public static FileTreeContextMenuController create(Place place,
      DropdownWidgets.Resources res,
      FileTreeUiController fileTreeUiController,
      FileTreeModel fileTreeModel,
      TreeNodeLabelRenamer<FileTreeNode> nodeLabelMutator,
      FileTreeController<?> controller,
      CanRunApplication applicationRunner) {

    FileTreeContextMenuController ctxMenuController = new FileTreeContextMenuController(place,
        fileTreeUiController,
        fileTreeModel,
        nodeLabelMutator,
        controller,
        applicationRunner);
    ctxMenuController.installContextMenu(res);
    return ctxMenuController;
  }

  private final FileTreeController<?> controller;

  private DropdownController<FileTreeMenuItem> contextDropdownController;

  private DropdownController<FileTreeMenuItem> buttonDropdownController;

  private final JsoArray<FileTreeNode> copiedNodes = JsoArray.create();
  private boolean copiedNodesAreCut = false;

  private final JsonArray<FileTreeMenuItem> rootMenuItems = JsonCollections.createArray();
  private final JsonArray<FileTreeMenuItem> dirMenuItems = JsonCollections.createArray();
  private final JsonArray<FileTreeMenuItem> fileMenuItems = JsonCollections.createArray();
  private final JsonArray<FileTreeMenuItem> readonlyRootMenuItems = JsonCollections.createArray();
  private final JsonArray<FileTreeMenuItem> readonlyDirMenuItems = JsonCollections.createArray();
  private final JsonArray<FileTreeMenuItem> readonlyFileMenuItems = JsonCollections.createArray();

  private final boolean isReadOnly = false;
  private final FileTreeItemRenderer renderer;
  private final List<FileTreeMenuItem> allMenuItems = new ArrayList<FileTreeMenuItem>();
  private final FileTreeModel fileTreeModel;
  private final FileTreeUiController fileTreeUiController;
  private final TreeNodeLabelRenamer<FileTreeNode> nodeLabelMutator;
  private final Place place;
  private final CanRunApplication applicationRunner;

  private Tooltip invalidNameTooltip;

  private ContextMenuMode mode = ContextMenuMode.READY;

  private TreeNodeElement<FileTreeNode> selectedNode;

  FileTreeContextMenuController(Place place,
      FileTreeUiController fileTreeUiController,
      FileTreeModel fileTreeModel,
      TreeNodeLabelRenamer<FileTreeNode> nodeLabelMutator,
      FileTreeController<?> controller,
      CanRunApplication applicationRunner) {
    this.place = place;
    this.fileTreeUiController = fileTreeUiController;
    this.fileTreeModel = fileTreeModel;
    this.controller = controller;
    this.nodeLabelMutator = nodeLabelMutator;
    this.applicationRunner = applicationRunner;

    createMenuItems();

    renderer = new FileTreeItemRenderer();
  }

  /**
   * Creates an additional dropdown menu attached to a button instead of a right click.
   *
   * @param anchorElement the element to attach the button to
   */
  public void createMenuDropdown(Element anchorElement) {
    // Create the dropdown controller for the file tree menu button.
    DropdownController.Listener<FileTreeMenuItem> listener =
        new DropdownController.BaseListener<FileTreeMenuItem>() {
          @Override
          public void onItemClicked(FileTreeMenuItem item) {
            if (item.isDisabled()) {
              return;
            }
            item.onClicked(null);
          }
        };
    Positioner positioner = new DropdownPositionerBuilder().setHorizontalAlign(
        HorizontalAlign.RIGHT).buildAnchorPositioner(anchorElement);
    buttonDropdownController = new DropdownController.Builder<FileTreeMenuItem>(positioner,
        anchorElement, controller.getResources(), listener, renderer).setShouldAutoFocusOnOpen(true)
        .build();
    buttonDropdownController.setItems(rootMenuItems);
  }

  /**
   * Simply handles the copy command from the context menu. Doesn't actually do anything other than
   * stash a reference to the copied node until a paste is issued.
   *
   * @param node the copied node
   * @param isCut true if the node is cut
   */
  private void handleCopy(TreeNodeElement<FileTreeNode> node, boolean isCut) {
    copiedNodesAreCut = isCut;
    copiedNodes.clear();

    SelectionModel<FileTreeNode> selectionModel =
        fileTreeUiController.getTree().getSelectionModel();
    copiedNodes.addAll(selectionModel.getSelectedNodes());

    // If there is no active selection, simply make the clicked on node the
    // copiedNode.
    if (copiedNodes.isEmpty()) {
      copiedNodes.add(node.getData());
    }
  }

  public void handleDelete(final TreeNodeElement<FileTreeNode> nodeToDelete) {
    WorkspaceTreeUpdate msg = fileTreeModel.makeEmptyTreeUpdate();
    JsoArray<FileTreeNode> selectedNodes =
        fileTreeUiController.getTree().getSelectionModel().getSelectedNodes();

    for (int i = 0, n = selectedNodes.size(); i < n; i++) {
      FileTreeNode node = selectedNodes.get(i);
      copiedNodes.remove(node);
      msg.getMutations().add(FileTreeUtils.makeMutation(
          Mutation.Type.DELETE, node.getNodePath(), null, node.isDirectory(),
          node.getFileEditSessionKey()));
    }

    controller.mutateWorkspaceTree(
        msg, new ApiCallback<EmptyMessage>() {

          @Override
          public void onMessageReceived(EmptyMessage message) {

            // We lean on the Tango broadcast to mutate the
            // tree.
          }

          @Override
          public void onFail(FailureReason reason) {
            // Do nothing.
          }
        });
  }

  public void handleNewFile(TreeNodeElement<FileTreeNode> parentTreeNode) {

    handleNodeWillBeAdded();

    FileTreeNode parentData = getDirData(parentTreeNode);
    String newFileName = FileTreeUtils.allocateName(parentData.<DirInfoImpl>cast(), NEW_FILE_NAME);

    FileInfoImpl newFile = FileInfoImpl.make().setSize("0");
    newFile.<TreeNodeInfoImpl>cast().setNodeType(TreeNodeInfo.FILE_TYPE);
    newFile.setName(newFileName);

    handleNewNode(parentTreeNode, parentData, newFile.<FileTreeNode>cast());
  }

  public void handleNewFolder(TreeNodeElement<FileTreeNode> parentTreeNode) {

    handleNodeWillBeAdded();

    FileTreeNode parentData = getDirData(parentTreeNode);
    String newDirName =
        FileTreeUtils.allocateName(parentData.<DirInfoImpl>cast(), NEW_FILE_NAME + "Folder");

    DirInfoImpl newDir = DirInfoImpl.make()
        .setFiles(JsoArray.<FileInfo>create()).setSubDirectories(JsoArray.<DirInfo>create())
        .setIsComplete(false);
    newDir.<TreeNodeInfoImpl>cast().setNodeType(TreeNodeInfo.DIR_TYPE);
    newDir.setName(newDirName);

    handleNewNode(parentTreeNode, parentData, newDir.<FileTreeNode>cast());
  }

  /**
   * Notify that a file is going to be added so that we can hide the template picker. This is
   * workspace/user specific, so we don't need to broadcast the message. The actual add will be
   * broadcast.
   */
  private void handleNodeWillBeAdded() {
    fileTreeUiController.nodeWillBeAdded();
  }

  public void handleDownload(TreeNodeElement<FileTreeNode> parentTreeNode, final boolean asZip) {
    FileTreeNode parentData = getDirData(parentTreeNode);
    final String path = parentData == null ? "/" : parentData.getNodePath().getPathString();

    if (parentTreeNode != null) {
      handleDownloadImpl(asZip, path, path.substring(path.lastIndexOf('/') + 1));
    } else {
// TODO: Re-enable workspace downloading.

//      workspaceManager.getWorkspace(new QueryCallback<Workspace>() {
//        @Override
//        public void onFail(FailureReason reason) {
//          handleDownloadImpl(asZip, "/", "workspace-" + );
//        }
//
//        @Override
//        public void onQuerySuccess(Workspace result) {
//          // Lose special characters that would confuse OS'es, shells, or
//          // people. We replace spaces and tabs; slash, colon and backslash;
//          // semicolon and quotes with underbar. To avoid confusing HTTP
//          // agents, we then URL-encode the result.
//          String name = result.getWorkspaceInfo().getName().replaceAll("[\\s/:\\\\;'\"]", "_");
//          name = URL.encodeQueryString(name);
//          handleDownloadImpl(asZip, "/", name);
//        }
//      });
    }
  }

  private void handleDownloadImpl(boolean asZip, String path, String fileSource) {
    String relativeUri =
        ResourceUriUtils.getAbsoluteResourceUri(fileSource);
    // actual .zip resources we download raw; anything else we do as a zip
    String source = relativeUri + (asZip ? ".zip?rt=zip" : "?rt=download") + "&cl="
        + BootstrapSession.getBootstrapSession().getActiveClientId();
    source = source + "&" + FrontendConstants.FILE_PARAM_NAME + "=" + path;

    // we're going to download the zip into a hidden iframe, which because
    // it's a zip the browser should offer to save on disk.
    final IFrameElement iframe = Elements.createIFrameElement();
    iframe.setId(DOWNLOAD_FRAME_ID);
    iframe.getStyle().setDisplay("none");
    iframe.setOnload(new EventListener() {
      @Override
      public void handleEvent(Event event) {
        iframe.removeFromParent();
      }
    });

    iframe.setSrc(source);
    Elements.getBody().appendChild(iframe);
  }

  /**
   * We do not do an optimistic UI update here since we potentially will need to re-fetch an entire
   * subtree of data, and doing an eager deep clone on the client seems like it would short circuit
   * a lot of our DTO->model data transformation.
   *
   *  Note that we do not support CUT for nodes in the tree. Only COPY and MOVE. Therefore PASTE
   * only has to consider the COPY case.
   *
   * @param parentDirNode the parent dir node (which may be incomplete), or null for the root
   */
  public void handlePaste(TreeNodeElement<FileTreeNode> parentDirNode) {
    if (copiedNodes.isEmpty()) {
      return;
    }

    // Figure out where it is being pasted to.
    FileTreeNode parentDirData = getDirData(parentDirNode);

    if (!parentDirData.isComplete()) {
      // Ensure we have its children so our duplicate check works
      fileTreeModel.requestDirectoryChildren(parentDirData, new NodeRequestCallback() {
        @Override
        public void onNodeAvailable(FileTreeNode node) {
          handlePasteForCompleteParent(node);
        }

        @Override
        public void onNodeUnavailable() {
          /*
           * This should be very rare, if you paste into an incomplete directory at the same time a
           * collaborator deletes it (your XHR response comes faster than the tree mutation push
           * message)
           */
          new StatusMessage(controller.getStatusManager(), MessageType.ERROR,
              "The destination folder for the paste no longer exists.").fire();
        }

        @Override
        public void onError(FailureReason reason) {
          new StatusMessage(controller.getStatusManager(), MessageType.ERROR,
              "The paste had a problem, please try again.").fire();
        }
      });
    } else {
      handlePasteForCompleteParent(parentDirData);
    }
  }

  private void handlePasteForCompleteParent(FileTreeNode parentDirData) {
    // TODO: Figure out if we are pasting on top of files that already
    // exist with the same name. If we do, we need to handle that via a prompted
    // replace.

    Mutation.Type mutationType = copiedNodesAreCut ? Mutation.Type.MOVE : Mutation.Type.COPY;
    WorkspaceTreeUpdate msg = fileTreeModel.makeEmptyTreeUpdate();
    for (int i = 0, n = copiedNodes.size(); i < n; i++) {
      FileTreeNode copiedNode = copiedNodes.get(i);
      PathUtil targetPath = new PathUtil.Builder().addPath(parentDirData.getNodePath())
          .addPathComponent(FileTreeUtils.allocateName(
              parentDirData.<DirInfoImpl>cast(), copiedNode.getName())).build();
      msg.getMutations().add(FileTreeUtils.makeMutation(
          mutationType, copiedNode.getNodePath(), targetPath, copiedNode.isDirectory(),
          copiedNode.getFileEditSessionKey()));
    }

    // Cut nodes can only be pasted once.
    if (copiedNodesAreCut) {
      copiedNodes.clear();
    }

    controller.mutateWorkspaceTree(
        msg, new ApiCallback<EmptyMessage>() {

          @Override
          public void onMessageReceived(EmptyMessage message) {

            // Do nothing. We lean on the multicasted COPY to have the action
            // update our local model.
          }

          @Override
          public void onFail(FailureReason reason) {
            // Do nothing.
          }
        });
  }

  /**
   * Renames the specified node via an inline edit UI.
   *
   *  In the event of a failure on the FE, the appropriate action is to restore the previous name of
   * the node.
   */
  public void handleRename(TreeNodeElement<FileTreeNode> renamedNode) {

    // We hang on to the old name in case we need to roll back the rename.
    FileTreeNode data = renamedNode.getData();
    final String oldName = data.getName();
    final PathUtil oldPath = data.getNodePath();

    // Go into "rename node" mode.
    setMode(ContextMenuMode.RENAME);
    nodeLabelMutator.enterMutation(renamedNode, new LabelRenamerCallback<FileTreeNode>() {
      @Override
      public void onCommit(String oldLabel, final TreeNodeElement<FileTreeNode> node) {
        if (invalidNameTooltip != null) {
          // if we were showing a tooltip related to the rename, hide it now
          invalidNameTooltip.destroy();
          invalidNameTooltip = null;
        }

        // If the name didn't change. Do nothing.
        if (oldLabel.equals(node.getData().getName())) {
          setMode(ContextMenuMode.READY);
          return;
        }

        // The node should have been renamed in the UI. This is where we
        // send a message to the frontend.
        WorkspaceTreeUpdate msg = fileTreeModel.makeEmptyTreeUpdate();
        msg.getMutations().add(FileTreeUtils.makeMutation(
            Mutation.Type.MOVE, oldPath, node.getData().getNodePath(), node.getData().isDirectory(),
            node.getData().getFileEditSessionKey()));

        controller.mutateWorkspaceTree(
            msg, new ApiCallback<EmptyMessage>() {

              @Override
              public void onFail(FailureReason reason) {

                // TODO: Differentiate between a server mutation problem
                // and some other failure. If the mutation succeeded,
                // this revert might be overzealous since the change
                // could have been applied, but timeout or something
                // afterwards. This is a rare corner case though.

                // Roll back!
                nodeLabelMutator.mutateNodeKey(node, oldName);
              }

              @Override
              public void onMessageReceived(EmptyMessage message) {
                // Notification of tree mutation will come via Tango.
                // If this file was open, EditorReloadingFileTreeListener will
                // ensure that the editor now points to the new path.

              }
            });

        setMode(ContextMenuMode.READY);
      }

      @Override
      public boolean passValidation(TreeNodeElement<FileTreeNode> node, String newLabel) {
        if (newLabel.equals(node.getData().getName())) {
          return true;
        }

        return notifyIfNameNotValid(node, newLabel);
      }
    });
  }

  private void handleViewFile(TreeNodeElement<FileTreeNode> node) {
    applicationRunner.runApplication(node.getData().getNodePath());
  }

  /**
   * Shows the context menu at the specified X and Y coordinates, for a given {@link FileTreeNode}.
   */
  public void show(int mouseX, int mouseY, TreeNodeElement<FileTreeNode> node) {
    if (fileTreeModel.getWorkspaceRoot() == null) {
      return;
    }
    selectedNode = node;

    if (isReadOnly) {
      if (node == null) {
        contextDropdownController.setItems(readonlyRootMenuItems);
      } else if (node.getData().isDirectory()) {
        contextDropdownController.setItems(readonlyDirMenuItems);
      } else {
        contextDropdownController.setItems(readonlyFileMenuItems);
      }
    } else {
      if (node == null) {
        contextDropdownController.setItems(rootMenuItems);
      } else if (node.getData().isDirectory()) {
        contextDropdownController.setItems(dirMenuItems);
      } else {
        contextDropdownController.setItems(fileMenuItems);
      }
    }
    contextDropdownController.showAtPosition(mouseX, mouseY);
  }

  /**
   * Sends a {@link WorkspaceTreeUpdate} message for an ADD mutation to the frontend to be
   * broadcasted to all clients.
   *
   *  Additions create a placeholder node in order to obtain a name for the new node via inline
   * editing of the node. In the event of a failure, we simply need to pop the added node out of the
   * tree.
   */
  private void broadcastAdd(final FileTreeNode installedNode) {

    TreeNodeElement<FileTreeNode> placeholderNode = installedNode.getRenderedTreeNode();

    assert (placeholderNode != null) : "Placeholder node was not allocated for newly added node: "
        + installedNode.getName();

    // Go into "rename node" mode.
    nodeLabelMutator.enterMutation(placeholderNode, new LabelRenamerCallback<FileTreeNode>() {
      @Override
      public void onCommit(String oldLabel, final TreeNodeElement<FileTreeNode> node) {
        if (invalidNameTooltip != null) {
          // if we were showing a tooltip related to the rename, hide it now
          invalidNameTooltip.destroy();
          invalidNameTooltip = null;
        }

        WorkspaceTreeUpdate msg = fileTreeModel.makeEmptyTreeUpdate();
        msg.getMutations().add(FileTreeUtils.makeMutation(Mutation.Type.ADD, null,
            installedNode.getNodePath(), installedNode.isDirectory(), null));

        controller.mutateWorkspaceTree(
            msg, new ApiCallback<EmptyMessage>() {

              @Override
              public void onFail(FailureReason reason) {

                // TODO: Differentiate between a server mutation problem
                // and some other failure. If the mutation succeeded,
                // this revert might be overzealous since the change
                // could have been applied, but timeout or something
                // afterwards. This is a rare corner case though.

                // Roll back! Pop the node out of the tree since the add failed
                // on the FE. Note that the node that was added was "optimistic"
                // and would not have been able to bump the tracked tip ID, so
                // just pass in the existing one.
                fileTreeModel.removeNode(
                    installedNode, fileTreeModel.getLastAppliedTreeMutationRevision());
              }

              @Override
              public void onMessageReceived(EmptyMessage message) {

                // Notification will come via Tango and ignored. Rerender the
                // parent directory so that we can get sort order.
                fileTreeUiController.reRenderSubTree(node.getData().getParent());

                fileTreeUiController.autoExpandAndSelectNode(node.getData(), true);
              }
            });
      }

      @Override
      public boolean passValidation(TreeNodeElement<FileTreeNode> node, String newLabel) {
        return notifyIfNameNotValid(node, newLabel);
      }
    });
  }

  private FileTreeNode getDirData(TreeNodeElement<FileTreeNode> parentDirNode) {
    return (parentDirNode == null) ? fileTreeModel.getWorkspaceRoot() : parentDirNode.getData();
  }

  private void handleNewNode(TreeNodeElement<FileTreeNode> parentTreeNode, FileTreeNode parentData,
      FileTreeNode newNodeData) {
    // Add a node that we will then open a label renamer for. We don't want the
    // model to synchronously update any model listeners since we want to wait
    // until the label rename action succeeds.
    fileTreeModel.setDisableChangeNotifications(true);
    try {

      // This is an optimistic addition. We cannot bump tracked tip, so just
      // pass in the existing one.
      // TODO: Add some affordance to FileTreeNode so that we can
      // know when a node has not yet been committed.
      fileTreeModel.addNode(
          parentData, newNodeData, fileTreeModel.getLastAppliedTreeMutationRevision());
    } finally {
      fileTreeModel.setDisableChangeNotifications(false);
    }

    // If we are adding to the root node, then we need to simply append nodes to
    // the tree's root container.
    if (parentTreeNode == null) {
      Tree<FileTreeNode> tree = fileTreeUiController.getTree();
      TreeNodeElement<FileTreeNode> newRenderedNode = tree.createNode(newNodeData);
      tree.getView().getElement().appendChild(newRenderedNode);
    } else {

      // Open the parent node which should create the rendered placeholder for
      // the new node.
      parentData.invalidateUnifiedChildrenCache();
      fileTreeUiController.expandNode(parentTreeNode);
    }

    broadcastAdd(newNodeData);
  }

  private void createMenuItems() {
    FileTreeMenuItem newFile = new FileTreeMenuItem() {
      @Override
      public void onClicked(TreeNodeElement<FileTreeNode> node) {
        handleNewFile(node);
      }

      @Override
      public String toString() {
        return "New File";
      }
    };

    FileTreeMenuItem newFolder = new FileTreeMenuItem() {
      @Override
      public void onClicked(TreeNodeElement<FileTreeNode> node) {
        handleNewFolder(node);
      }

      @Override
      public String toString() {
        return "New Folder";
      }
    };

    // Check if the cut menu option is enabled.
    FileTreeMenuItem cut = null;
    if (BrowserUtils.hasUrlParameter(FILE_TREE_CUT_URL_PARAM, "t")) {
      cut = new FileTreeMenuItem() {
        @Override
        public void onClicked(TreeNodeElement<FileTreeNode> node) {
          handleCopy(node, true);
        }

        @Override
        public String toString() {
          return "Cut";
        }
      };
    }

    FileTreeMenuItem copy = new FileTreeMenuItem() {
      @Override
      public void onClicked(TreeNodeElement<FileTreeNode> node) {
        handleCopy(node, false);
      }

      @Override
      public String toString() {
        return "Copy";
      }
    };

    FileTreeMenuItem rename = new FileTreeMenuItem() {
      @Override
      public void onClicked(TreeNodeElement<FileTreeNode> node) {
        handleRename(node);
      }

      @Override
      boolean isDisabled() {
        return fileTreeUiController.getTree().getSelectionModel().getSelectedNodes().size() > 1;
      }

      @Override
      public String toString() {
        return "Rename";
      }
    };

    FileTreeMenuItem delete = new FileTreeMenuItem() {
      @Override
      public void onClicked(TreeNodeElement<FileTreeNode> node) {
        handleDelete(node);
      }

      @Override
      public String toString() {
        return "Delete";
      }
    };

    FileTreeMenuItem viewFile = new FileTreeMenuItem() {
      @Override
      public void onClicked(TreeNodeElement<FileTreeNode> node) {
        handleViewFile(node);
      }

      @Override
      public String toString() {
        return "View in Browser";
      }
    };

    FileTreeMenuItem paste = new FileTreeMenuItem() {
      @Override
      public void onClicked(TreeNodeElement<FileTreeNode> node) {
        handlePaste(node);
      }

      @Override
      boolean isDisabled() {
        return copiedNodes.isEmpty();
      }

      @Override
      public String toString() {
        return "Paste";
      }
    };

    FileTreeMenuItem folderAsZip = new FileTreeMenuItem() {
      @Override
      public void onClicked(TreeNodeElement<FileTreeNode> node) {
        handleDownload(node, true);
      }

      @Override
      public String toString() {
        return "Download Folder as a Zip";
      }
    };

    FileTreeMenuItem branchAsZip = new FileTreeMenuItem() {
      @Override
      public void onClicked(TreeNodeElement<FileTreeNode> node) {
        handleDownload(node, true);
      }

      @Override
      public String toString() {
        return "Download Branch as a Zip";
      }
    };

    FileTreeMenuItem download = new FileTreeMenuItem() {
      @Override
      public void onClicked(TreeNodeElement<FileTreeNode> node) {
        handleDownload(node, false);
      }

      @Override
      public String toString() {
        return "Download";
      }
    };

    final PathUtil rootPath = PathUtil.EMPTY_PATH;
    FileTreeMenuItem uploadFile = new FileTreeMenuItem() {
      @Override
      public void onClicked(TreeNodeElement<FileTreeNode> node) {
        place.fireEvent(new UploadClickedEvent(UploadType.FILE, node == null ? rootPath
            : node.getData().getNodePath()));
      }

      @Override
      public String toString() {
        return "Upload File";
      }
    };

    FileTreeMenuItem uploadZip = new FileTreeMenuItem() {
      @Override
      public void onClicked(TreeNodeElement<FileTreeNode> node) {
        place.fireEvent(new UploadClickedEvent(UploadType.ZIP, node == null ? rootPath
            : node.getData().getNodePath()));
      }

      @Override
      public String toString() {
        return "Upload and Extract Zip";
      }
    };

    FileTreeMenuItem uploadFolder = new FileTreeMenuItem() {
      @Override
      public void onClicked(TreeNodeElement<FileTreeNode> node) {
        place.fireEvent(new UploadClickedEvent(UploadType.DIRECTORY, node == null ? rootPath
            : node.getData().getNodePath()));
      }

      @Override
      public String toString() {
        return "Upload Folder";
      }
    };

    FileTreeMenuItem newTab = new FileTreeMenuItem() {
      @Override
      public void onClicked(TreeNodeElement<FileTreeNode> node) {
        String link = WorkspaceUtils.createDeepLinkToFile(node.getData().getNodePath());
        Window.open(link, node.getData().getName(), null);
      }

      @Override
      public String toString() {
        return "Open in New Tab";
      }
    };

    rootMenuItems.add(newFile);
    rootMenuItems.add(newFolder);
    rootMenuItems.add(paste);
    rootMenuItems.add(uploadFile);
    rootMenuItems.add(uploadFolder);
    rootMenuItems.add(uploadZip);
    rootMenuItems.add(branchAsZip);

    dirMenuItems.add(newFile);
    dirMenuItems.add(newFolder);
    if (cut != null) {
      dirMenuItems.add(cut);
    }
    dirMenuItems.add(copy);
    dirMenuItems.add(paste);
    dirMenuItems.add(rename);
    dirMenuItems.add(delete);
    dirMenuItems.add(uploadFile);
    dirMenuItems.add(uploadFolder);
    dirMenuItems.add(uploadZip);
    dirMenuItems.add(folderAsZip);

    if (cut != null) {
      fileMenuItems.add(cut);
    }
    fileMenuItems.add(copy);
    fileMenuItems.add(viewFile);
    fileMenuItems.add(newTab);
    fileMenuItems.add(rename);
    fileMenuItems.add(delete);
    fileMenuItems.add(download);

    // Read Only Variety
    readonlyRootMenuItems.add(branchAsZip);

    readonlyDirMenuItems.add(folderAsZip);

    readonlyFileMenuItems.add(viewFile);
    readonlyFileMenuItems.add(newTab);
    readonlyFileMenuItems.add(download);


    allMenuItems.add(newFile);
    allMenuItems.add(newFolder);
    if (cut != null) {
      allMenuItems.add(cut);
    }
    allMenuItems.add(copy);
    allMenuItems.add(paste);
    allMenuItems.add(rename);
    allMenuItems.add(delete);
    allMenuItems.add(uploadFile);
    allMenuItems.add(uploadFolder);
    allMenuItems.add(uploadZip);
    allMenuItems.add(folderAsZip);
    allMenuItems.add(branchAsZip);
    allMenuItems.add(download);
  }

  /**
   * Create the context menu.
   */
  private void installContextMenu(DropdownWidgets.Resources res) {

    DropdownController.Listener<FileTreeMenuItem> listener =
        new DropdownController.BaseListener<FileTreeMenuItem>() {
