/*
 * 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.logging.Logger;
import jetbrains.mps.project.AbstractModule;
import jetbrains.mps.project.structure.modules.Dependency;
import jetbrains.mps.project.structure.modules.ModuleDescriptor;
import jetbrains.mps.smodel.CopyUtil;
import jetbrains.mps.smodel.DynamicReference;
import jetbrains.mps.smodel.Language;
import jetbrains.mps.smodel.ModelImports;
import jetbrains.mps.smodel.StaticReference;
import jetbrains.mps.textgen.trace.TracingUtil;
import jetbrains.mps.util.SNodeOperations;
import org.apache.log4j.LogManager;
import org.jetbrains.mps.openapi.language.SContainmentLink;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SReference;

public class CloneUtil {
  private static final Logger LOG = Logger.wrap(LogManager.getLogger(CloneUtil.class));

  private final SModel myInputModel;
  private final SModel myOutputModel;
  private final SModelReference myOutputModelRef;
  private boolean myTraceOriginalInput = false;
  private final Factory myFactory;

  public CloneUtil(SModel inputModel, SModel outputModel) {
    this(inputModel, outputModel, new RegularSModelFactory());
  }
  public CloneUtil(SModel inputModel, SModel outputModel, Factory factory) {
    myInputModel = inputModel;
    myOutputModel = outputModel;
    myOutputModelRef = outputModel.getReference();
    myFactory = factory;
  }

  /**
   * Record origin of copied node with TracingUtil
   * @return <code>this</code> for convenience
   */
  public CloneUtil traceOriginalInput() {
    myTraceOriginalInput = true;
    return this;
  }

  /**
   * Creates cloned model, each node in target model has the same nodeId that corresponding node in source model
   * it allows to resolve internal references much faster
   */
  public void cloneModelWithImports() {
    //copy model with imports, used languages and devkits
    cloneModel();
    final ModelImports modelImports = new ModelImports(myOutputModel);
    modelImports.copyImportedModelsFrom(myInputModel);
    modelImports.copyEmployedDevKitsFrom(myInputModel);
    modelImports.copyUsedLanguagesFrom(myInputModel);
  }

  /**
   * The same as above, but also clones all module imports
   */
  public void cloneModelWithAllImports() {
    cloneModelWithImports();
    AbstractModule inputModule = (AbstractModule) myInputModel.getModule();
    assert !(inputModule instanceof Language);
    ModuleDescriptor moduleDescriptor = inputModule.getModuleDescriptor();
    AbstractModule outputModule = (AbstractModule) myOutputModel.getModule();
    for (Dependency dependency : moduleDescriptor.getDependencies()) {
      outputModule.addDependency(dependency.getModuleRef(), dependency.isReexport());
    }
  }

  public void cloneModel() {
    for (SNode node : myInputModel.getRootNodes()) {
      SNode outputNode = clone(node);
      myOutputModel.addRootNode(outputNode);
    }
  }

  // FIXME CopyUtil.copy() respects references within cloned sub-tree, but doesn't work with node factory. Shall combine both into single utility
  public SNode clone(SNode inputNode) {
    SNode outputNode = myFactory.create(inputNode);

    CopyUtil.copyProperties(inputNode, outputNode);
    CopyUtil.copyUserObjects(inputNode, outputNode);
    // keep track of 'original input node'
    if (myTraceOriginalInput) {
      TracingUtil.putInputNode(outputNode, inputNode);
    }
    for (SReference reference : inputNode.getReferences()) {
      boolean ext = inputNode.getModel() == null || !inputNode.getModel().getReference().equals(reference.getTargetSModelReference());
      SModelReference targetModelReference = ext ? reference.getTargetSModelReference() : myOutputModelRef;
      SReference outRef = myFactory.create(reference, outputNode, targetModelReference);
      if (outRef != null) {
        outputNode.setReference(outRef.getLink(), outRef);
      }
    }

    for (SNode child : inputNode.getChildren()) {
      SContainmentLink role = child.getContainmentLink();
      assert role != null;
      outputNode.addChild(role, clone(child));
    }
    return outputNode;
  }

  public static DynamicReference create(SNode outputNode, SModelReference targetModelRef, DynamicReference prototype) {
    DynamicReference outputReference = new DynamicReference(
        prototype.getLink(),
        outputNode,
        targetModelRef,
        prototype.getResolveInfo());
    outputReference.setOrigin(prototype.getOrigin());
    return outputReference;
  }

  public interface Factory {
    SNode create(SNode prototype);
    SReference create(SReference prototype, SNode outputNode, SModelReference targetModelRef);
  }

  public static class RegularSModelFactory implements Factory {

    @Override
    public SNode create(SNode prototype) {
      return new jetbrains.mps.smodel.SNode(prototype.getConcept(), prototype.getNodeId());
    }

    @Override
    public SReference create(SReference prototype, SNode outputNode, SModelReference targetModelRef) {
      // [model] clone mechanism in smodel.SReference or elsewhere not to perform instanceof
      // Besides, what if there's custom openapi.SReference impl (GenSReference) I'm not aware of? How am I supposed to clone it here?
      if (prototype instanceof StaticReference) {
        if (targetModelRef == null) {
          LOG.warning("broken reference '" + prototype.getLink().getName() + "' in " + SNodeOperations.getDebugText(prototype.getSourceNode()), prototype.getSourceNode());
        } else {
          StaticReference outputReference = new StaticReference(
              prototype.getLink(),
              outputNode,
              targetModelRef,
              prototype.getTargetNodeId(),
              ((StaticReference) prototype).getResolveInfo());
          return outputReference;
        }
      } else if (prototype instanceof DynamicReference) {
        return CloneUtil.create(outputNode, targetModelRef, (DynamicReference) prototype);
      } else {
        LOG.error("internal error: can't clone reference '" + prototype.getLink().getName() + "' in " + SNodeOperations.getDebugText(prototype.getSourceNode()), prototype.getSourceNode());
        LOG.error(" -- was reference class : " + prototype.getClass().getName());
      }
      return null;
    }
  }
}
