package edu.stanford.nlp.sempre.roboy.lexicons;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

import edu.stanford.nlp.sempre.Formula;
import edu.stanford.nlp.sempre.SemType;
import edu.stanford.nlp.sempre.SemTypeHierarchy;
import edu.stanford.nlp.stats.ClassicCounter;
import edu.stanford.nlp.stats.Counter;
import edu.stanford.nlp.util.StringUtils;
import fig.basic.LispTree;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

public class LexicalEntry {

  public final String textDescription; // the query as submitted to the lexicon
  public final String normalizedTextDesc; // the query after normalization
  public final Set<String> dbDescriptions; // descriptions matching the formula
  public final Formula formula;
  public EntrySource source;
  public final double popularity;
  public final double distance;


  public LexicalEntry(String textDescription, String normalizedTextDesc, Set<String> dbDescriptions, Formula formula, EntrySource source, double popularity, double distance) {
    this.textDescription = textDescription;
    this.normalizedTextDesc = normalizedTextDesc;
    this.dbDescriptions = dbDescriptions;
    this.formula = formula;
    this.source = source;
    this.popularity = popularity;
    this.distance = distance;
  }

  public Formula getFormula() {
    return formula;
  }

  public double getPopularity() {
    return popularity;
  }

  public double getDistance() {
    return distance;
  }

  private String stringRepn;
  public String toString() {
    if (stringRepn == null) {
      stringRepn = textDescription + " (" + normalizedTextDesc + ")" +
          ", FB: " + dbDescriptions +
          ", formula: " + formula +
          ", source: " + source +
          ", popularity: " + popularity +
          ", distance: " + distance;
    }
    return stringRepn;
  }

  public static int computeEditDistance(String query, Set<String> descriptions) {

    int distance = Integer.MAX_VALUE;
    for (String description : descriptions) {
      int currDistance = StringUtils.editDistance(query, description.toLowerCase());
      if (currDistance < distance) {
        distance = currDistance;
      }
    }
    return Math.min(15, distance);
  }

  // Input: set of types coming from the lexicon {fb:common.topic, fb:people.person, ...}
  // Output: remove any element which is in the transitive closure. {fb:people.person, ...}
  // TODO(pliang): replace HashSet with something lighter weight.
  public static SemType setToType(Set<String> types) {
    Set<String> resultTypes = new HashSet<>(types);
    for (String entityType : types) {
      for (String supertype : SemTypeHierarchy.singleton.getSupertypes(entityType)) {
        if (!supertype.equals(entityType))
          resultTypes.remove(supertype);
      }
    }
    return SemType.newUnionSemType(resultTypes);
  }

  public static class BinaryLexicalEntry extends LexicalEntry {

    public String expectedType1;
    public String expectedType2;
    public String unitId = "";
    public String unitDescription = "";
    public Map<String, Double> alignmentScores;
    public String fullLexeme; // the lexeme as it is in the alignment without some normalization applied before uploading the lexicon

    public BinaryLexicalEntry(String textDescription, String normalizedTextDesc, Set<String> dbDescriptions, Formula formula, EntrySource source, double popularity,
        String expectedType1, String expectedType2, String unitId, String unitDesc, Map<String, Double> alignmentScores, String fullLexeme) {
      super(textDescription, normalizedTextDesc, dbDescriptions, formula, source, popularity, computeEditDistance(textDescription, dbDescriptions));
      this.expectedType1 = expectedType1;
      this.expectedType2 = expectedType2;
      this.unitId = unitId;
      this.unitDescription = unitDesc;
      this.alignmentScores = alignmentScores;
      this.fullLexeme = fullLexeme;
      assert (fullLexeme.contains(normalizedTextDesc));
    }

