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

import jetbrains.mps.smodel.SNodePointer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeId;
import org.jetbrains.mps.openapi.model.SNodeReference;
import org.jetbrains.mps.openapi.persistence.PersistenceFacade;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;

public class DebugInfo {
  private final Map<SNodeReference, DebugInfoRoot> myRoots = new HashMap<SNodeReference, DebugInfoRoot>();

  public DebugInfo() {
  }

  private DebugInfoRoot getOrCreateDebugInfoRoot(SNode rootNode) {
    SNodeReference ref = getRef(rootNode);
    DebugInfoRoot infoRoot = myRoots.get(ref);
    if (infoRoot == null) {
      infoRoot = new DebugInfoRoot(ref);
      myRoots.put(ref, infoRoot);
    }
    return infoRoot;
  }

  private SNodeReference getRef(SNode node) {
    return (node == null ? null : node.getReference());
  }

  public void addPosition(TraceablePositionInfo position, SNode containingRoot) {
    getOrCreateDebugInfoRoot(containingRoot).addPosition(position);
  }

  public void addScopePosition(ScopePositionInfo position, SNode containingRoot) {
    getOrCreateDebugInfoRoot(containingRoot).addScopePosition(position);
  }

  public void addUnitPosition(UnitPositionInfo unitPosition, SNode containingRoot) {
    getOrCreateDebugInfoRoot(containingRoot).addUnitPosition(unitPosition);
  }

  public DebugInfoRoot getRootInfo(@Nullable SNode root) {
    return myRoots.get(getRef(root));
  }

  public DebugInfoRoot getRootInfo(@Nullable SNodeReference root) {
    return myRoots.get(root);
  }

  public void putRootInfo(DebugInfoRoot root) {
    myRoots.put(root.getNodeRef(), root);
  }

  @Nullable
  public TraceablePositionInfo getPositionForNode(final SNode node) {
    // used in mbeddr 
    DebugInfoRoot root = myRoots.get(getRef(node.getContainingRoot()));
    if (root == null) {
      return null;
    }
    for (TraceablePositionInfo pi : root.getPositions()) {
      if (pi.matches(node.getNodeId())) {
        return pi;
      }
    }
    return null;
  }

  public Set<TraceablePositionInfo> getPositions(SNode rootNode) {
    // for mbeddr 
    assert (rootNode.getParent() == null);
    DebugInfoRoot root = myRoots.get(getRef(rootNode));
    if (root == null) {
      return Collections.emptySet();
    }
    return root.getPositions();
  }

  @NotNull
  public List<UnitPositionInfo> getUnitsForNode(SNode node) {
    final SNodeId id = node.getNodeId();
    DebugInfoRoot debugInfoRoot = myRoots.get(getRef(node.getContainingRoot()));
    if (debugInfoRoot != null) {
      ArrayList<UnitPositionInfo> unitPositions = new ArrayList<>(debugInfoRoot.getUnitPositions());
      // FIXME StartLineComparator is always wrapped with reverseOrder, no need to have it that way
      Collections.sort(unitPositions, Collections.reverseOrder(new PositionInfo.StartLineComparator()));
      unitPositions.removeIf(pi -> !pi.matches(id));
      return unitPositions;
    }
    return Collections.emptyList();
  }

  /**
   * @see #getTracedNodesForPosition(String, int, Consumer)
   */
  @NotNull
  public List<SNodeReference> getTracedNodesForPosition(@NotNull String fileName, int line) {
    return getTracedNodesForPosition(fileName, line, null);
  }

