package uk.co.weft.wordsearch;

import java.lang.*;		// import general utility classes
import java.util.*;		// for date handling
import java.awt.*;		// for graphics handling
import java.io.*;
import java.applet.*;		// this *is* an applet
import java.net.*;		// for URL class

/** an exception flagged when we completely fail to lay out a word */
final class LayoutException extends Exception
{
    LayoutException( String message)
    {
	super( message);
    }
}

/** an exception flagged when it is impossible to lay out a word */
final class WontFitException extends Exception
{
    WontFitException( String message)
    {
	super( message);
    }
}

final class GridView extends Canvas
{
    protected WordSearch parent;
    private Image buffer = null;	// my buffer into which I render myself
    private Graphics bufferGraphics;	// a graphics context for my buffer
    private Point dragStart = new Point();
    private Point dragEnd = new Point();
    protected Font font = new Font( "Helvetica", Font.BOLD, 14);
    protected Color background = Color.white;
				// my background colour
    protected Color foreground = Color.black;
				// my foreground colour
    protected Color hilight = Color.red;
				// my hilight (drag) colour
    protected Color gridColour = Color.blue;
				// the colour of my grid
    protected int lineWidth = 4;

    GridView( WordSearch wordsearch)
    {
	parent = wordsearch;
    }

    /** First check that my size hasn't changed. If it has, sort out
        my buffer. Then paint. */
    public void update( Graphics g)
    {
	Dimension size = size();

	super.update( g);

	if ( buffer == null)
	    {
		initGraphics();
	    }

	if ( size.width != buffer.getWidth( this) ||
	     size.height != buffer.getHeight( this))
	    {			// *not* funny. I've changed size...
		bufferGraphics.dispose();
				// get rid of my old buffer
		initGraphics();
				// and get a new one
	    }

	paint( bufferGraphics);

        getGraphics().drawImage( buffer, 0, 0, size.width, size.height, this);
    }

    /** If I understand what's happening right, the graphics can't be
        initialised until I have been added to my enclosing
        component. Consequently the method is construct me, add me, then
        init me */
    protected void initGraphics()
    {
	Dimension size = size();

	try
	    {
		buffer = createImage( size.width, size.height);
				// grab an off screen buffer for painting

		bufferGraphics = buffer.getGraphics();
				// and get a handle on it's graphics context.
	    }
	catch ( NullPointerException e)
	    {
		throw new NullPointerException( "createImage( " + size.width 
						+ ", " + size.height + 
						") returned null!?"); 
	    }
	catch ( IllegalArgumentException f)
	    {			// whattee dis ting is?
		throw new IllegalArgumentException( "Size: (" + size.width + 
						    "," + size.height + ")");
	    }
    }

    /** Repaint me on my graphics buffer to avoid flickering
     */
    public void paint( Graphics g)
    {
	Dimension size = getSize();
	int xstep = ( int) size.width / parent.gridX;
	int ystep = ( int) size.height / parent.gridY;

	g.setFont( font);
	FontMetrics fm = g.getFontMetrics();
	int fh = fm.getHeight();
	int fw = fm.charWidth( 'M');

	int fox = ( int)( xstep - fw) / 2;
				// font offset in the X dimension.
	int foy = ystep - ( ( int)( ystep - fh) / 2);
				// font offset in the Y dimension.

	g.setColor( background);
	g.fillRect( 0, 0, size.width, size.height);

	Enumeration words = parent.words.elements();

	while ( words.hasMoreElements())
	    {
		Word w = ( Word) words.nextElement();

		if ( w.found)
		    {
			int sx = ( int)( ( w.start.x + 0.5) * xstep),
			    sy = ( int)( ( w.start.y + 0.5) * ystep),
			    ex = sx + ( ( ( w.word.length() - 1) * w.delta.x) 
					* xstep),
			    ey = sy + ( ( ( w.word.length() - 1) * w.delta.y) 
				* ystep);
				// yes, I'm sure there's a simpler way, too...

			g.setColor( hilight);

			drawLine( g, new Point( sx, sy), new Point( ex, ey), 
				  lineWidth);
		    }
	    }

	g.setColor( gridColour);

	for ( int i = 0; i < size.width; i += xstep)
	    g.drawLine( i, 0, i, ystep * parent.gridY);

	for ( int i = 0; i < size.height; i += ystep)
	    g.drawLine( 0, i, xstep * parent.gridX, i);

	g.setColor( foreground);

	for ( int i = 0; i < parent.gridX; i ++)
	    for ( int j = 0; j < parent.gridY; j++)
		g.drawChars( parent.grid[ i], j, 1, 
					  ( i * xstep) + fox,
					  ( j * ystep) + foy);
    }

