package org.cmdline.sudoku;

import java.applet.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.table.*;
import javax.imageio.*;
import javax.imageio.stream.*;
import java.awt.image.BufferedImage;
import net.n3.nanoxml.*;
import netscape.javascript.JSObject;
import org.wrap.*;

public class SolvingSudokuBoard extends JApplet implements Constants, ActionListener, ItemListener, MouseMotionListener {
	JTable tbl = null;
	static JApplet applet = null;
	SudokuCellRenderer renderer = null;
	DefaultCellEditor editor = null;
	static JPanel colorDisplay = null;
	ColorChooser chooser = null;
	private SolvingTableModel model = null;
	private static Vector cells = new Vector();
 	private javax.swing.JMenuBar menuBar;
 	private javax.swing.JMenu fileMenu, gameMenu;
	static private int waitcount = 0;
	static Waiter waiter = null;
 	javax.swing.JRadioButton setup,solve,hint,generate;
 	private javax.swing.JRadioButtonMenuItem levelEasy, levelMedium, levelHard;
 	private javax.swing.JRadioButtonMenuItem templateEasy, templateMedium, templateHard;
	static boolean solving = false;
	static boolean colorFirst = false;
	static boolean is_applet = false;
	static Color currColor = Color.black;
	static SplashScreen ss = null;
	static OSXApplication osxapp = null;
	Stack solution = new Stack();
	SudokuBoard internal = new SudokuBoard();	// Internal representation
	int[] values;
	int newidx = 0;
	static ImageIcon ico;
	int lastMouseOver = -1;
	SudokuCell lastCell = null;
	HintViewer viewer = null;
	private static long seed = 0;
	private String lastState = null;
	boolean playSounds = true;
	AudioClip click = null,fanfare = null;
	
	private static final String xmlhdr1 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
	private static final String xmlhdr2 = "<!DOCTYPE sudoku SYSTEM \"http://www.geocities.com/mik3hall/sudoku.dtd\">";
	private static final String xmlhdr3 = "<sudoku version=\"1.0\">";
	private static final String puzzle_hdr = "<puzzle>";
	private static final String puzzle_trlr = "</puzzle>";
	private static final String xmltrlr = "</sudoku>";

	public static void main(String[] args) {
		is_applet = false;
		if (System.getProperty("os.name").equals("Mac OS X")) {
			ss = new SplashScreen();
			ss.setVisible(true);
		}
		applet = new SolvingSudokuBoard();
  		java.net.URL where = SolvingSudokuBoard.class.getClassLoader().getResource("sounds/uber_click.wav");
 		((SolvingSudokuBoard)applet).setClick(Applet.newAudioClip(where));			
		where = SolvingSudokuBoard.class.getClassLoader().getResource("sounds/sdwin.wav");
		((SolvingSudokuBoard)applet).setFanfare(Applet.newAudioClip(where));
		((SolvingSudokuBoard)applet).prepare();
		if (System.getProperty("os.name").equals("Mac OS X")) {
			osxapp = new OSXApplication();			
			waiter = ((SolvingSudokuBoard)applet).getWaiter(); 
		}
		else ((SolvingSudokuBoard)applet).frameIt();
	}
		
	public void init() {
		applet = this;
		is_applet = true;
		// Load audio clip
        click = getAudioClip(getDocumentBase(), "sounds/uber_click.wav");
        fanfare = getAudioClip(getDocumentBase(), "sounds/sdwin.wav");
		seed = (long)getCodeBase().toString().hashCode();
		prepare();
	}
	
	public SolvingSudokuBoard() {
		seed ^= System.currentTimeMillis();
		CannedGames.setSeed(seed);
	}

	public void setClick(AudioClip click) {
		this.click = click;
	}
	
	public void setFanfare(AudioClip fanfare) {
		this.fanfare = fanfare;
	}
	
	public Waiter getWaiter() { return new Waiter(500,this); }
	
