package jetbrains.mps.baseLanguage.util;

/*Generated by MPS */

import java.util.Map;
import org.jetbrains.mps.openapi.module.SModule;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import org.jetbrains.mps.openapi.model.SNode;
import jetbrains.mps.lang.smodel.generator.smodelAdapter.SNodeOperations;
import jetbrains.mps.internal.collections.runtime.ListSequence;
import java.util.ArrayList;
import jetbrains.mps.project.SModuleOperations;
import org.jetbrains.mps.openapi.module.SRepository;
import jetbrains.mps.lang.core.behavior.INamedConcept__BehaviorDescriptor;
import jetbrains.mps.util.JavaNameUtil;
import org.jetbrains.mps.openapi.model.SModelId;
import jetbrains.mps.java.stub.JavaPackageNameStub;
import org.jetbrains.mps.openapi.model.SNodeId;
import jetbrains.mps.internal.collections.runtime.Sequence;
import org.jetbrains.mps.openapi.model.SModel;
import jetbrains.mps.project.dependency.GlobalModuleDependenciesManager;
import jetbrains.mps.smodel.SModelStereotype;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SAbstractConcept;
import java.util.Objects;
import jetbrains.mps.lang.smodel.generator.smodelAdapter.SLinkOperations;
import jetbrains.mps.smodel.SNodePointer;
import jetbrains.mps.lang.smodel.generator.smodelAdapter.SPropertyOperations;
import org.jetbrains.mps.openapi.model.SNodeReference;
import jetbrains.mps.persistence.PersistenceRegistry;
import jetbrains.mps.lang.smodel.generator.smodelAdapter.SConceptOperations;
import jetbrains.mps.baseLanguage.behavior.BaseMethodDeclaration__BehaviorDescriptor;
import jetbrains.mps.lang.smodel.ConceptSwitchIndex;
import jetbrains.mps.lang.smodel.ConceptSwitchIndexBuilder;
import jetbrains.mps.smodel.adapter.ids.MetaIdFactory;
import org.jetbrains.mps.openapi.language.SConcept;
import jetbrains.mps.smodel.adapter.structure.MetaAdapterFactory;
import org.jetbrains.mps.openapi.language.SInterfaceConcept;
import org.jetbrains.mps.openapi.language.SContainmentLink;
import org.jetbrains.mps.openapi.language.SReferenceLink;
import org.jetbrains.mps.openapi.language.SProperty;

/**
 * Facility to match node<Classifier> against its stub counterpart (and vice versa).
 * For a node<ClassConcept> MPS generates Java code, which might get deployed as IDEA code and brought back
 * to MPS as a Java stub. This class helps to establish equivalent node<Classifier> for a stub node as well
 * as to reveal stub node for a node<Classifier>
 * 
 * Instances of this class cache SModule instances and shall not span more but single model read.
 * 
 * Note, this class expect nodes attached to a model from a repository; it's of no use for detached nodes.
 */
public final class StubClassDiscovery {
  private final boolean mySearchInVisibleModulesOnly;
  private final Map<SModule, Collection<SModule>> myModule2VisibleStubOwners;
  private Collection<SModule> myAllStubOwners;

  public StubClassDiscovery() {
    this(true);
    // I expect this to be the general case, although decided to keep 'whole repo' alternative
    // in case we'd need to perform 'workspace'-wide lookup (e.g. to find possible imports
    // Perhaps, shall extend this class with SModule scope and explicit repository to perform look-ups?
  }

  public StubClassDiscovery(boolean searchInVisibleModulesOnly) {
    mySearchInVisibleModulesOnly = searchInVisibleModulesOnly;
    // we use myAllStubOwners only when searchInVisibleModulesOnly == false,
    // otherwise, we cache lookup data in the map.
    myAllStubOwners = null;
    if (searchInVisibleModulesOnly) {
      myModule2VisibleStubOwners = new HashMap<>();
    } else {
      myModule2VisibleStubOwners = null;
    }
  }


