/*
 * 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.DumbService;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiJavaFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiReference;
import com.intellij.psi.impl.light.LightElement;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.psi.search.searches.ReferencesSearch.SearchParameters;
import com.intellij.util.Processor;
import jetbrains.mps.baseLanguage.javastub.ASMNodeId;
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.psi.impl.MPSPsiRef;
import jetbrains.mps.idea.core.refactoring.NodePtr;
import jetbrains.mps.idea.core.usages.IdeaSearchScope;
import jetbrains.mps.idea.java.psiStubs.JavaForeignIdBuilder;
import jetbrains.mps.persistence.java.library.JavaClassStubModelDescriptor;
import jetbrains.mps.smodel.ModuleRepositoryFacade;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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.SReference;
import org.jetbrains.mps.openapi.module.FindUsagesFacade;
import org.jetbrains.mps.openapi.module.SRepository;

import java.util.HashSet;
import java.util.Set;

/**
 * danilla 2/13/13
 */

// NOTE: It should be invoked by Idea only for classes and fields
// For methods MethodReferenceSearch is used.
// Local variables shouldn't bother us since we can't reference java code's local vars
public class MPSReferenceSearch extends QueryExecutorBase<PsiReference, ReferencesSearch.SearchParameters> {
  public MPSReferenceSearch() {
    // flag: requires read action
    super(true);
  }

  @Override
  public void processQuery(@NotNull SearchParameters queryParameters, final @NotNull Processor<PsiReference> consumer) {
    if (!(queryParameters.getEffectiveSearchScope() instanceof GlobalSearchScope)) {
      return;
    }

    final GlobalSearchScope scope = (GlobalSearchScope) queryParameters.getEffectiveSearchScope();

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

    final Project project = psiTarget.getProject();
    final MPSPsiProvider psiProvider = MPSPsiProvider.getInstance(project);
    SRepository repository = ProjectHelper.getProjectRepository(project);

    repository.getModelAccess().runReadAction(new Runnable() {
      @Override
      public void run() {
        if (DumbService.getInstance(project).isDumb()) {
          return;
        }

        if (psiTarget instanceof LightElement) {
          // we don't handle light psi elements we don't know about
          // we may not be able to compute their node id and their Java meaning can be represented in baseLanguage
          // in a special way
          return;
        }

        // if MPSReferenceSearch is moved to mps-core, it will be MPS2PsiMapperUtil.getNodeId
        final SNode targetNode = getNodeForElement(psiTarget);
        if (targetNode == null) {
          // it can't be referenced from MPS
          return;
        }

        Set<SNode> targetNodes = new HashSet<SNode>(1);
        targetNodes.add(targetNode);

        Set<SReference> references;
        try {
          references = FindUsagesFacade.getInstance().findUsages(new IdeaSearchScope(scope), targetNodes, null);
        } catch (IndexNotReadyException e) {
          // DumbService doesn't seem to work
          return;
        }

        for (SReference sReference : references) {
          SNode source = sReference.getSourceNode();
          MPSPsiNode psiNode = (MPSPsiNode) psiProvider.getPsi(source);
          // the source could have come from the psi stub itself
          if (psiNode == null) return;
          String refRole = sReference.getRole();
          MPSPsiRef[] refs = psiNode.getReferences(refRole);
          if (refs.length == 0) continue;

          for (MPSPsiRef r : refs) {
            if (targetNode.getNodeId().equals(r.getNodeId())) {
              // it's our reference: giving it out to find usages
              consumer.process(r.getReference());
            }
          }
        }

      }

    });
  }

  // Maybe will go to MPS2PsiMapper
  @Nullable
  public static SNode getNodeForElement(PsiElement element) {
    // baseLanguage specific check
    if (!(element instanceof PsiClass || element instanceof PsiMethod || element instanceof PsiField)) {
      return null;
    }

    PsiFile psiFile = element.getContainingFile();
    if (!(psiFile instanceof PsiJavaFile)) {
      return null;
    }

    if (PsiManager.getInstance(element.getProject()).isInProject(element)) {
      // It must be sources, try psi stubs
      // There might be psi stubs for this element, but there also might not (e.g. if it's inside a module
      // with no MPS facet)

      NodePtr nodePtr = JavaForeignIdBuilder.computeNodePtr(element);
      if (nodePtr == null) {
        return null;
      }
      SNode node = nodePtr.toSNodeReference().resolve(ProjectHelper.getProjectRepository(element.getProject()));

      return node;

    } else {
      // It must be from a library or SDK or something like that
      // Trying to find a suitable node in a class stub model
      SNodeId nodeId = null;
      if (element instanceof PsiClass) {
        nodeId = ASMNodeId.createId(((PsiClass) element).getQualifiedName());

      } else if (element instanceof PsiField) {
        PsiClass clas = ((PsiField) element).getContainingClass();
        String clasFqName = clas.getQualifiedName();
        nodeId = ASMNodeId.createFieldId(clasFqName, ((PsiField) element).getName());

      } else if (element instanceof PsiMethod) {
        // TODO argument types must be handled like they are in ASMNodeId (via asm classes)
      }

      if (nodeId == null) {
        // can't do anything about it
        return null;
      }

      String packageName = ((PsiJavaFile) psiFile).getPackageName();
      for (SModel model : new ModuleRepositoryFacade(ProjectHelper.getProjectRepository(element.getProject())).getAllModels()) {
        if (!(model instanceof JavaClassStubModelDescriptor)) continue;
        if (!packageName.equals(model.getName().getLongName())) {
          continue;
        }
        SNode node = model.getNode(nodeId);
        if (node == null) continue;
        return node;
      }
    }

    return null;
  }
}