    public boolean identicalFormulaInfo(Object other) {
      if (!(other instanceof BinaryLexicalEntry))
        return false;
      BinaryLexicalEntry otherBinary = (BinaryLexicalEntry) other;

      if (!formula.equals(otherBinary.formula))
        return false;
      if (Math.abs(popularity - otherBinary.popularity) > 0.000001)
        return false;
      if (!expectedType1.equals(otherBinary.expectedType1))
        return false;
      if (!expectedType2.equals(otherBinary.expectedType2))
        return false;
      if (!unitId.equals(otherBinary.unitId))
        return false;
      if (!unitDescription.equals(otherBinary.unitDescription))
        return false;
      return true;
    }

    public String getExpectedType1() {
      return expectedType1;
    }

    public String getExpectedType2() {
      return expectedType2;
    }

    public String getUnitId() {
      return unitId;
    }

    public String getUnitDescription() {
      return unitDescription;
    }

    private String stringRepn;
    public String toString() {
      if (stringRepn == null) {
        StringBuilder sb = new StringBuilder();
        sb.append(super.toString());
        sb.append(", " + expectedType1 + " x " + expectedType2);
        if (unitId != null) {
          sb.append(", " + unitId + ":" + unitDescription);
        }
        if (alignmentScores.size() > 0)
          sb.append(", " + alignmentScores);
        stringRepn = sb.toString();
      }
      return stringRepn;
    }

    public boolean isFullLexemeEqualToNormalizedText() {
      return fullLexeme.equals(normalizedTextDesc);
    }

    public String[] getLeftContext() {
      if (fullLexeme.startsWith(normalizedTextDesc))
        return new String[]{};
      String leftContext = fullLexeme.substring(0, fullLexeme.indexOf(normalizedTextDesc)).trim();
      return leftContext.split("\\s+");
    }

    public String[] getRightContext() {
      if (fullLexeme.endsWith(normalizedTextDesc))
        return new String[]{};
      String rightContext = fullLexeme.substring(fullLexeme.indexOf(normalizedTextDesc) + normalizedTextDesc.length()).trim();
      return rightContext.split("\\s+");
    }
  }

  public static class EntityLexicalEntry extends LexicalEntry {

    public Set<String> types = new HashSet<>();
    public SemType type;
    public Counter<String> entityFeatures;

    public EntityLexicalEntry(String textDescription, String normalizedTextDesc, Set<String> dbDescriptions,
                              Formula formula, EntrySource source, double popularity, double distance, Set<String> types,
                              Counter<String> entityFeatures) {
      super(textDescription, normalizedTextDesc, dbDescriptions, formula, source, popularity, distance);
      this.types = types;
      this.type = setToType(types);
      this.entityFeatures = entityFeatures;
    }

    public String toString() {
      return super.toString() + ", " + types;
    }
  }

  public static class UnaryLexicalEntry extends LexicalEntry {

    public Set<String> types = new HashSet<String>();
    public SemType type;
    public Map<String, Double> alignmentScores;

    public UnaryLexicalEntry(String textDescription, String normalizedTextDesc, Set<String> dbDescriptions, Formula formula, EntrySource source, double popularity,
        Map<String, Double> alignmentScores, Set<String> types) {
      super(textDescription, normalizedTextDesc, dbDescriptions, formula, source, popularity, computeEditDistance(textDescription, dbDescriptions));
      this.types = types;
      this.type = setToType(types);
      this.alignmentScores = alignmentScores;
    }

    String stringRepn;
    public String toString() {
      if (stringRepn == null)
        stringRepn = super.toString() + ", " + types;
      return stringRepn;
    }
  }

  /**
   * Holds the essential parts of a value in a lexicon
   * @author jonathanberant
   *
   */
  public static class LexiconValue {

    @JsonProperty public String lexeme;
    @JsonProperty public Formula formula;
    @JsonProperty public String source;
    @JsonProperty public Map<String, Double> features;

