// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.ide.util.gotoByName;

import com.intellij.concurrency.JobLauncher;
import com.intellij.ide.util.NavigationItemListCellRenderer;
import com.intellij.navigation.ChooseByNameContributor;
import com.intellij.navigation.ChooseByNameContributorEx;
import com.intellij.navigation.NavigationItem;
import com.intellij.openapi.application.ReadActionProcessor;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.util.ProgressIndicatorBase;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.project.PossiblyDumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.PomTargetPsiElement;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.Processor;
import com.intellij.util.Processors;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.indexing.FindSymbolParameters;
import com.intellij.util.indexing.IdFilter;
import consulo.logging.Logger;
import consulo.util.PluginExceptionUtil;
import gnu.trove.THashSet;
import gnu.trove.TIntHashSet;
import javax.annotation.Nonnull;

import javax.swing.*;
import java.util.*;
import java.util.concurrent.ConcurrentMap;

/**
 * Contributor-based goto model
 */
public abstract class ContributorsBasedGotoByModel implements ChooseByNameModelEx, PossiblyDumbAware {
  public static final Logger LOG = Logger.getInstance(ContributorsBasedGotoByModel.class);

  protected final Project myProject;
  private final List<ChooseByNameContributor> myContributors;

  protected ContributorsBasedGotoByModel(@Nonnull Project project, @Nonnull ChooseByNameContributor[] contributors) {
    this(project, Arrays.asList(contributors));
  }

  protected ContributorsBasedGotoByModel(@Nonnull Project project, @Nonnull List<ChooseByNameContributor> contributors) {
    myProject = project;
    myContributors = contributors;
    assert !contributors.contains(null);
  }

  @Override
  public boolean isDumbAware() {
    return ContainerUtil.find(getContributorList(), o -> DumbService.isDumbAware(o)) != null;
  }

  @Nonnull
  @Override
  public ListCellRenderer getListCellRenderer() {
    return new NavigationItemListCellRenderer();
  }

  public boolean sameNamesForProjectAndLibraries() {
    return false;
  }

  private final ConcurrentMap<ChooseByNameContributor, TIntHashSet> myContributorToItsSymbolsMap = ContainerUtil.createConcurrentWeakMap();

  @Override
  public void processNames(@Nonnull Processor<? super String> nameProcessor, @Nonnull FindSymbolParameters parameters) {
    long start = System.currentTimeMillis();
    List<ChooseByNameContributor> contributors = filterDumb(getContributorList());
    ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
    Processor<ChooseByNameContributor> processor = new ReadActionProcessor<ChooseByNameContributor>() {
      @Override
      public boolean processInReadAction(@Nonnull ChooseByNameContributor contributor) {
        try {
          if (!myProject.isDisposed()) {
            long contributorStarted = System.currentTimeMillis();
            processContributorNames(contributor, parameters, nameProcessor);

            if (LOG.isDebugEnabled()) {
              LOG.debug(contributor + " for " + (System.currentTimeMillis() - contributorStarted));
            }
          }
        }
        catch (ProcessCanceledException | IndexNotReadyException ex) {
          // index corruption detected, ignore
        }
        catch (Exception ex) {
          LOG.error(ex);
        }
        return true;
      }
    };
    if (!JobLauncher.getInstance().invokeConcurrentlyUnderProgress(contributors, indicator, processor)) {
      throw new ProcessCanceledException();
    }
    if (indicator != null) {
      indicator.checkCanceled();
    }
    long finish = System.currentTimeMillis();
    if (LOG.isDebugEnabled()) {
      LOG.debug("processNames(): " + (finish - start) + "ms;");
    }
  }

  public void processContributorNames(@Nonnull ChooseByNameContributor contributor, @Nonnull FindSymbolParameters parameters, @Nonnull Processor<? super String> nameProcessor) {
    TIntHashSet filter = new TIntHashSet(1000);
    if (contributor instanceof ChooseByNameContributorEx) {
      ((ChooseByNameContributorEx)contributor).processNames(s -> {
        if (nameProcessor.process(s)) {
          filter.add(s.hashCode());
        }
        return true;
      }, parameters.getSearchScope(), parameters.getIdFilter());
    }
    else {
      String[] names = contributor.getNames(myProject, parameters.isSearchInLibraries());
      for (String element : names) {
        if (nameProcessor.process(element)) {
          filter.add(element.hashCode());
        }
      }
    }
    myContributorToItsSymbolsMap.put(contributor, filter);
  }

  IdFilter getIdFilter(boolean withLibraries) {
    return IdFilter.getProjectIdFilter(myProject, withLibraries);
  }

  @Nonnull
  @Override
  public String[] getNames(final boolean checkBoxState) {
    final THashSet<String> allNames = new THashSet<>();

    Collection<String> result = Collections.synchronizedCollection(allNames);
    processNames(Processors.cancelableCollectProcessor(result), FindSymbolParameters.simple(myProject, checkBoxState));
    if (LOG.isDebugEnabled()) {
      LOG.debug("getNames(): (got " + allNames.size() + " elements)");
    }
    return ArrayUtilRt.toStringArray(allNames);
  }

