// 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 com.google.collide.client.code.debugging;

import collide.client.treeview.TreeNodeElement;
import collide.client.treeview.TreeNodeLabelRenamer;
import collide.client.treeview.TreeNodeMutator;

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.HorizontalAlign;
import com.google.collide.client.ui.menu.PositionController.Positioner;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.util.JsonCollections;
import com.google.collide.shared.util.StringUtils;

import elemental.dom.Element;

/**
 * Handles {@link RemoteObjectTree} tree context menu actions.
 *
 */
class RemoteObjectTreeContextMenuController {

  /**
   * Callbacks on the context menu actions.
   */
  interface Listener {
    void onAddNewChild(TreeNodeElement<RemoteObjectNode> node);
    void onNodeEdited(TreeNodeElement<RemoteObjectNode> node, String newLabel);
    void onNodeDeleted(TreeNodeElement<RemoteObjectNode> node);
    void onNodeRenamed(TreeNodeElement<RemoteObjectNode> node, String oldLabel);
  }

  /**
   * A menu item in the context menu.
   */
  private interface TreeNodeMenuItem {
    public void onClicked(TreeNodeElement<RemoteObjectNode> node);

    @Override
    public String toString();
  }

  /**
   * Renderer for an item in the context menu.
   */
  class TreeItemRenderer extends ListItemRenderer<TreeNodeMenuItem> {
    @Override
    public void render(Element listItemBase, TreeNodeMenuItem item) {
      listItemBase.setTextContent(item.toString());
    }
  }

  static RemoteObjectTreeContextMenuController create(DropdownWidgets.Resources resources,
      DebuggerState debuggerState, RemoteObjectNodeRenderer nodeRenderer,
      TreeNodeLabelRenamer<RemoteObjectNode> nodeLabelMutator) {

    RemoteObjectTreeContextMenuController controller =
        new RemoteObjectTreeContextMenuController(debuggerState, nodeRenderer, nodeLabelMutator);

    controller.installContextMenuController(resources);

    return controller;
  }

  private final DebuggerState debuggerState;
  private DropdownController<TreeNodeMenuItem> contextDropdownController;
  private final RemoteObjectNodeRenderer nodeRenderer;
  private final TreeNodeLabelRenamer<RemoteObjectNode> nodeLabelMutator;
  private Listener listener;

  private TreeNodeElement<RemoteObjectNode> selectedNode;
  private TreeItemRenderer renderer;

  private TreeNodeMenuItem menuRename;
  private TreeNodeMenuItem menuDelete;
  private TreeNodeMenuItem menuAdd;
  private TreeNodeMenuItem menuEdit;

  private RemoteObjectTreeContextMenuController(DebuggerState debuggerState,
      RemoteObjectNodeRenderer nodeRenderer,
      TreeNodeLabelRenamer<RemoteObjectNode> nodeLabelMutator) {
    this.debuggerState = debuggerState;
    this.nodeRenderer = nodeRenderer;
    this.nodeLabelMutator = nodeLabelMutator;

    this.renderer = new TreeItemRenderer();
    createMenuItems();
  }

  void setListener(Listener listener) {
    this.listener = listener;
  }

  private void installContextMenuController(DropdownWidgets.Resources res) {
    DropdownController.Listener<TreeNodeMenuItem> listener =
        new DropdownController.BaseListener<TreeNodeMenuItem>() {
          @Override
          public void onItemClicked(TreeNodeMenuItem item) {
            item.onClicked(selectedNode);
          }
        };

    Positioner mousePositioner = new DropdownPositionerBuilder().setHorizontalAlign(
        HorizontalAlign.RIGHT).buildMousePositioner();
    contextDropdownController = new DropdownController.Builder<TreeNodeMenuItem>(
        mousePositioner, null, res, listener, renderer).setShouldAutoFocusOnOpen(true).build();
  }

  void show(int mouseX, int mouseY, TreeNodeElement<RemoteObjectNode> nodeElement) {
    selectedNode = nodeElement;
    if (nodeElement == null) {
      return;
    }

    RemoteObjectNode node = nodeElement.getData();
    JsonArray<TreeNodeMenuItem> menuItems = JsonCollections.createArray();

    if (canAdd(node)) {
      menuItems.add(menuAdd);
    }
    if (canEdit(node)) {
      menuItems.add(menuEdit);
    }
    if (canRenameAndDelete(node)) {
      menuItems.add(menuRename);
      menuItems.add(menuDelete);
    }

    if (!menuItems.isEmpty()) {
      contextDropdownController.setItems(menuItems);
      contextDropdownController.showAtPosition(mouseX, mouseY);
    } else {
      contextDropdownController.hide();
    }
  }

