/*
 Jazzy - a Java library for Spell Checking
 Copyright (C) 2001 Mindaugas Idzelis
 Full text of license can be found in LICENSE.txt

 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-1301  USA
 */

/*
 * This is an dictionary based on a Trie datastructure. It's limited to a specific
 * characterset which is defined in the RECOGNIZED_CHARACTERS class attribute.
 * 
 * The default values are for the English character set and contains the letters
 * a to z and the apostrophee. Other characters can be added to this, but additional
 * characters will incurr and additional memory overhead on each level of the trie.
 * 
 * The code used the SpellDictionaryHashMap as a skeleton with the Trie code added 
 * by Al Sutton (sourceforge@alsutton.com).
 */

package com.swabunga.spell.engine;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.util.List;

/**
 * The SpellDictionaryTriep holds the dictionary using a Trie datastructure.
 */
public class SpellDictionaryEnglishOnlyTrie extends SpellDictionaryASpell {

	/**
	 * The size of the recognized characters list.
	 */

	private static final int RECOGNIZED_CHARACTERS_SIZE = 27;

	/**
	 * Holds the root trie node.
	 */

	private TrieEntry rootNode = new TrieEntry();

	/** Holds the dictionary file for appending */
	private File dictFile = null;

	/**
	 * Dictionary Constructor.
	 * 
	 * @throws java.io.IOException
	 *             indicates a problem with the file system
	 */
	public SpellDictionaryEnglishOnlyTrie() throws IOException {
		super((File) null);
	}

	/**
	 * Dictionary Constructor.
	 * 
	 * @param wordList
	 *            The file containing the words list for the dictionary
	 * @throws java.io.IOException
	 *             indicates problems reading the words list file
	 */
	public SpellDictionaryEnglishOnlyTrie(Reader wordList) throws IOException {
		super((File) null);
		createDictionary(new BufferedReader(wordList));
	}

	/**
	 * Dictionary convenience Constructor.
	 * 
	 * @param wordList
	 *            The file containing the words list for the dictionary
	 * @throws java.io.FileNotFoundException
	 *             indicates problems locating the words list file on the system
	 * @throws java.io.IOException
	 *             indicates problems reading the words list file
	 */
	public SpellDictionaryEnglishOnlyTrie(File wordList)
			throws FileNotFoundException, IOException {
		this(new FileReader(wordList));
		dictFile = wordList;
	}

	/**
	 * Dictionary constructor that uses an aspell phonetic file to build the
	 * transformation table.
	 * 
	 * @param wordList
	 *            The file containing the words list for the dictionary
	 * @param phonetic
	 *            The file to use for phonetic transformation of the wordlist.
	 * @throws java.io.FileNotFoundException
	 *             indicates problems locating the file on the system
	 * @throws java.io.IOException
	 *             indicates problems reading the words list file
	 */
	public SpellDictionaryEnglishOnlyTrie(File wordList, File phonetic)
			throws FileNotFoundException, IOException {
		super(phonetic);
		dictFile = wordList;
		createDictionary(new BufferedReader(new FileReader(wordList)));
	}

	/**
	 * Dictionary constructor that uses an aspell phonetic file to build the
	 * transformation table. Encoding is used for phonetic file only; default
	 * encoding is used for wordList
	 * 
	 * @param wordList
	 *            The file containing the words list for the dictionary
	 * @param phonetic
	 *            The file to use for phonetic transformation of the wordlist.
	 * @param phoneticEncoding
	 *            Uses the character set encoding specified
	 * @throws java.io.FileNotFoundException
	 *             indicates problems locating the file on the system
	 * @throws java.io.IOException
	 *             indicates problems reading the words list or phonetic
	 *             information
	 */
	public SpellDictionaryEnglishOnlyTrie(File wordList, File phonetic,
			String phoneticEncoding) throws FileNotFoundException, IOException {
		super(phonetic, phoneticEncoding);
		dictFile = wordList;
		createDictionary(new BufferedReader(new FileReader(wordList)));
	}

