package edu.stanford.nlp.sempre.freebase.utils;

/*
 * Distributed as part of WordWalk, a Java package for lexical
 * semantic relatedness using random graph walks.
 *
 * Copyright (C) 2008 Daniel Ramage
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.

 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.

 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110 USA
 */

import java.io.File;
import java.lang.ref.WeakReference;
import java.util.*;

import edu.stanford.nlp.io.RuntimeIOException;
import edu.stanford.nlp.objectbank.ObjectBank;


/**
 * <p>Fast, lightweight, constant time library for accessing WordNet.  Use
 * one of the public load methods to create an instance by reading the WordNet
 * data files from disk.  WordNet is represented with a set of WordID,
 * WordTagID, and SynsetID objects, each of which is unique within the WordNet
 * instance (so you can use == for equality checks).  There are many types
 * of edges between these nodes (mostly SynsetID to SynsetID), where
 * each edge type is defined in the EdgeType enum.</p>
 *
 * <p>This class would be Serializable except that the default Java
 * serialization mechanism can't handle the depth of recursion between
 * the inner WordTagID objects -- hence an instance can be created only
 * by loading the requisite file from disk.</p>
 *
 * @author dramage
 * @author Chris Manning made minimal changes to make it JavaNLP land not Ramage land
 */
public final class WordNet {

  /** Global counter for assigning unique index to each loaded id. */
  private final ArrayList<WordNetID> all = new ArrayList<WordNetID>(500000);

  /** Global immutable view of all */
  private final List<WordNetID> immutableAll = Collections.unmodifiableList(all);

  /** Set of loaded edges types */
  private final Set<EdgeType> loadedEdges = new HashSet<EdgeType>();

  /** Indexes mapping canonical strings to their typed versions */
  private final Map<String, SynsetID> synsets = new HashMap<String, SynsetID>();
  private final Map<String, WordID> words = new HashMap<String, WordID>();
  private final Map<String, WordTagID> wordtags = new HashMap<String, WordTagID>();

  /** File resource backing this WordNet instance */
  private final File path;

  /**
   * Private constructor - use one of the static constructor methods.
   *
   * @param path Path on disk to the underlying wordnet resource.
   */
  private WordNet(File path) {
    this.path = path;
  }

  /**
   * All types of edges in wordnet.  Some are transposes of each other,
   * as specified by parallel objects in edgeTransposePairs,
   * edgeTransposeTarget.
   */
  public enum EdgeType {
    // structural relationships between words, wordtags, and synsets. etc
    WORD_TO_WORDTAG,   WORDTAG_TO_WORD,    // word to all wordtag in play
    WORDTAG_IN_SYNSET, SYNSET_HAS_WORDTAG, // wordtag to all synsets
    SYNSET_WORDTAGS_OVERLAP,               // two synsets share wordtag

    // invertible synset relations
    HYPONYM,       HYPERNYM,       // from hyp; nouns and verbs
    INSTANCE_OF,   HAS_INSTANCE,   // from ins
    ENTAILS,       ENTAILED_BY,    // from ent; verbs only
    SIM_HEAD,      SIM_SATELLITE,  // from sim; adjectives only
    MM_HOLONYM,    MM_MERONYM,     // from mm; member meronyms
    MS_HOLONYM,    MS_MERONYM,     // from ms; substance meronym
    MP_HOLONYM,    MP_MERONYM,     // from mp; part meronym
    CAUSED_BY,     CAUSES,         // from cs; for verbs
    DERIVES,       DERIVED_FROM,   // from der; for noun to adj

    // self reflexive synset relations
    ATTRIBUTE,                     // from at

    // invertible word relations
    PARTICIPLE_OF, HAS_PARTICIPLE, // from ppl
    PERTAINS_TO, PERTANYM_OF,      // from per
    SEE_ALSO_TO, SEE_ALSO_FROM,    // from sa

    // self reflexive word relations
    ANTONYM,                       // from ant
    SIMILAR_VERBS,                 // from vgp

    // weird relations (words or synsets) from cls relation
    TERM_HAS_TOPIC, TOPIC_FROM_TERM,
    TERM_HAS_USAGE, USAGE_FROM_TERM,
    TERM_IN_REGION, REGION_HAS_TERM,
  }