	public void frameIt() {
		JFrame f = new JFrame("Sudoku");
		f.getContentPane().add(applet);
		f.addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent evt) {
				System.exit(0);
			}
		});
		f.setBackground(Color.white);
		f.setResizable(false);
        ((JComponent) f.getContentPane()).setBorder(new EmptyBorder(9,17,17,17));
		f.pack();
		SolvingSudokuBoard.positionWindow(f);
		f.setVisible(true);
		seed = (long)System.getProperty("user.name").hashCode();
	}
	
	public void prepare() {	
		try {
			if (!is_applet && System.getProperty("os.name").equals("Mac OS X")) 
				UIManager.setLookAndFeel("ch.randelshofer.quaqua.QuaquaLookAndFeel");
			else
  				UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
		} catch (Exception e) {
		}
	
		getContentPane().setBackground(Color.white);
		model = new SolvingTableModel(this);
		tbl = new JTable(model);
		tbl.addMouseMotionListener(this);
		tbl.setBackground(Color.white);
		
		tbl.addMouseListener(new java.awt.event.MouseAdapter() {
			public void mousePressed(java.awt.event.MouseEvent evt) {
				if (generate.isSelected()) {
		  			int mbh = (int)getJMenuBar().getSize().getHeight();
					int row = evt.getY() / 35;
					int col = evt.getX() / 40;
					int idx = row*COLS+col;
					int box = (idx / 27 * 3) + (idx % 9 / 3);
					SudokuCell cell = (SudokuCell)cells.elementAt(idx);
					cell.setSelected(true);
					model.fireTableCellUpdated(row,col);
					// Now determine symmetric twin cell
					int tbox = Math.abs(box-8);
					int box_idx = (row-row/3*3)*3 + (col-col/3*3);
					int tbox_idx = Math.abs(box_idx-8);
					int tidx_box = (tbox / 3 * 27) + (tbox % 3 * 3);
					int tidx = tidx_box + (tbox_idx / 3 * 9) + (tbox_idx % 3);
					cell = (SudokuCell)cells.elementAt(tidx);
					cell.setSelected(true);
					model.fireTableCellUpdated(tidx/9,tidx%9);
//					System.out.println("mousePressed: " + row + "," + col);
				}
				else if (hint.isSelected()) {
				}
			}
		});		
			
		menuBar = new JMenuBar();
		if (!is_applet) {
			fileMenu = new JMenu("File");
			fileMenu.setMnemonic(KeyEvent.VK_F);
			menuBar.add(fileMenu);
			JMenuItem openItem = new JMenuItem("Open...",KeyEvent.VK_O);
			openItem.addActionListener(this);
			fileMenu.add(openItem);
			JMenuItem saveItem = new JMenuItem("Save",KeyEvent.VK_S);
			saveItem.addActionListener(this);
			fileMenu.add(saveItem);
/* NOT SUPPORTED YET
			JMenuItem saveAsItem = new JMenuItem("Save As...");
			saveAsItem.addActionListener(this);
			fileMenu.add(saveAsItem);
*/
/* NOT SUPPORTED YET
			JMenuItem importItem = new JMenuItem("Import...",KeyEvent.VK_I);
			importItem.addActionListener(this);
			fileMenu.add(importItem);
			JMenuItem exportItem = new JMenuItem("Export...",KeyEvent.VK_E);
			exportItem.addActionListener(this);
			fileMenu.add(exportItem);
*/
			JMenuItem pdfItem = new JMenuItem("PDF",KeyEvent.VK_P);
			pdfItem.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent e) {
					SwingUtilities.invokeLater(new PDF(tbl));
				}
			});
			fileMenu.add(pdfItem);
			fileMenu.addSeparator();
			JMenuItem quitItem = new JMenuItem("Quit",KeyEvent.VK_Q);
			quitItem.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent e) {
					System.exit(0);
				}
			});
			fileMenu.add(quitItem);
		}
		gameMenu = new JMenu("Game");
		JMenuItem newItem = new JMenuItem("New",KeyEvent.VK_N);
		newItem.addActionListener(this);
		gameMenu.add(newItem);		
		JMenu levelItem = new JMenu("Level");
		ButtonGroup levels = new ButtonGroup();
		levelEasy = new JRadioButtonMenuItem("Easy",true);
		levels.add(levelEasy);
		levelItem.add(levelEasy);
		levelMedium = new JRadioButtonMenuItem("Medium");
		levels.add(levelMedium);
		levelItem.add(levelMedium);
		levelHard = new JRadioButtonMenuItem("Hard");
		levels.add(levelHard);
		levelItem.add(levelHard);
		gameMenu.add(levelItem);
		gameMenu.addSeparator();
		JMenuItem listItem = new JMenuItem("List",KeyEvent.VK_L);
		listItem.addActionListener(this);
		gameMenu.add(listItem);
		gameMenu.addSeparator();
		JCheckBoxMenuItem soundsItem = new JCheckBoxMenuItem("Sounds");
		soundsItem.addItemListener(this);
		soundsItem.setState(true);
		gameMenu.add(soundsItem);
		menuBar.add(gameMenu);
		setJMenuBar(menuBar);
		
		chooser = new ColorChooser();
		
		TableColumnModel tcm = tbl.getColumnModel();

		try {
			InputStream is = SolvingSudokuBoard.class.getResourceAsStream("images/question.gif");
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			BufferedOutputStream bos = new BufferedOutputStream(baos);
			BufferedInputStream bis = new BufferedInputStream(is);
			byte [] buf = new byte[1024];
			int cnt = bis.read(buf,0,buf.length);
			while (cnt > -1) {
				bos.write(buf,0,cnt);
				cnt = bis.read(buf,0,buf.length);
			}
			bis.close();
			bos.flush();
			bos.close();
			byte[] img = baos.toByteArray();
			ico = new ImageIcon(img,"question mark");
		}
		catch(IOException ioex) { ioex.printStackTrace(); }
		
		renderer = new SudokuCellRenderer(this);
		final JTextField tf = new JTextField();
   		tf.setHorizontalAlignment(SwingConstants.CENTER);
		editor = new DefaultCellEditor(tf);

		for (int i=0;i<COLS;i++) {
			tcm.getColumn(i).setPreferredWidth(40);
			tcm.getColumn(i).setMinWidth(40);
			tcm.getColumn(i).setCellRenderer(renderer); 
		}
		tbl.setRowHeight(35);
		for (int i=0;i<COLS*ROWS;i++) 
			if (is_applet)
				cells.addElement(new SudokuCell(model,i));
			else 
				cells.addElement(new NanoSudokuCell(model,i));

		tbl.setDefaultEditor(Number.class,editor);
		tbl.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
        GridBagLayout gbl = new GridBagLayout();
        GridBagConstraints c = new GridBagConstraints();
        
 		setLayout(gbl);
        c.gridwidth = 9;
        c.gridheight = 9;
        c.fill = c.NONE;
        c.weightx = 1.0;
        c.weighty = 1.0;
        gbl.setConstraints(tbl,c);
		add(tbl);
		c.gridwidth = 3;
		c.gridheight = 1;
		c.gridx = 10;
		Border raisedbevel = BorderFactory.createRaisedBevelBorder();
		Border blackline = BorderFactory.createLineBorder(Color.black);
		Border border = BorderFactory.createCompoundBorder(blackline,raisedbevel);
		JPanel buttons = new JPanel();
		buttons.setBackground(Color.white);
		buttons.setLayout(new GridLayout(4,1));
		ButtonGroup group = new ButtonGroup();
		setup = new JRadioButton("Setup",true);
		setup.setBackground(Color.white);
		setup.addActionListener(this);
		buttons.add(setup);
		group.add(setup);
		solve = new JRadioButton("Solve");
		solve.setBackground(Color.white);
		solve.addActionListener(this);
		buttons.add(solve);
		group.add(solve);
		hint = new JRadioButton("Hint");
		hint.setBackground(Color.white);
		hint.addActionListener(this);
		buttons.add(hint);
		group.add(hint);
		generate = new JRadioButton("Generate");
		generate.setBackground(Color.white);
		generate.addActionListener(this);
		buttons.add(generate);
		group.add(generate);
		buttons.setBorder(border);
		c.fill = c.HORIZONTAL;
		c.insets = insets(0,10,0,15);
		gbl.setConstraints(buttons,c);
		add(buttons);
		c.fill = c.NONE;
		c.gridy = 1;
		c.anchor = GridBagConstraints.NORTH;
		JPanel colors = new JPanel();
		colors.setLayout(new GridBagLayout());
		colors.setBackground(Color.white);
		colors.setBorder(border);
		GridBagConstraints cp = new GridBagConstraints();
		colors.add(chooser,cp);
		cp.gridy = 2;
		JLabel curr = new JLabel("Current color");
		colors.add(curr,cp);
		cp.gridy = 3;
		colorDisplay = new JPanel();
		colorDisplay.setBackground(Color.black);
		colors.add(colorDisplay,cp);
		gbl.setConstraints(colors,c);
		add(colors);
		JPanel controls = new JPanel();
		controls.setBackground(Color.white);
		JButton rewind = new JButton("<<");
		rewind.setBackground(Color.white);
		rewind.addActionListener(this);
		JButton backstep = new JButton("<");
		backstep.setBackground(Color.white);
		backstep.addActionListener(this);		
		controls.add(rewind);
		controls.add(backstep);
		JButton finish = new JButton("Finish");
		finish.setBackground(Color.white);
		finish.addActionListener(this);
		controls.add(finish);
		JButton clean = new JButton("Clean");
		clean.setBackground(Color.white);
		clean.addActionListener(this);
		controls.add(clean);
		c.gridwidth = 9; c.gridheight = 2;
		c.gridx = 1;
		c.gridy = 11;
		gbl.setConstraints(controls,c);
  		add(controls);
	}
	
	public void mouseDragged(MouseEvent e) {
		// NOP
	}
	
	public void mouseMoved(MouseEvent e) {
		int row = e.getY() / 35;
		int col = e.getX() / 40;
		if (lastMouseOver == row*COLS+col) {	// Do nothing if unchanged from last
			e.consume();
			return;
		}
		lastMouseOver = row*COLS+col;
		SudokuCell cell = (SudokuCell)cells.elementAt(lastMouseOver);
		if (cell.isHint()) {
			lastCell = cell;
			javax.swing.Timer t = new javax.swing.Timer(1000,this);
			t.setRepeats(false);
			t.start();	
		}
	}

	public void itemStateChanged(ItemEvent e) {
    	if (e.getStateChange() == ItemEvent.DESELECTED) playSounds = false;
    	else playSounds = true;	
	}
	
	public void actionPerformed(final ActionEvent evt) {
		String cmd = evt.getActionCommand();
		if (cmd == null) cmd = "TIMER";
		if (cmd.equals("Easy") || cmd.equals("Hard")) 
			return;
		if (cmd.equals("Solve")) {
			boolean changed = false;
			for (int i=0;i<cells.size();i++) {
				SudokuCell cell = (SudokuCell)cells.elementAt(i);
				if (cell.isGenerate()) {
					changed = true;
					cell.setValue(0);
					cell.setGenerate(false);
				}
			}
			JMenuBar bar = getJMenuBar(); 
			if (bar.getMenuCount() > 2) {
				bar.remove(bar.getMenu(2));
				bar.repaint();
			}
			if (changed) {	// was generate, update solution
				for (int i=0;i<cells.size();i++) {
					SudokuCell cell = (SudokuCell)cells.elementAt(i);
					if (cell.getValue() != 0)
						addCell(cell);
				}
				model.fireTableDataChanged();
			}
			solve();
			lastState = "Solve";
		}
		else if (cmd.equals("Setup")) {
			clean();
			solving = false;
			colorFirst = false;
			currColor = Color.black;
			lastState = "Setup";
		}
		else if (cmd.equals("Hint")) {
			values = internal.solve(solution);
			// Check if current solution is off-track
			// Set to flash first wrong entry and return
			for (int i=0;i<solution.size();i++) {
				SudokuCell cell = (SudokuCell)solution.elementAt(i);
				if (cell.getValue() != Math.abs(values[cell.getIndex()])) {		// Have a wrong one
					Toolkit.getDefaultToolkit().beep();
					cell.startError();
					// set back to solve
					solve();
					solve.setSelected(true);
					return;	
				}
			}
			for (int i=0;i<cells.size();i++) {
				SudokuCell cell = (SudokuCell)cells.elementAt(i);
				if (cell.getValue() == 0)
					cell.setHint(true);
			}
			model.fireTableDataChanged();
			lastState = "Hint";
		}
		else if (cmd.equals("New")) {
			clean();
			Vector v;
			byte[] b;
			if (levelEasy.isSelected())
				v = CannedGames.getGame(EASY);
			else if (levelMedium.isSelected())
				v = CannedGames.getGame(MEDIUM);
			else v = CannedGames.getGame(HARD);
			b = (byte[])v.elementAt(0);
			newidx = ((Integer)v.elementAt(1)).intValue();
			for (int i=0;i<b.length;i++) {
				if (b[i] == 0) continue;
				SudokuCell cell = (SudokuCell)cells.elementAt(i);
				int result = internal.set(b[i],cell.getIndex());
		  		if (result != 0) 	// Q. Invalid duplicate value? shouldn't happen
					throw new IllegalStateException("New already includes duplicate");
				cell.setValue(b[i]);
				cell.setColor(Color.black);
				addCell(cell);
			}
			solve();
			solve.setSelected(true);
			model.fireTableDataChanged();
			lastState = "New";
		}
		else if (cmd.equals("Open...")) {
			clean();
 			JFrame frame = new JFrame("Open file");
			JFileChooser chooser = new JFilePainter();
			chooser.setCurrentDirectory(new File(System.getProperty("user.dir")));
			int rc = chooser.showOpenDialog(frame);
			if (rc == JFileChooser.APPROVE_OPTION) 
				SwingUtilities.invokeLater(new PuzzleOpener(this,cells,solution,model,chooser.getSelectedFile()));			
		}
		else if (cmd.equals("Save")) {
			String ls = System.getProperty("line.separator");
			// Get the puzzle string
			StringBuffer puzzle = new StringBuffer(xmlhdr1).append(ls);
//			puzzle.append(xmlhdr2).append(ls);
			puzzle.append(xmlhdr3).append(ls);
			puzzle.append("   ").append(puzzle_hdr).append(ls);
			// Add cells part of original puzzle 
			for (int i=0;i<81;i++) {
				SudokuCell cell = (SudokuCell)cells.elementAt(i);
				if (generate.isSelected() && !cell.isSelected()) continue;
//				if (cell.getColor().equals(Color.black)) 
				if (cell.getValue() > 0)
					puzzle.append("      ").append(cell.toString()).append(ls);
			}
			// Now add one part of current solution if any
			if (solution.size() > 0) {
				for (int i=0;i<solution.size();i++) {
					SudokuCell cell = (SudokuCell)solution.elementAt(i);
					puzzle.append("      ").append(cell.toString()).append(ls);					
				}
			}
			puzzle.append("   ").append(puzzle_trlr).append(ls);
			puzzle.append(xmltrlr).append(ls);
			SwingUtilities.invokeLater(new PuzzleSaver(puzzle.toString()));		
		}
		else if (cmd.equals("Save As...")) {
			System.out.println("Save As...");
		}
		else if (cmd.equals("Import...")) {
			JFrame frame = new JFrame("Import file");
			JFileChooser chooser = new JFilePainter();
			chooser.setCurrentDirectory(new File(System.getProperty("user.dir")));
			int rc = chooser.showOpenDialog(frame);
			if (rc == JFileChooser.APPROVE_OPTION) 
				SwingUtilities.invokeLater(new PuzzleImport(cells,solution,model,chooser.getSelectedFile()));			
		}
		else if (cmd.equals("Export...")) {
			JFrame frame = new JFrame("Puzzle save");
			JFileChooser chooser = new JFilePainter();
			chooser.setCurrentDirectory(new File(System.getProperty("user.dir")));
			int rc = chooser.showSaveDialog(frame);
			if (rc == JFileChooser.APPROVE_OPTION) 
				SwingUtilities.invokeLater(new PuzzleExport(cells,solution,chooser.getSelectedFile(),generate.isSelected()));	
		}
		else if (cmd.equals("List")) {
			if (is_applet) {

				JSObject window = JSObject.getWindow(applet );
				boolean enabled = new Boolean(window.call("isEnabled",null).toString()).booleanValue();
				String cookie = null;
				if (enabled) {
					Object jsobj = window.call("getCookie",new String[] { "solved" });
					if (jsobj != null)
						cookie = jsobj.toString();
				}
				if (cookie == null) {
					StringBuffer cookieBuff = new StringBuffer();
					for (int i=0;i<45;i++) cookieBuff.append("0");
					cookie = cookieBuff.toString();
				}
				final String cookieParm = cookie;
//				System.out.println("cookieParm = " + cookieParm + " === cookie = " + cookie);
				SwingUtilities.invokeLater(new Runnable() {
					public void run() {
						new PuzzleList(cookieParm).setVisible(true);
					}
				});
			}
			else {
				SwingUtilities.invokeLater(new Runnable() {
					public void run() {
						new PuzzleList().setVisible(true);
					}
				});
			}
		}
		else if (cmd.equals("<")) {		// backstep - erase only last
			if (solution.empty()) return;
			SudokuCell last = (SudokuCell)solution.pop();
			last.setValue(0);
			internal.set((byte)0,last.getIndex());
			last.setColor((Color)null);
			last.setColorFirst(false);	
			if (!solution.empty() && 
				!((SudokuCell)solution.peek()).getColor().equals(currColor)) {
				currColor = ((SudokuCell)solution.peek()).getColor();
				colorDisplay.setBackground(currColor);				
			}
			int row = last.getRow();	
			int col = last.getCol();
	  		model.fireTableCellUpdated(row,col);
		}
		else if (cmd.equals("<<")) {	// rewind - erase all for color
			if (solution.empty()) return;
			while (((SudokuCell)solution.peek()).getColor().equals(currColor)) {
				SudokuCell erased = (SudokuCell)solution.pop();
				erased.setValue(0);
				internal.set((byte)0,erased.getIndex());
				erased.setColor((Color)null);
				erased.setColorFirst(false);
				int row = erased.getRow();
				int col = erased.getCol();
				model.fireTableCellUpdated(row,col);
			}
			if (!solution.empty()) {
				currColor = ((SudokuCell)solution.peek()).getColor();
				colorDisplay.setBackground(currColor);
			}
			colorFirst = true;	
		}
		else if (cmd.equals("Finish")) {
			values = internal.solve(solution);
			for (int i=0;i<values.length;i++) {
				SudokuCell cell = (SudokuCell)cells.elementAt(i);
				if (cell.getValue() == 0) {
					cell.setBackground(lightGreen);
					cell.setValue(Math.abs(values[i]));
				}
				else if (cell.getValue() != values[i]) {
					cell.setValue(values[i]);
//					cell.setBackground(Color.pink);
					cell.setBackground(lightRed);
				}
			}
			model.fireTableDataChanged();
		}
		else if (cmd.equals("Clean")) {
			JMenuBar bar = getJMenuBar(); 
			if (bar.getMenuCount() > 2) {
				bar.remove(bar.getMenu(2));
				bar.repaint();
			}
			clean(); 
		}
		else if (cmd.equals("Generate")) {
			int[] values = internal.generate(seed ^ System.currentTimeMillis());
			if (getJMenuBar().getMenuCount() < 3)
				generateMenu();
			for (int i=0;i<values.length;i++) {
				SudokuCell cell = (SudokuCell)cells.elementAt(i);
				cell.setGenerate(true);
				cell.setValue(values[i]);
			}
			model.fireTableDataChanged();
		}
		else {
			if (lastCell == null || lastCell.getIndex() != lastMouseOver) return;
			if (viewer != null) {
				if (lastCell.getIndex() == viewer.getIndex()) return;
				viewer.setVisible(false);
				viewer = null;
			}
			int[] possibles = internal.possibles(lastCell.getIndex(),solution);
			viewer = lastCell.hintOver(possibles);
			Point pscr = getLocationOnScreen();
  			int mbh = (int)getJMenuBar().getSize().getHeight();
			int x = pscr.x;
			int y = pscr.y+mbh;
			viewer.setLocation(x+lastCell.getCol()*40,y+lastCell.getRow()*35);
			viewer.update(viewer.getGraphics());
		}
	}
	
	void open(File f) {
		SwingUtilities.invokeLater(new PuzzleOpener(this,cells,solution,model,f));		
	}
	
	void doPuzzle(Vector v) {
		byte[] b = (byte[])v.elementAt(0);
		newidx = ((Integer)v.elementAt(1)).intValue();
		clean();
		for (int i=0;i<b.length;i++) {
			if (b[i] == 0) continue;
			SudokuCell cell = (SudokuCell)cells.elementAt(i);
			int result = internal.set(b[i],cell.getIndex());
			if (result != 0) 	// Q. Invalid duplicate value? shouldn't happen
				throw new IllegalStateException("New already includes duplicate");
			cell.setValue(b[i]);
			cell.setColor(Color.black);
			addCell(cell);
		}
		solve();
		solve.setSelected(true);
		model.fireTableDataChanged();
		lastState = "New";
	}
	
	private void generateMenu() {
		JMenuBar bar = getJMenuBar();
		JMenu generate = new JMenu("Templates");
		JMenuItem template = new JMenuItem("Template",KeyEvent.VK_T);
		template.addActionListener(this);
		generate.add(template);
		ButtonGroup templateLevels = new ButtonGroup();
		templateEasy = new JRadioButtonMenuItem("Easy",true);
		templateLevels.add(templateEasy);
		generate.add(templateEasy);		
		bar.add(generate);
	}
	
	private void solve() {
		solving = true;
		if (lastState == null || lastState.equals("Setup")) {
			colorFirst = true;
			currColor = Color.gray;
		}
		boolean changed = false;
		for (int i=0;i<cells.size();i++) {
			SudokuCell cell = (SudokuCell)cells.elementAt(i);
			if (cell.isHint()) {
				changed = true;
				cell.setHint(false);
			}
		}
		if (viewer != null) {
			viewer.setVisible(false);
			viewer = null;
		}
		if (changed)
			model.fireTableDataChanged();
	}
	
	private void clean() {
		solving = false;
		colorFirst = false;
		lastState = null;
		internal = new SudokuBoard();		// replace with new
		cells = new Vector();
		for (int i=0;i<COLS*ROWS;i++) 
			if (is_applet)
				cells.addElement(new SudokuCell(model,i));
			else 
				cells.addElement(new NanoSudokuCell(model,i));
		solution = new Stack();
		lastMouseOver = -1;
		lastCell = null;
		viewer = null;
		chooser.reset();
		model.fireTableDataChanged();
	}
	
	Vector getCells() { return cells; }
	static Color getCurrColor() { return currColor; }
	
	static void setCurrColor(Color c) {
		setCurrColor(c,true);
	}
	
	static void setCurrColor(Color c,boolean first) {
		colorFirst = first;
		currColor = c;
		colorDisplay.setBackground(c);
	}

	void addCell(SudokuCell cell) {
		solution.push(cell);
	}
	
	void removeCell(SudokuCell cell) {
		for (int i=0;i<solution.size();i++) {
			if (cell.getIndex() == ((SudokuCell)solution.elementAt(i)).getIndex()) {
				solution.remove(i);
				break;
			}	
		}
	}
	
	void updateCookie() {
		JSObject window = JSObject.getWindow(applet );
		boolean enabled = new Boolean(window.call("isEnabled",null).toString()).booleanValue();
		if (!enabled) return;
		StringBuffer cookieBuff = new StringBuffer();
		String cookie = null;
		Object jsobj = window.call("getCookie",new String[] { "solved" });
		if (jsobj != null)
			cookie = jsobj.toString();
		if (cookie == null) 
			for (int i=0;i<45;i++) cookieBuff.append("0");
		else cookieBuff.append(cookie);
		cookieBuff.setCharAt(newidx,'1');
		cookie = cookieBuff.toString();
		window.call("setCookie",new String[] {"solved",cookie});	
	}
  	
	boolean complete() {
		for (int i=0;i<cells.size();i++) 
			if (((SudokuCell)cells.elementAt(i)).getValue() == 0) return false;
		return true;
	}
	
	public Insets insets(int t, int l, int b, int r)
	{
		return new Insets(t,l,b,r);
	}

	public static void positionWindow(Window w)
	{
		Dimension sSize = w.getToolkit().getScreenSize();	// Position the window
  	 	int sHeight = sSize.height;
  	 	int sWidth = sSize.width;
  	 	Dimension aSize = w.getSize();
  	 	int aHeight = aSize.height;
  	 	int aWidth = aSize.width;
 	 	w.setLocation((sWidth-aWidth)/2,(sHeight-aHeight)/2);
	}
	
	  /*
		* Re-paint all non-text components (Text components didn't draw right)
		* Wrote this due to apparent missed component updates on window activate events
		*/
	  public static void allPaintAll(Container c)
	  {
		   Graphics g;
		   
		   Component [] comps = c.getComponents();
		   for (int i = 0; i < c.countComponents(); i++)
		   {              
				if (!(comps[i] instanceof TextArea | comps[i] instanceof TextField))    
				{
					 g = comps[i].getGraphics();
					 comps[i].paintAll(g);
				}
				if (comps[i] instanceof Container) allPaintAll((Container)comps[i]);
		   }
		   g = c.getGraphics();
		   c.paintAll(g);
		   c.layout();
	  }

	class FileProcessor implements Runnable {
		String filename = null;
		SolvingSudokuBoard ssb = null;
		
		public FileProcessor(String filename,SolvingSudokuBoard ssb) {
			this.filename = filename;
			this.ssb = ssb;
		}
		
		public void run() {
			processFile();
		} 
				
		private void processFile() {
			ssb.open(new File(filename));
		}
		
	}
		  
	class Waiter extends AsyncWaiter {
		long pause;
		SolvingSudokuBoard ssb = null;
		
		public Waiter(long pause,SolvingSudokuBoard ssb) { 
			this.pause = pause;
			this.ssb = ssb;
			new AsyncTimer(this,pause); 
		}
		
		public void expired(long evtime,long elapsed) {
			if (waitcount == -1) return;		// Terminate the timers
			else if (++waitcount > 5) {				
				SwingUtilities.invokeLater(new Runnable() {
					public void run() {
						ss.setVisible(false);
					}
				});
				ssb.frameIt();
				return;
			}
			// Determine if we have a file or files to process
			if (osxapp.hasDropped()) {
				waitcount = -1;
				ss.setVisible(false);
				ssb.frameIt();
				SwingUtilities.invokeLater(new FileProcessor(osxapp.getNextFile(),ssb));
				return;
			}
			new AsyncTimer(this,pause);					// Wait a bit longer
		}
	}

	class AsyncWaiter {
	
		protected void expired(long evttime,long elapsed) {}
		protected void expired(long evttime,long elapsed,Object parm) {}
		
	}		