  private List<ChooseByNameContributor> filterDumb(List<ChooseByNameContributor> contributors) {
    if (!DumbService.getInstance(myProject).isDumb()) return contributors;
    List<ChooseByNameContributor> answer = new ArrayList<>(contributors.size());
    for (ChooseByNameContributor contributor : contributors) {
      if (DumbService.isDumbAware(contributor)) {
        answer.add(contributor);
      }
    }

    return answer;
  }

  @Nonnull
  public Object[] getElementsByName(@Nonnull final String name, @Nonnull final FindSymbolParameters parameters, @Nonnull final ProgressIndicator canceled) {
    long elementByNameStarted = System.currentTimeMillis();
    final List<NavigationItem> items = Collections.synchronizedList(new ArrayList<>());

    Processor<ChooseByNameContributor> processor = contributor -> {
      if (myProject.isDisposed()) {
        return true;
      }
      TIntHashSet filter = myContributorToItsSymbolsMap.get(contributor);
      if (filter != null && !filter.contains(name.hashCode())) return true;
      try {
        boolean searchInLibraries = parameters.isSearchInLibraries();
        long contributorStarted = System.currentTimeMillis();

        if (contributor instanceof ChooseByNameContributorEx) {
          ((ChooseByNameContributorEx)contributor).processElementsWithName(name, item -> {
            canceled.checkCanceled();
            if (acceptItem(item)) items.add(item);
            return true;
          }, parameters);

          if (LOG.isDebugEnabled()) {
            LOG.debug(System.currentTimeMillis() - contributorStarted + "," + contributor + ",");
          }
        }
        else {
          NavigationItem[] itemsByName = contributor.getItemsByName(name, parameters.getLocalPatternName(), myProject, searchInLibraries);
          for (NavigationItem item : itemsByName) {
            canceled.checkCanceled();
            if (item == null) {
              PluginExceptionUtil.logPluginError(LOG, "null item from contributor " + contributor + " for name " + name, null, contributor.getClass());
              continue;
            }
            VirtualFile file = item instanceof PsiElement && !(item instanceof PomTargetPsiElement) ? PsiUtilCore.getVirtualFile((PsiElement)item) : null;
            if (file != null && !parameters.getSearchScope().contains(file)) continue;

            if (acceptItem(item)) {
              items.add(item);
            }
          }

          if (LOG.isDebugEnabled()) {
            LOG.debug(System.currentTimeMillis() - contributorStarted + "," + contributor + "," + itemsByName.length);
          }
        }
      }
      catch (ProcessCanceledException ex) {
        // index corruption detected, ignore
      }
      catch (Exception ex) {
        LOG.error(ex);
      }
      return true;
    };
    if (!JobLauncher.getInstance().invokeConcurrentlyUnderProgress(filterDumb(getContributorList()), canceled, processor)) {
      canceled.cancel();
    }
    canceled.checkCanceled(); // if parallel job execution was canceled because of PCE, rethrow it from here
    if (LOG.isDebugEnabled()) {
      LOG.debug("Retrieving " + name + ":" + items.size() + " for " + (System.currentTimeMillis() - elementByNameStarted));
    }
    return ArrayUtil.toObjectArray(items);
  }

  /**
   * Get elements by name from contributors.
   *
   * @param name          a name
   * @param checkBoxState if true, non-project files are considered as well
   * @param pattern       a pattern to use
   * @return a list of navigation items from contributors for
   * which {@link #acceptItem(NavigationItem) returns true.
   */
  @Nonnull
  @Override
  public Object[] getElementsByName(@Nonnull final String name, final boolean checkBoxState, @Nonnull final String pattern) {
    return getElementsByName(name, FindSymbolParameters.wrap(pattern, myProject, checkBoxState), new ProgressIndicatorBase());
  }

  @Override
  public String getElementName(@Nonnull Object element) {
    if (!(element instanceof NavigationItem)) {
      throw new AssertionError(element + " of " + element.getClass() + " in " + this + " of " + getClass());
    }
    return ((NavigationItem)element).getName();
  }

  @Override
  public String getHelpId() {
    return null;
  }

  protected List<ChooseByNameContributor> getContributorList() {
    return myContributors;
  }

  protected ChooseByNameContributor[] getContributors() {
    return getContributorList().toArray(new ChooseByNameContributor[0]);
  }

  /**
   * This method allows extending classes to introduce additional filtering criteria to model
   * beyond pattern and project/non-project files. The default implementation just returns true.
   *
   * @param item an item to filter
   * @return true if the item is acceptable according to additional filtering criteria.
   */
  protected boolean acceptItem(NavigationItem item) {
    return true;
  }

  @Override
  public boolean useMiddleMatching() {
    return true;
  }

  public
  @Nonnull
  String removeModelSpecificMarkup(@Nonnull String pattern) {
    return pattern;
  }

  @Nonnull
  public Project getProject() {
    return myProject;
  }
}