/*
 * KanjiTrainer.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 4/30/06
 *
 * Main file of an appication for studying kanji.
 *
 * A note on data member access.  I took a fairly liberal approach to data member
 * access for accessing fields of the Example and Kanji classes.  Direct access to
 * some data fields is needed for manipulating and displaying the data.  I didn't
 * want to have a large number of getters and setters, so I just kept everything in
 * the same package and accessed some members directly.  In hindsight, maybe it would
 * have been better to use the getters and setters.  For example, there's some
 * redundant information in the Example class (split and unsplit version) which could
 * be easily eliminated if it weren't for the direct member access.
 *
 *
 */

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusListener;
import java.awt.event.FocusEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.GraphicsEnvironment;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.Toolkit;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.AbstractList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Locale;
import java.util.ResourceBundle;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.border.LineBorder;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.filechooser.FileFilter;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JEditorPane;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingConstants;
import javax.swing.Timer;
import javax.swing.ToolTipManager;


class KanjiTrainer extends JPanel implements ActionListener, KTUpdateListener {
	private static ResourceBundle resources = ResourceBundle.getBundle("KanjiTrainer");
	private Pool pool;
	private PoolSpec poolSpec;
	private DeckSpec deckSpec;
	private Hashtable<String,KanjiPerformanceDetail> performanceHash;
	private Options options;
	private UseCalendar useCalendar;
	private Kanji currentKanji;
	public static void main(String[] args) {
		JFrame frame = new JFrame();

		// Set up the frame.
		frame.setTitle(resources.getString("TITLE"));
		URL url = KanjiTrainer.class.getResource(resources.getString("ICON_PATH")); 
		Image iconImage = Toolkit.getDefaultToolkit().getImage(url);
		//frame.setIconImage(new ImageIcon(resources.getString("ICON_PATH")).getImage());
		frame.setIconImage(iconImage);
		frame.setSize(800,600);
		frame.setExtendedState(JFrame.MAXIMIZED_BOTH);
		frame.setLayout(new BorderLayout());
		frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);

		// We create an instance of KanjiTrainer, and then
		// add it to the frame from within the static main method.  So, the outer frame
		// is created from the static main method, but the contents are an instance of the
		// (non-static) main class.
		KanjiTrainer kanjiTrainer = new KanjiTrainer(frame);
		frame.setJMenuBar(kanjiTrainer.createMenuBar());
		frame.add(kanjiTrainer, BorderLayout.CENTER);
		frame.addWindowListener(kanjiTrainer.getAppCloser());