    /** Convert the pixel x, y co-ordinates of this point to equivalent
     *  grid x, y and return the point
     *
     *  @param p a point with pixel coordinates
     *  @return the point that was passed, suitably ammended */
    protected Point pixToGrid( Point p)
    {
	Dimension size = getSize();
	int xstep = ( int) size.width / parent.gridX;
	int ystep = ( int) size.height / parent.gridY;

	p.x /= xstep;
	p.y /= ystep;

	return p;
    }

    /** Convert the pixel x, y co-ordinates of this point to equivalent
     *  grid x, y and return the point
     *
     *  @param p a point with grid coordinates
     *  @return the point that was passed, suitably ammended */
    protected Point gridToPix( Point p)
    {
	Dimension size = getSize();
	int xstep = ( int) size.width / parent.gridX;
	int ystep = ( int) size.height / parent.gridY;

	p.x *= xstep; p.x += ( xstep / 2);
	p.y *= ystep; p.y += ( ystep / 2); 

	return p;
    }

    /** Respond to mouse clicks */
    public boolean mouseDown( Event e, int x, int y) 
    {
	Point p = new Point( x, y);

	pixToGrid( p);
	dragStart = gridToPix( p); // 'normalise' the start point on the grid
	dragEnd = dragStart;

	return true;
    }

    /** Respond to mouse drags */
    public boolean mouseDrag( Event e, int x, int y) 
    {
	Graphics g = getGraphics();
	Point p = new Point( x, y);

	g.setXORMode( hilight);
	drawLine( g, dragStart, dragEnd, lineWidth);

	pixToGrid( p);
	dragEnd = gridToPix( p);
	drawLine( g, dragStart, dragEnd, lineWidth);

	g.setPaintMode();

	return true;
    }

    /** respond to mouse up */
    public boolean mouseUp( Event e, int x, int y)
    {
	Graphics g = getGraphics();
	boolean found = false;

	dragEnd = pixToGrid( new Point( x, y));
	dragStart = pixToGrid( dragStart);

	Enumeration words = parent.words.elements();

	while ( words.hasMoreElements() && !found)
	    {
		Word word = ( Word) words.nextElement();

		if ( word.start.x == dragStart.x && 
		     word.start.y == dragStart.y &&
		     word.end.x == dragEnd.x &&
		     word.end.y == dragEnd.y)
		    {
			word.found = true;
			found = true;

			parent.wordList.remove( word.word);
			parent.toFind.removeElement( word);
			parent.maybeWin();
		    }
	    }
	repaint();
			
	return found;
    }

    /** a hack at drawing a wide line - actually an extruded cross.
     *  I know I could do better if I were prepared to mess with trig.
     *
     *  @param g the graphics objecton which to draw.
     *  @param start the point from which the line should start
     *  @param end the point at which it should end
     *  @param width the width of the line (ish) 
     */
    private void drawLine( Graphics g, Point start, Point end, int width)
    {
	int[] x = new int[ 4];
	int[] y = new int[ 4];		// my vertices...
	int delta = ( int) width/2;

	x[ 0] = start.x + delta;	y[ 0] = start.y;
	x[ 1] = start.x - delta;	y[ 1] = start.y;
	x[ 2] = end.x - delta;		y[ 2] = end.y;
	x[ 3] = end.x + delta;		y[ 3] = end.y;
	    
	g.fillPolygon( x, y, 4);

	x[ 0] = start.x;	y[ 0] = start.y + delta;
	x[ 1] = start.x;	y[ 1] = start.y - delta;
	x[ 2] = end.x;		y[ 2] = end.y - delta;
	x[ 3] = end.x;		y[ 3] = end.y + delta;
	    
	g.fillPolygon( x, y, 4);
    }
}


