//
// Cryptogram
//
// This is a helper for solving cryptograms. The user chooses letters,
// and the application replaces the letters in the cryptogram.
//
import java.applet.*;
import java.awt.*;

//
// The CryptogramArea is the basic component which draws the letters
// and provides the user interface. It gives no border---the parent
// component should almost surely add one.
//
// both mouse events and keyboard events. All the relevant actions are
// defined within the object
//
class CryptogramArea extends Canvas {
	// these are character constants that indicate the backspace,
	// delete, enter, and return keys
	protected static final char CHAR_BS = (char) 010;
	protected static final char CHAR_DEL = (char) 07F;
	protected static final char CHAR_NL = (char) 012;
	protected static final char CHAR_CR = (char) 015;

	// the component will attempt to lay out its characters with this
	// width:height ratio
	protected static final double ASPECT_RATIO = 2.25;

	// this many pixels will be between the underline for the selection
	// and the letter that is selected
	protected static final int UNDERLINE_GAP = 2;

	// this character is what everything begins as
	protected char UNKNOWN_CHAR = ' ';

	// the two states: setting the encryption and solving it
	public static final int STATE_SET = 0;
	public static final int STATE_SOLVE = 1;

	protected String encryption; // the encrypted string
	protected char[] decryption; // the current decryption
	protected int pos_select = -1; // which character is now selected
	protected int cur_state; // either STATE_SET or STATE_SOLVE

	public CryptogramArea(String str) {
		int i;

		// set up the local data
		encryption = str;
		decryption = new char[str.length()];
		for(i = 0; i < decryption.length; i++) {
			decryption[i] = UNKNOWN_CHAR;
		}
		cur_state = STATE_SOLVE;

		// set up myself for listening to both mouse and key events
	}

	public void setUnknownChar(char which) {
		if(decryption != null) {
			for(int pos = 0; pos < decryption.length; pos++) {
				if(decryption[pos] == UNKNOWN_CHAR) {
					decryption[pos] = which;
				}
			}
		}
		UNKNOWN_CHAR = which;
	}

	public void replaceChars(char which, char value) {
		for(int pos = 0; pos < decryption.length; pos++) {
			if(encryption.charAt(pos) == which) {
				decryption[pos] = value;
			}
		}
	}

	//
	// setState
	//
	// Call this to change the state between setting the encryption
	// (STATE_SET) and solving it (STATE_SOLVE).
	//
	public void setState(int new_state) {
		int i;

		if(new_state == cur_state) return;

		if(new_state == STATE_SOLVE) {
			decryption = new char[encryption.length()];
			for(i = 0; i < decryption.length; i++) {
				decryption[i] = UNKNOWN_CHAR;
			}
			pos_select = -1;
			cur_state = STATE_SOLVE;
		} else {
			encryption = new String("");
			decryption = null;
			pos_select = 0;
			cur_state = STATE_SET;
		}
		getGraphics().clearRect(0, 0, size().width,
			size().height);
		update(getGraphics());
	}

	//
	// paint
	//
	// draw the current state, with the current decryption laid out
	// just above the encrypted string, and the current selection
	// underlined
	//
	public void paint(Graphics g) {
		// find the height and width allocated to each character
		FontMetrics metric = g.getFontMetrics();
		int char_width = metric.getMaxAdvance();
		int char_height = (metric.getMaxAscent() + metric.getMaxDescent() + UNDERLINE_GAP)
			* 9 / 4;

		// see how many characters fit on a line; we'll fit them all
		int chars_per_line = this.size().width / char_width;
		if(chars_per_line == 0) chars_per_line = 1;
		int i, pos, x, y;
		int row = 0;
		int col = 0;
		char[] cryptstr = encryption.toCharArray();
		char[] cur;

		// draw each character pair at the (x,y) position calculated
		for(pos = 0; pos <= cryptstr.length; pos++) {
			if(pos < cryptstr.length && cryptstr[pos] == '\n') {
				++row; col = 0;
			} else {
				++col;
				if(col == chars_per_line) { ++row; col = 0; }
				x = col * char_width;
				y = row * char_height;
				for(i = 0; i < 2; i++) {
					// once for decryption, once for encryption
					y += metric.getMaxAscent();
					if(i == 0) cur = decryption; else cur = cryptstr;
					if(cur != null && pos < cur.length) {
						// center the character in the space
						g.drawChars(cur, pos, 1, x +
							(char_width - metric.charWidth(cur[pos])) / 2, y);
					}
					if(i == 0) y += metric.getMaxDescent();
				}
				if(pos == pos_select) { // this is the current selection
					y += UNDERLINE_GAP;
					g.drawLine(x, y, x + char_width - 1, y);
				}
			}
		}
	}

