package jetbrains.mps.baseLanguage.closures.helper;

/*Generated by MPS */

import jetbrains.mps.generator.template.TemplateQueryContext;
import org.jetbrains.mps.openapi.model.SNode;
import jetbrains.mps.lang.smodel.generator.smodelAdapter.SConceptOperations;
import jetbrains.mps.lang.smodel.generator.smodelAdapter.SNodeOperations;
import jetbrains.mps.lang.smodel.generator.smodelAdapter.SLinkOperations;
import java.util.List;
import jetbrains.mps.internal.collections.runtime.ListSequence;
import java.util.ArrayList;
import jetbrains.mps.baseLanguage.behavior.Classifier__BehaviorDescriptor;
import jetbrains.mps.internal.collections.runtime.Sequence;
import jetbrains.mps.baseLanguage.closures.constraints.ClassifierTypeUtil;
import jetbrains.mps.typechecking.TypecheckingFacade;
import jetbrains.mps.baseLanguage.closures.behavior.FunctionType__BehaviorDescriptor;
import jetbrains.mps.util.JavaNameUtil;
import jetbrains.mps.lang.smodel.generator.smodelAdapter.SPropertyOperations;
import jetbrains.mps.lang.smodel.generator.smodelAdapter.SModelOperations;
import jetbrains.mps.smodel.adapter.structure.MetaAdapterFactory;
import java.util.Iterator;
import jetbrains.mps.internal.collections.runtime.ISelector;
import jetbrains.mps.lang.core.behavior.BaseConcept__BehaviorDescriptor;
import java.util.Map;
import jetbrains.mps.internal.collections.runtime.MapSequence;
import org.jetbrains.mps.openapi.language.SAbstractConcept;
import jetbrains.mps.baseLanguage.tuples.runtime.Tuples;
import jetbrains.mps.baseLanguage.closures.util.FunctionalInterfaceHelper;
import org.jetbrains.mps.openapi.language.SReferenceLink;
import org.jetbrains.mps.openapi.language.SContainmentLink;
import org.jetbrains.mps.openapi.language.SConcept;
import org.jetbrains.mps.openapi.language.SProperty;

public class ClosureLiteralTarget {
  private TemplateQueryContext genContext;
  public ClosureLiteralTarget(TemplateQueryContext genContext) {
    this.genContext = genContext;
  }
  public void setTarget(SNode literal, SNode targetIface, SNode actualType) {
    SNode meth = getFunctionMethod(literal, targetIface);
    if ((meth != null)) {
      // This is only applicable if there's an existing function
      // TODO if not, it is probably only applicable for Object as a target, maybe an error is necessary
      SNode targetIfaceErase = SConceptOperations.createNewNode(SNodeOperations.asInstanceConcept(SNodeOperations.getConcept(targetIface)));
      SLinkOperations.setTarget(targetIfaceErase, LINKS.classifier$cxMr, SLinkOperations.getTarget(targetIface, LINKS.classifier$cxMr));
      matchTypeParameters(meth, literal, targetIfaceErase, SLinkOperations.getChildren(targetIface, LINKS.parameter$oqG$));

      targetIface = targetIfaceErase;
    }

    Values.LITERAL.set(genContext, targetIface, literal);
    Values.LITERAL_TARGET.set(genContext, literal, targetIface);
    Values.LITERAL_TYPE.set(genContext, literal, actualType);

    if (Values.LITERAL_RETURN_DEPEDENCY.isSet(genContext, literal)) {
      List<SNode> dependants = (List<SNode>) Values.LITERAL_RETURN_DEPEDENCY.get(genContext, literal);
      final SNode type = resolveIfaceReturnType(targetIface);

      ListSequence.fromList(dependants).visitAll((it) -> FunctionTypeUtil.prepAdaptations(genContext, type, it));
      ListSequence.fromList(dependants).clear();
    }
  }