  void hide() {
    contextDropdownController.hide();
  }

  Element getContextMenuElement() {
    return contextDropdownController.getElement();
  }

  private boolean canAdd(RemoteObjectNode node) {
    return node.canAddRemoteObjectProperty();
  }

  private boolean canEdit(RemoteObjectNode node) {
    return node.isWritable()
        && debuggerState.isActive()
        && (node.isRootChild() || isRealRemoteObject(node.getParent()));
  }

  private boolean canRenameAndDelete(RemoteObjectNode node) {
    return node.isDeletable()
        && (node.isRootChild() || isRealRemoteObject(node.getParent()));
  }

  private static boolean isRealRemoteObject(RemoteObjectNode node) {
    return node != null && node.getRemoteObject() != null && !node.isTransient();
  }

  private void enterAddProperty(TreeNodeElement<RemoteObjectNode> node) {
    if (listener != null) {
      listener.onAddNewChild(node);
    }
  }

  boolean enterEditPropertyValue(TreeNodeElement<RemoteObjectNode> nodeToEdit) {
    if (!canEdit(nodeToEdit.getData())) {
      return false;
    }
    nodeLabelMutator.cancel();
    nodeLabelMutator.getTreeNodeMutator().enterMutation(
        nodeToEdit, new TreeNodeMutator.MutationAction<RemoteObjectNode>() {
          @Override
          public Element getElementForMutation(TreeNodeElement<RemoteObjectNode> node) {
            return nodeRenderer.getPropertyValueElement(node.getNodeLabel());
          }

          @Override
          public void onBeforeMutation(TreeNodeElement<RemoteObjectNode> node) {
            nodeRenderer.enterPropertyValueMutation(node.getNodeLabel());
          }

          @Override
          public void onMutationCommit(
              TreeNodeElement<RemoteObjectNode> node, String oldLabel, String newLabel) {
            String trimmedLabel = StringUtils.trimNullToEmpty(newLabel);
            nodeRenderer.exitPropertyValueMutation(node.getNodeLabel(), trimmedLabel);

            if (listener != null && !StringUtils.equalStringsOrEmpty(oldLabel, trimmedLabel)) {
              listener.onNodeEdited(node, trimmedLabel);
            }
          }

          @Override
          public boolean passValidation(TreeNodeElement<RemoteObjectNode> node, String newLabel) {
            return !StringUtils.isNullOrWhitespace(newLabel);
          }
        });
    return true;
  }

  boolean enterRenameProperty(TreeNodeElement<RemoteObjectNode> nodeToRename) {
    if (!canRenameAndDelete(nodeToRename.getData())) {
      return false;
    }
    nodeLabelMutator.cancel();
    nodeLabelMutator.enterMutation(
        nodeToRename, new TreeNodeLabelRenamer.LabelRenamerCallback<RemoteObjectNode>() {
          @Override
          public void onCommit(String oldLabel, TreeNodeElement<RemoteObjectNode> node) {
            if (listener != null && !oldLabel.equals(node.getData().getName())) {
              listener.onNodeRenamed(node, oldLabel);
            }
          }

          @Override
          public boolean passValidation(TreeNodeElement<RemoteObjectNode> node, String newLabel) {
            return !StringUtils.isNullOrWhitespace(newLabel);
          }
        });
    return true;
  }

  private void enterDeleteProperty(TreeNodeElement<RemoteObjectNode> node) {
    if (listener != null) {
      listener.onNodeDeleted(node);
    }
  }

  private void createMenuItems() {
    menuRename = new TreeNodeMenuItem() {
      @Override
      public void onClicked(TreeNodeElement<RemoteObjectNode> node) {
        enterRenameProperty(node);
      }

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

    menuDelete = new TreeNodeMenuItem() {
      @Override
      public void onClicked(TreeNodeElement<RemoteObjectNode> node) {
        enterDeleteProperty(node);
      }

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

    menuAdd = new TreeNodeMenuItem() {
      @Override
      public void onClicked(TreeNodeElement<RemoteObjectNode> node) {
        enterAddProperty(node);
      }

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

    menuEdit = new TreeNodeMenuItem() {
      @Override
      public void onClicked(TreeNodeElement<RemoteObjectNode> node) {
        enterEditPropertyValue(node);
      }

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