  public List<SNode> findStubClassifiers(SNode nodeClassifier) {
    SModule mpsModule = check_h9urwj_a0a0j(SNodeOperations.getModel(nodeClassifier));
    List<SNode> result = ListSequence.fromList(new ArrayList<SNode>());
    if (!(SModuleOperations.isJavaModuleCompiledExternally(mpsModule))) {
      return result;
    }
    assert mpsModule != null : "SModuleOperations.isJavaModuleCompiledExternally contract";
    SRepository repository = mpsModule.getRepository();
    if (repository == null) {
      return null;
    }
    String fqName = INamedConcept__BehaviorDescriptor.getFqName_idhEwIO9y.invoke(nodeClassifier);
    String packageName = JavaNameUtil.packageName(SNodeOperations.getModel(nodeClassifier));
    String nestedShortName = fqName.substring(packageName.length() + 1).replace('.', '$');
    SModelId stubModelId = new JavaPackageNameStub(packageName).asModelId();
    SNodeId nodeId = new jetbrains.mps.smodel.SNodeId.Foreign(jetbrains.mps.smodel.SNodeId.Foreign.ID_PREFIX + nestedShortName);

    for (SModule m : Sequence.fromIterable(modulesWithStubModels(mpsModule))) {
      SModel model = m.getModel(stubModelId);
      if (model == null) {
        continue;
      }
      SNode stubClass = model.getNode(nodeId);
      if (SNodeOperations.isInstanceOf(stubClass, CONCEPTS.Classifier$Ix)) {
        ListSequence.fromList(result).addElement(SNodeOperations.cast(stubClass, CONCEPTS.Classifier$Ix));
      }
    }
    return result;
  }

  private Iterable<SModule> modulesWithStubModels(SModule start) {
    // start != null && attached to a repo
    Iterable<SModule> scope;
    if (mySearchInVisibleModulesOnly) {
      Collection<SModule> cached = myModule2VisibleStubOwners.get(start);
      if (cached != null) {
        return cached;
      }
      GlobalModuleDependenciesManager gmdm = new GlobalModuleDependenciesManager(start);
      scope = gmdm.getModules(GlobalModuleDependenciesManager.Deptype.VISIBLE);
    } else {
      if (myAllStubOwners != null) {
        return myAllStubOwners;
      }
      scope = start.getRepository().getModules();
    }
    ArrayList<SModule> rv = new ArrayList<>();
    for (SModule mod : Sequence.fromIterable(scope)) {
      // XXX if this is slow, consider adding more effective lookup operations into ModuleRepositoryFacade
      //   e.g. those that won't collect but just iterate
      // Alternatively, may want to keep Collection<SModel> with all stub models visible for a module
      // - we do collect these models here anyway.
      if (!(mod.getModels((SModel m) -> SModelStereotype.isStubModel(m)).isEmpty())) {
        rv.add(mod);
      }
    }
    // cache for the next time
    if (mySearchInVisibleModulesOnly) {
      myModule2VisibleStubOwners.put(start, rv);
    } else {
      myAllStubOwners = rv;
    }
    return rv;
  }

  @Nullable
  public SNode findNodeClassifier(final SNode stubClassifier) {
    if (SNodeOperations.getParent(stubClassifier) != null) {
      SNode root = SNodeOperations.as(SNodeOperations.getContainingRoot(stubClassifier), CONCEPTS.Classifier$Ix);
      SNode nodeRoot = findNodeClassifier(root);
      return ListSequence.fromList(SNodeOperations.getNodeDescendants(nodeRoot, CONCEPTS.Classifier$Ix, false, new SAbstractConcept[]{})).where((it) -> Objects.equals(INamedConcept__BehaviorDescriptor.getFqName_idhEwIO9y.invoke(it), INamedConcept__BehaviorDescriptor.getFqName_idhEwIO9y.invoke(stubClassifier))).first();
    }
    SNode generatedAnnotation = ListSequence.fromList(SLinkOperations.getChildren(stubClassifier, LINKS.annotation$K49I)).where((it) -> SLinkOperations.hasPointer(it, LINKS.annotation$12Ek, new SNodePointer("3f233e7f-b8a6-46d2-a57f-795d56775243/java:jetbrains.mps.annotations(Annotations/)", "~GeneratedClass"))).first();
    if (generatedAnnotation == null) {
      return null;
    }
    String nodeParameter = SPropertyOperations.getString(SNodeOperations.as(SLinkOperations.getTarget(ListSequence.fromList(SLinkOperations.getChildren(generatedAnnotation, LINKS.value$uK2B)).where((it) -> SLinkOperations.hasPointer(it, LINKS.key$bSmV, new SNodePointer("3f233e7f-b8a6-46d2-a57f-795d56775243/java:jetbrains.mps.annotations(Annotations/)", "~GeneratedClass.node()"))).first(), LINKS.value$Y7om), CONCEPTS.StringLiteral$xu), PROPS.value$w7MM);
    if (nodeParameter == null) {
      return null;
    }
    SNodeReference nodeReference = PersistenceRegistry.getInstance().createNodeReference(nodeParameter);
    return SNodeOperations.as(nodeReference.resolve(SNodeOperations.getModel(stubClassifier).getRepository()), CONCEPTS.Classifier$Ix);
  }

