public class LexiconAdapter {

	private DictionaryCollection lc;
	
	/**
	 * Dictionary names
	 */
	private final static String WORDFORMS = "paliwordforms",
			LEMMA = "pali_main_new", GENERATED = "paligenerated";

	/**
	 * Client
	 */
	private DictionaryClient dc;

	public LexiconAdapter () throws Exception {
		this("germa232.uni-trier.de", 8080, "testrw", "testrw");
	}
	
	/**
	 * Constructor
	 * @throws Exception
	 */
	public LexiconAdapter(String domain, int port, String user, String pw) throws Exception {
		dc = new DictionaryClient(new ConnectionProfile(domain,port,user,pw));
		lc = dc.getCollection(LEMMA);
	}

	/**
	 * Performs a ping
	 * @throws Exception
	 */
	public void ping () throws Exception {
		dc.ping();
	}

	/**
	 * Returns the names of all collections currently in the lexical database
	 * @throws Exception
	 */
	public void listCollections () throws Exception {
		for (String s : dc.listCollections()) 
			System.out.println(s);
	}

	/**
	 * Checks whether the wordform collection contains the given word
	 * @param form word
	 * @return true if wordform collection contains word
	 * @throws Exception
	 */
	public boolean wordformContains (String form) throws Exception {
		DictionaryCollection c = dc.getCollection(WORDFORMS);
		return c.getWordsByTags("word", form).length > 0;
	}

	/**
	 * Checks whether the lemma collection contains the given word
	 * @param lemma word
	 * @return true if lemma collection contains word
	 * @throws Exception
	 */
	public boolean lemmaContains (String lemma) throws Exception {
		return lc.getWordsByTags("form.lemma", lemma).length > 0;
	}