  /** Set of relations that are transposes of eachother. */
  private static final EdgeType[][]  transpose = {
          {EdgeType.HYPONYM,           EdgeType.HYPERNYM},
          {EdgeType.INSTANCE_OF,       EdgeType.HAS_INSTANCE},
          {EdgeType.ENTAILS,           EdgeType.ENTAILED_BY},
          {EdgeType.SIM_HEAD,          EdgeType.SIM_SATELLITE},
          {EdgeType.MM_HOLONYM,        EdgeType.MM_MERONYM},
          {EdgeType.MS_HOLONYM,        EdgeType.MS_MERONYM},
          {EdgeType.MP_HOLONYM,        EdgeType.MP_MERONYM},
          {EdgeType.CAUSED_BY,         EdgeType.CAUSES},
          {EdgeType.DERIVES,           EdgeType.DERIVED_FROM},
          {EdgeType.PARTICIPLE_OF,     EdgeType.HAS_PARTICIPLE},
          {EdgeType.PERTAINS_TO,       EdgeType.PERTANYM_OF},
          {EdgeType.SEE_ALSO_TO,       EdgeType.SEE_ALSO_FROM},
          {EdgeType.TERM_HAS_TOPIC,    EdgeType.TOPIC_FROM_TERM},
          {EdgeType.TERM_HAS_USAGE,    EdgeType.USAGE_FROM_TERM},
          {EdgeType.TERM_IN_REGION,    EdgeType.REGION_HAS_TERM}
  };

  /** Set of relations that are self-reflexive, i.e. if a->b then b-> a */
  private static final EdgeType[] reflexive = {
          EdgeType.SYNSET_WORDTAGS_OVERLAP,
          EdgeType.ATTRIBUTE,
          EdgeType.SIMILAR_VERBS,
          EdgeType.ANTONYM,
  };

  /**
   * WordNet-defined part of speech tags.  Folds adjective satellite
   * in with adjective.
   */
  public enum PartOfSpeech {
    NOUN('n'), VERB('v'), ADJECTIVE('a'), ADVERB('r');

    /** WordNet character for encoding the part of speech. */
    public final char ssType;

    private PartOfSpeech(char ssType) {
      this.ssType = ssType;
    }

    public static PartOfSpeech fromWordNetSSType(char ssType) {
      for (PartOfSpeech pos : PartOfSpeech.values()) {
        if (pos.ssType == ssType) {
          return pos;
        }
      }
      if (ssType == 's') {
        return ADJECTIVE;
      }
      throw new IllegalArgumentException("Unexpected ss_type: " + ssType);
    }
  }

  /** Base of all wordnet ids.  Comparison is based on index number. */
  public abstract class WordNetID implements Comparable<WordNetID> {
    /** Globally unique index of this WordNetID */
    private int index;

    /** Outgoing links from this node */
    private Map<EdgeType, ArrayList<WordNetID>> links
            = new EnumMap<EdgeType, ArrayList<WordNetID>>(EdgeType.class);

    /** Private no-arg constructor prevents new subclasses */
    private WordNetID() {
      index = all.size();
      all.add(this);
    }

    /**
     * Returns the set of nodes linked from this note by the given
     * edge type.
     */
    public List<WordNetID> get(EdgeType type) {
      List<WordNetID> set = links.get(type);
      if (set == null) { set = Collections.emptyList(); }
      return Collections.unmodifiableList(set);
    }

    /** Returns the index of this WordNetID in the enclosing WordNet instance */
    public int index() {
      return index;
    }

    /** Adds a link with the given edge type to the object */
    protected void add(EdgeType type, WordNetID target) {
      ArrayList<WordNetID> list = links.get(type);
      if (list == null) {
        list = new ArrayList<WordNetID>();
        links.put(type, list);
      }
      if (!list.contains(target)) {
        list.add(target);
      }
    }

    /** Compacts and sorts each edge list. */
    protected void compact() {
      for (ArrayList<WordNetID> edge : links.values()) {
        Collections.sort(edge);
        edge.trimToSize();
      }
    }

    /** Ordered by index in enclosing WordNet */
    public int compareTo(WordNetID other) {
      return this.index < other.index ? -1 :
              (this.index > other.index ?  1 : 0);
    }