  public List<SNode> findCompatibleClassifiers(SNode classifier) {
    List<SNode> result = ListSequence.fromList(new ArrayList<SNode>(1));
    if (SNodeOperations.getModel(classifier) == null) {
      return result;
    }
    if (SModelStereotype.isStubModel(SNodeOperations.getModel(classifier))) {
      SNode node = findNodeClassifier(classifier);
      if (node != null) {
        ListSequence.fromList(result).addElement(node);
      }
    } else {
      ListSequence.fromList(result).addSequence(ListSequence.fromList(findStubClassifiers(classifier)));
    }
    return result;
  }

  public boolean areTheSame(SNode c1, SNode c2) {
    if (c1 == c2) {
      return true;
    }
    if (!(Objects.equals(SNodeOperations.getConcept(c1), SNodeOperations.getConcept(c2)))) {
      // this also rules out either is null
      return false;
    }
    // Classifier.isSame() logic which is StubClassDiscovery-related
    // XXX might be worth refactoring as we can check where c1 and c2 come from, and do not collect 
    //    Node/Stub classifiers unless necessary. For now, just copied as is.
    List<SNode> firstMirrors = findCompatibleClassifiers(c1);
    List<SNode> secondMirrors = findCompatibleClassifiers(c2);
    return ListSequence.fromList(firstMirrors).contains(c2) || ListSequence.fromList(secondMirrors).contains(c1);
  }

  /**
   * Look up original node counterpart for a stub classifier member.
   * Intended use: migration of MPS-generated code exposed/referenced as stubs back to original nodes
   */
  public SNode findMemberDeclarationNode(final SNode stubDeclaration) {
    SNode owner = findNodeClassifier(SNodeOperations.getNodeAncestor(stubDeclaration, CONCEPTS.Classifier$Ix, false, false));
    if (owner == null) {
      return (SNodeOperations.isInstanceOf(stubDeclaration, CONCEPTS.Classifier$Ix) ? findNodeClassifier(SNodeOperations.cast(stubDeclaration, CONCEPTS.Classifier$Ix)) : null);
    }
    List<SNode> ownerSameMembers = Sequence.fromIterable(SNodeOperations.ofConcept(SLinkOperations.getChildren(owner, LINKS.member$L_2d), SNodeOperations.asSConcept(SNodeOperations.getConcept(stubDeclaration)))).toList();
    if (ListSequence.fromList(ownerSameMembers).isEmpty()) {
      if (SNodeOperations.isInstanceOf(stubDeclaration, CONCEPTS.ConstructorDeclaration$yG)) {
        SNode scd = SNodeOperations.cast(stubDeclaration, CONCEPTS.ConstructorDeclaration$yG);
        if (ListSequence.fromList(SLinkOperations.getChildren(scd, LINKS.parameter$5xBj)).isEmpty()) {
          // assume it's default constructor invocation, which is a member in stubs but a reference to classifier (DefaultClassCreator)
          // in case of regular nodes
          // This seems to be a hack - how come I'm sure the Classifier is fine to substitute this stubDeclaration target?
          return owner;
        }
      }
      return null;
    }
    SAbstractConcept cncpt = SNodeOperations.getConcept(stubDeclaration);
    switch (conceptIndex.index(cncpt)) {
      case 0:
        return Sequence.fromIterable(SNodeOperations.ofConcept(ownerSameMembers, CONCEPTS.FieldDeclaration$ie)).findFirst((it) -> Objects.equals(SPropertyOperations.getString(it, PROPS.name$MnvL), SPropertyOperations.getString(SNodeOperations.cast(stubDeclaration, CONCEPTS.FieldDeclaration$ie), PROPS.name$MnvL)));
      case 1:
        return Sequence.fromIterable(SNodeOperations.ofConcept(ownerSameMembers, CONCEPTS.StaticFieldDeclaration$jR)).findFirst((it) -> Objects.equals(SPropertyOperations.getString(it, PROPS.name$MnvL), SPropertyOperations.getString(SNodeOperations.cast(stubDeclaration, CONCEPTS.StaticFieldDeclaration$jR), PROPS.name$MnvL)));
      default:
        boolean noneMatched = true;
        if (noneMatched && SConceptOperations.isSubConceptOf(cncpt, CONCEPTS.BaseMethodDeclaration$kD)) {
          noneMatched = false;
          // covers InstanceMethodDeclaration, StaticMethodDeclaration and ConstructorDeclaration
          final SNode md = SNodeOperations.cast(stubDeclaration, CONCEPTS.BaseMethodDeclaration$kD);
          // Not sure if it's proper to use hasSameSignature/hasSameParameters as same method parameters may use
          // counterparts (node/stub) of the same target type, and I'm afraid erasure signature may not detect they are the same in all cases.
          // FWIW, cast to ClassifierMember is ok as we know stubDeclaration is CM, therefore filtered members are CM, too
          return SNodeOperations.as(Sequence.fromIterable(SNodeOperations.ofConcept(ownerSameMembers, CONCEPTS.BaseMethodDeclaration$kD)).findFirst((it) -> Objects.equals(SPropertyOperations.getString(it, PROPS.name$MnvL), SPropertyOperations.getString(md, PROPS.name$MnvL)) && (boolean) BaseMethodDeclaration__BehaviorDescriptor.hasSameParameters_idJuSt8W4$Q2.invoke(md, it)), CONCEPTS.ClassifierMember$At);
        }
    }
    return null;
  }
  private static SModule check_h9urwj_a0a0j(SModel checkedDotOperand) {
    if (null != checkedDotOperand) {
      return checkedDotOperand.getModule();
    }
    return null;
  }
  private static final ConceptSwitchIndex conceptIndex = new ConceptSwitchIndexBuilder().put(MetaIdFactory.conceptId(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0xf8c108ca68L), MetaIdFactory.conceptId(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0xf93c84351fL)).seal();