/** a wrapper round a string with it's position in the grid */
final class Word
{
    Point start = new Point( 0,0);	// position of start
    Point end = new Point( 0,0);	// amount of change each character
    Point delta = new Point( 0,0);	// position of end

    boolean found = false;	// whether this word has been found
    int maxPlaceTries = 200;	// how many time I try to place this word
				// before I give up

    String word;		// the actual word
    WordSearch ws;		// the wordsearch object I belong to

    Word( String word, WordSearch ws)
    {
	this.word = word.toUpperCase();
	this.ws = ws;
    }

    /** attempt to place this word; try repeatedly; if at last you
     *  don't succeed, give up in disgust.
     *  @return whether or not place was successful
     */
    protected void place() throws LayoutException, WontFitException
    {
	boolean placed = false;
	int l = word.length() - 1;
	found = false;

	if ( l > ws.gridX || l > ws.gridY)
	    throw ( new WontFitException( "Word [" + word + 
					"] too long for this grid"));
	else
	    for ( int tries = 0; ( tries < maxPlaceTries) && ! placed; 
		  tries ++)
		{
		    start.x = Math.abs( ws.random.nextInt()) % ws.gridX;
		    start.y = Math.abs( ws.random.nextInt()) % ws.gridY;
		    delta.x = ( Math.abs( ws.random.nextInt()) % 3) -1;
		    delta.y = ( Math.abs( ws.random.nextInt()) % 3) -1;
	
		    end.x = start.x + ( delta.x * l);
		    end.y = start.y + ( delta.y * l);

		    if ( start.x >= 0 && start.y >= 0 && start.x < ws.gridX
			 && start.y < ws.gridY && end.x >= 0 && end.y >= 0 && 
			 end.x < ws.gridX && end.y < ws.gridY &&
			 ( delta.x != 0 || delta.y != 0))
				// it fits in the box...
			try
			    {
				if ( maybePlace( false))
				    {
					placed = maybePlace( true);
				    }
			    }
			catch ( ArrayIndexOutOfBoundsException e)
			    {
				placed = false;
			    }
				// Shouldn't happen. Just go round again...
		}

	if ( ! placed)
	    throw ( new LayoutException( "Couldn't place [" + word +
					 "] after " + maxPlaceTries + 
					 " tries"));
    }
		

    /** try to inser this word into the grid in this position. 
     *  @param write if true, actually set the characters while trying
     *  @return whether or not it was possible.
     */
    protected boolean maybePlace( boolean write)
    {
	boolean placed = true;		// assume we can...
	int l = word.length();

	for ( int i = 0; i < l && placed; i++)
	    {
		int cx = start.x + ( delta.x * i);
		int cy = start.y + ( delta.y * i);
		char oldc = ws.grid[ cx][cy];
		char newc = word.charAt( i);

		if ( cx >= 0 && cx < ws.gridX &&
		     cy >= 0 && cy < ws.gridY)
		    {
			if ( oldc == ws.blankChar || oldc == newc)
			    {
				if ( write) 
				    ws.grid[ cx][cy] = newc;
			    }
			else
			    placed = false;
				// unless we find we can't.
		    }
		else
		    placed = false;
				// if we walk off the visible grid
				// we're out, too.
	    }
	return placed;
    }
}
    

