package edu.stanford.nlp.sempre;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.collect.Lists;

import fig.basic.*;
import fig.exec.Execution;
import fig.prob.SampleUtils;

import java.io.*;
import java.util.*;

/**
 * A dataset contains a set of examples, which are keyed by group (e.g., train,
 * dev, test).
 *
 * @author Percy Liang
 */
public class Dataset {
  public static class Options {
    @Option(gloss = "Paths to read input files (format: <group>:<file>)")
    public ArrayList<Pair<String, String>> inPaths = new ArrayList<Pair<String, String>>();
    @Option(gloss = "Maximum number of examples to read")
    public ArrayList<Pair<String, Integer>> maxExamples = new ArrayList<Pair<String, Integer>>();

    // Training file gets split into:
    // |  trainFrac  -->  |           | <-- devFrac |
    @Option(gloss = "Fraction of trainExamples (from the beginning) to keep for training")
    public double trainFrac = 1;
    @Option(gloss = "Fraction of trainExamples (from the end) to keep for development")
    public double devFrac = 0;
    @Option(gloss = "Used to randomly divide training examples")
    public Random splitRandom = new Random(1);
    @Option(gloss = "whether to split dev from train")
    public boolean splitDevFromTrain = true;

    @Option(gloss = "Only keep examples which have at most this number of tokens")
    public int maxTokens = Integer.MAX_VALUE;

    @Option(gloss = "Path to a knowledge graph that will be uploaded as global context")
    public String globalGraphPath;
  }

  public static Options opts = new Options();

  // Group id -> examples in that group
  private LinkedHashMap<String, List<Example>> allExamples = new LinkedHashMap<String, List<Example>>();

  // General statistics about the examples.
  private final HashSet<String> tokenTypes = new HashSet<String>();
  private final StatFig numTokensFig = new StatFig();  // For each example, number of tokens

  public Set<String> groups() { return allExamples.keySet(); }
  public List<Example> examples(String group) { return allExamples.get(group); }

  /** For JSON. */
  static class GroupInfo {
    @JsonProperty final String group;
    @JsonProperty final List<Example> examples;
    String path;  // Optional, used if this was read from a path.
    @JsonCreator
    public GroupInfo(@JsonProperty("group") String group,
                     @JsonProperty("examples") List<Example> examples) {
      this.group = group;
      this.examples = examples;
    }
  }

  /** For JSON. */
  @JsonProperty("groups")
  public List<GroupInfo> getAllGroupInfos() {
    List<GroupInfo> all = Lists.newArrayList();
    for (Map.Entry<String, List<Example>> entry : allExamples.entrySet())
      all.add(new GroupInfo(entry.getKey(), entry.getValue()));
    return all;
  }

  /** For JSON. */
  // Allows us to creates dataset from arbitrary JSON, not requiring a
  // path from which to read.
  @JsonCreator
  public static Dataset fromGroupInfos(@JsonProperty("groups") List<GroupInfo> groups) {
    Dataset d = new Dataset();
    d.readFromGroupInfos(groups);
    return d;
  }

  public void read() {
    readFromPathPairs(opts.inPaths);
  }

  public void readFromPathPairs(List<Pair<String, String>> pathPairs) {
    // Try to detect whether we need JSON.
    for (Pair<String, String> pathPair : pathPairs) {
      if (pathPair.getSecond().endsWith(".json")) {
        readJsonFromPathPairs(pathPairs);
        return;
      }
    }
    readLispTreeFromPathPairs(pathPairs);
    updateGlobalContext();
  }

  private void updateGlobalContext() {
    if (opts.globalGraphPath != null) {
      KnowledgeGraph graph = NaiveKnowledgeGraph.fromFile(opts.globalGraphPath);
      for (String group : allExamples.keySet()) {
        for (Example ex : allExamples.get(group)) {
          ex.setContext(new ContextValue(graph));
        }
      }
    }
  }


  private void readJsonFromPathPairs(List<Pair<String, String>> pathPairs) {
    List<GroupInfo> groups = Lists.newArrayListWithCapacity(pathPairs.size());
    for (Pair<String, String> pathPair : pathPairs) {
      String group = pathPair.getFirst();
      String path = pathPair.getSecond();
      List<Example> examples = Json.readValueHard(
          IOUtils.openInHard(path),
          new TypeReference<List<Example>>() { });
      GroupInfo gi = new GroupInfo(group, examples);
      gi.path = path;
      groups.add(gi);
    }
    readFromGroupInfos(groups);
  }

  private void readFromGroupInfos(List<GroupInfo> groupInfos) {
    LogInfo.begin_track_printAll("Dataset.read");

    for (GroupInfo groupInfo : groupInfos) {
      int maxExamples = getMaxExamplesForGroup(groupInfo.group);
      List<Example> examples = allExamples.get(groupInfo.group);
      if (examples == null)
        allExamples.put(groupInfo.group, examples = new ArrayList<Example>());
      readHelper(groupInfo.examples, maxExamples, examples, groupInfo.path);
    }
    if (opts.splitDevFromTrain) splitDevFromTrain();
    collectStats();

    LogInfo.end_track();
  }

