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

import jetbrains.mps.extapi.model.GeneratableSModel;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SNodeReference;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Evgeny Gryaznov, May 14, 2010
 */
public class GenerationDependencies {

  private static final int DEPENDENCIES_VERSION = 2;

  private static final String ROOT_ELEMENT = "dependencies";
  private static final String NODE_ROOT = "source";
  private static final String NODE_COMMON = "common";
  private static final String ATTR_MODEL_HASH = "modelHash";
  private static final String ATTR_PARAMS_HASH = "parametersHash";
  private static final String ATTR_VERSION = "version";

  private static final String NODE_MODEL = "dep";
  private static final String ATTR_MODEL_ID = "model";
  private static final String ATTR_HASH = "hash";

  private final boolean containsIncrementalInfo;

  private final List<GenerationRootDependencies> myRootDependencies;
  private final Map<String, GenerationRootDependencies> myRootDependenciesMap;
  private final String myModelHash;
  private final String myParametersHash;

  private final Map<String, String> myUsedModelsHashes;

  private /* transient */ Map<String, String> myDependenciesTraces;
  private /* transient */ final List<GenerationRootDependencies> myUnchanged;
  private /* transient */ final int mySkippedCount;
  private /* transient */ final int myFromCacheCount;

  /*package*/ GenerationDependencies(String modelHash, String parametersHash) {
    this.containsIncrementalInfo = false;
    this.myModelHash = modelHash;
    this.myParametersHash = parametersHash;

    this.myRootDependencies = Collections.emptyList();
    this.myRootDependenciesMap = Collections.emptyMap();
    this.myUsedModelsHashes = Collections.emptyMap();
    this.myUnchanged = Collections.emptyList();
    this.mySkippedCount = 0;
    this.myFromCacheCount = 0;
  }

  /*package*/ GenerationDependencies(List<GenerationRootDependencies> data, String modelHash, String parametersHash, Map<String, String> externalHashes,
      List<GenerationRootDependencies> unchanged, int skippedCount, int fromCacheCount, Map<String, String> dependenciesTraces) {
    this.containsIncrementalInfo = true;
    this.myRootDependencies = data;
    this.mySkippedCount = skippedCount;
    this.myFromCacheCount = fromCacheCount;
    this.myRootDependenciesMap = new HashMap<String, GenerationRootDependencies>(data.size());
    this.myModelHash = modelHash;
    this.myParametersHash = parametersHash;
    this.myUnchanged = unchanged;
    this.myUsedModelsHashes = externalHashes.isEmpty() ? Collections.<String, String>emptyMap() : externalHashes;
    for (GenerationRootDependencies rd : data) {
      String id = rd.getRootId();
      myRootDependenciesMap.put(id == null ? GeneratableSModel.HEADER : id, rd);
    }
    this.myDependenciesTraces = dependenciesTraces;
  }

  public String getModelHash() {
    return myModelHash;
  }

  public String getParametersHash() {
    return myParametersHash;
  }

  public int getSkippedCount() {
    return mySkippedCount;
  }

  public int getFromCacheCount() {
    return myFromCacheCount;
  }

  public boolean isContainsIncrementalInfo() {
    return containsIncrementalInfo;
  }

  public GenerationRootDependencies getDependenciesFor(String rootId) {
    return myRootDependenciesMap.get(rootId);
  }

  public Map<String, String> getExternalHashes() {
    return Collections.unmodifiableMap(myUsedModelsHashes);
  }

  public List<GenerationRootDependencies> getRootDependencies() {
    return Collections.unmodifiableList(myRootDependencies);
  }

  public List<GenerationRootDependencies> getUnchangedDependencies() {
    return Collections.unmodifiableList(myUnchanged);
  }

  public String extractDependenciesTraces() {
    if (myDependenciesTraces == null) return null;
    StringBuilder sb = new StringBuilder();
    String[] keys = myDependenciesTraces.keySet().toArray(new String[myDependenciesTraces.keySet().size()]);
    Arrays.sort(keys);
    for (String key : keys) {
      sb.append("-------------------------\n");
      sb.append(key).append("\n");
      String val = myDependenciesTraces.get(key);
      for (String s : val.split("\n")) {
        sb.append("\t\t").append(s).append("\n");
      }
      sb.append("\n");
    }
    myDependenciesTraces = null;
    return sb.toString();
  }

  public void update(@Nullable SNodeReference root, @NotNull String fileName) {
    final GenerationRootDependencies rd;
    if (root == null) {
      rd = getDependenciesFor(GeneratableSModel.HEADER);
    } else {
      rd = getDependenciesFor(root.getNodeId().toString());
    }
    if (rd != null) {
      rd.addGeneratedFile(fileName);
    }
  }

  public Element toXml() {
    Element root = new Element(ROOT_ELEMENT);
    root.setAttribute(ATTR_VERSION, Integer.toString(DEPENDENCIES_VERSION));
    if (myModelHash != null) {
      root.setAttribute(ATTR_MODEL_HASH, myModelHash);
    }
    if (myParametersHash != null) {
      root.setAttribute(ATTR_PARAMS_HASH, myParametersHash);
    }
    String[] models = myUsedModelsHashes.keySet().toArray(new String[myUsedModelsHashes.size()]);
    Arrays.sort(models);
    for (String model : models) {
      Element e = new Element(NODE_MODEL);
      e.setAttribute(ATTR_MODEL_ID, model);
      String hash = myUsedModelsHashes.get(model);
      if (hash != null) {
        e.setAttribute(ATTR_HASH, hash);
      }
      root.addContent(e);
    }
    for (GenerationRootDependencies data : myRootDependencies) {
      Element e = new Element(data.getRootId() != null ? NODE_ROOT : NODE_COMMON);
      data.saveTo(e);
      root.addContent(e);
    }
    return root;
  }

  public static GenerationDependencies fromXml(Element root) {
    String version = GenerationRootDependencies.getValue(root, ATTR_VERSION);
    if (version == null || !version.equals(Integer.toString(DEPENDENCIES_VERSION))) {
      /* regenerate all */
      return null;
    }
    // XXX Might be worth sharing intern with other reads/files, but this would take another refactoring round
    Intern intern = new Intern();
    Map<String, String> externalHashes = new HashMap<String, String>();
    for (Element e : root.getChildren(NODE_MODEL)) {
      String modelReference = GenerationRootDependencies.getValue(e, ATTR_MODEL_ID);
      String rootHash = GenerationRootDependencies.getValue(e, ATTR_HASH);
      if (modelReference != null) {
        externalHashes.put(intern.value(modelReference), rootHash);
      }
    }
    List<GenerationRootDependencies> data = new ArrayList<GenerationRootDependencies>();
    for (Element e : root.getChildren(NODE_COMMON)) {
      data.add(GenerationRootDependencies.fromXml(e, true, intern));
    }
    for (Element e : root.getChildren(NODE_ROOT)) {
      data.add(GenerationRootDependencies.fromXml(e, false, intern));
    }
    String modelHash = GenerationRootDependencies.getValue(root, ATTR_MODEL_HASH);
    String paramsHash = GenerationRootDependencies.getValue(root, ATTR_PARAMS_HASH);
    if (externalHashes.isEmpty() && data.isEmpty()) {
      return new GenerationDependencies(modelHash, paramsHash);
    }
    return new GenerationDependencies(data, modelHash, paramsHash, externalHashes, Collections.<GenerationRootDependencies>emptyList(), 0, 0, null);
  }
}
