/*
 * 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 jetbrains.mps.smodel.adapter.structure.concept.SAbstractConceptAdapter;
import jetbrains.mps.util.containers.MultiMap;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.language.SAbstractConcept;
import org.jetbrains.mps.openapi.model.SNodeReference;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

/**
 * New serialization format for trace.info files
 *
 * @author Artem Tikhomirov
 */
final class SerializeSupport {
  //Achtung! For now this must be in sync with FileType.TRACE_CACHE's data!!
  private static final String ELEMENT_DEBUG_INFO = "debug-info";

  private static final String ATTR_VER = "version";
  private static final String CURRENT_VERSION = "2";
  private static final String ELEMENT_ROOT = "root";
  private static final String ELEMENT_FILE = "file";
  private static final String ELEMENT_NODE_INFO = "node";
  private static final String ELEMENT_SCOPE_INFO = "scope";
  private static final String ELEMENT_UNIT_INFO = "unit";
  private static final String ELEMENT_VAR_INFO = "var";
  private static final String CONCEPT = "concept";
  private static final String ATTR_NODE_REF = "nodeRef";
  private static final String ATTR_NAME = "name";
  private static final String ATTR_TRACEABLE_PROP = "trace";
  private static final String ATTR_NODE_ID = "id";
  private static final String ATTR_AT = "at";
  private static final String ATTR_FQN = "fqn";

  @NotNull
  public static Element serialize(@NotNull DebugInfo debugInfo) {
    Element top = new Element(ELEMENT_DEBUG_INFO);
    top.setAttribute(ATTR_VER, CURRENT_VERSION);

    DebugInfoRoot[] roots = sortedRoots(debugInfo);
    TreeSet<SAbstractConcept> allConcepts = new TreeSet<SAbstractConcept>(new Comparator<SAbstractConcept>() {
      @Override
      public int compare(SAbstractConcept o1, SAbstractConcept o2) {
        return o1.getQualifiedName().compareTo(o2.getQualifiedName());
      }
    });
    collectAllConcepts(roots, allConcepts);
    int i = 0;
    HashMap<SAbstractConcept, Integer> conceptsOrder = new HashMap<SAbstractConcept, Integer>();
    for (SAbstractConcept concept : allConcepts) {
      conceptsOrder.put(concept, i++);
      top.addContent(new Element(CONCEPT).setAttribute(ATTR_FQN, ((SAbstractConceptAdapter) concept).serialize()));
    }
    for (DebugInfoRoot dr : roots) {
      Element r = new Element(ELEMENT_ROOT);
      top.addContent(r);
      SNodeReference nr = dr.getNodeRef();
      if (nr != null) {
        r.setAttribute(ATTR_NODE_REF, nr.toString());
      }
      r.addContent(serialize(dr, conceptsOrder));
    }
    return top;
  }

  @NotNull
  public static DebugInfo restore(@NotNull Element top) {
    if (!ELEMENT_DEBUG_INFO.equals(top.getName())) {
      return new DebugInfo();
    }
    if (!CURRENT_VERSION.equals(top.getAttributeValue(ATTR_VER))) {
      //drop older versions
      return new DebugInfo();
    }
    DebugInfo rv = new DebugInfo();
    int i = 0;
    HashMap<Integer, SAbstractConcept> conceptsOrder = new HashMap<Integer, SAbstractConcept>();
    for (Element c : top.getChildren(CONCEPT)) {
      conceptsOrder.put(i++, SAbstractConceptAdapter.deserialize(c.getAttributeValue(ATTR_FQN)));
    }
    for (Element r : top.getChildren(ELEMENT_ROOT)) {
      String nr = r.getAttributeValue(ATTR_NODE_REF);
      DebugInfoRoot dr = new DebugInfoRoot(nr == null ? null : SNodePointer.deserialize(nr));
      rv.putRootInfo(dr);
      for (Element f : r.getChildren(ELEMENT_FILE)) {
        String filename = f.getAttributeValue(ATTR_NAME);
        for (Element e : f.getChildren(ELEMENT_NODE_INFO)) {
          TraceablePositionInfo pi = new TraceablePositionInfo();
          pi.setFileName(filename);
          restore(e, pi);
          String conceptAttr = e.getAttributeValue(CONCEPT);
          SAbstractConcept conc = null;
          if (conceptAttr != null) {
            conc = conceptsOrder.get(Integer.parseInt(conceptAttr));
          }
          pi.setConcept(conc);
          pi.setPropertyString(e.getAttributeValue(ATTR_TRACEABLE_PROP));
          dr.addPosition(pi);
        }
        for (Element e : f.getChildren(ELEMENT_SCOPE_INFO)) {
          ScopePositionInfo pi = new ScopePositionInfo();
          pi.setFileName(filename);
          restore(e, pi);
          for (Element varInfo : e.getChildren(ELEMENT_VAR_INFO)) {
            VarInfo vi = new VarInfo();
            vi.setVarName(varInfo.getAttributeValue(ATTR_NAME));
            vi.setNodeId(varInfo.getAttributeValue(ATTR_NODE_ID));
            pi.addVarInfo(vi);
          }
          dr.addScopePosition(pi);
        }
        for (Element e : f.getChildren(ELEMENT_UNIT_INFO)) {
          UnitPositionInfo pi = new UnitPositionInfo();
          pi.setFileName(filename);
          restore(e, pi);
          pi.setUnitName(e.getAttributeValue(ATTR_NAME));
          dr.addUnitPosition(pi);
        }
      }
    }
    return rv;
  }

