package edu.stanford.nlp.sempre;

import fig.basic.*;

import java.io.BufferedReader;
import java.io.IOException;
import java.util.*;

import java.lang.reflect.Type;
import com.google.gson.Gson;
import com.google.gson.stream.JsonReader;
import com.google.gson.reflect.TypeToken;

/**
 * A Lexicon maps phrases (e.g., born) to lexical entries, which contain a
 * formula (e.g., fb:people.person.place_of_birth) and a type.
 * This class is meant to be simpler/faster than the normal
 * UnaryLexicon/BinaryLexicon.
 *
 * Note: this class exists because it was annoying to add types into the normal
 * UnaryLexicon and I didn't want to break backward compatibility with the rest
 * of the code.  Also, the normal lexicon is really slow with all those hash
 * maps.
 *
 * The lexicon here only knows about Formulas and fuzzy matching.
 * In the lexicon, we have rawPhrase.
 * In the user query, we have phrase.
 *
 * @author Percy Liang
 *
 * @modification
 * -- by emlozin
 * Modified to handle comments and empty lines
 */


public final class SimpleLexicon {
  public static class Entry {
    // rawPhrase was the original phrase in the Lexicon
    public Entry(String rawPhrase, Formula formula, SemType type, StringDoubleVec features) {
      this.rawPhrase = rawPhrase;
      this.formula = formula;
      this.type = type;
      this.features = features;
    }
    public final String rawPhrase;
    public final Formula formula;
    public final SemType type;
    public final StringDoubleVec features;

    @Override public String toString() {
      return "[" + rawPhrase + " => " + formula + " : " + type + "]";
    }
  }

  public static class Options {
    @Option(gloss = "Path to load lexicon files from") public List<String> inPaths;
    @Option(gloss = "Types to allow suffix (last word) matche (for people names") public List<String> matchSuffixTypes;
  }
  public static Options opts = new Options();

  private static SimpleLexicon lexicon;
  public static SimpleLexicon getSingleton() {
    if (lexicon == null) lexicon = new SimpleLexicon();
    return lexicon;
  }

  private SimpleLexicon() {
    if (opts.inPaths == null) return;
    for (String path : opts.inPaths) read(path);
  }

  // Mapping from phrase
  Map<String, List<Entry>> entries = new HashMap<String, List<Entry>>();

  public void read(String path) {
    LogInfo.begin_track("SimpleLexicon.read(%s)", path);
    try {
      BufferedReader in = IOUtils.openIn(path);
      String line;
      int numLines = 0;
      int oldNumEntries = entries.size();
      while ((line = in.readLine()) != null) {
        if ((!line.startsWith("#") && line.length() != 0) && (line.contains("{") && line.contains("}"))) {
          Map<String, Object> map = Json.readMapHard(line);
          numLines++;

          String rawPhrase = (String) map.get("lexeme");
          if (((String) map.get("formula")).contains("(") && ((String) map.get("formula")).indexOf("(")>1)
            continue;

          Formula formula = Formula.fromString((String) map.get("formula"));

          // Type
          String typeStr = (String) map.get("type");
          SemType type = typeStr != null ? SemType.fromString(typeStr) : TypeInference.inferType(formula);

          // Features
          StringDoubleVec features = null;
          Map<String, Double> featureMap = (Map<String, Double>) map.get("features");
          if (featureMap != null) {
            features = new StringDoubleVec();
            for (Map.Entry<String, Double> e : featureMap.entrySet())
              features.add(e.getKey(), e.getValue());
            features.trimToSize();
          }

          // Add verbatim feature
//          if (((String) map.get("formula")).contains("("))
//            System.out.println("Formula: "+formula.toString());

          Entry entry = new Entry(rawPhrase, formula, type, features);
          String phrase = entry.rawPhrase.toLowerCase();
          MapUtils.addToList(entries, phrase, entry);

          // For last names
          String[] parts = phrase.split(" ");
          if (opts.matchSuffixTypes != null && opts.matchSuffixTypes.contains(typeStr) && parts.length > 1) {
            StringDoubleVec newFeatures = new StringDoubleVec();
            if (features != null) {  // Copy over features
              for (StringDoubleVec.Entry e : features)
                newFeatures.add(e.getFirst(), e.getSecond());
            }
            newFeatures.add("isSuffix", 1);
            newFeatures.trimToSize();

            Entry newEntry = new Entry(rawPhrase, formula, type, newFeatures);
            MapUtils.addToList(entries, parts[parts.length - 1], newEntry);
          }
          // In the future, add other mechanisms for lemmatization.
        }
      }
      LogInfo.logs("Read %s lines, generated %d entries (now %d total)", numLines, entries.size() - oldNumEntries, entries.size());
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
    LogInfo.end_track();
  }

  public void add(String json) {
      Type t = new TypeToken<Map<String, String>>(){}.getType();
      Gson gson = new Gson();
      Map<String, Object> map = gson.fromJson(json, t);
      String rawPhrase = (String) map.get("lexeme");
      if (((String) map.get("formula")).contains("(") && ((String) map.get("formula")).indexOf("(")>1)
        return;
      Formula formula = Formula.fromString((String) map.get("formula"));
      // Type
      String typeStr = (String) map.get("type");
      SemType type = typeStr != null ? SemType.fromString(typeStr) : TypeInference.inferType(formula);

      // Features
      StringDoubleVec features = null;
      t = new TypeToken<Map<String, Double>>(){}.getType();
      Map<String, Double> feat = gson.fromJson(map.get("features").toString(), t);

      Map<String, Double> featureMap = feat;//(Map<String, Double>) map.get("features");
      if (featureMap != null) {
        features = new StringDoubleVec();
        for (Map.Entry<String, Double> e : featureMap.entrySet())
          features.add(e.getKey(), e.getValue());
        features.trimToSize();
      }

      Entry entry = new Entry(rawPhrase, formula, type, features);
      String phrase = entry.rawPhrase.toLowerCase();
      if (!entries.containsValue(entry))
        MapUtils.addToList(entries, phrase, entry);

      // For last names
      String[] parts = phrase.split(" ");
      if (opts.matchSuffixTypes != null && opts.matchSuffixTypes.contains(typeStr) && parts.length > 1) {
        StringDoubleVec newFeatures = new StringDoubleVec();
        if (features != null) {  // Copy over features
          for (StringDoubleVec.Entry e : features)
            newFeatures.add(e.getFirst(), e.getSecond());
        }
        newFeatures.add("isSuffix", 1);
        newFeatures.trimToSize();

        Entry newEntry = new Entry(rawPhrase, formula, type, newFeatures);
        if (!entries.containsValue(newEntry))
          MapUtils.addToList(entries, parts[parts.length - 1], newEntry);
      }
  }

  public List<Entry> lookup(String phrase) {
    return MapUtils.get(entries, phrase, Collections.EMPTY_LIST);
  }

  public Set<String> lookup_type(String type) {
    return entries.keySet();
  }
}