  private void splitDevFromTrain() {
    // Split original training examples randomly into train and dev.
    List<Example> origTrainExamples = allExamples.get("train");
    if (origTrainExamples != null) {
      int split1 = (int) (opts.trainFrac * origTrainExamples.size());
      int split2 = (int) ((1 - opts.devFrac) * origTrainExamples.size());
      int[] perm = SampleUtils.samplePermutation(opts.splitRandom, origTrainExamples.size());

      List<Example> trainExamples = new ArrayList<Example>();
      allExamples.put("train", trainExamples);
      List<Example> devExamples = allExamples.get("dev");
      if (devExamples == null) {
        // Preserve order
        LinkedHashMap<String, List<Example>> newAllExamples = new LinkedHashMap<>();
        for (Map.Entry<String, List<Example>> entry : allExamples.entrySet()) {
          newAllExamples.put(entry.getKey(), entry.getValue());
          if (entry.getKey().equals("train"))
            newAllExamples.put("dev", devExamples = new ArrayList<>());
        }
        allExamples = newAllExamples;
      }
      for (int i = 0; i < split1; i++)
        trainExamples.add(origTrainExamples.get(perm[i]));
      for (int i = split2; i < origTrainExamples.size(); i++)
        devExamples.add(origTrainExamples.get(perm[i]));
    }
  }

  private void readHelper(List<Example> incoming,
                          int maxExamples,
                          List<Example> examples,
                          String path) {
    if (examples.size() >= maxExamples)
      return;

    int i = 0;
    for (Example ex : incoming) {
      if (examples.size() >= maxExamples) break;

      if (ex.id == null) {
        String id = (path != null ? path : "<nopath>") + ":" + i;
        ex = new Example.Builder().withExample(ex).setId(id).createExample();
      }
      i++;
      ex.preprocess();

      // Skip example if too long
      if (ex.numTokens() > opts.maxTokens) continue;

      LogInfo.logs("Example %s (%d): %s => %s",
          ex.id, examples.size(), ex.getTokens(), ex.targetValue);

      examples.add(ex);
      numTokensFig.add(ex.numTokens());
      for (String token : ex.getTokens()) tokenTypes.add(token);
    }
  }

  private void readLispTreeFromPathPairs(List<Pair<String, String>> pathPairs) {
    LogInfo.begin_track_printAll("Dataset.read");
    for (Pair<String, String> pathPair : pathPairs) {
      String group = pathPair.getFirst();
      String path = pathPair.getSecond();
      int maxExamples = getMaxExamplesForGroup(group);
      List<Example> examples = allExamples.get(group);
      if (examples == null)
        allExamples.put(group, examples = new ArrayList<Example>());
      readLispTreeHelper(path, maxExamples, examples);
    }
    if (opts.splitDevFromTrain) splitDevFromTrain();
    LogInfo.end_track();
  }

  private void readLispTreeHelper(String path, int maxExamples, List<Example> examples) {
    if (examples.size() >= maxExamples) return;
    LogInfo.begin_track("Reading %s", path);

    Iterator<LispTree> trees = LispTree.proto.parseFromFile(path);
    int n = 0;
    while (examples.size() < maxExamples && trees.hasNext()) {
      // Format: (example (id ...) (utterance ...) (targetFormula ...) (targetValue ...))
      LispTree tree = trees.next();
      if (tree.children.size() < 2 || !"example".equals(tree.child(0).value)) {
        if ("metadata".equals(tree.child(0).value)) continue;
        throw new RuntimeException("Invalid example: " + tree);
      }

      Example ex = Example.fromLispTree(tree, path + ":" + n);  // Specify a default id if it doesn't exist
      n++;
      ex.preprocess();

      // Skip example if too long
      if (ex.numTokens() > opts.maxTokens) continue;

      LogInfo.logs("Example %s (%d): %s => %s => %s", ex.id, examples.size(), ex.getTokens(), ex.targetValue, ex.targetFormula);

      examples.add(ex);
      numTokensFig.add(ex.numTokens());
      for (String token : ex.getTokens()) tokenTypes.add(token);
    }
    LogInfo.end_track();
  }

  private void collectStats() {
    LogInfo.begin_track_printAll("Dataset stats");
    Execution.putLogRec("numTokenTypes", tokenTypes.size());
    Execution.putLogRec("numTokensPerExample", numTokensFig);
    for (Map.Entry<String, List<Example>> e : allExamples.entrySet())
      Execution.putLogRec("numExamples." + e.getKey(), e.getValue().size());
    LogInfo.end_track();
  }

  public static int getMaxExamplesForGroup(String group) {
    int maxExamples = Integer.MAX_VALUE;
    for (Pair<String, Integer> maxPair : opts.maxExamples)
      if (maxPair.getFirst().equals(group))
        maxExamples = maxPair.getSecond();
    return maxExamples;
  }

  public static void appendExampleToFile(String path, Example ex) {
    // JSON is an annoying format because we can't just append.
    // So currently we have to read the entire file in and write it out.
    List<Example> examples;
    if (new File(path).exists()) {
      examples = Json.readValueHard(
          IOUtils.openInHard(path),
          new TypeReference<List<Example>>() { });
    } else {
      examples = new ArrayList<Example>();
    }
    examples.add(ex);
    Json.prettyWriteValueHard(new File(path), examples);
  }
}