    @JsonCreator
    public LexiconValue(@JsonProperty("normLexeme") String lexeme,
        @JsonProperty("formula") Formula formula,
        @JsonProperty("source") String source,
        @JsonProperty("features") Map<String, Double> features) {
      this.lexeme = lexeme;
      this.formula = formula;
      this.source = source;
      this.features = features;
    }
  }

  public static class LexicalEntrySerializer {
    // Utilities that should move into fig later.
    static Counter<String> counterFromLispTree(LispTree tree) {
      Counter<String> counter = new ClassicCounter<String>();
      for (int i = 0; i < tree.children.size(); i++)
        counter.incrementCount(tree.child(i).child(0).value, Double.parseDouble(tree.child(i).child(1).value));
      return counter;
    }
    static LispTree counterToLispTree(Counter<String> counter) {
      LispTree tree = LispTree.proto.newList();
      for (String feature : counter.keySet())
        tree.addChild(LispTree.proto.newList(feature, "" + counter.getCount(feature)));
      return tree;
    }

    static Map<String, Double> featureMapFromLispTree(LispTree tree) {
      Map<String, Double> featureMap = new TreeMap<String, Double>();
      for (int i = 0; i < tree.children.size(); i++)
        featureMap.put(tree.child(i).child(0).value, Double.parseDouble(tree.child(i).child(1).value));
      return featureMap;
    }

    static LispTree featureMapToLispTree(Map<String, Double> featureMap) {
      LispTree tree = LispTree.proto.newList();
      for (String feature : featureMap.keySet())
        tree.addChild(LispTree.proto.newList(feature, "" + featureMap.get(feature)));
      return tree;
    }


    static Set<String> setFromLispTree(LispTree tree) {
      Set<String> set = new HashSet<String>();
      for (int i = 0; i < tree.children.size(); i++)
        set.add(tree.child(i).value);
      return set;
    }
    static LispTree setToLispTree(Set<String> set) {
      LispTree tree = LispTree.proto.newList();
      for (String x : set)
        tree.addChild(x);
      return tree;
    }

    static String[] stringArrayFromLispTree(LispTree tree) {
      String[] result = new String[tree.children.size()];
      for (int i = 0; i < tree.children.size(); i++)
        result[i] = tree.child(i).value;
      return result;
    }
    static LispTree stringArrayToLispTree(String[] array) {
      LispTree tree = LispTree.proto.newList();
      for (String x : array)
        tree.addChild(x);
      return tree;
    }

    public static LexicalEntry entryFromLispTree(LispTree tree) {
      int i = 1;
      if (tree.child(0).value.equals("entity")) {

        String textDescription = tree.child(i++).value;
        String normalizedTextDesc = tree.child(i++).value;
        Set<String> dbDescriptions = setFromLispTree(tree.child(i++));
        Formula formula = Formula.fromString(tree.child(i++).value);
        EntrySource source = EntrySource.parseSourceDesc(tree.child(i++).value);
        double popularity = Double.parseDouble(tree.child(i++).value);
        double distance = Double.parseDouble(tree.child(i++).value);
        Set<String> types = setFromLispTree(tree.child(i++));
        Counter<String> tokenEditDistanceFeatures = counterFromLispTree(tree.child(i++));

        return new LexicalEntry.EntityLexicalEntry(
            textDescription, normalizedTextDesc, dbDescriptions, formula,
            source, popularity, distance, types, tokenEditDistanceFeatures);
      } else if (tree.child(0).value.equals("unary")) {
        String textDescription = tree.child(i++).value;
        String normalizedTextDesc = tree.child(i++).value;
        Set<String> dbDescriptions = setFromLispTree(tree.child(i++));
        Formula formula = Formula.fromString(tree.child(i++).value);
        EntrySource source = EntrySource.parseSourceDesc(tree.child(i++).value);
        double popularity = Double.parseDouble(tree.child(i++).value);
        Double.parseDouble(tree.child(i++).value);
        Map<String, Double> alignmentScores = featureMapFromLispTree(tree.child(i++));
        Set<String> types = setFromLispTree(tree.child(i++));
        return new LexicalEntry.UnaryLexicalEntry(
            textDescription, normalizedTextDesc, dbDescriptions, formula, source,
            popularity, alignmentScores, types);
      } else if (tree.child(0).value.equals("binary")) {
        String textDescription = tree.child(i++).value;
        String normalizedTextDesc = tree.child(i++).value;
        Set<String> dbDescriptions = setFromLispTree(tree.child(i++));
        Formula formula = Formula.fromString(tree.child(i++).value);
        EntrySource source = EntrySource.parseSourceDesc(tree.child(i++).value);
        double popularity = Double.parseDouble(tree.child(i++).value);
        Double.parseDouble(tree.child(i++).value); // this is computed in the constructor so need not save it
        String expectedType1 = tree.child(i++).value;
        String expectedType2 = tree.child(i++).value;
        String unitId = tree.child(i++).value;
        String unitDescription = tree.child(i++).value;
        Map<String, Double> alignmentScores = featureMapFromLispTree(tree.child(i++));
        String fullLexeme = tree.child(i++).value;
        return new LexicalEntry.BinaryLexicalEntry(
            textDescription, normalizedTextDesc, dbDescriptions, formula, source,
            popularity, expectedType1, expectedType2, unitId, unitDescription, alignmentScores, fullLexeme);
      } else {
        throw new RuntimeException("Invalid: " + tree);
      }
    }

