/*
 * Copyright 2000-2014 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.openapi.externalSystem.view;

import com.intellij.ide.util.treeView.NodeDescriptor;
import com.intellij.openapi.externalSystem.ExternalSystemUiAware;
import com.intellij.openapi.externalSystem.model.DataNode;
import com.intellij.openapi.externalSystem.service.project.manage.ExternalProjectsManager;
import com.intellij.openapi.externalSystem.service.project.manage.ExternalSystemShortcutsManager;
import com.intellij.openapi.externalSystem.service.project.manage.ExternalSystemTaskActivator;
import com.intellij.openapi.externalSystem.util.ExternalSystemBundle;
import com.intellij.openapi.externalSystem.util.Order;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.Navigatable;
import com.intellij.ui.JBColor;
import com.intellij.ui.SimpleTextAttributes;
import com.intellij.ui.treeStructure.SimpleNode;
import com.intellij.ui.treeStructure.SimpleTree;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.awt.*;
import java.awt.event.InputEvent;
import java.util.*;
import java.util.List;

/**
 * @author Vladislav.Soroka
 * @since 10/15/2014
 */
public abstract class ExternalSystemNode<T> extends SimpleNode implements Comparable<ExternalSystemNode> {

  public static final int BUILTIN_TASKS_DATA_NODE_ORDER = 10;
  public static final int BUILTIN_DEPENDENCIES_DATA_NODE_ORDER = BUILTIN_TASKS_DATA_NODE_ORDER + 10;
  public static final int BUILTIN_RUN_CONFIGURATIONS_DATA_NODE_ORDER = BUILTIN_DEPENDENCIES_DATA_NODE_ORDER + 10;
  public static final int BUILTIN_MODULE_DATA_NODE_ORDER = BUILTIN_RUN_CONFIGURATIONS_DATA_NODE_ORDER + 10;

  @NotNull public static final Comparator<ExternalSystemNode> ORDER_AWARE_COMPARATOR = new Comparator<ExternalSystemNode>() {

    @Override
    public int compare(@NotNull ExternalSystemNode o1, @NotNull ExternalSystemNode o2) {
      int order1 = getOrder(o1);
      int order2 = getOrder(o2);
      if (order1 == order2) return o1.compareTo(o2);
      return order1 < order2 ? -1 : 1;
    }

    private int getOrder(@NotNull Comparable o) {
      Order annotation = o.getClass().getAnnotation(Order.class);
      if (annotation != null) {
        return annotation.value();
      }
      return 0;
    }
  };


  protected static final ExternalSystemNode[] NO_CHILDREN = new ExternalSystemNode[0];

  private final ExternalProjectsView myExternalProjectsView;
  private final List<ExternalSystemNode<?>> myChildrenList = ContainerUtil.newArrayList();
  protected DataNode<T> myDataNode;
  @Nullable
  private ExternalSystemNode myParent;
  private ExternalSystemNode[] myChildren;
  private ExternalProjectsStructure.ErrorLevel myErrorLevel = ExternalProjectsStructure.ErrorLevel.NONE;
  private final List<String> myErrors = ContainerUtil.newArrayList();
  private ExternalProjectsStructure.ErrorLevel myTotalErrorLevel = null;

  public ExternalSystemNode(@NotNull ExternalProjectsView externalProjectsView,
                            @Nullable ExternalSystemNode parent) {
    this(externalProjectsView, parent, null);
  }

  public ExternalSystemNode(@NotNull ExternalProjectsView externalProjectsView,
                            @Nullable ExternalSystemNode parent,
                            @Nullable DataNode<T> dataNode) {
    super(externalProjectsView.getProject(), null);
    myExternalProjectsView = externalProjectsView;
    myDataNode = dataNode;
    myParent = parent;
  }

  public boolean isAutoExpandNode() {
    SimpleNode parent = getParent();
    return parent != null && parent.getChildCount() == 1;
  }

  public void setParent(@Nullable ExternalSystemNode parent) {
    myParent = parent;
  }

  @Nullable
  public T getData() {
    return myDataNode != null ? myDataNode.getData() : null;
  }

  @Override
  public NodeDescriptor getParentDescriptor() {
    return myParent;
  }

  protected ExternalProjectsView getExternalProjectsView() {
    return myExternalProjectsView;
  }

  protected ExternalSystemUiAware getUiAware() {
    return myExternalProjectsView.getUiAware();
  }

  protected ExternalProjectsStructure getStructure() {
    return myExternalProjectsView.getStructure();
  }

  protected ExternalSystemShortcutsManager getShortcutsManager() {
    return myExternalProjectsView.getShortcutsManager();
  }

  protected ExternalSystemTaskActivator getTaskActivator() {
    return myExternalProjectsView.getTaskActivator();
  }