/** a wordsearch puzzle for use as eye-candy on a web page. Not
 *  intended to be extensible or adaptable, uses too many hacks and
 *  bodges. The following parameters are recognised (all default
 *  sensibly):</p>
 *
 *   <ul>
 *     <li>Colours (must be set as siz-digit hexadecimal values)
 *   
 *	<ul>
 *	  <li>background</li>
 *	  <li>    foreground</li>
 *	  <li>    grid_background</li>
 *	  <li>    grid_foreground</li>
 *	  <li>    highlight</li>
 *	  <li>    gridcolour</li></ul></li>
 *
 *      <li>    Integers
 *	<dl>
 *	  <dt>grid_x</dt>
 *	  <dd>    the number of cells horizontally in the grid, maximum 64</dd>
 *	  <dt>    grid_y</dt>
 *	  <dd>    the number of cells vertically in the grid, maximum 64</dd>
 *	  <dt>    mark_width</dt>
 *	  <dd>    the width in pixels of the line used to mark words</dd>
 *	  <dt>    font_size</dt>
 *	  <dd>    the size in points of the font used in the grid. Used only if
 *		  supplied in conjunction with a valid font_name.</dd>
 *       </dl>
 *     </li>
 *
 *     <li>    Strings
 *	<dl>
 *	  <dt>font_name</dt>
 *	  <dd>    the name of the font to use in the grid. One of
 *	    <ul>
 *	      <li>TimesRoman</li>
 *	      <li>	    Helvetica</li>
 *	      <li>	    Courier</li>
 *	      <li>	    Dialog</li>
 *	      <li>	    DialogInput</li></ul>
 *	    Used only if supplied in conjunction 
 *	    with a valid font_size.</dd>
 *	  <dt>    reward_url</dt>
 *	  <dd>    the URL of the page to jump to as reward on
 *	    successful completion of the puzzle</dd>
 *	  <dt>	  welcome</dt>
 *	  <dd>	the message to show when the applet is initialised. Note that
 *	    this does not get wordwrapped, so if you want neat formatting
 *	    count your spaces!</dd>
 *	  <dt>    words</dt>
 *	  <dd>    a space separated list of words to use in the
 *	    puzzle. We strongly advise against using words less than
 *	    four characters long - there are two of these in our test
 *	    set and I'm sure you will agree that they are hardest to find!</dd>
 *	</dl>
 *     </li>
 *   </ul>
 */

public final class WordSearch extends Applet
{
    Color background = Color.white;
				// my background colour
    Color foreground = Color.black;
				// my foreground colour

    Dimension size;		// how big I am!

    int gridX = 64;		// how big my grid is in X and Y directions
    int gridY = 64;
    char grid[][] = new char[ gridX][ gridY];
				// the grid itself

    GridView gridView = null;	// the user's view of the grid
    List wordList = null;	// the user's view of the wordlist
    Button reset = null;	// the reset button

    char blankChar = ' ';	// the character considered to be 'null'

    private final String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
				// the alphabet, no less!

    Random random = new Random();

    URL rewardURL = null;	// the URL to jump to if the user wins

    String welcome = "Welcome! Click 'Start' to play";
    Vector words = new Vector();
    Vector toFind = new Vector();
    String defaultWords = "officinale beesianum nudiflorum";