  private static List<Element> serialize(DebugInfoRoot debugRoot, HashMap<SAbstractConcept, Integer> conceptsOrder) {
    MultiMap<String, TraceablePositionInfo> p1 = new MultiMap<String, TraceablePositionInfo>();
    MultiMap<String, ScopePositionInfo> p2 = new MultiMap<String, ScopePositionInfo>();
    MultiMap<String, UnitPositionInfo> p3 = new MultiMap<String, UnitPositionInfo>();
    for (TraceablePositionInfo pi : debugRoot.getPositions()) {
      p1.putValue(pi.getFileName(), pi);
    }
    for (ScopePositionInfo pi : debugRoot.getScopePositions()) {
      p2.putValue(pi.getFileName(), pi);
    }
    for (UnitPositionInfo pi : debugRoot.getUnitPositions()) {
      p3.putValue(pi.getFileName(), pi);
    }
    HashSet<String> allFiles = new HashSet<String>();
    allFiles.addAll(p1.keySet());
    allFiles.addAll(p2.keySet());
    allFiles.addAll(p3.keySet());
    final String[] allFilesSorted = allFiles.toArray(new String[allFiles.size()]);
    Arrays.sort(allFilesSorted);
    ArrayList<Element> rv = new ArrayList<Element>(allFilesSorted.length);
    for (String filename : allFilesSorted) {
      Element fileElement = new Element(ELEMENT_FILE);
      fileElement.setAttribute(ATTR_NAME, filename);
      rv.add(fileElement);
      for (TraceablePositionInfo pi : p1.get(filename)) {
        Element e = new Element(ELEMENT_NODE_INFO);
        fileElement.addContent(e);
        serialize(pi, e);
        if (pi.getConcept() != null) {
          assert conceptsOrder.containsKey(pi.getConcept());
          e.setAttribute(CONCEPT, conceptsOrder.get(pi.getConcept()).toString());
        }
        if (pi.getPropertyString() != null) {
          e.setAttribute(ATTR_TRACEABLE_PROP, pi.getPropertyString());
        }
      }
      for (ScopePositionInfo pi : p2.get(filename)) {
        Element e = new Element(ELEMENT_SCOPE_INFO);
        fileElement.addContent(e);
        serialize(pi, e);
        for (String varName : pi.getVarNames()) {
          Element var = new Element(ELEMENT_VAR_INFO);
          var.setAttribute(ATTR_NAME, varName);
          var.setAttribute(ATTR_NODE_ID, pi.getVarId(varName));
          e.addContent(var);
        }
      }
      for (UnitPositionInfo pi : p3.get(filename)) {
        Element e = new Element(ELEMENT_UNIT_INFO);
        fileElement.addContent(e);
        serialize(pi, e);
        if (pi.getUnitName() != null) {
          e.setAttribute(ATTR_NAME, pi.getUnitName());
        }
      }
    }
    return rv;
  }

  private static void serialize(PositionInfo pi, Element e) {
    if (pi.getNodeId() != null) {
      e.setAttribute(ATTR_NODE_ID, pi.getNodeId());
    }
    e.setAttribute(ATTR_AT, String.format("%d,%d,%d,%d", pi.getStartLine(), pi.getStartPosition(), pi.getEndLine(), pi.getEndPosition()));
  }

  private static void restore(Element e, PositionInfo pi) {
    final String id = e.getAttributeValue(ATTR_NODE_ID);
    if (id != null) {
      pi.setNodeId(id);
    }
    final String[] at = e.getAttributeValue(ATTR_AT).split(",");
    pi.setStartLine(Integer.parseInt(at[0]));
    pi.setStartPosition(Integer.parseInt(at[1]));
    pi.setEndLine(Integer.parseInt(at[2]));
    pi.setEndPosition(Integer.parseInt(at[3]));
  }

  // ensure roots get serialized in the same order
  private static DebugInfoRoot[] sortedRoots(DebugInfo debugInfo) {
    DebugInfoRoot[] rv = toArray(debugInfo.getRoots());
    Arrays.sort(rv, new Comparator<DebugInfoRoot>() {
      @Override
      public int compare(DebugInfoRoot o1, DebugInfoRoot o2) {
        if (o1.getNodeRef() == null || o2.getNodeRef() == null) {
          if (o1.getNodeRef() == null) {
            // in fact, both never null, as DebugInfo uses nodeRef as key. Just to keep it complete.
            return o2.getNodeRef() == null ? 0 : -1;
          }
          return 1;
        }
        return o1.getNodeRef().toString().compareTo(o2.getNodeRef().toString());
      }
    });
    return rv;
  }

  private static DebugInfoRoot[] toArray(Iterable<DebugInfoRoot> roots) {
    ArrayList<DebugInfoRoot> rv = new ArrayList<DebugInfoRoot>();
    for (DebugInfoRoot root : roots) {
      rv.add(root);
    }
    return rv.toArray(new DebugInfoRoot[rv.size()]);
  }

  private static void collectAllConcepts(DebugInfoRoot[] roots, Set<SAbstractConcept> concepts) {
    for (DebugInfoRoot r : roots) {
      for (TraceablePositionInfo pi : r.getPositions()) {
        final SAbstractConcept concept = pi.getConcept();
        if (concept != null) {
          concepts.add(concept);
        }
      }
    }
  }
}