/**
  * Allow a non-blocking wait
  * We start a Timer and continue
  * when it complete we are notified and can check on whatever we were waiting on
  */
  class AsyncTimer extends Thread
  {  	
  	private long	started;
  	private long	pause;
	private AsyncWaiter	target;
	private Object	parm;
		  	 
  	public AsyncTimer(AsyncWaiter target,long pause)
  	{
  		this.pause = pause;
  		this.target = target;
  		this.setName("Timer Event");
  		parm = null;
  		start();
  	}		
  
  	public AsyncTimer(AsyncWaiter target,long pause,Object parm) {
  		this.pause = pause;
  		this.target = target;
  		this.setName("Timer Event");
  		this.parm = parm;
  		start();
  	}
  	
  	public void start()  
  	{
  		started = System.currentTimeMillis(); 
  		super.start();
  	}

  	public void run()
  	{
  		try
  		{
  			sleep(pause);
  		}
  		catch (InterruptedException iex) {}
  		long evttime = System.currentTimeMillis();
   		long elapsed = evttime - started;
   		if (parm == null)
  			target.expired(evttime,elapsed);
  		else target.expired(evttime,elapsed,parm);
  	}
  }
}

class PDF implements Runnable {			
	JTable tbl = null;
	
	public PDF(JTable tbl) { this.tbl = tbl; }
						
