/*
 * 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.smodel.action;

import jetbrains.mps.openapi.actions.descriptor.ActionAspectDescriptor;
import jetbrains.mps.openapi.actions.descriptor.NodeFactory;
import jetbrains.mps.openapi.editor.EditorContext;
import jetbrains.mps.smodel.CopyUtil;
import jetbrains.mps.smodel.SModelUtil_new;
import jetbrains.mps.smodel.adapter.MetaAdapterByDeclaration;
import jetbrains.mps.smodel.behaviour.BHReflection;
import jetbrains.mps.smodel.language.LanguageRegistry;
import jetbrains.mps.smodel.language.LanguageRuntime;
import jetbrains.mps.smodel.legacy.ConceptMetaInfoConverter;
import jetbrains.mps.util.annotation.ToRemove;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SAbstractConcept;
import org.jetbrains.mps.openapi.language.SConcept;
import org.jetbrains.mps.openapi.language.SContainmentLink;
import org.jetbrains.mps.openapi.language.SInterfaceConcept;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.util.DepthFirstConceptIterator;

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

public class NodeFactoryManager {
  private static Logger LOG = LogManager.getLogger(NodeFactoryManager.class);

  public static SNode createNode(SNode enclosingNode, EditorContext editorContext, String linkRole) {
    SAbstractConcept linkTarget = null;
    // this mimics deprecated SAbstractConcept.getLink(String), first aggregation, then association, albeit without NPE for non-existent ConceptDescriptor
    for (SContainmentLink link : enclosingNode.getConcept().getContainmentLinks()) {
      if (link.getName().equals(linkRole)) {
        linkTarget = link.getTargetConcept();
        break;
      }
    }
    if (linkTarget == null) {
      linkTarget = ((ConceptMetaInfoConverter) enclosingNode.getConcept()).convertAssociation(linkRole).getTargetConcept();
    }
    SModel model = enclosingNode.getModel();
    return createNode(linkTarget, null, enclosingNode, model);
  }

  /**
   * @deprecated use {@link #createNode(org.jetbrains.mps.openapi.language.SAbstractConcept, org.jetbrains.mps.openapi.model.SNode, org.jetbrains.mps.openapi.model.SNode, org.jetbrains.mps.openapi.model.SModel)}
   */
  @Deprecated
  @ToRemove(version = 3.2)
  public static SNode createNode(@NotNull SNode nodeConcept, SNode sampleNode, SNode enclosingNode, @Nullable SModel model) {
    return createNode(asSConcept(nodeConcept), sampleNode, enclosingNode, model);
  }

  public static SNode createNode(@NotNull SAbstractConcept nodeConcept, SNode sampleNode, SNode enclosingNode, @Nullable SModel model) {
    return createNode(nodeConcept, sampleNode, enclosingNode, model, new HashSet<SAbstractConcept>());
  }

  private static SNode createNode(@NotNull SAbstractConcept nodeConcept, SNode sampleNode, SNode enclosingNode, @Nullable SModel model,
      Set<SAbstractConcept> visitedNonOptionalChildConcepts) {
    SNode newNode = SModelUtil_new.instantiateConceptDeclaration(nodeConcept, model, null, false);
    if (newNode == null) return null;
    if (nodeConcept instanceof SInterfaceConcept) {
      return newNode;
    }
    BHReflection.initNode(newNode);
    if (sampleNode != null) {
      sampleNode = CopyUtil.copy(sampleNode);
    }
    nodeConcept = newNode.getConcept(); // XXX is it possible to get another concept on creation?
    setupNode(nodeConcept, newNode, sampleNode, enclosingNode, model);
    createNodeStructure(nodeConcept, newNode, sampleNode, enclosingNode, model, visitedNonOptionalChildConcepts);
    return newNode;
  }

  private static void createNodeStructure(SAbstractConcept nodeConcept, SNode newNode, SNode sampleNode, SNode enclosingNode, SModel model,
      Set<SAbstractConcept> visitedNonOptionalChildConcepts) {
    for (SContainmentLink linkDeclaration : nodeConcept.getContainmentLinks()) {
      if (linkDeclaration.isOptional()) {
        continue;
      }
      SAbstractConcept targetConcept = linkDeclaration.getTargetConcept();
      if (!newNode.getChildren(linkDeclaration).iterator().hasNext()) {
        if (visitedNonOptionalChildConcepts.add(targetConcept)) {
          try {
            SNode childNode = createNode(targetConcept, sampleNode, enclosingNode, model, visitedNonOptionalChildConcepts);
            newNode.addChild(linkDeclaration, childNode);
          } finally {
            visitedNonOptionalChildConcepts.remove(targetConcept);
          }
        } else {
          LOG.error("Cyclic containment found while initializing node: " + newNode.getNodeId() + ", containment link: " + linkDeclaration.getName());
        }
      }
    }
  }

  /**
   * @deprecated use {@link #setupNode(org.jetbrains.mps.openapi.language.SAbstractConcept, org.jetbrains.mps.openapi.model.SNode, org.jetbrains.mps.openapi.model.SNode, org.jetbrains.mps.openapi.model.SNode, org.jetbrains.mps.openapi.model.SModel)}
   */
  @Deprecated
  @ToRemove(version = 3.2)
  public static void setupNode(SNode nodeConcept, SNode node, SNode sampleNode, SNode enclosingNode, SModel model) {
    setupNode(asSConcept(nodeConcept), node, sampleNode, enclosingNode, model);
  }

  public static void setupNode(SAbstractConcept nodeConcept, SNode node, SNode sampleNode, SNode enclosingNode, SModel model) {
    for (SAbstractConcept ancestor : new DepthFirstConceptIterator(nodeConcept)) {
      ActionAspectDescriptor actionAspectDescriptor = null;
      LanguageRuntime languageRuntime = LanguageRegistry.getInstance().getLanguage(ancestor.getLanguage());
      if (languageRuntime != null) {
        actionAspectDescriptor = languageRuntime.getAspect(ActionAspectDescriptor.class);
      }
      if (actionAspectDescriptor == null) {
        continue;
      }
      Collection<NodeFactory> factories = actionAspectDescriptor.getFactories(ancestor);
      for (NodeFactory factory : factories) {
        factory.setup(node, sampleNode, enclosingNode, model);
      }
    }
  }

  private static SConcept asSConcept(SNode nodeConcept) {
    return MetaAdapterByDeclaration.getInstanceConcept(nodeConcept);
  }
}
