/*
 * Copyright 2003-2016 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.openapi.editor.colors.ColorKey;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import jetbrains.mps.ide.icons.IdeIcons;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.Icon;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeNode;
import java.awt.Color;
import java.awt.Font;
import java.awt.font.TextAttribute;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author Kostik
 */
public class MPSTreeNode extends DefaultMutableTreeNode implements Iterable<MPSTreeNode> {
  private static final Logger LOG = LogManager.getLogger(MPSTreeNode.class);

  private MPSTree myTree;
  private boolean myAdded;

  private Icon myCollapsedIcon = IdeIcons.CLOSED_FOLDER;
  private Icon myExpandedIcon = IdeIcons.OPENED_FOLDER;
  private String myNodeIdentifier;
  private String myText;
  private String myAdditionalText = null;
  private String myTooltipText;
  private Color myColor = EditorColorsManager.getInstance().getGlobalScheme().getColor(ColorKey.createColorKey("FILESTATUS_NOT_CHANGED"));
  private int myFontStyle = Font.PLAIN;
  private boolean myAutoExpandable = true;
  private ErrorState myErrorState = ErrorState.NONE;
  private ErrorState myCombinedErrorState = ErrorState.NONE;
  private final Object myTreeMessagesLock = new Object();
  private List<TreeMessage> myTreeMessages = null;
  private Map<TextAttribute, Object> myFontAttributes = new HashMap<TextAttribute, Object>();
  private int myToggleClickCount = 2;

  public MPSTreeNode() {
    super(null);
  }

  public MPSTreeNode(Object userObject) {
    super(userObject);
  }

  @Override
  @SuppressWarnings("unchecked")
  public Iterator<MPSTreeNode> iterator() {
    if (children == null) {
      return Collections.<MPSTreeNode>emptySet().iterator();
    }
    return children.iterator();
  }

  public MPSTree getTree() {
    if (myTree == null && getParent() instanceof MPSTreeNode) {
      return ((MPSTreeNode) getParent()).getTree();
    }
    return myTree;
  }

  /**
   *  returns the closest ancestor (nodes or the containing tree) which implements the given class
   */
  public <T> T getAncestor(@NotNull Class<T> cls) {
    TreeNode parent = getParent();
    while (parent != null) {
      if (cls.isInstance(parent)) {
        return cls.cast(parent);
      }
      parent = parent.getParent();
    }
    if (myTree != null && cls.isInstance(myTree)) {
      return cls.cast(myTree);
    }
    return null;
  }

  public void setTree(MPSTree tree) {
    myTree = tree;
  }

  public boolean isInitialized() {
    return true;
  }

  public boolean hasInfiniteSubtree() {
    return false;
  }

  public void doubleClick() {
  }

  protected void onRemove() {
    getTree().fireTreeNodeRemoved(this);
  }

  protected void onAdd() {
    updatePresentation();
    getTree().fireTreeNodeAdded(this);
  }

  /**
   * Deemed for tree clients to ensure node is initialized (i.e. has its children).
   * If the node is already {@link #isInitialized() initialized}, does nothing.
   * Otherwise, {@link jetbrains.mps.ide.ui.tree.MPSTree#performInit(MPSTreeNode)}  delegates} to owning tree, if any,
   * to perform actual initialization, with respect to tree's considerations (e.g. might wrap with model read action or
   * "Loading..."  notification nodes).
   * Tree shall call {@link #doInit()} at some point where actual node initialization takes place.
   * Although not final, extra care should be taken if overriding (FIXME perhaps, shall make final, and move isInitialized field here as well).
   */
  public void init() {
    if (isInitialized()) {
      return;
    }
    MPSTree tree = getTree();
    if (tree != null) {
      tree.performInit(this);
    } else {
      doInit();
    }
  }

  /**
   * This method shall not be invoked by code outside of MPSTree framework.
   * Subclasses shall override and perform actual node initialization here.
   * Default implementation does nothing, subclasses don't need to invoke <code>super.doInit()</code>
   */
  protected void doInit() {
  }

  public void updateSubTree() {
    getTree().runRebuildAction(new Runnable() {
      @Override
      public void run() {
        update();
      }
    }, true);
  }

  public void update() {
    doUpdate();
    ((DefaultTreeModel) getTree().getModel()).nodeStructureChanged(this);
  }

  protected void doUpdate() {
  }

  @Override
  public void remove(int childIndex) {
    if (myAdded && getTree() != null && !getTree().isDisposed()) {
      ((MPSTreeNode) getChildAt(childIndex)).removeThisAndChildren();
    }
    super.remove(childIndex);
    updateErrorState();
  }


  @Override
  public void insert(MutableTreeNode newChild, int childIndex) {
    super.insert(newChild, childIndex);
    if (myAdded && getTree() != null && !getTree().isDisposed()) {
      ((MPSTreeNode) getChildAt(childIndex)).addThisAndChildren();
    }
    updateErrorState();
  }