    /** Returns the enclosing WordNet instance */
    public WordNet getEnclosingWordNet() {
      return WordNet.this;
    }

    /** Ordered by index in enclosing WordNet */
    @Override
    public boolean equals(Object other) {
      if (other == this) return true;
      if (!(other instanceof WordNetID)) return false;
      WordNetID w = (WordNetID) other;
      return this.index == w.index;
    }

    @Override
    public int hashCode() {
      return index;
    }

  }

  /**
   * Represents a word, e.g. "dog".
   *
   * Instances of this class are unique for a given {@link WordNet},
   * so direct object equality and hashcode are the default behavior.
   *
   * @author dramage
   */
  public final class WordID extends WordNetID {

    /** WordTags for this Word */
    private final EnumMap<PartOfSpeech, WordTagID> mWordTags
            = new EnumMap<PartOfSpeech, WordTagID>(PartOfSpeech.class);

    /** The string of our word (with spaces converted to underscore) */
    public final String word;

    /** Immutable view of all WordTagIDs this WordID is part of. */
    public final Map<PartOfSpeech, WordTagID> wordTags
            = Collections.unmodifiableMap(mWordTags);

    private WordID(String word) {
      this.word = word;
    }

    /**
     * Gets the WordTagID for this word with the given part of speech
     * or null if the part of speech does not apply to this word.
     */
    public WordTagID getWordTag(PartOfSpeech tag) {
      return mWordTags.get(tag);
    }

    /** Adds a reference to the given WordTagID */
    private void addWordTag(WordTagID wordTagId) {
      assert !mWordTags.containsKey(wordTagId.tag)
              || mWordTags.get(wordTagId.tag) == wordTagId
              : "Unexpected duplicate WordTagID";

      this.mWordTags.put(wordTagId.tag, wordTagId);
    }

    @Override
    public String toString() {
      return word;
    }
  }

  /**
   * Represents a word with part of speech tag, e.g. "dog#n".
   *
   * Instances of this class are unique for a given {@link WordNet},
   * so direct object equality and hashcode are the default behavior.
   *
   * @author dramage
   */
  public final class WordTagID extends WordNetID {

    /** Mutable view of word senses for this WordTag. */
    private final ArrayList<SynsetID> mSynsets
            = new ArrayList<SynsetID>(1);

    /** The WordID we are an instance of */
    public final WordID word;

    /** Our part of speech tag */
    public PartOfSpeech tag;

    /** Immutable list of all synsets this WordTag takes part in. */
    public final List<SynsetID> synsets
            = Collections.unmodifiableList(mSynsets);

    public List<SynsetID> getSynsets() {
      return this.synsets;
    }
    private WordTagID(WordID word, PartOfSpeech tag) {
      this.word = word;
      this.tag = tag;
    }

    /**
     * Gets the n'th synset associated with this word tag.  This index
     * is 1-based to be consistent with wordnet's numbering scheme; i.e.
     * getSynset(0) will throw an IllegalArgumentException.
     * */
    public SynsetID getSynset(int num) {
      num--;  // convert to 0-indexed
      if (num < 0) {
        throw new IllegalArgumentException("SynsetIDs are 1-based");
      }
      return mSynsets.get(num);
    }

    /** Adds the given Synset into this WordTag with the given number */
    private void addSynset(SynsetID synsetId, int num) {
      num--;  // convert to 0-indexed
      assert num >= 0 && (mSynsets.size() <= num ||
              mSynsets.get(num) == null || mSynsets.get(num) == synsetId)
              : "Unexpected repeat of word sense " + synsetId + " on " + this
              + ": already " + mSynsets.get(num);

      while (mSynsets.size() <= num) {
        mSynsets.add(null);
      }
      mSynsets.set(num, synsetId);
    }

    @Override
    public String toString() {
      return word + "#" + tag.ssType;
    }
  }

  /**
   * Represents a WordNet synset id as an int, e.g. 301380127.
   *
   * Instances of this class are unique for a given {@link WordNet},
   * so direct object equality and hashcode are the default behavior.
   *
   * Each SynsetID has potentially many WordTagSenseID's.
   *
   * @author dramage
   */
  public final class SynsetID extends WordNetID {