	//
	// repaintChar
	//
	// clear and draw just a single character
	//
	public void repaintChar(int which) {
		if(which < 0) {
			; // do nothing
		} else if(which >= encryption.length()) {
			repaintChar(which, ' ');
		} else {
			repaintChar(which, encryption.charAt(which));
		}
	}
	public void repaintChar(int which, char what) {
		// find the height and width allocated to each character
		Graphics g = this.getGraphics();
		FontMetrics metric = g.getFontMetrics();
		int char_width = metric.getMaxAdvance();
		int char_height = (metric.getMaxAscent() + metric.getMaxDescent() + UNDERLINE_GAP)
			* 9 / 4;

		// see how many characters fit on a line; we'll fit them all
		int chars_per_line = this.size().width / char_width;
		if(chars_per_line == 0) chars_per_line = 1;
		int i, pos, x, y;
		int row = 0;
		int col = 0;
		char[] cryptstr = encryption.toCharArray();
		char[] cur;

		// draw each character pair at the (x,y) position calculated
		for(pos = 0; pos <= cryptstr.length; pos++) {
			if(pos < cryptstr.length && cryptstr[pos] == '\n') {
				if(pos == which) {
					x = col * char_width;
					y = row * char_height;
					g.clearRect(x, y, char_width, char_height);
				}
				++row; col = 0;
			} else {
				++col;
				if(col == chars_per_line) { ++row; col = 0; }
				if(pos == which) {
					x = col * char_width;
					y = row * char_height;
					cur = new char[1];
					// clear the area
					g.clearRect(x, y, char_width, char_height);
					// now refill it
					// draw decrypted character (centered)
					y += metric.getMaxAscent();
					if(decryption != null && pos < decryption.length) {
						cur[0] = decryption[pos];
						g.drawChars(cur, 0, 1, x +
							(char_width - metric.charWidth(cur[0])) / 2, y);
					}
					// draw encrypted character (centered)
					y += metric.getMaxDescent() + metric.getMaxAscent();
					cur[0] = what;
					g.drawChars(cur, 0, 1, x +
						(char_width - metric.charWidth(cur[0])) / 2, y);
					if(pos == pos_select) { // this is the current selection
						y += UNDERLINE_GAP;
						g.drawLine(x, y, x + char_width - 1, y);
					}
				}
			}
		}
		if(pos == which) {
			++col;
			if(col == chars_per_line) { ++row; col = 0; }
			x = col * char_width;
			y = row * char_height;
			g.clearRect(x, y, char_width, char_height);
		}
	}

	//
	// mouse events
	//
	// the only mouse event I care about is mouseClicked. In this case
	// I want to compute which character - if any - the user clicked.
	//
	public boolean mouseUp(Event evt, int x, int y) {
		this.requestFocus();
		if(cur_state == STATE_SOLVE) {
			// find the height and width allocated to each character
			FontMetrics metric = this.getGraphics().getFontMetrics();
			int char_width = metric.getMaxAdvance();
			int char_height = (metric.getMaxAscent() + metric.getMaxDescent() + UNDERLINE_GAP)
				* 9 / 4;

			// we fit as many charactrs on a line as possible
			int chars_per_line = (this.size().width)
				/ char_width;
			if(chars_per_line == 0) chars_per_line = 1;

			// determine in which column and row the click occurred
			int select_col = x / char_width;
			int select_row = y / char_height;

			// go through the string seeing if any character matches
			int row = 0;
			int col = 0;
			int pos;
			if(select_col >= 0 && select_row >= 0) {
				for(pos = 0; pos < encryption.length(); pos++) {
					if(encryption.charAt(pos) == '\n') {
						++row; col = 0;
					} else {
						++col;
						if(col == chars_per_line) { ++row; col = 0; }
						if(row == select_row && col == select_col) {
							// found where the user clicked
							col = pos_select;
							pos_select = pos;
							repaintChar(col);
							repaintChar(pos);
/*
							getGraphics().clearRect(0, 0, size().width,
								size().height);
							update(getGraphics());
*/
							return true;
						}
					}
				}
			}
		}
		return false;
	}

