/*
 * Copyright 2003-2014 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.textgen.trace;

import jetbrains.mps.smodel.CopyUtil;
import jetbrains.mps.smodel.SNodePointer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.module.SRepository;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;

/**
 * Mechanism Generator uses to keep trace of origin for transformed nodes. As the package name suggests, it's rather part of [debuginfo-api] module,
 * however, generated code uses this class in runtime, and as long as [debuginfo-api] is auxiliary (not part of the [kernel]/mps-core), it's better to keep this class
 * here. After all, [debuginfo-api] is not the only client of the trace.
 *
 * Perhaps, shall push this class even further, down to [smodel], as it's quite generic. Meanwhile, there's CopyUtil that got quite some dependencies to [kernel].
 * Once either CopyUtil's dependencies are in [smodel], or there's simplified alternative (I don't need too much from CopyUtil here, in fact), consider moving
 * the class further.
 */
public final class TracingUtil {
  // key for 'node user object' used to keep track of 'original input node' for a generated node
  // only used when output node has been created as a 'copy' of 'original input node'
  public static final String ORIGINAL_INPUT_NODE = "originalInputNode";

  public static SNode copyWithTrace(SNode node) {
    if (node == null) {
      return null;
    }
    return copyWithTrace(Collections.singletonList(node)).get(0);
  }

  public static List<SNode> copyWithTrace(List<SNode> nodes) {
    HashMap<SNode, SNode> nodeMap = new HashMap<SNode, SNode>();
    List<SNode> result = CopyUtil.copy(nodes, nodeMap);
    for (Entry<SNode, SNode> entry : nodeMap.entrySet()) {
      SNodeReference input = getInput(entry.getKey());
      if (input != null) {
        putInput(entry.getValue(), input);
      } else {
        putInputNode(entry.getValue(), entry.getKey());
      }
    }
    return result;
  }

  @Nullable
  public static SNodeReference getInput(@NotNull SNode output) {
    return (SNodeReference) output.getUserObject(ORIGINAL_INPUT_NODE);
  }

  public static void putInput(@NotNull SNode output, SNodeReference input) {
    output.putUserObject(ORIGINAL_INPUT_NODE, input);
  }

  @Nullable
  public static SNode getInputNode(@NotNull SNode output, @NotNull SRepository repo) {
    // FIXME there are 3 uses of this method in quotations' QueriesGenerated, where we could use context(TQC).getOriginalCopiedInputNode
    // and two more (TQC and TextPreviewModel_Action) where we have access to input model, so that we could simply do
    // originalInputModel.getNode(ptr.getNodeId()), no need to (a) resolve through repo; (b) keep whole reference (nodeId suffice)
    // However, shall look into cases when original input comes from a model different than the one being generated. Perhaps, shall use
    // different key in that case?
    SNodeReference inputNodePointer = (SNodeReference) output.getUserObject(ORIGINAL_INPUT_NODE);
    if (inputNodePointer == null) {
      return null;
    }
    return inputNodePointer.resolve(repo);
  }

  public static void putInputNode(@NotNull SNode output, @NotNull SNode input) {
    output.putUserObject(ORIGINAL_INPUT_NODE, new SNodePointer(input));
  }

  public static void fillOriginalNode(@NotNull SNode inputNode, @NotNull SNode outputNode, boolean originalInput) {
    if (originalInput) {
      putInputNode(outputNode, inputNode);
    } else {
      SNodeReference originalInputNode = getInput(inputNode);
      if (originalInputNode != null) {
        putInput(outputNode, originalInputNode);
      }
    }
  }
}