    /** Modifiable WordTagIDs associated with this SynsetID */
    private final ArrayList<WordTagID> mWordTags
            = new ArrayList<WordTagID>(1);

    /** The synset id number */
    public final int synset;

    /** Number of times this sense was tagged in a corpus or 0 for not seen. */
    public final int count;

    /** The number of this synset in the first WordTagID (for toString) */
    private int numberInFirstWordTagID = 0;

    /** All WordTags that this Synset is a part of */
    public final List<WordTagID> wordtags
            = Collections.unmodifiableList(mWordTags);

    private SynsetID(int synset, int count) {
      this.synset = synset;
      this.count = count;
    }

    /** Returns the n'th word sense */
    public WordTagID getWordTag(int num) {
      num--; // convert to 0-indexed
      if (num < 0) {
        throw new IllegalArgumentException("WordTagIDs are 1-based");
      }

      return mWordTags.get(num);
    }

    /**
     * Adds the given WordTagSense into this synset
     */
    private void addWordSense(WordTagID wordTagId, int num) {
      num--;  // convert to 0-indexed
      assert num >= 0 && (mWordTags.size() <= num ||
              mWordTags.get(num) == null || mWordTags.get(num) == wordTagId)
              : "Unexpected repeat of word " + wordTagId + " on " + this
              + ": already " + mWordTags.get(num);

      while (mWordTags.size() <= num) {
        mWordTags.add(null);
      }
      mWordTags.set(num, wordTagId);
    }

    @Override
    public String toString() {
      return mWordTags.get(0) + "#" + numberInFirstWordTagID;
    }
  }

  /** Returns a collection of all the WordNetIDs loaded. */
  public Collection<WordNetID> getAllWordNetIDs() {
    return immutableAll;
  }

  /** Returns a collection of all the words loaded. */
  public Collection<String> getAllWords() {
    return words.keySet();
  }
  /**
   * Returns the given WordNetID by it's index.  The return value could
   * be cast to one of the three possible subclasses: WordID, WordTagID,
   * or SynsetID.
   */
  public WordNetID getWordNetID(int index) {
    return all.get(index);
  }

  /**
   * Returns the WordNetID referred to by the given string.  Delegates
   * to one of getSynsetID, getWordTagID, or getWordID depending on the
   * format of the string or null if no such ID can be found.
   */
  public WordNetID getWordNetID(String string) {
    int firstHash = string.indexOf('#');
    if (firstHash < 0) {
      WordNetID rv = getWordID(string);
      return rv != null ? rv : getSynsetID(string);
    } else {
      int secondHash = string.lastIndexOf('#');
      if (firstHash == secondHash) {
        return getWordTagID(string);
      } else {
        return getSynsetID(string);
      }
    }
  }

  /**
   * Returns the SynsetID instance as described by the given id string
   * or null if there is no such id.  The string can either be a synset id
   * such as "100001740" or a word sense such as "entity#n#1".
   */
  public SynsetID getSynsetID(String string) {
    // check if specified as "100001740"
    SynsetID id = synsets.get(string);
    if (id == null) {
      // check if specified as "entity#n#1"
      int split = string.lastIndexOf('#');
      if (split >= 1) {
        int num;
        try {
          num = Integer.parseInt(string.substring(split + 1));
        } catch (NumberFormatException e) {
          return null;
        }
        WordTagID wordTagId = wordtags.get(string.substring(0, split));
        if (wordTagId != null) {
          id = wordTagId.getSynset(num);
        }
      }
    }
    return id;
  }

  /**
   * Returns the WordID instance from a string such as "dog"
   * or null if there is no such id.
   */
  public WordID getWordID(String string) {
    return words.get(string);
  }

  /**
   * Returns the WordTagID instance from a string such as "dog#n"
   * or null if there is no such id.
   */
  public WordTagID getWordTagID(String string) {
    return wordtags.get(string);
  }

  /**
   * The total number of loaded nodes in the WordNet instance.
   */
  public int size() {
    return all.size();
  }

  /**
   * Returns the path on disk of the underlying WordNet database.
   */
  @Override
  public String toString() {
    return path.toString();
  }

