/*
 * 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.generator.ModelGenerationPlan.Checkpoint;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeId;
import org.jetbrains.mps.openapi.model.SNodeUtil;
import org.jetbrains.mps.openapi.util.TreeIterator;

/**
 * Ensures we trace origin of node in a transient model, whether it's original input model or one of previous checkpoint steps.
 * Keeps traces for each node in the model during transformation phase, and once the phase is over, this trace is used to populate
 * label mappings information that will persist. Once mappings are ready, the trace is of no use and can be disposed (unless we decide
 * to use it as a replacement for {@link jetbrains.mps.textgen.trace.TracingUtil} and navigation from transient models back to original one.
 *
 * ModelTransitions is the source of TransitionTrace, and keeps track of CP-CP transitions (similar to ModelCheckpoints->CheckpointState relation, just for the
 * active transformation).
 * {@code TransitionTrace} knows last checkpoint (if any), and doesn't know next one. Now it's {@link GenPlanActiveStep} that keeps track of the plan and actual
 * position in there. Perhaps, that's not the most elegant approach.
 *
 * FIXME inputNode may not necessarily come from the input model, it might be arbitrary non-transient (or even perhaps checkpoint?!) model,
 *       thus saving nodeId is not sufficient. OTOH, don't want to save SNodeReference as it's superficial in most regular cases
 * @author Artem Tikhomirov
 */
class TransitionTrace {
  /**
   * IMPLEMENTATION NOTE:
   * For prototype purposes, follow approach of TracingUtil, with user object pointer to origin, associated with each node.
   * Although I don't like untyped node.userObject and data tied to a model structure, I'm not yet confident whether per-node approach
   * is inherently bad, huge maps, after all, aren't nice, too.
   */
  private static final String ORIGIN_TRACE = "originTrace";

  private final Checkpoint myCheckpoint;
  private final ModelTransitions myModelTrace;

  TransitionTrace(@NotNull ModelTransitions modelTrace) {
    // original input model
    myCheckpoint = null;
    myModelTrace = modelTrace;
  }

  TransitionTrace(@NotNull Checkpoint checkpoint, @NotNull ModelTransitions modelTrace) {
    myCheckpoint = checkpoint;
    myModelTrace = modelTrace;
  }

  /**
   * Indicate all nodes of the transient model originate from identical nodes of a cloned checkpoint model
   * FIXME could do the same for TracingUtil.ORIGINAL_INPUT_NODE, i.e. TT.reset(transientModel@0) to make it uniform
   * FIXME perhaps, shall take originModel as well, and a function that maps nodes of transientModel to nodes of originalModel
   * (with getNodeId() as default). Otherwise connection to originalModel is not apparent here.
   */
  void reset(@NotNull SModel transientModel) {
    for (SNode n : SNodeUtil.getDescendants(transientModel)) {
      n.putUserObject(ORIGIN_TRACE, n.getNodeId());
    }
  }

  /**
   * @return never {@code null}
   */
  public ModelTransitions getModelTrace() {
    return myModelTrace;
  }

  public boolean hasOrigin(@NotNull SNode node) {
    return doGet(node) instanceof SNodeId;
  }

  /*package*/ SNodeId getOrigin(@NotNull SNode node) {
    Object rv = doGet(node);
    return rv instanceof SNodeId ? (SNodeId) rv : null;
  }

  /**
   * Output node derives its origin from the given input node.
   * If output node already got an origin, it's preserved.
   * If input node has no trace, output node gets none as well.
   * No child of output node is processed.
   */
  public void deriveOrigin(@NotNull SNode inputNode, @NotNull SNode outputNode) {
    if (inputNode == outputNode) {
      // no-op for the present approach (user object at a node),
      // perhaps, would change if we decide to handle in-place copied nodes in a different way (e.g. trace this fact)
      return;
    }
    if (hasOrigin(outputNode)) {
      return;
    }
    Object originNode = doGet(inputNode);
    doSet(outputNode, originNode);
  }

  /**
   * Initialize origin for a tree of nodes. Output nodes get origin unless they already got one, in this case
   * complete subtree is ignored (it's assumed the sub-tree already got origin assigned, no reason to override it)
   */
  public void deriveOrigin(@NotNull SNode inputNode, @NotNull TreeIterator<SNode> outputNodes) {
    Object originNode = doGet(inputNode);
    if (!(originNode instanceof SNodeId)) {
      return;
    }
    while (outputNodes.hasNext()) {
      SNode n = outputNodes.next();
      Object existing = doGet(n);
      if (existing instanceof SNodeId) {
        outputNodes.skipChildren();
        continue;
      }
      doSet(n, originNode);
    }
  }

  private static Object doGet(SNode n) {
    return n.getUserObject(ORIGIN_TRACE);
  }

  private static void doSet(SNode n, Object value) {
    n.putUserObject(ORIGIN_TRACE, value);
  }
}