		frame.setVisible(true);
	}

	JFrame frame;
	JLabel kanjiDisplay; // displays the kanji in a large font
	JLabel kanjiBasics;    // displays the readings and other basic information about the kanji
	StatusBar status;
	JMenuBar menuBar;
	JMenuItem delayToggleMenuItem;
	JTextArea extraInfo;
	JScrollPane extraInfoScrollPane;
	KTAction actionShow;
	KTAction actionRight;
	KTAction actionWrong;
	JButton showButton;
	JButton rightButton;
	JButton wrongButton;
	String[] fontList;

	// Time limit related
	Kanji timerKanji;
	int timerCount;
		// When the timer is set, timerKanji will be set to the current kanji.
		// When the timer goes off, if the current kanji is the same as this
		// kanji, then it will be marked wrong.  Otherwise, the timer will be
		// ignored.  However, that's not enough.  If there are more than one
		// timer, then we need to make sure only to handle the last remaining
		// one.

	int drillMode;
	static final int MODE_START = 1;
	static final int MODE_HIDE = 2;
	static final int MODE_SHOW = 3;
	int lowerDisplayMode;
	static final int DISPLAY_EXAMPLES = 1;
	static final int DISPLAY_HELP = 2;
	static final int DISPLAY_STATISTICS = 3;
	static final int DISPLAY_NOTHING = 4;

	static void dmp(String s) { // for debugging
		System.out.println(s);
	}

	String getDecentFontChoice() {
		String[] defaultPreferences = resources.getString("DEFAULT_FONT_PREFERENCES").split(";");
		String[] fontList = getFontList();
		for( int i=0; i<defaultPreferences.length; i++ ) {
			for( String fontName : fontList ) {
				if( fontName.equals(defaultPreferences[i]) ) {
					return fontName;
				}
			}
		}
		return null;
	}

	String[] getFontList() {
		if( fontList==null ) {
			// This operations is rather slow, so we only want to do it once per session, at most.
			String fontNames[] = 
					GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
			LinkedList<String> fontLinkedList = new LinkedList<String>();
			for( String s : fontNames ) {
				Font f = new Font(s,Font.PLAIN,14);
				if( f.canDisplay('\u4EF2') && f.canDisplay('\u3042') ) {
					fontLinkedList.add(s);
				}
			}
			fontList = fontLinkedList.toArray(new String[fontLinkedList.size()]);
		}
		assert fontList!=null;
		if( fontList.length==0 ) {
			JOptionPane.showMessageDialog(frame,resources.getString("NO_FONTS_MSG"),
					resources.getString("NO_FONTS_TITLE"), JOptionPane.ERROR_MESSAGE);
			throw new RuntimeException("No suitable fonts are installed on this system.");
		}
		return fontList;
	}

	void noMoreKanji() {
		JOptionPane.showMessageDialog(frame, 
				resources.getString("MSG_NO_KANJI_TO_SHOW"),
				resources.getString("MSG_NO_KANJI_TO_SHOW_TITLE"),
				JOptionPane.INFORMATION_MESSAGE);
		clearUI();
	}
	
	void loadNextKanji() {
		currentKanji = pool.getNext();
		if( currentKanji==null && pool.getTotalKanjiInCurrentDeck()>0 ) {
			deckDone();
			pool.loadNewDeck(deckSpec);
			currentKanji = pool.getNext();
		}
		if( currentKanji==null ) {
			noMoreKanji();
		}
		else {
			setKanjiDisplay(currentKanji);
			setKanjiBasics("","","");
			drillMode = MODE_HIDE;
			updateLowerDisplay();
			updateStatus();
			actionShow.setEnabled(true);
			actionRight.setEnabled(false);
			actionWrong.setEnabled(false);
		}
		if( options.delayIsEnabled() ) {
			setTimer();
		}
	}
	void updateStatus() {
		String msg = pool.statusMessage();
		if( options.delayIsEnabled() )
			msg += String.format(", time limit set to %d seconds",options.getDelaySeconds());
		status.setStatus(msg);
	}

	void showAnswer() {
		setKanjiBasics(	currentKanji.getOnReadingsString(),
						currentKanji.getKunReadingsString(),
						currentKanji.getMeaningsString() );
		drillMode = MODE_SHOW;
		updateLowerDisplay();
		actionShow.setEnabled(false);
		actionRight.setEnabled(true);
		actionWrong.setEnabled(true);
	}

	void setupActions() {
		KTAction action;
		String mappingKey;
		
		// "Show"
		actionShow = new KTAction(	this,
									resources.getString("BTN_TXT_SHOW"),
							 		resources.getString("BTN_TXT_SHOW_DESCRIPTION"),
							  		KeyEvent.VK_S);
		mappingKey = "show";
		this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("COMMA"),mappingKey);
		this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("P"),mappingKey);
		this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("NUMPAD4"),mappingKey);
		this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("LEFT"),mappingKey);
		this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("Z"),mappingKey);
		getActionMap().put(mappingKey, actionShow);

		// "Right"
		actionRight = new KTAction(	this,
									resources.getString("BTN_TXT_RIGHT"),
							 		resources.getString("BTN_TXT_RIGHT_DESCRIPTION"),
							  		KeyEvent.VK_R);
		mappingKey = "right";
		this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("PERIOD"),mappingKey);
		this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("OPEN_BRACKET"),mappingKey);
		this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("NUMPAD5"),mappingKey);
		this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("DOWN"),mappingKey);
		this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("X"),mappingKey);
		getActionMap().put(mappingKey, actionRight);

		// "Wrong"
		actionWrong = new KTAction(	this,
									resources.getString("BTN_TXT_WRONG"),
							 		resources.getString("BTN_TXT_WRONG_DESCRIPTION"),
							  		KeyEvent.VK_W);
		mappingKey = "wrong";
		this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("SLASH"),mappingKey);
		this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("CLOSE_BRACKET"),mappingKey);
		this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("NUMPAD6"),mappingKey);
		this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("RIGHT"),mappingKey);
		this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("C"),mappingKey);
		getActionMap().put(mappingKey, actionWrong);
	}

	KanjiTrainer(JFrame frame) {
		super(new BorderLayout(),true);
		this.frame = frame;
		drillMode = MODE_START;
		lowerDisplayMode = DISPLAY_EXAMPLES;
		ToolTipManager.sharedInstance().setDismissDelay(30000); // make the tooltip stay for 30 seconds.

		///////////////////////////////
		// Load data files
		boolean newUser = false;

		// Establish the filenames and proper encodings
		String kanjiFileName = resources.getString("DATAFILE_KANJI_NAME");
		String kanjiFileEncoding = resources.getString("DATAFILE_KANJI_ENCODING");
		String exampleFileName = resources.getString("DATAFILE_EXAMPLES_NAME");
		String exampleFileEncoding = resources.getString("DATAFILE_EXAMPLES_ENCODING");
		String profileFileName = resources.getString("PROFILE_FILENAME");
		String profileFileEncoding = resources.getString("PROFILE_FILE_ENCODING");
		Hashtable<String,Kanji> kanjiHash = loadKanjiHash(kanjiFileName,kanjiFileEncoding);
		Hashtable<String,Example> exampleHash = loadExampleHash(exampleFileName,exampleFileEncoding);
		Profile profile = null;
		if( (new File(profileFileName).exists()) ) { //TODO, replace this with an exception
			newUser = false;  // for clarity
			try {
				profile = loadProfile();
			}
			catch( BadProfileException e ) {
				String msg = e.getMessage();
				JOptionPane.showMessageDialog( frame,
						resources.getString("MSG_CANT_READ_PROFILE") + (msg.length()==0 ? "" : "\n\n"+msg),
						resources.getString("MSG_CANT_READ_PROFILE_TITLE"),
						JOptionPane.ERROR_MESSAGE );
				System.exit(1); // **************** POSSIBLE EXIT POINT **************** //
			}
			poolSpec = profile.poolSpec;
			deckSpec = profile.deckSpec;
			performanceHash = profile.performanceHash;
		}
		else {
			newUser = true;
			poolSpec = new PoolSpec();// make a default PoolSpec
			deckSpec = new DeckSpec(); // make a default DeckSpec
			performanceHash = new Hashtable<String,KanjiPerformanceDetail>();
			options = new Options(false, 5, false, 5, true, getDecentFontChoice());
			useCalendar = new UseCalendar();
			profile = new Profile(performanceHash, deckSpec, poolSpec, options, useCalendar);
		}
		pool = new Pool(resources, profile, kanjiHash, exampleHash );
		

		////////////////////////////
		// Set up the UI
		JPanel innerPanel;
		JPanel outerPanel = new JPanel(); // actually mid-level
		outerPanel.setLayout(new BoxLayout(outerPanel,BoxLayout.Y_AXIS));

		// Set up the actions which will handle some keyboard stuff.
		setupActions();

		// Set up the top stuff.
		innerPanel = new JPanel(new BorderLayout());

		// Set up display for large kanji.
		kanjiDisplay = new JLabel("", SwingConstants.CENTER);
		kanjiDisplay.setFont(new Font(options.getFont(),Font.PLAIN,Integer.parseInt(resources.getString("KANJI_DISPLAY_FONT_SIZE"))));
		clearKanjiDisplay();
		innerPanel.add(kanjiDisplay,BorderLayout.WEST);

		// Set up area for displaying information about kanji.
		kanjiBasics = new JLabel();
		kanjiBasics.setFont(
				new Font(options.getFont(), Font.PLAIN,
				Integer.parseInt(resources.getString("KANJI_BASICS_FONT_SIZE"))));
		setKanjiBasics("","","");
		innerPanel.add(kanjiBasics,BorderLayout.CENTER);
		innerPanel.setMinimumSize(new Dimension(800,260));
		outerPanel.add(innerPanel);
		add(outerPanel,BorderLayout.NORTH);

		// Set up row of buttons
		innerPanel = new JPanel(new GridLayout(1,3,4,4));
		innerPanel.add(showButton = new JButton(actionShow));
		innerPanel.add(rightButton = new JButton(actionRight));
		innerPanel.add(wrongButton = new JButton(actionWrong));
		outerPanel.add(innerPanel);

		// Set up radio buttons that select the content for the bottom panel.
		innerPanel = new JPanel(new FlowLayout());
		innerPanel.add(new JLabel(resources.getString("MORE_INFO")));
		ButtonGroup bg = new ButtonGroup();
		JRadioButton rb;
		rb = new JRadioButton(resources.getString("RBTN_TXT_EXAMPLES"));
		rb.setSelected( lowerDisplayMode==DISPLAY_EXAMPLES );
		rb.addActionListener(this);
		rb.setMnemonic(KeyEvent.VK_E);
		bg.add(rb);
		innerPanel.add(rb);
		rb = new JRadioButton(resources.getString("RBTN_TXT_STATISTICS"));
		rb.setSelected( lowerDisplayMode==DISPLAY_STATISTICS );
		rb.addActionListener(this);
		rb.setMnemonic(KeyEvent.VK_A);
		bg.add(rb);
		innerPanel.add(rb);
		rb = new JRadioButton(resources.getString("RBTN_TXT_KBD_SHORTCUTS"));
		rb.setSelected( lowerDisplayMode==DISPLAY_HELP );
		rb.addActionListener(this);
		rb.setMnemonic(KeyEvent.VK_K);
		bg.add(rb);
		innerPanel.add(rb);
		rb = new JRadioButton(resources.getString("RBTN_TXT_NOTHING"));
		rb.setSelected( lowerDisplayMode==DISPLAY_NOTHING );
		rb.addActionListener(this);
		rb.setMnemonic(KeyEvent.VK_N);
		bg.add(rb);
		innerPanel.add(rb);
		outerPanel.add(innerPanel);

		// Set up the bottom content panel.
		extraInfo = new JTextArea(5,30);
		extraInfo.setFont(new Font(options.getFont(),
							Font.PLAIN,
							Integer.parseInt(resources.getString("EXTRA_INFO_FONT_SIZE"))));
		extraInfo.setEditable(false);
		extraInfo.setWrapStyleWord(true);
		extraInfo.setTabSize(10);
		extraInfo.setText(resources.getString("MSG_FOR_LANDING_ON_BLANK"));
		extraInfoScrollPane = new JScrollPane(extraInfo);
		add(extraInfoScrollPane);

		// Add in the Status Bar
		add(createStatusBar(),BorderLayout.SOUTH);




		// Get the kanji game started.
		if( newUser && userWantsToChangeSettings() ) {
			new PoolSpecDialog(frame,resources,poolSpec,this,options);
		}
		else {
			loadNextKanji();
		}
	}
	
	public void deckDone() {
		float averageShowsToSuccess = pool.getAverageShowsToRight();
		int totalCards = pool.getTotalKanjiInCurrentDeck();
		updateStatus();
		String msg = String.format(resources.getString("MSG_DECK_FINISHED"),
						totalCards,averageShowsToSuccess,totalCards);
		JOptionPane.showMessageDialog(frame,msg,resources.getString("MSG_DECK_FINISHED_TITLE"),JOptionPane.INFORMATION_MESSAGE);
	}

	protected boolean userWantsToChangeSettings() {
		String[] options = new String[] {
					resources.getString("MSG_GETTING_STARTED_CHANGE_SETTINGS"),
					resources.getString("MSG_GETTING_STARTED_DONT_CHANGE")};
		int n = JOptionPane.showOptionDialog(frame, resources.getString("MSG_GETTING_STARTED_GREETING"),
					resources.getString("MSG_GETTING_STARTED_TITLE"),
					JOptionPane.YES_NO_OPTION,
					JOptionPane.QUESTION_MESSAGE,
					null,
					options,
					options[0]);
		return n==0;
	}

	public void optionsUpdated() {
		changeFont();
		if( options.delayIsEnabled() && drillMode==MODE_HIDE ) {
			setTimer();
		}
		if(drillMode==MODE_SHOW) 
			displayExamples();
		delayToggleMenuItem.setSelected(options.delayIsEnabled());
		updateStatus();
	}

	protected void changeFont() {
		JComponent[] comps = new JComponent[] {kanjiDisplay,kanjiBasics,extraInfo};
		String name = options.getFont();
		int style, size;
		Font oldFont;
		for( JComponent comp : comps ) {
			oldFont = comp.getFont();
			style = oldFont.getStyle();
			size = oldFont.getSize();
			comp.setFont( new Font( name, style, size) );
		}
	}
	protected void clearUI() {
		status.setStatus("");
		clearKanjiDisplay();
		setKanjiBasics("","","");
		displayNothing();
	}

	protected void updateLowerDisplay() {
		if( lowerDisplayMode==DISPLAY_EXAMPLES && drillMode==MODE_SHOW )
			displayExamples();
		if( lowerDisplayMode==DISPLAY_EXAMPLES && drillMode==MODE_HIDE )
			clearExamples();
		else if( lowerDisplayMode==DISPLAY_STATISTICS )
			displayStatistics();
	}
	protected void displayNothing() {
		extraInfo.setText("");
	}
	protected void displayStatistics() {
		extraInfo.setText(pool.statisticsMessage());
		extraInfo.setLineWrap(true);
		extraInfo.setCaretPosition(0);
	}
	protected void displayHelp() {
		String filename = resources.getString("HELP_FILENAME");
		String encoding = resources.getString("HELP_FILE_ENCODING");
		InFile infile = new JarInFile(filename,encoding);
		String contents = infile.readAll();
		infile.close();
		extraInfo.setText(contents);
		extraInfo.setLineWrap(true);
		extraInfo.setCaretPosition(0);
	}
	protected void clearExamples() {
		displayNothing();
	}
	protected void displayExamples() {
		// In this function, we're going to get a list of examples (as Example objects)
		// from the Pool.  Then, we need to break them up by reading and present just
		// as many as the user has selected.  The user is allowed to limit the number
		// of examples shown per example.
		Example[] examplesArray = pool.getExamples(currentKanji.kanjiChar);
		LinkedList<Example> examples = new LinkedList<Example>();
		for( Example exampleInArray : examplesArray )
			examples.add(exampleInArray);

		// Sort the examples according to the popularity level.  Popularity level will
		// probably be defined in the following order, from most popular to least popular.
		// JLPT4, JLPT3, JLPT2, JLPT1, Edict-P, NoneOfTheAbove
		
		Collections.sort(examples, new Comparator<Example>() {
			public int compare(Example e1, Example e2) {
				int p1=e1.popularityLevel();
				int p2=e2.popularityLevel();
				if( p1>p1 ) return -1;
				else if( p1<p2 ) return 1;
				else return 0;
			}
			public boolean equals(Example e1, Example e2) {
				return e1.popularityLevel()==e2.popularityLevel();
			}
		});
		
		String display = "", line="";
		String reading, kanjiSplit, kanaSplit;
		String[] kanjiParts, kanaParts;
		int whichPart = -1;
		int examplesPerReadingLimit = options.getExampleLimit();
		boolean exampleLimitSet = options.exampleLimitSet();
		
		// Make a hashtable that links a reading to a formatted line
		// containing the full detail of an example, including
		// the reading of the example, kanji version, and meanings.
		Hashtable<String,LinkedList<String>> readingToDetail = new Hashtable<String,LinkedList<String>>();
		LinkedList<String> exampleStringsList;
		for( Example example : examples ) {
			if( example.kanaSplit.length()>0 ) {
				kanjiParts = example.kanjiSplit.split(Example.SPLIT_SEPARATOR);
				kanaParts = example.kanaSplit.split(Example.SPLIT_SEPARATOR);
				for( int i=0; i<kanjiParts.length; i++ ) {
					if( kanjiParts[i].equals(currentKanji.kanjiChar) ) {
						whichPart = i;
						break;
					}
				}
				assert whichPart>=0 : String.format("whichPart==%d",whichPart);
				reading = Japanese.toHiragana(kanaParts[whichPart]);
				assert(Japanese.charIsKana(reading.charAt(0)));
			}
			else {
				// If there is no kanaSplit, then it must be irregular.  We'll keep these and use them.
				reading = ""; // means the kanji is irregular
			}
			line = String.format("%s (%s) - %s",example.inKanji,example.inKana,example.getMeaningsString());

			// Get the list of examples for this reading, or make a new one, if necessary.
			if( !readingToDetail.containsKey(reading) ) {
				exampleStringsList = new LinkedList<String>();
				readingToDetail.put(reading,exampleStringsList);
			}
			else {
				exampleStringsList = readingToDetail.get(reading);
			}

			// Add the line to the list.
			if( exampleLimitSet==false || exampleStringsList.size() < examplesPerReadingLimit )
				exampleStringsList.add(line);
		}
		Enumeration keys = readingToDetail.keys();
		LinkedList<String> keyList = new LinkedList<String>();
		while( keys.hasMoreElements() ) {
			keyList.add((String)keys.nextElement());
		}
		Collections.sort(keyList);
		ListIterator keyIter = keyList.listIterator();
		while( keyIter.hasNext() ) {
			reading = (String)keyIter.next();
			display += (display.length()==0 ? "" : "\n") + 
						(reading.equals("") ? resources.getString("EXAMPLE_IRREGULAR_LABEL") : reading);
			for( String lineForReading : readingToDetail.get(reading) ) {
				display += "\n    "+lineForReading;
			}
		}
		extraInfo.setText(display);
		extraInfo.setLineWrap(false);
		extraInfo.setCaretPosition(0);
	}
	
	protected void clearKanjiDisplay() {
		kanjiDisplay.setText(" ");
		kanjiDisplay.setToolTipText("");
	}

	static String getJoyoName(Kanji kanji) {
		if( kanji.joyo == 1 ) return resources.getString("JOYO_LEVEL_1");
		if( kanji.joyo == 2 ) return resources.getString("JOYO_LEVEL_2");
		if( kanji.joyo == 3 ) return resources.getString("JOYO_LEVEL_3");
		if( kanji.joyo == 4 ) return resources.getString("JOYO_LEVEL_4");
		if( kanji.joyo == 5 ) return resources.getString("JOYO_LEVEL_5");
		if( kanji.joyo == 6 ) return resources.getString("JOYO_LEVEL_6");
		if( kanji.joyo == 8 ) return resources.getString("JOYO_LEVEL_8");
		if( kanji.joyo == 9 ) return resources.getString("JOYO_LEVEL_9");
		return resources.getString("JOYO_NOT");
	}
		
	protected void setKanjiDisplay(Kanji kanji) {
		kanjiDisplay.setText(kanji.kanjiChar+" ");
		String toolTipText = "<html>";
		toolTipText += String.format("%s<br>",getJoyoName(kanji));
		
		if( kanji.hasFrequency() )
			toolTipText += String.format(resources.getString("TOOLTIP_FREQ"),kanji.frequency);
		if( kanji.hasJlptLevel() )
			toolTipText += String.format(resources.getString("TOOLTIP_JLPT"),kanji.jlptLevel);
		if( kanji.hasHalpern() )
			toolTipText += String.format(resources.getString("TOOLTIP_HALPERN"),kanji.halpern);
		if( kanji.hasHenshall() )
			toolTipText += String.format(resources.getString("TOOLTIP_HENSHALL"),kanji.henshall);
		if( kanji.hasHeisig() )
			toolTipText += String.format(resources.getString("TOOLTIP_HEISIG"),kanji.heisig);
		if( toolTipText=="</html>" )
			toolTipText = "";
		else if( toolTipText.endsWith("<br>") )
			toolTipText = toolTipText.substring(0,toolTipText.length()-4);
		toolTipText += "</html>";
		kanjiDisplay.setToolTipText(toolTipText);
	}
	
	protected void setKanjiBasics(String onYomi, String kunYomi, String englishMeanings) {
		String on=resources.getString("BASICS_ON");
		String kun=resources.getString("BASICS_KUN");
		String meanings=resources.getString("BASICS_MEANINGS");
		kanjiBasics.setText(String.format("<html>%s %s<br>%s %s<br>%s %s</html>",
					on,onYomi,kun,kunYomi,meanings,englishMeanings));
	}

	protected StatusBar createStatusBar() {
		status = new StatusBar();
		return status;
	}

	protected JMenuItem makeMenuItem(JMenu menu, String resourceName, 
				int mnemonic, int acceleratorBase, int acceleratorMask,
				boolean asCheckBox ) {
		JMenuItem menuitem;
		if( asCheckBox )
			menuitem = new JCheckBoxMenuItem(resources.getString(resourceName));
		else
			menuitem = new JMenuItem(resources.getString(resourceName));
		menuitem.setAccelerator(KeyStroke.getKeyStroke(acceleratorBase,acceleratorMask));
		menuitem.setMnemonic(mnemonic);
		menuitem.addActionListener(this);
		menu.add(menuitem);
		return menuitem;
	}

	protected JMenuBar createMenuBar() {
		this.menuBar = new JMenuBar();

		JMenu menu = new JMenu(resources.getString("MENU_TXT_FILE"));
		menu.setMnemonic(KeyEvent.VK_F);
		makeMenuItem(menu,"MENU_TXT_EXIT"        ,KeyEvent.VK_X,KeyEvent.VK_X,ActionEvent.CTRL_MASK,false);
		menuBar.add(menu);

		menu = new JMenu(resources.getString("MENU_TXT_TOOLS"));
		menu.setMnemonic(KeyEvent.VK_T);
		makeMenuItem(menu,"MENU_TXT_DRAW_DECK"       ,KeyEvent.VK_T,KeyEvent.VK_T,ActionEvent.CTRL_MASK,false);
		makeMenuItem(menu,"MENU_TXT_SELECT_POOL"     ,KeyEvent.VK_G,KeyEvent.VK_G,ActionEvent.CTRL_MASK,false);
		delayToggleMenuItem = makeMenuItem(
				menu,"MENU_TXT_TOGGLE_DELAY",
				KeyEvent.VK_L,KeyEvent.VK_L,ActionEvent.CTRL_MASK,true);
		delayToggleMenuItem.setSelected( options.delayIsEnabled() );
		makeMenuItem(menu,"MENU_TXT_CALENDAR"        ,KeyEvent.VK_C,KeyEvent.VK_C,ActionEvent.CTRL_MASK,false);
		makeMenuItem(menu,"MENU_TXT_OPTIONS"         ,KeyEvent.VK_O,KeyEvent.VK_O,ActionEvent.CTRL_MASK,false);
		menuBar.add(menu);

		menu = new JMenu(resources.getString("MENU_TXT_HELP"));
		menu.setMnemonic(KeyEvent.VK_H);
		makeMenuItem(menu,"MENU_TXT_ABOUT"           ,KeyEvent.VK_B,KeyEvent.VK_B,ActionEvent.CTRL_MASK,false);
		makeMenuItem(menu,"MENU_TXT_ACKNOWLEDGEMENTS",KeyEvent.VK_K,KeyEvent.VK_K,ActionEvent.CTRL_MASK,false);
		menuBar.add(menu);

		return menuBar;
	}

	class StatusBar extends JPanel {
		private JLabel statusLabel;
		StatusBar() {
			statusLabel = new JLabel(" ");
			add(statusLabel);
			this.setBackground(Color.lightGray);
			this.setBorder(new LineBorder(Color.black,1));
		}
		public void paint(Graphics g) {
			super.paint(g);
		}
		public void setStatus(String msg) {
			statusLabel.setText(msg);
		}
	}

	static void printFontNames() {
		String[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
		for(String fnt : fonts) {
			System.out.println(fnt);
		}
	}

	public WindowListener getAppCloser() {
		return new WindowAdapter() {
			public void windowClosing(WindowEvent e) {
				exit();
			}
		};
	}
	void showAboutDialog() {
		String aboutText = JarInFile.readFileQuick(
				resources.getString("ABOUT_FILENAME"),
				resources.getString("ABOUT_FILE_ENCODING")).replace("\n","");
		JOptionPane.showMessageDialog(frame,aboutText, 
				resources.getString("ABOUT_TITLE"), 
				JOptionPane.INFORMATION_MESSAGE);
	}
	void showAcknowledgementsDialog() {
		String acknowledgementsText = JarInFile.readFileQuick(
			resources.getString("ACKNOWLEDGEMENTS_FILENAME"),
			resources.getString("ACKNOWLEDGEMENTS_FILE_ENCODING")).replace("\n","");
		JOptionPane.showMessageDialog(frame,acknowledgementsText, 
			resources.getString("ACKNOWLEDGEMENTS_TITLE"), 
			JOptionPane.INFORMATION_MESSAGE);
	}

	public void actionPerformed( ActionEvent e ) {
		if(e.getActionCommand()==resources.getString("MENU_TXT_SELECT_POOL")) {
			new PoolSpecDialog(frame,resources,poolSpec,this,options);
		}
		else if(e.getActionCommand()==resources.getString("MENU_TXT_TOGGLE_DELAY")) {
			boolean delayOn = ((JMenuItem)e.getSource()).isSelected();
			options.setDelayEnabled(delayToggleMenuItem.isSelected());
			optionsUpdated();
			System.out.format("caught delayToggleMenuItem, %b\n",delayToggleMenuItem.isSelected());
		}
		else if(e.getActionCommand()==resources.getString("MENU_TXT_OPTIONS")) {
			new OptionsDialog(frame,resources,options,this, getFontList());
		}
		else if(e.getActionCommand()==resources.getString("MENU_TXT_CALENDAR")) {
			new UseCalendarDialog(frame,resources,useCalendar);
		}
		else if(e.getActionCommand()==resources.getString("MENU_TXT_ACKNOWLEDGEMENTS")) {
			showAcknowledgementsDialog();
		}
		else if(e.getActionCommand()==resources.getString("MENU_TXT_ABOUT")) {
			showAboutDialog();
		}
		else if(e.getActionCommand()==resources.getString("MENU_TXT_DRAW_DECK")) {
			new DeckSpecDialog(frame,resources,deckSpec,this);
		}
		else if(e.getActionCommand()==resources.getString("MENU_TXT_EXIT")) {
			exit();
		}
		else if(e.getActionCommand()==resources.getString("RBTN_TXT_EXAMPLES")) {
			lowerDisplayMode = DISPLAY_EXAMPLES;
			updateLowerDisplay();
		}
		else if(e.getActionCommand()==resources.getString("RBTN_TXT_STATISTICS")) {
			lowerDisplayMode = DISPLAY_STATISTICS;
			updateLowerDisplay();
		}
		else if(e.getActionCommand()==resources.getString("RBTN_TXT_KBD_SHORTCUTS")) {
			lowerDisplayMode = DISPLAY_HELP;
			displayHelp();
		}
		else if(e.getActionCommand()==resources.getString("RBTN_TXT_NOTHING")) {
			lowerDisplayMode = DISPLAY_NOTHING;
			displayNothing();
		}
	}

	void exit() {
		boolean saveChanges = false;
		if( options.saveChangesWithoutAsking() ) {
			saveChanges = true;
		}
		else {
			int response = JOptionPane.showConfirmDialog(
					frame, 
					resources.getString("EXIT_SAVE_CHANGES_TEXT"), 
					resources.getString("EXIT_SAVE_CHANGES_TITLE"),
					JOptionPane.YES_NO_CANCEL_OPTION,
					JOptionPane.QUESTION_MESSAGE);
			if( response==JOptionPane.YES_OPTION )
				saveChanges = true;
			else
				saveChanges = false;
			if( response==JOptionPane.CANCEL_OPTION || response==JOptionPane.CLOSED_OPTION )
				return;
		}
		if( saveChanges )
			saveProfile();
		System.exit(0);
	}

	void handleTooLate() {
		handleShow();
		actionRight.setEnabled(false);
	}
	void setTimer() {
		if( options.delayIsEnabled() ) {
			timerCount++;
			timerKanji = currentKanji;
			ActionListener listener = new ActionListener() {
				public void actionPerformed( ActionEvent e ) {
					if( options.delayIsEnabled() ) {
						timerCount--;
						if( timerCount==0 && currentKanji==timerKanji && drillMode==MODE_HIDE ) {
							handleTooLate();
						}
						else {
							timerKanji = null;
							timerCount = 0;
						}
					}
					
				}
			};
			Timer timer = new Timer(options.getDelaySeconds()*1000, listener );
			timer.setRepeats(false);
			timer.start();
		}
		else {
			timerKanji = null;
			timerCount = 0;
		}
	}
	void handleShow() {
		showAnswer();
	}
	void handleRight() {
		if( pool.kanjiIsPreviouslyKnown(currentKanji) )
			useCalendar.kanjiReviewed(currentKanji.kanjiChar);
		else
			useCalendar.kanjiIntroduced(currentKanji.kanjiChar);
		pool.markCurrentAsRight();
		loadNextKanji();
	}
	void handleWrong() {
		pool.markCurrentAsWrong();
		loadNextKanji();
	}
	class KTAction extends AbstractAction {
		KanjiTrainer kanjiTrainer;
		KTAction(KanjiTrainer kanjiTrainer, String text, String desc, Integer mnemonic) {
			super(text);
			this.kanjiTrainer = kanjiTrainer;
			putValue(SHORT_DESCRIPTION, desc);
			putValue(MNEMONIC_KEY, mnemonic);
		}
		public void actionPerformed(ActionEvent e) {
			String name = (String)getValue(NAME);
			if( drillMode==MODE_START ) {
				// do nothing
			}
			else if( name==kanjiTrainer.resources.getString("BTN_TXT_SHOW") ) {
				kanjiTrainer.handleShow();
			}
			else if( name==kanjiTrainer.resources.getString("BTN_TXT_RIGHT") ) {
				kanjiTrainer.handleRight();
			}
			else if( name==kanjiTrainer.resources.getString("BTN_TXT_WRONG") ) {
				kanjiTrainer.handleWrong();
			}
		}
	}
	public void deckSpecUpdated() { // deckSpec was updated by reference
		pool.loadNewDeck(deckSpec);
		loadNextKanji();
	}
	public void poolSpecUpdated() { // poolSpec was updated by reference
		pool.updateWithNewPoolSpec(poolSpec);
		if( drillMode==MODE_START )
			new DeckSpecDialog(frame,resources,deckSpec,this);
		else {
			pool.loadNewDeck(deckSpec);
			loadNextKanji();
		}
	}
	private Hashtable<String,Example> loadExampleHash(String filename, String encoding) {
		InFile infile = new JarInFile(filename, encoding);
		Example example;
		int i=0;
		Hashtable<String,Example> exampleHash = new Hashtable<String,Example>();
		String line = infile.readLine();
		while( line!= null ) {
			example = new Example(line);
			exampleHash.put(example.inKanji,example);
			line = infile.readLine();
		}
		return exampleHash;
	}
	private Hashtable<String,Kanji> loadKanjiHash(String filename, String encoding) {
		Hashtable<String,Kanji> kanjiHash = new Hashtable<String,Kanji>();
		InFile infile = new JarInFile(filename, encoding);
		Kanji kanji;
		Example example;
		int i=0;
		String line = infile.readLine();
		while( line!= null ) {
			kanji = new Kanji(line);
			kanjiHash.put(kanji.kanjiChar,kanji);
			line = infile.readLine();
		}
		return kanjiHash;
	}

	Profile loadProfile() throws BadProfileException {
		String filename = resources.getString("PROFILE_FILENAME");
		String encoding = resources.getString("PROFILE_FILE_ENCODING");
		InFile infile = new InFile(filename,encoding);
		int lineNumber = 0;
		String[] parts;
		String line = infile.readLine();
		lineNumber++;
		if( !line.trim().contains("Version 1.0.0") ) {
			System.err.format("'%s'\n",(int)(line.charAt(0)));
			throw new BadProfileException(resources.getString("ERROR_BAD_VERSION"));
		}

		// Read the PoolSpec
		line = infile.readLine();
		lineNumber++;
		if( line==null || line.trim().length()==0 )
			throw new BadProfileException(resources.getString("ERROR_POOLSPEC_EMPTY"));
		parts = line.split("\t");
		if( parts[0].equals(PoolSpec.STRING_TAG)==false )
			throw new BadProfileException(resources.getString("ERROR_POOLSPEC_TAG_MISSING"));
		try {
			poolSpec = new PoolSpec(line);
		} catch( ArrayIndexOutOfBoundsException e ) {
			throw new BadProfileException(resources.getString("ERROR_POOLSPEC_DATA"));
		}

		// Read the DeckSpec
		line = infile.readLine();
		lineNumber++;
		if( line==null || line.trim().length()==0 )
			throw new BadProfileException(resources.getString("ERROR_EMPTY_DECKSPEC_LINE"));
		parts = line.split("\t");
		if( parts[0].equals(DeckSpec.STRING_TAG)==false )
			throw new BadProfileException(resources.getString("ERROR_DECKSPEC_TAG_MISSING"));
		try {
			deckSpec = new DeckSpec(line);
		} catch( ArrayIndexOutOfBoundsException e ) {
			throw new BadProfileException(resources.getString("ERROR_DECKSPEC_PROBLEM"));
		}

		// Read the Options
		line = infile.readLine();
		lineNumber++;
		if( line==null || line.trim().length()==0 )
			throw new BadProfileException(resources.getString("ERROR_EMPTY_OPTIONS_LINE"));
		parts = line.split("\t");
		if( parts[0].equals(Options.STRING_TAG)==false )
			throw new BadProfileException(resources.getString("ERROR_OPTIONS_LINE_MISSING"));
		try {
			options = new Options(line);
		} catch( ArrayIndexOutOfBoundsException e ) {
			throw new BadProfileException(resources.getString("ERROR_OPTIONS_PROBLEM"));
		}

		// Read the Use Calendar
		line = infile.readLine();
		lineNumber++;
		if( line==null || line.trim().length()==0 )
			throw new BadProfileException(resources.getString("ERROR_EMPTY_CALENDAR"));
		parts = line.split("\t");
		if( parts[0].equals(UseCalendar.STRING_TAG)==false )
			throw new BadProfileException(resources.getString("ERROR_CALENDAR_TAG_MISSING"));
		try {
			useCalendar = new UseCalendar(line);
		} catch( ArrayIndexOutOfBoundsException e ) {
			e.printStackTrace();
			throw new BadProfileException(resources.getString("ERROR_CALENDAR_PROBLEM"));
		}

		// Read the performance data
		Hashtable<String,KanjiPerformanceDetail> performanceHash = new Hashtable<String,KanjiPerformanceDetail>();
		line = infile.readLine();
		lineNumber++;
		while( line!=null ) {
			if( line.trim().length()>0) {
				if( Japanese.charIsKanji(line.charAt(0))==false )
					throw new BadProfileException(resources.getString("ERROR_PERFORMANCE_BAD_CHAR"));
				String kanjiChar = line.substring(0,1);
				KanjiPerformanceDetail kpd = new KanjiPerformanceDetail(line);
				performanceHash.put(kanjiChar,kpd);
				line = infile.readLine();
			}
		}
		infile.close();
		return new Profile(performanceHash,deckSpec,poolSpec,options,useCalendar);
	}
	public void saveProfile() {
		String filename = resources.getString("PROFILE_FILENAME");
		String encoding = resources.getString("PROFILE_FILE_ENCODING");
		OutFile outfile = new OutFile(filename,encoding);
		outfile.writeLine(resources.getString("VERSION"));
		outfile.writeLine(poolSpec.toString());
		outfile.writeLine(deckSpec.toString());
		outfile.writeLine(options.toString());
		outfile.writeLine(useCalendar.toString());
		Enumeration keys = performanceHash.keys();
		String kanjiChar;
		KanjiPerformanceDetail kpd;
		while( keys.hasMoreElements() ) {
			kanjiChar = (String)keys.nextElement();
			kpd = performanceHash.get(kanjiChar);
			outfile.writeLine(kpd.toString(kanjiChar));
		}
		outfile.close();
	}
}