  /**
   * Stitches together the structural edge types, WORD_TOWORDTAG,
   * SYNSET_HAS_WORDTAG, etc.
   */
  private void createStructuralEdges() {
    // create WORD_TO_WORDTAG and WORDTAG_TO_WORD
    for (WordID word : words.values()) {
      for (WordTagID wordtag : word.wordTags.values()) {
        word.add(EdgeType.WORD_TO_WORDTAG, wordtag);
        wordtag.add(EdgeType.WORDTAG_TO_WORD, word);
      }
    }

    // create WORTAG_IN_SYNSET and SYNSET_HAS_WORDTAG
    for (WordTagID wordtag : wordtags.values()) {
      for (SynsetID synset : wordtag.synsets) {
        wordtag.add(EdgeType.WORDTAG_IN_SYNSET, synset);
        synset.add(EdgeType.SYNSET_HAS_WORDTAG, wordtag);
      }
    }

    // create SYNSET_WORDTAGS_OVERLAP
    for (WordTagID wordtag : wordtags.values()) {
      for (SynsetID synsetA : wordtag.mSynsets) {
        if (synsetA == null) { continue; }
        for (SynsetID synsetB : wordtag.mSynsets) {
          if (synsetB == null || synsetA == synsetB) { continue; }
          synsetA.add(EdgeType.SYNSET_WORDTAGS_OVERLAP, synsetB);
        }
      }
    }

    // record that we have created these edges
    loadedEdges.addAll(Arrays.asList(new EdgeType[]{
            EdgeType.WORD_TO_WORDTAG, EdgeType.WORDTAG_TO_WORD,
            EdgeType.WORDTAG_IN_SYNSET, EdgeType.SYNSET_HAS_WORDTAG,
            EdgeType.SYNSET_WORDTAGS_OVERLAP, }));
  }

  /** Compacts and finalizes ordering of all data structures */
  private void compact() {
    // sort the WordNetID's by type (then by order added)
    Collections.sort(all, new Comparator<WordNetID>() {
      public int compare(WordNetID o1, WordNetID o2) {
        if (o1.getClass() == o2.getClass()) {
          return o1.compareTo(o2);
        } else if (o1.getClass() == SynsetID.class) {
          return -1;
        } else if (o2.getClass() == SynsetID.class) {
          return 1;
        } else if (o1.getClass() == WordTagID.class) {
          return -1;
        } else if (o2.getClass() == WordTagID.class) {
          return 1;
        } else {
          throw new RuntimeException("Unexpected WordNetID type");
        }
      }
    });

    // re-number the wordnet id's
    for (int i = 0; i < all.size(); i++) {
      all.get(i).index = i;
    }

    for (WordNetID id : all) {
      id.compact();
    }
    for (SynsetID id : synsets.values()) {
      id.mWordTags.trimToSize();
    }
    for (WordTagID id : wordtags.values()) {
      id.mSynsets.trimToSize();
    }
    all.trimToSize();
  }

  /** Checks the representation invariants */
  private void checkrep() {
    for (EdgeType edgetype : EdgeType.values()) {
      assertIt(loadedEdges.contains(edgetype), "Failed to load " + edgetype);
    }

    for (SynsetID synset : synsets.values()) {
      assertIt(all.get(synset.index()) == synset, "Misplaced synset " + synset);
      assertIt(synset.getWordTag(1).getSynset(synset.numberInFirstWordTagID) == synset,
              "Wrong number in first WordTagID " + synset);
      assertIt(!synset.wordtags.contains(null), synset + " contains null WordTagID");

      for (WordTagID wordtag : synset.wordtags) {
        assertIt(wordtag.synsets.contains(synset), "Miswired Synset " + synset);
      }
    }

    for (WordTagID wordtag : wordtags.values()) {
      assertIt(all.get(wordtag.index()) == wordtag, "Misplaced wordtag " + wordtag);
      assertIt(!wordtag.synsets.contains(null), wordtag + " contains null SynsetID");

      for (SynsetID synset : wordtag.synsets) {
        assertIt(synset.wordtags.contains(wordtag), "Miswired WordTag " + wordtag);
      }

      assertIt(wordtag.word.wordTags.containsValue(wordtag), "Miswired WordTag " + wordtag);
    }

    for (WordID word : words.values()) {
      assertIt(all.get(word.index()) == word, "Misplaced word " + word);
      for (WordTagID wordtag : word.wordTags.values()) {
        assertIt(wordtag.word == word, "Miswired WordTag " + word);
      }
    }

    for (WordNetID id1 : all) {
      for (EdgeType[] pair : transpose) {
        for (WordNetID id2 : id1.get(pair[0])) {
          assertIt(id2.get(pair[1]).contains(id1),
                  "Missing transpose " + Arrays.asList(transpose)
                          + " " + id1 + " " + id2);
        }
      }
    }

    for (WordNetID id1 : all) {
      for (EdgeType type : reflexive) {
        for (WordNetID id2 : id1.get(type)) {
          assertIt(id2.get(type).contains(id1),
                  "Missing reflextive " + type + " " + id1 + " " + id2);
        }
      }
    }
  }

