/*
 * Example.java
 *
 * Ace Kanji Workout
 * Copyright (C) 2006 Alex Quinn <aquinn@cs.washington.edu>
 * Licensed under GPL v2, http://www.gnu.org/licenses/gpl.html
 *
 * Created: May 31, 2006
 * Author:  Alex Quinn
 */

import java.util.Hashtable;
import java.util.LinkedList;
import java.util.HashSet;
import java.util.ListIterator;

class Example {
	protected static final String SPLIT_SEPARATOR = ":";
	String inKanji;
	String inKana;
	boolean isPopular;
	String[] meanings;
	int jlptLevel;
	String kanjiSplit;
	String kanaSplit;

	public Example(String inKanji, String inKana, boolean isPopular, String[] meanings) {
	// This constructor sets the JLPT level to 0, for use when reading the
	// Edict file before the JLPT levels are known.
		this(inKanji,inKana,isPopular,meanings,0);
	}

	public String getMeaningsString() {
		String result = "";
		for( String meaning : meanings ) {
			result += ", "+meaning;
		}
		result = result.substring(2);
		return result;
	}

	public Example(String inKanji, String inKana, boolean isPopular, String[] meanings, int jlptLevel) {
		this.inKanji = inKanji;
		this.inKana = inKana;
		this.isPopular = isPopular;
		this.meanings = meanings;
		this.jlptLevel = jlptLevel;
		this.kanjiSplit = "";
		this.kanaSplit = "";
	}

	private static final String FIELD_SEPARATOR = "/";

	public String toString() {
		String meaningsString = "";
		for( String meaning : meanings )
			meaningsString += FIELD_SEPARATOR+meaning.replaceAll(FIELD_SEPARATOR,"-");
		meaningsString = meaningsString.substring(1);
		return String.format("%s\t%s\t%s\t%s\t%s\t%d\t%s",
			inKanji,
			kanjiSplit,
			inKana,
			kanaSplit,
			Boolean.toString(isPopular),
			jlptLevel,
			meaningsString
		);
	}
	public Example(String stringForm) {
		String[] parts = stringForm.split("\t");
		this.inKanji = parts[0];
		this.kanjiSplit = parts[1];
		this.inKana = parts[2];
		this.kanaSplit = parts[3];
		this.isPopular = Boolean.parseBoolean(parts[4]);
		this.jlptLevel = Integer.parseInt(parts[5]);
		this.meanings = parts[6].split(FIELD_SEPARATOR);
	}

	public String debugString() {
		// Just used for debugging.  Not connected to any production code.
		String meaningString = "";
		for( String s : meanings ) { meaningString += ":"+s; }  meaningString += ":";
		return String.format("%s %s %b %s %d",inKanji,inKana,isPopular,meaningString,jlptLevel);
	}

	public void setJlptLevel(int jlptLevel) {
		this.jlptLevel = jlptLevel;
	}

