/*
 * Copyright 2003-2016 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.generator.impl;

import jetbrains.mps.classloading.ClassLoaderManager;
import jetbrains.mps.generator.GenerationSessionContext;
import jetbrains.mps.generator.ModelExports;
import jetbrains.mps.generator.crossmodel.ExportLabelContext;
import jetbrains.mps.generator.runtime.TemplateContext;
import jetbrains.mps.smodel.SModelStereotype;
import jetbrains.mps.smodel.SModelUtil_new;
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 java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

/**
 * Counterpart to {@link jetbrains.mps.generator.impl.GenerationSession} to handle exports
 * during generation of a given model.
 *
 * There's one ExportsSessionContext per model generation/GenerationSession.
 * To facilitate access from multiple generation threads, some instance methods are synchronized on <code>this</code>
 *
 * @author Artem Tikhomirov
 */
public class ExportsSessionContext {
  private final ExportsVault myExportsVault;
  private final GenerationSessionContext myContext;
  private SModel myExportsModel;

  public ExportsSessionContext(@NotNull ExportsVault exportsVault, @NotNull GenerationSessionContext context) {
    myExportsVault = exportsVault;
    myContext = context;
  }

  /**
   * @return holder for exports of the current generation session's model, or <code>null</code> if there are no exports
   */
  @Nullable
  public ModelExports getExports() {
    if (myExportsModel == null || !myExportsModel.getRootNodes().iterator().hasNext()) {
      return null;
    }
    return new ModelExports(myExportsModel);
  }


  // ExportEntry shall keep:
  //        SNodeReference to export label (at least now expect ExportLabel to be accessible)
  //        SNode of Keeper object (now through myValue.getKeeper())
  //        SModelReference / model name for the proxy model
  //        identity of input node (concept and node id) to match
  //        identity of output node (concept, to instantiate, node id for proxy instance)
  public void record(TemplateContext templateContext, SNode/*node<ExportLabel>*/ exportLabel, List<SNode> values) {
    if (values.isEmpty()) {
      return;
    }
    String functionName = CrossModelUtil.getMarshalFunctionName(exportLabel);
    final SNode keeperConcept = exportLabel.getReferenceTarget("dataHolder");
    // XXX here we imply templateContext.inputNode is from the model being processed (though technically it's feasible to have inputNode
    // from another model). We need original (not transient) model for streamManager to find proper location
    assert templateContext.getEnvironment().getGenerator().getInputModel() == templateContext.getInput().getModel();
    // ExportsSessionContext
    synchronized (this) {
      if (myExportsModel == null) {
        // when the model is being generated, overwrite any existing exports file - there gonna be new values.
        myExportsModel = myExportsVault.newExportsModel(myContext.getOriginalInputModel());
      }
      for (SNode v : values) {
        SNode keeper = SModelUtil_new.instantiateConceptDeclaration(keeperConcept, null, true);
        ExportLabelContext ctx = new ExportLabelContextImpl(templateContext.getInput(), v, keeper);
        invokeExportFunction(exportLabel.getModel(), functionName, ctx);
        SModel outputModel = v.getModel() != null ? v.getModel() : templateContext.getEnvironment().getOutputModel();
        final SNode/*node<ExportEntry>*/ exportEntry =
            new CrossModelUtil().newEntry(ctx, exportLabel, myExportsModel, outputModel);
        // FIXME likely CrossModelUtil would get some of these arguments right into constructor.
        //       Just unsure at the moment which one, gonna decide once there are more uses.
        myExportsModel.addRootNode(exportEntry);
      }
    }
  }

  // FIXME copied from j.m.u.QueryMethodGenerated
  private static void invokeExportFunction(SModel m, String funcName, ExportLabelContext ctx) {
    String packageName = m.getName().getLongName();
    String className = packageName + ".ExportLabelFunctions";
    try {
      Class elfClass = ClassLoaderManager.getInstance().getClass(m.getModule(), className);
      if (elfClass == null) {
        throw new IllegalStateException(String.format("Can't find %s class for %s model", className, m.getModelName()));
      }
      final Method method = elfClass.getMethod(funcName, ExportLabelContext.class);
      method.invoke(null, ctx);
    } catch (NoSuchMethodException ex) {
      throw new IllegalStateException(String.format("Can't find method %s in class %s", funcName, className), ex);
    } catch (InvocationTargetException ex) {
      throw new IllegalStateException(String.format("Failed to marshal/unmarshal export label. Method %s in class %s", funcName, className), ex);
    } catch (IllegalAccessException ex) {
      throw new IllegalStateException(String.format("Failed to marshal/unmarshal export label. Method %s in class %s", funcName, className), ex);
    }
  }