  /** Assertion method used by checkRep */
  private static void assertIt(boolean condition, String message) {
    if (!condition) {
      throw new AssertionError(message);
    }
  }


  //
  // WordNet loader
  //

  /** Weak collection of WordNet instances based on file name */
  private static final Collection<WeakReference<WordNet>> instances
          = new LinkedList<WeakReference<WordNet>>();

  /**
   * Loads an instance of WordNet from the given WordNet database --
   * currently supports only Prolog DB format.
   */
  public static WordNet load(File path) {
    System.err.println("WordNet.load: " + path);

    // see if already loaded
    for (Iterator<WeakReference<WordNet>> it = instances.iterator(); it.hasNext();) {

      WordNet wordnet = it.next().get();

      if (wordnet == null) {
        it.remove();
      } else if (wordnet.path.equals(path)) {
        return wordnet;
      }
    }

    // not already loaded, load now
    try {
      WordNet wordnet = loadPrologWordNet(path);
      instances.add(new WeakReference<WordNet>(wordnet));
      return wordnet;
    } catch (RuntimeIOException e) {
      throw new IllegalArgumentException(
              "Provided path not a valid WordNet PrologDB directory", e);
    }
  }

  /**
   * Returns an instance of WordNet based on the contents of the WordNet
   * databases as stored in the given path prolog WordNet 3.0 format.
   */
  public static WordNet loadPrologWordNet(File path) {
    WordNet wordnet = new WordNet(path);

    /** Global stashing point for unassigned word senses depending on wn POS tag */
    Map<WordTagID, SynsetID> deferredPositionSynsets = new HashMap<WordTagID, SynsetID>();

    //
    // read the synsets file
    //
    {
      for (String line : ObjectBank.getLineIterator(new File(path, "wn_s.pl"))) {
        if (line.length() == 0) continue;

        // fields from the line
        String[] fields = line.substring(2, line.length() - 2).split(",");
        final int wordTagNumberInSynset = Integer.parseInt(fields[1]);
        final int synsetNumberInWordTag = Integer.parseInt(
                fields.length > 4 ? fields[4] : "0");
        final int senseCount = Integer.parseInt(
                fields.length > 5 ? fields[5] : "0");
        final String word = new String(fields[2].substring(
                1, fields[2].length() - 1).replaceAll("\\s+", "_"))
                .toLowerCase();
        final PartOfSpeech tag = PartOfSpeech.fromWordNetSSType(
                fields[3].charAt(0));
        final String wordTag = word + "#" + tag.ssType;
        final String synset = fields[0];

        // add WordID
        WordID wordId = wordnet.words.get(word);
        if (wordId == null) {
          wordId = wordnet.new WordID(word);
          wordnet.words.put(word, wordId);
        }

        // add WordTagID
        WordTagID wordTagId = wordnet.wordtags.get(wordTag);
        if (wordTagId == null) {
          wordTagId = wordnet.new WordTagID(wordId, tag);
          wordnet.wordtags.put(wordTag, wordTagId);
        }

        // add SynsetID
        SynsetID synsetId = wordnet.synsets.get(synset);
        if (synsetId == null) {
          synsetId = wordnet.new SynsetID(Integer.parseInt(synset),
                  senseCount);
          wordnet.synsets.put(synset, synsetId);
        }

        // link WordID to WordTagID
        wordId.addWordTag(wordTagId);

        // link WordTagID to SynsetID
        if (synsetNumberInWordTag == 0) {
          if (deferredPositionSynsets.containsKey(wordTagId)) {
            throw new RuntimeException("Error: don't know what " +
                    "to do when more than one synset doesn't come" +
                    " with a valid sense number");
          } else {
            deferredPositionSynsets.put(wordTagId, synsetId);
          }
        } else {
          wordTagId.addSynset(synsetId, synsetNumberInWordTag);
        }
        synsetId.addWordSense(wordTagId, wordTagNumberInSynset);
      }

      // add in deferredPositionSynsets
      for (Map.Entry<WordTagID, SynsetID> entry : deferredPositionSynsets.entrySet()) {
        boolean placed = false;
        for (int i = 0; i < entry.getKey().mSynsets.size(); i++) {
          if (entry.getKey().mSynsets.get(i) == null) {
            entry.getKey().mSynsets.set(i, entry.getValue());
            placed = true;
          }
        }
        if (!placed) {
          throw new AssertionError("Unable to place deferred synset");
        }
      }

      // tell each SynsetID its position in its first WordTagID
      for (SynsetID synset : wordnet.synsets.values()) {
        int position = synset.wordtags.get(0).mSynsets.indexOf(synset) + 1;
        assert position >= 1 : "Unexpected: couldn't find the synset";
        synset.numberInFirstWordTagID = position;
      }

      // add all structural edges
      wordnet.createStructuralEdges();
    }


    //
    // read all synset relations defined over SynsetID pairs
    //

    // invert-ready synset relations
    wordnet.loadSynsetRelation(path, "hyp", EdgeType.HYPONYM);
    wordnet.loadSynsetRelation(path, "ins", EdgeType.INSTANCE_OF);
    wordnet.loadSynsetRelation(path, "ent", EdgeType.ENTAILS);
    wordnet.loadSynsetRelation(path, "sim", EdgeType.SIM_HEAD);
    wordnet.loadSynsetRelation(path, "mm",  EdgeType.MM_HOLONYM);
    wordnet.loadSynsetRelation(path, "ms",  EdgeType.MS_HOLONYM);
    wordnet.loadSynsetRelation(path, "mp",  EdgeType.MP_HOLONYM);
    wordnet.loadSynsetRelation(path, "cs",  EdgeType.CAUSED_BY);

    // self-reflexive synset relations
    wordnet.loadSynsetRelation(path, "at",  EdgeType.ATTRIBUTE);

    // invert-ready word relations
    wordnet.loadWordRelation(path, "ppl", EdgeType.PARTICIPLE_OF);
    wordnet.loadWordRelation(path, "per", EdgeType.PERTAINS_TO);
    wordnet.loadWordRelation(path, "sa",  EdgeType.SEE_ALSO_TO);

    // self-reflexive word relations
    wordnet.loadWordRelation(path, "der", EdgeType.DERIVES);
    wordnet.loadWordRelation(path, "vgp", EdgeType.SIMILAR_VERBS);
    wordnet.loadWordRelation(path, "ant", EdgeType.ANTONYM);

    // weird class relations
    wordnet.loadCLSRelations(path);

    // do transposes
    for (EdgeType[] pair : transpose) {
      wordnet.addTranspose(pair[0], pair[1]);
    }

    // compact data structure and check rep invariants

    wordnet.compact();
    wordnet.checkrep();

    return wordnet;
  }