  /**
   * Handle dependency of the given rexpr on the return type of the given closure literal. The target type of the expression
   * will be computed if/once the target type of the dependency is resolved.
   * 
   * @param parentToDependOn closure whose return type is required
   * @param rexpr expresion to apply to return type of the closure on
   */
  public void setReturnDependency(SNode parentToDependOn, SNode rexpr) {
    if (Values.LITERAL_TARGET.isSet(genContext, parentToDependOn)) {
      // Closure target type already resolved: call prep adaptations
      SNode targetType = (SNode) Values.LITERAL_TARGET.get(genContext, parentToDependOn);
      SNode type = resolveIfaceReturnType(targetType);
      FunctionTypeUtil.prepAdaptations(genContext, type, rexpr);
    } else {
      // Otherwise save dependency for later: see setTarget()
      List<SNode> dependencies;

      if (!(Values.LITERAL_RETURN_DEPEDENCY.isSet(genContext, parentToDependOn))) {
        dependencies = new ArrayList<SNode>();
        Values.LITERAL_RETURN_DEPEDENCY.set(genContext, parentToDependOn, dependencies);
      } else {
        dependencies = (List<SNode>) Values.LITERAL_RETURN_DEPEDENCY.get(genContext, parentToDependOn);
      }

      ListSequence.fromList(dependencies).addElement(rexpr);
    }
  }

  private SNode resolveIfaceReturnType(SNode targetType) {
    Iterable<SNode> imds = Classifier__BehaviorDescriptor.methods_id4_LVZ3pBKCn.invoke(SLinkOperations.getTarget(targetType, LINKS.classifier$cxMr));
    if (Sequence.fromIterable(imds).isNotEmpty()) {
      return FunctionTypeUtil.unmeet(FunctionTypeUtil.unbound(ClassifierTypeUtil.resolveType(SLinkOperations.getTarget(Sequence.fromIterable(imds).first(), LINKS.returnType$5xoi), targetType)));
    }

    return null;
  }