class Deck {
	AbstractList<Kanji> kanjiList;
	ListIterator<Kanji> listIterator;
	Deck(AbstractList<Kanji> kanjiList) {
		this.kanjiList = kanjiList;
		this.listIterator = kanjiList.listIterator();
	}
	void markCurrentAsRight() {
		listIterator.remove();
	}
	Kanji getNext() {
		if( listIterator.hasNext() )
			return listIterator.next();
		else if( kanjiList.size()>0 ) {
			this.listIterator = kanjiList.listIterator();
			return listIterator.next();
		}
		else {
			return null;
		}
	}
}

class DeckSpec {
	static final int INCREMENT_BY_FREQUENCY = 1;
	static final int INCREMENT_RANDOMLY = 2;
	public static final String STRING_TAG = "DeckSpec";
	static final int MAX_KANJI_COUNT = 999;
	static final int MIN_KANJI_COUNT = 0;

	int unseenCount, seenCount;
	int incrementMethod;
	int wrongALotTries, wrongFirstTimePercent, haveNotSeenLatelyDays;
	boolean wrongLastTimeToggle, wrongALotToggle, wrongFirstShowToggle, haveNotSeenLatelyToggle;

	public DeckSpec() {
		unseenCount=15;
		incrementMethod=INCREMENT_RANDOMLY;
		seenCount=10;
		wrongALotTries=2;
		wrongFirstTimePercent=50;
		haveNotSeenLatelyDays=20;
		wrongLastTimeToggle=true;
		wrongALotToggle=false;
		wrongFirstShowToggle=false;
		haveNotSeenLatelyToggle=false;
	}

	public String toString() {
		String result = String.format("%s\t%d\t%d\t%d\t%d\t%d\t%d\t%b\t%b\t%b\t%b",
			STRING_TAG,
			unseenCount,
			incrementMethod,
			seenCount,
			wrongALotTries,
			wrongFirstTimePercent,
			haveNotSeenLatelyDays,
			wrongLastTimeToggle,
			wrongALotToggle,
			wrongFirstShowToggle,
			haveNotSeenLatelyToggle );
		return result;
	}
	public DeckSpec(String stringForm) {
		String[] parts = stringForm.split("\t");
		assert parts[0].equals(STRING_TAG);
		unseenCount = Integer.parseInt(parts[1]);
		incrementMethod = Integer.parseInt(parts[2]);
		seenCount = Integer.parseInt(parts[3]);
		wrongALotTries = Integer.parseInt(parts[4]);
		wrongFirstTimePercent = Integer.parseInt(parts[5]);
		haveNotSeenLatelyDays = Integer.parseInt(parts[6]);
		wrongLastTimeToggle = Boolean.parseBoolean(parts[7]);
		wrongALotToggle = Boolean.parseBoolean(parts[8]);
		wrongFirstShowToggle = Boolean.parseBoolean(parts[9]);
		haveNotSeenLatelyToggle = Boolean.parseBoolean(parts[10]);
	}
}

class Pool {
	Hashtable<String,Kanji> kanjiHash;
	Hashtable<String,Example> exampleHash;
	Hashtable<String,KanjiPerformanceDetail> performanceHash;
	Hashtable<String,Date> lastSeenHash;
	PoolSpec poolSpec;
	private AbstractList<Kanji> kanjiList; // kanji that are in the pool
	private AbstractList<Kanji> originalDeckContents;
	Kanji currentKanji;
	Deck deck;
	DeckSpec deckSpec;
	ResourceBundle resources;

