/*
 * Copyright 2003-2015 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 jetbrains.mps.ide.ui.tree;

import com.intellij.ide.dnd.aware.DnDAwareTree;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.ui.TreeUIHelper;
import com.intellij.util.ui.tree.WideSelectionTreeUI;
import com.intellij.util.ui.update.MergingUpdateQueue;
import com.intellij.util.ui.update.Update;
import jetbrains.mps.RuntimeFlags;
import jetbrains.mps.ide.ThreadUtils;
import jetbrains.mps.smodel.ModelAccess;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.AbstractAction;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;
import javax.swing.ToolTipManager;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeExpansionListener;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.plaf.TreeUI;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.ExpandVetoException;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

public abstract class MPSTree extends DnDAwareTree implements Disposable {
  public static final String PATH = "path";

  protected static final Logger LOG = LogManager.getLogger(MPSTree.class);

  public static final String TREE_PATH_SEPARATOR = "/";

  private int myTooltipManagerRecentInitialDelay;
  private boolean myAutoExpandEnabled = true;
  private boolean myAutoOpen = false;
  private boolean myLoadingDisabled;

  private Set<MPSTreeNode> myExpandingNodes = new HashSet<MPSTreeNode>();

  private List<MPSTreeNodeListener> myTreeNodeListeners = new ArrayList<MPSTreeNodeListener>();

  // todo: make unique name
  private MergingUpdateQueue myQueue = new MergingUpdateQueue("MPS Tree Rebuild Later Watcher Queue", 500, true, null);
  private final Object myUpdateId = new Object();

  private boolean myDisposed = false;

  protected MPSTree() {
    setRootNode(new TextTreeNode("Empty"));

    new MPSTreeSpeedSearch(this);

    ToolTipManager.sharedInstance().registerComponent(this);

    largeModel = true;

    TreeUIHelper.getInstance().installToolTipHandler(this);

    setCellRenderer(new NewMPSTreeCellRenderer());

    addTreeWillExpandListener(new MyTreeWillExpandListener());
    addTreeExpansionListener(new MyTreeExpansionListener());
    addMouseListener(new MyMouseAdapter());

    registerKeyboardAction(new MyOpenNodeAction(), KeyStroke.getKeyStroke("F4"), WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
    registerKeyboardAction(new MyRefreshAction(), KeyStroke.getKeyStroke("F5"), WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
  }

  /**
   * Initialization sequence common for each node initialized in the tree.
   * Shall invoke {@link MPSTreeNode#doInit()} to perform actual initialization, does this through appropriate runnable
   * supplied to {@link #doInit(MPSTreeNode, Runnable)}},  which is left protected for sub-classes
   * that may add extra utility stuff, like progress indication.
   * Primary responsibility is guards to prevent endless initialization
   */
  /*package*/ final void performInit(final MPSTreeNode node) {
    assert ThreadUtils.isInEDT();
    if (myExpandingNodes.contains(node)) {
      return;
    }

    myExpandingNodes.add(node);
    try {
      doInit(node, new Runnable() {
        @Override
        public void run() {
          node.doInit();
        }
      });
    } finally {
      myExpandingNodes.remove(node);
    }
  }

  /**
   * Extra initialization stuff, by default adds progress indication (conditional, myLoadingDisabled and node.isLoadingEnabled).
   * Sub-classes may override to provide extra initialization stuff (shall invoke nodeInitRunnable if do not delegate to super)
   * or to wrap nodeInitRunnable into proper access code (e.g. model read if tree node initialization might demand access to model).
   *
   * Note, if tree nodes do not initialize lazily with access to a data model, there's no need to override the method and to wrap nodeInitRunnable with
   * data model access control (e.g. model read)
   */
  protected void doInit(MPSTreeNode node, Runnable nodeInitRunnable) {
    TextTreeNode progressNode = null;
    if (!myLoadingDisabled && node.isLoadingEnabled()) {
      progressNode = new TextTreeNode("loading...");
      node.add(progressNode);
      ((DefaultTreeModel) getModel()).nodeStructureChanged(node);
      expandPath(new TreePath(progressNode.getPath()));

      Graphics g = getGraphics();
      if (g != null && g.getClipBounds() != null) {
        paint(g);
      }
    }

    nodeInitRunnable.run();

    if (!myLoadingDisabled && node.isLoadingEnabled() && node.hasChild(progressNode)) {
      // node initialization code (nodeInitRunnable, node.doInit()) may remove all the children
      node.remove(progressNode);
    }

    // initialization of a node is supposed to update its children, notify structure had likely changed
    ((DefaultTreeModel) getModel()).nodeStructureChanged(node);
  }

  public void addTreeNodeListener(MPSTreeNodeListener listener) {
    myTreeNodeListeners.add(listener);
  }

  public void removeTreeNodeListener(MPSTreeNodeListener listener) {
    myTreeNodeListeners.remove(listener);
  }

  public void fireBeforeTreeDisposed() {
    for (MPSTreeNodeListener listener : new HashSet<MPSTreeNodeListener>(myTreeNodeListeners)) {
      listener.beforeTreeDisposed(this);
    }
  }

  void fireTreeNodeUpdated(MPSTreeNode node) {
    for (MPSTreeNodeListener listener : new HashSet<MPSTreeNodeListener>(myTreeNodeListeners)) {
      listener.treeNodeUpdated(node, this);
    }
  }

  void fireTreeNodeAdded(MPSTreeNode node) {
    for (MPSTreeNodeListener listener : new HashSet<MPSTreeNodeListener>(myTreeNodeListeners)) {
      listener.treeNodeAdded(node, this);
    }
  }

  void fireTreeNodeRemoved(MPSTreeNode node) {
    for (MPSTreeNodeListener listener : new HashSet<MPSTreeNodeListener>(myTreeNodeListeners)) {
      listener.treeNodeRemoved(node, this);
    }
  }

  void myMouseReleased(MouseEvent e) {
    if (e.isPopupTrigger()) showPopup(e.getX(), e.getY());
  }

  void myMousePressed(final MouseEvent e) {
    IdeFocusManager.findInstanceByComponent(this).requestFocus(this, true);

    if (e.getButton() == 0) {
      // This is a workaround for handling context menu button
      TreePath path = getSelectionPath();
      if (path == null) return;
      int rowNum = getRowForPath(path);
      Rectangle r = getRowBounds(rowNum);
      showPopup(r.x, r.y);
      return;
    }

    if (e.isPopupTrigger()) {
      showPopup(e.getX(), e.getY());
      return;
    }

    TreePath path = getClosestPathForLocation(e.getX(), e.getY());
    if (path == null) return;

    MPSTreeNode nodeToClick = getOpenableNode(e);
    if (nodeToClick != null) {
      if ((e.getClickCount() == 1 && isAutoOpen())) {
        autoscroll(nodeToClick);
      }
    }
  }

  private void myMouseClicked(MouseEvent e) {
    if (e.isPopupTrigger()) {
      showPopup(e.getX(), e.getY());
      return;
    }

    MPSTreeNode nodeToClick = getOpenableNode(e);
    if (nodeToClick != null && e.getClickCount() == 2) {
      doubleClick(nodeToClick);
    }
  }

  private MPSTreeNode getOpenableNode(MouseEvent e) {
    MPSTreeNode node = getNodeFromPath(getClosestPathForLocation(e.getX(), e.getY()));

    if (node == null) return null;
    if (!node.canBeOpened()) return null;

    return node;
  }

  @Nullable
  private MPSTreeNode getNodeFromPath(@Nullable TreePath path) {
    if (path == null) return null;
    Object lastPathComponent = path.getLastPathComponent();

    if (!(lastPathComponent instanceof MPSTreeNode)) return null;
    return (MPSTreeNode) lastPathComponent;
  }

  /**
   * Gives owning tree a chance to process double-click event.
   * By default, delegates to {@link MPSTreeNode#doubleClick()}
   */
  protected void doubleClick(@NotNull MPSTreeNode nodeToClick) {
    nodeToClick.doubleClick();
  }

  /**
   * Single point to dispatch auto scrolling event.
   * By default, delegates to {@link MPSTreeNode#autoscroll()} ()}
   */
  protected void autoscroll(@NotNull MPSTreeNode nodeToClick) {
    nodeToClick.autoscroll();
  }

  // if we navigate to a node which is MPSTreeNode#isAutoExpandable() == true,
  // it's automatically expanded, unless myAutoExpandEnabled == false
  // XXX is there any case but select node in the runnable? Perhaps, could
  // get better api (e.g. selectWithoutExpansion(MPSTreeNode)?)
  public void runWithoutExpansion(Runnable r) {
    try {
      myAutoExpandEnabled = false;
      r.run();
    } finally {
      myAutoExpandEnabled = true;
    }
  }

  public boolean isAutoOpen() {
    return myAutoOpen;
  }

  public void setAutoOpen(boolean autoOpen) {
    myAutoOpen = autoOpen;
  }

  @Override
  public String getToolTipText(MouseEvent event) {
    TreePath path = getPathForLocation(event.getX(), event.getY());
    MPSTreeNode node = getNodeFromPath(path);
    if (node != null) {
      return node.getTooltipText();
    }
    return null;
  }

  protected JPopupMenu createDefaultPopupMenu() {
    return null;
  }

  protected final JPopupMenu createPopupMenu(final MPSTreeNode node) {
    ActionGroup actionGroup = createPopupActionGroup(node);
    if (actionGroup == null) return null;
    return ActionManager.getInstance().createActionPopupMenu(getPopupMenuPlace(), actionGroup).getComponent();
  }

  protected String getPopupMenuPlace() {
    return ActionPlaces.UNKNOWN;
  }

  protected ActionGroup createPopupActionGroup(final MPSTreeNode node) {
    return null;
  }

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

  private boolean isWideSelectionUI() {
    TreeUI ui = getUI();
    return ui instanceof WideSelectionTreeUI && ((WideSelectionTreeUI) ui).isWideSelection();
  }

  private void showPopup(int x, int y) {
    if (!insideTreeItemsArea(y)) {
      return;
    }

    TreePath closestPath = getClosestPathForLocation(x, y);
    TreePath path;
    if (isWideSelectionUI()) {
      path = closestPath;
    } else {
      path = getPathForLocation(x, y);
    }

    // Always select the closest path, even if the tree does not have wide selection, or the node doesn't have a menu.
    if (closestPath != null && !isPathSelected(closestPath)) {
      setSelectionPath(path);
    }

    final MPSTreeNode node = getNodeFromPath(path);
    if (node != null) {
      JPopupMenu menu = createPopupMenu(node);
      if (menu != null) {
        menu.show(this, x, y);
        return;
      }
    }

    JPopupMenu defaultMenu = createDefaultPopupMenu();
    if (defaultMenu == null) return;
    defaultMenu.show(this, x, y);
  }

  @Nullable
  public Comparator<Object> getChildrenComparator() {
    return null;
  }

  protected abstract MPSTreeNode rebuild();

  public void expandAll() {
    MPSTreeNode node = getRootNode();
    expandAll(node);
  }

  public void collapseAll() {
    MPSTreeNode node = getRootNode();
    collapseAll(node);
  }

  public void selectFirstLeaf() {
    List<MPSTreeNode> path = new ArrayList<MPSTreeNode>();
    MPSTreeNode current = getRootNode();

    while (true) {
      path.add(current);
      if (current.getChildCount() == 0) break;
      current = (MPSTreeNode) current.getChildAt(0);
    }

    setSelectionPath(new TreePath(path.toArray()));
  }

  public void expandRoot() {
    expandPath(new TreePath(new Object[]{getRootNode()}));
    getRootNode().init();
  }

  public void expandAll(MPSTreeNode node) {
    boolean wasLoadingDisabled = myLoadingDisabled;
    myLoadingDisabled = true;
    try {
      expandAllImpl(node);
    } finally {
      myLoadingDisabled = wasLoadingDisabled;
    }
  }
  private void expandAllImpl(MPSTreeNode node) {
    expandPath(new TreePath(node.getPath()));
    for (MPSTreeNode c : node) {
      expandAllImpl(c);
    }
  }

  public void collapseAll(MPSTreeNode node) {
    boolean wasAutoExpandEnabled = myAutoExpandEnabled;
    try {
      myAutoExpandEnabled = false;
      collapseAllImpl(node);
    } finally {
      myAutoExpandEnabled = wasAutoExpandEnabled;
    }
  }

  private void collapseAllImpl(MPSTreeNode node) {
    for (MPSTreeNode c : node) {
      collapseAllImpl(c);
    }
    if (node.isInitialized()) {
      super.collapsePath(new TreePath(node.getPath()));
    }
  }

  public void selectNode(TreeNode node) {
    List<TreeNode> nodes = new ArrayList<TreeNode>();
    while (node != null) {
      nodes.add(0, node);
      node = node.getParent();
    }
    if (nodes.size() == 0) return;
    TreePath path = new TreePath(nodes.toArray());
    setSelectionPath(path);
    scrollRowToVisible(getRowForPath(path));
  }

  // FIXME perhaps, shall be protected, rebuildAction is sort of implementation detail when there are rebuildNow() and rebuildLater()
  public void runRebuildAction(final Runnable rebuildAction, final boolean saveExpansion) {
    if (RuntimeFlags.isTestMode()) {
      return;
    }
    if (!ThreadUtils.isInEDT()) {
      throw new RuntimeException("Rebuild now can be only called from UI thread");
    }


    myLoadingDisabled = true;
    try {
      Runnable restoreExpansion = null;
      if (saveExpansion) {
        final List<String> expansion = getExpandedPaths();
        final List<String> selection = getSelectedPaths();
        restoreExpansion = new Runnable() {
          @Override
          public void run() {
            expandPaths(expansion);
            selectPaths(selection);
          }
        };
      }
      // debugger.GroupedTree assumes read action for rebuild() call
      ModelAccess.instance().runReadAction(rebuildAction);
      if (restoreExpansion != null) {
        runWithoutExpansion(restoreExpansion);
      }
    } finally {
      myLoadingDisabled = false;
    }
  }

  // XXX Rename to scheduleRebuild()?
  public void rebuildLater() {
    myQueue.queue(new SafeUpdate(myUpdateId, new Runnable() {
      @Override
      public void run() {
        // myQueue is attached to EDT
        if (MPSTree.this.isDisposed()) return;
        rebuildNow();
      }
    }, LOG));
  }

  public void rebuildNow() {
    if (!ThreadUtils.isInEDT()) {
      throw new RuntimeException("Rebuild now can be only called from UI thread");
    }
    assert !isDisposed() : "Trying to reconstruct disposed tree. Try finding \"later\" in stacktrace";

    runRebuildAction(new Runnable() {
      @Override
      public void run() {
        setAnchorSelectionPath(null);
        setLeadSelectionPath(null);

        MPSTreeNode root = rebuild();
        setRootNode(root);
      }
    }, true);
  }

  public void clear() {
    setRootNode(new TextTreeNode("Empty"));
  }

  private void setRootNode(@Nullable MPSTreeNode root) {
    final Object oldRoot = getModel().getRoot();
    if (oldRoot instanceof MPSTreeNode) {
      ((MPSTreeNode) oldRoot).removeThisAndChildren();
      ((MPSTreeNode) oldRoot).setTree(null);
    }

    if (root != null) {
      root.setTree(this);
      root.addThisAndChildren();
    }

    DefaultTreeModel model = new DefaultTreeModel(root);
    setModel(model);
  }

  private String pathToString(TreePath path) {
    StringBuilder result = new StringBuilder();
    for (int i = 1; i < path.getPathCount(); i++) {
      MPSTreeNode node = (MPSTreeNode) path.getPathComponent(i);
      result.append(TREE_PATH_SEPARATOR);
      result.append(node.getNodeIdentifier().replaceAll(TREE_PATH_SEPARATOR, "-"));
    }
    if (result.length() == 0) return TREE_PATH_SEPARATOR;
    return result.toString();
  }

  public TreeNode findNodeWith(Object userObject) {
    MPSTreeNode root = getRootNode();
    return findNodeWith(root, userObject);
  }

  public MPSTreeNode getRootNode() {
    return (MPSTreeNode) getModel().getRoot();
  }

  public MPSTreeNode getCurrentNode() {
    return getNodeFromPath(getLeadSelectionPath());
  }

  public void setCurrentNode(@NotNull MPSTreeNode node) {
    TreePath path = new TreePath(node.getPath());
    setSelectionPath(path);
    this.scrollPathToVisible(path);
  }

  private MPSTreeNode findNodeWith(MPSTreeNode root, Object userObject) {
    if (root.getUserObject() == userObject) return root;
    if (!(root.isInitialized() || root.hasInfiniteSubtree())) root.init();
    for (MPSTreeNode child : root) {
      MPSTreeNode result = findNodeWith(child, userObject);
      if (result != null) return result;
    }
    return null;
  }

  private TreePath listToPath(List<String> pathComponents) {
    return getTreePath(pathComponents, false);
  }

  private TreePath stringToPath(String pathString) {
    List<String> components = Arrays.asList(pathString.split(TREE_PATH_SEPARATOR));

    return getTreePath(components, true);
  }

  @Nullable
  private TreePath getTreePath(List<String> components, boolean escapePathSep) {
    List<Object> path = new ArrayList<Object>();
    MPSTreeNode current = getRootNode();

    current.init();

    path.add(current);

    for (Iterator<String> it = components.iterator(); it.hasNext(); ) {
      String component = it.next();
      assert current.isInitialized();
      if (component == null || component.length() == 0) continue;
      boolean found = false;
      for (int i = 0; i < current.getChildCount(); i++) {
        MPSTreeNode node = (MPSTreeNode) current.getChildAt(i);
        String treeNodeId =
            escapePathSep ? node.getNodeIdentifier().replaceAll(TREE_PATH_SEPARATOR, "-") :
                            node.getNodeIdentifier();
        if (treeNodeId.equals(component)) {
          current = node;
          path.add(current);
          if (!current.isInitialized() && it.hasNext()) {
            current.init();
          }
          found = true;
          break;
        }
      }
      if (!found) {
        return null;
      }
    }
    return new TreePath(path.toArray());
  }

  protected void expandPathsRaw(List<List<String>> paths) {
    for (List<String> path : paths) {
      TreePath treePath = listToPath(path);
      if (treePath != null) {
        ensurePathInitialized(treePath);
        expandPath(treePath);
      }
    }
  }

  protected void expandPaths(List<String> paths) {
    for (String path : paths) {
      TreePath treePath = stringToPath(path);
      if (treePath != null) {
        ensurePathInitialized(treePath);
        expandPath(treePath);
      }
    }
  }

  private void ensurePathInitialized(TreePath path) {
    for (Object item : path.getPath()) {
      MPSTreeNode node = (MPSTreeNode) item;
      node.init();
    }
  }

  protected void selectPaths(List<String> paths) {
    List<TreePath> treePaths = new ArrayList<TreePath>();
    for (String path : paths) {
      treePaths.add(stringToPath(path));
    }
    setSelectionPaths(treePaths.toArray(new TreePath[treePaths.size()]));
  }

  protected void selectPathsRaw(List<List<String>> paths) {
    List<TreePath> treePaths = new ArrayList<TreePath>();
    for (List<String> path : paths) {
      treePaths.add(listToPath(path));
    }
    setSelectionPaths(treePaths.toArray(new TreePath[treePaths.size()]));
  }

  // TODO: refactor TreeState to include these instead of the old format
  public List<List<String>> getExpandedPathsRaw() {
    List<List<String>> result = new ArrayList<List<String>>();
    Enumeration<TreePath> expanded = getExpandedDescendants(new TreePath(new Object[]{getModel().getRoot()}));
    if (expanded == null) return result;
    while (expanded.hasMoreElements()) {
      List<String> path = new ArrayList<String>();
      TreePath expandedPath = expanded.nextElement();
      if (expandedPath.getLastPathComponent() == getModel().getRoot()) {
        continue;
      }
      for (int i = 1; i < expandedPath.getPathCount(); i++) {
        MPSTreeNode node = (MPSTreeNode) expandedPath.getPathComponent(i);
        path.add(node.getNodeIdentifier());
      }
      result.add(path);
    }
    return result;
  }

  private List<String> getExpandedPaths() {
    List<String> result = new ArrayList<String>();
    Enumeration<TreePath> expanded = getExpandedDescendants(new TreePath(new Object[]{getModel().getRoot()}));
    if (expanded == null) return result;
    while (expanded.hasMoreElements()) {
      TreePath path = expanded.nextElement();
      String pathString = pathToString(path);
      if (result.contains(pathString))
        LOG.warn("two expanded paths have the same string representation");
      result.add(pathString);
    }
    return result;
  }

  // TODO: refactor TreeState to include these instead of the old format
  public List<List<String>> getSelectedPathsRaw() {
    List<List<String>> result = new ArrayList<List<String>>();
    if (getSelectionPaths() == null) return result;
    for (TreePath selectedPath: getSelectionPaths()) {
      List<String> path = new ArrayList<String>();
      for (int i = 1; i < selectedPath.getPathCount(); i++) {
        MPSTreeNode node = (MPSTreeNode) selectedPath.getPathComponent(i);
        path.add(node.getNodeIdentifier());
      }
      result.add(path);
    }
    return result;
  }

  private List<String> getSelectedPaths() {
    List<String> result = new ArrayList<String>();
    if (getSelectionPaths() == null) return result;
    for (TreePath selectionPart : getSelectionPaths()) {
      String pathString = pathToString(selectionPart);
      if (result.contains(pathString))
        LOG.warn("two selected paths have the same string representation");
      result.add(pathString);
    }
    return result;
  }

  public TreeState saveState() {
    TreeState result = new TreeState();
    result.myExpansion.addAll(getExpandedPaths());
    result.mySelection.addAll(getSelectedPaths());
    return result;
  }

  public void loadState(TreeState state) {
    selectPaths(state.mySelection);
    expandPaths(state.myExpansion);
  }

  // TODO: refactor TreeState to include these instead of the old format
  public void loadState(List<List<String>> expandedPaths, List<List<String>> selectedPaths) {
    expandPathsRaw(expandedPaths);
    selectPathsRaw(selectedPaths);
  }

  @Override
  public int getToggleClickCount() {
    MPSTreeNode node = getNodeFromPath(getSelectionPath());
    if (node == null) return -1;
    return node.getToggleClickCount();
  }

  public boolean isDisposed() {
    return myDisposed;
  }

  @Override
  public void dispose() {
    assert !myDisposed;

    fireBeforeTreeDisposed();
    myDisposed = true;
    setRootNode(null);
    myTreeNodeListeners.clear();
  }

  public static class TreeState {
    private List<String> myExpansion = new ArrayList<String>();
    private List<String> mySelection = new ArrayList<String>();

    public List<String> getExpansion() {
      return myExpansion;
    }

    public void setExpansion(List<String> expansion) {
      myExpansion = expansion;
    }

    public List<String> getSelection() {
      return mySelection;
    }

    public void setSelection(List<String> selection) {
      mySelection = selection;
    }
  }

  private class MyTreeWillExpandListener implements TreeWillExpandListener {
    @Override
    public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException {
      MPSTreeNode treeNode = getNodeFromPath(event.getPath());
      if (treeNode != null) treeNode.init();
    }

    @Override
    public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {
    }
  }

  private class MyTreeExpansionListener implements TreeExpansionListener {
    @Override
    public void treeExpanded(TreeExpansionEvent event) {
      if (!myAutoExpandEnabled) return;

      TreePath eventPath = event.getPath();
      MPSTreeNode node = getNodeFromPath(eventPath);

      if (node != null && node.getChildCount() == 1) {
        List<MPSTreeNode> path = new ArrayList<MPSTreeNode>();
        for (Object item : eventPath.getPath()) {
          path.add((MPSTreeNode) item);
        }
        MPSTreeNode onlyChild = (MPSTreeNode) node.getChildAt(0);

        if (onlyChild.isAutoExpandable()) {
          path.add(onlyChild);
          expandPath(new TreePath(path.toArray()));
        }
      }
    }

    @Override
    public void treeCollapsed(TreeExpansionEvent event) {
    }
  }

  private class MyMouseAdapter extends MouseAdapter {
    @Override
    public void mousePressed(MouseEvent e) {
      myMousePressed(e);
    }

    @Override
    public void mouseReleased(MouseEvent e) {
      myMouseReleased(e);
    }

    @Override
    public void mouseClicked(MouseEvent e) {
      myMouseClicked(e);
    }

    @Override
    public void mouseEntered(MouseEvent e) {
      myTooltipManagerRecentInitialDelay = ToolTipManager.sharedInstance().getInitialDelay();
      ToolTipManager.sharedInstance().setInitialDelay(10);
    }

    @Override
    public void mouseExited(MouseEvent e) {
      ToolTipManager.sharedInstance().setInitialDelay(myTooltipManagerRecentInitialDelay);
    }
  }

  private class MyOpenNodeAction extends AbstractAction {
    @Override
    public void actionPerformed(ActionEvent e) {
      MPSTreeNode selNode = getNodeFromPath(getSelectionPath());
      if (selNode == null) return;

      doubleClick(selNode);
    }
  }

  private class MyRefreshAction extends AbstractAction {
    @Override
    public void actionPerformed(ActionEvent e) {
      rebuildNow();
    }
  }

  // XXX this class is worth re-use
  private static final class SafeUpdate extends Update {
    private final Runnable myDelegate;
    @Nullable
    private Logger myLogger;
    private Exception myException;

    public SafeUpdate(@NotNull Object updateId, @NotNull Runnable updateCode, @Nullable Logger logger) {
      super(updateId);
      myDelegate = updateCode;
      myLogger = logger;
    }

    @Override
    public void run() {
      try {
        myDelegate.run();
      } catch (Exception ex) {
        if (myLogger != null) {
          myLogger.error("Update failed", ex);
        }
        myException = ex;
      }
    }

    @Nullable
    public Exception getException() {
      return myException;
    }
  }
}