  public boolean hasChild(MutableTreeNode node) {
    for (int i = 0; i < getChildCount(); i++) {
      if (getChildAt(i) == node) return true;
    }
    return false;
  }

  final void removeThisAndChildren() {
    if (!myAdded) {
      throw new IllegalStateException(
          String.format("Trying to remove tree node which have not been added, tree=%s, node=%s",
              myTree != null ? myTree.getClass().getName() : "null", getClass().getName()));
    }
    try {
      onRemove();
    } catch (Throwable t) {
      LOG.error(null, t);
    }
    myAdded = false;
    if (!isInitialized()) {
      return;
    }
    for (MPSTreeNode node : this) {
      node.removeThisAndChildren();
    }
    setParent(null);
    children = null;
  }

  final void addThisAndChildren() {
    if (myAdded) {
      throw new IllegalStateException(
          String.format("Trying to add tree node which have already been added, tree=%s, node=%s",
              myTree != null ? myTree.getClass().getName() : "null", getClass().getName()));
    }
    try {
      onAdd();
    } catch (Throwable t) {
      LOG.error(null, t);
    }
    myAdded = true;
    if (!isInitialized()) {
      return;
    }
    for (int i = 0; i < getChildCount(); i++) {
      MPSTreeNode node = (MPSTreeNode) getChildAt(i);
      node.addThisAndChildren();
    }
  }

  public MPSTreeNode findExactChildWith(Object userObject) {
    for (MPSTreeNode child : this) {
      if (child.getUserObject() == userObject) return child;
    }
    return null;
  }

  /**
   * Ignores subtree of nodes that have not been initialized yet.
   */
  @Nullable
  public final MPSTreeNode findDescendantWith(Object userObject) {
    if (getUserObject() == null ? userObject == null : getUserObject().equals(userObject)) return this;
    if (isInitialized()) {
      for (int i = 0; i < getChildCount(); i++) {
        MPSTreeNode result = ((MPSTreeNode) getChildAt(i)).findDescendantWith(userObject);
        if (result != null) return result;
      }
    }
    return null;
  }

  /**
   * Default value is: 2
   */
  public int getToggleClickCount() {
    return myToggleClickCount;
  }

  public void setToggleClickCount(int clickCount) {
    myToggleClickCount = clickCount;
  }

  //updates and refreshes tree
  public void renewPresentation() {
    final MPSTree tree = getTree();
    if (tree == null || tree.isDisposed()) {
      return;
    }
    updatePresentation();
    updateNodePresentationInTree();
  }

  //todo make final
  protected void updatePresentation() {
    setColor(EditorColorsManager.getInstance().getGlobalScheme().getColor(ColorKey.createColorKey("FILESTATUS_NOT_CHANGED")));
    doUpdatePresentation();
    if (myTree == null) {
      myTree = getTree();
    }
    if (myTree != null) {
      myTree.fireTreeNodeUpdated(this);
    }
    Color c = null;
    String additionalText = null;
    synchronized (myTreeMessagesLock) {
      if (myTreeMessages != null) {
        int maxColorPriority = Integer.MIN_VALUE;
        int maxAdditionalTextPriority = Integer.MIN_VALUE;
        for (TreeMessage message : myTreeMessages) {
          if (maxColorPriority < message.getPriority() && message.alternatesColor()) {
            c = message.getColor();
          }
          if (maxAdditionalTextPriority < message.getPriority() && message.hasAdditionalText()) {
            additionalText = message.getAdditionalText();
          }
        }
      }
    }
    if (c != null) {
      setColor(c);
    }
    if (additionalText != null) {
      setAdditionalText(additionalText);
    }
  }

  public void updatePresentation(final boolean reloadSubTree, final boolean updateAncestors) {
    renewPresentation();
    if (reloadSubTree) {
      updateSubTree();
    }

    if (updateAncestors) {
      updateAncestorsPresentationInTree();
    }
  }

  /**
   * Attach an extra message to a node. Messages are identified by their {@link jetbrains.mps.ide.ui.tree.TreeMessageOwner owner}.
   * This method may be invoked from any thread, and doesn't trigger UI update, use {@link #renewPresentation()} from correct (EDT/UI) thread
   * if needed (e.g. if messages are attached the moment tree is being constructed, there's no reason to renew each node individually,
   * they get a chance to update them once tree becomes visible)
   * @param message message to attach
   */
  public void addTreeMessage(@NotNull TreeMessage message) {
    synchronized (myTreeMessagesLock) {
      if (myTreeMessages == null) {
        myTreeMessages = new ArrayList<TreeMessage>(1);
      }
      myTreeMessages.add(message);
    }
  }