	// Stats on the current drill
	int totalKanjiThisDeck;  // that's before any were removed
	int totalRight; // number that have been gotten right so far in this deck
	int totalWrong; // number that have been gotten wrong so far in this deck
	int totalRightFirstTime; // number that have been gotten right the first time in this deck
	int getTotalKanjiInCurrentDeck() { return totalKanjiThisDeck; }

	float getAverageShowsToRight() {
		int totalKanji = 0;
		int totalShows = 0;
		for( Kanji kanji : originalDeckContents ) {
			KanjiPerformanceDetail kpd = performanceHash.get(kanji.kanjiChar);
			assert kpd.wasRightLastTimeSeen() : 
					"(Error #0458) There seems to be a problem with a kanji that means: "+kanji.meanings[0];
			if( kpd.wasRightLastTimeSeen() ) {
				totalKanji++;
				totalShows += kpd.getTimesSeenThisTime();
			}
		}
		return ((float)totalShows)/totalKanji;
	}

	
	void loadNewDeck(DeckSpec deckSpec) {
		deck = chooseDeck(deckSpec);
		originalDeckContents = new LinkedList<Kanji>();
		originalDeckContents.addAll(deck.kanjiList);

		// Update the last seen hash to indicate when it was seen prior to being chosen this time.
		// Also, update the times it has been seen "this time".
		KanjiPerformanceDetail kpd;
		lastSeenHash = new Hashtable<String,Date>();
		for( Kanji kanji : originalDeckContents ) {
			kpd = performanceHash.get(kanji.kanjiChar);
			assert kpd != null;
			lastSeenHash.put(kanji.kanjiChar, kpd.getLastSeen());
			kpd.resetThisTimeData();
		}

		totalKanjiThisDeck = deck.kanjiList.size();
		totalRight = 0;
		totalWrong = 0;
		totalRightFirstTime = 0;
		this.deckSpec = deckSpec;
	}

	Kanji getNext() {
		currentKanji = deck.getNext();
		return currentKanji;
	}

	public Deck chooseDeck(DeckSpec deckSpec) {
		AbstractList<Kanji> deckContents = new LinkedList<Kanji>();
		Collections.shuffle(kanjiList); // a little suspect.  Can I really do this without side effects?  I think so.
		int addedSoFar = 0;

		// Choose new kanji (aka "unseen" kanji)
		if( deckSpec.unseenCount>0 ) {
			// Sort the list of kanji in the pool by frequency or randomly, depending on the user's preference.
			if( deckSpec.incrementMethod==DeckSpec.INCREMENT_BY_FREQUENCY ) {
				Comparator<Kanji> cmp = new Comparator<Kanji>() {
					public int compare(Kanji k1, Kanji k2) {
						return Integer.valueOf(k1.getFrequency()).compareTo(Integer.valueOf(k2.getFrequency()));
					}
				};
				Collections.sort(kanjiList,cmp);
			}
			else if( deckSpec.incrementMethod==DeckSpec.INCREMENT_RANDOMLY ) {
				Collections.shuffle(kanjiList);
			}
			// Add the specified number of new kanji, starting at the front of the sorted list.
			addedSoFar = 0;
			for( Kanji kanji : kanjiList ) {
				if( kanjiIsKnown(kanji)==false ) {
					deckContents.add(kanji);
					addedSoFar++;
					if( addedSoFar==deckSpec.unseenCount )
						break;
				}		
			}
		}
		// Add the specified number of "new" (aka "seen") kanji.
		if( deckSpec.seenCount>0 ) {
			addedSoFar = 0;
			for( Kanji kanji : kanjiList ) {
				if( kanjiIsKnown(kanji)==true ) {
					boolean isWrongLastTime=false, isWrongFirstTime=false, isWrongALot=false, isHaveNotSeenLately=false;
					KanjiPerformanceDetail kpd = performanceHash.get(kanji.kanjiChar);
					isWrongLastTime = !kpd.wasRightFirstTimeLastTime();
					isWrongFirstTime = kpd.percentOfTheTimeRightOnFirstShot()<deckSpec.wrongFirstTimePercent;
					isWrongALot = kpd.timesToGetRight() > deckSpec.wrongALotTries;
					isHaveNotSeenLately = kpd.daysSinceLastSeen()>deckSpec.haveNotSeenLatelyDays;
					if( deckSpec.wrongLastTimeToggle && isWrongLastTime ||
							deckSpec.wrongFirstShowToggle && isWrongFirstTime ||
							deckSpec.wrongALotToggle && isWrongALot ||
							deckSpec.haveNotSeenLatelyToggle && isHaveNotSeenLately ) {
						deckContents.add(kanji);
						addedSoFar++;
						if( addedSoFar==deckSpec.seenCount )
							break;
					}
				}		
			}
		}
		Collections.shuffle(deckContents);
		return new Deck(deckContents);
	}
	String statisticsMessage() {
		long beforeTime = new Date().getTime();
		String msg = resources.getString("STATISTICS1"); // "Statistics\n\n"
		KanjiPerformanceDetail kpd;
		msg += resources.getString("STATISTICS2"); // "Progress toward your long-term goal:\n\n"
		int seenCount=0, unseenCount=0, rightFirstTimeLastTimeCount=0;
		for( Kanji kanji : kanjiList ) {
			kpd = performanceHash.get(kanji.kanjiChar);
			assert poolSpec.isIncluded(kanji) : "Expected that every kanji in kanjiList would be part of the pool";
			if( kpd.isKnown() )
				seenCount++;
			else
				unseenCount++;
			if( kpd.wasRightFirstTimeLastTime() ) {
				rightFirstTimeLastTimeCount++;
			}
		}
		msg += String.format(resources.getString("STATISTICS3"),seenCount+unseenCount);//"Total kanji in your long-term goal: %d\n"
		msg += String.format(resources.getString("STATISTICS4"),unseenCount);//"Not seen yet: %d\n"
		msg += String.format(resources.getString("STATISTICS5"),seenCount);//"Seen at least once: %d\n"
		msg += String.format(resources.getString("STATISTICS6"),rightFirstTimeLastTimeCount);//"Right on the first showing, last time you saw them: %d\n"
		msg += "\n\n";
			
		msg += resources.getString("STATISTICS7");
		msg += resources.getString("STATISTICS8");//"Kanji\tToday\tStatus\tInitial\tOverall\tLast seen\t\t(See explanation below)\n"
		int i=0;
		SimpleDateFormat sdf = new SimpleDateFormat("MM-dd-yyyy hh:mm a");
		String dateMsg;
		Date lastSeen;
		for( Kanji kanji : originalDeckContents ) {
			kpd = performanceHash.get(kanji.kanjiChar);
			lastSeen = lastSeenHash.get(kanji.kanjiChar);
			if( lastSeen.getTime()==0 )
				dateMsg = "never";
			else {
				dateMsg = sdf.format(lastSeen);
				if( dateMsg.charAt(11)=='0' )
					dateMsg = dateMsg.substring(0,11)+" "+dateMsg.substring(12);
				if( dateMsg.charAt(0)=='0' )
					dateMsg = dateMsg.substring(1);
			}
			msg += String.format(resources.getString("STATISTICS9"),
									++i,
									kanji.kanjiChar,
									kpd.getTimesSeenThisTime(),
									kpd.wasRightLastTimeSeen() ? resources.getString("STATISTICS10") : resources.getString("STATISTICS11"),
									kpd.getTimesRightFirstTimeAllTime(),
									kpd.getTimesDrawn(),
									kpd.getTimesRightAllTime(),
									kpd.getTimesSeenAllTime(),
									dateMsg
									);
		}
		msg += resources.getString("STATISTICS12");//"Initial - Times you've gotten this right the first time it was shown versus times it has been included.\n";
		msg += resources.getString("STATISTICS13");//"Overal - Times you've marked it as right, versus total times it has ever been presented to you.\n";
		msg += resources.getString("STATISTICS14");//"Today - Times you have attempted it today.\n";
		msg += resources.getString("STATISTICS15");//"Status - Tells whether or not you've marked it as Right today.\n";
		msg += resources.getString("STATISTICS16");//"Last seen - Last time this kanji was studied, prior to this time.\n";
		msg += String.format(resources.getString("STATISTICS17"),(new Date()).getTime()-beforeTime);//"Calculating these statistics took %d milliseconds."
		return msg;
	}
	String statusMessage() {
		int notAttempted = totalKanjiThisDeck-totalRight-totalWrong;
		return String.format(resources.getString("STATUS_BAR_MSG"),
								totalRight, totalRightFirstTime, totalWrong, notAttempted);
		//"%d total right (%d right the first time), %d still wrong, %d not attempted yet"
	}
	boolean kanjiIsKnown(Kanji kanji) {
		return performanceHash.get(kanji.kanjiChar).isKnown();
	}
	boolean kanjiIsPreviouslyKnown(Kanji kanji) {
		return performanceHash.get(kanji.kanjiChar).isPreviouslyKnown();
	}

	void markCurrentAsWrong() {
		KanjiPerformanceDetail kpd = performanceHash.get(currentKanji.kanjiChar);
		if( kpd.getTimesWrongThisTime()==0 ) {
			totalWrong++;
		}
		kpd.gotWrong();
	}
	void markCurrentAsRight() {
		KanjiPerformanceDetail kpd = performanceHash.get(currentKanji.kanjiChar);
		if( kpd.timesWrongThisTime==0 ) {
			totalRightFirstTime++;
			kpd.gotRightFirstTime();
		}
		else {
			totalWrong--;
			kpd.gotRight();
		}
		deck.markCurrentAsRight();
		totalRight++;
	}
	
	public Example[] getExamples(String kanjiChar) {
		LinkedList<Example> list = new LinkedList<Example>();
		String[] kanjiVersions = kanjiHash.get(kanjiChar).getExamples();
		Example example;
		for( String kanjiVersion : kanjiVersions ) {
			example = exampleHash.get(kanjiVersion);
			if( example!=null ) {
				list.add(example);
			}
		}
		return list.toArray(new Example[list.size()]);
	}


	public Pool(ResourceBundle resources, Profile profile, 
				Hashtable<String,Kanji> kanjiHash, 
				Hashtable<String,Example> exampleHash) {
		Kanji kanji;
		String kanjiChar;
		this.resources = resources;
		this.poolSpec = profile.poolSpec;
		this.deckSpec = profile.deckSpec;
		this.kanjiHash = kanjiHash;
		this.exampleHash = exampleHash;
		this.performanceHash = profile.performanceHash;
		this.kanjiList = new LinkedList<Kanji>();
		Enumeration keys = kanjiHash.keys();
		while( keys.hasMoreElements() ) {
			kanji = kanjiHash.get(keys.nextElement());
			kanjiChar = kanji.kanjiChar;
			if(poolSpec.isIncluded(kanji))
				kanjiList.add(kanji);
			if( performanceHash.containsKey(kanjiChar)==false )
				performanceHash.put(kanjiChar, new KanjiPerformanceDetail());
		}
		loadNewDeck(this.deckSpec);
	}
	public void updateWithNewPoolSpec(PoolSpec poolSpec) {
		Kanji kanji;
		String kanjiChar;
		this.poolSpec = poolSpec;
		Enumeration keys = kanjiHash.keys();
		kanjiList.clear();
		while( keys.hasMoreElements() ) {
			kanji = kanjiHash.get(keys.nextElement());
			kanjiChar = kanji.kanjiChar;
			if(poolSpec.isIncluded(kanji))
				kanjiList.add(kanji);
		}
		loadNewDeck(this.deckSpec);
	}
}

class Profile {
	Hashtable<String,KanjiPerformanceDetail> performanceHash;
	DeckSpec deckSpec;
	PoolSpec poolSpec;
	Options options;
	UseCalendar useCalendar;
	Profile( Hashtable<String,KanjiPerformanceDetail> performanceHash,
			DeckSpec deckSpec, PoolSpec poolSpec, Options options, UseCalendar useCalendar) {
		this.performanceHash = performanceHash;
		this.deckSpec = deckSpec;
		this.poolSpec = poolSpec;
		this.options = options;
		this.useCalendar = useCalendar;
	}
}

class PoolSpec {
	int minJoyo, maxJoyo, minFreq, maxFreq, minJlpt, maxJlpt;
	boolean useJoyo, useFreq, useJlpt, useImported;
	int logical;
	String importedKanji;
	public static final String STRING_TAG = "PoolSpec";
	public String toString() {
		return String.format("%s\t%b\t%d\t%d\t%b\t%d\t%d\t%b\t%d\t%d\t%b\t%s\t%d;",
			STRING_TAG,
			useJoyo,minJoyo,maxJoyo,
			useFreq,minFreq,maxFreq,
			useJlpt,minJlpt,maxJlpt,
			useImported, importedKanji,
			logical
			);
	}

	public PoolSpec(String stringForm) {
		String[] parts = stringForm.split("\t");
		assert parts[0].equals(STRING_TAG);
		useJoyo = Boolean.parseBoolean(parts[1]);
		minJoyo = Integer.parseInt(parts[2]);
		maxJoyo = Integer.parseInt(parts[3]);
		useFreq = Boolean.parseBoolean(parts[4]);
		minFreq = Integer.parseInt(parts[5]);
		maxFreq = Integer.parseInt(parts[6]);
		useJlpt = Boolean.parseBoolean(parts[7]);
		minJlpt = Integer.parseInt(parts[8]);
		maxJlpt = Integer.parseInt(parts[9]);
		useImported = Boolean.parseBoolean(parts[10]);
		importedKanji = parts[11];
		logical = Integer.parseInt(parts[12].substring(0,parts[12].length()-1));
	}
	
	public static final int LOGICAL_ANY = 1;
	public static final int LOGICAL_ALL= 2;
	public static final int MAX_ALLOWABLE_FREQ = 2495;
	public static final int MIN_ALLOWABLE_FREQ = 1;

	public boolean isIncluded(Kanji kanji) {
		int joyo = kanji.getJoyo();
		int freq = kanji.getFrequency();
		int jlpt = kanji.getJlptLevel();
		boolean result = false;
		boolean joyoPart = joyo>= minJoyo && joyo<=maxJoyo;
		boolean freqPart = freq>= minFreq && freq<=maxFreq;
		boolean jlptPart = jlpt>= minJlpt && jlpt<=maxJlpt;
		boolean importPart=importedKanji.contains(kanji.kanjiChar);
		if(!(useJoyo || useJlpt || useFreq || useImported))
			return false;
		if( logical==LOGICAL_ANY )
			result = joyoPart&&useJoyo || freqPart&&useFreq || jlptPart&&useJlpt || importPart&&useImported;
		else {
			assert logical==LOGICAL_ALL;
			result = (!useJoyo||joyoPart) && (!useFreq||freqPart) && (!useJlpt||jlptPart) && (!useImported||importPart);
		}
		return result;
	}