    /** initialise myself. Mainly, over-ride default values of my instance
	variables from the calling document */
    public void init()
    {
	super.init();

	/* set up my components */
	gridView = new GridView( this);
	wordList = new java.awt.List();
	reset = new Button( "Start");

	/* set up my colours */
	background = paramColour( "background", background);
	foreground = paramColour( "foreground", foreground);
	gridView.background = paramColour( "grid_background", background);
	gridView.foreground = paramColour( "grid_foreground", foreground);
	gridView.hilight = paramColour( "highlight", gridView.foreground);
	gridView.gridColour = paramColour( "gridcolour", gridView.foreground);

	setBackground( background );
				// set the background colour

	/* set up all my other configurable stuff */
	try			// you can make the grid smaller but not 
	    {			// bigger
		int x = Integer.parseInt( getParameter( "grid_x"));
		    
		if ( x < gridX) gridX = x;
	    }
	catch ( Exception nx){}
				// never mind
	try 
	    {
		int y = Integer.parseInt( getParameter( "grid_y"));
		    
		if ( y < gridY) gridY = y;
	    }
	catch ( Exception ny){}
				// never mind
	try 
	    {
		gridView.lineWidth = 
		    Integer.parseInt( getParameter( "mark_width"));
	    }
	catch ( Exception lw){}
				// never mind

	try 
	    {
		int h = Integer.parseInt( getParameter( "font_size"));
		String s = getParameter( "font_name");

		gridView.font = new Font( s, Font.BOLD, h);
	    }
	catch ( Exception fx){}
				// never mind

	try
	    {
		rewardURL = new URL( getDocumentBase(), 
				     getParameter( "reward_url"));
	    }
	catch ( Exception ru){}
				// never mind

	String w = getParameter( "welcome");
	if ( w != null && w.length() > 0)
	    welcome = w;

	fillGrid( true);	// initialise the grid to empty
	showMessage( welcome);

	GridBagLayout gridbag  =  new  GridBagLayout();
	setLayout( gridbag);	

	GridBagConstraints  c  =  new  GridBagConstraints();
	c.fill  =  GridBagConstraints.BOTH;
	c.gridwidth = GridBagConstraints.RELATIVE;
	c.anchor = GridBagConstraints.SOUTH;
				// basic settings of gridbag constraints

	c.gridx = 0;		// the grid is in the left...
	c.gridy = 0;		// top corner of the GridBag...
	c.weightx = 3;		// and it wants about three-quarters of
				// the available width
 	c.gridheight = 2;	// it spans the height of the two
				// components on the right
	c.weighty = 5;		// and uses five fifthe the available height

	gridbag.setConstraints( gridView, c);
	add( gridView);		// set up the grid viewer, and add it.

	c.gridx = 1;		// the list is to the right...
	c.gridy = 0;		// top corner of the GridBag...
	c.weightx = 1;		// and it wants about a quarter of
				// the available width...
	c.gridheight = 1;
	c.weighty = 4;		// and four fifths of the available height
	gridbag.setConstraints( wordList, c);
	add( wordList);

	c.gridx = 1;		// the list is to the right...
	c.gridy = 1;		// top corner of the GridBag...
	c.weightx = 1;		// and it wants about a quarter of
				// the available width...
	c.weighty = 1;		// and four fifths of the available height
	gridbag.setConstraints( reset, c);
	add( reset);

	w = getParameter( "words");
	if ( w == null) w = defaultWords;
	StringTokenizer tok = new StringTokenizer( w);

	while ( tok.hasMoreTokens())
	    {
		Word word = new Word( tok.nextToken(), this);
		
		words.addElement( word);
		toFind.addElement( word);
	    }

	addNotify();		// May not be necessary?
    }


    /** fill unfilled elements in the grid with characters.
     *
     *  @param clear if set, clear the grid to known default character.
     */
    protected void fillGrid( boolean clear)
    {
	for ( int x = 0; x < gridX; x ++)
	    for ( int y = 0; y < gridY; y ++)
		if ( clear)
		    grid[ x][y] = blankChar;
		else
		    {
			if ( grid[ x][y] == blankChar)
			    {
				int r = Math.abs( random.nextInt() % 
						  alphabet.length());

				grid[ x][ y] = alphabet.charAt( r);
			    }
		    }
    }