	public void run() {
		try {
			File tmp_img = File.createTempFile("sudoku","jpg");
			JFrame frame = new JFrame("PDF Output");
			JFileChooser chooser = new JFilePainter();
			chooser.setCurrentDirectory(new File(System.getProperty("user.dir")));
			int rc = chooser.showSaveDialog(null);
			if (rc != JFileChooser.APPROVE_OPTION) return;
			String pdfPath = chooser.getSelectedFile().getPath();	
			if (!pdfPath.endsWith(".pdf")) pdfPath += ".pdf";	
			Dimension d = tbl.getSize();
			if (d.width == 0) {
				d = tbl.getPreferredSize();
				tbl.setSize(d);
			}
			Rectangle region = new Rectangle(0,0,d.width,d.height);
			boolean opaqueValue = tbl.isOpaque();
			tbl.setOpaque(true);
			BufferedImage img = new BufferedImage(region.width,region.height,BufferedImage.TYPE_INT_RGB);
			Graphics2D g2d = img.createGraphics();
			g2d.setClip(region);
			tbl.paint(g2d);
			g2d.dispose();
			tbl.setOpaque(opaqueValue);
			ImageIO.write(img,"JPG",tmp_img);
			PDFInfo info = new PDFInfo(pdfPath,new Frame(""),tmp_img.getPath());
			info.setVisible(true);
			// This shouldn't be necessary, but?
			SolvingSudokuBoard.allPaintAll(info);
		}
		catch (Throwable tossed) { tossed.printStackTrace(); }
	}
}
			