	public PoolSpec() { // make a default PoolSpec
		this(true,3,5,false,1,2495,false,4,1,false,"",LOGICAL_ALL);
	}

	public PoolSpec(boolean useJoyo, int minJoyo, int maxJoyo,
					boolean useFreq, int minFreq, int maxFreq,
					boolean useJlpt, int minJlpt, int maxJlpt,
					boolean useImported, String importedKanji,
					int logical) {
		this.useJoyo = useJoyo;
		this.minJoyo = minJoyo;
		this.maxJoyo = maxJoyo;
		this.useFreq = useFreq;
		this.minFreq = minFreq;
		this.maxFreq = maxFreq;
		this.useJlpt = useJlpt;
		this.minJlpt = minJlpt;
		this.maxJlpt = maxJlpt;
		this.useImported = useImported;
		this.importedKanji = importedKanji;
		this.logical = logical;
	}
}

interface KTUpdateListener {
	public void deckSpecUpdated();
	public void poolSpecUpdated();
	public void optionsUpdated();
}

class DeckSpecDialog extends JDialog implements ChangeListener, ActionListener {
	JPanel panel;
	JButton okButton, cancelButton;
	static final boolean DISABLE_LABELS_WITH_CHECKBOXES = false;

	// These controls contain the real data.
	JRadioButton byFreqRadioButton, byRandomRadioButton;
	JCheckBox wrongLastTimeToggle, wrongFirstShowToggle, wrongALotToggle, haveNotSeenLatelyToggle;
	JSpinner seenCounter, unseenCounter, wrongFirstTimePercent, wrongALotTries, haveNotSeenLatelyDays;
	JLabel wrongLastTimeLabel, wrongFirstShowLabel1, wrongFirstShowLabel2, wrongALotLabel1, wrongALotLabel2;
	JLabel haveNotSeenLatelyLabel1, haveNotSeenLatelyLabel2;

	KTUpdateListener ktUpdateListener;
	DeckSpec deckSpec;

	private void enableWrongLastTimeControls(boolean b) {
		if( DISABLE_LABELS_WITH_CHECKBOXES ) {
			wrongLastTimeLabel.setEnabled(b);
		}
	}
	private void enableWrongFirstShowControls(boolean b) {
		if( DISABLE_LABELS_WITH_CHECKBOXES ) {
			wrongFirstShowLabel1.setEnabled(b);
			wrongFirstShowLabel2.setEnabled(b);
		}
		wrongFirstTimePercent.setEnabled(b);
	}
	private void enableWrongALotControls(boolean b) {
		if( DISABLE_LABELS_WITH_CHECKBOXES ) {
			wrongALotLabel1.setEnabled(b);
			wrongALotLabel2.setEnabled(b);
		}
		wrongALotTries.setEnabled(b);
	}
	private void enableHaveNotSeenLatelyControls(boolean b) {
		if( DISABLE_LABELS_WITH_CHECKBOXES ) {
			haveNotSeenLatelyLabel1.setEnabled(b);
			haveNotSeenLatelyLabel2.setEnabled(b);
		}
		haveNotSeenLatelyDays.setEnabled(b);
	}
	private void enableSeenControls(boolean b) {
		wrongLastTimeToggle.setEnabled(b);
		wrongFirstShowToggle.setEnabled(b);
		wrongALotToggle.setEnabled(b);
		haveNotSeenLatelyToggle.setEnabled(b);

		enableWrongLastTimeControls(b);
		enableWrongFirstShowControls(b);
		enableWrongALotControls(b);
		enableHaveNotSeenLatelyControls(b);
		if( wrongLastTimeToggle.isSelected()==false )
			enableWrongLastTimeControls(false);
		if( wrongFirstShowToggle.isSelected()==false )
			enableWrongFirstShowControls(false);
		if( wrongALotToggle.isSelected()==false )
			enableWrongALotControls(false);
		if( haveNotSeenLatelyToggle.isSelected()==false )
			enableHaveNotSeenLatelyControls(false);
	}
	private void enableUnseenControls(boolean b) {
		byFreqRadioButton.setEnabled(b);
		byRandomRadioButton.setEnabled(b);
	}
/*
	Excerpt from the properties file, just for convenience.
		DECKSPEC_TITLE=Specify how to choose the kanji to study today
		DECKSPEC_TOP_MSG=Deck Selection
		DECKSPEC_UNSEEN_LABEL1=Include
		DECKSPEC_UNSEEN_LABEL2=kanji that I've never seen
		DECKSPEC_WRONGLASTTIME_LABEL=kanji I got wrong last time I saw them
		DECKSPEC_WRONGFIRSTSHOW_LABEL1=kanji I got right on the first try, at least
		DECKSPEC_WRONGFIRSTSHOW_LABEL2=percent of the time
		DECKSPEC_WRONGALOT_LABEL1=kanji that, on average, take me at least
		DECKSPEC_WRONGALOT_LABEL2=tries to get right
		DECKSPEC_HAVENOTSEENLATELY_LABEL1=kanji I haven't seen in at least
		DECKSPEC_HAVENOTSEENLATELY_LABEL2=days
		DECKSPEC_SEEN_LABEL1=Include
		DECKSPEC_SEEN_LABEL2=kanji that I need to review, taken from the following:
		DECKSPEC_BY_RANDOM_RADIO_BUTTON=Choose at random
		DECKSPEC_BY_FREQ_RADIO_BUTTON=Choose in order of frequency ranking
*/
	public void stateChanged( ChangeEvent e ) {
		JSpinner spinner = (JSpinner)e.getSource();
		boolean isPositive = ((Integer)spinner.getValue()).intValue()>0;
		if( e.getSource()==unseenCounter ) {
			enableUnseenControls(isPositive);
		}
		else if( e.getSource()==seenCounter ) {
			enableSeenControls(isPositive);
		}
	}
	public void actionPerformed( ActionEvent e ) {
		JCheckBox cb = (JCheckBox)e.getSource();
		boolean isChecked = cb.isSelected();
		if( cb==wrongLastTimeToggle )
			enableWrongLastTimeControls(isChecked);
		else if( cb==wrongFirstShowToggle )
			enableWrongFirstShowControls(isChecked);
		else if( cb==wrongALotToggle )
			enableWrongALotControls(isChecked);
		else if( cb==haveNotSeenLatelyToggle )
			enableHaveNotSeenLatelyControls(isChecked);
		else {
			assert false : "actionPerformed got called one something we didn't expect";
		}
	}
	DeckSpecDialog(JFrame frame,ResourceBundle resources,DeckSpec deckSpec,KTUpdateListener ktUpdateListener) {
		super(frame,true);
		JPanel innerPanel;
		this.deckSpec = deckSpec;
		this.ktUpdateListener = ktUpdateListener;

		setTitle(resources.getString("DECKSPEC_TITLE"));
		
		// Make the main panel which will hold everything.
		panel = new JPanel();
		panel.setLayout(new BoxLayout(panel,BoxLayout.Y_AXIS));

		// Add a title to the top
		innerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
		innerPanel.add(new JLabel(resources.getString("DECKSPEC_TOP_MSG")));
		panel.add(innerPanel);

		//////////////////
		// Add the section for selecting kanji that you haven't seen yet.
		ButtonGroup bg = new ButtonGroup(); // for the increment by frequency/random radio buttons
		
		innerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
		unseenCounter = new JSpinner(new SpinnerNumberModel(deckSpec.unseenCount,
			DeckSpec.MIN_KANJI_COUNT, DeckSpec.MAX_KANJI_COUNT,1));
		unseenCounter.addChangeListener( this );
		innerPanel.add(new JLabel(resources.getString("DECKSPEC_UNSEEN_LABEL1")));
		innerPanel.add(unseenCounter);
		innerPanel.add(new JLabel(resources.getString("DECKSPEC_UNSEEN_LABEL2")));
		panel.add(innerPanel);

		// increment by frequency radio button
		innerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
		byFreqRadioButton = new JRadioButton(resources.getString("DECKSPEC_BY_FREQ_RADIO_BUTTON"));
		byFreqRadioButton.setSelected(deckSpec.incrementMethod==DeckSpec.INCREMENT_BY_FREQUENCY);
		bg.add(byFreqRadioButton);
		innerPanel.add(byFreqRadioButton);

		// increment by random radio button
		byRandomRadioButton = new JRadioButton(resources.getString("DECKSPEC_BY_RANDOM_RADIO_BUTTON"));
		byRandomRadioButton.setSelected(deckSpec.incrementMethod==DeckSpec.INCREMENT_RANDOMLY);
		bg.add(byRandomRadioButton);
		innerPanel.add(byRandomRadioButton);
		panel.add(innerPanel);


		// make a divider
		innerPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
		innerPanel.add(new JLabel(" "));
		innerPanel.add(new JLabel(resources.getString("DECKSPEC_DIVIDER")));
		innerPanel.add(new JLabel(" "));
		panel.add(innerPanel);


		//////////////////
		// Add the section for selecting kanji that you haven't seen yet.
		innerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
		innerPanel.add(new JLabel(resources.getString("DECKSPEC_SEEN_LABEL1")));
		seenCounter = new JSpinner(new SpinnerNumberModel(deckSpec.seenCount,
			DeckSpec.MIN_KANJI_COUNT, DeckSpec.MAX_KANJI_COUNT,1));
		seenCounter.addChangeListener(this);
		innerPanel.add(seenCounter);
		innerPanel.add(new JLabel(resources.getString("DECKSPEC_SEEN_LABEL2")));
		panel.add(innerPanel);

		innerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
		wrongLastTimeToggle = new JCheckBox();
		wrongLastTimeToggle.addActionListener(this);
		wrongLastTimeToggle.setSelected(deckSpec.wrongLastTimeToggle);
		innerPanel.add(wrongLastTimeToggle);
		wrongLastTimeLabel = new JLabel(resources.getString("DECKSPEC_WRONGLASTTIME_LABEL"));
		innerPanel.add(wrongLastTimeLabel);
		panel.add(innerPanel);

		innerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
		wrongFirstShowToggle = new JCheckBox();
		wrongFirstShowToggle.addActionListener(this);
		wrongFirstShowToggle.setSelected(deckSpec.wrongFirstShowToggle);
		innerPanel.add(wrongFirstShowToggle);
		wrongFirstShowLabel1 = new JLabel(resources.getString("DECKSPEC_WRONGFIRSTSHOW_LABEL1"));
		innerPanel.add(wrongFirstShowLabel1);
		wrongFirstTimePercent = new JSpinner(new SpinnerNumberModel(deckSpec.wrongFirstTimePercent,0,100,5));
		innerPanel.add(wrongFirstTimePercent);
		wrongFirstShowLabel2 = new JLabel(resources.getString("DECKSPEC_WRONGFIRSTSHOW_LABEL2"));
		innerPanel.add(wrongFirstShowLabel2);
		panel.add(innerPanel);

		innerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
		wrongALotToggle = new JCheckBox();
		wrongALotToggle.addActionListener(this);
		wrongALotToggle.setSelected(deckSpec.wrongALotToggle);
		innerPanel.add(wrongALotToggle);
		wrongALotLabel1 = new JLabel(resources.getString("DECKSPEC_WRONGALOT_LABEL1"));
		innerPanel.add(wrongALotLabel1);
		wrongALotTries = new JSpinner(new SpinnerNumberModel(deckSpec.wrongALotTries,1,99,1));
		innerPanel.add(wrongALotTries);
		wrongALotLabel2 = new JLabel(resources.getString("DECKSPEC_WRONGALOT_LABEL2"));
		innerPanel.add(wrongALotLabel2);
		panel.add(innerPanel);

		innerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
		haveNotSeenLatelyToggle = new JCheckBox();
		haveNotSeenLatelyToggle.addActionListener(this);
		haveNotSeenLatelyToggle.setSelected(deckSpec.haveNotSeenLatelyToggle);
		innerPanel.add(haveNotSeenLatelyToggle);
		haveNotSeenLatelyLabel1 = new JLabel(resources.getString("DECKSPEC_HAVENOTSEENLATELY_LABEL1"));
		innerPanel.add(haveNotSeenLatelyLabel1);
		haveNotSeenLatelyDays = new JSpinner(new SpinnerNumberModel(deckSpec.haveNotSeenLatelyDays,
			0, 999,1));
		innerPanel.add(haveNotSeenLatelyDays);
		haveNotSeenLatelyLabel2 = new JLabel(resources.getString("DECKSPEC_HAVENOTSEENLATELY_LABEL2"));
		innerPanel.add(haveNotSeenLatelyLabel2);
		panel.add(innerPanel);

		// Disable some controls, if it makes sense.  For example, if a checkbox is
		// unchecked, then the associated labels and other controls should be disabled.
		// If one of the main two spinners is set to 0, then all the controls under
		// them should also be set to zero.
		if( ((Integer)unseenCounter.getValue()).intValue()==0 )
			enableUnseenControls(false);
		if( ((Integer)seenCounter.getValue()).intValue()==0 )
			enableSeenControls(false);
		else {
			if( wrongLastTimeToggle.isSelected()==false )
				enableWrongLastTimeControls(false);
			if( wrongFirstShowToggle.isSelected()==false )
				enableWrongFirstShowControls(false);
			if( wrongALotToggle.isSelected()==false )
				enableWrongALotControls(false);
			if( haveNotSeenLatelyToggle.isSelected()==false )
				enableHaveNotSeenLatelyControls(false);
		}

		// Add the OK button
		innerPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
		AbstractAction okAction = new AbstractAction(resources.getString("DIALOG_OK")) {
			public void actionPerformed(ActionEvent e) {
				updateDeckSpec();
				setVisible(false);
			}
		};
		okButton = new JButton(okAction);
		okButton.setAction(okAction);
		panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("ENTER"),"ok");
		panel.getActionMap().put("ok",okAction);
		innerPanel.add(okButton);