  @Nullable
  public <DataType extends ExternalSystemNode> DataType findParent(Class<DataType> parentClass) {
    ExternalSystemNode node = this;
    while (true) {
      node = node.myParent;
      if (node == null || parentClass.isInstance(node)) {
        //noinspection unchecked
        return (DataType)node;
      }
    }
  }

  @Nullable
  public <DataType> DataType findParentData(Class<DataType> parentDataClass) {
    ExternalSystemNode node = this;
    while (true) {
      node = node.myParent;
      if (node == null) return null;
      if (node.getData() != null && parentDataClass.isInstance(node.getData())) {
        //noinspection unchecked
        return (DataType)node.getData();
      }
    }
  }

  public boolean isVisible() {
    return getDisplayKind() != ExternalProjectsStructure.DisplayKind.NEVER && !(isIgnored() && !myExternalProjectsView.getShowIgnored());
  }

  public boolean isIgnored() {
    if (myDataNode != null) {
      return myDataNode.isIgnored();
    }
    final SimpleNode parent = getParent();
    return parent instanceof ExternalSystemNode && ((ExternalSystemNode)parent).isIgnored();
  }

  public void setIgnored(final boolean ignored) {
    if (myDataNode != null) {
      ExternalProjectsManager.getInstance(myExternalProjectsView.getProject()).setIgnored(myDataNode, ignored);
    }
  }

  public ExternalProjectsStructure.DisplayKind getDisplayKind() {
    Class[] visibles = getStructure().getVisibleNodesClasses();
    if (visibles == null) return ExternalProjectsStructure.DisplayKind.NORMAL;
    for (Class each : visibles) {
      if (each.isInstance(this)) return ExternalProjectsStructure.DisplayKind.ALWAYS;
    }
    return ExternalProjectsStructure.DisplayKind.NEVER;
  }

  @NotNull
  public final ExternalSystemNode[] getChildren() {
    if (myChildren == null) {
      myChildren = buildChildren();
      onChildrenBuilt();
    }
    return myChildren;
  }

  protected void onChildrenBuilt() {
  }

  @NotNull
  private ExternalSystemNode[] buildChildren() {
    List<? extends ExternalSystemNode> newChildrenCandidates = doBuildChildren();
    if (newChildrenCandidates.isEmpty()) return NO_CHILDREN;

    addAll(newChildrenCandidates, true);
    sort(myChildrenList);
    List<ExternalSystemNode> visibleNodes = new ArrayList<>();
    for (ExternalSystemNode each : myChildrenList) {
      if (each.isVisible()) visibleNodes.add(each);
    }
    return visibleNodes.toArray(new ExternalSystemNode[visibleNodes.size()]);
  }

  public void cleanUpCache() {
    myChildren = null;
    myChildrenList.clear();
    myTotalErrorLevel = null;
  }

  @Nullable
  protected ExternalSystemNode[] getCached() {
    return myChildren;
  }

  protected void sort(List<? extends ExternalSystemNode> list) {
    Collections.sort(list, ORDER_AWARE_COMPARATOR);
  }

  public boolean addAll(Collection<? extends ExternalSystemNode> externalSystemNodes) {
    return addAll(externalSystemNodes, false);
  }

  private boolean addAll(Collection<? extends ExternalSystemNode> externalSystemNodes, boolean silently) {
    if (externalSystemNodes.isEmpty()) return false;

    for (ExternalSystemNode externalSystemNode : externalSystemNodes) {
      externalSystemNode.setParent(this);
      myChildrenList.add(externalSystemNode);
    }
    if (!silently) {
      childrenChanged();
    }

    return true;
  }

  public boolean add(ExternalSystemNode externalSystemNode) {
    return addAll(ContainerUtil.list(externalSystemNode));
  }

  public boolean removeAll(Collection<ExternalSystemNode> externalSystemNodes) {
    return removeAll(externalSystemNodes, false);
  }

  private boolean removeAll(Collection<ExternalSystemNode> externalSystemNodes, boolean silently) {
    if (externalSystemNodes.isEmpty()) return false;

    for (ExternalSystemNode externalSystemNode : externalSystemNodes) {
      externalSystemNode.setParent(null);
      myChildrenList.remove(externalSystemNode);
    }
    if (!silently) {
      childrenChanged();
    }

    return true;
  }

  public void remove(ExternalSystemNode externalSystemNode) {
    removeAll(ContainerUtil.list(externalSystemNode));
  }

  protected void childrenChanged() {
    ExternalSystemNode each = this;
    while (each != null) {
      each.myTotalErrorLevel = null;
      each = (ExternalSystemNode)each.getParent();
    }

    sort(myChildrenList);
    final List<ExternalSystemNode<?>> visibleNodes = ContainerUtil.filter(myChildrenList, node -> node.isVisible());
    myChildren = visibleNodes.toArray(new ExternalSystemNode[visibleNodes.size()]);
    myExternalProjectsView.updateUpTo(this);
  }