class JFilePainter  extends JFileChooser {
          
     protected JDialog createDialog(Component parent) throws HeadlessException {
		String title = getUI().getDialogTitle(this);
        JDialog dialog = new JDialogPainter(new Frame("test"),title,true);
 
        Container contentPane = dialog.getContentPane();
        contentPane.setLayout(new BorderLayout());
        contentPane.add(this, BorderLayout.CENTER);
 
        dialog.pack();
        SolvingSudokuBoard.positionWindow(dialog);
		return dialog;
    }
}
 
class JDialogPainter extends JDialog {
	
	public JDialogPainter(Frame f,String title,boolean modal) {
		super(f,title,modal);
	}
	
	public JDialogPainter(Dialog d) {
		super(d);
	}
	
	public void processEvent(AWTEvent e) {
		super.processEvent(e);
		int id = e.getID();
		if (id == ComponentEvent.COMPONENT_SHOWN)
			SolvingSudokuBoard.allPaintAll(this);
	}
}

interface Constants {
  	static final int COLS = 9;
  	static final int ROWS = 9;	
 	static final Color lightGreen = new Color(0xccffcc);
	static final Color lightBlue = new Color(0xccccff);
	static final Color lightRed = new Color(0xffcccc);
	static final int EASY = 0, MEDIUM = 1, HARD = 2;
}