	/**
	 * Dictionary constructor that uses an aspell phonetic file to build the
	 * transformation table.
	 * 
	 * @param wordList
	 *            The file containing the words list for the dictionary
	 * @param phonetic
	 *            The reader to use for phonetic transformation of the wordlist.
	 * @throws java.io.IOException
	 *             indicates problems reading the words list or phonetic
	 *             information
	 */
	public SpellDictionaryEnglishOnlyTrie(Reader wordList, Reader phonetic)
			throws IOException {
		super(phonetic);
		dictFile = null;
		createDictionary(new BufferedReader(wordList));
	}

	/**
	 * Add words from a file to existing dictionary hashmap. This function can
	 * be called as many times as needed to build the internal word list.
	 * Duplicates are not added.
	 * <p>
	 * Note that adding a dictionary does not affect the target dictionary file
	 * for the addWord method. That is, addWord() continues to make additions to
	 * the dictionary file specified in createDictionary()
	 * <P>
	 * 
	 * @param wordList
	 *            a File object that contains the words, on word per line.
	 * @throws FileNotFoundException
	 * @throws IOException
	 */
	public void addDictionary(File wordList) throws FileNotFoundException,
			IOException {
		addDictionaryHelper(new BufferedReader(new FileReader(wordList)));
	}

	/**
	 * Add words from a Reader to existing dictionary hashmap. This function can
	 * be called as many times as needed to build the internal word list.
	 * Duplicates are not added.
	 * <p>
	 * Note that adding a dictionary does not affect the target dictionary file
	 * for the addWord method. That is, addWord() continues to make additions to
	 * the dictionary file specified in createDictionary()
	 * <P>
	 * 
	 * @param wordList
	 *            a Reader object that contains the words, on word per line.
	 * @throws IOException
	 */
	public void addDictionary(Reader wordList) throws IOException {
		addDictionaryHelper(new BufferedReader(wordList));
	}

	/**
	 * Add a word permanently to the dictionary (and the dictionary file).
	 * <p>
	 * This needs to be made thread safe (synchronized)
	 * </p>
	 */
	public void addWord(String word) {
		putWord(word);
		if (dictFile == null)
			return;
		try {
			FileWriter w = new FileWriter(dictFile.toString(), true);
			// Open with append.
			w.write(word);
			w.write("\n");
			w.close();
		} catch (IOException ex) {
			System.out.println("Error writing to dictionary file");
		}
	}

	/**
	 * Constructs the dictionary from a word list file.
	 * <p>
	 * Each word in the reader should be on a separate line.
	 * <p>
	 * This is a very slow function. On my machine it takes quite a while to
	 * load the data in. I suspect that we could speed this up quite allot.
	 */
	protected void createDictionary(BufferedReader in) throws IOException {
		String line = "";
		while (line != null) {
			line = in.readLine();
			if (line != null && line.length() > 0) {
				line = new String(line.toCharArray());
				putWord(line);
			}
		}
	}

	/**
	 * Adds to the existing dictionary from a word list file. If the word
	 * already exists in the dictionary, a new entry is not added.
	 * <p>
	 * Each word in the reader should be on a separate line.
	 * <p>
	 * Note: for whatever reason that I haven't yet looked into, the phonetic
	 * codes for a particular word map to a vector of words rather than a hash
	 * table. This is a drag since in order to check for duplicates you have to
	 * iterate through all the words that use the phonetic code. If the
	 * vector-based implementation is important, it may be better to subclass
	 * for the cases where duplicates are bad.
	 */
	protected void addDictionaryHelper(BufferedReader in) throws IOException {
		String line = "";
		while (line != null) {
			line = in.readLine();
			if (line != null && line.length() > 0) {
				line = new String(line.toCharArray());
				putWord(line);
			}
		}
	}

	/**
	 * Allocates a word in the dictionary
	 * 
	 * @param word
	 *            The word to add
	 */
	protected void putWord(String word) {
		rootNode.addWord(word);
	}

	/**
	 * Returns a list of strings (words) for the code.
	 */
	public List getWords(String code) {
		return null;
	}