    /**
     * Look up all {@link TraceablePositionInfo} that cover specified location, sort them by distance of starting line from the
     * line specified (closes coming first), prepare {@code SNodeReference} and yield the list.
     * Takes an optional code that may further filter out {@link TraceablePositionInfo} according to specific needs. Consumer can respect certain
     * approaches to code generation, language differences and relation of line and position.
     *
     * @param fileName name of generated file with text
     * @param line index of line in question
     * @param extraProcessing optional code that takes sorted list of {@linkplain TraceablePositionInfo positions} that span specified line, the one with
     *                        closest start coming first. The list could be modified/augmented or even emptied, positions left in the list are translated
     *                        to respective node references and constitute the outcome of the method. Consumer list argument is never {@code null}.
     */
  @NotNull
  public List<SNodeReference> getTracedNodesForPosition(@NotNull String fileName, int line, @Nullable Consumer<List<TraceablePositionInfo>> extraProcessing) {
    // XXX implementation note: this method, much like the next one, getVariableNodesForPosition, is identical to getUnitNodesForPosition
    //     and all are worth refactoring (though not the way it used to be in TraceInfoUtil)
    //     Perhaps, can throw in some Java 8?
    PersistenceFacade persFacade = PersistenceFacade.getInstance();
    ArrayList<SNodeReference> traceNode = new ArrayList<SNodeReference>();
    for (DebugInfoRoot dr : getRootsForFile(fileName)) {
      ArrayList<TraceablePositionInfo> positionInfos = new ArrayList<TraceablePositionInfo>(dr.getPositions());
      Collections.sort(positionInfos, Collections.reverseOrder(new PositionInfo.StartLineComparator()));
      ArrayList<TraceablePositionInfo> infosForLine = new ArrayList<TraceablePositionInfo>(positionInfos.size());
      for (TraceablePositionInfo tpi : positionInfos) {
        if (tpi.contains(fileName, line)) {
          infosForLine.add(tpi);
        }
      }
      if (extraProcessing != null) {
        extraProcessing.accept(infosForLine);
      }
      for (TraceablePositionInfo tpi : infosForLine) {
        // FIXME new SNodePointer here seems to be the only reason we depend from [smodel]
        traceNode.add(new SNodePointer(dr.getNodeRef().getModelReference(), persFacade.createNodeId(tpi.getNodeId())));
      }
    }
    return traceNode;
  }


  /**
   * Look up for variable nodes within scope that covers specified line of the file.
   * List is sorted in reverse order, with variables from the bottom (most nested) coming first.
   * <p/>
   * The reason I don't want to expose ScopePositionInfo and let clients filter positions themselves is
   * that they'd need access to DebugInfoRoot as well (to find out container node/model), and the API would get too complicated.
   */
  @NotNull
  public List<SNodeReference> getVariableNodesForPosition(@NotNull String fileName, int line, @NotNull String varName) {
    PersistenceFacade persFacade = PersistenceFacade.getInstance();
    ArrayList<SNodeReference> rv = new ArrayList<SNodeReference>();
    for (DebugInfoRoot dr : getRootsForFile(fileName)) {
      ArrayList<ScopePositionInfo> positionInfos = new ArrayList<>(dr.getScopePositions());
      Collections.sort(positionInfos, Collections.reverseOrder(new PositionInfo.StartLineComparator()));
      for (ScopePositionInfo spi : positionInfos) {
        if (!spi.contains(fileName, line)) {
          continue;
        }
        if (spi.getVarNames().contains(varName)) {
          rv.add(new SNodePointer(dr.getNodeRef().getModelReference(), persFacade.createNodeId(spi.getVarId(varName))));
        }
      }
    }
    return rv;
  }

  /**
   * @return list of nodes known at the specified line, sorted in reversed order (with the node most close to the line coming first)
   */
  @NotNull
  public List<SNodeReference> getUnitNodesForPosition(String fileName, int line) {
    PersistenceFacade persFacade = PersistenceFacade.getInstance();
    ArrayList<SNodeReference> unitNodes = new ArrayList<SNodeReference>();
    for (DebugInfoRoot dr : getRootsForFile(fileName)) {
      ArrayList<UnitPositionInfo> positionInfos = new ArrayList<UnitPositionInfo>(dr.getUnitPositions());
      Collections.sort(positionInfos, Collections.reverseOrder(new PositionInfo.StartLineComparator()));
      for (UnitPositionInfo upi : positionInfos) {
        if (upi.contains(fileName, line)) {
          unitNodes.add(new SNodePointer(dr.getNodeRef().getModelReference(), persFacade.createNodeId(upi.getNodeId())));
        }
      }
    }
    return unitNodes;
  }

  public Iterable<DebugInfoRoot> getRoots() {
    return myRoots.values();
  }

  private List<DebugInfoRoot> getRootsForFile(String filename) {
    ArrayList<DebugInfoRoot> rv = new ArrayList<DebugInfoRoot>(myRoots.size());
    for (DebugInfoRoot dr : myRoots.values()) {
      if (dr.getFileNames().contains(filename)) {
        rv.add(dr);
      }
    }
    return rv;
  }
}