    public static String emptyIfNull(String s) { return s == null ? "" : s; }

    public static LispTree entryToLispTree(LexicalEntry rawEntry) {
      LispTree result = LispTree.proto.newList();
      if (rawEntry instanceof LexicalEntry.EntityLexicalEntry) {
        LexicalEntry.EntityLexicalEntry entry = (LexicalEntry.EntityLexicalEntry) rawEntry;
        result.addChild("entity");

        result.addChild(entry.textDescription);
        result.addChild(entry.normalizedTextDesc);
        result.addChild(setToLispTree(entry.dbDescriptions));
        result.addChild(entry.formula.toString());
        result.addChild(entry.source.toString());
        result.addChild("" + entry.popularity);
        result.addChild("" + entry.distance);
        result.addChild(setToLispTree(entry.types));
        result.addChild(counterToLispTree(entry.entityFeatures));
      } else if (rawEntry instanceof LexicalEntry.UnaryLexicalEntry) {
        LexicalEntry.UnaryLexicalEntry entry = (LexicalEntry.UnaryLexicalEntry) rawEntry;
        result.addChild("unary");

        result.addChild(entry.textDescription);
        result.addChild(entry.normalizedTextDesc);
        result.addChild(setToLispTree(entry.dbDescriptions));
        result.addChild(entry.formula.toString());
        result.addChild(entry.source.toString());
        result.addChild("" + entry.popularity);
        result.addChild("" + entry.distance);
        result.addChild(featureMapToLispTree(entry.alignmentScores));
        result.addChild(setToLispTree(entry.types));
      } else if (rawEntry instanceof LexicalEntry.BinaryLexicalEntry) {
        LexicalEntry.BinaryLexicalEntry entry = (LexicalEntry.BinaryLexicalEntry) rawEntry;
        result.addChild("binary");

        result.addChild(entry.textDescription);
        result.addChild(entry.normalizedTextDesc);
        result.addChild(setToLispTree(entry.dbDescriptions));
        result.addChild(entry.formula.toString());
        result.addChild(entry.source.toString());
        result.addChild("" + entry.popularity);
        result.addChild("" + entry.distance);
        result.addChild(entry.expectedType1);
        result.addChild(entry.expectedType2);
        result.addChild(emptyIfNull(entry.unitId));
        result.addChild(emptyIfNull(entry.unitDescription));
        result.addChild(featureMapToLispTree(entry.alignmentScores));
        result.addChild(entry.fullLexeme);
      }
      return result;
    }
  }
}