  private void matchTypeParameters(SNode meth, SNode literal, SNode targetIfaceErase, List<SNode> reifiedTargetIfaceTypeParams) {
    SNode funType = SNodeOperations.cast(TypecheckingFacade.getFromContext().getTypeOf(literal), CONCEPTS.FunctionType$9U);
    TypeMatcher matcher = new TypeMatcher();

    if ((SLinkOperations.getTarget(meth, LINKS.returnType$5xoi) != null) && !(SNodeOperations.isInstanceOf(SLinkOperations.getTarget(meth, LINKS.returnType$5xoi), CONCEPTS.VoidType$BF))) {
      matcher.matchReturnType(SLinkOperations.getTarget(meth, LINKS.returnType$5xoi), FunctionType__BehaviorDescriptor.getNormalizedReturnType_idhEwIOp4.invoke(funType));
      if (SNodeOperations.isInstanceOf(SLinkOperations.getTarget(meth, LINKS.returnType$5xoi), CONCEPTS.ClassifierType$bL)) {
        // TODO: refactor this code
        SNode absRetCT = SNodeOperations.copyNode(SNodeOperations.cast(SLinkOperations.getTarget(meth, LINKS.returnType$5xoi), CONCEPTS.ClassifierType$bL));
        SNode ftResCT = FunctionTypeUtil.unmeet(FunctionType__BehaviorDescriptor.getNormalizedReturnType_idhEwIOp4.invoke(funType));
        String adapterName = JavaNameUtil.shortName(SPropertyOperations.getString(SLinkOperations.getTarget(absRetCT, LINKS.classifier$cxMr), PROPS.name$MnvL)) + JavaNameUtil.shortName(SPropertyOperations.getString(SLinkOperations.getTarget(SNodeOperations.as(FunctionTypeUtil.unmeet(FunctionTypeUtil.unbound(ftResCT)), CONCEPTS.ClassifierType$bL), LINKS.classifier$cxMr), PROPS.name$MnvL)) + "Adapter";
        for (SNode cls : SModelOperations.nodes(SNodeOperations.getModel(SLinkOperations.getTarget(absRetCT, LINKS.classifier$cxMr)), CONCEPTS.Classifier$Ix)) {
          if (adapterName.equals(JavaNameUtil.shortName(SPropertyOperations.getString(cls, PROPS.name$MnvL)))) {
            SNode newRetCT = SConceptOperations.createNewNode(MetaAdapterFactory.getConcept(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0x101de48bf9eL, "jetbrains.mps.baseLanguage.structure.ClassifierType"));
            SLinkOperations.setTarget(newRetCT, LINKS.classifier$cxMr, cls);
            Values.RETURN_TYPE.set(genContext, targetIfaceErase, newRetCT);
            break;
          }
        }
      }
    }

    {
      Iterator<SNode> funParamIt = ListSequence.fromList(FunctionType__BehaviorDescriptor.getNormalizedParameterTypes_idhEwIOps.invoke(funType)).iterator();
      Iterator<SNode> methParamIt = ListSequence.fromList(SLinkOperations.getChildren(meth, LINKS.parameter$5xBj)).iterator();
      while (funParamIt.hasNext() && methParamIt.hasNext()) {
        matcher.matchType(SLinkOperations.getTarget(methParamIt.next(), LINKS.type$a1UY), funParamIt.next());
      }
      if (funParamIt.hasNext() || methParamIt.hasNext()) {
        genContext.showErrorMessage(literal, "Closure parameters count doesn't match method '" + SPropertyOperations.getString(meth, PROPS.name$MnvL) + "' in " + JavaNameUtil.fqClassName(SLinkOperations.getTarget(targetIfaceErase, LINKS.classifier$cxMr), SPropertyOperations.getString(SLinkOperations.getTarget(targetIfaceErase, LINKS.classifier$cxMr), PROPS.name$MnvL)));
      }
    }

    {
      // TODO: rewrite throws types matching algorithm
      List<SNode> normFunThrowsTypes = FunctionType__BehaviorDescriptor.getNormalizedThrowsTypes_id2ZrgyPlTDZP.invoke(funType);
      Iterator<SNode> funThrIt = ListSequence.fromList(normFunThrowsTypes).sort(new ISelector<SNode, Comparable<?>>() {
        @Override
        public Comparable<?> select(SNode t) {
          String key = Classifier__BehaviorDescriptor.getNestedName_id7q4lzBFjvIX.invoke(SLinkOperations.getTarget(SNodeOperations.as(t, CONCEPTS.ClassifierType$bL), LINKS.classifier$cxMr));
          return (key != null ? key : BaseConcept__BehaviorDescriptor.getPresentation_idhEwIMiw.invoke(t));
        }
      }, true).iterator();
      Iterator<SNode> methThrIt = ListSequence.fromList(FunctionTypeUtil.normalizeThrowsTypes(SLinkOperations.getChildren(meth, LINKS.throwsItem$CdW$))).iterator();

      while (funThrIt.hasNext() && methThrIt.hasNext()) {
        matcher.matchType(methThrIt.next(), funThrIt.next());
      }
      if (funThrIt.hasNext()) {
        // method can declare extra thrown types, it's alright to ignore these
        genContext.showWarningMessage(literal, "Closure generic throws types count doesn't match method '" + SPropertyOperations.getString(meth, PROPS.name$MnvL) + "' in " + JavaNameUtil.fqClassName(SLinkOperations.getTarget(targetIfaceErase, LINKS.classifier$cxMr), SPropertyOperations.getString(SLinkOperations.getTarget(targetIfaceErase, LINKS.classifier$cxMr), PROPS.name$MnvL)));
      }
    }

    Values.TYPE_MAP.set(genContext, targetIfaceErase, matcher.getMap());

    // Copy reified target ClassifierType parameters to the erasure target ClassifierType
    Map<SNode, SNode> map = matcher.getMap();
    if (map != null) {
      Iterator<SNode> reifiedTypeIt = ListSequence.fromList(reifiedTargetIfaceTypeParams).iterator();
      Iterator<SNode> targetTypeVarIt = ListSequence.fromList(SLinkOperations.getChildren(SLinkOperations.getTarget(targetIfaceErase, LINKS.classifier$cxMr), LINKS.typeVariableDeclaration$Lipp)).iterator();
      while (reifiedTypeIt.hasNext() && targetTypeVarIt.hasNext()) {
        SNode typeVar = targetTypeVarIt.next();
        SNode rtp = reifiedTypeIt.next();
        SNode substituteType = (MapSequence.fromMap(map).containsKey(typeVar) ? SNodeOperations.copyNode(MapSequence.fromMap(map).get(typeVar)) : SNodeOperations.copyNode(rtp));
        // TODO: we need a better way to handle wildcards in the substitutes
        // Any (completely) reified type has precedence over calculated function type's parameter
        // Example: String s = { => null; } must use String, not Object as the return value
        if (!(hasTypeVariable(rtp)) && !(TypecheckingFacade.getFromContext().isSubtype(substituteType, rtp))) {
          substituteType = ClassifierTypeUtil.getTypeCoercedToClassifierType(SNodeOperations.copyNode(rtp));
        }
        SNode param = ListSequence.fromList(SLinkOperations.getChildren(targetIfaceErase, LINKS.parameter$oqG$)).addElement(substituteType);
      }
      if (reifiedTypeIt.hasNext() || targetTypeVarIt.hasNext()) {
        genContext.showWarningMessage(literal, "Reified type parameters count doesn't match method declaration '" + SPropertyOperations.getString(meth, PROPS.name$MnvL) + "' in " + JavaNameUtil.fqClassName(SLinkOperations.getTarget(targetIfaceErase, LINKS.classifier$cxMr), SPropertyOperations.getString(SLinkOperations.getTarget(targetIfaceErase, LINKS.classifier$cxMr), PROPS.name$MnvL)));
      }
    }
  }
  private boolean hasTypeVariable(SNode type) {
    return ListSequence.fromList(SNodeOperations.getNodeDescendants(type, CONCEPTS.TypeVariableReference$WL, true, new SAbstractConcept[]{})).isNotEmpty();
  }
  private SNode getFunctionMethod(SNode literal, SNode targetIface) {
    Tuples._2<SNode, String> functionalMethod = FunctionalInterfaceHelper.getFunctionalMethod(SLinkOperations.getTarget(targetIface, LINKS.classifier$cxMr));

    if (functionalMethod._1() == FunctionalInterfaceHelper.MORE_THAN_ONE_METHOD) {
      genContext.showWarningMessage(literal, "The adaptation target interface has more than one method");
    }

    return functionalMethod._0();
  }