		// Add the Cancel button
		AbstractAction cancelAction = new AbstractAction(resources.getString("DIALOG_CANCEL")) {
			public void actionPerformed(ActionEvent e) {
				setVisible(false);
			}
		};
		cancelButton = new JButton( cancelAction );
		cancelButton.setAction(cancelAction);
		panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("ESCAPE"),"cancel");
		panel.getActionMap().put("cancel",cancelAction);
		innerPanel.add(cancelButton);
		
		// Add the OK and Cancel buttons to the main panel.
		panel.add(innerPanel);

		// Set up and show the dialog box, itself.
		setContentPane(panel);
		pack();
		setLocationRelativeTo(frame);
		setVisible(true);
	}
	private void updateDeckSpec() {
		deckSpec.unseenCount = ((Integer)this.unseenCounter.getValue()).intValue();
		deckSpec.seenCount = ((Integer)this.seenCounter.getValue()).intValue();
		deckSpec.wrongALotTries = ((Integer)this.wrongALotTries.getValue()).intValue();
		deckSpec.wrongFirstTimePercent = ((Integer)this.wrongFirstTimePercent.getValue()).intValue();
		deckSpec.haveNotSeenLatelyDays = ((Integer)this.haveNotSeenLatelyDays.getValue()).intValue();
		if( byFreqRadioButton.isSelected() ) {
			deckSpec.incrementMethod = DeckSpec.INCREMENT_BY_FREQUENCY;
		}
		else {
			assert byRandomRadioButton.isSelected();
			deckSpec.incrementMethod = DeckSpec.INCREMENT_RANDOMLY;
		}
		deckSpec.wrongALotToggle = this.wrongALotToggle.isSelected();
		deckSpec.wrongFirstShowToggle = this.wrongFirstShowToggle.isSelected();
		deckSpec.haveNotSeenLatelyToggle = this.haveNotSeenLatelyToggle.isSelected();
		deckSpec.wrongLastTimeToggle = this.wrongLastTimeToggle.isSelected();
		ktUpdateListener.deckSpecUpdated();
	}
}

class PoolSpecDialog extends JDialog implements ActionListener {
	JPanel panel;
	JCheckBox useJoyo, useJlpt, useFreq, useImported;
	JComboBox joyoMin, joyoMax, jlptMin, jlptMax;
	JSpinner freqMin, freqMax;
	JButton okButton, cancelButton, importChooseFile, importFromClipboard, importView;
	JTextField fileField;
	JRadioButton logicalAny, logicalAll;
	PoolSpec poolSpec;
	KTUpdateListener ktUpdateListener;
	String importedKanji;
	ResourceBundle resources;
	Options options;
	static final boolean DISABLE_LABELS_WITH_CHECKBOXES = false;

	PoolSpecDialog(JFrame frame,ResourceBundle resources,PoolSpec poolSpec, KTUpdateListener ktUpdateListener,Options options) {
		super(frame,true);
		this.resources = resources;
		this.options = options;
		importedKanji = poolSpec.importedKanji;
		setTitle(resources.getString("POOLSPEC_TITLE"));

		panel = new JPanel();
		panel.setLayout(new BoxLayout(panel,BoxLayout.Y_AXIS));

		this.poolSpec = poolSpec;
		this.ktUpdateListener = ktUpdateListener;

		String[] joyoOptions = resources.getString("POOLSPEC_JOYO_OPTIONS").split(";");
		String[] jlptOptions = resources.getString("POOLSPEC_JLPT_OPTIONS").split(";");
		AbstractAction cancelAction = new AbstractAction(resources.getString("DIALOG_CANCEL")) {
			public void actionPerformed(ActionEvent e) {
				setVisible(false);
				dispose();
			}
		};
		AbstractAction okAction = new AbstractAction(resources.getString("DIALOG_OK")) {
			public void actionPerformed(ActionEvent e) {
				updatePoolSpec();
				setVisible(false);
				dispose();
			}
		};
		// make the important UI components
		joyoMin = new JComboBox(joyoOptions);
		joyoMax = new JComboBox(joyoOptions);
		jlptMin = new JComboBox(jlptOptions);
		jlptMax = new JComboBox(jlptOptions);
		freqMin = new JSpinner(new SpinnerNumberModel(poolSpec.minFreq,
						PoolSpec.MIN_ALLOWABLE_FREQ,PoolSpec.MAX_ALLOWABLE_FREQ,1));
		freqMax = new JSpinner(new SpinnerNumberModel(poolSpec.maxFreq,
						PoolSpec.MIN_ALLOWABLE_FREQ,PoolSpec.MAX_ALLOWABLE_FREQ,1));
		useJoyo = new JCheckBox();
		useJlpt = new JCheckBox();
		useFreq = new JCheckBox();
		logicalAny = new JRadioButton(resources.getString("POOLSPEC_ANY"));
		logicalAll= new JRadioButton(resources.getString("POOLSPEC_ALL"));

		// set the defaults according to what was passed in
		joyoMin.setSelectedIndex( joyoValueToIndex( poolSpec.minJoyo ) );
		joyoMax.setSelectedIndex( joyoValueToIndex( poolSpec.maxJoyo ) );
		jlptMin.setSelectedIndex( jlptValueToIndex( poolSpec.minJlpt ) );
		jlptMax.setSelectedIndex( jlptValueToIndex( poolSpec.maxJlpt ) );
		freqMin.setValue(Integer.valueOf(poolSpec.minFreq));
		freqMax.setValue(Integer.valueOf(poolSpec.maxFreq));
		useJoyo.setSelected(poolSpec.useJoyo);
		useFreq.setSelected(poolSpec.useFreq);
		useJlpt.setSelected(poolSpec.useJlpt);
		logicalAny.setSelected(poolSpec.logical==PoolSpec.LOGICAL_ANY);
		logicalAll.setSelected(poolSpec.logical==PoolSpec.LOGICAL_ALL);

		okButton = new JButton(okAction);
		okButton.setAction(okAction);
		panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("ENTER"),"ok");
		panel.getActionMap().put("ok",okAction);

		cancelButton = new JButton( cancelAction );
		cancelButton.setAction(cancelAction);
		panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("ESCAPE"),"cancel");
		panel.getActionMap().put("cancel",cancelAction);

		JPanel innerPanel = new JPanel( new FlowLayout(FlowLayout.LEFT) );
		innerPanel.add(new JLabel(resources.getString("POOLSPEC_INCLUDE")));
		panel.add(innerPanel);

		addRangeLine(panel,useJoyo,joyoMin,joyoMax,resources,"POOLSPEC_JOYO_START");
		addRangeLine(panel,useJlpt,jlptMin,jlptMax,resources,"POOLSPEC_JLPT_START");
		addRangeLine(panel,useFreq,freqMin,freqMax,resources,"POOLSPEC_FREQ_START");

		innerPanel = new JPanel( new FlowLayout(FlowLayout.LEFT) );
		useImported = new JCheckBox();
		useImported.setSelected(poolSpec.useImported);
		innerPanel.add(useImported);
		innerPanel.add(new JLabel(resources.getString("POOLSPEC_IMPORTED_KANJI")));
		useImported.addActionListener(this);
		panel.add(innerPanel);

		innerPanel = new JPanel( new FlowLayout(FlowLayout.CENTER) );
		importChooseFile = new JButton(resources.getString("POOLSPEC_TEXT_FILE"));
		importChooseFile.addActionListener(this);
		importFromClipboard = new JButton(resources.getString("POOLSPEC_CLIPBOARD"));
		importFromClipboard.addActionListener(this);
		importView = new JButton(resources.getString("POOLSPEC_VIEW_IMPORTED"));
		importView.addActionListener(this);
		innerPanel.add(importChooseFile);
		innerPanel.add(importFromClipboard);
		innerPanel.add(importView);
		panel.add(innerPanel);

		panel.add(Box.createVerticalStrut(10));

		innerPanel = new JPanel( new FlowLayout(FlowLayout.CENTER) );
		innerPanel.add( new JLabel(resources.getString("POOLSPEC_LOGICAL_START")) );
		ButtonGroup bg = new ButtonGroup();
		bg.add(logicalAny);
		innerPanel.add(logicalAny);
		bg.add(logicalAll);
		innerPanel.add(logicalAll);
		innerPanel.add( new JLabel(resources.getString("POOLSPEC_LOGICAL_END")) );
		panel.add(innerPanel);

		innerPanel = new JPanel( new FlowLayout(FlowLayout.CENTER) );
		innerPanel.add(okButton);
		innerPanel.add(cancelButton);
		panel.add(innerPanel);

		shadeCheckboxSiblingsAsNeeded(useImported);
		setContentPane(panel);
		pack();
		setLocationRelativeTo(frame);
		setVisible(true);
	}

	private void addRangeLine(JPanel panel, JCheckBox toggle, Component fromInput, Component toInput,
						ResourceBundle resources, String resourceName) {
		// A helper function to eliminate duplicated code.
		JPanel box = new JPanel();
		box.setLayout( new FlowLayout(FlowLayout.LEFT) );
		toggle.addActionListener(this);
		box.add( toggle );
		box.add( new JLabel(resources.getString(resourceName)) );
		box.add( fromInput );
		box.add( new JLabel(resources.getString("POOLSPEC_TO")) );
		box.add( toInput );
		box.add( new JLabel(resources.getString("POOLSPEC_PERIOD")) );
		box.add( new JPanel() );
		shadeCheckboxSiblingsAsNeeded(toggle);
		panel.add(box);
	}

	void updatePoolSpec() {
		poolSpec.minJoyo = joyoIndexToValue(joyoMin.getSelectedIndex());
		poolSpec.maxJoyo = joyoIndexToValue(joyoMax.getSelectedIndex());
		poolSpec.minFreq = (int)((Integer)freqMin.getValue());
		poolSpec.maxFreq = (int)((Integer)freqMax.getValue());
		poolSpec.minJlpt = jlptIndexToValue(jlptMin.getSelectedIndex());
		poolSpec.maxJlpt = jlptIndexToValue(jlptMax.getSelectedIndex());
		poolSpec.useJoyo = useJoyo.isSelected();
		poolSpec.useFreq = useFreq.isSelected();
		poolSpec.useJlpt = useJlpt.isSelected();
		poolSpec.logical = logicalAny.isSelected() ? PoolSpec.LOGICAL_ANY : PoolSpec.LOGICAL_ALL;
		if( poolSpec.minJoyo>poolSpec.maxJoyo ) {
			int temp = poolSpec.maxJoyo;
			poolSpec.maxJoyo = poolSpec.minJoyo;
			poolSpec.minJoyo = temp;
		}
		if( poolSpec.minFreq>poolSpec.maxFreq ) {
			int temp = poolSpec.maxFreq;
			poolSpec.maxFreq = poolSpec.minFreq;
			poolSpec.minFreq = temp;
		}
		if( poolSpec.minJlpt>poolSpec.maxJlpt ) {
			int temp = poolSpec.maxJlpt;
			poolSpec.maxJlpt = poolSpec.minJlpt;
			poolSpec.minJlpt = temp;
		}
		poolSpec.useImported = useImported.isSelected();
		poolSpec.importedKanji = importedKanji;
		ktUpdateListener.poolSpecUpdated();
	}

	// convert the index into the control to the value to be stored in the poolSpec
	int joyoIndexToValue(int index) { 
		return index<=5 ? index+1 : index+2;
	}
	int joyoValueToIndex(int value) {
		return value<=6 ? value-1 : value-2;
	}
	int jlptIndexToValue(int index) {
		return 4-index;
	}
	int jlptValueToIndex(int value) {
		return 4-value;
	}

	void shadeCheckboxSiblingsAsNeeded(JCheckBox cb) {
		Container parent = cb.getParent();
		Object[] kids = parent.getComponents();
		for( Object kid : kids ) {
			if( kid!=cb ) {
				if( DISABLE_LABELS_WITH_CHECKBOXES || JLabel.class.isInstance(kid)==false) {
					((Component)kid).setEnabled(cb.isSelected());
				}
			}
		}
		if( cb==useImported ) {
			importFromClipboard.setEnabled(cb.isSelected());
			importChooseFile.setEnabled(cb.isSelected());
			importView.setEnabled(cb.isSelected());
		}
	}

	void doFileImport() {
		JFileChooser fc = new JFileChooser();
		FileFilter filterUtf = new FileFilter() {
			public String getDescription() { return resources.getString("FILE_CHOOSER_UTF"); }
			public boolean accept( File f ) { return f.getName().toLowerCase().endsWith(".txt") || f.isDirectory(); }
		};
		FileFilter filterSjis = new FileFilter() {
			public String getDescription() { return resources.getString("FILE_CHOOSER_SJIS"); }
			public boolean accept( File f ) { return f.getName().toLowerCase().endsWith(".txt") || f.isDirectory(); }
		};
		FileFilter filterEuc = new FileFilter() {
			public String getDescription() { return resources.getString("FILE_CHOOSER_EUCJP"); }
			public boolean accept( File f ) { return f.getName().toLowerCase().endsWith(".txt") || f.isDirectory(); }
		};
		fc.addChoosableFileFilter(filterUtf);
		fc.addChoosableFileFilter(filterSjis);
		fc.addChoosableFileFilter(filterEuc);
		fc.setFileFilter(filterUtf);
		int response = fc.showOpenDialog(this);
		try {
			if( response==JFileChooser.APPROVE_OPTION ) {
				FileFilter filter = fc.getFileFilter();
				String encoding = "UTF-8";
				if( filter==filterUtf )
					encoding = "UTF-8";
				else if( filter==filterSjis )
					encoding = "Shift_JIS";
				else if( filter==filterEuc )
					encoding = "EUC-JP";
				String filename = fc.getSelectedFile().getCanonicalPath();
				String contents = InFile.readFileSafe(filename,encoding);
				extractKanji(contents);
			}
		}
		catch( IOException e ) {
			JOptionPane.showMessageDialog(this,
						resources.getString("ERROR_CANNOT_OPEN_FILE_TEXT"),
						resources.getString("ERROR_CANNOT_OPEN_FILE_TITLE"),
						JOptionPane.ERROR_MESSAGE);
		}
	}
	public void doClipboardImport() {
		Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
		try {
			String contents = (String)(clipboard.getData(DataFlavor.stringFlavor));
			extractKanji(contents);
		}
		catch( IOException e ) {
			JOptionPane.showMessageDialog(this,
						resources.getString("ERROR_CANNOT_READ_CLIPBOARD_IO_TEXT"),
						resources.getString("ERROR_CANNOT_READ_CLIPBOARD_IO_TITLE"),
						JOptionPane.ERROR_MESSAGE);
		}
		catch( UnsupportedFlavorException e ) {
			JOptionPane.showMessageDialog(this,
						resources.getString("ERROR_CANNOT_READ_CLIPBOARD_FLAVOR_TEXT"),
						resources.getString("ERROR_CANNOT_READ_CLIPBOARD_FLAVOR_TITLE"),
						JOptionPane.ERROR_MESSAGE);
		}
	}
	public void extractKanji(String contents) {
		contents = contents.trim();
		HashSet<Character> charSet = new HashSet<Character>();
		char ch;
		int i;
		for( i=0; i<contents.length(); i++) {
			ch = contents.charAt(i);
			if( Japanese.charIsKanji(ch) ) {
				charSet.add(ch);
			}
		}
		if( charSet.size()>0 ) {
			LinkedList<Character> chList = new LinkedList<Character>();
			for( Character chInSet : charSet ) {
				chList.add(chInSet);
			}
			Collections.sort(chList);
			importedKanji = "";
			for( Character chInList : chList ) {
				importedKanji += chInList;
			}
			JOptionPane.showMessageDialog(this,
						String.format(resources.getString("POOLSPEC_IMPORT_CONFIRM"),
							importedKanji.length(),importedKanji.length()==1 ? "was" : "were"),
						"Import",
						JOptionPane.ERROR_MESSAGE);
		}
		else {
			JOptionPane.showMessageDialog(this,
						resources.getString("POOLSPEC_IMPORT_NONE_FOUND_TEXT"),
						resources.getString("POOLSPEC_IMPORT_NONE_FOUND_TITLE"),
						JOptionPane.ERROR_MESSAGE);
		}
	}
	public void actionPerformed(ActionEvent e) {
		if( e.getSource()==useJoyo ||
			e.getSource()==useJlpt ||
			e.getSource()==useFreq ||
			e.getSource()==useImported ) {
			JCheckBox cb = (JCheckBox)e.getSource();
			shadeCheckboxSiblingsAsNeeded(cb);
		}
		else if( e.getSource()==importChooseFile ) {
			doFileImport();
		}
		else if( e.getSource()==importView ) {
			viewImportedKanji();
		}
		else if( e.getSource()==importFromClipboard ) {
			doClipboardImport();
		}
	}
	public void viewImportedKanji() {
		ViewKanjiDialog dialog = new ViewKanjiDialog(this,resources.getString("POOLSPEC_VIEW_IMPORTED"),importedKanji,options.getFont(),resources);
	}
}
class ViewKanjiDialog extends JDialog {
	public ViewKanjiDialog(JDialog owner, String title,String importedKanji,String fontName, ResourceBundle resources) {
		super(owner,title,true);
		JPanel panel = new JPanel();

		AbstractAction closeAction = new AbstractAction(resources.getString("DIALOG_OK")) {
			public void actionPerformed(ActionEvent e) {
				setVisible(false);
				dispose();
			}
		};
		panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("ESCAPE"),"close");
		panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("ENTER"),"close");
		panel.getActionMap().put("close",closeAction);
		panel.setLayout(new BoxLayout(panel,BoxLayout.Y_AXIS));

		JTextArea text = new JTextArea(20,60);
		text.setFont(new Font(fontName,Font.PLAIN,14));
		text.setEditable(false);
		text.setLineWrap(true);
		text.setWrapStyleWord(false);
		text.setText(importedKanji);
		JScrollPane scrollPane = new JScrollPane(text);
		panel.add(scrollPane);
		JButton okButton = new JButton("OK");
		okButton.addActionListener( new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				setVisible(false);
				dispose();
			}
		});
		JPanel innerPanel = new JPanel();
		innerPanel.add(new JLabel(String.format("%d total kanji",importedKanji.length())));
		panel.add(innerPanel);
		innerPanel = new JPanel();
		innerPanel.add(okButton);
		panel.add(innerPanel);
		setContentPane(panel);
		pack();
		setLocationRelativeTo(owner);
		setVisible(true);
	}
}