	/**
	 * Checks whether the generated collection contains the given word
	 * @param word word
	 * @return true if generated collection contains word
	 * @throws Exception
	 */
	public boolean generatedContains (String word) {
		DictionaryCollection c;
		try {
			c = dc.getCollection(GENERATED);
		} catch (Exception e) {
			return false;
		}
		
		try {
			return c.getWordsByTags("word", word).length > 0;
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return false;
	}
	
	/**
	 * Returns the dictionary words corresponding to the given
	 * word, extracted from the generated collection
	 * @param word word to extract
	 * @return dictionary words
	 * @throws Exception
	 */
	public String getGenerated (String word) throws Exception {
		if (generatedContains(word)) {
			DictionaryCollection c = dc.getCollection(GENERATED);
			DictWord[] dws = c.getWordsByTags("word", word);
			if (dws.length > 1) {
				StringBuilder out = new StringBuilder("[");
				for (DictWord dw : dws) {
					out.append(dw.toJSON()).append(",");
				}
				out.deleteCharAt(out.length()-1);
				out.append("]");
				return out.toString();
			} else {
				return dws[0].toJSON();
			}
		} else {
			System.err.println("Wordform " + word + " cannot be found in " + GENERATED);
		}
		return null;
	}
	
	/**
	 * Returns the dictionary words corresponding to the given
	 * word, extracted from the wordform collection
	 * @param form word to extract
	 * @return dictionary words
	 * @throws Exception
	 */
	public List<DictWord> getWordform (String form) throws Exception {
		if (wordformContains(form)) {
			DictionaryCollection c = dc.getCollection(WORDFORMS);
			DictWord[] dws = c.getWordsByTags("word", form);
			if (dws.length > 1) {
				return Arrays.asList(dws);
			} else {
				return Collections.singletonList(dws[0]);
			}
		} else
			System.err.println("Wordform " + form + " cannot be found in " + WORDFORMS);
		return null;
	}
	
	/**
	 * Returns the dictionary words corresponding to the given
	 * word, extracted from the lemma collection
	 * @param lemma word to extract
	 * @return dictionary words
	 * @throws Exception
	 */
	public String getLemma (String lemma) throws Exception {
		DictionaryCollection c = dc.getCollection(LEMMA);
		if (lemmaContains(lemma)) {
			DictWord[] dws = c.getWordsByTags("form.lemma", lemma);
			if (dws.length > 1) {
				StringBuilder out = new StringBuilder("[");
				for (DictWord dw : dws) {
					out.append(dw.toJSON()).append(",");
				}
				out.deleteCharAt(out.length()-1);
				out.append("]");
				return out.toString();
			} else {
				return dws[0].toJSON();
			}
		} else {
			System.err.println("Lemma " + lemma + " cannot be found in " + LEMMA);
		}
		return null;
	}
		
	public JObject[] getLemmaEntriesAsJObjectArray (String lemma) throws Exception {
		
		if (lemmaContains(lemma)) {
			return lc.getWordsByTags("form.lemma", lemma);
		}
		return null;
	}
	
	/**
	 * Checks whether the given word is contained in any collection
	 * @param word word to check
	 * @return true if any collection contains word
	 * @throws Exception
	 */
	public boolean wordInDict (String word) throws Exception {
		return wordformContains(word) || lemmaContains(word) || generatedContains(word);
	}
	
	public List<DictWord> getAllWordforms() throws Exception {
		List<DictWord> list = new ArrayList<DictWord>();
		DictionaryCollection c = dc.getCollection(LEMMA);
		Iterator<DictWord> i = c.getWordIterator(200);
		
		for (DictWord id = i.next(); i.hasNext(); id = i.next()) {
			list.add(id);
			
		}
		DictionaryCollection c2 = dc.getCollection(WORDFORMS);
		Iterator<DictWord> i2 = c2.getWordIterator(200);
		for (DictWord id = i2.next(); i2.hasNext(); id = i2.next()) {
			if (!list.contains(id))
				list.add(id);
		}
		return list;
	}

	public String getLemmaById(String id) throws Exception {
		DictionaryCollection c = dc.getCollection(LEMMA);
		
		DictWord dw = c.getWord(id);
		String[] path = {"form", "lemma"};
		String p = dw.getPropertyStringValueNormalized(path);
		
		return p;
	}
	
} public class ConstructedWord
{

	////////////////////////////////////////////////////////////////////////////////////////////////
	// Constants
	////////////////////////////////////////////////////////////////////////////////////////////////

	////////////////////////////////////////////////////////////////////////////////////////////////
	// Variables
	////////////////////////////////////////////////////////////////////////////////////////////////

	/**
	 * Fields
	 */
	private String word;
	private String stem;
	private String lemma;

	/**
	 * Morphological information
	 */
	private FeatureSet featureSet;

	////////////////////////////////////////////////////////////////////////////////////////////////
	// Constructor
	////////////////////////////////////////////////////////////////////////////////////////////////

	/**
	 * Constructor
	 */
	public ConstructedWord () {
		featureSet = new FeatureSet();
	}
	
	/**
	 * Constructor
	 * @param s word
	 */
	public ConstructedWord (String s) {
		this();
		word = s;
	}
	
	/**
	 * Constructor
	 * @param s word
	 * @param fs feature set
	 */
	public ConstructedWord (String s, FeatureSet fs) {
		word = s;
		FeatureSet fsc = new FeatureSet();
		for (Feature f : fs) 
		{
			fsc.add(f);
		}
		this.featureSet = fsc;
	}
	
	////////////////////////////////////////////////////////////////////////////////////////////////
	// Methods
	////////////////////////////////////////////////////////////////////////////////////////////////

	/**
	 * Returns the word string
	 * @return word
	 */
	public String getWord() {
		return word;
	}
	
	/**
	 * Sets the word string
	 * @param s word
	 */
	public void setWord(String s) {
		word = s;
	}
	
	/**
	 * Sets the feature set
	 * @param fs feature set
	 */
	public void setFeatureSet (FeatureSet fs) {
		FeatureSet fsc = new FeatureSet();
		for (Feature f : fs)
			fsc.add(f);
		this.featureSet = fsc;
	}
	
	/**
	 * Returns the feature set
	 * @return feature set
	 */
	public FeatureSet getFeatureSet () {
		return featureSet;
	}
	
	/**
	 * Default toString method
	 */
	public String toString () {
		String l = lemma == null ? "" : "\nLemma:\n" + lemma;
		return "Constructed word: " + word + l + "\nFeatures:\n" + featureSet.toString() + "\n\n";
	}
	
	/**
	 * Adds a feature to this feature set
	 * @param f feature
	 */
	public void addFeature (Feature f) {
		featureSet.add(f);
	}
	
	/**
	 * Returns the stem string
	 * @return stem
	 */
	public String getStem () {
		return stem;
	}
	
	/**
	 * Sets the stem string
	 * @param s stem
	 */
	public void setStem (String s) {
		stem = s;
	}

	/**
	 * Sets the lemma string
	 * @param lemma2 lemma
	 */
	public void setLemma(String lemma2) {
		lemma = lemma2;
	}

	/**
	 * Returns the lemma string
	 * @return lemma
	 */
	public String getLemma() {
		return lemma;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((featureSet == null) ? 0 : featureSet.hashCode());
		result = prime * result + ((lemma == null) ? 0 : lemma.hashCode());
		result = prime * result + ((stem == null) ? 0 : stem.hashCode());
		result = prime * result + ((word == null) ? 0 : word.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		ConstructedWord other = (ConstructedWord) obj;
		if (featureSet == null) {
			if (other.featureSet != null)
				return false;
		} else if (!featureSet.equals(other.featureSet))
			return false;
		if (lemma == null) {
			if (other.lemma != null)
				return false;
		} else if (!lemma.equals(other.lemma))
			return false;
		if (stem == null) {
			if (other.stem != null)
				return false;
		} else if (!stem.equals(other.stem))
			return false;
		if (word == null) {
			if (other.word != null)
				return false;
		} else if (!word.equals(other.word))
			return false;
		return true;
	}
	
	/**
	 * Returns a deep copy of this object
	 */
	public ConstructedWord clone () {
		ConstructedWord copy = new ConstructedWord();
		copy.stem = this.stem;
		copy.lemma = this.lemma;
		copy.word = this.word;
		FeatureSet fs = new FeatureSet();
		for (Feature f : this.featureSet) {
			fs.add(new Feature(f.getKey(), f.getValue()));
		}
		copy.setFeatureSet(featureSet);
		return copy;
	}
} public class Feature {

	/**
	 * Key-value
	 */
	private String key, value;
	
	/**
	 * Constructor
	 */
	public Feature () {
		
	}
	
	/**
	 * Constructor with values
	 * @param key key
	 * @param value value
	 */
	public Feature (String key, String value) {
		this.key = key;
		this.value = value;
	}
	
	/**
	 * Returns the key of the key-value pair
	 * @return key
	 */
	public String getKey () {
		return key;
	}
	
	/**
	 * Returns the value of the key-value pair
	 * @return value
	 */
	public String getValue () {
		return value;
	}
	
	/**
	 * Returns a string representation
	 * @return string
	 */
	public String toString () {
		return key + "->" + value;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((key == null) ? 0 : key.hashCode());
		result = prime * result + ((value == null) ? 0 : value.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Feature other = (Feature) obj;
		if (key == null) {
			if (other.key != null)
				return false;
		} else if (!key.equals(other.key))
			return false;
		if (value == null) {
			if (other.value != null)
				return false;
		} else if (!value.equals(other.value))
			return false;
		return true;
	}

	public boolean setValue(String featureValue) {
		this.value = featureValue;
		return true;
	}
} public class FeatureSet implements Iterable<Feature> {

	/**
	 * Features
	 */
	private List<Feature> features;
	
	/**
	 * Constructor
	 */
	public FeatureSet () {
		features = new ArrayList<Feature>();
	}
	
	/**
	 * Constructor with feature
	 * @param f feature
	 */
	public FeatureSet (Feature f) {
		this();
		features.add(f);
	}
	
	/**
	 * Constructor with key-value pair
	 * @param k key
	 * @param v value
	 */
	public FeatureSet (String k, String v) {
		this(new Feature(k,v));
	}
	
	/**
	 * Adds a feature to this feature set
	 * @param f feature
	 * @return true on success
	 */
	public boolean add (Feature f) {
		return features.add(f);
	}

	/**
	 * Returns an iterator
	 */
	@Override
	public Iterator<Feature> iterator() {
		return features.iterator();
	}
	
	/**
	 * Returns a string representation
	 * @return string
	 */
	public String toString () {
		StringBuilder sb = new StringBuilder("{");
		for (Feature f : features) {
			sb.append(f.toString());
			sb.append(";");
		}
		sb.deleteCharAt(sb.length()-1);
		sb.append("}");
		return sb.toString();
	}
	
	/**
	 * Checks whether the specified feature set <em>fs</em>
	 * is a subset of this feature set
	 * @param fs specified feature set
	 * @return true if fs is a subset of this feature set
	 */
	public boolean satisfies (FeatureSet fs) {
		for (Feature f : fs) {
			if (!features.contains(f)) {
				return false;
			}
		}
		return true;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((features == null) ? 0 : features.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		FeatureSet other = (FeatureSet) obj;
		if (features == null) {
			if (other.features != null)
				return false;
		} else if (!features.equals(other.features))
			return false;
		return true;
	}

	public boolean contains(Feature f) {
		for (Feature fe : features) {
			if (fe.getKey().equals(f.getKey()) &&
					fe.getValue().equals(f.getValue()))
				return true;
		}
		return false;
	}

	public String getFeature(String name) {
		for (Feature f : features) {
			if (f.getKey().equals(name)) {
				return f.getValue();
			}
		}
		return "";
	}
	
	public boolean setFeature(String featureName, String featureValue) {
		for(Feature f : features) {
			if (f.getKey().equals(featureName)) {
				return f.setValue(featureValue);
			}
		}
		return false;
	}
} public class Morpheme {

	/**
	 * Morphological features
	 */
	private FeatureSet featureSet;
	/**
	 * Concrete morph realizations
	 */
	private List<Morph> allomorphs;
	
	/**
	 * Constructor
	 */
	public Morpheme() {
		featureSet = new FeatureSet();
		allomorphs = new ArrayList<Morph>();
	}
	
	/**
	 * Constructor
	 * @param fs feature set
	 */
	public Morpheme (FeatureSet fs) {
		this();
		this.featureSet = fs;
	}
	
	/**
	 * Constructor
	 * @param fs feature set
	 * @param am allomorphs
	 */
	public Morpheme (FeatureSet fs, List<Morph> am) {
		this(fs);
		this.allomorphs = am;
	}
	
	/**
	 * Constructor
	 * @param fs feature set
	 * @param m morph
	 */
	public Morpheme (FeatureSet fs, Morph m) {
		this(fs);
		allomorphs.add(m);
	}
	
	public Morpheme (String m) {
		this();
		allomorphs.add(new Morph(m));
	}
	
	/**
	 * Returns the feature set
	 * @return feature set
	 */
	public FeatureSet getFeatureSet () {
		return featureSet;
	}
	
	/**
	 * Returns the allomorphs
	 * <p>
	 * Allomorphs are equivalent <b>concrete</b> realizations
	 * of this <em>abstract</em> morpheme
	 * @return allomorphs
	 */
	public List<Morph> getAllomorphs () {
		return allomorphs;
	}
	
	/**
	 * Adds a morph to this morpheme
	 * @param m morph
	 * @return true on success
	 */
	public boolean add (Morph m) {
		return allomorphs.add(m);
	}
	
	/**
	 * Adds a feature set to this morpheme
	 * @param fs feature set
	 */
	public void addFS (FeatureSet fs) {
		featureSet = fs;
	}
	
	/**
	 * Default toString method
	 */
	public String toString () {
		StringBuilder sb = new StringBuilder();
		sb.append(featureSet.toString());
		sb.append("[");
		for (Morph m : allomorphs) {
			sb.append(m.toString()).append(" ");
		}
		sb.deleteCharAt(sb.length()-1);
		sb.append("]");
		return sb.toString();
	}

	/**
	 * Adds a list of morphs to this morpheme
	 * @param allomorphs list of morphs
	 */
	public void add(List<Morph> allomorphs) {
		this.allomorphs.addAll(allomorphs);
	}

	/**
	 * Checks whether this morpheme has a feature <em>subtype</em>
	 * with the given subtype
	 * @param type subtype
	 * @return true if feature set contains feature 
	 */
	public boolean hasSubtype (String type) {
		return featureSet.satisfies(new FeatureSet("subtype", type));
	}
	
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((featureSet == null) ? 0 : featureSet.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Morpheme other = (Morpheme) obj;
		if (featureSet == null) {
			if (other.featureSet != null)
				return false;
		} else if (!featureSet.equals(other.featureSet))
			return false;
		return true;
	}

	/**
	 * Checks whether the given word can be the result
	 * of appending this morpheme to a stem
	 * @param word word to check
	 * @return true if word ends with morpheme
	 */
	public boolean isApplicable(String word) {
		for (Morph m : allomorphs) {
			if (word.endsWith(m.getMorph()))
				return true;
		}
		return false;
	}
	
	/**
	 * Returns a feature value from this feature set by name
	 * @param name name
	 * @return feature value
	 */
	public String getFeatureByName (String name) {
		return featureSet.getFeature(name);
	}

	/**
	 * Returns the morph that the given word ends with
	 * @param word word
	 * @return morph
	 */
	public String match(String word) {
		for (Morph m : allomorphs) {
			if (word.endsWith(m.getMorph())) 
				return m.getMorph();
		}
		return "";
	}

	/**
	 * Checks whether an allomorph from this morph set
	 * matches the given word exactly
	 * @param word word to check
	 * @return true if match
	 */
	public boolean exactly(String word) {
		for (Morph m : allomorphs) {
			if (m.getMorph().equals(word))
				return true;
		}
		return false;
	}
} public class Paradigm {

	/**
	 * Feature map
	 */
	private List<Morpheme> morphemes;

	/**
	 * No-argument constructor
	 */
	public Paradigm () {
		morphemes = new ArrayList<Morpheme>();
	}

	/**
	 * Standard toString method
	 * @return string representation
	 */
	public String toString() {
		StringBuilder sb = new StringBuilder("Paradigm");
		sb.append(System.lineSeparator());
		for (Morpheme m : morphemes) {
			sb.append(m.toString()).append(System.lineSeparator());
		}
		return sb.toString();
	}

	/**
	 * Returns the endings in this paradigm
	 * @return endings
	 */
	public List<Morph> getEndings () {
		List<Morph> out = new ArrayList<Morph>();
		for (Morpheme m : morphemes) {
			out.addAll(m.getAllomorphs());
		}
		return out;
	}

	/**
	 * Adds morphological information to the paradigm
	 * <p>
	 * <em>morpheme</em> represents any type of morphological
	 * feature (i.e. nominal ending) and <em>features</em>
	 * represents the associated morphological features
	 * @param m
	 */
	public void add (Morpheme m) {
		if (morphemes.contains(m)) {
	
			morphemes.get(morphemes.indexOf(m)).add(m.getAllomorphs());
		} else {
			morphemes.add(m);
		}
	}

	/**
	 * Returns the complete paradigm
	 * <p>
	 * The complete paradigm consists of the morphemes and
	 * their related morphological information as a list of 
	 * mappings
	 * @return paradigm
	 */
	public List<Morpheme> getMorphemes () {
		return morphemes;
	}

	/**
	 * Returns whether the specified word could belong
	 * to this paradigm, or in other words, whether this
	 * paradigm is applicable to the specified word 
	 * @param word word
	 * @return whether paradigm is applicable
	 */
	public boolean isApplicable (String word) {
		for (Morph end : getEndings()) {
			if (word.endsWith(end.getMorph()))
				return true;
		}
		return false;
	}

	/**
	 * Returns the paradigm that matches the specified 
	 * feature set
	 * <p>
	 * The feature set can cover all features, but 
	 * generally only specifies a few features (i.e. word 
	 * class)
	 * <br/><br/>
	 * <b>If the resulting Paradigm is empty, null is returned!</b>
	 * @param feat feature set
	 * @return paradigm matching feature set
	 */
	public Paradigm getParadigmByFeatures (FeatureSet feat) {
		Paradigm out = new Paradigm();
		for (Morpheme m : morphemes) {
			if (m.getFeatureSet().satisfies(feat)) {
				Morpheme newMorpheme = new Morpheme();
				newMorpheme.add(m.getAllomorphs());
				newMorpheme.addFS(m.getFeatureSet());
				out.add(newMorpheme);
			}
		}
		return (out.isEmpty())?null:out;
	}

	public boolean isEmpty () {
		return morphemes.size() == 0;
	}

	/**
	 * Checks whether this Paradigm has the given subtype
	 * <p>
	 * This Paradigm is said to have the given subtype,
	 * <em>if and only if</em> all of the contained morphemes
	 * have the given subtype
	 * @param type subtype to check
	 * @return true if all morphemes in paradigm have given subtype
	 */
	public boolean hasSubtype (String type) {
		for (Morpheme m : morphemes)
			if (!m.hasSubtype(type))
				return false;
		return true;
	}
	
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((morphemes == null) ? 0 : morphemes.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Paradigm other = (Paradigm) obj;
		if (morphemes == null) {
			if (other.morphemes != null)
				return false;
		} else if (!morphemes.equals(other.morphemes))
			return false;
		return true;
	}

	/**
	 * Calculates the difference between this paradigm
	 * and a given paradigm
	 * <p>
	 * Returns a paradigm that consists of this paradigm
	 * with all elements from the given paradigm removed 
	 * and the given paradigm
	 * @param p2 given paradigm
	 * @return difference
	 */
	public Paradigm difference (Paradigm p2) {
		
		Paradigm copy = new Paradigm();
		for (Morpheme m : this.morphemes) {
			if (p2.getMorphemes().contains(m))
				continue;
			Morpheme morpheme = new Morpheme();
			for (Morph mo : m.getAllomorphs()) {
				Morph morph = new Morph(mo.getMorph(), mo.getOccurrence());
				morpheme.add(morph);
			}
			morpheme.addFS(m.getFeatureSet());
			copy.add(morpheme);
		}
		//copy.morphemes.removeAll(p2.morphemes);
		return copy;
	}

	/**
	 * Returns the feature set associated with the given word
	 * @param word word
	 * @return feature set
	 */
	public FeatureSet getFeatureSet(String word) {
		for (Morpheme m : morphemes) {
			if (m.exactly(word))
				return m.getFeatureSet();
		}
		System.err.println("Could not retrieve feature set for " + word);
		return null;
	}
	
} public class ParadigmAccessor {

	/**
	 * Paradigm Reader
	 */
	private ParadigmReader pr;
	
	/**
	 * Constructor
	 */
	public ParadigmAccessor () throws Exception
	{
		pr = new ParadigmReader();
	}
	
	/**
	 * Returns a list of Paradigms that satisfy the constraints
	 * specified by the feature map
	 * @param features feature map
	 * @return paradigms
	 */
	public Paradigm getParadigmsByFeatures (FeatureSet features) {
		return pr.getParadigmsByFeatures(features);
	}
	
	/**
	 * Returns the generated paradigm
	 * @return paradigms
	 */
	public Paradigm getParadigms () {
		return pr.getParadigm();
	}
	
	/**
	 * Returns the full noun paradigm
	 * @return noun paradigm
	 */
	public Paradigm getNounParadigm () {
		
		return pr.getNounParadigm();
	}
	
	/**
	 * Returns the full pronoun paradigm
	 * @return pronoun paradigm
	 */
	public Paradigm getPronounParadigm () {
		return pr.getPronounParadigm();
	}
	
	/**
	 * Returns the full verb paradigm
	 * @return verb paradigm
	 */
	public Paradigm getVerbParadigm () {
		return pr.getVerbParadigm();
	}
	
	/**
	 * Returns the full suffix paradigm
	 * @return suffix paradigm
	 */
	public Paradigm getSuffixParadigm () {
		return pr.getSuffixParadigm();
	}
	
	/**
	 * Returns the full prefix paradigm
	 * @return prefix paradigm
	 */
	public Paradigm getPrefixParadigm () {
		return pr.getPrefixParadigm();
	}
	
	/**
	 * Returns the full numeral paradigm
	 * @return numeral paradigm
	 */
	public Paradigm getNumeralParadigm () {
		return pr.getNumeralParadigm();
	}
	
	/**
	 * Returns the irregular noun paradigms
	 * @return irregular noun paradigms
	 */
	public IrregularNouns getIrregularNouns () {
		return pr.getIrregularNouns();
	}
	
	/**
	 * Returns the irregular numeral paradigms
	 * @return irregular numeral paradigms
	 */
	public IrregularNumerals getIrregularNumerals () {
		return pr.getIrregularNumerals();
	}

	/**
	 * Returns the full adjective paradigm
	 * @return adjective paradigm
	 */
	public Paradigm getAdjectiveParadigm() {
		return pr.getAdjectiveParadigm();
	}
} public class IrregularNumerals implements Irregular {
	/**
	 * List of paradigms
	 */
	private List<Paradigm> paradigms;

	/**
	 * Constructor
	 */
	public IrregularNumerals () {
		paradigms = new ArrayList<Paradigm>();
	}

	public void add (Paradigm p) {
		paradigms.add(p);
	}

	public boolean isIrregular (String word) {
		for (Paradigm p : paradigms) {
			for (Morpheme m : p.getMorphemes()) {
				for (Morph n : m.getAllomorphs()) {
					if (n.getMorph().equals(word))
						return true;
				}
			}
		}
		return false;
	}

	public Paradigm getForms (String word) {
		for (Paradigm p : paradigms) {
			for (Morpheme m : p.getMorphemes()) {
				for (Morph n : m.getAllomorphs()) {
					if (n.getMorph().equals(word))
						return p;
				}
			}
		}
		return null;
	}

	public Paradigm getLemma (String word) {
		// case caturo etc
		// return catur
		FeatureSet fs1 = new FeatureSet();
		fs1.add(new Feature("number", "singular"));
		fs1.add(new Feature("case", "vocative"));
		fs1.add(new Feature("gender", "masculine"));
		FeatureSet fs2 = new FeatureSet();
		fs2.add(new Feature("number", "plural"));
		fs2.add(new Feature("case", "vocative"));
		fs2.add(new Feature("gender", "masculine"));
		FeatureSet fs3 = new FeatureSet();
		fs3.add(new Feature("number", "singular"));
		fs3.add(new Feature("case", "nominative"));
		fs3.add(new Feature("gender", "masculine"));
		FeatureSet fs4 = new FeatureSet();
		fs4.add(new Feature("number", "plural"));
		fs4.add(new Feature("case", "nominative"));
		fs4.add(new Feature("gender", "masculine"));
		FeatureSet fs5 = new FeatureSet();
		fs1.add(new Feature("number", "singular"));
		fs1.add(new Feature("case", "vocative"));
		
		FeatureSet fs6 = new FeatureSet();
		fs2.add(new Feature("number", "plural"));
		fs2.add(new Feature("case", "vocative"));
		
		
		for (Paradigm p : paradigms) {
			for (Morpheme m : p.getMorphemes()) {
				for (Morph n : m.getAllomorphs()) {
					if (n.getMorph().equals(word)) {
						if (p.getParadigmByFeatures(fs1) != null && !p.getParadigmByFeatures(fs1).isEmpty()) {
							return p.getParadigmByFeatures(fs1);
						} else if (p.getParadigmByFeatures(fs2) != null && !p.getParadigmByFeatures(fs2).isEmpty()) {
							return p.getParadigmByFeatures(fs2);
						} else if (p.getParadigmByFeatures(fs3) != null && !p.getParadigmByFeatures(fs3).isEmpty()) {
							return p.getParadigmByFeatures(fs3);
						} else if (p.getParadigmByFeatures(fs4) != null && !p.getParadigmByFeatures(fs4).isEmpty()) {
							return p.getParadigmByFeatures(fs4);
						} else if (p.getParadigmByFeatures(fs5) != null && !p.getParadigmByFeatures(fs5).isEmpty()) {
							return p.getParadigmByFeatures(fs5);
						} else if (p.getParadigmByFeatures(fs6) != null && !p.getParadigmByFeatures(fs6).isEmpty()) {
							return p.getParadigmByFeatures(fs6);
						} else {
							return null;
						}
					}
				}
			}
		}
		return null;
	}

	public String toString () {
		StringBuilder sb = new StringBuilder();
		for (Paradigm p : paradigms) {
			sb.append(p);
		}
		return sb.toString();
	}
} public class WordClassGuesser {

	////////////////////////////////////////////////////////////////
	// Constants
	////////////////////////////////////////////////////////////////

	////////////////////////////////////////////////////////////////
	// Variables
	////////////////////////////////////////////////////////////////

	/**
	 * Prune limit
	 */
	private int prune;
	private ParadigmAccessor pa;

	////////////////////////////////////////////////////////////////
	// Constructors
	////////////////////////////////////////////////////////////////

	public WordClassGuesser(ParadigmAccessor pa) {
		this.pa = pa;
		prune = 16;
	}
	
	public WordClassGuesser (ParadigmAccessor pa, int p) {
		this.pa = pa;
		prune = p;
	}

	////////////////////////////////////////////////////////////////
	// Methods
	////////////////////////////////////////////////////////////////

	/**
	 * Guesses the word class of a given word
	 * <p>
	 * The returned result depends on the <em>prune</em> parameter. A
	 * low prune will more likely return more word class results, whereas
	 * a higher prune will tend to cut off the result list early and return
	 * only the top result
	 * @param word word 
	 * @return word class
	 */
	public List<String> guessWordClassFromWordForm(String word)
	{
		// TODO: read irregular from dictionary
		//IrregularNouns inoun = pa.getIrregularNouns();
		//IrregularNumerals inum = pa.getIrregularNumerals();
		//List<String> indecl = pa.getIndeclinables();
		
		Map<String, Integer> map = new HashMap<String, Integer>();

		// if word is a lemma
		List<String> out = new ArrayList<String>(); 
				//guessWordClassFromLemma(word);
		/*
		for (String s : indecl) {
			if (s.equals(word)) {
				out.add("indeclinable");
			}
		}
		if (inoun.isIrregular(word)) {
			return Collections.singletonList("noun");
		} else if (inum.isIrregular(word)) {
			return Collections.singletonList("numeral");
		}
		*/
		// add weight to potential verb lemma
		if (word.endsWith("ti"))
			map.put("verb", 10);
		for (Morpheme m : pa.getParadigms().getMorphemes()) {
			if (m.getFeatureByName("paradigm").equals("affix"))
				continue;
			if (m.isApplicable(word)) {
				String wordclass = m.getFeatureByName("paradigm");
				// weigh the result so that longer matches are given more importance than short matches
				map.put(wordclass, inc(wordclass, map)+(m.match(word).length()));
				
			}
		}
		List<Entry<String, Integer>> list = new LinkedList<Entry<String, Integer>>(map.entrySet());
		Collections.sort(list, new Comparator<Entry<String, Integer>> () {
			@Override
			public int compare(Entry<String, Integer> o1,
					Entry<String, Integer> o2) {
				if (o1.getValue() > o2.getValue())
					return -1;
				if (o1.getValue() < o2.getValue())
					return 1;
				return 0;
			}
		});
		System.out.println(list);
		if (list.size() == 0) {
			out.add("adverb");
			out.add("indeclinable");
			return out;
		}
		if (list.size() == 1)
			return Collections.singletonList(list.get(0).getKey());
		if (list.get(0).getValue() - list.get(1).getValue() > prune) {
			return Collections.singletonList(list.get(0).getKey());
		}
		for (int i = 0; i < list.size()-1; i += 1) {
			if ((list.get(i).getValue()-list.get(i+1).getValue()) <= prune) {
				String key = list.get(i).getKey();
				if (!out.contains(key))
					out.add(list.get(i).getKey());
				out.add(list.get(i+1).getKey());
			} else {
				return out;
			}
		}
		return out;
	}

	/**
	 * Guesses the word class of a given lemma
	 * @param lemma lemma
	 * @return word classes
	 */
	public List<String> guessWordClassFromLemma(String lemma) {
		List<String> guesses = new ArrayList<String>();
		
		IrregularNouns inoun = pa.getIrregularNouns();
		IrregularNumerals inum = pa.getIrregularNumerals();
		
		if (inoun.isIrregular(lemma)) {
			return Collections.singletonList("noun");
		} else if (inum.isIrregular(lemma)) {
			return Collections.singletonList("numeral");
		}
		for (Morpheme m : pa.getPronounParadigm().getMorphemes()) {
			if (m.exactly(lemma)) {
				return Collections.singletonList("pronoun");
			}
		}
		
		if (lemma.endsWith("ti")) {
			guesses.add("verb");
		}
		if (ends(lemma, "a", "ā", "i", "in", "ī", "u", "ū", "ar", "an", "ant", "as", "us", "o")) {
			guesses.add("noun");
		}
		if (ends(lemma, "a", "ā", "i", "ī", "u", "ū", "ant", "vā", "mā", "at")) {
			guesses.add("adjective");
		}
		if (ends(lemma, "a", "i", "aṃ", "ma", "ya")) {
			guesses.add("numeral");
		}
		if (ends(lemma, "uṃ")) {
			guesses.add("indeclinable");
		}
		return guesses;
	}
	
	/**
	 * Checks whether the given lemma ends with one of 
	 * the given endings
	 * @param lemma lemma
	 * @param ends endings
	 * @return true if lemma ends with ending
	 */
	private boolean ends (String lemma, String... ends) {
		List<String> lest = Arrays.asList(ends);
		Collections.sort(lest, new Comparator<String>() {
			@Override
			public int compare(String arg0, String arg1) {
				if (arg0.length() > arg1.length())
					return -1;
				if (arg0.length() < arg1.length())
					return 1;
				return 0;
			}
		});
		for (String e : lest) {
			if (lemma.endsWith(e))
				return true;
		}
		return false;
	}
	/**
	 * Increase the frequency of the given string
	 * @param pos part of speech (word class)
	 * @param map map
	 * @return frequency of word class incremented by one
	 */
	private int inc (String pos, Map<String, Integer> map) {
		if (map.containsKey(pos))
			return map.get(pos) + 1;
		return 1;
	}
	
	public void setPruningParameter(int val) {
		this.prune = val;
	}
	
	public int getPruningParameter() {
		return prune;
	}
} public class VerbHelper {

	public static boolean debug = false;

	/**
	 * Returns possible stems given a root and a declension
	 * <p>
	 * It is possible to specify the declension
	 * of the root as a number. The following table
	 * illustrates possible declension numbers:
	 * <table border="1">
	 * <tr><th>Declension</th><th>Declension number</th></tr>
	 * <tr><td>First</td><td>1</td></tr>
	 * <tr><td>Second</td><td>2</td></tr>
	 * <tr><td>Third</td><td>3</td></tr>
	 * <tr><td>Fourth</td><td>4</td></tr>
	 * <tr><td>Fifth</td><td>5</td></tr>
	 * <tr><td>Sixth</td><td>6</td></tr>
	 * <tr><td>Seventh</td><td>7</td></tr>
	 * </table>
	 * The specified declension is used to derive the stems. 
	 * If an invalid declension number is specified,
	 * all possible stems are returned.<br/><br/>
	 * If the declension is <b>unknown</b> or all possible
	 * stems are to be generated, use {@link #stemFromRoot(String)}
	 * @param root root
	 * @param declension declension
	 * @return possible stems
	 * @see #stemFromRoot(String)
	 */
	public List<String> stemFromRoot (String root, int declension) {
		FromRootToStem frts = new FromRootToStem();
		switch (declension) {
		case 1: return frts.first(root);
		case 2: return frts.second(root);
		case 3: return frts.third(root);
		case 4: return frts.fourth(root);
		case 5: return frts.fifth(root);
		case 6: return frts.sixth(root);
		case 7: return frts.seventh(root);
		default: return frts.stemFromRoot(root);
		}
	}

	/**
	 * Returns possible stems given a root
	 * <p>
	 * Use this method, if the declension type is
	 * <b>unknown</b>. This method generates stems
	 * using all declension types.<br/>
	 * If the declension type <b>known</b>, use
	 * {@link #stemFromRoot(String, int)} instead
	 * @param root root
	 * @return possible stems
	 * @see #stemFromRoot(String, int)
	 */
	public List<String> stemFromRoot (String root) {
		return stemFromRoot(root, 0);
	}

	/**
	 * Returns possible roots given a stem
	 * <p>
	 * It is possible to specify the declension
	 * of the stem as a number. The following table
	 * illustrates possible declension numbers:
	 * <table border="1">
	 * <tr><th>Declension</th><th>Declension number</th></tr>
	 * <tr><td>First</td><td>1</td></tr>
	 * <tr><td>Second</td><td>2</td></tr>
	 * <tr><td>Third</td><td>3</td></tr>
	 * <tr><td>Fourth</td><td>4</td></tr>
	 * <tr><td>Fifth</td><td>5</td></tr>
	 * <tr><td>Sixth</td><td>6</td></tr>
	 * <tr><td>Seventh</td><td>7</td></tr>
	 * </table>
	 * The specified declension is used to derive the roots. 
	 * If an invalid declension number is specified,
	 * all possible roots are returned.<br/><br/>
	 * If the declension is <b>unknown</b> or all possible
	 * roots are to be generated, use {@link #rootFromStem(String)}
	 * @param stem stem
	 * @param declension declension
	 * @return possible roots
	 * @see #rootFromStem(String)
	 */
	public List<String> rootFromStem (String stem, int declension) {
		FromStemToRoot fstr = new FromStemToRoot();
		List<String> temp = new ArrayList<String>();
		switch (declension) {
		case 1: temp.addAll(fstr.first(stem)); break;
		case 2: temp.addAll(fstr.second(stem)); break;
		case 3: temp.addAll(fstr.third(stem)); break;
		case 4: temp.addAll(fstr.fourth(stem)); break;
		case 5: temp.addAll(fstr.fifth(stem)); break;
		case 6: temp.addAll(fstr.sixth(stem)); break;
		case 7: temp.addAll(fstr.seventh(stem)); break;
		default: temp.addAll(fstr.rootFromStem(stem)); break;
		}
		// filter out improbable roots
		List<String> out = new ArrayList<String>();
		for (String s : temp) {
			if (this.isPlausibleRoot(s))
				if (!out.contains(s))
					out.add(s);
				else 
					System.out.println("Discarded calculated root: " + s);
		}
		return out;
	}

	/**
	 * Returns possible roots given a stem
	 * <p>
	 * Use this method, if the declension type is
	 * <b>unknown</b>. This method generates roots
	 * using all declension types that can be applied.<br/>
	 * If the declension type <b>known</b>, use
	 * {@link #rootFromStem(String, int)} instead
	 * @param stem stem
	 * @return possible roots
	 * @see #rootFromStem(String, int)
	 */
	public List<String> rootFromStem (String stem) {
		return rootFromStem(stem, 0);
	}

	/**
	 * Checks whether the given root is a plausible root
	 * <p>
	 * Plausible roots consist of maximally three units
	 * and contain a vowel
	 * @param root root
	 * @return true if root is plausible
	 */
	public boolean isPlausibleRoot (String root) {
		return (Segmenter.segmentToArray(root).length < 4) && 
				(root.matches(".*"+Patterner.patternGroup(Alphabet.getVowels())+".*"));
	}

	/**
	 * Internal class responsible for stem calculation
	 * @author David
	 *
	 */
	private class FromRootToStem {
		/**
		 * Returns possible stems given a root 
		 * @param root root
		 * @return possible stems
		 */
		private List<String> stemFromRoot (String root) {
			List<String> output = new ArrayList<String>();
			List<String> output2 = new ArrayList<String>();
			output.addAll(first(root));
			output.addAll(second(root));
			output.addAll(third(root));
			output.addAll(fourth(root));
			output.addAll(fifth(root));
			output.addAll(sixth(root));
			output.addAll(seventh(root));
			for (String s : output) {
				if (!output2.contains(s))
					output2.add(s);
			}
			return output2;
		}

		/**
		 * Strengthens the vowel of a root
		 * @param root root
		 * @return root with strengthened vowel
		 */
		private String strengthenRoot (String root) {
			StringBuilder sb = new StringBuilder();
			String[] parts = Segmenter.segmentToArray(root);
			for (String s : parts) {
				if (Alphabet.isVowel(s))
					sb.append(Alphabet.getStrong(s));
				else 
					sb.append(s);
			}
			return sb.toString();
		}

		/**
		 * Reduplicates a root 
		 * <p>
		 * Reduplicating roots can be found in the first
		 * conjugation
		 * @param root root
		 * @return reduplicated root
		 */
		private List<String> reduplicate (String root) {
			List<String> output = new ArrayList<String>();
			String[] parts = Segmenter.segmentToArray(root);
			if (parts.length < 2) {
				return Collections.singletonList(root);
			}
			String one = null;
			String two = null;

			if (Alphabet.isVowel(parts[0])) {
				one = Alphabet.getStrong(parts[0]);
				two = "";
			} else if (Alphabet.isGuttural(parts[0])) {
				one = Alphabet.getPalatalForGuttural(parts[0]);
				if (Alphabet.isAspirated(one)) {
					one = one.substring(0, 1);
				}
			} else if (parts[0].equals("h")) {
				one = "j";
			} else if (parts[0].equals("v")) {
				one = "u";
				two = "";
			} else if (Alphabet.isAspirated(parts[0])) {
				one = parts[0].substring(0, 1);
			} else {
				one = parts[0];
			}
			if (two == null) {
				if (parts[1].equals("a") || parts[1].equals("ā")) {
					output.add(one + "a" + root);
					if (parts[1].equals("a"))
						output.add(one + "a" + strengthenRoot(root));
				} else if (parts[1].equals("i") || parts[1].equals("ī")) {
					output.add(one + "i" + root);
					output.add(one + "i" + strengthenRoot(root));
					if (parts[1].equals("i")) {
						output.add(one + "e" + root);
						output.add(one + "e" + strengthenRoot(root));
					}
				} else if (parts[1].equals("u") || parts[1].equals("ū")) {
					output.add(one + "u" + root);
					output.add(one + "a" + root);
					output.add(one + "u" + strengthenRoot(root));
					output.add(one + "a" + strengthenRoot(root));
					if (parts[1].equals("u")) {
						output.add(one + "o" + root);
						output.add(one + "o" + strengthenRoot(root));
					}
				}
			} else {
				output.add(one + two + root);
				output.add(one + two + strengthenRoot(root));
			}
			return output;
		}

		/**
		 * Applies the first conjugation to derive stems
		 * @param root root
		 * @return stems
		 */
		private List<String> first (String root) {
			List<String> output = new ArrayList<String>();
			if (root.matches("^.*"+Patterner.patternGroup(Alphabet.getVowels()) + "$")) {
				output.add(root);
				if (root.endsWith("i") || root.endsWith("ī")) {
					output.add(root.substring(0, root.length()-1) + "e");
					output.add(root.substring(0, root.length()-1) + "aya");
				}
				if (root.endsWith("u") || root.endsWith("ū")) {
					output.add(root.substring(0, root.length()-1) + "o");
					output.add(root.substring(0, root.length()-1) + "ava");
				}
			} else {
				output.add(root + "a");
				output.add(strengthenRoot(root) + "a");
			}
			for (String s : reduplicate(root)) {
				if (s.endsWith("a") || s.endsWith("ā"))
					output.add(s);
				else
					output.add(s + "a");
			}
			return output;
		}

		/**
		 * Applies the second conjugation to derive stems
		 * @param root root
		 * @return stems
		 */
		private List<String> second (String root) {
			List<String> output = new ArrayList<String>();
			String[] parts = Segmenter.segmentToArray(root);
			String lc = parts[parts.length-1];
			String nigg = Alphabet.getAssimilatedNiggahita(lc);
			StringBuilder sb = new StringBuilder();
			for (int i = 0; i < parts.length-1; i++) {
				sb.append(parts[i]);
			}
			output.add(sb.append(nigg + lc + "a").toString());
			return output;
		}

		/**
		 * Applies the third conjugation to derive stems
		 * @param root root
		 * @return stems
		 */
		private List<String> third (String root) {
			List<String> output = new ArrayList<String>();
			String[] parts = Segmenter.segmentToArray(root);
			String pre = "";
			String post = "";
			if (parts.length == 3) {
				pre = parts[0] + parts[1];
				post = parts[2];
			} else if (parts.length == 2) {
				pre = parts[0];
				post = parts[1];
			} else {
				if (debug)
					System.err.println("Unexpected input for third declension: " + root);
			}
			output.add(pre + Alphabet.getAssimilatedWithYa(post));
			return output;
		}

		/**
		 * Applies the fourth conjugation to derive stems
		 * @param root root
		 * @return stems
		 */
		private List<String> fourth (String root) {
			List<String> output = new ArrayList<String>();
			if (root.matches("^.*"+Patterner.patternOr(Alphabet.getConsonants())+"$")) {
				output.add(root + "uṇu");
				output.add(root + "uṇo");
				output.add(root + "uṇā");
			} else {
				output.add(root + "ṇu");
				output.add(root + "ṇo");
				output.add(root + "ṇā");
			}
			return output;
		}

		/**
		 * Applies the fifth conjugation to derive stems
		 * @param root root
		 * @return stems
		 */
		private List<String> fifth (String root) {
			List<String> output = new ArrayList<String>();
			if (root.matches("^.*"+Patterner.patternGroup(Alphabet.getVowels()) + "$")) {
				output.add(root + "nā");
			}
			return output;
		}

		/**
		 * Applies the sixth conjugation to derive stems
		 * @param root root
		 * @return stems
		 */
		private List<String> sixth (String root) {
			List<String> output = new ArrayList<String>();
			output.add(root + "o");
			output.add(root + "av");
			return output;
		}

		/**
		 * Applies the seventh conjugation to derive stems
		 * @param root root
		 * @return stems
		 */
		private List<String> seventh (String root) {
			List<String> output = new ArrayList<String>();
			output.add(root + "aya");
			output.add(root + "e");
			output.add(strengthenRoot(root) + "aya");
			output.add(strengthenRoot(root) + "e");
			return output;
		}
	}

	/**
	 * Internal class responsible for root calculation
	 * @author David
	 *
	 */
	private class FromStemToRoot {
		/**
		 * Returns possible roots given a stem
		 * @param stem stem
		 * @return roots
		 */
		private List<String> rootFromStem (String stem) {
			List<String> output = new ArrayList<String>();
			output.addAll(first(stem));
			output.addAll(second(stem));
			output.addAll(third(stem));
			output.addAll(fourth(stem));
			output.addAll(fifth(stem));
			output.addAll(sixth(stem));
			output.addAll(seventh(stem));
			return output;
		}

		/**
		 * Weakens the vowel of a root
		 * @param root root
		 * @return root with weakened vowel
		 */
		private List<String> weakenRoot (String root) {
			List<String> out = new ArrayList<String>();
			List<String> vowels = new ArrayList<String>();
			String[] headtail = root.split(Patterner.patternGroup(Alphabet.getVowels()));
			try {
				String v = headtail[0];
				if (v.isEmpty()) {
					return Collections.singletonList(root);
				}
			} catch(ArrayIndexOutOfBoundsException e) {
				return Collections.singletonList(root);
			}
			String[] parts = Segmenter.segmentToArray(root);

			for (String s : parts) {
				if (Alphabet.isVowel(s))
					vowels.addAll(Alphabet.getWeak(s));
			}
			for (String s : vowels) {
				if (headtail.length > 1)
					out.add(headtail[0] + s + headtail[1]);
				else
					out.add(headtail[0] + s);
			}
			return out;
		}

		/**
		 * Undoes the reduplication from the first conjugation
		 * @param stem stem
		 * @return unduplicated stem
		 */
		private String unduplicate (String stem) {
			String[] split = Segmenter.segmentToArray(stem);
			if (split.length < 2) {
				return stem;
			}
			if (Alphabet.isConsonant(split[1])) {
				return stem.substring(1);
			}
			int index = 0;
			for (int i = 1; i < split.length; i++) {
				if (Alphabet.isConsonant(split[i])) {
					index = i;
					break;
				}
			}
			return stem.substring(index);
		}

		/**
		 * Applies the first conjugation to derive roots
		 * @param stem stem
		 * @return roots
		 */
		private List<String> first (String stem) {
			List<String> output = new ArrayList<String>();
			// use stem as root if it is a plausible root
			if (isPlausibleRoot(stem)) {
				output.add(stem);	
			}
			// undo reduplication
			String undup = unduplicate(stem);
			if (undup == null || undup.isEmpty()) {
				return output;
			}
			String sroot = undup.substring(0, undup.length()-1);
			output.add(sroot);
			output.addAll(weakenRoot(sroot));
			// string replacements 
			if (stem.endsWith("e")) {
				output.add(stem.substring(0, stem.length()-1) + "i");
				output.add(stem.substring(0, stem.length()-1) + "ī");
			} else if (stem.endsWith("o")) {
				output.add(stem.substring(0, stem.length()-1) + "u");
				output.add(stem.substring(0, stem.length()-1) + "ū");
			} else if (stem.endsWith("aya")) {
				output.add(stem.substring(0, stem.length()-3) + "i");
				output.add(stem.substring(0, stem.length()-3) + "ī");
			} else if (stem.endsWith("ava")) {
				output.add(stem.substring(0, stem.length()-3) + "u");
				output.add(stem.substring(0, stem.length()-3) + "ū");
			} else if (stem.endsWith("a")) {
				String root = stem.substring(0, stem.length()-1);
				output.add(root);
				output.addAll(weakenRoot(root));
			} else {
				if (debug)
					System.err.println("Could not apply first declension to derive root from " + stem);
			}
			return output;
		}

		/**
		 * Applies the second conjugation to derive roots
		 * @param stem stem
		 * @return roots
		 */
		private List<String> second (String stem) {
			String pattern = Patterner.patternGroup(Alphabet.getNasals())+"("+Patterner.patternOr(Alphabet.getConsonants())+")a";
			List<String> output = new ArrayList<String>();
			if (stem.matches(".*"+pattern)) { 
				output.add(stem.replaceAll(pattern, "$2"));
			} else {
				if (debug)
					System.err.println("Could not apply second declension to derive root from " + stem);
			}
			return output;
		}

		/**
		 * Applies the third conjugation to derive roots
		 * @param stem stem
		 * @return roots
		 */
		private List<String> third (String stem) {
			SandhiSplit ss = new SandhiSplit();
			List<String> output = new ArrayList<String>();
			if (Segmenter.segmentToArray(stem).length < 4) {
				if (debug)
					System.err.println("Could not apply third declension to derive root from " + stem);
				return output;
			}
//			List<SplitResult> list = ss.split(stem, 1);
//			if (list.isEmpty()) {
//				if (debug)
//					System.err.println("Could not apply third declension to derive root from " + stem);
//			}
//			for (SplitResult sr : list) {
//				if (sr.getSplit().size() < 2)
//					continue;
//				if (sr.getSplit().get(1).equals("ya")) {
//					output.add(sr.getSplit().get(0));
//				}
//			}
			return output;
		}

		/**
		 * Applies the fourth conjugation to derive roots
		 * @param stem stem
		 * @return roots
		 */
		private List<String> fourth (String stem) {
			List<String> output = new ArrayList<String>();
			if (stem.matches("^(.{2,})uṇ[uoā]$")) {
				Pattern p = Pattern.compile("^(.{2,})uṇ[uoā]$");
				Matcher m = p.matcher(stem);
				m.find();
				output.add(m.group(1));
			} else {
				if (stem.matches("^(.*)ṇ[uoā]$")) {
					Pattern p = Pattern.compile("^(.*)ṇ[uoā]$");
					Matcher m = p.matcher(stem);
					m.find();
					output.add(m.group(1));	
				} else {
					if (debug)
						System.err.println("Could not apply fourth declension to derive root from " + stem);
				}
			}
			return output;
		}

		/**
		 * Applies the fifth conjugation to derive roots
		 * @param stem stem
		 * @return root
		 */
		private List<String> fifth (String stem) {
			List<String> output = new ArrayList<String>();
			if (stem.matches("^.*" + Patterner.patternGroup(Alphabet.getVowels()) + "nā$")) {
				Pattern p = Pattern.compile("^(.*" + Patterner.patternGroup(Alphabet.getVowels()) + ")nā$");
				Matcher m = p.matcher(stem);
				m.find();
				output.add(m.group(1));
			} else {
				if (debug)
					System.err.println("Could not apply fifth declension to derive root from " + stem);
			}
			return output;
		}

		/**
		 * Applies the sixth conjugation to derive roots
		 * @param stem stem
		 * @return roots
		 */
		private List<String> sixth (String stem) {
			List<String> output = new ArrayList<String>();
			if (stem.matches("^.*o$")) {
				Pattern p = Pattern.compile("^(.*)o$");
				Matcher m = p.matcher(stem);
				m.find();
				output.add(m.group(1));
			} else if (stem.matches("^.*av$")) {
				Pattern p = Pattern.compile("^(.*)av$");
				Matcher m = p.matcher(stem);
				m.find();
				output.add(m.group(1));
			} else {
				if (debug)
					System.err.println("Could not apply sixth declension to derive root from " + stem);
			}
			return output;
		}

		/**
		 * Applies the seventh conjugation to derive roots
		 * @param stem stem
		 * @return roots
		 */
		private List<String> seventh (String stem) {
			List<String> output = new ArrayList<String>();
			if (stem.matches("^.*aya$")) {
				Pattern p = Pattern.compile("^(.*)aya$");
				Matcher m = p.matcher(stem);
				m.find();
				String strongRoot = m.group(1);
				output.add(strongRoot);
				output.addAll(weakenRoot(strongRoot));
			} else if (stem.matches("^.*e$")) {
				Pattern p = Pattern.compile("^(.*)e$");
				Matcher m = p.matcher(stem);
				m.find();
				String strongRoot = m.group(1);
				output.add(strongRoot);
				output.addAll(weakenRoot(strongRoot));
			} else {
				if (debug)
					System.err.println("Could not apply seventh declension to derive root from " + stem);
			}
			return output;
		}
	}
} public class NumeralStrategy extends AbstractStrategy {

	////////////////////////////////////////////////////////////////
	// Constants
	////////////////////////////////////////////////////////////////

	/**
	 * Irregular numerals 1-4
	 * and "ubho" (both)
	 */
	private final static String[] a = {"eka", "dve", "ubho", "tayo", "cattāro"};
	/**
	 * Irregular numerals 5-18
	 */
	private final static String[] b = {
		"pañca",
		"cha",
		"satta",
		"aṭṭha",
		"nava",
		"dasa", "rasa", "lasa", "ḷasa", // spelling variation (10)
		"akārasa", "ekādasa", // spelling variation (11)
		"bārasa", "dvārasa", //...
		"tedasa", "terasa", "telasa",
		"catuddasa", "cuddasa", "coddasa",
		"pañcadasa", "paṇṇarasa", "pannarasa",
		"soḷasa", "sorasa",
		"sattadasa", "sattarasa",
		"aṭṭhādasa", "aṭṭhārasa"
		};
	private final static List<String> oneToFour = Arrays.asList(a);
	private final static List<String> fiveTo18 = Arrays.asList(b);
	
	////////////////////////////////////////////////////////////////
	// Variables
	////////////////////////////////////////////////////////////////

	ParadigmAccessor pa;

	////////////////////////////////////////////////////////////////
	// Constructors
	////////////////////////////////////////////////////////////////

	public NumeralStrategy(ParadigmAccessor pa)
	{
		this.pa = pa;
	}

	////////////////////////////////////////////////////////////////
	// Methods
	////////////////////////////////////////////////////////////////

	@Override
	public List<ConstructedWord> apply(ILogInterface log, String lemma, String... options) {
		if (isOneToFour(lemma)) {
			return oneToFourDec(lemma);
		}
		GeneralDeclensionStrategy gds = new GeneralDeclensionStrategy();
		
		Paradigm num = pa.getNumeralParadigm();
		Paradigm fte = num.getParadigmByFeatures(new FeatureSet("restriction", "5to18"));
		Paradigm nu = num.getParadigmByFeatures(new FeatureSet("restriction", "19up"));
		Paradigm fte_a = fte.getParadigmByFeatures(new FeatureSet("declension", "a"));
		Paradigm nu_a = nu.getParadigmByFeatures(new FeatureSet("declension", "a"));
		Paradigm nu_i = nu.getParadigmByFeatures(new FeatureSet("declension", "i"));
		Paradigm nu_am = nu.getParadigmByFeatures(new FeatureSet("declension", "aṃ"));
		List<ConstructedWord> out = new ArrayList<ConstructedWord>();
		if (lemma.endsWith("a")) {
			if (isFiveTo18(lemma)) {
				out= gds.apply(lemma, fte_a, new RightDeletingRule(1));
			} else if (is19up(lemma)) {
				out= gds.apply(lemma, nu_a, new RightDeletingRule(1));
			}
		} else if (lemma.endsWith("i")) {
			out= gds.apply(lemma, nu_i, new RightDeletingRule(1));
		} else if (lemma.endsWith("aṃ")) {
			out= gds.apply(lemma, nu_am, new RightDeletingRule(2));
		}
		// TODO include cardinals??
		for (int i = 0; i < out.size(); i++) {
			ConstructedWord cw = out.get(i);
			cw.setLemma(lemma);
		}
		return out;
	}

	/**
	 * Checks whether the given lemma is a number from
	 * 1 to 4
	 * @param lemma lemma
	 * @return true if lemma is number word from 1 to 4
	 */
	private static boolean isOneToFour (String lemma) {
		return oneToFour.contains(lemma);
	}
	
	/**
	 * Checks whether the given lemma is a number from
	 * 5 to 18
	 * @param lemma lemma
	 * @return true if lemma is number word from 5 to 18
	 */
	private static boolean isFiveTo18 (String lemma) {
		return fiveTo18.contains(lemma);
	}
	
	/**
	 * Checks whether the given lemma is a number greater
	 * than 19
	 * @param lemma lemma
	 * @return true if lemma is number word greater than 19
	 */
	private static boolean is19up (String lemma) {
		return !(oneToFour.contains(lemma)) && !(fiveTo18.contains(lemma));
	}
	
	/**
	 * Returns the declension of the number (1-4) specified by lemma
	 * @param lemma lemma
	 * @return morphological forms
	 */
	private List<ConstructedWord> oneToFourDec (String lemma) {
		IrregularNumerals in = pa.getIrregularNumerals();
		Paradigm p = in.getForms(lemma);
		List<ConstructedWord> out = new ArrayList<ConstructedWord>();
		for (Morpheme m : p.getMorphemes()) {
			for (Morph n : m.getAllomorphs()) {
				ConstructedWord cw = new ConstructedWord(n.getMorph(), m.getFeatureSet());
				cw.setLemma(lemma);
				out.add(cw);
			}
		}
		return out;
	}
	
	/**
	 * Checks whether the given stem could be the
	 * stem of a number from 1 to 4
	 * @param stem stem
	 * @return true if stem could be number 1-4
	 */
	public static boolean isOneToFourStem (String stem) {
		if (isOneToFour(stem))
			return true;
		for (String s : oneToFour) {
			if (s.startsWith(stem) && (s.length() - stem.length() < 3))
				return true;
		}
		return false;
	}
	
	/**
	 * Checks whether the given stem could be
	 * the stem of a number from 5 to 18
	 * @param stem stem
	 * @return true if stem could be number 5-18
	 */
	public static boolean isFiveTo18Stem (String stem) {
		if (isFiveTo18(stem))
			return true;
		for (String s : fiveTo18) {
			if (s.startsWith(stem) && (s.length() - stem.length() < 3))
				return true;
		}
		return false;
	}
	
	/**
	 * Checks whether the given stem could be
	 * the stem of a number greater than 19
	 * @param stem stem
	 * @return true if stem could be number greater than 19
	 */
	public static boolean is19upStem (String stem) {
		return (!isOneToFourStem(stem)) && (!isFiveTo18Stem(stem));
	}
} public class WordConverter {
	private static JsonSerializer js = new JsonSerializer();
	/**
	 * Converts the given ConstructedWord to String
	 * 
	 * @param cw
	 *            constructed word
	 * @return string
	 */
	public static String toString(ConstructedWord cw) {
		return cw.toString();
	}

	/**
	 * Converts the given ConstructedWord to DictWord
	 * 
	 * @param cw
	 *            constructed word
	 * @return DictWord
	 * @throws Exception
	 */
	public static DictWord toDictWord(ConstructedWord cw) throws Exception {
		
		String json = toJSONString(cw);
		JObject jo = js.deserialize(json);
		DictWordN dwn = new DictWordN(jo);
		return dwn;
	}

	/**
	 * Converts the given JSON String to DictWord
	 * @param s json string
	 * @return DictWord
	 * @throws Exception
	 */
	public static DictWord toDictWord(String s) throws Exception {
		JObject jo = js.deserialize(s);
		return new DictWordN(jo);
	}
	
	public static JObject toJObject (String json) throws Exception {
		return js.deserialize(json);
	}
	/**
	 * Converts the given list of ConstructedWord to list of DictWord
	 * 
	 * @param list
	 *            list of constructed words
	 * @return list of dict words
	 * @throws Exception
	 */
	public static List<DictWord> toDictWord(List<ConstructedWord> list)
			throws Exception {
		List<DictWord> out = new ArrayList<DictWord>();
		for (ConstructedWord cw : list)
			out.add(toDictWord(cw));
		return out;
	}

	/**
	 * Converts the given constructed word to JSON string
	 * 
	 * @param cw
	 *            constructed word
	 * @return JSON string
	 * @throws JsonProcessingException
	 */
	public static String toJSONString(ConstructedWord cw)
			throws JsonProcessingException {
		ObjectMapper om = new ObjectMapper();
		ObjectNode toplevel = om.createObjectNode();
		toplevel.put("word", cw.getWord());
		ObjectNode grammar = toplevel.putObject("grammar");
		ObjectNode morphology = grammar.putObject("morphology");
		morphology.put("lemma", cw.getLemma());
		ObjectNode paradigm = morphology.putObject("information");
		for (Feature f : cw.getFeatureSet()) {
			paradigm.put(f.getKey(), f.getValue());
		}
		String mys = om.writeValueAsString(toplevel);
		return mys;
	}
	
	/**
	 * Converts the given list of constructed words to list of JSON strings
	 * 
	 * @param list
	 *            list of constructed words
	 * @return list of JSON strings
	 * @throws JsonProcessingException
	 */
	public static List<String> toJSONStringList(List<ConstructedWord> list)
			throws JsonProcessingException {
		List<String> out = new ArrayList<String>();
		ObjectMapper om = new ObjectMapper();
		ObjectNode toplevel = om.createObjectNode();
		
		ConstructedWord first = list.remove(0);
		
		
		toplevel.put("lemma", first.getLemma());
		
		ObjectNode forms = toplevel.putObject("forms");
		 
		
		ArrayNode array = forms.putArray(first.getFeatureSet().getFeature("paradigm"));
		
		ObjectNode content = array.addObject();
		content.put("word", first.getWord());
		ObjectNode grammar = content.putObject("grammar");
		for (Feature f : first.getFeatureSet()) {
			if (f.getKey().equals("paradigm"))
				continue;
			grammar.put(f.getKey(), f.getValue());
		}
		
		for (ConstructedWord cw : list) {
			ObjectNode c = array.addObject();	
			c.put("word", cw.getWord());
			ObjectNode g = c.putObject("grammar");
			for (Feature f : cw.getFeatureSet()) {
				if (f.getKey().equals("paradigm"))
					continue;
				g.put(f.getKey(), f.getValue());
			}	
			
			//om = null;
		}
		out.add(om.writeValueAsString(toplevel));
		return out;
	}	
	
	public static String toJSONStringGenerator(List<ConstructedWord> list) throws JsonProcessingException {
		ObjectMapper om = new ObjectMapper();
		ObjectNode toplevel = om.createObjectNode();
		
		ConstructedWord first = list.remove(0);
		
		
		toplevel.put("lemma", first.getLemma());
		
		ObjectNode forms = toplevel.putObject("forms");
		 
		
		ArrayNode array = forms.putArray(first.getFeatureSet().getFeature("paradigm"));
		String initialPos = first.getFeatureSet().getFeature("paradigm");
		ObjectNode content = array.addObject();
		content.put("word", first.getWord());
		ObjectNode grammar = content.putObject("grammar");
		for (Feature f : first.getFeatureSet()) {
			if (f.getKey().equals("paradigm"))
				continue;
			grammar.put(f.getKey(), f.getValue());
		}
		
		for (ConstructedWord cw : list) {
			if (!cw.getFeatureSet().getFeature("paradigm").equals(initialPos)) {
				array = forms.putArray(cw.getFeatureSet().getFeature("paradigm"));
			}
			ObjectNode c = array.addObject();	
			c.put("word", cw.getWord());
			ObjectNode g = c.putObject("grammar");
			for (Feature f : cw.getFeatureSet()) {
				if (f.getKey().equals("paradigm"))
					continue;
				g.put(f.getKey(), f.getValue());
			}	
			
			//om = null;
		}
		return om.writeValueAsString(toplevel);
		
	}

	public static String toJSONStringLemmatizer(List<ConstructedWord> list) throws JsonProcessingException {
		ObjectMapper om = new ObjectMapper();
		ObjectNode toplevel = om.createObjectNode();
		ConstructedWord first = list.remove(0);
		toplevel.put("word", first.getWord());
		ObjectNode forms = toplevel.putObject("forms");
		String initialPos = first.getFeatureSet().getFeature("paradigm");
		ArrayNode array = forms.putArray(initialPos);
		ObjectNode content = array.addObject();
		content.put("lemma", first.getLemma());
		for (ConstructedWord cw : list) {
			if (!cw.getFeatureSet().getFeature("paradigm").equals(initialPos)) {
				array = forms.putArray(cw.getFeatureSet().getFeature("paradigm"));
			}
			
			ObjectNode c = array.addObject();	
			c.put("lemma", cw.getLemma());
		}
		return om.writeValueAsString(toplevel);
	}

	public static String toJSONStringAnalyzer(List<ConstructedWord> list) throws JsonProcessingException {
		List<String> analyses = new ArrayList<String>();
		ObjectMapper om = new ObjectMapper();
		ObjectNode toplevel = om.createObjectNode();
		
		ConstructedWord first = list.remove(0);
		
		String initialLemma = first.getLemma();
		
		toplevel.put("lemma", initialLemma);
		
		ObjectNode forms = toplevel.putObject("forms");
		 
		String initialPos = first.getFeatureSet().getFeature("paradigm");
		ArrayNode array = forms.putArray(initialPos);
		
		ObjectNode content = array.addObject();
		content.put("word", first.getWord());
		ObjectNode grammar = content.putObject("grammar");
		for (Feature f : first.getFeatureSet()) {
			if (f.getKey().equals("paradigm"))
				continue;
			grammar.put(f.getKey(), f.getValue());
		}
		
		for (ConstructedWord cw : list) {
			if (!cw.getFeatureSet().getFeature("paradigm").equals(initialPos)) {
				array = forms.putArray(cw.getFeatureSet().getFeature("paradigm"));
			}
			if (!cw.getLemma().equals(initialLemma)) {
				analyses.add(om.writeValueAsString(toplevel));
				om = new ObjectMapper();
				toplevel = om.createObjectNode();
				
				toplevel.put("lemma", cw.getLemma());
				
				forms = toplevel.putObject("forms");
				 
				initialPos = cw.getFeatureSet().getFeature("paradigm");
				array = forms.putArray(initialPos);
				initialLemma = cw.getLemma();
			}
			ObjectNode c = array.addObject();	
			c.put("word", cw.getWord());
			ObjectNode g = c.putObject("grammar");
			for (Feature f : cw.getFeatureSet()) {
				if (f.getKey().equals("paradigm"))
					continue;
				g.put(f.getKey(), f.getValue());
			}	
		}
		
		analyses.add(om.writeValueAsString(toplevel));
		StringBuilder sb = new StringBuilder("{\"analyses\":[");
		for (String a : analyses) {
			sb.append(a).append(",");
		}
		sb.deleteCharAt(sb.length()-1);
		sb.append("]}");
		return sb.toString();
	}

	public static String toJSONStringStemmer(String word, List<String> list) throws JsonProcessingException {
		ObjectMapper om = new ObjectMapper();
		ObjectNode toplevel = om.createObjectNode();
		toplevel.put("word", word);
		ObjectNode forms = toplevel.putObject("forms");
		
		ArrayNode array = forms.putArray("stems");
		
		for (String cw : list) {
			ObjectNode c = array.addObject();	
			c.put("stem", cw);
		}
		return om.writeValueAsString(toplevel);

	}

	public static String toJSONStringMerger(String word, String word2, List<String> list) throws JsonProcessingException {
		ObjectMapper om = new ObjectMapper();
		ObjectNode toplevel = om.createObjectNode();
		toplevel.put("word", word);
		toplevel.put("word2", word2);
		ObjectNode forms = toplevel.putObject("forms");
		
		ArrayNode array = forms.putArray("merged");
		
		for (String cw : list) {
			ObjectNode c = array.addObject();	
			c.put("form", cw);
		}
		return om.writeValueAsString(toplevel);
		
	}

	public static String toJSONStringSplitter(String word, List<SplitResult> list) throws Exception {
		ObjectMapper om = new ObjectMapper();
		ObjectNode toplevel = om.createObjectNode();
		toplevel.put("word", word);
		
		ObjectNode forms = toplevel.putObject("forms");
		
		ArrayNode splits = forms.putArray("splits");
		
		for (SplitResult cw : list) {
			ObjectNode node = splits.addObject();
			ArrayNode words = node.putArray("words");	
			for (String w : cw.getSplit())
				words.add(w);
			node.put("confidence", cw.getConfidence());
			
		}
		return om.writeValueAsString(toplevel);
		
	}
}
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.fasterxml.jackson.core.JsonProcessingException;
import de.general.json.JObject;
import de.general.log.*;
import de.cl.dictclient.DictWord;
import de.unitrier.daalft.pali.lexicon.LexiconAdapter;
import de.unitrier.daalft.pali.morphology.element.ConstructedWord;
import de.unitrier.daalft.pali.morphology.element.Feature;
import de.unitrier.daalft.pali.morphology.element.FeatureSet;
import de.unitrier.daalft.pali.morphology.element.Morph;
import de.unitrier.daalft.pali.morphology.element.Morpheme;
import de.unitrier.daalft.pali.morphology.paradigm.Paradigm;
import de.unitrier.daalft.pali.morphology.paradigm.ParadigmAccessor;
import de.unitrier.daalft.pali.morphology.paradigm.irregular.IrregularNouns;
import de.unitrier.daalft.pali.morphology.paradigm.irregular.IrregularNumerals;
import de.unitrier.daalft.pali.morphology.strategy.*;
import de.unitrier.daalft.pali.morphology.tools.WordClassGuesser;
import de.unitrier.daalft.pali.morphology.tools.VerbHelper;
import de.unitrier.daalft.pali.morphology.strategy.NumeralStrategy;
import de.unitrier.daalft.pali.tools.WordConverter;
package de.unitrier.daalft.pali.morphology;





/**
 * Class responsible for the morphological analysis of words
 * @author David
 *
 */
public class MorphologyAnalyzer {

	////////////////////////////////////////////////////////////////
	// Constants
	////////////////////////////////////////////////////////////////

	/**
	 * Word class specific endings
	 */
	private final static String 
			VERB_ENDING = "ti",
			NUMERAL_ENDING_5_18 = "a";
	/**
	 * Word class specific endings
	 */
	private final static String[] 
			NOUN_ENDINGS = {"as", "a", "u", "us", "i", "in", "ar", "an", "ant", "ā", "ī", "ū"},
			ADJ_ENDINGS = {"a", "i", "u", "ant", "ā", "ī", "ū"},
			NUMERAL_ENDING_19 = {"a", "i", "aṃ"};

	/**
	 * Separator between morphemes
	 */
	private final static String SEPARATOR = "_";

	////////////////////////////////////////////////////////////////
	// Variables
	////////////////////////////////////////////////////////////////

	private ParadigmAccessor pa;