	//
	// key events
	//
	// again, the only event I care about is the keyTyped event. In
	// this case we should replace all the characters matching the
	// selection with the key typed
	//
	public boolean keyDown(Event e, int key) {
		int i;

		if(cur_state == STATE_SOLVE) {
			if(pos_select < 0) return false; // nothing selected; do nothing
			for(i = 0; i < encryption.length(); i++) {
				if(encryption.charAt(i) == encryption.charAt(pos_select)) {
					// this character matches the character selected;
					// set the decryption to be the key typed
					decryption[i] = (char) key;
					repaintChar(i);
				}
			}

/*
			// repaint everything to reflect changes
			getGraphics().clearRect(0, 0, size().width,
				size().height);
			update(getGraphics());
*/
		} else {
			if(key == CHAR_BS || key == CHAR_DEL) {
				// remove the last character
				if(encryption.length() == 0) return false;
				--pos_select;
				repaintChar(pos_select + 1, ' ');
				encryption = encryption.substring(0,
					encryption.length() - 1);
				repaintChar(pos_select);
			} else if(key == CHAR_NL || key == CHAR_CR) {
				++pos_select;
				repaintChar(pos_select - 1);
				encryption = encryption.concat("\n");
				repaintChar(pos_select);
			} else {
				// append the typed character
				encryption = encryption.concat(String.valueOf((char) key));
				pos_select = encryption.length();
				repaintChar(pos_select - 1);
				repaintChar(pos_select);
			}
		}
		return true;
	}
}

class CryptButton extends Button {
	protected CryptogramArea cryptarea;

	public CryptButton(CryptogramArea cryptarea, String title) {
		super(title);
		this.cryptarea = cryptarea;
	}
	public boolean action(Event evt, Object what) {
		cryptarea.setState(getLabel().equals("Set")
			? CryptogramArea.STATE_SET : CryptogramArea.STATE_SOLVE);
		cryptarea.requestFocus();
		return true;
	}
}

//
// The Cryptogram object is a very simple object which simply maintains
// a CryptogramArea.
//
// This means that this object will include several methods defining
// what should be done in the case of the window being opened, closed,
// resized, etc.
//
public class Cryptogram extends Applet {
	// the string that the application will be decrypting
    protected static final Color FOREGROUND_DFLT = Color.black;
	protected final static String CRYPTSTRING_DFLT =
		"OKXYUOPUAKURCCAPLUBLMR!LBULSUPBMMXYU\nBLSU,OQQSVDMIGYU" +
		"DABLUSJSPUMCUC OTSYU\nIOTSUDLACC AK!UBLVMR!LUBLSUBR !S" +
		"JUDMMXYU\nOKXUQRVQ SXUOPUABUIOTSW";

	protected String crypt_str;
	protected CryptogramArea canvas; // the picture itself
	protected Button set; // click this to set the cryptogram
	protected Button solve; // click this to begin solving it