// This class manages the download table's data.
class SolvingTableModel extends AbstractTableModel
  implements Constants
{
  // These are the names for the table's columns.
  private static final String[] columnNames = new String[0];
  private static final int COLS = 9;
  private static final int ROWS = 9;
  private SolvingSudokuBoard board = null;
  
  public SolvingTableModel(SolvingSudokuBoard board) {
  	this.board = board;
  }
  
  // Get table's column count.
  public int getColumnCount() {
    return COLS;
  }

  // Get a column's name.
  public String getColumnName(int col) {
     return "";
  }

  // Get a column's class.
  public Class getColumnClass(int col) {
    return Number.class;
  }

  // Get table's row count.
  public int getRowCount() {
    return ROWS;
  }

  // Return editable
  public boolean isCellEditable(int row, int col) {
  	return true;
  }
  
  // Get value for a specific row and column combination.
  public Object getValueAt(int row, int col) {
	return new Integer(((SudokuCell)board.getCells().elementAt(row*COLS+col)).getValue());
  }

  // Set value for a specific row and column combination.
  public void setValueAt(Object value,int row,int col) {
  	if (value.equals("")) {
  		SudokuCell cell = (SudokuCell)board.getCells().elementAt(row*COLS+col);
		cell.setValue(0);  		
  	}
    if (isValidValue(value)) {	
  		int ivalue = new Integer((String)value).intValue(); 
  		SudokuCell cell = (SudokuCell)board.getCells().elementAt(row*COLS+col);
  		if (cell.isHintCell()) {
	 		cell.setValue(Math.abs(board.values[row*COLS+col]));
	 		board.addCell(cell);
	 		fireTableCellUpdated(row,col);
			return;
		}
  		int result = board.internal.set((byte)ivalue,cell.getIndex());
  		if (result != 0) {	// Q. Invalid duplicate value?
  			SudokuCell dup = (SudokuCell)board.getCells().elementAt(result);
  			dup.startError();
  		}
  		else {
  			cell.setValue(ivalue);
  			cell.setColor(board.getCurrColor());
  			board.addCell(cell);
  			if (board.complete()) {
  				fanfare();
  				if (board.is_applet)
  					board.updateCookie();
  			}
 		}
  		fireTableCellUpdated(row,col);
  		click();
  	}
  	else Toolkit.getDefaultToolkit().beep();
  }
    
  private void click() {
    if (board.playSounds && board.click != null) board.click.play();
  }
  
  private void fanfare() {
  	if (board.playSounds && board.fanfare != null) board.fanfare.play();
  }
  
  protected boolean isValidValue(Object value) {
  	if (value instanceof String) {
  		String sValue = (String)value;
  		
  		try {
  			int ivalue = new Integer(sValue).intValue();
  			return (ivalue > 0 && ivalue < 10);
  		}
  		catch (Throwable tossed) { return false; }
  	}
  	return false;
  }
}