  /**
   * Loads the given relation from the prolog file, storing the result in
   * the given EdgeType.
   */
  private void loadSynsetRelation(File path, String relation, EdgeType type) {
    if (loadedEdges.contains(type)) {
      throw new IllegalArgumentException("Unexpected error: trying to load " + type + " twice");
    }
    loadedEdges.add(type);

    for (String line : ObjectBank.getLineIterator(new File(path, "wn_" + relation + ".pl"))) {
      if (line.length() == 0) continue;
      String[] fields = line.substring(relation.length() + 1, line.length() - 2).split(",");

      SynsetID id1 = getSynsetID(fields[0]);
      SynsetID id2 = getSynsetID(fields[1]);

      id1.add(type, id2);
    }
  }

  /**
   * Loads the given relation from the prolog file, storing the result in
   * the given EdgeType.
   */
  private void loadWordRelation(File path, String relation, EdgeType type) {
    if (loadedEdges.contains(type)) {
      throw new IllegalArgumentException("Unexpected error: trying to load " + type + " twice");
    }
    loadedEdges.add(type);

    for (String line : ObjectBank.getLineIterator(new File(path, "wn_" + relation + ".pl"))) {
      if (line.length() == 0) continue;
      String[] fields = line.substring(relation.length() + 1, line.length() - 2).split(",");

      final SynsetID sid1 = getSynsetID(fields[0]);
      final SynsetID sid2 = getSynsetID(fields[2]);

      if (sid1 == sid2) {
        System.err.println("WordNet.loadWordRelation(" + relation + "): skipping self-loop on " + sid1);
      } else {
        sid1.add(type, sid2);
      }
    }
  }

