package Practicals.Ex4;

import java.io.*;
import java.util.*;
import Practicals.Ex3.FileWordStream;
import Practicals.Ex3.Word;
import Practicals.Ex3.WordStream;

public class FileConcordance implements Concordance, java.io.Serializable
{
	protected TreeMap m_wordLocationMap = new TreeMap(new FunkyComparator());		// String -> LinkedList<Word>
	protected TreeMap m_pathWordsMap = new TreeMap(new CaseInsensitiveComparator());	// String -> HashSet<String>

	public void addWords(WordStream ws)
	{
		while(ws.hasNext())
		{
			Word w = ws.next();

			// Maintain a list of the words in each file to make it more efficient to remove words from a specific
			// file later on if desired.
			HashSet hs = null;
			if(m_pathWordsMap.containsKey(w.path))
			{
				hs = (HashSet)m_pathWordsMap.get(w.path);
			}
			else
			{
				hs = new HashSet();
			}
			hs.add(w.text);
			m_pathWordsMap.put(w.path, hs);

			// Add the word location to the map.
			LinkedList lst = null;
			if(m_wordLocationMap.containsKey(w.text))
			{
				lst = (LinkedList)m_wordLocationMap.get(w.text);
			}
			else
			{
				lst = new LinkedList();
			}
			lst.addLast(w);
			m_wordLocationMap.put(w.text, lst);
		}
	}