	public LinkedList<String> makeSubstituteSplits(String candidate) {
	// Take a possible split of a example reading, and output an array of
	// altered versions.  This is to account for the fact that kanji readings
	// frequently change when they are put into a compound.  Here are the
	// things it will try:
	// 1. Add voicing (ten-ten) just after the split.
	// 3. Change H to P (small circle) just after the split.
	// 3. Change KU or TSU to small-TSU just before the split.
		LinkedList<Integer> splitPositions = new LinkedList<Integer>();
		LinkedList<String> substitutes = new LinkedList<String>();
		assert candidate.length()>0;
		for( int i=0; i<candidate.length(); i++ ) {
			try {
				if( candidate.substring(i,i+1).equals(SPLIT_SEPARATOR) ) {
					splitPositions.add(i);
					assert i>0;
					assert i<candidate.length()-1 : String.format("%d %d",i,candidate.length());
				}
			}
			catch( Exception e ) {
				System.err.println("\n");
				System.err.println(i);
				System.err.println(candidate.length());
				System.err.println("\n");
				System.err.flush();
				e.printStackTrace();
				System.exit(-1);
			}
		}
		substitutes.add(candidate);
		String oldSubstitution, newSubstitution;
		char[][] replaceWithVoicedArray = Japanese.getReplaceWithVoicedArray();
		char[][] replaceHWithPArray = Japanese.getReplaceHWithPArray();
		char[][] replaceKuTsuWithXtsuArray = Japanese.getReplaceKuTsuWithXtsuArray();
		for( int splitPosition : splitPositions ) {
			char[][] replacementArray;
			final int ADD_VOICE = 0;
			final int CHANGE_H_TO_P = 1;
			final int REPLACE_KU_WITH_XTSU = 2;
			int positionOfCharToChangeRelativeToSplit = 0;
			for( int j=0; j<3; j++ ) {
				if( j==ADD_VOICE ) {
					replacementArray = replaceWithVoicedArray;
					positionOfCharToChangeRelativeToSplit = 1;
				}
				else if( j==CHANGE_H_TO_P ) {
					replacementArray = replaceHWithPArray;
					positionOfCharToChangeRelativeToSplit = 1;
				}
				else {
					assert j==REPLACE_KU_WITH_XTSU;
					replacementArray = replaceKuTsuWithXtsuArray;
					positionOfCharToChangeRelativeToSplit = -1;
				}
				for( char[] substitutionPair : replacementArray ) {
					if( candidate.charAt(splitPosition+positionOfCharToChangeRelativeToSplit)==substitutionPair[0] ) {
						int i = 0;
						int substituteCount = substitutes.size();
						ListIterator<String> iter = substitutes.listIterator(0);
						LinkedList<String> newSubstitutionsList = new LinkedList<String>();
						for( i=0; i<substituteCount; i++ ) {
							oldSubstitution = new String(iter.next());
							newSubstitution = oldSubstitution.substring(0,splitPosition+positionOfCharToChangeRelativeToSplit) +
											substitutionPair[1] +
											oldSubstitution.substring(splitPosition+positionOfCharToChangeRelativeToSplit+1);
							newSubstitutionsList.addLast( newSubstitution );
						}
						while( newSubstitutionsList.size()>0 ) {
							substitutes.add(newSubstitutionsList.removeFirst());
						}
					}
				}
			}
		}
		String first = substitutes.removeFirst();
		assert first==candidate;
		return substitutes;
	}

	public boolean goodForStudying() {
		// Used for filtering out examples which are actually relevant for
		// somebody learning Japanese.  By reducing the number of examples,
		// we also reduce the number of readings, because generally a reading
		// is not used unless it appears in at least one example.
		return isPopular || jlptLevel>=1;
	}