  /**
   * Detach all messages of the specified owner.
   * This method can be invoked from any thread.
   * To trigger UI update, use {@link #renewPresentation()} from correct (EDT/UI) thread.
   * @param owner identifies messages to remove
   * @return set of detached messages, or empty collection if none found
   */
  @NotNull
  public Set<TreeMessage> removeTreeMessages(TreeMessageOwner owner) {
    Set<TreeMessage> result = new HashSet<TreeMessage>(1);
    if (owner == null) {
      return result;
    }
    final ArrayList<TreeMessage> copy;
    synchronized (myTreeMessagesLock) {
      if (myTreeMessages == null) {
        return result;
      }
      copy = new ArrayList<TreeMessage>(myTreeMessages);
    }
    for (TreeMessage message : copy) {
      if (owner.equals(message.getOwner())) {
        result.add(message);
      }
    }
    synchronized (myTreeMessagesLock) {
      myTreeMessages.removeAll(result);
    }
    return result;
  }

  protected void doUpdatePresentation() {

  }

  public final Icon getIcon(boolean expanded) {
    if (expanded) {
      return myExpandedIcon;
    } else {
      return myCollapsedIcon;
    }
  }

  public final void setIcon(Icon newIcon, boolean expanded) {
    if (expanded) {
      myExpandedIcon = newIcon;
    } else {
      myCollapsedIcon = newIcon;
    }
  }

  public final void setIcon(Icon newIcon) {
    setIcon(newIcon, true);
    setIcon(newIcon, false);
  }

  public final Color getColor() {
    return myColor;
  }

  public final void setColor(Color color) {
    myColor = color;
  }

  public final int getFontStyle() {
    return myFontStyle;
  }

  public final void setFontStyle(int fontStyle) {
    myFontStyle = fontStyle;
  }

  public final void addFontAttribute(TextAttribute key, Object value) {
    myFontAttributes.put(key, value);
  }

  public final Map getFontAttributes() {
    return myFontAttributes;
  }

  @NotNull
  public final String getNodeIdentifier() {
    if (myNodeIdentifier == null) {
      // extra info for assertion failed due to MPS-12305
      String parentId = null;
      if (getParent() instanceof MPSTreeNode) {
        parentId = ((MPSTreeNode) getParent()).getNodeIdentifier();
      }
      throw new IllegalStateException("MPSTreeNode identifier cannot be null, class="
          + getClass().getName() + ", parent id=" + parentId);
    } else {
      return myNodeIdentifier;
    }
  }

  public final void setNodeIdentifier(@NotNull String newNodeIdentifier) {
    myNodeIdentifier = newNodeIdentifier;
  }

  public final String getAdditionalText() {
    return myAdditionalText;
  }

  public final void setAdditionalText(String newAdditionalText) {
    myAdditionalText = newAdditionalText;
  }

  public final String getText() {
    if (myText == null) {
      return getNodeIdentifier();
    } else {
      return myText;
    }
  }

  public final void setText(String text) {
    myText = text;
  }

  public final String getTooltipText() {
    return myTooltipText;
  }

  public final void setTooltipText(String tooltipText) {
    myTooltipText = tooltipText;
  }

  public final boolean isErrorState() {
    return myErrorState == ErrorState.ERROR;
  }

  public final void setErrorState(ErrorState state) {
    myErrorState = state;
    updateErrorState();
  }

  public final ErrorState getErrorState() {
    return myErrorState;
  }

  public final ErrorState getAggregatedErrorState() {
    return myCombinedErrorState;
  }

  protected void updateErrorState() {
    ErrorState state = ErrorState.NONE;
    if (propogateErrorUpwards()) {
      for (MPSTreeNode node : this) {
        state = state.combine(node.getAggregatedErrorState());
      }
    }
    myCombinedErrorState = state.combine(myErrorState);
    if (getParent() != null) {
      ((MPSTreeNode) getParent()).updateErrorState();
    }
  }

  protected boolean propogateErrorUpwards() {
    return true;
  }

  public String toString() {
    return getText();
  }

  public final boolean isAutoExpandable() {
    return myAutoExpandable;
  }

  public final void setAutoExpandable(boolean autoExpandable) {
    myAutoExpandable = autoExpandable;
  }

  public final void updateNodePresentationInTree() {
    if (getTree() == null) return;
    ((DefaultTreeModel) getTree().getModel()).nodeChanged(this);
  }

  public void updateAncestorsPresentationInTree() {
    updateNodePresentationInTree();
    if (getParent() == null) return;
    ((MPSTreeNode) getParent()).updateAncestorsPresentationInTree();
  }

  protected boolean canBeOpened() {
    return true;
  }

  public void autoscroll() {

  }

  public boolean isLoadingEnabled() {
    return false;
  }
}