  public boolean hasChildren() {
    return getChildren().length > 0;
  }

  @NotNull
  protected List<? extends ExternalSystemNode> doBuildChildren() {
    if (myDataNode != null && !myDataNode.getChildren().isEmpty()) {
      final ExternalProjectsView externalProjectsView = getExternalProjectsView();
      return externalProjectsView.createNodes(externalProjectsView, this, myDataNode);
    }
    else {
      return myChildrenList;
    }
  }

  protected void setDataNode(DataNode<T> dataNode) {
    myDataNode = dataNode;
  }

  public ExternalProjectsStructure.ErrorLevel getTotalErrorLevel() {
    if (myTotalErrorLevel == null) {
      myTotalErrorLevel = calcTotalErrorLevel();
    }
    return myTotalErrorLevel;
  }

  private ExternalProjectsStructure.ErrorLevel calcTotalErrorLevel() {
    ExternalProjectsStructure.ErrorLevel childrenErrorLevel = getChildrenErrorLevel();
    return childrenErrorLevel.compareTo(myErrorLevel) > 0 ? childrenErrorLevel : myErrorLevel;
  }

  public ExternalProjectsStructure.ErrorLevel getChildrenErrorLevel() {
    ExternalProjectsStructure.ErrorLevel result = ExternalProjectsStructure.ErrorLevel.NONE;
    for (SimpleNode each : getChildren()) {
      ExternalProjectsStructure.ErrorLevel eachLevel = ((ExternalSystemNode)each).getTotalErrorLevel();
      if (eachLevel.compareTo(result) > 0) result = eachLevel;
    }
    return result;
  }

  public void setErrorLevel(ExternalProjectsStructure.ErrorLevel level, String... errors) {
    if (myErrorLevel == level) return;
    myErrorLevel = level;
    myErrors.clear();
    Collections.addAll(myErrors, errors);
    myExternalProjectsView.updateUpTo(this);
  }

  @Override
  protected void doUpdate() {
    setNameAndTooltip(getName(), null);
  }

  protected void setNameAndTooltip(String name, @Nullable String tooltip) {
    setNameAndTooltip(name, tooltip, (String)null);
  }

  protected void setNameAndTooltip(String name, @Nullable String tooltip, @Nullable String hint) {
    final boolean ignored = isIgnored();
    final SimpleTextAttributes textAttributes = ignored ? SimpleTextAttributes.GRAYED_ITALIC_ATTRIBUTES : getPlainAttributes();
    setNameAndTooltip(name, tooltip, textAttributes);
    if (!StringUtil.isEmptyOrSpaces(hint)) {
      addColoredFragment(" (" + hint + ")", ignored ? SimpleTextAttributes.GRAYED_ITALIC_ATTRIBUTES : SimpleTextAttributes.GRAY_ATTRIBUTES);
    }
  }

  protected void setNameAndTooltip(String name, @Nullable String tooltip, SimpleTextAttributes attributes) {
    clearColoredText();
    addColoredFragment(name, prepareAttributes(attributes));
    final String s = (tooltip != null ? tooltip + "\n\r" : "") + StringUtil.join(myErrors, "\n\r");
    getTemplatePresentation().setTooltip(s);
  }

  private SimpleTextAttributes prepareAttributes(SimpleTextAttributes from) {
    ExternalProjectsStructure.ErrorLevel level = getTotalErrorLevel();
    Color waveColor = level == ExternalProjectsStructure.ErrorLevel.NONE ? null : JBColor.RED;
    int style = from.getStyle();
    if (waveColor != null) style |= SimpleTextAttributes.STYLE_WAVED;
    return new SimpleTextAttributes(from.getBgColor(), from.getFgColor(), waveColor, style);
  }

  @Nullable
  @NonNls
  protected String getActionId() {
    return null;
  }

  @Nullable
  @NonNls
  protected String getMenuId() {
    return null;
  }

  protected String message(@NotNull String key, @NotNull Object... params) {
    return ExternalSystemBundle.message(key, params);
  }

  @Nullable
  public VirtualFile getVirtualFile() {
    return null;
  }

  @Nullable
  public Navigatable getNavigatable() {
    return null;
  }

  @Override
  public void handleDoubleClickOrEnter(SimpleTree tree, InputEvent inputEvent) {
    String actionId = getActionId();
    getExternalProjectsView().handleDoubleClickOrEnter(this, actionId, inputEvent);
  }

  @Override
  public int compareTo(@NotNull ExternalSystemNode node) {
    return StringUtil.compare(this.getName(), node.getName(), true);
  }
}