class KanjiPerformanceDetail {
	int timesRightThisTime;
	int timesWrongThisTime;
	int timesRightAllTime;
	int timesWrongAllTime;
	boolean rightLastTimeSeen;
	boolean rightFirstTimeLastTime;
	int timesRightFirstTimeAllTime; 
	int timesDrawn;        
	Date lastSeen;

	// Getters for all fields.
	// 	Since this class basically serves as an organization of a bunch
	//  of facts, it makes sense to have a lot of getters.  The data
	//	will need to be used directly by other classes.
	int getTimesRightThisTime(){return timesRightThisTime;}
	int getTimesWrongThisTime(){return timesWrongThisTime;}
	int getTimesRightAllTime() {return timesRightAllTime;}
	int getTimesWrongAllTime() {return timesWrongAllTime;}
	boolean wasRightLastTimeSeen() {return rightLastTimeSeen;}
	boolean wasRightFirstTimeLastTime() {return rightFirstTimeLastTime;}
	void resetThisTimeData() {
		timesRightThisTime = 0;
		timesWrongThisTime = 0;
	}
	double percentOfTheTimeRightOnFirstShot() {
		if( timesDrawn==0 )
			return 0;
		else
			return (double)timesRightFirstTimeAllTime/(double)timesDrawn*100.0;
	}
	double timesToGetRight() {
		if( timesRightAllTime==0 )
			return 0;
		else
			return (timesRightAllTime+timesWrongAllTime)/timesRightAllTime;
	}
	boolean isKnown() {return getTimesSeenAllTime()>0;}
	boolean isPreviouslyKnown() {
		// Return true, iff the user has marked this kanji as either right or wrong
		// at some time in the past, not including this session.
		return timesRightThisTime+timesWrongThisTime-timesRightAllTime-timesWrongAllTime>0;
	}
	double daysSinceLastSeen() {
		long lastSeenMilliseconds = lastSeen.getTime();
		if( lastSeenMilliseconds==0 ) {
			assert isKnown()==true;
			return -1.0;
		}
		else {
			double daysDiff = ((double)(new Date()).getTime()-(double)lastSeenMilliseconds)/(1000.0*60.0*60.0*24.0);
			assert daysDiff < Integer.MAX_VALUE;
			assert daysDiff > Integer.MIN_VALUE;
			assert daysDiff < 365*10;
			assert daysDiff > 0.0 : String.format("daysDiff=%f, lastSeenMilliseconds=%d, currenttime=%d",daysDiff,lastSeenMilliseconds,(new Date()).getTime());
			return daysDiff;
		}
	}
	int getTimesRightFirstTimeAllTime() {return timesRightFirstTimeAllTime;}
	int getTimesDrawn() {return timesDrawn;}
	Date getLastSeen() {return lastSeen;}

	int getTimesSeenAllTime() {return timesRightAllTime+timesWrongAllTime;}
	int getTimesSeenThisTime(){return timesRightThisTime+timesWrongThisTime;} //doesn't include displayed but not answered
	
	String toString(String kanjiChar) {
		return String.format(
			"%s\t%b\t%d\t%d\t%d\t%d\t%d",
				kanjiChar,
				rightFirstTimeLastTime,
				timesRightAllTime,
				timesWrongAllTime,
				timesRightFirstTimeAllTime,
				timesDrawn,
				lastSeen.getTime()/1000);
	}
	KanjiPerformanceDetail(String stringForm) {
		String[] parts = stringForm.split("\t");
		rightFirstTimeLastTime = Boolean.parseBoolean(parts[1]);
		timesRightAllTime = Integer.parseInt(parts[2]);
		timesWrongAllTime = Integer.parseInt(parts[3]);
		timesRightFirstTimeAllTime = Integer.parseInt(parts[4]);
		timesDrawn = Integer.parseInt(parts[5]);
		lastSeen = new Date(Long.parseLong(parts[6])*1000);
	}
	KanjiPerformanceDetail() {
		timesRightThisTime = 0;
		timesWrongThisTime = 0;
		timesRightAllTime = 0;
		timesWrongAllTime = 0;
		rightLastTimeSeen = false;
		rightFirstTimeLastTime = false;
		timesRightFirstTimeAllTime = 0;
		timesDrawn = 0;
		lastSeen = new Date(0);
	}
	void gotRightFirstTime() {
		timesRightFirstTimeAllTime++;
		rightFirstTimeLastTime = true;
		gotRight();
	}
	void gotWrong() {
		if( timesWrongThisTime+timesRightThisTime==0 )
			timesDrawn++;
		timesWrongThisTime++;
		timesWrongAllTime++;
		rightLastTimeSeen = false;
		rightFirstTimeLastTime = false;
		lastSeen = new Date();
	}
	void gotRight() {
		if( timesWrongThisTime+timesRightThisTime==0 )
			timesDrawn++;
		timesRightThisTime++;
		timesRightAllTime++;
		rightLastTimeSeen = true;
		lastSeen = new Date();
	}
}

class Options {
	String fontName;
	int delaySeconds;
	boolean delayToggle;
	boolean exampleLimitToggle;
	boolean saveChangesWithoutAskingToggle;
	boolean saveChangesWithoutAsking() { return saveChangesWithoutAskingToggle; }
	int exampleLimitCount;
	public final static String STRING_TAG = "Options";
	Options(boolean delayToggle, int delaySeconds, boolean exampleLimitToggle, int exampleLimitCount, boolean saveChangesWithoutAskingToggle, String fontName) {
		this.fontName = fontName;
		this.delaySeconds = delaySeconds;
		this.delayToggle = delayToggle;
		this.saveChangesWithoutAskingToggle = saveChangesWithoutAskingToggle;
		this.exampleLimitCount = exampleLimitCount;
		this.exampleLimitToggle = exampleLimitToggle;
	}
	Options(String stringForm) {
		String[] parts = stringForm.split("\t");
		assert parts[0].equals(STRING_TAG);
		delayToggle = Boolean.parseBoolean(parts[1]);
		delaySeconds = Integer.parseInt(parts[2]);
		exampleLimitToggle = Boolean.parseBoolean(parts[3]);
		exampleLimitCount = Integer.parseInt(parts[4]);
		saveChangesWithoutAskingToggle = Boolean.parseBoolean(parts[5]);
		fontName = parts[6];
	}
	public String toString() {
		return String.format("%s\t%b\t%d\t%b\t%d\t%b\t%s",
				STRING_TAG,
				delayToggle,delaySeconds,
				exampleLimitToggle, exampleLimitCount,
				saveChangesWithoutAskingToggle,
				fontName);
	}
	public boolean delayIsEnabled() {
		return delayToggle;
	}
	public int getDelaySeconds() {
		return delaySeconds;
	}
	public String getFont() {
		return fontName;
	}
	public void setDelayEnabled(boolean enabled) {
		delayToggle = enabled;
	}
	public boolean exampleLimitSet() {
		return exampleLimitToggle;
	}
	public int getExampleLimit() {
		return exampleLimitCount;
	}
}

class OptionsDialog extends JDialog implements ActionListener {
	JPanel panel;
	KTUpdateListener ktUpdateListener;
	JFrame frame;
	ResourceBundle resources;
	Options options;
	JCheckBox delayToggle;
	JSpinner delaySeconds;
	JLabel delayLabel1;
	JLabel delayLabel2;
	JComboBox fontBox;
	JSpinner exampleLimitSpinner;
	JLabel exampleLimitLabel1;
	JLabel exampleLimitLabel2;
	JCheckBox exampleLimitToggle;
	JCheckBox saveChangesToggle;
	JLabel saveChangesLabel;
	static final boolean DISABLE_LABELS_WITH_CHECKBOXES = false;

	private void enableComponentsAsNeeded() {
		boolean delayIsActive = delayToggle.isSelected();
		boolean exampleLimitIsActive = exampleLimitToggle.isSelected();
		delaySeconds.setEnabled(delayIsActive);
		delayLabel2.setEnabled(delayIsActive); // this one gets enabled/disabled regardless because it's a sub-option.
		if( DISABLE_LABELS_WITH_CHECKBOXES ) {
			delayLabel1.setEnabled(delayIsActive);
			exampleLimitLabel1.setEnabled(exampleLimitIsActive);
			exampleLimitLabel2.setEnabled(exampleLimitIsActive);
		}
		exampleLimitSpinner.setEnabled(exampleLimitIsActive);
	}
	public void actionPerformed( ActionEvent e ) {
		enableComponentsAsNeeded();
	}

	OptionsDialog(JFrame frame, ResourceBundle resources, Options options, 
					KTUpdateListener ktUpdateListener, String[] fontList) {
		super(frame,resources.getString("OPTIONS_TITLE"),true);
		this.resources = resources;
		this.options = options;
		this.ktUpdateListener = ktUpdateListener;
		panel = new JPanel();
		panel.setLayout(new BoxLayout(panel,BoxLayout.Y_AXIS));

		JPanel innerPanel;
		
		innerPanel = new JPanel( new FlowLayout(FlowLayout.LEFT));
		delayToggle = new JCheckBox();
		delayToggle.setSelected(options.delayIsEnabled());
		delayToggle.addActionListener(this);
		innerPanel.add(delayToggle);
		delayLabel1 = new JLabel(resources.getString("OPTIONS_ABOUT_DELAY"));
		innerPanel.add(delayLabel1);
		panel.add(innerPanel);

		innerPanel = new JPanel( new FlowLayout(FlowLayout.LEFT));
		innerPanel.add(Box.createHorizontalStrut(30));
		delaySeconds = new JSpinner(new SpinnerNumberModel(options.getDelaySeconds(),1,60,1));
		innerPanel.add(delaySeconds);
		delayLabel2 = new JLabel(resources.getString("OPTIONS_DELAY_SECONDS"));
		innerPanel.add(delayLabel2);
		panel.add(innerPanel);

		panel.add(Box.createVerticalStrut(10));

		innerPanel = new JPanel( new FlowLayout(FlowLayout.LEFT));
		exampleLimitToggle = new JCheckBox();
		exampleLimitToggle.setSelected(options.exampleLimitSet());
		exampleLimitToggle.addActionListener(this);
		innerPanel.add(exampleLimitToggle);
		exampleLimitLabel1 = new JLabel(resources.getString("OPTIONS_EXAMPLE_LIMIT1"));
		innerPanel.add(exampleLimitLabel1);
		exampleLimitSpinner = new JSpinner(new SpinnerNumberModel(options.getExampleLimit(),1,99,1));
		innerPanel.add(exampleLimitSpinner);
		exampleLimitLabel2 = new JLabel(resources.getString("OPTIONS_EXAMPLE_LIMIT2"));
		innerPanel.add(exampleLimitLabel2);
		panel.add(innerPanel);

		panel.add(Box.createVerticalStrut(10));

		innerPanel = new JPanel( new FlowLayout(FlowLayout.LEFT));
		saveChangesToggle = new JCheckBox();
		saveChangesToggle.addActionListener(this);
		saveChangesToggle.setSelected(options.saveChangesWithoutAsking());
		innerPanel.add(saveChangesToggle);
		saveChangesLabel = new JLabel(resources.getString("OPTIONS_AUTOSAVE"));
		innerPanel.add(saveChangesLabel);
		panel.add(innerPanel);

		panel.add(Box.createVerticalStrut(10));

		innerPanel = new JPanel( new FlowLayout(FlowLayout.CENTER));
		innerPanel.add(new JLabel(resources.getString("OPTIONS_FONT")));
		fontBox = new JComboBox(fontList);
		for( int i=0; i<fontList.length; i++ ) {
			if( fontList[i].equals(options.getFont()) ) {
				fontBox.setSelectedIndex(i);
				break;
			}
		}
		innerPanel.add(fontBox);
		panel.add(innerPanel);

		panel.add(Box.createVerticalStrut(10));

		// Actions for the OK and Cancel buttons
		AbstractAction cancelAction = new AbstractAction(resources.getString("DIALOG_CANCEL")) {
			public void actionPerformed(ActionEvent e) {
				setVisible(false);
				dispose();
			}
		};
		AbstractAction okAction = new AbstractAction(resources.getString("DIALOG_OK")) {
			public void actionPerformed(ActionEvent e) {
				updateOptions();
				setVisible(false);
				dispose();
			}
		};

		innerPanel = new JPanel( new FlowLayout(FlowLayout.CENTER));
		JButton okButton = new JButton(okAction);
		okButton.setAction(okAction);
		innerPanel.add(okButton);
		panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("ENTER"),"ok");
		panel.getActionMap().put("ok",okAction);