	public static FileConcordance load(String path) throws ClassNotFoundException, IOException
	{
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path));
		return (FileConcordance)ois.readObject();
	}

	public WordStream occurrences(String word)
	{
		// Precondition: m_wordLocationMap.containsKey(word) == true
		return new ConcordanceWordStream((LinkedList)m_wordLocationMap.get(word));
	}

	public void remove(String path)
	{
		if(!m_pathWordsMap.containsKey(path)) return;

		HashSet hs = (HashSet)m_pathWordsMap.get(path);
		Iterator it = hs.iterator();

		while(it.hasNext())
		{
			String word = (String)it.next();
			LinkedList locations = (LinkedList)m_wordLocationMap.get(word);
			ListIterator jt = locations.listIterator();
			while(jt.hasNext())
			{
				Word w = (Word)jt.next();
				if(w.path.equals(path)) jt.remove();
			}

			if(locations.isEmpty())
			{
				m_wordLocationMap.remove(word);
			}
		}

		m_pathWordsMap.remove(path);
	}

	public void save(String path) throws IOException
	{
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path));
		oos.writeObject(this);
	}

	public Iterator words()
	{
		Set keySet = m_wordLocationMap.keySet();
		return keySet.iterator();
	}

	public static void main(String[] args) throws Exception
	{
		/*FileConcordance conc = new FileConcordance();
		for(int i=0; i<args.length; ++i)
		{
			conc.addWords(new FileWordStream(args[i]));
		}
		Iterator words = conc.words();
		while(words.hasNext())
		{
			String path = null;
			String word = (String)words.next();
			int n = 0;
			System.out.print(word);
			WordStream occurrences = conc.occurrences(word);
			while(occurrences.hasNext())
			{
				Word occurrence = occurrences.next();
				if(occurrence.path != path)
				{
					path = occurrence.path;
					System.out.print("\n\t");
					System.out.print(path);
					n = 0;
				}
				++n;
				if(n % 10 == 0) System.out.print("\n\t");
				else System.out.print(" ");
				System.out.print(occurrence.lineNum);
			}
			System.out.println();
		}*/

		// CONCORDANCE SHELL
		FileConcordance conc = new FileConcordance();
		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

		for(;;)
		{
			System.out.print("Concordance Shell - Menu\n\n1. build <file list>\n2. extend <file list>\n" +
					 "3. remove <file list>\n4. save <filename>\n5. load <filename>\n6. print\n" +
					 "7. query <word>\n8. clear\n9. quit\n\n");

			System.out.print("> ");
			String command = reader.readLine();
			System.out.println();

			if(command.equals("quit")) break;

			if(command.length() >= 5 && command.substring(0,5).equals("build"))
			{
				conc = new FileConcordance();
				StringTokenizer st = new StringTokenizer(command.substring(6));
				while(st.hasMoreTokens()) conc.addWords(new FileWordStream(st.nextToken()));
			}
			else if(command.length() >= 6 && command.substring(0,6).equals("extend"))
			{
				StringTokenizer st = new StringTokenizer(command.substring(7));
				while(st.hasMoreTokens()) conc.addWords(new FileWordStream(st.nextToken()));
			}
			else if(command.length() >= 6 && command.substring(0,6).equals("remove"))
			{
				StringTokenizer st = new StringTokenizer(command.substring(7));
				while(st.hasMoreTokens()) conc.remove(st.nextToken());
			}
			else if(command.length() >= 4 && command.substring(0,4).equals("save"))
			{
				conc.save(command.substring(5));
			}
			else if(command.length() >= 4 && command.substring(0,4).equals("load"))
			{
				conc = FileConcordance.load(command.substring(5));
			}
			else if(command.length() >= 5 && command.substring(0,5).equals("print"))
			{
				Iterator words = conc.words();
				while(words.hasNext())
				{
					String path = null;
					String word = (String)words.next();
					int n = 0;
					System.out.print(word);
					WordStream occurrences = conc.occurrences(word);
					while(occurrences.hasNext())
					{
						Word occurrence = occurrences.next();
						if(occurrence.path != path)
						{
							path = occurrence.path;
							System.out.print("\n\t");
							System.out.print(path);
							n = 0;
						}
						++n;
						if(n % 10 == 0) System.out.print("\n\t");
						else System.out.print(" ");
						System.out.print(occurrence.lineNum);
					}
					System.out.println();
				}
				System.out.println();
			}
			else if(command.length() >= 5 && command.substring(0,5).equals("query"))
			{
				String path = null;
				String word = command.substring(6);
				int n = 0;
				System.out.print(word);
				WordStream occurrences = conc.occurrences(word);
				while(occurrences.hasNext())
				{
					Word occurrence = occurrences.next();
					if(occurrence.path != path)
					{
						path = occurrence.path;
						System.out.print("\n\t");
						System.out.print(path);
						n = 0;
					}
					++n;
					if(n % 10 == 0) System.out.print("\n\t");
					else System.out.print(" ");
					System.out.print(occurrence.lineNum);
				}
				System.out.print("\n\n");
			}
			else if(command.length() >= 5 && command.substring(0,5).equals("clear"))
			{
				conc = new FileConcordance();
			}
			else
			{
				System.out.print("Invalid command!\n\n");
			}
		}
	}

	////////////////// NESTED CLASS SECTION //////////////////

	private static class CaseInsensitiveComparator implements Comparator, java.io.Serializable
	{
		public int compare(Object o1, Object o2)
		{
			return ((String)o1).toLowerCase().compareTo(((String)o2).toLowerCase());
		}
	}

	private static class ConcordanceWordStream implements WordStream
	{
		private LinkedList m_words = new LinkedList();
		private ListIterator m_iter = null;

		public ConcordanceWordStream(LinkedList lst)
		{
			m_words = lst;
			m_iter = m_words.listIterator();
		}

		public boolean hasNext()
		{
			return m_iter.hasNext();
		}

		public Word next()
		{
			return (Word)m_iter.next();
		}
	}

	private static class FunkyComparator implements Comparator, java.io.Serializable
	{
		public int compare(Object o1, Object o2)
		{
			// We'd like to just do this, but we have a problem. Because things like "class" and "CLASS" now become equivalent,
			// we don't get both stored in the map as we'd like. What we actually want is not to treat "class" and "CLASS" as the
			// same, but to define a new total ordering, e.g. ClAss < Class < class < Collection. This requires a little bit more
			// effort than the "simple" version, but it's substantially more useful.
			//return ((String)o1).toLowerCase().compareTo(((String)o2).toLowerCase());

			// Explanation: We step through the strings one letter at a time. If one letter comes strictly before the other in the
			// alphabet (if one lower-case letter is strictly less than the other lower-case letter), then that string comes before
			// the other one, since all the previous letters were the same (or the same to within a change of case). If the letters
			// are the same but for a change of case, then we note that ceteris paribus (all other things being equal), the one with
			// a capital letter will come before the one with a small letter. Of course, if two letters later on compare strictly
			// different in lowercase, we'll resolve the conflict that way. Note that if we do have to resort to using case to
			// separate the two strings, we take the first point at which their cases differ. That's what the ret == 0 test is
			// doing.

			int ret = 0;

			String s1 = (String)o1, s2 = (String)o2, ls1 = s1.toLowerCase(), ls2 = s2.toLowerCase();
			int i = 0;
			for(; i<s1.length() && i<s2.length(); ++i)
			{
				if(ls1.charAt(i) < ls2.charAt(i)) return -1;
				else if(ls1.charAt(i) > ls2.charAt(i)) return 1;
				else if(s1.charAt(i) < s2.charAt(i) && ret == 0) ret = -1;
				else if(s1.charAt(i) > s2.charAt(i) && ret == 0) ret = 1;
			}

			// If we get here, they're the same up to the end of the shorter one except for (possibly) change of case. If they're
			// not the same length, the shorter one comes first.
			if(s1.length() < s2.length()) return -1;
			if(s2.length() < s1.length()) return 1;

			return ret;
		}
	}
}