  private static final class CONCEPTS {
    /*package*/ static final SConcept Classifier$Ix = MetaAdapterFactory.getConcept(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0x101d9d3ca30L, "jetbrains.mps.baseLanguage.structure.Classifier");
    /*package*/ static final SConcept StringLiteral$xu = MetaAdapterFactory.getConcept(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0xf93d565d10L, "jetbrains.mps.baseLanguage.structure.StringLiteral");
    /*package*/ static final SConcept ConstructorDeclaration$yG = MetaAdapterFactory.getConcept(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0xf8cc56b204L, "jetbrains.mps.baseLanguage.structure.ConstructorDeclaration");
    /*package*/ static final SConcept BaseMethodDeclaration$kD = MetaAdapterFactory.getConcept(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0xf8cc56b1fcL, "jetbrains.mps.baseLanguage.structure.BaseMethodDeclaration");
    /*package*/ static final SInterfaceConcept ClassifierMember$At = MetaAdapterFactory.getInterfaceConcept(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0x112574373bdL, "jetbrains.mps.baseLanguage.structure.ClassifierMember");
    /*package*/ static final SConcept FieldDeclaration$ie = MetaAdapterFactory.getConcept(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0xf8c108ca68L, "jetbrains.mps.baseLanguage.structure.FieldDeclaration");
    /*package*/ static final SConcept StaticFieldDeclaration$jR = MetaAdapterFactory.getConcept(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0xf93c84351fL, "jetbrains.mps.baseLanguage.structure.StaticFieldDeclaration");
  }

  private static final class LINKS {
    /*package*/ static final SContainmentLink annotation$K49I = MetaAdapterFactory.getContainmentLink(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0x114a6be947aL, 0x114a6beb0bdL, "annotation");
    /*package*/ static final SReferenceLink annotation$12Ek = MetaAdapterFactory.getReferenceLink(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0x114a6b4ccabL, 0x114a6b85d40L, "annotation");
    /*package*/ static final SContainmentLink value$uK2B = MetaAdapterFactory.getContainmentLink(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0x114a6b4ccabL, 0x114a71c697fL, "value");
    /*package*/ static final SReferenceLink key$bSmV = MetaAdapterFactory.getReferenceLink(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0x114a71b1af4L, 0x114a71b44e3L, "key");
    /*package*/ static final SContainmentLink value$Y7om = MetaAdapterFactory.getContainmentLink(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0x114a71b1af4L, 0x114a71c0fc4L, "value");
    /*package*/ static final SContainmentLink member$L_2d = MetaAdapterFactory.getContainmentLink(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0x101d9d3ca30L, 0x4a9a46de59132803L, "member");
    /*package*/ static final SContainmentLink parameter$5xBj = MetaAdapterFactory.getContainmentLink(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0xf8cc56b1fcL, 0xf8cc56b1feL, "parameter");
  }

  private static final class PROPS {
    /*package*/ static final SProperty value$w7MM = MetaAdapterFactory.getProperty(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0xf93d565d10L, 0xf93d565d11L, "value");
    /*package*/ static final SProperty name$MnvL = MetaAdapterFactory.getProperty(0xceab519525ea4f22L, 0x9b92103b95ca8c0cL, 0x110396eaaa4L, 0x110396ec041L, "name");
  }
}