		JButton cancelButton = new JButton( cancelAction );
		cancelButton.setAction(cancelAction);
		innerPanel.add(cancelButton);
		panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("ESCAPE"),"cancel");
		panel.getActionMap().put("cancel",cancelAction);
		panel.add(innerPanel);

		enableComponentsAsNeeded();
		setContentPane(panel);
		pack();
		setLocationRelativeTo(frame);
		setVisible(true);
	}
	void updateOptions() {
		options.delaySeconds = ((Integer)this.delaySeconds.getValue()).intValue();
		options.delayToggle = this.delayToggle.isSelected();
		options.fontName = (String)(this.fontBox.getSelectedItem());
		options.exampleLimitToggle = this.exampleLimitToggle.isSelected();
		options.exampleLimitCount =  (int)((Integer)(this.exampleLimitSpinner.getValue()));
		options.saveChangesWithoutAskingToggle = saveChangesToggle.isSelected();
		ktUpdateListener.optionsUpdated();
	}
}

class UseCalendar {
	Hashtable<String,DailyUseInfo> useHash;
	public static final String STRING_TAG = "UseCalendar";

	protected static String makeTodayDateString() {
		SimpleDateFormat sdf = new SimpleDateFormat();
		sdf.applyPattern(DailyUseInfo.dateFormat);
		return sdf.format(new Date());
	}
	public UseCalendar() {
		useHash = new Hashtable<String,DailyUseInfo>();
	}
	private void check( boolean condition, String msg ) throws BadProfileException {
		if( condition==false )
			throw new BadProfileException("The usage calendar information is corrupt. ("+msg+")");
	}
	private void check( boolean condition ) throws BadProfileException {
		if( condition==false )
			throw new BadProfileException("The usage calendar information is corrupt.");
	}
	public UseCalendar(String stringForm) throws BadProfileException {
		this();
		String[] lineParts = stringForm.split("\t");
		DailyUseInfo dui;
		int introduced=0;
		int reviewed=0;
		String introducedKanji;
		String reviewedKanji;
		String dateString;
		for( int i=1; i<lineParts.length; i++ ) {
			String[] parts = lineParts[i].concat(";").split("/");
			parts[2] = parts[2].substring(0,parts[2].length()-1);
			check( parts.length==3, "0229");
			dateString = parts[0];
			if( parts[1].length()==0 || Japanese.charIsKanji(parts[1].charAt(0)) ) {
				check( parts[2].length()==0 || Japanese.charIsKanji(parts[2].charAt(0)), "0230" );
				introducedKanji = parts[1];
				reviewedKanji = parts[2];
				introduced = introducedKanji.length();
				reviewed = reviewedKanji.length();
				dui = new DailyUseInfo(dateString,introduced, reviewed, introducedKanji, reviewedKanji);
			}
			else {
				try {
					introduced = Integer.parseInt(parts[1]);
					reviewed = Integer.parseInt(parts[2]);
				}
				catch( NumberFormatException e ) {
					check(false, "introduced/reviewed is not a number");
				}
				dui = new DailyUseInfo(dateString,introduced, reviewed);
			}
			useHash.put(dateString,dui);
		}
	}
	public String toString() {
		String todayDate = makeTodayDateString();
		String result = STRING_TAG;
		Enumeration keys = useHash.keys();
		DailyUseInfo dui;
		LinkedList<String> dateStringList = new LinkedList<String>();
		while( keys.hasMoreElements() ) {
			dateStringList.add((String)(keys.nextElement()));
		}
		Collections.sort(dateStringList);
		for( String dateString : dateStringList ) {
			dui = useHash.get(dateString);
			if( dateString.equals(todayDate) )
				result += String.format("\t%s/%s/%s", dateString,dui.introducedKanji,dui.reviewedKanji);
			else
				result += String.format("\t%s/%d/%d", dateString,dui.introduced,dui.reviewed);
		}
		return result;
	}
	public DailyUseInfo getTodaysDailyUseInfo() {
		DailyUseInfo dui;
		String todayDate = makeTodayDateString();
		if( useHash.containsKey(todayDate)==false ) {
			dui = new DailyUseInfo(todayDate);
			useHash.put(todayDate,dui);
		}
		else {
			dui = useHash.get(todayDate);
		}
		return dui;
	}
	public void kanjiIntroduced(String kanjiChar) {
		getTodaysDailyUseInfo().kanjiIntroduced(kanjiChar);
	}
	public void kanjiReviewed(String kanjiChar) {
		getTodaysDailyUseInfo().kanjiReviewed(kanjiChar);
	}
	class DailyUseInfo implements Comparable<DailyUseInfo> {
		String dateString;
		int introduced;
		int reviewed;
		int getIntroduced() { return introduced; }
		int getReviewed() { return reviewed; }
		String introducedKanji;
		String reviewedKanji;
		static final String dateFormat = "yyyyMMdd";
		int getYear()  { return Integer.parseInt(dateString.substring(0,4)); }
		int getMonth() { return Integer.parseInt(dateString.substring(4,6)); }
		int getDate()  { return Integer.parseInt(dateString.substring(6,8)); }
		DailyUseInfo(String dateString, int introduced, int reviewed, String introducedKanji, String reviewedKanji) {
			this.dateString = dateString;
			this.introduced = introduced;
			this.reviewed = reviewed;
			this.introducedKanji = introducedKanji;
			this.reviewedKanji = reviewedKanji;
		}
		DailyUseInfo(String dateString) {
			this(dateString,0,0,"","");
		}
		DailyUseInfo(String dateString, int introduced, int reviewed) {
			this(dateString,introduced,reviewed,"","");
		}
		public int compareTo(DailyUseInfo dui2) {
			return this.dateString.compareTo(((DailyUseInfo)dui2).dateString);
		}
		public void kanjiIntroduced(String kanjiChar) {
			assert reviewedKanji.contains(kanjiChar)==false;
			if( introducedKanji.contains(kanjiChar)==false ) {
				introducedKanji += kanjiChar;
				introduced++;
			}
		}
		public void kanjiReviewed(String kanjiChar) {
			if( introducedKanji.contains(kanjiChar)==false && reviewedKanji.contains(kanjiChar)==false ) {
				reviewedKanji += kanjiChar;
				reviewed++;
			}
		}
	}
	public LinkedList<DailyUseInfo> getDailyUsageInfoList() {
		Enumeration keys = useHash.keys();
		Collection<DailyUseInfo> duiCollection = useHash.values();
		LinkedList<DailyUseInfo> duiList = new LinkedList<DailyUseInfo>();
		for( DailyUseInfo dui : duiCollection ) {
			duiList.add(dui);
		}
		Collections.sort(duiList);
		return duiList;
	}
}
class BadProfileException extends Exception {
	public BadProfileException(String msg) {
		super(msg);
	}
}
class UseCalendarDialog extends JDialog implements ActionListener {
	JFrame frame;
	UseCalendar useCalendar;
	ResourceBundle resources;
	public UseCalendarDialog(JFrame frame, ResourceBundle resources, UseCalendar useCalendar) {
		super(frame,"Study calendar",true);
		this.resources = resources;
		this.useCalendar = useCalendar;
		JPanel panel = new JPanel(new BorderLayout(2,2));
		String html = makeHtml();
		JEditorPane contentHolder = new JEditorPane("text/html",html);
		contentHolder.setEditable(false);
		JScrollPane scrollPane = new JScrollPane(contentHolder);
		panel.add(scrollPane, BorderLayout.CENTER);
		AbstractAction okAction = new AbstractAction(resources.getString("DIALOG_OK")) {
			public void actionPerformed(ActionEvent e) {
				setVisible(false);
				dispose();
			}
		};
		panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("ESCAPE"),"cancel");
		panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("ENTER"),"cancel");
		panel.getActionMap().put("cancel",okAction);
		okAction.putValue(Action.ACCELERATOR_KEY, new Integer(KeyEvent.VK_ENTER));
		JButton okButton = new JButton(okAction);
		okButton.addActionListener(this);
		getRootPane().setDefaultButton(okButton);
		panel.add(okButton, BorderLayout.SOUTH);
		setContentPane(panel);
		pack();
		setSize(565,665);
		setLocationRelativeTo(frame);
		contentHolder.setCaretPosition(0);
		setVisible(true);
		// We want the enter key to activate this button under any circumstances.
		// Unfortunately, the JEditorPane that I use to display the calendar
		// is grabbing the Enter key.  I don't care to find out why or how to stop
		// it.  Since there's only one button, I'm just wiring that one button to
		// be a focus hog.  As soon as it loses focus, it requests it again.  It
		// seems to solve the problem.
		okButton.requestFocus();
		okButton.addFocusListener( new FocusListener() {
			public void focusGained(FocusEvent e) {};
			public void focusLost(FocusEvent e) { requestFocus(); }
		} );
	}
	String makeHtml() {
		LinkedList<UseCalendar.DailyUseInfo> duiList = useCalendar.getDailyUsageInfoList();
		if( duiList.size()==0 )
			return resources.getString("CALENDAR_NO_HISTORY");
		String outerTemplate = JarInFile.readFileQuick(
			resources.getString("CALENDAR_TEMPLATE_OUTER_FILENAME"),
			resources.getString("CALENDAR_TEMPLATE_OUTER_FILE_ENCODING"));
		String innerTemplate = JarInFile.readFileQuick(
			resources.getString("CALENDAR_TEMPLATE_INNER_FILENAME"),
			resources.getString("CALENDAR_TEMPLATE_INNER_FILE_ENCODING"));
		innerTemplate = innerTemplate.replace("%\"","%%\"");
		String html = "", currentMonthHtml="";
		int month=-1, year=-1;
		String[] inputs = null;
		String[] monthNames = resources.getString("MONTH_NAMES").split(";");
		int i;
		int dayOfMonth, introduced, reviewed;
		int todayMonth, todayYear, todayDayOfMonth;
		int prevMonth, prevYear;
		final int rowsInCalendar = 6;
		GregorianCalendar gc = new GregorianCalendar();
		todayMonth = gc.get(GregorianCalendar.MONTH)+1;
		todayYear = gc.get(GregorianCalendar.YEAR);
		todayDayOfMonth = gc.get(GregorianCalendar.DAY_OF_MONTH);
		for(UseCalendar.DailyUseInfo dui : duiList) {
			prevMonth = month;
			prevYear = year;
			month = dui.getMonth();
			year = dui.getYear();
			int blanksAtFront = getBlankDaysInMonth(month,year);
			if( prevMonth!=month || prevYear!=year ) {
				if( prevMonth>0 && prevYear>0 )
					html = String.format(innerTemplate,(Object[])inputs) + (html.length()==0 ? "" : "<br><br>") + html;
				inputs = new String[7*rowsInCalendar+1];
				inputs[0] = monthNames[month-1];
				if( year!=todayYear )
					inputs[0] += " " + String.valueOf(year);
				for( i=1; i<inputs.length; i++ ) {
					if( i<=blanksAtFront )
						inputs[i] = "";
					else {
						dayOfMonth = i-blanksAtFront;
						if( dayOfMonth>getDaysInMonth(month,year) )
							inputs[i] = String.format("&nbsp;<br><big>&nbsp;</big><br> &nbsp;",dayOfMonth);
						else if(month==todayMonth && year==todayYear && dayOfMonth>todayDayOfMonth)
							inputs[i] = String.format("%s<br><big>%s</big><br> &nbsp;",
									dayOfMonth,
									resources.getString("CALENDAR_MARK_NOTHING"));
						else
							inputs[i] = String.format("%s<br><big>%s</big><br> &nbsp;",
									dayOfMonth,
									resources.getString("CALENDAR_MARK_WRONG"));
					}
				}
			}
			dayOfMonth = dui.getDate();
			introduced = dui.getIntroduced();
			reviewed = dui.getReviewed();
			inputs[dayOfMonth+blanksAtFront] = String.format("%s<br><big>%s</big><br>%d / %d",
					dayOfMonth,
					resources.getString("CALENDAR_MARK_RIGHT"),
					introduced,
					reviewed);
		}
		html = String.format(innerTemplate,(Object[])inputs) + "<br><br>" + html;
		return String.format(outerTemplate,html);
	}
	static int getBlankDaysInMonth(int month, int year) { // number of days at the start of the calendar
		GregorianCalendar gc = new GregorianCalendar(year,month-1,1);
		int dayOfWeek = gc.get(GregorianCalendar.DAY_OF_WEEK);
		int result = -1;
		if( dayOfWeek==GregorianCalendar.SUNDAY )
			result = 0;
		else if( dayOfWeek==GregorianCalendar.MONDAY )
			result = 1;
		else if( dayOfWeek==GregorianCalendar.TUESDAY )
			result = 2;
		else if( dayOfWeek==GregorianCalendar.WEDNESDAY )
			result = 3;
		else if( dayOfWeek==GregorianCalendar.THURSDAY )
			result = 4;
		else if( dayOfWeek==GregorianCalendar.FRIDAY )
			result = 5;
		else if( dayOfWeek==GregorianCalendar.SATURDAY )
			result = 6;
		assert result>=0 && result <=6;
		return result;
	}
	static int getDaysInMonth(int month, int year) {
		GregorianCalendar gc = new GregorianCalendar(year,month-1,1);
		return gc.getMaximum(GregorianCalendar.DAY_OF_MONTH);
	}
	public void actionPerformed(ActionEvent e) {
		this.setVisible(false);
		this.dispose();
	}
}