  /**
   * Loads the given relation from the prolog file, storing the result in
   * the given EdgeType.
   */
  private void loadCLSRelations(File path) {
    final String relation = "cls";

    if (loadedEdges.contains(EdgeType.TERM_HAS_TOPIC) ||
            loadedEdges.contains(EdgeType.TERM_HAS_USAGE) ||
            loadedEdges.contains(EdgeType.TERM_IN_REGION)) {

      throw new IllegalArgumentException("Unexpected error while loading " + relation);
    }

    loadedEdges.add(EdgeType.TERM_HAS_TOPIC);
    loadedEdges.add(EdgeType.TERM_HAS_USAGE);
    loadedEdges.add(EdgeType.TERM_IN_REGION);

    for (String line : ObjectBank.getLineIterator(new File(path, "wn_" + relation + ".pl"))) {
      if (line.length() == 0) continue;
      String[] fields = line.substring(relation.length() + 1, line.length() - 2).split(",");

      if (fields.length != 5 || fields[4].length() != 1) {
        throw new IllegalArgumentException("Badly formed file for " + relation);
      }

      SynsetID sid1 = getSynsetID(fields[0]);
      SynsetID sid2 = getSynsetID(fields[2]);

      int num1 = Integer.parseInt(fields[1]);
      int num2 = Integer.parseInt(fields[3]);
      assert !((num1 == 0) ^ (num2 == 0));

      WordNetID id1 = num1 == 0 ? sid1 : sid1.getWordTag(num1);
      WordNetID id2 = num2 == 0 ? sid2 : sid2.getWordTag(num2);

      switch (fields[4].charAt(0)) {
        case 't':
          id1.add(EdgeType.TERM_HAS_TOPIC, id2);
          break;

        case 'u':
          id1.add(EdgeType.TERM_HAS_USAGE, id2);
          break;

        case 'r':
          id1.add(EdgeType.TERM_IN_REGION, id2);
          break;

        default:
          throw new IllegalArgumentException("Unexpected relation type " + fields[4]);
      }
    }
  }

  /** Adds a new edge type t2 as the transpose of t1 */
  private void addTranspose(EdgeType t1, EdgeType t2) {
    if (!loadedEdges.contains(t1)) {
      throw new IllegalArgumentException("Cannot transpose: doesn't contain " + t1);
    } else if (loadedEdges.contains(t2)) {
      throw new IllegalArgumentException("Cannot transpose: already contains " + t2);
    }
    loadedEdges.add(t2);

    /** Add reverse links for all types */
    for (WordNetID ab : all) {
      for (WordNetID ba : ab.get(t1)) {
        ba.add(t2, ab);
      }
    }
  }


  //
  // Sample main method for testing
  //
  public static void main(String[] args) {
    WordNet wordnet = load(new File(args[0]));

    SynsetID id1 = wordnet.getSynsetID("run#v#1");

    for (EdgeType edgetype : EdgeType.values()) {
      for (WordNetID id2 : id1.get(edgetype)) {
        System.out.println(edgetype + " " + id2);
      }
    }

    System.out.println(wordnet.getAllWordNetIDs().size());

    for (SynsetID synset : wordnet.synsets.values()) {
      System.out.printf("%06d %s\n", synset.synset, synset.toString());
    }
  }

}