  // FIXME input at EXPOSE is from transient model K, while input at query time is from (transient) model L. How do we match them?
  // Perhaps, there should be match function in addition to marshal/unmarshal
  public Collection<SNode> find(@NotNull String exportLabelName, @NotNull SModel processedModel, @NotNull SNode inputNode) {
    // inputNode either comes from the model we generate from, and is transient, or some external model (either regular or proxy, for multi-step exports).
    // Use original input model in former case, and expect outer mode to be regular model (so that stream manager knows where to find exports model)
    // For hanging nodes, we could either say 'no', or resort to myContext.getOriginalInputModel, but for the time being, I don't see value in the latter.
    if (inputNode.getModel() == null) {
      return Collections.emptyList();
    }
    final SModel originModel = originModel(inputNode, processedModel);
    SModel exportsModel = myExportsVault.getExportsModel(originModel);
    if (exportsModel == null) {
      return Collections.emptyList();
    }
    final CrossModelUtil cmu = new CrossModelUtil();
    final List<SNode>/*nlist<ExportEntry>*/ entries = cmu.find(exportsModel, exportLabelName, inputNode);
    final ArrayList<SNode> rv = new ArrayList<SNode>(entries.size());
    for (SNode exportEntry : entries) {
      final SModel proxyModel = cmu.newProxyModel(myContext.getModule(), exportEntry);
      final SNode proxyNode = cmu.newProxyNode(exportEntry, proxyModel);
      identifyAsProxy(proxyNode, originModel);
      proxyModel.addRootNode(proxyNode);
      populateOutputProxy(exportEntry, inputNode, proxyNode);
      rv.add(proxyNode);
    }
    return rv;
  }

  /**
   * Fill instantiated proxy with values saved
   */
  private void populateOutputProxy(SNode/*node<ExportEntry*/ exportEntry, SNode input, SNode outputProxy) {
    final SNode/*node<ExportLabel>*/ exportLabel = exportEntry.getReferenceTarget("label");
    String functionName = CrossModelUtil.getUnmarshalFunctionNameFromEntry(exportEntry);
    ExportLabelContext ctx = new ExportLabelContextImpl(input, outputProxy, CrossModelUtil.dataKept(exportEntry));
    invokeExportFunction(exportLabel.getModel(), functionName, ctx);
  }

  private static final String PROXY_KEY = "#proxy";
  private static final String PROXY_ORIGIN_MODEL_KEY = "#model";

  /**
   * Fill proxy with implementation-specific data that helps to identify and use it.
   */
  private void identifyAsProxy(@NotNull SNode outputProxy, @NotNull SModel originModel) {
    outputProxy.putUserObject(PROXY_KEY, Boolean.TRUE);
    outputProxy.putUserObject(PROXY_ORIGIN_MODEL_KEY, originModel);
  }

  /**
   * Discover model origin to find exports for.
   */
  private SModel originModel(@NotNull SNode inputNode, @NotNull SModel processedModel) {
    if (inputNode.getModel() == processedModel) {
      return myContext.getOriginalInputModel();
    }
    if (isProxyNode(inputNode)) {
      return (SModel) inputNode.getUserObject(PROXY_ORIGIN_MODEL_KEY);
    }
    return inputNode.getModel();
  }

  private static boolean isProxyNode(@NotNull SNode inputNode) {
    return Boolean.TRUE.equals(inputNode.getUserObject(PROXY_KEY));
  }

  private static class ExportLabelContextImpl implements ExportLabelContext {
    private final SNode myInput;
    private final SNode myOutput;
    private final SNode myKeeper;

    public ExportLabelContextImpl(SNode input, SNode output, SNode keeper) {
      myInput = input;
      myOutput = output;
      myKeeper = keeper;
    }
    @Override
    public SNode getInput() {
      return myInput;
    }

    @Override
    public SNode getOutput() {
      return myOutput;
    }

    @Override
    public SNode getKeeper() {
      return myKeeper;
    }
  }
}