    /** reset the grid */
    protected void reset() throws WontFitException
    {
	boolean placed = false;
	int maxLayoutTries = 25;
	Enumeration w;

	for ( int tries = 0; ( tries < maxLayoutTries) && ! placed; tries ++)
	    {
		placed = true;	// assume we will...

		try
		    {
			w = words.elements();

			fillGrid( true);

			wordList.removeAll();
			
			while ( w.hasMoreElements())
			    {
				Word word = ( Word) w.nextElement();

				word.place();
				wordList.add( word.word);
			    }
		    }
		catch ( LayoutException l)
		    {
			placed = false;
				// unless we don't.
		    }
	    }

	if ( ! placed)
	    throw ( new WontFitException( "Failed to place wordset after "
					  + maxLayoutTries + " tries"));

	fillGrid( false);

			       
	toFind.removeAllElements(); // clear...
	w = words.elements();
	while ( w.hasMoreElements()) // and refill the 'toFind' list
	    toFind.addElement( w.nextElement());

	gridView.repaint( );
				// invalidate the area of the grid.

	reset.setLabel( "Reset");
    }


    /** handle a button click action */
    public boolean action(Event event, Object arg) 
    {
	boolean result = false;

	try
	    {
		if ( event.target == reset) 
		    {
			if ( toFind.isEmpty())
			    getAppletContext().showDocument( rewardURL);
			else
			    reset();
			result = true;
		    }
		
		if ( event.target == wordList)
		    {
			wordList.handleEvent( event);
			
			String w = wordList.getSelectedItem();

			Enumeration e = words.elements();

			while ( e.hasMoreElements() && ! result)
			    {
				Word word = ( Word) e.nextElement();

				if ( word.word.equalsIgnoreCase( w))
				    {
					result = true;
					word.found = true;
					wordList.remove( word.word);
				// cheat = true!
					reset.setLabel( "Cheated!");
					repaint();
				    }
			    }
		    }
	    }
	catch ( Exception e)	// oh, whoops!
	    {
		String m = e.getMessage();

	    }
	
	return result;
    }


    /** check whether the use has won, and if so show congratulatory
     *  message */
    protected boolean maybeWin()
    {
	if ( toFind.isEmpty())
	    {
		showMessage( "Congratulations!");
		reset.setLabel( "Go");
		return true;
	    }
	else return false;
    }
		    


    /** show a message in the grid. Not very elegant, doesn't
     *  word-wrap or format
     *
     *  @param message the message to show */
    protected void showMessage( String message)
    {
	fillGrid( true);	// clear the grid
		
	Enumeration e = words.elements();
				// clear all the found flags so the
				// words don't underline. Yes, this
				// *is* a hack.

	while ( e.hasMoreElements())
	    {
		( ( Word) e.nextElement()).found = false;
	    }

	for ( int i = 0; i < message.length(); i ++)
	    {			// show the message
		grid[ ( int)( i % gridX)][ ( int)( i / gridX)] =
		    message.charAt( i);
	    } // ugly, but 'twill serve

	gridView.repaint();
    }


    /** pass requests to update me on to all my components */
    public void update( Graphics g)
    {
	super.update( g);
	
	Component[] bits = getComponents();
	int i;

	gridView.update( g);

    }


    /** read a colour from a hex paramater in the form RRGGBB;
     *  if found, return a that colour, else the stated default.
     *
     *  @param arg the name of the applet parameter from which to read
     *  the colour 
     *  @param defaultCol the colour to use if we fail to interpret the 
     *  parameter
     */
    public Color paramColour( String arg, Color defaultCol)
    {
	int r = 0, g = 0, b = 0; // colour components
	String value;		// a string to read paramater values into
	Color colour = defaultCol;

	value = getParameter( arg);

	if ( value != null)
	    {
		try
		    {
			r = Integer.parseInt( value, 16);

			b = r % 256;	
				// strip off the bottom byte into blue...
			r /= 256;	// divide down...
			g = r % 256;	// strip the next byte into green...
			r /= 256;
				// divide down to leave the top byte in red...

			colour = new Color( r, g, b);
		    }
		catch ( NumberFormatException e)
		    {
			showStatus( "ERROR: " + arg + 
					": bad number format: " +
					e.getMessage());
		    }
	    }
	return colour;		// return the colour we found
    }

    /** talk about myself 
     *
     *  @return a string describing myself.
     */
    public String getAppletInfo()
    {
	return "WordSearch applet (c) 2000 Weft Technology Ltd";
    }

}