class SudokuCellRenderer implements TableCellRenderer, Constants {
	SolvingSudokuBoard board;
	
	public SudokuCellRenderer(SolvingSudokuBoard board) {
		this.board = board;
	}
	
	public Component getTableCellRendererComponent(JTable tbl,
						Object value,boolean isSelected,
						boolean hasFocus, int row, int column) {
		return (SudokuCell)board.getCells().elementAt(row*COLS+column);	
	}
}

class SudokuCell extends JPanel implements Constants, ActionListener {
	private int index;
	Color c = null;
	int value = 0, err_count = 0;
	boolean colorFirst = false;	// Slightly fancier painting for first
	boolean dup = false;
	boolean hint = false;
	boolean hintCell = false;
	boolean generate = false;
	AbstractTableModel model = null;
	
	public SudokuCell(SolvingTableModel model,int index) { 
		this.index = index; 
		this.model = model;
		setColor(SolvingSudokuBoard.getCurrColor());
		setBackground(Color.white);
	}

	public String toString() {
		StringBuffer s = new StringBuffer("<cell ");
		string1(s);
		string2(s);
		return s.toString();
	}
	
	public String toString(int seq) {
		StringBuffer s = new StringBuffer("<cell ");
		string1(s);
		s.append("seq=\"").append(new Integer(seq).toString()).append("\" ");
		string2(s);
		return s.toString();
	}
	
	private void string1(StringBuffer s) {
		s.append("row=\"").append(new Integer(getRow()).toString()).append("\" ");
		s.append("col=\"").append(new Integer(getCol()).toString()).append("\" ");
		if (c != null) {
			s.append("color=\"");
			if (c.equals(c.black)) s.append("black\" ");
			else if (c.equals(c.gray)) s.append("gray\" ");
			else if (c.equals(c.blue)) s.append("blue\" ");
			else if (c.equals(c.cyan)) s.append("cyan\" ");
			else if (c.equals(c.green)) s.append("green\" ");
			else if (c.equals(c.lightGray)) s.append("lightGray\" ");
			else if (c.equals(c.magenta)) s.append("magenta\" ");
			else if (c.equals(c.orange)) s.append("orange\" ");
			else if (c.equals(c.pink)) s.append("pink\" ");
			else if (c.equals(c.red)) s.append("red\" ");
			else if (c.equals(c.yellow)) s.append("yellow\" ");
		}
		if (value != 0) 
			s.append("value=\"").append(new Integer(value).toString()).append("\" ");
		if (colorFirst) s.append("first=\"true\" ");
	}
	
	private void string2(StringBuffer s) {
		s.append("/>");
	}

	public void actionPerformed(ActionEvent evt) {	
		model.fireTableCellUpdated(getRow(),getCol());
	}
	
	public int getIndex() { return index; }
	public int getRow() { return index / 9; }
	public int getCol() { return (index+9) % 9; }
	
	public Color getColor() { return c; }
	public void setColor(Color c) { 
		if (SolvingSudokuBoard.colorFirst) {
			colorFirst = true;
			SolvingSudokuBoard.colorFirst = false;
		}	
		this.c = c; 
	}
	public void setColor(String color) {
		if (color.equals("black")) c = Color.black;
		else if (color.equals("gray")) c = Color.gray;
		else if (color.equals("blue")) c = Color.blue;
		else if (color.equals("cyan")) c = Color.cyan;
		else if (color.equals("green")) c = Color.green;
		else if (color.equals("lightGray")) c = Color.gray;
		else if (color.equals("magenta")) c = Color.magenta;
		else if (color.equals("orange")) c = Color.orange;
		else if (color.equals("pink")) c = Color.pink;
		else if (color.equals("red")) c = Color.red;
		else if (color.equals("yellow")) c = Color.yellow;
	}		
	
	public void setColorFirst(boolean b) { colorFirst = b; }
	public boolean isColorFirst() { return colorFirst; }
	
	public boolean isGenerate() { return generate; }
	public void setGenerate(boolean generate) {
		this.generate = generate;
	}
	public void setSelected(boolean selected) {
		if (selected) generate = false;		// Just turn this off good enough?
		else generate = true;	// Won't currently happen. But might?
	}
	public boolean isSelected() { return !generate; }
	
