/*
 * Copyright 2003-2013 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.idea.java.usages;

import com.intellij.openapi.application.QueryExecutorBase;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.impl.light.LightElement;
import com.intellij.psi.search.DelegatingGlobalSearchScope;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.searches.ReferencesSearch.SearchParameters;
import com.intellij.util.CollectConsumer;
import com.intellij.util.Consumer;
import com.intellij.util.Processor;
import com.intellij.util.indexing.FileBasedIndex;
import com.intellij.util.indexing.FileBasedIndex.ValueProcessor;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.idea.core.psi.impl.MPSPsiNode;
import jetbrains.mps.idea.core.psi.impl.MPSPsiNodeBase;
import jetbrains.mps.idea.core.psi.impl.MPSPsiProvider;
import jetbrains.mps.idea.core.refactoring.NodePtr;
import jetbrains.mps.idea.core.usages.IdeaSearchScope;
import jetbrains.mps.idea.java.index.ForeignIdReferenceIndex;
import jetbrains.mps.idea.java.psiStubs.JavaForeignIdBuilder;
import jetbrains.mps.smodel.SNodeId.Foreign;
import jetbrains.mps.smodel.SNodePointer;
import jetbrains.mps.smodel.StaticReference;
import jetbrains.mps.util.Pair;
import jetbrains.mps.workbench.goTo.index.SNodeDescriptor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.EditableSModel;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeId;
import org.jetbrains.mps.openapi.model.SNodeReference;
import org.jetbrains.mps.openapi.model.SReference;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.module.SearchScope;

import java.util.Collection;
import java.util.LinkedList;

/**
 * danilla 3/24/13
 */

public class IdPrefixSearch extends QueryExecutorBase<PsiReference, SearchParameters> {

  public IdPrefixSearch() {
    // flag: requires read action
    super(true);
  }

  @Override
  public void processQuery(@NotNull SearchParameters queryParameters, @NotNull final Processor<PsiReference> consumer) {

    if (!(queryParameters.getEffectiveSearchScope() instanceof GlobalSearchScope)) {
      return;
    }
    final GlobalSearchScope scope = (GlobalSearchScope) queryParameters.getEffectiveSearchScope();

    final PsiElement target = queryParameters.getElementToSearch();
    if (target instanceof MPSPsiNodeBase) return;

    // Only class names can be prefixes in foreign ids of other nodes
    if (!(target instanceof PsiClass)) return;
    final SRepository repository = ProjectHelper.getProjectRepository(scope.getProject());
    if (repository == null) {
      return;
    }

    repository.getModelAccess().runReadAction(new Runnable() {
      @Override
      public void run() {

        if (target instanceof LightElement) {
          // we don't handle them by default
          return;
        }

        final NodePtr nodePtr = JavaForeignIdBuilder.computeNodePtr(target);
        if (nodePtr == null) return;

        final SNodeReference mpsTarget = new SNodePointer(nodePtr.getSModelReference(), nodePtr.getNodeId());
        // do we have this node?
        if (mpsTarget.resolve(repository) == null) return;

        String prefix = nodePtr.getNodeId().toString();
        final String prefixToSearch = (prefix.startsWith(Foreign.ID_PREFIX) ? prefix.substring(1) : prefix);
        final String prefixToSearchWithDot = prefixToSearch + ".";
        final Project project = target.getProject();

        // first look into changed models
        SearchScope mpsSearchScope = new IdeaSearchScope(scope, true);
        CollectConsumer<VirtualFile> processedFilesConsumer = new CollectConsumer<VirtualFile>();

        for (SModel model : mpsSearchScope.getModels()) {
          boolean changed = model instanceof EditableSModel && ((EditableSModel) model).isChanged();
          if (!changed) continue;

          findInModel(model, prefixToSearch, processedFilesConsumer, new Consumer<SReference>() {
            @Override
            public void consume(SReference ref) {

              String role = ref.getRole();
              SNode source = ref.getSourceNode();

              PsiElement psiNode = MPSPsiProvider.getInstance(project).getPsi(source.getReference());
              assert psiNode instanceof MPSPsiNode;

              consumer.process(new IdPrefixReference(mpsTarget, role, psiNode));
            }
          });
        }

        // now index
        final Collection<VirtualFile> filesOfChangedModels = processedFilesConsumer.getResult();

        GlobalSearchScope truncatedScope = new DelegatingGlobalSearchScope(scope) {
          @Override
          public boolean contains(VirtualFile file) {
            if (filesOfChangedModels.contains(file)) return false;
            return super.contains(file);
          }
        };

        ValueProcessor<Collection<Pair<SNodeDescriptor, String>>> sReferenceProcessor = new ValueProcessor<Collection<Pair<SNodeDescriptor, String>>>() {
          @Override
          public boolean process(VirtualFile file, Collection<Pair<SNodeDescriptor, String>> refs) {
            for (Pair<SNodeDescriptor, String> ref : refs) {
              SNodeReference nodeRef = ref.o1.getNodeReference();
              String role = ref.o2;

              // index got out-of-date on this
              // unfortunately our indices are not always up-to-date, as we don't index yet-unsaved changes
              if (nodeRef.resolve(repository) == null) continue;

              PsiElement psiNode = MPSPsiProvider.getInstance(project).getPsi(nodeRef);

              // original node came from MPS index, it must be converted to our PSI element
              assert psiNode instanceof MPSPsiNode;

              consumer.process(new IdPrefixReference(mpsTarget, role, psiNode));
            }
            return true;
          }
        };

        FileBasedIndex.getInstance().processValues(ForeignIdReferenceIndex.ID, prefixToSearchWithDot, null, sReferenceProcessor, truncatedScope);
      }
    });
  }

  private void findInModel(SModel model, String prefix, Consumer<VirtualFile> processedConsumer, Consumer<SReference> consumer) {
    for (SNode root : model.getRootNodes()) {
      findPrefixReferences(prefix, root, consumer);
    }
  }

  private void findPrefixReferences(String prefix, SNode node, Consumer<SReference> consumer) {
    LinkedList<SNode> queue = new LinkedList<SNode>();
    queue.add(node);

    while (!queue.isEmpty()) {
      SNode n = queue.pop();
      for (SReference ref : n.getReferences()) {
        processReference(prefix, ref, consumer);
      }
      for (SNode child : n.getChildren()) {
        queue.add(child);
      }
    }
  }

  private void processReference(String prefix, SReference ref, Consumer<SReference> consumer) {
    if (!(ref instanceof StaticReference)) return;
    SNodeId target = ref.getTargetNodeId();
    if (!(target instanceof Foreign)) return;

    String targetNodeIdStr = target.toString();

    boolean found = false;
    int idx = targetNodeIdStr.indexOf(prefix);

    while (idx >= 0) {
      if (idx == 0) {
        found = true;
        break;
      }

      char c = targetNodeIdStr.charAt(idx - 1);
      if (c == '.' || Character.isLetterOrDigit(c)) {
        idx = targetNodeIdStr.indexOf(prefix, idx + 1);
        continue;
      }

      found = true;
      break;
    }

    if (found) {
      consumer.consume(ref);
    }
  }
}