  private static final class LINKS {
    /*package*/ static final SReferenceLink classifier$cxMr = MetaAdapterFactory.getReferenceLink(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0x101de48bf9eL, 0x101de490babL, "classifier");
    /*package*/ static final SContainmentLink parameter$oqG$ = MetaAdapterFactory.getContainmentLink(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0x101de48bf9eL, 0x102419671abL, "parameter");
    /*package*/ static final SContainmentLink returnType$5xoi = MetaAdapterFactory.getContainmentLink(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0xf8cc56b1fcL, 0xf8cc56b1fdL, "returnType");
    /*package*/ static final SContainmentLink parameter$5xBj = MetaAdapterFactory.getContainmentLink(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0xf8cc56b1fcL, 0xf8cc56b1feL, "parameter");
    /*package*/ static final SContainmentLink type$a1UY = MetaAdapterFactory.getContainmentLink(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0x450368d90ce15bc3L, 0x4ed4d318133c80ceL, "type");
    /*package*/ static final SContainmentLink throwsItem$CdW$ = MetaAdapterFactory.getContainmentLink(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0xf8cc56b1fcL, 0x10f383d6949L, "throwsItem");
    /*package*/ static final SContainmentLink typeVariableDeclaration$Lipp = MetaAdapterFactory.getContainmentLink(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0x102463b447aL, 0x102463bb98eL, "typeVariableDeclaration");
  }

  private static final class CONCEPTS {
    /*package*/ static final SConcept FunctionType$9U = MetaAdapterFactory.getConcept(0xfd3920347849419dL, 0x907112563d152375L, 0x1174a4d19ffL, "jetbrains.mps.baseLanguage.closures.structure.FunctionType");
    /*package*/ static final SConcept ClassifierType$bL = MetaAdapterFactory.getConcept(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0x101de48bf9eL, "jetbrains.mps.baseLanguage.structure.ClassifierType");
    /*package*/ static final SConcept Classifier$Ix = MetaAdapterFactory.getConcept(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0x101d9d3ca30L, "jetbrains.mps.baseLanguage.structure.Classifier");
    /*package*/ static final SConcept VoidType$BF = MetaAdapterFactory.getConcept(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0xf8cc6bf96dL, "jetbrains.mps.baseLanguage.structure.VoidType");
    /*package*/ static final SConcept TypeVariableReference$WL = MetaAdapterFactory.getConcept(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, 0x102467229d8L, "jetbrains.mps.baseLanguage.structure.TypeVariableReference");
  }

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