	public void makeSplits(Hashtable<String, Kanji> kanjiHash) throws KanjiTrainerBadExampleException {
	// Fill up the kanaSplit and kanjiSplit fields in this Example object.
	// You start with a kanji representation and a kana representation.
	// Place a symbol between any division of kanji-kanji, kana-kanji, or kanji-kana.
	// Then, you look at the kana reading and try to figure out which kana
	// correspond to which kanji.  The goal is to place a divider in the kana
	// representation in an equivalent place.  This allows the program to classify
	// the examples by the way the kanji is read in that particular example.
	//
	// Doing this was a little complicated.  Basically, each kanji can correspond
	// to one of several readings, which are listed in the kanjidic information.
	// Using the kanjidic information, we list out every possible way to read that
	// example, without respect to the known reading, and without respect to which
	// readings are kun and which are on readings.  If there are 2 kanji, with
	// N and M readings, respectively, then there will be NxM possible ways to read
	// the example.  Then, we go through and see which of those possibilities
	// actually matches the known readings.  In many cases this doesn't work because
	// when you put two kanji together, often their pronunciation changes.  So,
	// if we fail on the first try, we try again, this time trying various
	// possibilities for altering the pronunciation of the kanji within.  If this
	// creates a single match, then the problem is solved.
	//
	// Some examples cannot be successfully split with this approach.  Typical scenarios
	// are:
	// 1. Irregular readings
	// 2. Word is in Edict, but not in Kojien (a very large and well-respected 
	//    Japanese dictionary), at least with the same spelling.
	// 3. Word is spelled the same way in Edict and Kojien, but Kanjidic seems to be
	//    missing the needed readings.
	// 4. There should be one or more extra hiragana or kanji added, but they are omitted
	//    in Edict.
	// 5. Edict matches Kojien, but there are some hiragana that are omitted and generally
	//    understood to be pronounced even if not written.
	//
	// As of 6/20/06, my program doesn't take care of these cases, which account for 304
	// out of 12967 examples (2.3%).
	//
	// Another problem is that occassionally, when listing out all the possibilities, there
	// will be more than one valid way to split the reading.  For example, the word
	// kawara (riverbank) is made up of the kanji kawa (river) and hara.  Kawa can be read
	// as "ka" or "kawa".  Hara can be read as "hara", "wara", or "ra".  Thus, the division
	// is ambiguous.  It could be "kawa:ra" or it could be "ka:wara".  My algorithm can't
	// deal with this.  As of today, these cases accounted for 6 out of 12967 examples (0.01%).
		char ch;
		int i,j;
		int charType=Japanese.NOTHING, prevCharType=Japanese.NOTHING;
		int kanjiCount = 0;
		Kanji kanji;
		
		assert inKanji.length()>0;
		assert inKana.length()>0 : "inKana==\""+inKana+"\" and meaning=="+meanings[0];
		assert kanjiSplit=="";
		assert Japanese.countKanji(inKanji)>0;

		// Make the split kanji version and count the kanji.
		for( i=0; i<inKanji.length(); i++ ) {
			ch = inKanji.charAt(i);
			prevCharType = charType;
			charType = Japanese.getCharType(ch);
			if( charType!=prevCharType || charType==Japanese.KANJI )
				kanjiSplit += SPLIT_SEPARATOR;
			kanjiSplit += ch;
			if( charType==Japanese.KANJI )
				kanjiCount++;
			if( Japanese.charIsKanji(ch) && kanjiHash.containsKey(String.valueOf(ch))==false ) {
				throw new KanjiTrainerBadExampleException(inKanji,"contains unknown kanji",false);
			}
		}
		if( kanjiSplit.startsWith(SPLIT_SEPARATOR) ) {
			kanjiSplit = kanjiSplit.substring(SPLIT_SEPARATOR.length());
		}
		assert kanjiSplit.length()>0;

		if( kanjiCount==1 ) {
			// Before we go into the really complicated part of this algorithm, let's try an
			// easier method that only works in cases where there is only one kanji.  This
			// should be a little faster.  This also catches some oddball readings that the
			// main algorithm can't deal with for one reason or another.
			//
			// Essentially, we're going to strip off the kana from the front and back of
			// the kanji representation, while simultaneously removing the same kana from
			// the kana representation.  That tells us how the kanji is read in that particular
			// word.  Once that's done, it's easy to make the split.
			assert kanaSplit.length()==0;
			assert kanjiSplit.length()>0;
			assert inKanji.length()>0;
			assert inKana.length()>0;
			assert inKana.length()>=inKanji.length();
			String kanjiPart=inKanji, kanaTemp=inKana;
			while(Japanese.charIsKana(kanjiPart.charAt(0))) {
				kanjiPart = kanjiPart.substring(1);
				kanaTemp = kanaTemp.substring(1);
			}
			int kanaLength=kanaTemp.length();
			int kanjiLength=kanjiPart.length();
			while(Japanese.charIsKana(kanjiPart.charAt(kanjiLength-1))) {
				kanjiPart = kanjiPart.substring(0,kanjiLength-1);
				kanaTemp = kanaTemp.substring(0,kanaLength-1);
				kanjiLength--;
				kanaLength--;
			}
			i=0;
			String[] partsOfKanjiSplit = kanjiSplit.split(SPLIT_SEPARATOR);
			assert partsOfKanjiSplit.length<=3;
			for( String partOfKanjiSplit : partsOfKanjiSplit ) {
				if( kanaSplit.length()>0 )
					kanaSplit += SPLIT_SEPARATOR;
				if( Japanese.charIsKanji(partOfKanjiSplit.charAt(0)))
					kanaSplit += kanaTemp;
				else
					kanaSplit += partOfKanjiSplit;
			}
		}
		else {
			String[][] readings = new String[kanjiCount][];
			HashSet<String> readingSet = new HashSet<String>();
			int[] readingCounts = new int[kanjiCount];
			int kanjiCounter=0, totalPatterns=1;
			for( i=0; i<inKanji.length(); i++ ) {
				readingSet.clear();
				ch = inKanji.charAt(i);
				if( Japanese.charIsKanji(ch) ) {
					kanji = kanjiHash.get(String.valueOf(ch));
					kanjiCounter++;
					for( String reading : kanji.getOnReadings() ) {
						if( reading.contains(".") )
							reading = reading.substring(0,reading.indexOf('.'));
						if( reading.endsWith("-") )
							reading = reading.substring(0,reading.length()-1);
						readingSet.add(reading);
					}
					for( String reading : kanji.getKunReadings() ) {
						if( reading.contains(".") )
							reading = reading.substring(0,reading.indexOf('.'));
						if( reading.endsWith("-") )
							reading = reading.substring(0,reading.length()-1);
						readingSet.add(reading);
					}
					try {
					readings[kanjiCounter-1] = new String[readingSet.size()];
					} catch( ArrayIndexOutOfBoundsException e ) {
						System.err.println("kanjiCounter="+String.valueOf(kanjiCounter));
						System.err.println("readings.length="+String.valueOf(readings.length));
						System.err.println("meanings[0]="+meanings[0]);
						System.err.flush();
					}
					readings[kanjiCounter-1] = readingSet.toArray(readings[kanjiCounter-1]);
					readingCounts[kanjiCounter-1] = readingSet.size();
					totalPatterns *= readingCounts[kanjiCounter-1];
				}
			}
			if( totalPatterns>100000 ) {
				throw new KanjiTrainerBadExampleException(inKanji, "too many patterns",false);
			}
			int[] patterns = new int[kanjiCount];
			int n,divisor,last_divisor,digit;
			String[] parts = null;
			final int JUST_BASICS=0, TRY_ALTERNATIVES=1;

			LinkedList<String> possibilityList = new LinkedList<String>();
			// Try every combination of readings.
			for( i=0; i<totalPatterns; i++ ) {
				String possibleReading = "";
				n=i;
				divisor=0;
				last_divisor=1;
				digit=0;
				possibleReading = "";
				assert kanjiCount>0 : "Didn't find any kanji in this example.";

				// Compute the parts of this particular pattern, just as integers,
				// Those integers will index into the arrays of readings for each kanji.
				// This algorithm is a lot like what you'd use to convert number bases.
				for(j=0; j<kanjiCount; j++) {
					divisor = readingCounts[kanjiCount-j-1];
					n = (n-digit)/last_divisor;
					digit = n % divisor;
					last_divisor = divisor;
					patterns[kanjiCount-j-1] = digit;
					parts = kanjiSplit.split(SPLIT_SEPARATOR);
				}
				// Construct a reading of the compound, using the pattern generated above.
				kanjiCount = 0;
				for( String part : parts ) { // for each piece of the split kanji version
					assert part!=null;
					assert part!="";

					// If this part is a kanji, then plug in a reading for it.
					if( Japanese.charIsKanji(part.charAt(0)) ) {
						assert part.length()==1;
						kanjiCount++;
						possibleReading += readings[kanjiCount-1][patterns[kanjiCount-1]];
					}
					else {  // Otherwise, it must be kana.  Just add the kana as is.
						possibleReading += part;
					}
					possibleReading+=SPLIT_SEPARATOR;
				}
				possibleReading = possibleReading.substring(0,possibleReading.length()-1);
				possibilityList.add(possibleReading);
			}
			boolean tryingAlternatives = false;
			assert kanaSplit=="";
			while( kanaSplit=="" ) {
				for( String possibleReading : possibilityList ) {
					LinkedList<String> possibilitiesWithAlternatives;
					if( tryingAlternatives==false ) {
						possibilitiesWithAlternatives = new LinkedList<String>();
						possibilitiesWithAlternatives.add(possibleReading);
					}
					else {
						possibilitiesWithAlternatives = makeSubstituteSplits(possibleReading);
					}
					for( String readingToTry : possibilitiesWithAlternatives ) {
						String rawReading = readingToTry.replaceAll(SPLIT_SEPARATOR,"");
						String rawHiraganaReading = Japanese.toHiragana(rawReading);
						if( rawHiraganaReading.equals(inKana) ) {
							if( kanaSplit=="" )
								kanaSplit = readingToTry;
							else {
								if( Japanese.toHiragana(kanaSplit).equals(Japanese.toHiragana(readingToTry))==false ) {
									throw new KanjiTrainerBadExampleException(inKanji,"Multiple ways to split reading",true);
								}
							}
						}
					}
				}
				if( kanaSplit=="" ) {
					if( tryingAlternatives==true ) {
						throw new KanjiTrainerBadExampleException(inKanji, "Couldn't split reading",true);
					}
					else {
						tryingAlternatives = true;
					}
				}
			}
		}
	}
	String meaningsAsStringDebug() {  // just for debugging
		String all="";
		int i;
		for(i=0; i<meanings.length; i++) {
			all += meanings[i];
			if(i<meanings.length-1)
				//all += ", ";
				all += "\t";
		}
		return all;
	}

	public int popularityLevel() {
		// This is used for sorting the Examples for display,
		// such that the most common examples will show at the top.
		// Frequency rankings don't seem to be so useful for this.
		if(jlptLevel>0)
			return jlptLevel + 1;
		else if( isPopular )
			return 1;
		else
			return 0;
	}
}