    //
    // read in the applet parameters
    //
    public void init() {
        crypt_str = this.getParameter("cryptogram");
		if(crypt_str == null) crypt_str = CRYPTSTRING_DFLT;
		StringBuffer buf = new StringBuffer();
		for(int pos = 0; pos < crypt_str.length(); pos++) {
			char ch = crypt_str.charAt(pos);
			if(ch == '\\' && pos < crypt_str.length() - 1) {
				++pos;
				ch = crypt_str.charAt(pos);
				if(ch == 'n') ch = '\n';
			}
			buf.append(ch);
		}
		crypt_str = buf.toString();

        this.setForeground(getColorParameter("foreground", FOREGROUND_DFLT));
        Color bg = getColorParameter("background", null);
        if(bg != null) this.setBackground(bg);

		//
		// This will define the layout for the window.
		// A GridBagLayout is the most sophisticated and most useful
		// of the different layout choices.
		//
		GridBagConstraints c = new GridBagConstraints();
		GridBagLayout layout = new GridBagLayout();
		this.setLayout(layout);

		// set up the CryptogramArea
		canvas = new CryptogramArea(crypt_str);
		c.gridx = 0; c.gridy = 0; // this occupies cells (0,0) and (1,0)
		c.gridwidth = 2; c.gridheight = 1;
		c.fill = GridBagConstraints.BOTH; // make the object fill its area
		c.insets = new Insets(20, 20, 20, 20); // a border round the object
		c.anchor = GridBagConstraints.CENTER;
		c.weightx = 1.0; c.weighty = 1.0;
		layout.setConstraints(canvas, c);
		this.add(canvas); // add the canvas into the frame

		// see if the set and solve buttons should appear
		String settable = this.getParameter("setbuttons");
		if(settable == null || settable.equalsIgnoreCase("on")) {
			// insert the Set button
			set = new CryptButton(canvas, "Set");
			c.gridx = 0; c.gridy = 1; // this occupies cell (0,1)
			c.gridwidth = 1; c.gridheight = 1;
			c.fill = GridBagConstraints.NONE; // don't expand the button
			c.insets = new Insets(0,0,0,0);   // if the cell is too big
			c.ipadx = 0; c.ipady = 0;
			c.weightx = 1.0; c.weighty = 0.0; // don't expand vertically
			layout.setConstraints(set, c);
			this.add(set);

			// insert the Solve button
			solve = new CryptButton(canvas, "Solve");
			c.gridx = 1; c.gridy = 1; // this occupies cell (1,1)
			layout.setConstraints(solve, c);
			this.add(solve);
		}

		// set the unknown character
		String unknown_char = this.getParameter("unknownchar");
		if(unknown_char != null && unknown_char.length() >= 1) {
			canvas.setUnknownChar(unknown_char.charAt(0));
		}

		// do any of the presolves
		String crypt_chars = this.getParameter("cryptchars");
		String decrypt_chars = this.getParameter("decryptchars");
		if(crypt_chars != null && decrypt_chars != null) {
			for(int pos = 0; pos < crypt_chars.length()
					&& pos < decrypt_chars.length(); pos++) {
				canvas.replaceChars(crypt_chars.charAt(pos),
					decrypt_chars.charAt(pos));
			}
		}
    }

    public int getIntParameter(String name, int dflt) {
        String value = this.getParameter(name);
        if(value == null) return dflt;
        try {
            return Integer.parseInt(value);
        } catch(NumberFormatException e) {
            return dflt;
        }
    }

    public Color getColorParameter(String name, Color dflt) {
        String value = this.getParameter(name);
        if(value == null) return dflt;
        try {
            int code = Integer.parseInt(value, 16);
            return new Color(code);
        } catch(NumberFormatException e) {
            return dflt;
        }
    }

    //
    // these seem to be required
    //
    public String[][] getParameterInfo() {
        String[][] ret = {
            { "cryptogram", "text string", "the initial encrypted string" },
            { "cryptchars", "text string", "characters initially translated" },
            { "decryptchars", "text string", "these characters' true values" },
            { "unknownchar", "character", "what to print for unknown characters" },
            { "setbuttons", "on/off", "whether to include Set and Solve buttons" },
            { "foreground", "color code (hexadecimal)", "foreground color" },
            { "background", "color code (hexadecimal)", "background color" }
        };
        return ret;
    }

    public String getAppletInfo() {
        return "Cryptogram solver, by Carl Burch, Jun 1998";
    }
}