	public boolean isHint() { return hint; }
	public void setHint(boolean hint) { 
		this.hint = hint; 
	}
	public boolean isHintCell() { return hintCell; }
	public HintViewer hintOver(int[] possibles) {
		HintViewer viewer = new HintViewer(this,index,possibles);
		viewer.setVisible(true);
		return viewer;
	}
	public void hint() {
		setBackground(lightBlue);
		hintCell = true;		// set that it's a hint cell
		c = Color.black;		
		hint = false;			// Not hint mode anymore
		model.setValueAt("1",getRow(),getCol());
	}
		
	public int getValue() { return value; }
	public void setValue(int value) { this.value = value; }

	public void startError() {	// Start simple animation showing errored entry
		Toolkit.getDefaultToolkit().beep();
		err_count = 1;
  		model.fireTableCellUpdated(getRow(),getCol());
	}
	
	public void paintComponent(Graphics g) {
		super.paintComponent(g);

		if (err_count == 1 || err_count == 3) {
			flashRed(g);
		}
		else if (err_count == 2) {		// Paint normally but schedule another red flash
			frameIt(g,c);
			javax.swing.Timer timer = new javax.swing.Timer(600,this);
			timer.setRepeats(false);
			timer.start();
		}
		else frameIt(g,c);

		if (hint) {
			g.setColor(lightGreen);
			g.fillOval(5,5,getWidth()-10,getHeight()-10);
			SolvingSudokuBoard.ico.paintIcon(this,g,9,4);
		}
		else if (generate) {
			g.setColor(lightGreen);
			g.fillRect(3,3,getWidth()-6,getHeight()-6);
		}
		else if (value != 0) {
			g.setColor(c);
			Font f = g.getFont().deriveFont(18f);
			if (!SolvingSudokuBoard.solving) 
				f = f.deriveFont(Font.BOLD);
			g.setFont(f);
			g.drawString(new Integer(value).toString(),14,24);
		}
		
		if (err_count == 4) {
			err_count = 0;	// reset
//			model = null;
		}
		else if (err_count > 0) err_count++;
	}

	private void flashRed(Graphics g) {
		g.setColor(Color.red);
		g.fillRect(0,0,getWidth()-1,getHeight()-1);
		g.setColor(Color.white);
		javax.swing.Timer timer = new javax.swing.Timer(600,this);
		timer.setRepeats(false);
		timer.start();
	}
		
	private void frameIt(Graphics g,Color c) {
		if (colorFirst) 
			g.setColor(c);
		else
			g.setColor(Color.black);
		int row = index / 9;
		int col = (index+9) % 9;
		g.drawRect(0,0,getWidth()-1,getHeight()-1);
		if ((col + 3) % 3 == 0) 
			g.drawLine(1,0,1,getHeight()-1);
		if ((col + 3) % 3 == 2)
			g.drawLine(getWidth()-2,0,getWidth()-2,getHeight()-1);
		if ((row + 3) % 3 == 0)
			g.drawLine(0,1,getWidth()-1,1);
		if ((row + 3) % 3 == 2)
			g.drawLine(0,getHeight()-2,getWidth()-1,getHeight()-2);
			
		g.setColor(c);
	}
}

class NanoSudokuCell extends SudokuCell {

	public NanoSudokuCell(SolvingTableModel model,int index) { 
		super(model,index);
	}
	
	// Return NanoXML IXMLElement representation of this cell
	public IXMLElement toNanoXML() {
		IXMLElement e = new XMLElement("cell");
		e.setAttribute("row",new Integer(getRow()).toString());
		e.setAttribute("col",new Integer(getCol()).toString());
		if (c.equals(c.black)) e.setAttribute("color","black");
		else if (c.equals(c.gray)) e.setAttribute("color","gray");
		else if (c.equals(c.blue)) e.setAttribute("color","blue");
		else if (c.equals(c.cyan)) e.setAttribute("color","cyan");
		else if (c.equals(c.green)) e.setAttribute("color","green");
		else if (c.equals(c.lightGray)) e.setAttribute("color","lightGray");
		else if (c.equals(c.magenta)) e.setAttribute("color","magenta");
		else if (c.equals(c.orange)) e.setAttribute("color","orange");
		else if (c.equals(c.pink)) e.setAttribute("color","pink");
		else if (c.equals(c.red)) e.setAttribute("color","red");
		else if (c.equals(c.yellow)) e.setAttribute("color","yellow");
		if (value != 0)
			e.setAttribute("value",new Integer(value).toString());
		if (colorFirst)
			e.setAttribute("first","true");			
		return e;
	}

}

class HintViewer extends JWindow implements Constants {
	private int index = 0;
	private int[] possibles;
	
	public HintViewer(final SudokuCell cell,int index,int[] possibles) {
		this.index = index;
		this.possibles = possibles;
		setBackground(lightBlue);
		Dimension d = new Dimension(40,35);
		setSize(d);
		setPreferredSize(d);
		setMinimumSize(d);	
		addMouseListener(new java.awt.event.MouseAdapter() {
			public void mousePressed(java.awt.event.MouseEvent evt) {
				setVisible(false);
				cell.hint();
			}
		});		
	}
	
	public int getIndex() { return index; }
	
	public void paint(Graphics g) {
		super.paint(g);
/*
		Font f = g.getFont().deriveFont(12f);
		f = f.deriveFont(Font.BOLD);
		g.setFont(f);
		int cnt = 0;
		for (int i=0;i<9;i++)
			if (possibles[i] != 0) cnt++;
		g.setColor(Color.blue);
		g.drawString(new Integer(cnt).toString(),0,10);
*/
		Font f = g.getFont().deriveFont(10f);
		f = f.deriveFont(Font.PLAIN);
		g.setFont(f);
		int x = 10;
		int row = 0, col = 0;
		g.setColor(Color.black);
		for (int i=0; i<possibles.length;i++) {
			if (possibles[i] == 0) continue;
			g.drawString(new Integer(possibles[i]).toString(),x+(col*9),11+row*10);
			col++;
			if (col == 3) {
				row++;
				col = 0;
			}
		}
	}
		
}