	/**
	 * Returns true if the word is correctly spelled against the current word
	 * list.
	 */
	public boolean isCorrect(String word) {
		return rootNode.findMatch(word);
	}

	/**
	 * An entry for a trie.
	 */

	private class TrieEntry {
		/**
		 * The list of trie entries below this one.
		 */

		private TrieEntry[] children = new TrieEntry[RECOGNIZED_CHARACTERS_SIZE];

		/**
		 * Whether or not this can be a terminator
		 */

		private boolean canBeTerminator = false;

		/**
		 * Adds a word to this trie.
		 * 
		 * @param word The word to add.
		 */

		public void addWord(String word) {
			addWord(word.toLowerCase(), 0);
		}

		/**
		 * Adds the specific character from a word into the trie at
		 * this level.
		 * 
		 * @param word The word being added to the trie.
		 * @param position The position of the character within the word appropriate
		 * 		for this level of the trie.
		 */

		public void addWord(String word, int position) {
			if (position == word.length()) {
				canBeTerminator = true;
				return;
			}

			char character = word.charAt(position);
			int value;
			if( character == '\'' ) {
				value = 26;
			} else {
				value = (int)(character - 'a');
			}
			
			synchronized (children) {
				TrieEntry entry = children[value];
				if (entry == null) {
					entry = new TrieEntry();
					children[value] = entry;
				}
				entry.addWord(word, position + 1);
			}
		}

		/**
		 * Attempts to match a word in the trie. 
		 * 
		 * @param word The word being checked.
		 * 
		 * @return true If the word is in the trie, false if not.
		 */

		public boolean findMatch(String word) {
			return findMatch(word.toLowerCase(), 0);
		}

		/**
		 * Attempts to match a word in the trie. It uses the position
		 * value to determine which character in the word is appropriate 
		 * for this level of the trie, checks that an entry exists, and,
		 * if appropriate, sends the word onto the next level for checking.
		 * 
		 * @param word The word being checked.
		 * @param position The position appropriate to this level.
		 * 
		 * @return true If the word is in the trie, false if not.
		 */

		public boolean findMatch(String word, int position) {
			if (position == word.length()) {
				if( canBeTerminator ) {
					return true;
				} else {
					return false;
				}
			}

			TrieEntry entry = null;

			char character = word.charAt(position);
			int value;
			if( character == '\'' ) {
				value = 26;
			} else {
				value = (int)(character - 'a');
			}

			entry = children[value];

			if (entry == null) {
				return false;
			}

			return entry.findMatch(word, position + 1);
		}
	}

	/**
	 * Test method to give timings
	 * 
	 * @param args
	 */
	public static void main(String[] args) {
		try {
			long start = System.currentTimeMillis();
			SpellDictionaryEnglishOnlyTrie tester = new SpellDictionaryEnglishOnlyTrie(
					new FileReader("\\utils\\jazzy-0.5.2\\english.0"));
			long end = System.currentTimeMillis();
			System.out.println("Addition Time : " + (end - start) + "ms");

			start = System.currentTimeMillis();
			boolean check = true;
			for (int i = 0; i < 100000 && check; i++) {
				tester.isCorrect("ACM");
				tester.isCorrect("zoos");
			}
			end = System.currentTimeMillis();
			System.out.println("Valid Lookup Time : " + (end - start) + "ms - "
					+ check);

			start = System.currentTimeMillis();
			check = false;
			for (int i = 0; i < 100000 && check == false; i++) {
				check = tester.isCorrect("Testerx");
			}
			end = System.currentTimeMillis();
			System.out.println("Oversided False Lookup Time : " + (end - start) + "ms - "
					+ check);

			start = System.currentTimeMillis();
			check = false;
			for (int i = 0; i < 100000 && check == false; i++) {
				check = tester.isCorrect("Tes");
			}
			end = System.currentTimeMillis();
			System.out.println("Undersized False Lookup Time : " + (end - start) + "ms - "
					+ check);

		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}
}
