/*
 * Copyright 2000-2012 JetBrains s.r.o.
 *
 * 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.intellij.ui.treeStructure;

import com.intellij.ide.util.treeView.AbstractTreeBuilder;
import com.intellij.ide.util.treeView.NodeRenderer;
import com.intellij.ide.util.treeView.TreeVisitor;
import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionPopupMenu;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.ui.TreeUIHelper;
import com.intellij.util.ui.EmptyIcon;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.tree.TreeUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.plaf.basic.BasicTreeUI;
import javax.swing.text.Position;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;

public class SimpleTree extends Tree implements CellEditorListener {
  private static final SimpleNode NULL_NODE = new NullNode();
  private static final int INVALID = -1;

  private ActionGroup myPopupGroup;
  private String myPlace;

  private JComponent myEditorComponent;
  private boolean myEscapePressed;
  private int myEditingRow;
  private boolean myIgnoreSelectionChange;

  private int myMinHeightInRows = 5;

  private Icon myExpandedHandle;
  private Icon myCollapsedHandle;
  private Icon myEmptyHandle;

  public SimpleTree() {
    setModel(new DefaultTreeModel(new PatchedDefaultMutableTreeNode()));
    TreeUtil.installActions(this);

    configureUiHelper(TreeUIHelper.getInstance());

    addMouseListener(new MyMouseListener());
    setCellRenderer(new NodeRenderer());

    setEditable(false);
    getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
    ToolTipManager.sharedInstance().registerComponent(this);

    getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() {
      public void valueChanged(TreeSelectionEvent e) {
        if (!myIgnoreSelectionChange && hasSingleSelection()) {
          getNodeFor(getSelectionPath()).handleSelection(SimpleTree.this);
        }
      }
    });

    addKeyListener(new KeyAdapter() {
      public void keyPressed(KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_ENTER && hasSingleSelection()) {
          handleDoubleClickOrEnter(getSelectionPath(), e);
        }
        if (e.getKeyCode() == KeyEvent.VK_F2 && e.getModifiers() == 0) {
          e.consume(); // ignore start editing by F2
        }
      }
    });

    UIUtil.setLineStyleAngled(this);
    if (SystemInfo.isWindows && !SystemInfo.isWinVistaOrNewer) {
      setUI(new BasicTreeUI());   // In WindowsXP UI handles are not shown :(
    }

    setOpaque(UIUtil.isUnderGTKLookAndFeel());
  }

  public SimpleTree(TreeModel aModel) {
    this();
    setModel(aModel);
  }

  protected void configureUiHelper(final TreeUIHelper helper) {
    helper.installTreeSpeedSearch(this);
  }

  @Override
  public TreePath getNextMatch(final String prefix, final int startingRow, final Position.Bias bias) {
    // turn JTree Speed Search off
    return null;
  }

  public boolean accept(AbstractTreeBuilder builder, final SimpleNodeVisitor visitor) {
    return builder.accept(SimpleNode.class, new TreeVisitor<SimpleNode>() {
      public boolean visit(@NotNull SimpleNode node) {
        return visitor.accept(node);
      }
    }) != null;
  }

  public void setPopupGroup(ActionGroup aPopupGroup, String aPlace) {
    myPopupGroup = aPopupGroup;
    myPlace = aPlace;
  }

  public SimpleNode getNodeFor(int row) {
    return getNodeFor(getPathForRow(row));
  }

  public SimpleNode getNodeFor(TreePath aPath) {
    if (aPath == null) {
      return NULL_NODE;
    }

    DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode)aPath.getLastPathComponent();
    if (treeNode == null) {
      return NULL_NODE;
    }

    final Object userObject = treeNode.getUserObject();
    if (userObject instanceof SimpleNode) {
      return (SimpleNode)userObject;
    }
    else {
      return NULL_NODE;
    }
  }

  @Nullable
  public TreePath getPathFor(SimpleNode node) {
    final TreeNode nodeWithObject = TreeUtil.findNodeWithObject((DefaultMutableTreeNode)getModel().getRoot(), node);
    if (nodeWithObject != null) {
      return TreeUtil.getPathFromRoot(nodeWithObject);
    }
    return null;
  }

  @Nullable
  public SimpleNode getSelectedNode() {
    if (isSelectionEmpty()) {
      return null;
    }

    return getNodeFor(getSelectionPath());
  }

  public boolean isSelectionEmpty() {
    final TreePath selection = super.getSelectionPath();
    return selection == null || getNodeFor(selection) == NULL_NODE;

  }

  public SimpleNode[] getSelectedNodesIfUniform() {
    List<SimpleNode> result = new ArrayList<>();
    TreePath[] selectionPaths = getSelectionPaths();
    if (selectionPaths != null) {
      SimpleNode lastNode = null;
      for (TreePath selectionPath : selectionPaths) {
        SimpleNode nodeFor = getNodeFor(selectionPath);

        if (lastNode != null && lastNode.getClass() != nodeFor.getClass()) {
          return new SimpleNode[0];
        }

        result.add(nodeFor);
        lastNode = nodeFor;
      }
    }
    return result.toArray(new SimpleNode[result.size()]);
  }

  public void setSelectedNode(AbstractTreeBuilder builder, SimpleNode node, boolean expand) {
    builder.select(node.getElement(), null, false);
  }

  protected void paintChildren(Graphics g) {
    super.paintChildren(g);
    g.setColor(UIManager.getColor("Tree.line"));

    for (int row = 0; row < getRowCount(); row++) {
      final TreePath path = getPathForRow(row);
      if (!getNodeFor(path).shouldHaveSeparator()) {
        continue;
      }

      final Rectangle bounds = getRowBounds(row);
      int x = (int)bounds.getMaxX();
      int y = (int)(bounds.getY() + bounds.height / 2);
      g.drawLine(x, y, getWidth() - 5, y);
    }
  }

  public void doClick(int row) {
    setSelectionRow(row);
  }

  // From FTree:

  public void cancelEditing() {
    if (isEditing()) {
      cellEditor.cancelCellEditing();
      doStopEditing();
    }
  }

  public void editingStopped(ChangeEvent e) {
    doStopEditing();
  }

  public void editingCanceled(ChangeEvent e) {
    doStopEditing();
  }

  public JComponent getEditorComponent() {
    return myEditorComponent;
  }

  public boolean isEditing() {
    return myEditorComponent != null;
  }

  public TreePath getEditingPath() {
    if (isEditing()) {
      return getPathForRow(myEditingRow);
    }
    return super.getEditingPath();
  }

  public boolean isPathEditable(TreePath path) {
    return true;
  }

  @Override
  public final boolean isFileColorsEnabled() {
    return false;
  }

  @Override
  protected boolean paintNodes() {
    return true;
  }

  protected void paintComponent(Graphics g) {
    super.paintComponent(g);

    if (isEditing()) {
      Rectangle editedNodeRect = getRowBounds(myEditingRow);
      if (editedNodeRect == null) return;
      g.setColor(getBackground());
      g.fillRect(editedNodeRect.x, editedNodeRect.y, editedNodeRect.width, editedNodeRect.height);
    }
  }

  public void setCellEditor(TreeCellEditor aCellEditor) {
    if (cellEditor != null) {
      cellEditor.removeCellEditorListener(this);
    }
    super.setCellEditor(aCellEditor);

    if (cellEditor != null) {
      cellEditor.addCellEditorListener(this);
    }
  }

  public boolean stopEditing() {
    boolean result = isEditing();
    if (result) {
      if (!cellEditor.stopCellEditing()) {
        cellEditor.cancelCellEditing();
      }
      doStopEditing();
    }
    return result;
  }

  public void startEditingAtPath(final TreePath path) {
    if (path != null && isVisible(path)) {

      if (isEditing() && !stopEditing()) {
        return;
      }

      startEditing(path);
    }
  }

  private void startEditing(final TreePath path) {

    CellEditor editor = getCellEditor();
    if (editor != null && editor.isCellEditable(null) && isPathEditable(path)) {

      getSelectionModel().clearSelection();
      getSelectionModel().setSelectionPath(path);

      myEditingRow = getRowForPath(path);
      myEditorComponent = (JComponent)getCellEditor()
        .getTreeCellEditorComponent(this, path.getLastPathComponent(), isPathSelected(path), isExpanded(path),
                                    treeModel.isLeaf(path.getLastPathComponent()), myEditingRow);

      putEditor(path);

      if (myEditorComponent.isFocusable()) {
        myEditorComponent.requestFocusInWindow();
      }

      SwingUtilities.invokeLater(() -> scrollPathToVisible(path));
    }
  }

  private void putEditor(TreePath path) {

    add(myEditorComponent);
    Rectangle nodeBounds = getPathBounds(path);
    Dimension editorPrefSize = myEditorComponent.getPreferredSize();
    if (editorPrefSize.height > nodeBounds.height) {
      nodeBounds.y -= (editorPrefSize.height - nodeBounds.height) / 2;
      nodeBounds.height = editorPrefSize.height;
    }

    myEditorComponent.setBounds(nodeBounds);

    myEscapePressed = false;
  }

  private void doStopEditing() {
    if (isEditing()) {
      remove(myEditorComponent);
      myEditorComponent = null;
      setSelectionRow(myEditingRow);
      myEditingRow = INVALID;
      repaint();
    }
  }

  public boolean isEscapePressed() {
    return myEscapePressed;
  }

  public void setEscapePressed() {
    myEscapePressed = true;
  }

  public void addSelectionPath(TreePath path) {
    myIgnoreSelectionChange = true;
    super.addSelectionPath(path);
    myIgnoreSelectionChange = false;
  }

  public void addSelectionPaths(TreePath[] path) {
    myIgnoreSelectionChange = true;
    super.addSelectionPaths(path);
    myIgnoreSelectionChange = false;
  }

  private boolean isSelected(TreePath path) {
    TreePath[] selectionPaths = getSelectionPaths();
    if (selectionPaths != null) {
      for (TreePath selectionPath : selectionPaths) {
        if (path.equals(selectionPath)) {
          return true;
        }
      }
    }
    return false;
  }

  public boolean isMultipleSelection() {
    return getSelectionRows() != null && getSelectionRows().length > 1;
  }

  private void handleDoubleClickOrEnter(final TreePath treePath, final InputEvent e) {
    Runnable runnable = () -> getNodeFor(treePath).handleDoubleClickOrEnter(this, e);
    ApplicationManager.getApplication().invokeLater(runnable, ModalityState.stateForComponent(this));
  }

  // TODO: move to some util?
  public static boolean isDoubleClick(MouseEvent e) {
    return e != null && e.getClickCount() > 0 && e.getClickCount() % 2 == 0;
  }

  protected ActionGroup getPopupGroup() {
    return myPopupGroup;
  }

  protected void invokeContextMenu(final MouseEvent e) {
    SwingUtilities.invokeLater(() -> {
      final ActionPopupMenu menu = ActionManager.getInstance().createActionPopupMenu(myPlace, myPopupGroup);
      menu.getComponent().show(e.getComponent(), e.getPoint().x, e.getPoint().y);
    });
  }

  private class MyMouseListener extends MouseAdapter {
    public void mousePressed(MouseEvent e) {
      if (e.isPopupTrigger()) {
        invokePopup(e);
      }
      else if (isDoubleClick(e)) {
        handleDoubleClickOrEnter(getClosestPathForLocation(e.getX(), e.getY()), e);
        /*
        if (!TreeWizardPopupImpl.isLocationInExpandControl(SimpleTree.this, getSelectionPath(), e.getX(), e.getY())) {
          TreePath treePath = getClosestPathForLocation(e.getX(), e.getY());
          handleDoubleClickOrEnter(treePath, e);
        }
        */
      }
    }

    public void mouseReleased(MouseEvent e) {
      invokePopup(e);
    }

    public void mouseClicked(MouseEvent e) {
      invokePopup(e);
    }

    private void invokePopup(final MouseEvent e) {
      if (e.isPopupTrigger() && insideTreeItemsArea(e)) {

        selectPathUnderCursorIfNeeded(e);

        if (myPopupGroup != null) {
          invokeContextMenu(e);
        }
      }
    }

    private void selectPathUnderCursorIfNeeded(final MouseEvent e) {
      TreePath pathForLocation = getClosestPathForLocation(e.getX(), e.getY());
      if (!isSelected(pathForLocation)) {
        setSelectionPath(pathForLocation);
      }
    }

    private boolean insideTreeItemsArea(MouseEvent e) {
      Rectangle rowBounds = getRowBounds(getRowCount() - 1);
      if (rowBounds == null) {
        return false;
      }
      double lastItemBottomLine = rowBounds.getMaxY();
      return e.getY() <= lastItemBottomLine;
    }
  }

  public boolean select(AbstractTreeBuilder aBuilder, final SimpleNodeVisitor aVisitor, boolean shouldExpand) {
    return aBuilder.select(SimpleNode.class, new TreeVisitor<SimpleNode>() {
      public boolean visit(@NotNull SimpleNode node) {
        return aVisitor.accept(node);
      }
    }, null, false);
  }

  private void debugTree(AbstractTreeBuilder aBuilder) {
    TreeUtil.traverseDepth((TreeNode)aBuilder.getTree().getModel().getRoot(), node -> {
      System.out.println("Node: " + node);
      return true;
    });
  }

  private boolean hasSingleSelection() {
    return !isSelectionEmpty() && getSelectionPaths().length == 1;
  }

  public DefaultTreeModel getBuilderModel() {
    return (DefaultTreeModel)getModel();
  }

  public Dimension getPreferredScrollableViewportSize() {
    return super.getPreferredSize();
  }

  public NodeRenderer getRenderer() {
    return (NodeRenderer)getCellRenderer();
  }

  public String toString() {
    return getClass().getName() + '#' + System.identityHashCode(this);
  }

  public final void setMinSizeInRows(int rows) {
    myMinHeightInRows = rows;
  }

  public Dimension getMinimumSize() {
    Dimension superSize = super.getMinimumSize();

    if (myMinHeightInRows == -1) return superSize;
    int rowCount = getRowCount();
    if (rowCount == 0) return superSize;

    double rowHeight = getRowBounds(0).getHeight();
    return new Dimension(superSize.width, (int)(rowHeight * myMinHeightInRows));
  }

  public final int getToggleClickCount() {
    SimpleNode node = getSelectedNode();
    if (node != null) {
      if (!node.expandOnDoubleClick()) return -1;
    }
    return super.getToggleClickCount();
  }

  @Override
  public void processKeyEvent(final KeyEvent e) {
    super.processKeyEvent(e);
  }

  private int getBoxWidth(TreePath path) {
    final Object root = getModel().getRoot();
    if (!isRootVisible()) {
      if (path.getPathCount() == 2) {
        final TreePath parent = path.getParentPath();
        if (parent.getLastPathComponent() == root && !getShowsRootHandles()) {
          return 0;
        }
      }
    }

    return getBoxWidth(this);
  }

  private static int getBoxWidth(JTree tree) {
    BasicTreeUI basicTreeUI = (BasicTreeUI)tree.getUI();
    int boxWidth;
    if (basicTreeUI.getExpandedIcon() != null) {
      boxWidth = basicTreeUI.getExpandedIcon().getIconWidth();
    }
    else {
      boxWidth = 8;
    }
    return boxWidth;
  }

  @Override
  public void updateUI() {
    super.updateUI();

    myExpandedHandle = null;
    myCollapsedHandle = null;
    myExpandedHandle = null;
  }

  public Icon getHandleIcon(DefaultMutableTreeNode node, TreePath path) {
    if (node.getChildCount() == 0) return getEmptyHandle();

    
    return isExpanded(path) ? getExpandedHandle() : getCollapsedHandle();

  }

  public Icon getExpandedHandle() {
    if (myExpandedHandle == null) {
      myExpandedHandle = UIUtil.getTreeExpandedIcon();
    }

    return myExpandedHandle;
  }

  public Icon getCollapsedHandle() {
    if (myCollapsedHandle == null) {
      myCollapsedHandle = UIUtil.getTreeCollapsedIcon();
    }

    return myCollapsedHandle;
  }

  public Icon getEmptyHandle() {
    if (myEmptyHandle == null) {
      final Icon expand = getExpandedHandle();
      myEmptyHandle = expand != null ? EmptyIcon.create(expand) : EmptyIcon.create(0);
    }

    return myEmptyHandle;
  }

}
