/*
 * Copyright 2000-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 com.intellij.ide.projectView;

import com.intellij.ide.util.treeView.AbstractTreeNode;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.problems.WolfTheProblemSolver;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiFileSystemItem;
import com.intellij.psi.util.PsiUtilCore;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

/**
 * A node in the project view tree.
 *
 * @see TreeStructureProvider#modify(com.intellij.ide.util.treeView.AbstractTreeNode, java.util.Collection, com.intellij.ide.projectView.ViewSettings)
 */

public abstract class ProjectViewNode <Value> extends AbstractTreeNode<Value> implements RootsProvider, SettingsProvider {

  protected static final Logger LOG = Logger.getInstance("#com.intellij.ide.projectView.ProjectViewNode");

  private final ViewSettings mySettings;
  private boolean myValidating;

  /**
   * Creates an instance of the project view node.
   *
   * @param project      the project containing the node.
   * @param value        the object (for example, a PSI element) represented by the project view node
   * @param viewSettings the settings of the project view.
   */
  protected ProjectViewNode(Project project, Value value, ViewSettings viewSettings) {
    super(project, value);
    mySettings = viewSettings;
  }

  /**
   * Checks if this node or one of its children represents the specified virtual file.
   *
   * @param file the file to check for.
   * @return true if the file is found in the subtree, false otherwise.
   */
  public abstract boolean contains(@NotNull VirtualFile file);

  /**
   * Returns the virtual file represented by this node or one of its children.
   *
   * @return the virtual file instance, or null if the project view node doesn't represent a virtual file.
   */
  @Override
  @Nullable
  public VirtualFile getVirtualFile() {
    return null;
  }

  @Override
  public final ViewSettings getSettings() {
    return mySettings;
  }

  public static List<AbstractTreeNode> wrap(Collection objects,
                                            Project project,
                                            Class<? extends AbstractTreeNode> nodeClass,
                                            ViewSettings settings) {
    try {
      ArrayList<AbstractTreeNode> result = new ArrayList<>();
      for (Object object : objects) {
        result.add(createTreeNode(nodeClass, project, object, settings));
      }
      return result;
    }
    catch (Exception e) {
      LOG.error(e);
      return new ArrayList<>();
    }
  }

  public static AbstractTreeNode createTreeNode(Class<? extends AbstractTreeNode> nodeClass,
                                                Project project,
                                                Object value,
                                                ViewSettings settings) throws
                                                                       InstantiationException {
    Object[] parameters = {project, value, settings};
    for (Constructor<? extends AbstractTreeNode> constructor : (Constructor<? extends AbstractTreeNode>[])nodeClass.getConstructors()) {
      if (constructor.getParameterTypes().length != 3) continue;
      try {
        return constructor.newInstance(parameters);
      }
      catch (InstantiationException | InvocationTargetException | IllegalArgumentException | IllegalAccessException ignored) {
      }
    }
    throw new InstantiationException("no constructor found in " + nodeClass);
  }

  public boolean someChildContainsFile(final VirtualFile file) {
    return someChildContainsFile(file, true);
  }

  public boolean someChildContainsFile(final VirtualFile file, boolean optimizeByCheckingFileRootsFirst) {
    VirtualFile parent = file.getParent();

    boolean mayContain = false;

    if (optimizeByCheckingFileRootsFirst && parent != null) {
      Collection<VirtualFile> roots = getRoots();
      for (VirtualFile eachRoot : roots) {
        if (parent.equals(eachRoot.getParent())) {
          mayContain = true;
          break;
        }

        if (VfsUtilCore.isAncestor(eachRoot, file, true)) {
          mayContain = true;
          break;
        }
      }
    } else {
      mayContain = true;
    }

    if (!mayContain) {
      return false;
    }

    Collection<? extends AbstractTreeNode> kids = getChildren();
    for (final AbstractTreeNode kid : kids) {
      ProjectViewNode node = (ProjectViewNode)kid;
      if (node.contains(file)) return true;
    }
    return false;
  }

  @NotNull
  @Override
  public Collection<VirtualFile> getRoots() {
    Value value = getValue();

    if (value instanceof RootsProvider) {
      return ((RootsProvider)value).getRoots();
    } else if (value instanceof PsiFile) {
      PsiFile vFile = ((PsiFile)value).getContainingFile();
      if (vFile != null && vFile.getVirtualFile() != null) {
        return Collections.singleton(vFile.getVirtualFile());
      }
    } else if (value instanceof VirtualFile) {
      return Collections.singleton(((VirtualFile)value));
    } else if (value instanceof PsiFileSystemItem) {
      return Collections.singleton(((PsiFileSystemItem)value).getVirtualFile());
    }

    return EMPTY_ROOTS;
  }


  @Override
  protected boolean hasProblemFileBeneath() {
    if (!Registry.is("projectView.showHierarchyErrors")) return false;

    return WolfTheProblemSolver.getInstance(getProject()).hasProblemFilesBeneath(virtualFile -> {
      Value value;
      return contains(virtualFile)
             // in case of flattened packages, when package node a.b.c contains error file, node a.b might not.
             && ((value = getValue()) instanceof PsiElement && Comparing.equal(PsiUtilCore.getVirtualFile((PsiElement)value), virtualFile) ||
                 someChildContainsFile(virtualFile));
    });
  }

  /**
   * Efficiently checks if there are nodes under the project view node which match the specified condition. Should
   * return true if it's not possible to perform the check efficiently (for example, if recursive traversal of
   * all child nodes is required to check the condition).
   *
   * @param condition the condition to check the nodes.
   */
  public boolean canHaveChildrenMatching(Condition<PsiFile> condition) {
    return true;
  }

  @Nullable
  public String getTitle() {
    return null;
  }

  public boolean isSortByFirstChild() {
    return false;
  }

  public int getTypeSortWeight(boolean sortByType) {
    return 0;
  }

  /**
   * When nodes are sorted by type all objects with same weigh will be sorted using
   * some common algorithm (e.g alpha comparator). This method allows to perform custom
   * sorting for such objects. And default comparison will be applied only if nodes are equal
   * from custom comparator's point of view. Also comparison will be applied only if both objects
   * have not null comparable keys.
   * @return Comparable object.
   */
  @Nullable
  public Comparable getTypeSortKey() {
    return null;
  }

  /**
   * When nodes aren't sorted by type all objects with same weigh will be sorted using
   * some common algorithm (e.g alpha comparator). This method allows to perform custom
   * sorting for such objects. And default comparison will be applied only if nodes are equal
   * from custom comparator's point of view. Also comparison will be applied only if both objects
   * have not null comparable keys.
   * @return Comparable object.
   */
  @Nullable
  public Comparable getSortKey() {
    return null;
  }

  @Nullable
  public Comparable getManualOrderKey() {
    return null;
  }

  @Nullable
  public String getQualifiedNameSortKey() {
    return null;
  }

  public boolean shouldDrillDownOnEmptyElement() {
    return false;
  }

  public boolean validate() {
    setValidating(true);
    try {
      update();
    }
    finally {
      setValidating(false);
    }
    return getValue() != null;
  }

  @Override
  protected boolean shouldPostprocess() {
    return !isValidating();
  }

  @Override
  protected boolean shouldApply() {
    return !isValidating();
  }

  private void setValidating(boolean validating) {
    myValidating = validating;
  }

  public boolean isValidating() {
    return myValidating;
  }
}
