//
//              Javasaw applet by Rob Griffin
//
//  Implements a jigsaw based on a single image. Pieces are rectangular and
//  of varying dimensions. A grid is displayed where the pieces have to be
//  placed, the user drags them with the mouse to move them, and when the
//  the button is released the pieces 'dock' if near enough to the grid and
//  of the correct size.
//
//  Some classes are based on the work of other people, these are noted as such.

import java.applet.*;
import java.awt.*;
import java.awt.image.*;
import java.lang.Math;
import java.net.*;
import java.util.Date;
import java.util.Vector;
import java.util.StringTokenizer;
import java.io.*;
import java.lang.Class;

public class Javasaw extends Applet implements Runnable, ImageObserver
{
    CardLayout         cardLayout;
    Button             cheatButton;
    JavasawCheatWindow cheatWindow;         // Window that shows completed puzzle
    Label              elaspedTimeDisplayLabel;
    Image              entireImage[];       // Image/s that puzzle is based on
    Choice             imageChoice;
    Vector             imageFileName;       // File names of images
    Label              imageLabel;
    int                imageNo;
    int                imageWidth = -1;
    int                imageHeight = -1;
    int                imageState;
    Label              label;               // Label showing select a piece
    int                noOfSides = 1;       // Number of 'Sides' to each piece
    Button             pauseButton;
    int                pcBefore;
    boolean            playing = false;
    JavasawPlayingArea playingArea;
    JavasawToolbar     toolbar;
    Button             restartButton;
    Vector             imageSizeInPixels;
    int                pixelsToLoad;
    boolean            browserIsNetscape = false;
    int                pixelsLoaded;
    String             selectedImageName;
    boolean            showLoading;
    Checkbox           skillCheck[];
    Label              skillInUseLabel;
    Label              skillSelectionLabel;
    int                skillLevel;
    Button             startButton;
    Thread             thisThread;          // Thread
    public long        timeLastSecond = 0;
    private long       top10Time[];
    private long       bestTime[];
    private int        pixels[] = null;
    private int        revPixels[] = null;

    String skill[] = {  "Simple" ,
                        "Easy",
                        "Moderate",
                        "Harder",
                        "Difficult",
                        "Most Difficult"};

    String puzzleDesc[] = {
           "Puzzle has 9 single sided pieces, unique sizes   ",
           "Puzzle has 20 single sided pieces, unique sizes  ",
           "Puzzle has 36 single sided pieces, random sizes  ",
           "Puzzle has 36 double sided pieces, similar sizes  ",
           "Puzzle has 56 double sided pieces, similar sizes  ",
           "Puzzle has 56 double sided, rotatable pieces, similar sizes  " };

    /**
     * Over-ridden Component method.
     *
     * Handle actions (button presses) in the applet
     *
     * @param ev  The event generated by the AWT
     * @param arg What
     * @return    True if event was handled
     **/
    public boolean action(Event ev, Object arg)
    {
        if (playingArea.isDialogActive())
            return false;

        if (ev.target == cheatButton)
        {
            showCheatWindow();
            return true;
        }

        if (ev.target == pauseButton)
        {
            pausedPressed();
            return true;
        }

        if (ev.target == restartButton)
        {
            if (cheatWindow != null)
            {
                cheatWindow.dispose();
                cheatWindow = null;
            }

            if (playingArea.isPaused())
                pausedPressed();

            selectAnotherPuzzle();
            return true;
        }

        return super.action(ev,arg);
    }

    /**
     * Called by cheat window when it closes to let applet know it has gone
     **/
    public void cheatWindowClosed()
    {
        cheatWindow = null;
    }

    /**
     * Over-ridden Applet method.
     *
     * When the applet is destroyed close the cheat window too
     **/
    public void destroy()
    {
        if (cheatWindow != null)
            cheatWindow.dispose();

        playingArea.freeResources();
    }

    /**
     * Over-ridden Component method.
     *
     * Force a full paint in case the user returned focus to this applet by clicking on a
     * piece. In this case the paint method sent by the AWT can come after the mousedown
     * event is handled by which time the pieceBeingPainted variable is set and thus the
     * full screen is not repainted. {:(
     *
     * @param evt  The event generated by the AWT
     * @param what
     * @return    True if event was handled
     **/
    public boolean gotFocus(Event evt, Object what)
    {
        if (playing && playingArea != null)
            playingArea.forceFullPaint();
        return false;
    }

    /**
     * Over-ridden Component method.
     *
     * Used by the selection process
     *
     *
     * @param e The event generated by the AWT
     * @return  True if the event is handled
     **/
    public boolean handleEvent(Event e)
    {
        if (e.target == startButton)
        {
            // If the user clicks the start button or presses Enter while on it then go

            if (e.id == Event.ACTION_EVENT
                || (e.id == Event.KEY_PRESS && e.key == 10) )
            {
                startButtonPressed();
                return true;
            }
            // The Tab or up or down keys move from the button to the selected checkbox

            else if (   (e.id == Event.KEY_PRESS && e.key == 9 )
                     || e.key == Event.UP || e.key == Event.DOWN )
                for (int i = 0; i < skillCheck.length; i++)
                    if (skillCheck[i].getState())
                    {
                        skillCheck[i].requestFocus();
                        break;
                    }
        }

        if (e.target instanceof Checkbox)
        {
            if (e.id == Event.ACTION_EVENT && e.arg instanceof Boolean)
                for (int i = 0; i < puzzleDesc.length; i++)
                    if (((Boolean)e.arg).booleanValue() && e.target == skillCheck[i])
                        skillSelectionLabel.setText(puzzleDesc[i]);

            // The tab key moves to the start button

            if (e.id == Event.KEY_PRESS)
                if (e.key == 9)
                    startButton.requestFocus();

            // The Up and DOWN keys move up and down the checkbox group

            if (e.id == Event.KEY_ACTION)
            {
                for (int i = 0; i < skillCheck.length; i++)
                    if (e.target == skillCheck[i])
                    {
                        if (e.key == Event.UP && i > 0)
                        {
                            skillCheck[i-1].requestFocus();
                            break;
                        }
                        if (e.key == Event.DOWN)
                            if (i == (skillCheck.length - 1))
                            {
                                startButton.requestFocus();
                                break;
                            }
                            else
                            {
                                skillCheck[i+1].requestFocus();
                                break;
                            }
                    }
             }

        }
        return super.handleEvent(e);
    }

    /**
     * Method for ImageObserver interface
     *
     * Calling prepareImage causes this method to be repeatedly called as the image is
     * transmitted. Only the main image is loaded via prepareImage. This is done so that
     * a % loaded message can be shown
     *
     * @param theImage    The image being loaded
     * @param infoflags   Status about the image
     * @param x           The x position of the image portion being processed
     * @param y           The y position of the image portion being processed
     * @param width       The width of the image portion being processed
     * @param height      The height of the image portion being processed
     * @return            Duh?
     **/
    public boolean imageUpdate(Image theImage,
                               int infoflags,
                               int x,
                               int y,
                               int width,
                               int height)
    {
        if ((infoflags & ImageObserver.SOMEBITS) != 0)   // Still loading?
            if (showLoading)   // Still loading?
            {
                if (pixelsToLoad == 0)   // Know how many pixels are coming down the wire?
                    showStatus("Loading image - line # " + y); // Nup
                else
                {
                    pixelsLoaded += (width * height);
                    int pc = Math.min((pixelsLoaded * 100) / pixelsToLoad,100);
                    if (pc != pcBefore)
                    {
                        pcBefore = pc;
                        showStatus("Loading image - " + pc + "% complete");
                    }
                }
            }

        imageState = infoflags;
        return true;
    }

    /**
     * Over-ridden Applet method.
     *
     * Init the applet, create the selection panel, the playing area
     * and a card layout to control them.
     **/
    public void init()
    {
        int ix;
        int pos;
        String imageDetails;

        top10Time = new long[6];
        bestTime = new long[6];
        String timeParam = null;

        for (int ti=0; ti < 6; ti++)
        {
            timeParam=getParameter("Top10." + ti);
            if (timeParam == null)
                top10Time[ti] = 0;
            else
                top10Time[ti] = getMinSec(timeParam);

            timeParam=getParameter("Best." + ti);
            if (timeParam == null)
                bestTime[ti] = 0;
            else
                bestTime[ti] = getMinSec(timeParam);
        }


        imageFileName = new Vector(10,10);
        imageSizeInPixels = new Vector(10,10);

        entireImage = new Image[2];
        entireImage[0]=null;
        entireImage[1]=null;

        imageChoice = new Choice();

        // The list of images is in the format
        //    Image0 = "filename,description,size in pixels"

        ix = 0;
        while (true)
        {
            imageDetails = getParameter("Image" + (ix+1));
            if (imageDetails == null)
                break;

            ix++;

            pos = imageDetails.indexOf(",");
            if (pos < 0)  // No comma?
            {
                // No separate desciption, use filename
                imageFileName.addElement(imageDetails);
                imageChoice.addItem(imageDetails);
                // No size in pixels, use 0
                imageSizeInPixels.addElement(new Integer(0));
            }
            else
            {
                imageFileName.addElement(imageDetails.substring(0,pos));
                String remain = imageDetails.substring(pos+1);
                pos = remain.indexOf(",");
                if (pos < 0)
                {
                    // No size in pixels, use 0
                    imageChoice.addItem(remain);
                    imageSizeInPixels.addElement(new Integer(0));
                }
                else
                {
                    // Store size in pixels
                    imageChoice.addItem(remain.substring(0,pos));
                    imageSizeInPixels.addElement(new Integer(remain.substring(pos+1)));
                }
            }
        }

        cheatWindow = null;

        cardLayout = new CardLayout();
        setLayout(cardLayout);

        Panel selectionPanel = new Panel();
        add("select",selectionPanel);

        GridBagLayout selectLayMan = new GridBagLayout();
        selectionPanel.setLayout(selectLayMan);
        GridBagConstraints con = new GridBagConstraints();

        Panel skillPanel = new Panel();
        skillPanel.setLayout(new GridLayout(skill.length+1,1));
        skillCheck = new Checkbox[skill.length];
        CheckboxGroup group = new CheckboxGroup();
        for (int i = 0; i < skill.length; i++)
        {
            skillCheck[i] = new Checkbox(skill[i],group,(i==0 ? true : false));
            skillPanel.add(skillCheck[i]);
        }
        skillSelectionLabel = new Label(puzzleDesc[skill.length-1]);  // Use longest description for layout purposes
        skillPanel.add(skillSelectionLabel);

        JavasawGroupBox skillBox = new JavasawGroupBox(skillPanel,"Select the puzzle complexity");

        con.anchor = GridBagConstraints.WEST;
        con.gridwidth = GridBagConstraints.REMAINDER;
        selectLayMan.setConstraints(skillBox,con);
        selectionPanel.add(skillBox);

        con.insets = new Insets(2,0,2,0);
        con.anchor = GridBagConstraints.EAST;
        con.gridwidth = GridBagConstraints.RELATIVE;

        Label imageFileNameLabel = new Label("Image name:");
        selectLayMan.setConstraints(imageFileNameLabel,con);
        selectionPanel.add(imageFileNameLabel);

        con.fill = GridBagConstraints.NONE;
        con.anchor = GridBagConstraints.WEST;
        con.gridwidth = GridBagConstraints.REMAINDER;

        selectLayMan.setConstraints(imageChoice,con);
        selectionPanel.add(imageChoice);

        con.anchor = GridBagConstraints.SOUTH;
        startButton = new Button("Start");
        selectLayMan.setConstraints(startButton,con);
        selectionPanel.add(startButton);

        // Set up the playing surface on a different 'card'
        toolbar = new JavasawToolbar();
        add("play",toolbar);
        GridBagLayout playLayMan = new GridBagLayout();
        toolbar.setLayout(playLayMan);

        cheatButton = new Button("Cheat");
        con.fill = GridBagConstraints.NONE;
        con.insets = new Insets(2,2,3,0);
        con.anchor = GridBagConstraints.NORTHWEST;
        con.gridwidth = 1;
        con.gridheight = GridBagConstraints.RELATIVE;
        playLayMan.setConstraints(cheatButton,con);
        toolbar.add(cheatButton);

        pauseButton = new Button("Continue");
        playLayMan.setConstraints(pauseButton,con);
        toolbar.add(pauseButton);

        restartButton = new Button("Restart");
        playLayMan.setConstraints(restartButton,con);
        toolbar.add(restartButton);

        Label elapsedWords = new Label("Elapsed time:",Label.RIGHT);
        con.insets = new Insets(2,0,3,0);
        playLayMan.setConstraints(elapsedWords,con);
        toolbar.add(elapsedWords);

        elaspedTimeDisplayLabel = new Label("99:99:99");
        playLayMan.setConstraints(elaspedTimeDisplayLabel,con);
        toolbar.add(elaspedTimeDisplayLabel);

        skillInUseLabel = new Label(skill[5]);
        con.insets = new Insets(2,20,3,2);
        playLayMan.setConstraints(skillInUseLabel,con);
        toolbar.add(skillInUseLabel);

        imageLabel = new Label();
        con.insets = new Insets(2,2,3,0);
        con.gridwidth = GridBagConstraints.REMAINDER;
        con.fill = GridBagConstraints.HORIZONTAL;
        playLayMan.setConstraints(imageLabel,con);
        toolbar.add(imageLabel);

        playingArea = new JavasawPlayingArea(this);
        con.anchor = GridBagConstraints.SOUTH;
        con.gridwidth = GridBagConstraints.REMAINDER;
        con.gridheight = GridBagConstraints.REMAINDER;
        con.fill = GridBagConstraints.BOTH;
        con.weightx=100;
        con.weighty=100;
        con.insets = new Insets(0,0,0,0);
        playLayMan.setConstraints(playingArea,con);
        toolbar.add(playingArea);

        // Force the size of the playingArea to be calculated
        cardLayout.last(this);
        layout();
        cardLayout.first(this);
        show();
        toolbar.layout();
        playingArea.init();
        toolbar.setHeight(toolbar.size().height - playingArea.size().height);

        skillCheck[0].requestFocus();
        pauseButton.setLabel("Pause");
        skillSelectionLabel.setText(puzzleDesc[0]);
        playing = false;
    }

    /*
     * Private method. Converts a string of the format mm:ss to a long containing (mm*60) + ss
     *
     * @param timeParam The time as a string
     * @return The time as a long
     **/
    private long getMinSec(String timeParam)
    {
        String mins = null;
        String secs = null;
        int min = 0;
        int sec = 0;
        int pos;

        pos = timeParam.indexOf(":");
        if (pos >= 0)
        {
            mins = timeParam.substring(0,pos);
            secs = timeParam.substring(pos+1);
        }
        else
        {
            mins = "0";
            secs = timeParam;
        }
        try
        {
            min = Integer.parseInt(mins);
        }
        catch (NumberFormatException ex)
        {
            min = 0;
        }

        try
        {
            sec = Integer.parseInt(secs);
        }
        catch (NumberFormatException ex)
        {
            sec = 0;
        }

        return (min * 60) + sec;
    }


    /**
     * Over-ridden Component method.
     * Handle a key down event
     *
     * @param evt  The event generated by the AWT
     * @param key  The key pressed
     * @return     True if event was handled
     **/
    public boolean keyDown(Event evt, int key)
    {
        // See if the playing area wants the event, if so simply return
        if (playingArea.keyPressed(evt,key))
            return true;

        // Check for the c or C keys, show the cheat window if hit
        if (key == 99 || key == 67)
            showCheatWindow();

        return false;
    }

    /*
     * Private method. Action the pause key
     *
     **/
    private void pausedPressed()
    {
        if (playingArea.isPaused())
        {
            pauseButton.setLabel("Pause");
            cheatButton.enable();
        }
        else
        {
            pauseButton.setLabel("Continue");
            cheatButton.disable();
            if (cheatWindow != null)
                cheatWindow.hide();
        }
        playingArea.togglePaused();
    }

    /**
     * Method for Runnable interface. Used to drive the elapsed time display.
     *
     **/
    public void run()
    {
        while (true)
        {
            try
            {
                thisThread.sleep(1000);
                if (playing)
                    tick();
            }
            catch (Exception x) {}
        }
    }

    /**
     * Allow user to select another puzzle to do.
     **/
    public void selectAnotherPuzzle()
    {
        if (cheatWindow != null)
            cheatWindow.hide();

        startButton.enable();
        cardLayout.first(this);
        playing = false;
    }

    /*
     * Private method. Show the cheat window, creating it if necessary
     *
     **/
    private void showCheatWindow()
    {
        if (cheatWindow == null)
        {
            // First time, set up a new cheat window
            cheatWindow = new JavasawCheatWindow(this,entireImage,noOfSides);
            cheatWindow.show();
            Insets insets = cheatWindow.insets();
            cheatWindow.reshape(location().x+playingArea.getGridX()-insets.left,
                location().y+playingArea.getGridY()-insets.top,
                playingArea.getImageWidth()+insets.left+insets.right,
                playingArea.getImageHeight()+insets.top+insets.bottom);
        }
        cheatWindow.show();
        cheatWindow.requestFocus();
    }

    /**
     * Over-ridden Applet method.
     *
     * Start the applet. Create a thread for the elapsed time display to run off.
     **/
    public void start()
    {
        if (thisThread == null)
        {
            thisThread = new Thread(this);
            thisThread.start();
        }
    }

    /**
     * Respond to the start button. Get the image name and the skill level, load
     * the image and set up the playing area.
     **/
    public void startButtonPressed()
    {
        int     piecesAcross =1;        // Number of pieces across puzzle
        int     piecesDown =1;          // Number of pieces down puzzle
        int     pieceSizeType = JavasawPlayingArea.SIZES_UNIQUE; // Unique, Random or Same
        boolean reversed = false;
        boolean rotatable = false;

        if (System.getProperty("java.vendor").toLowerCase().indexOf("netscape") >= 0)
            browserIsNetscape = true;

        startButton.disable();
        String selectedImageFileName;
        noOfSides = 1;

        // wait for image to be prepared because the width & height have to
        // be known

        imageNo = imageChoice.getSelectedIndex();
        if (imageNo < 0)
        {
            imageNo = 0;
            selectedImageName = imageChoice.getItem(0);
        }
        else
            selectedImageName = imageChoice.getSelectedItem();

        selectedImageFileName = (String) imageFileName.elementAt(imageNo);
        if (selectedImageFileName.indexOf(".") < 0)
            selectedImageFileName = selectedImageFileName;

        pixelsToLoad = ((Integer)imageSizeInPixels.elementAt(imageNo)).intValue() - 1;
        pixelsLoaded = 0;
        pcBefore = -1;
        imageWidth = -1;
        imageHeight = -1;

        if (entireImage[0] != null)
        {
            entireImage[0].flush();
            entireImage[0] = null;
        }
        System.gc();
        entireImage[0] = getImage(getDocumentBase(),selectedImageFileName);
        showStatus("Loading image");

        imageState = 0;
        showLoading = true;

        if (1 == 1) //browserIsNetscape)
        {
            if (!prepareImage(entireImage[0],this))
            {
                while ((imageState & ImageObserver.ALLBITS) == 0)
                {
                    try
                    {
                        thisThread.sleep(20);
                    }
                    catch (Exception e2) {}
                }
            }
        }
        else
        {
            MediaTracker imageTracker= new MediaTracker(this);
            imageTracker.addImage(entireImage[0],0);
            try
            {
                imageTracker.waitForID(0);
            }
            catch (InterruptedException e)
            {
                showStatus("tracker exception");
                System.out.println("Exception loading image " + e);
            }
        }

        try
        {
            imageWidth = entireImage[0].getWidth(this);
            if (imageWidth == -1)
            {
                while ((imageState & ImageObserver.WIDTH) == 0)
                {
                    thisThread.sleep(20);
                }
                imageWidth = entireImage[0].getWidth(this);
            }

            imageHeight = entireImage[0].getHeight(this);
            if (imageHeight == -1)
            {
                while ((imageState & ImageObserver.HEIGHT) == 0)
                {
                    thisThread.sleep(20);
                }
                imageHeight = entireImage[0].getHeight(this);
            }

            pixels = new int[imageWidth * imageHeight];
            PixelGrabber pixelGrabber = new PixelGrabber(entireImage[0], 0, 0, imageWidth, imageHeight, pixels, 0, imageWidth);
            pixelGrabber.grabPixels();
        }
        catch (InterruptedException e)
        {
            System.out.println("Exception getting pixels " + e);
        }

        // Set no of pieces, reversable, and rotatable based on skill level
        for (int i = 0; i < skillCheck.length; i++)
            if (skillCheck[i].getState())
            {
                skillLevel = i;
                skillInUseLabel.setText("Level: " + skill[i]);
                switch (i)
                {
                    case 0:
                        piecesAcross = 3;
                        piecesDown = 3;
                        pieceSizeType = JavasawPlayingArea.SIZES_UNIQUE;
                        break;

                    case 1:
                        piecesAcross = 5;
                        piecesDown = 4;
                        pieceSizeType = JavasawPlayingArea.SIZES_UNIQUE;
                        break;

                    case 2:
                        piecesAcross = 6;
                        piecesDown = 6;
                        pieceSizeType = JavasawPlayingArea.SIZES_RANDOM;
                        break;

                    case 3:
                        piecesAcross = 6;
                        piecesDown = 6;
                        pieceSizeType = JavasawPlayingArea.SIZES_SAME;
                        reversed = true;
                        break;

                    case 4:
                        piecesAcross = 8;
                        piecesDown = 7;
                        pieceSizeType = JavasawPlayingArea.SIZES_SAME;
                        reversed = true;
                        break;

                    case 5:
                        piecesAcross = 8;
                        piecesDown = 7;
                        pieceSizeType = JavasawPlayingArea.SIZES_SAME;
                        reversed = true;
                        rotatable = true;
                        break;
                }
                break;
            }

        if (reversed)
        {
            // Reverse the image using the JavasawMirrorImageFilter
            showStatus("Reversing image");
            if (entireImage[1] != null)
            {
                entireImage[1].flush();
                entireImage[1] = null;
            }
            System.gc();
            //JavasawMirrorImageFilter mif = new JavasawMirrorImageFilter();
            imageState = 0;
            showLoading=false;

            // Some strange things happen if you use an image which is constructed
            // with an ImageFilter with another ImageFilter. The original ImageFilter
            // keeps getting called!! Grabbing the pixels of the reversed image and
            // creating another image from these solves the problem.
            try
            {
                revPixels = new int[imageWidth * imageHeight];
                int revPos = imageWidth - 1;
                for (int pos=0; pos < pixels.length; pos++)
                {
                    if (pos % imageWidth == 0)
                        revPos = pos + imageWidth - 1;
                    revPixels[revPos--] = pixels[pos];
                }
                entireImage[1] = createImage(new MemoryImageSource(imageWidth, imageHeight, revPixels, 0, imageWidth));
                MediaTracker imageTracker= new MediaTracker(this);
                imageTracker.addImage(entireImage[1],1);
                imageTracker.waitForID(1);
            }
            catch (InterruptedException e)
            {
                System.out.println("Exception loading image " + e);
            }

            noOfSides++;
        }

        int bracketPos = selectedImageName.indexOf("(");
        if (bracketPos >= 0)
            imageLabel.setText("Image: " +selectedImageName.substring(0,bracketPos-1));
        else
            imageLabel.setText("Image: " +selectedImageName);

        // Have to have widest text on button before layout is called.
        pauseButton.setLabel("Continue");
        toolbar.layout();

        timeLastSecond = System.currentTimeMillis();
        pauseButton.setLabel("Pause");
        elaspedTimeDisplayLabel.setText("");
        if (playingArea.setupJigsaw(entireImage,
            piecesAcross, piecesDown, pieceSizeType, noOfSides, rotatable,
            selectedImageName, skillLevel, top10Time[skillLevel] , bestTime[skillLevel],
            pixels, revPixels ))
        {
            playing = true;
            cardLayout.last(this);
            repaint();
        }
        else
        {
            new JavasawDialog(null,"Unable to create all the pieces. Try selecting less or single sided pieces",
                    "Error",null,null);
            startButton.enable();
        }
    }

    //----------------------------------------------------------------------

    /**
     * Over-ridden Applet method.
     *
     * Stop the thread when the applet is stopped.
     **/
    public void stop()
    {
        if (thisThread != null)
        {
            thisThread.stop();
            thisThread = null;
        }
    }

    /*
     * Private method. Handle timer tick. Used to update elapsed time.
     *
     **/
    private void tick()
    {
        long now = System.currentTimeMillis();
        if (!playingArea.isPaused())
        {
            elaspedTimeDisplayLabel.setText(playingArea.getElaspedAsString());
        }

        // Check for cheating, see if the time has gone backwards
        if (timeLastSecond > now)
            playingArea.showCaughtDialog();
        else
            timeLastSecond = now;
    }
}

//************************************************************************
//
//  Class that represents the cell where the pieces are placed
//

class JavasawCell extends Rectangle
{
    static int              correctlyPlacedPieces;      // correct pieces in place
    static int              placedPieces;               // all pieces in place

    private JavasawPiece    correctPiece;               // The correct piece
    private boolean         leftMost;                   // Cell is at left edge
    private JavasawPiece    pieceInPlace;               // The piece that is placed
    private boolean         topMost;                    // Cell is at top edge
    private int             xRight;                     // X pos of right border
    private int             yBottom;                    // Y pos of bottom border

    /**
     * Constructor. Creates a JavasawCell object.
     *
     * @param correctPiece The correct piece for this cell
     * @param x            The x position of the top left hand corner of the cell
     * @param y            The y position of the top left hand corner of the cell
     * @param width        The width of the cell
     * @param height       The height of the cell
     * @param across       The number of cells across the 'grid' from the left edge
     * @param down         The number of cells down the 'grid' from the left edge
     **/
    public JavasawCell(JavasawPiece correctPiece,int x,int y,int width,int height,
        int across, int down)
    {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        leftMost = (across == 0);
        topMost = (down == 0);
        xRight = x+width-1;
        yBottom = y+height-1;
        pieceInPlace = null;
        this.correctPiece = correctPiece;
    }

    /**
     * Determine if a piece can be placed in this cell
     *
     * @param pieceToPlace The piece that the user is attempting to place in the cell
     * @return True if the piece was placed
     **/
    public boolean canPlacePiece(JavasawPiece pieceToPlace)
    {
        // Can't have two pieces in place
        if (pieceInPlace != null)
            return false;

        // Piece is placed elsewhere, shouldn't happen
        if (pieceToPlace.isPlaced())
            return false;

        // If close enough to edge and size is the same then it fits
        if (   (Math.abs(this.x - pieceToPlace.getX()) <= 20)
            && (Math.abs(this.y - pieceToPlace.getY()) <= 20)
            && (pieceToPlace.getWidth() == width)
            && (pieceToPlace.getHeight() == height)  )
        {
            pieceInPlace = pieceToPlace;
            placedPieces++;
            if (pieceInPlace == correctPiece)
                correctlyPlacedPieces++;

            pieceToPlace.setPlaced(true);
            return true;
        }
        else
            return false;
    }

    /**
     * Determine if a piece can be removed from this cell
     *
     * @param pieceToRemove The piece that the user is attempting to remove from the cell
     * @return True if the piece was removed from the cell
     **/
    public boolean canRemovePiece(JavasawPiece pieceToRemove)
    {
        if (pieceToRemove == pieceInPlace)
        {
            placedPieces--;
            if (pieceInPlace == correctPiece)
                correctlyPlacedPieces--;

            pieceInPlace = null;
            pieceToRemove.setPlaced(false);
            return true;
        }
        return false;
    }

    /**
     * Over-ridden Component method.
     * Paint the cell showing border lines only if no piece is in place.
     *
     * @param g Graphics context passed by the AWT
     **/
    public void paint(Graphics g)
    {
        if (pieceInPlace == null)  // Is cell uncovered?
        {
            if (topMost)
                g.drawLine(x,y,xRight,y);   // horizontal top line
            if (leftMost)
                g.drawLine(x,y,x,yBottom);   // vertical left line
            g.drawLine(x,yBottom,xRight,yBottom); // horizontal bottom line
            g.drawLine(xRight,y,xRight,yBottom);  // vertical right line
        }
    }

    /**
     * Paint the cell at an offset showing border lines only if no piece is in place.
     * Used when the caller is using a separate graphics context that will be drawn at
     * an offset in the playing area.
     *
     * @param g Graphics context passed by the AWT
     * @param xOffset The x offset
     * @param yOffset The y offset
     **/
    public void paint(Graphics g,int xOffset, int yOffset)
    {
        if (pieceInPlace == null)  // Is cell uncovered?
        {
            if (topMost)
                g.drawLine(x-xOffset,y-yOffset,xRight-xOffset,y-yOffset);   // horizontal top line
            if (leftMost)
                g.drawLine(x-xOffset,y-yOffset,x-xOffset,yBottom-yOffset);   // vertical left line
            g.drawLine(x-xOffset,yBottom-yOffset,xRight-xOffset,yBottom-yOffset); // horizontal bottom line
            g.drawLine(xRight-xOffset,y-yOffset,xRight-xOffset,yBottom-yOffset);  // vertical right line
        }
    }


}

//*************************************************************************
//
// Shows the cheat window, which has a completed image of the jigsaw

class JavasawCheatWindow extends Frame
{
    private Image   completedImage[];
    private Javasaw  parentJigsaw;
    private int     sideShown;
    private int     noOfSides;

    /**
     * Constructor Create a JavasawCheatWindow object
     *
     * @param parent The parent applet
     * @param completedImage[] An array of images containing the full (completed) images
     * @param noOfSides How many sides (1-2) the puzzle has.
     **/
    JavasawCheatWindow(Javasaw parent,Image completedImage[],int noOfSides)
    {
        super("Completed jigsaw");
        this.completedImage = completedImage;
        parentJigsaw = parent;
        sideShown = 0;
        this.noOfSides = noOfSides;
    }

    /**
     * Over-ridden Component method. Handle a key down in the frame
     *
     * @param evt  The event generated by the AWT
     * @param key  The key pressed
     * @return     True if event was handled
     **/
    public boolean keyDown(Event evt, int key)
    {
        if (key == 99 || key == 67)      // c or C key?
            parentJigsaw.requestFocus();

        return false;
    }

    /**
     * Over-ridden Component method.
     *
     * Handle the escape key as well as a window close event.
     *
     *
     * @param e The event generated by the AWT
     * @return  True if the event is handled
     **/
    public boolean handleEvent(Event e)
    {
        if (   (e.id == Event.KEY_PRESS && e.key == 27)
            || (e.id == Event.WINDOW_DESTROY) )
        {
            parentJigsaw.cheatWindowClosed();
            dispose();
            return true;
        }
        return super.handleEvent(e);
    }

    /**
     * Over-ridden Component method.
     * Handle a mouse down event. For reversable images mouse down shows the other side.
     *
     * @param e The event generated by the AWT
     * @param x The x position of the mouse cursor
     * @param y The y position of the mouse cursor
     * @return True if the event was handled
     **/
    public boolean mouseDown(Event e, int x, int y)
    {
        if (noOfSides > 1)
        {
            sideShown++;
            if (sideShown == noOfSides)
                sideShown = 0;
            repaint();
        }
        return true;
    }


    /**
     * Over-ridden Component method. Display the image
     *
     * @param g Graphics context passed by the AWT
     **/
    public synchronized void paint(Graphics g)
    {
        Insets insets = insets();
        g.drawImage(completedImage[sideShown],0,0,size().width-+insets.left-insets.right,
            size().height-insets.top-insets.bottom,this);
    }

    /**
     * Over-ridden Component method. Call paint. Over-ridden to prevent flicker.
     *
     * @param g Graphics context passed by the AWT
     **/
    public synchronized void update(Graphics g)
    {
        paint(g);
    }
}

//*************************************************************************
//
// JavasawDialog = dialog class that displays a message
//
class JavasawDialog extends Frame
{
    private JavasawPlayingArea playingArea;
    Button OKButton;
    TextField nameField;

    /**
     * Constructor
     *
     * @param playingArea   The playing area
     * @param message       A message to display
     * @param bigMessage    A message to display in large font
     * @param eMail         An optional e-mail address, may be null
     * @param completionDetails An optional string containg completion details such as
     *   image name, skill level, time, magic number etc.
     **/
    public JavasawDialog(JavasawPlayingArea playingArea,String message,String bigMessage,
                            String eMail,String completionDetails)
    {
        super("Javasaw");
        this.playingArea = playingArea;


        GridBagLayout gridbag = new GridBagLayout();
        Insets inset = new Insets(5,5,5,5);
        GridBagConstraints constrain = new GridBagConstraints();
        setLayout(gridbag);

        Label bigLabel = new Label(bigMessage);
        constrain.weightx = 0;
        constrain.weighty = 0;
        constrain.gridy = GridBagConstraints.RELATIVE;
        constrain.insets = inset;
        constrain.gridwidth = GridBagConstraints.REMAINDER; //end row
        gridbag.setConstraints(bigLabel, constrain);
        add(bigLabel);

        Label label = new Label(message);

        constrain.gridwidth = GridBagConstraints.REMAINDER; //end row
        gridbag.setConstraints(label, constrain);
        add(label);

        if (eMail != null)
        {
            Label txt = new Label("Send me e-mail if you want your time placed into the hall of fame");
            constrain.gridwidth = GridBagConstraints.REMAINDER; //end row
            constrain.weighty = 1.0;
            gridbag.setConstraints(txt, constrain);
            add(txt);

            Label txt2 = new Label("Add your name and send the following details to " + eMail);
            constrain.gridwidth = GridBagConstraints.REMAINDER; //end row
            constrain.weighty = 1.0;
            gridbag.setConstraints(txt2, constrain);
            add(txt2);

            TextArea details = new TextArea(completionDetails);
            add(details);

            constrain.fill = GridBagConstraints.BOTH;
            constrain.insets = new Insets(5,5,5,5);
            constrain.anchor = GridBagConstraints.EAST;
            constrain.gridwidth = GridBagConstraints.REMAINDER; //end row
            gridbag.setConstraints(details, constrain);


            constrain.insets = new Insets(5,5,5,5);
            constrain.anchor = GridBagConstraints.NORTH;
        }

        OKButton = new Button("OK");
        constrain.fill = GridBagConstraints.NONE;
        constrain.gridwidth = GridBagConstraints.REMAINDER; //end row
        constrain.weighty = 1.0;
        gridbag.setConstraints(OKButton, constrain);
        add(OKButton);
        OKButton.requestFocus();

        hide();
        layout();
        pack();

        Font f = bigLabel.getFont();
        bigLabel.setFont(new Font(f.getName(),f.getStyle(),f.getSize()+4));
        layout();

        Dimension prefer = gridbag.preferredLayoutSize(this);
        resize((int)(prefer.width), (int)(prefer.height));

        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        move((screenSize.width-size().width)/2,(screenSize.height-size().height)/2);
        show();
        requestFocus();
    }

    //----------------------------------------------------------------------
    public void setFocus()
    {
        OKButton.requestFocus();
    }

    /**
     * Over-ridden Component method.
     *
     * Handle button click, close window
     *
     * @param ev  The event generated by the AWT
     * @param arg What
     * @return    True if event was handled
     **/
    public boolean action(Event ev, Object arg)
    {
        if (ev.target instanceof Button)
        {
            dispose();
            if (playingArea != null)
                playingArea.dialogClosed();
        }

        return true;
    }
}

//*************************************************************************
// Based on EtchedRectange class in 'Graphic Java' by Geary & McClellan
class JavasawEtchedRect extends Rectangle {

    protected Component drawInto;
    private   Color     lineColor ;

    public JavasawEtchedRect(Component drawInto)
    {
        this.drawInto = drawInto;
        reshape(x,y,width,height);
    }


    /**
     * Get the colour of the line
     *
     * @return The line colour
     **/
    public Color getLineColor()
    {
        if(lineColor == null)
            lineColor = drawInto.getBackground().darker().darker().darker();
        return lineColor;
    }


    /**
     * Make a brighter version of the line colour
     *
     * @return
     **/
    protected Color brighter()
    {
        return getLineColor().brighter().brighter().brighter().brighter();
    }


    /**
     * Paint the EtchedRectange. Note that this is not an AWT event.
     **/
    public void paint()
    {
        Graphics g = drawInto.getGraphics();
        if(g != null)
        {
            int  thickness = 2;
            int  w = width  - thickness;
            int  h = height - thickness;

            g.setColor(getLineColor());
            for(int i=0; i < thickness/2; ++i)
                g.drawRect(x+i, y+i, w, h);

            g.setColor(brighter());

            for(int i=0; i < thickness/2; ++i)
                g.drawRect(x+(thickness/2)+i, y+(thickness/2)+i, w, h);
            g.dispose();
        }
    }
}

//*************************************************************************
// Based on Box class in 'Graphic Java' by Geary & McClellan
class JavasawGroupBox extends Panel
{
    private JavasawEtchedRect box = new JavasawEtchedRect(this);
    private Label           titleLabel;


    /**
     * Constructor. Create a JavasawGroupBox object
     *
     * @param surrounded The component that will be surrounded by the box
     * @param title A title for the group box
     **/
    public JavasawGroupBox(Component surrounded, String title)
    {
        titleLabel = new Label(title, Label.CENTER);

        GridBagLayout      gbl = new GridBagLayout();
        GridBagConstraints gbc = new GridBagConstraints();

        setLayout(gbl);
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        gbc.anchor    = GridBagConstraints.NORTH;
        gbl.setConstraints(titleLabel, gbc);
        add(titleLabel);

        gbc.insets  = new Insets(0,10,10,10);
        gbc.anchor  = GridBagConstraints.CENTER;
        gbc.weighty = 1.0;
        gbc.weightx = 1.0;
        gbc.fill    = GridBagConstraints.BOTH;
        gbl.setConstraints(surrounded,gbc);
        add(surrounded);
    }


    /**
     * Over-ridden Component method.
     * Paint the box
     *
     * @param g Graphics context passed by the AWT
     **/
    public void paint(Graphics g)
    {
        box.paint();
    }           


    /**
     * Over-ridden Component method. Resize a component
     *
     * @param w The new width
     * @param h The new height
     **/
    public void resize(int w, int h)
    {
        reshape(location().x, location().y, w, h);
    }


    /**
     * Over-ridden Component method. Resize and/or move a component
     *
     * @param x The new x position
     * @param y The new y position
     * @param w The new width
     * @param h The new height
     **/
    public void reshape(int x, int y, int w, int h)
    {
        super.reshape(x,y,w,h);

        FontMetrics fm   = titleLabel.getFontMetrics(titleLabel.getFont());
        int         top  = insets().top + fm.getAscent();
        Dimension   size = size();

        box.reshape(0, top, size.width-1, size.height-top-1);
    }
}

//*************************************************************************
// Panel that acts as a toolbar

class JavasawToolbar extends Panel
{
    private int height = 0;


    /**
     * Constructor.  Create a JavasawToolbar object.
     *
     **/
    public JavasawToolbar()
    {
    }

    /**
     * Over-ridden Component method.
     * Paint the cell showing border lines only if no piece is in place.
     *
     * @param g Graphics context passed by the AWT
     **/
    public void paint(Graphics g)
    {
        if (height > 0)
        {
            g.setColor(Color.black);
            g.drawLine(0, height-1, size().width-1, height-1);
        }
    }

    /**
     * Over-ridden Component method. Call paint. Over-ridden to prevent flicker.
     *
     * @param g Graphics context passed by the AWT
     **/
    public void update(Graphics g)
    {
        paint(g);
    }

    /**
     * Set the height of the toolbar.
     *
     * @param height The height (what else?)
     **/
    public void setHeight(int height)
    {
        this.height = height;
    }

}

//*************************************************************************
//
//  Class that represents a jigsaw piece
//

class JavasawPiece
{
    public final static int ROTATE_90 = 90;
    public final static int ROTATE_180 = 180;
    public final static int ROTATE_270  = 270;

    // Static variables
    private static JavasawPiece currentPiece = null;
    private static JavasawPiece pieceInPixelBuffer = null;

    // Instance variables
    private Image pieceImage[];         // Image of piece (multiple sides)
    private int mouseXOffset;           // Mouse X & Y offset within piece
    private int mouseYOffset;           
    private int sideShown;              // Which side is showing
    private int noOfSides;              // How many sides
    private boolean pieceIsPlaced;      // True if piece is placed anywhere
    private Rectangle bounds;           // Position & size of piece
    private int orientation = 0;        // Degrees rotation
    private boolean mouseDown = false;  // True if mouse is down within piece
    private int imageSize = 0;          // Size of image in pixels
    private int pixelBuffer[][];

    public JavasawPiece(int imagePixels[][],int x,int y,int width,int height,int sideShown,
        Component component, int rotation)
            throws Exception
    {
        pixelBuffer = imagePixels;
        noOfSides = imagePixels.length;
        pieceImage = new Image[noOfSides];
        bounds = new Rectangle(x,y,width,height);
        this.sideShown = sideShown;
        pieceInPixelBuffer = this;
        imageSize = bounds.width * bounds.height;
        if (!rotate(component, rotation))
            throw new Exception("Can't create Image");
    }

    /**
     * Clear the mouse offset held in this piece.
     **/
    public void clearMouseOffset()
    {
        mouseXOffset = 0;
        mouseYOffset = 0;
        mouseDown = false;
    }

    /**
     * Move the piece to a new position.
     *
     * @param newX The new x position
     * @param newY ditto
     * @return     A rectangle containing the new co-ordinates.
     **/
    public Rectangle newPosition(int newX, int newY)
    {
        return new Rectangle(newX - mouseXOffset, newY - mouseYOffset, bounds.width, bounds.height) ;
    }

    /**
     * Show the other side of the piece if double sided.
     **/
    public void changeSideShown()
    {
        sideShown++;
        if (sideShown == noOfSides)
            sideShown = 0;
    }


    /**
     * @return The side number that is showing.
     **/
    public int getSideShown()   { return sideShown;   }


    /**
     * @return The x position of the piece
     **/
    public int getX()  { return bounds.x; }
    /**
     * @return The y position of the piece
     **/
    public int getY()  { return bounds.y; }
    /**
     * @return The width of the piece
     **/
    public int getWidth() { return bounds.width; }
    /**
     * @return The height of the piece
     **/
    public int getHeight() { return bounds.height; }


    /**
     * @return True if this is the current piece
     **/
    public boolean isCurrentPiece()
    {
        if (currentPiece == this)
            return true;

        return false;
    }

    /**
     * @return True if this piece is rotated
     **/
    public boolean isRotated()
    {
        if (orientation == 0)
            return false;

        return true;
    }


    /**
     * See if the passed point is inside this piece
     *
     * @param x The x position of the point
     * @param y The y position of the point
     * @return True if point inside piece
     **/
    public boolean inside(int x,int y) { return bounds.inside(x,y); }


    /**
     * See if the passed rectange intersects the piece.
     *
     * @param rect The rectangle to check
     * @return True if rect intersects piece
     **/
    public boolean intersects(Rectangle rect) { return bounds.intersects(rect); }

    /**
     * @return True if piece is placed.
     **/
    public boolean isPlaced() { return pieceIsPlaced; }


    /**
     * Static method.
     *
     * @return The current piece or null if there is none.
     **/
    public static JavasawPiece getCurrentPiece() { return currentPiece; }

    /**
     * Set the clipping
     *
     * @param clippingRegion
     **/
    public void reshapeClippingRegion(Rectangle clippingRegion)
    {
        if (isCurrentPiece())
            clippingRegion.reshape(bounds.x-1, bounds.y-1, bounds.width+2, bounds.height+2);
        else
            clippingRegion.reshape(bounds.x, bounds.y, bounds.width, bounds.height);
    }
    //----------------------------------------------------------------------
    public static void setCurrentPiece(JavasawPiece newCurrentPiece)
    {
        currentPiece = newCurrentPiece;
    }
    //----------------------------------------------------------------------
    public void setPlaced( boolean placed ) { pieceIsPlaced = placed; }
    //----------------------------------------------------------------------
    public void setXYPos(int x,int y)
    {
        bounds.x=x;
        bounds.y=y;
    }
    public void setXYPosWithOffset(int x,int y)
    {
        bounds.x=x - mouseXOffset;
        bounds.y=y - mouseYOffset;
    }
    //----------------------------------------------------------------------
    public void paint(Graphics g,Applet applet)
    {
        paintIt(g,applet,bounds.x,bounds.y);
    }

    //----------------------------------------------------------------------
    public void paint(Graphics g,Applet applet,int xOffset, int yOffset)
    {
        paintIt(g,applet,bounds.x-xOffset,bounds.y-yOffset);
    }
    //----------------------------------------------------------------------
    public void paintIt(Graphics g,Applet applet,int xPos, int yPos)
    {
        if (isCurrentPiece())
        {
            g.setColor(Color.red);
            g.drawRect(xPos-1,yPos-1,bounds.width+1,bounds.height+1);
        }
        g.drawImage(pieceImage[sideShown],xPos,yPos,bounds.width,bounds.height,applet);
    }
    //----------------------------------------------------------------------
    public void setMouseOffset(int x,int y)
    {
        mouseXOffset = x - bounds.x;
        mouseYOffset = y - bounds.y;
        mouseDown = true;
    }
    //----------------------------------------------------------------------
    public Rectangle union(Rectangle other)
    {
        if (!isCurrentPiece())
            return bounds.union(other);

        Rectangle paintBounds = new Rectangle(bounds.x-1,bounds.y-1,bounds.width+2,bounds.height+2);
        return paintBounds.union(other);
    }

    public boolean rotate(Component component, int rotation)
    {
        // Get the image pixels into the buffer if not there already
        /*
        if (pieceInPixelBuffer != this)
        {
            pixelBuffer = new int[noOfSides][bounds.width * bounds.height];
            for (int i = 0; i < noOfSides; i++)
            {
                PixelGrabber pg = new PixelGrabber(pieceImage[i], 0, 0, bounds.width, bounds.height, pixelBuffer[i], 0, bounds.width);
                try
                {
                    pg.grabPixels();
                }
                catch (InterruptedException e)
                {
                    System.err.println("interrupted waiting for pixels!");
                    return false;
                }
            }
            pieceInPixelBuffer = this;
        }
        */

        int rotatedPixels[][] = new int[noOfSides][imageSize];
        int pos = 0;
        int newPos;
        int newX;
        int newY;
        int saveWidth;
        int startPos;
        int newWidth;
        int newHeight;

        // Apply the rotation
        for (int i = 0; i < noOfSides; i++)
        {
            pos = 0;
            switch (rotation)
            {
                // Only true when called from constructor
                case 0:
                    rotatedPixels = pixelBuffer;
                    break;

                case ROTATE_90:
                    startPos = (bounds.width - 1) * bounds.height;
                    for (int oldY=0; oldY < bounds.height; oldY++)
                    {
                        newPos = startPos + oldY;
                        for (int oldX=0; oldX < bounds.width; oldX++)
                        {
                            rotatedPixels[i][newPos] = pixelBuffer[i][pos];
                            newPos -= bounds.height;
                            pos++;
                        }
                    }
                    break;

                case ROTATE_180:
                    newPos = imageSize - 1;
                    for (pos=0; pos < imageSize; pos++)
                    {
                        rotatedPixels[i][newPos] = pixelBuffer[i][pos];
                        newPos--;
                    }
                    break;

                case ROTATE_270:
                    for (int oldY=0; oldY < bounds.height; oldY++)
                    {
                        newPos = bounds.height - oldY - 1;
                        for (int oldX=0; oldX < bounds.width; oldX++)
                        {
                            rotatedPixels[i][newPos] = pixelBuffer[i][pos];
                            newPos += bounds.height;
                            pos++;
                        }
                    }
                    break;

            }
        }

        // Recalc the new image sizes

        switch (rotation)
        {
            case ROTATE_90:
            case ROTATE_270:
                int centreX = bounds.x + (bounds.width/2);
                int centreY = bounds.y + (bounds.height/2);
                saveWidth = bounds.width;
                newWidth = bounds.height;
                newHeight = saveWidth;
                bounds.reshape(centreX - (newWidth/2),centreY - (newHeight/2), newWidth,newHeight);
                // Ensure mouse is inside the new bounding box, if not then move the box
                if (mouseDown)
                {
                    if (!bounds.inside(bounds.x + mouseXOffset,bounds.y + mouseYOffset))
                    {
                        bounds.reshape(bounds.x + mouseXOffset,bounds.y + mouseYOffset, newWidth,newHeight);
                        mouseXOffset=0;
                        mouseYOffset=0;
                    }
                }
                break;
        }

        orientation += rotation;
        if (orientation >= 360)
            orientation -= 360;
        else
            if (orientation < 0)
                orientation += 360;

        for (int i = 0; i < noOfSides; i++)
        {

            if (pieceImage[i] != null)
                pieceImage[i].flush();

            //pixelBuffer[i] = null;
            pieceImage[i] = component.createImage(new MemoryImageSource(bounds.width, bounds.height,
                rotatedPixels[i], 0, bounds.width));
            MediaTracker tracker = new MediaTracker(component);
            tracker.addImage(pieceImage[i],i);
            try
            {
                tracker.waitForID(i);
            }
            catch (InterruptedException e)
            {
                System.out.println("JavasawPiece.rotate excption=" + e);
                return false;
            }
            if (tracker.isErrorAny())
            {
                System.out.println("JavasawPiece.rotate mediatracker error");
                return false;
            }
        }
        // Save buffer in case this same piece is rotated again
        pixelBuffer = rotatedPixels;
        return true;
    }
}

//************************************************************************
//
// Class used as playing area where pieces and grid are displayed.

class JavasawPlayingArea extends Canvas implements ImageObserver
{
    static final int SIZES_UNIQUE = 0;
    static final int SIZES_RANDOM = 1;
    static final int SIZES_SAME = 2;

    private long          bestTime;
    private JavasawCell   cell[];              // Array of cells where pieces go
    private Rectangle     clippingRegion;      // Region to paint while dragging
    private JavasawDialog dialog;
    private Rectangle     dragArea;            // Area where pieces can be dragged
    private long          endTime;
    private int           imageHeight;
    private int           imageWidth;
    private int           gridX;
    private int           gridY;
    private boolean       imageReady = false;
    private boolean       initialising = true; // True while in initialise phase
    private Javasaw       javasawApplet;
    private boolean       jigsawIsComplete;
    private int           noOfSides;           // Number of 'Sides' to each piece
    private Graphics      offScreenGC;         // Graphics context for double bufferring
    private Image         offScreenImage;      // Image for double bufferring
    private JavasawPiece  piecesInZOrder[];    // Order in which pieces are drawn
    private boolean       paused =true;
    private JavasawPiece  pieceBeingDragged;   // Piece that mouse is dragging
    private JavasawPiece  pieceBeingPainted;   // Piece that paint() should repaint
    private int           pieceCount;
    private int           pieceSizeType;       // Unique, Random or Same
    private JavasawPiece  piece[];             // Array of pieces
    private int           piecesAcross;        // Number of pieces across puzzle
    private int           piecesDown;          // Number of pieces down puzzle
    private AudioClip     placedSound;         // Sound when piece placed
    private int           playAreaHeight;      // Height of entire page
    private int           playAreaWidth;       // Width of entire page
    private boolean       rotatable;
    private String        selectedImageName;
    private int           skillLevel;
    private long          startTime;           // Time in ms when user started to solve
    private boolean       timeGoingDown;
    private int           timeInterval;
    private long          top10Time;

    public JavasawPlayingArea(Javasaw javasawApplet)
    {
        this.javasawApplet = javasawApplet;
        clippingRegion = new Rectangle();
    }
    //----------------------------------------------------------------------
    // For rotatable pieces work out the maximum possible number of square pieces that
    // can be made while leaving the remaining pieces big enough to be useable
    public boolean calcSquarePieces(int puzzleWidth, int width[], int puzzleHeight, int height[])
    {
        int squareSize[]      = new int[pieceCount];
        int squareAcross[]    = new int[pieceCount];
        int squareDown[]      = new int[pieceCount];
        int otherAcrossSize[] = new int[pieceCount];
        int otherDownSize[]   = new int[pieceCount];
        boolean result = false;

        try
        {
            
            for (int ix = 0; ix < pieceCount; ix++)
            {
                squareSize[ix] = 0;
                squareAcross[ix] = 0;
                squareDown[ix] = 0;
                otherAcrossSize[ix] = 0;
                otherDownSize[ix] = 0;
            }

            int tryWidth = puzzleWidth / piecesAcross;
            int tryHeight = puzzleHeight / piecesDown;
            int tryMaxSize = 0;

            if (tryWidth < tryHeight)
                tryMaxSize = tryWidth;
            else
                tryMaxSize = tryHeight;

            int     xLeft;
            int     trySizeBy2;
            int     xOtherPieces;
            int     xOtherSize;
            int     yLeft;
            int     yOtherPieces;
            int     yOtherSize;
            int     sameSizeCount;

            for (int trySize = tryMaxSize; trySize > 30; trySize-- )
            {
                trySizeBy2 = trySize * 2;
                for (int sameSizedXPieces = piecesAcross; sameSizedXPieces > 1; sameSizedXPieces--)
                {
                    xLeft = puzzleWidth - (sameSizedXPieces * trySize);
                    if (xLeft < 0)
                        continue;

                    xOtherPieces = piecesAcross - sameSizedXPieces;
                    if (xOtherPieces == 0)
                    {
                        if (xLeft != 0)   // Maximum number of pieces must fit exactly
                            continue;
                        xOtherSize = 0;
                    }
                    else
                    {
                        xOtherSize = xLeft / xOtherPieces;
                        if (xOtherSize*2 < trySize || xOtherSize > trySizeBy2 )
                            continue;
                    }

                    for (int sameSizedYPieces = piecesDown; sameSizedYPieces > 1; sameSizedYPieces--)
                    {
                        yLeft = puzzleHeight - (sameSizedYPieces * trySize);
                        if (yLeft < 0)
                            continue;

                        yOtherPieces = piecesDown - sameSizedYPieces;
                        if (yOtherPieces == 0)
                        {
                            if (yLeft != 0)  // Maximum number of pieces must fit exactly
                                continue;
                            yOtherSize = 0;
                        }
                        else
                        {
                            yOtherSize = yLeft / yOtherPieces;
                            if (yOtherSize*2 < trySize || yOtherSize > trySizeBy2  )
                                continue;
                        }

                        sameSizeCount = (sameSizedXPieces*sameSizedYPieces)-1;

                        if (   (squareSize[sameSizeCount] == 0)
                            || (   (   xOtherSize >= otherAcrossSize[sameSizeCount]
                                    && yOtherSize >= otherDownSize[sameSizeCount]) ) ) 
                        {
                            squareSize[sameSizeCount] = trySize;
                            squareAcross[sameSizeCount] = sameSizedXPieces;
                            squareDown[sameSizeCount] = sameSizedYPieces;
                            otherAcrossSize[sameSizeCount] = xOtherSize;
                            otherDownSize[sameSizeCount] = yOtherSize;
                        }
                    }
                }
            }
  
            for (int ix = pieceCount-1; ix >=0; ix--)
            {
                if (squareSize[ix] != 0)
                {
                    // Calc the X dimension first

                    int remainder = puzzleWidth;
                    for (int across = 0; across < squareAcross[ix]; across++)
                    {
                        width[across] = squareSize[ix];
                        remainder -= squareSize[ix];
                    }

                    for (int across = squareAcross[ix]; across < piecesAcross -1 ; across++)
                    {
                        width[across] = otherAcrossSize[ix];
                        remainder -= otherAcrossSize[ix];
                    }

                    // Last one gets remainder as long as not all same size
                    if (squareAcross[ix] < piecesAcross)
                        width[piecesAcross -1] = remainder;

                    // Now do the Y dimension
                    remainder = puzzleHeight;
                    for (int down = 0; down < squareDown[ix]; down++)
                    {
                        height[down] = squareSize[ix];
                        remainder -= squareSize[ix];
                    }

                    for (int down = squareDown[ix]; down < piecesDown -1 ; down++)
                    {
                        height[down] = otherDownSize[ix];
                        remainder -= otherDownSize[ix];
                    }

                    // Last one gets remainder as long as not all same size
                    if (squareDown[ix] < piecesDown)
                        height[piecesDown -1] = remainder;

                    result = true;
                    break;
                }
            }
        }
        catch (Exception exception)
        {
            System.out.println("calcSquarePieces exception=" + exception);
        }

        return result;
    }
    //----------------------------------------------------------------------

    private void checkForCompleteness()
    {
        // See if the puzzle is finished
        if (!isPuzzleComplete() && JavasawCell.placedPieces == pieceCount)
            javasawApplet.showStatus("Some pieces are in the wrong place, show the wrong side or are rotated!");
    }
    //----------------------------------------------------------------------
    private void checkPieceCanDock(JavasawPiece pieceToCheck)
    {
        for (int n = 0; n < pieceCount ; n++)
        {
            if (cell[n].canPlacePiece(pieceToCheck))
            {
                pieceToCheck.setXYPos(cell[n].x,cell[n].y);
                if (!rotatable)
                    JavasawPiece.setCurrentPiece(null);

                // Put the docked piece at the bottom of the paint order
                moveToBackOfZOrder(pieceToCheck);

                // See if the puzzle is finished
                pieceBeingPainted = pieceToCheck;
                forcePaint();
                checkForCompleteness();
                if (placedSound != null)
                    placedSound.play();
                break;
            }
        }
    }

    //----------------------------------------------------------------------
    // Create an array for a dimension (width or height) which contains the
    // size of each piece in that dimension

    private int[] createADimension(int wholeDimension, int piecesInDimension,
        int pieceSizeType)
    {
        int         attempts = 0;
        int[]       dimensionSize = new int[piecesInDimension];
        int         remainder;
        boolean     sizingDone = false;
        float       stdSize = wholeDimension / piecesInDimension;
        float       maxDelta = stdSize / 4;
        int         pieceNumber;

        try {
            do
            {
                remainder = wholeDimension;
                for (pieceNumber = 0; pieceNumber < piecesInDimension - 1; pieceNumber++ )
                {
                    sizingDone = false;
                    do
                    {
                        attempts++;
                        if (attempts > 400)
                        {
                            javasawApplet.showStatus("Unable to size pieces");
                            System.out.println("Unable to size pieces");
                            System.exit(1);
                        }
                        if (pieceSizeType == SIZES_SAME)
                            dimensionSize[pieceNumber] = (int) stdSize;
                        else
                            dimensionSize[pieceNumber] = (int) (stdSize + (Math.random() * 2 * maxDelta)
                                - maxDelta);

                        sizingDone = true;
                        if (pieceSizeType == SIZES_UNIQUE)
                        {
                            // Check that this size hasn't already been used
                            for (int otherPieceNumber = 0; otherPieceNumber < pieceNumber; otherPieceNumber++)
                                if ( dimensionSize[pieceNumber] == dimensionSize[otherPieceNumber] )
                                {
                                    sizingDone =false;
                                    break;
                                }
                        }
                    }
                    while (!sizingDone);
                    remainder -= dimensionSize[pieceNumber];
                }

                sizingDone = true;
                // Final piece gets remainder
                dimensionSize[piecesInDimension -1] = remainder;
                if (pieceSizeType == SIZES_UNIQUE)
                    for (int otherPieceNumber = 0; otherPieceNumber < piecesInDimension - 1; otherPieceNumber++)
                        if ( remainder == dimensionSize[otherPieceNumber] )
                        {
                            sizingDone = false;
                            break;
                        }
            }
            while (!sizingDone);
        }
        catch (Exception ex)
        {
            System.out.println("createADimension Exception " + ex);
            ex.printStackTrace();
        }    

        return dimensionSize;

    }

    //----------------------------------------------------------------------
    public void dialogClosed()
    {
        forceFullPaint();
        if (jigsawIsComplete)
            javasawApplet.selectAnotherPuzzle();
        dialog = null;
    }

    //----------------------------------------------------------------------
    public void forceFullPaint()
    {
        pieceBeingPainted = null;
        forcePaint();
    }

    //----------------------------------------------------------------------
    // Force an immediate paint
    public void forcePaint()
    {
        Graphics graphics = getGraphics();
        paint(graphics);
        graphics.dispose(); // Don't ever forget this
    }

    //----------------------------------------------------------------------
    public void freeResources()
    {
        if (offScreenImage != null)
            offScreenImage.flush();
    }

    //----------------------------------------------------------------------
    // turn the elapsed time into string

    public String getElaspedAsString()
    {
        long elapsedTime;
        if (jigsawIsComplete)
            elapsedTime = (endTime - startTime) / 1000;
        else
            elapsedTime = (System.currentTimeMillis() - startTime) / 1000;

        long elapsedHours = elapsedTime / 3600;
        String timeString;

        if (elapsedHours > 0)
        {
            timeString = elapsedHours + ":";
            elapsedTime -= (elapsedHours * 3600);
        }
        else
            timeString = "";

        long elapsedMinutes = elapsedTime / 60;
        long elapsedSeconds = elapsedTime - (elapsedMinutes*60);

        if (elapsedMinutes > 9 || elapsedHours == 0)
            timeString += elapsedMinutes + ":";
        else
            timeString += "0" + elapsedMinutes + ":";

        if (elapsedSeconds > 9)
            timeString += elapsedSeconds;
        else
            timeString += "0" + elapsedSeconds;

        return timeString;
    }

    public int getImageHeight() { return imageHeight; }
    public int getImageWidth() { return imageWidth; }
    public int getGridX() { return gridX; }
    public int getGridY() { return gridY; }


    //----------------------------------------------------------------------
    // Standard AWT method : Calling prepareImage causes this method to be repeatedly called as the image is
    // transmitted. Only the main image is loaded via prepareImage. This is done so that
    // a % loaded message can be shown
    public boolean imageUpdate(Image theImage,
                               int infoflags,
                               int x,
                               int y,
                               int width,
                               int height)
    {
        if ( (infoflags & ImageObserver.ALLBITS) != 0)
            imageReady = true;

        return true;
    }

    //----------------------------------------------------------------------
    // One time initialisation code. Not done in constructor since this component's
    // size is unknown there.
    public void init()
    {
        initialising = true;
        playAreaHeight = size().height;
        playAreaWidth = size().width;
        dragArea = new Rectangle(10,10,playAreaWidth-20,playAreaHeight-20);
        offScreenImage = createImage(playAreaWidth, playAreaHeight);
        placedSound = javasawApplet.getAudioClip(javasawApplet.getDocumentBase(),"placed.au");
    }

    //----------------------------------------------------------------------
    public boolean isDialogActive()
    {
        if (dialog == null)
            return false;

        dialog.setFocus();
        return true;
    }
    //----------------------------------------------------------------------
    public boolean isPaused()  { return paused; }

    //----------------------------------------------------------------------
    // Check if puzzle is complete
    //
    private boolean isPuzzleComplete()
    {
        if (JavasawCell.correctlyPlacedPieces == pieceCount)
        {
            // Check that all pieces show the same side
            int sideNo = piece[0].getSideShown();
            boolean allDone = true;
            for (int pieceNo = 0; pieceNo < pieceCount ; pieceNo++)
                if (sideNo != piece[pieceNo].getSideShown() || piece[pieceNo].isRotated() )
                {
                    allDone = false;
                    break;
                }

            if (allDone)
            {
                showJigsawDone();
                return true;
            }
        }

        return false;
    }
    //----------------------------------------------------------------------
    // returns true if the key pressed has been handled

    public boolean keyPressed(Event event,int key)
    {
        int rotation = 0;
        if (!rotatable)
            return false;

        if (key == Event.LEFT || key == Event.UP || key == Event.PGUP || key == 9)
            rotation = JavasawPiece.ROTATE_90;
        else
            if (key == Event.RIGHT || key == Event.DOWN || key == Event.PGDN)
                rotation = JavasawPiece.ROTATE_270;
            else
                return false;

        JavasawPiece currentPiece = JavasawPiece.getCurrentPiece();
        if (currentPiece != null)
        {
            currentPiece.reshapeClippingRegion(clippingRegion);
            currentPiece.rotate(this,rotation);

            // If piece was placed then it can no longer be as it is 90 deg out of place now
            if (currentPiece.isPlaced())
            {
                for (int m = 0; m < pieceCount; m++)
                    if (cell[m].canRemovePiece(currentPiece))
                        break;
                moveToFrontOfZOrder(currentPiece);
            }
            else
                checkPieceCanDock(currentPiece);

            pieceBeingPainted = currentPiece;
            forcePaint();
        }
        return true;
    }

    //----------------------------------------------------------------------
    public Dimension minimumSize() { return preferredSize(); }
    //----------------------------------------------------------------------
    // Handle mouse down, see if this occurs over a piece and if it does
    // commence the dragging process
    //
    public boolean mouseDown(Event e, int x, int y)
    {
        JavasawPiece pieceToCheck;
        boolean done;

        // Ignore mouse down while still loading images or paused
        if (initialising || paused)
            return true;

        // Ignore mouse down if a dialog is showing
        if (dialog != null)
        {
            dialog.setFocus();
            return false;
        }

        // Find the piece at the top of the paint order that the mouse is over

        pieceBeingDragged = null;
        for (int n =pieceCount-1; n>=0 ; n--)  // Note reverse order
        {
            pieceToCheck = piecesInZOrder[n];
            if (pieceToCheck.inside(x,y))
            {
                JavasawPiece currentPiece = JavasawPiece.getCurrentPiece();
                if (currentPiece != null)
                {
                    JavasawPiece oldCurrent = currentPiece;
                    oldCurrent.reshapeClippingRegion(clippingRegion);
                    JavasawPiece.setCurrentPiece(null);
                    pieceBeingPainted = oldCurrent;
                    forcePaint();
                }
                if (rotatable)
                    javasawApplet.showStatus("Use up, down, left, right or tab to rotate this piece");
                else
                    if (noOfSides > 1)
                        javasawApplet.showStatus("Right click to show other side");
                    else
                        javasawApplet.showStatus("Right click to show other side");

                JavasawPiece.setCurrentPiece(pieceToCheck);
                if (e.modifiers == Event.META_MASK && noOfSides > 1)  // Rightclick flips sides
                {
                    JavasawPiece oldPainted = pieceBeingPainted;
                    pieceBeingPainted = pieceToCheck;
                    // Set initial clipping region to piece pos and size
                    pieceBeingPainted.reshapeClippingRegion(clippingRegion);
                    pieceBeingPainted.changeSideShown();
                    moveToFrontOfZOrder(pieceBeingPainted);

                    forcePaint();
                    pieceBeingPainted = oldPainted;
                    isPuzzleComplete();
                    break;
                }
                else
                {
                    // Found a piece, this is the one to be dragged
                    pieceBeingDragged = pieceToCheck;
                    pieceBeingPainted = pieceBeingDragged;
                    pieceBeingDragged.setMouseOffset(x,y);

                    // Set initial clipping region to piece pos and size
                    pieceBeingPainted.reshapeClippingRegion(clippingRegion);

                    // bring the selected piece to the top of the paint order
                    moveToFrontOfZOrder(pieceBeingDragged);
                    repaint();  // Show piece on top
                    break;
                }
            }
        }
        return true;
    }

    //----------------------------------------------------------------------
    // Handle mouse drag. If a piece is being dragged then move it
    //
    public boolean mouseDrag(Event e, int x, int y)
    {
        int oldX = 0;
        int oldY = 0;
        if (pieceBeingDragged != null && dialog == null)
        {
            // Check if dragging in valid area. Prevents pieces going off edge.
            if (dragArea.intersects(pieceBeingDragged.newPosition(x, y)))
            {
                if (pieceBeingDragged.isPlaced())
                {
                    oldX = pieceBeingDragged.getX();
                    oldY = pieceBeingDragged.getY();
                }

                pieceBeingPainted = pieceBeingDragged;
                pieceBeingDragged.setXYPosWithOffset(x,y);

                // Remove the piece from the cell if it is placed
                if (       pieceBeingDragged.isPlaced()
                        && (pieceBeingDragged.getX() != oldX || pieceBeingDragged.getY() != oldY))
                    for (int m = 0; m < pieceCount; m++)
                        if (cell[m].canRemovePiece(pieceBeingDragged))
                            break;

                repaint();
            }
        }
        return true;
    }
    //----------------------------------------------------------------------
    // Handle mouse up. If a piece is being dragged then see if it can 'dock'
    // into a cell in the grid

    public boolean mouseUp(Event e, int x, int y)
    {
        if (pieceBeingDragged != null && dialog == null && e.modifiers != Event.META_MASK)
        {
            // Check the time to ensure that the user doesn't cheat

            if (javasawApplet.timeLastSecond > System.currentTimeMillis())
            {
                showCaughtDialog();
                return false;
            }

            // 'Dock' the piece into a cell if the piece is close enough to
            // the edge of the cell and of the same size

            checkPieceCanDock(pieceBeingDragged);
            pieceBeingDragged.clearMouseOffset();
            pieceBeingDragged = null;
            pieceBeingPainted = null;
        }

        return true;
    }

    //----------------------------------------------------------------------
    // Move a piece to the back of the paint Z order
    //
    private void moveToBackOfZOrder(JavasawPiece pieceToMove)
    {
        boolean startMoving;

        startMoving = false;
        for (int m = pieceCount-1; m >=0 ; m--)
        {
            if (startMoving)
                piecesInZOrder[m+1] = piecesInZOrder[m];
            else
                if (piecesInZOrder[m] == pieceToMove)
                    startMoving = true;
        }
        piecesInZOrder[0] = pieceToMove;
    }

    //----------------------------------------------------------------------
    // Move a piece to the front of the paint Z order
    //
    private void moveToFrontOfZOrder(JavasawPiece pieceToMove)
    {
        boolean startMoving;

        startMoving = false;
        for (int m = 0; m <= pieceCount-1 ; m++)
        {
            if (startMoving)
                piecesInZOrder[m-1] = piecesInZOrder[m];
            else
                if (piecesInZOrder[m] == pieceToMove)
                    startMoving = true;
        }
        piecesInZOrder[pieceCount-1] = pieceToMove;
    }

    //----------------------------------------------------------------------
    // Handle painting of Jigsaw. Two types of double buffering are used,
    // when a piece is being dragged only the redraw region around the piece is
    // built in the buffer, otherwise the entire screen is built
    public void paint(Graphics g)
    {
        // Blank playing area if jigsaw is paused

        if (paused)
        {
            g.setColor(Color.lightGray);
            g.fillRect(0, 0, playAreaWidth, playAreaHeight);
            return;
        }

        // Ignore any paints the system sends before the off-screen graphics
        // context is created

        if (offScreenImage == null)
            return;

        offScreenGC = offScreenImage.getGraphics();
        if (offScreenGC == null)
            return;

        // The pieceBeingPainted variable is non-null whenever the user is
        // dragging a piece. If it null then the entire screen is redrawn otherwise only
        // the clipping area is redrawn (using partial double buffering)

        if (pieceBeingPainted == null)
        {
            // Draw the entire screen
            //
            offScreenGC.setColor(Color.lightGray);
            offScreenGC.fillRect(0, 0, playAreaWidth, playAreaHeight);
            offScreenGC.setColor(Color.black);

            // Draw the grid
            for (int n = 0; n < pieceCount; n++)
                cell[n].paint(offScreenGC);

            // Draw pieces in the correct paint order.
            for (int n = 0; n < pieceCount; n++)
                piecesInZOrder[n].paint(offScreenGC,javasawApplet);

            g.drawImage(offScreenImage, 0, 0, this);
        }
        else
        {
            // Draw a clipping area only. This area is defined by position of dragged piece
            // last time it was drawn expanded in any direction to the new piece position

            clippingRegion = pieceBeingPainted.union(clippingRegion);

            // Draw into the upper left corner of the off-screen graphics context
            offScreenGC.setColor(Color.lightGray);
            offScreenGC.fillRect(0, 0, clippingRegion.width, clippingRegion.height);
            offScreenGC.setColor(Color.black);

            // Draw the grid cells
            for (int n = 0; n < pieceCount; n++)
                cell[n].paint(offScreenGC,clippingRegion.x,clippingRegion.y);

            // Draw pieces in the correct paint order but only paint pieces
            // that fall in the drawing area. Note when a piece is being
            // dragged it has already been placed at the top of the paint order.

            for (int n = 0; n < pieceCount; n++)
                if (piecesInZOrder[n].intersects(clippingRegion))
                    piecesInZOrder[n].paint(offScreenGC,javasawApplet,clippingRegion.x,clippingRegion.y);

            // This is where the speed advantage takes place, its faster to
            // do this than repaint the entire screen
            g.clipRect(clippingRegion.x, clippingRegion.y, clippingRegion.width, clippingRegion.height); // clip to draw area
            g.drawImage(offScreenImage, clippingRegion.x, clippingRegion.y, this);

            // Reset next clipping region to dragged piece area
            pieceBeingPainted.reshapeClippingRegion(clippingRegion);
        }
        offScreenGC.dispose();
    }

    //-------------------------------------------------------
    // Must do this otherwise canvas never displays. Thanks to 'Graphic Java'
    public Dimension preferredSize()
    {
        return new Dimension(1,1);
    }
    //----------------------------------------------------------------------
    // Create Javasaw pieces and grid
    //
    public boolean setupJigsaw(Image entireImage[],
        int piecesAcross, int piecesDown, int pieceSizeType, int noOfSides,
        boolean rotatable, String selectedImageName, int skillLevel,
        long top10Time, long bestTime,
        int pixels[], int revPixels[] )
    {
        int        height[];
        int        pieceX = 0;  // The X position within the image where a piece is cut
        int        pieceY = 0;  // The Y position within the image where a piece is cut
        int        pieceImageHeight =0;
        int        pieceImageWidth = 0;
        //Image      pieceImage[];
        int        pieceNumber;
        int        width[];
        int        cellX;  // The X position in the playing
        int        cellY;

        javasawApplet.showStatus("Creating pieces");
        initialising = true;
        jigsawIsComplete = false;
        dialog = null;
        JavasawCell.correctlyPlacedPieces = 0;
        JavasawCell.placedPieces = 0;
        this.piecesAcross  = piecesAcross;
        this.piecesDown    = piecesDown;
        this.pieceSizeType = pieceSizeType;
        this.noOfSides = noOfSides;
        this.rotatable = rotatable;
        this.selectedImageName = selectedImageName;
        this.skillLevel = skillLevel;
        this.top10Time = top10Time;
        this.bestTime = bestTime;

        pieceBeingDragged = null;
        pieceBeingPainted = null;
        pieceCount = piecesAcross * piecesDown;
        piecesInZOrder = new JavasawPiece[pieceCount];
        piece = new JavasawPiece[pieceCount];
        cell = new JavasawCell[pieceCount];

        imageHeight = entireImage[0].getHeight(this);
        imageWidth = entireImage[0].getWidth(this);
        if (imageHeight > playAreaHeight || imageWidth > playAreaWidth)
        {
            javasawApplet.showStatus("Image too big for page!");
            return false;
        }

        // Position grid in centre of play area
        gridX = (playAreaWidth - imageWidth) / 2;
        gridY = (playAreaHeight - imageHeight) / 2;

        if (rotatable)
        {
            width = new int[piecesAcross];
            height = new int[piecesDown];
            if (!calcSquarePieces(imageWidth,width,imageHeight,height))
            {
                width = createADimension(imageWidth, piecesAcross, pieceSizeType);
                height = createADimension(imageHeight, piecesDown, pieceSizeType);
            }
        }
        else
        {
            width = createADimension(imageWidth, piecesAcross, pieceSizeType);
            height = createADimension(imageHeight, piecesDown, pieceSizeType);
        }

        //pieceImage = new Image[noOfSides];

        // Randomly position the jigsaw pieces & create their images

        pieceNumber = 0;
        cellX = gridX;

        pieceX = 0;

        int eitherPixels[] = null;
        for (int across = 0; across < piecesAcross; across++  )
        {
            pieceY = 0;
            cellY = gridY;
            pieceImageWidth = width[across];

            for (int down = 0; down < piecesDown; down++ )
            {
                javasawApplet.showStatus("Cutting out piece " + (pieceNumber + 1));
                int allSidesPixels[][] = new int[noOfSides][];
                int rotation = 0;
                JavasawPiece onePiece = null;
                if (rotatable)
                    rotation = ((int) (Math.random() * 3.999)) * 90;

                eitherPixels = pixels;
                pieceImageHeight = height[down];
                int oneSidesPixels[] = null;
                int srcPixelPos = 0;
                int destPixelPos = 0;
                int startPos = 0;
                for (int sideNo = 0; sideNo < noOfSides; sideNo++)
                {
                    oneSidesPixels = new int[ pieceImageWidth * pieceImageHeight ];
                    destPixelPos = 0;
                    startPos = pieceX + (pieceY * imageWidth);
                    for (int py = pieceY; py < pieceY + pieceImageHeight; py++)
                    {
                        srcPixelPos = startPos;
                        for (int px = pieceX; px < pieceX + pieceImageWidth; px++)
                        {
                            oneSidesPixels[destPixelPos++] = eitherPixels[srcPixelPos++];
                        }
                        startPos += imageWidth;
                    }
                    allSidesPixels[sideNo] = oneSidesPixels;
                    eitherPixels = revPixels;
                }
                try
                {
                    onePiece = new JavasawPiece(allSidesPixels,
                        (int)(Math.random() * (playAreaWidth - width[across])),
                        (int)(Math.random() * (playAreaHeight - height[down])),
                        pieceImageWidth,pieceImageHeight,
                        (int)(Math.random() * noOfSides),
                        this,
                        rotation);
                }
                catch (Exception ex)
                {
                    System.out.println("new JavasawPiece failed with " + ex);
                    ex.printStackTrace();
                    javasawApplet.showStatus("Unable to create all pieces");
                    return false;
                }

                piece[pieceNumber] = onePiece;
                piecesInZOrder[pieceNumber] = onePiece;

                // Create a cell in the grid
                cell[pieceNumber]= new JavasawCell(onePiece,cellX,cellY,pieceImageWidth,
                    pieceImageHeight,across,down);
                cellY += pieceImageHeight;
                pieceY += pieceImageHeight;

                // Must be last in loop, increment counter
                pieceNumber++;
            }
            cellX += pieceImageWidth;
            pieceX += pieceImageWidth;
        }

        javasawApplet.showStatus("");
        startTime = System.currentTimeMillis();
        initialising = false;
        paused = false;
        return true;
    }
    //----------------------------------------------------------------------
    public void showCaughtDialog()
    {
        jigsawIsComplete = true;
        dialog = new JavasawDialog(this,"The time in your computer has gone backwards. You aren't trying to cheat are you?",
                    "Caught you!",null,null);
    }
    //----------------------------------------------------------------------
    // Show the done dialog
    private void showJigsawDone()
    {
        String message;
        jigsawIsComplete = true;
        endTime = System.currentTimeMillis();
        String elapsedAsString = getElaspedAsString();

        message = "You finished the puzzle in " + elapsedAsString;
        long seconds = (endTime - startTime) / 1000;
        String eMailTo = null;

        if (javasawApplet.getParameter("EMail") == null)
            message += ".";
        else
            if (bestTime == 0 || seconds <= bestTime)
            {
                message += " which is the best time for this skill level!";
                eMailTo = javasawApplet.getParameter("EMail");
            }
            else
                if (top10Time == 0 || seconds <= top10Time)
                {
                    message += " which is a 'top 10' time for this skill level!";
                    eMailTo = javasawApplet.getParameter("EMail");
                }
                else
                    message += " which doesn't get into the 'top 10'. Try again!";

        String completionDetails = null;
        if (eMailTo != null)
            completionDetails =
                  "Your name :                 "
                + "\nImage  : " + selectedImageName
                + "\nLevel  : " + skillLevel
                + "\nTime   : " + elapsedAsString
                + "\nMagic# : " + encode(skillLevel + "" + elapsedAsString);

        dialog = new JavasawDialog(this,message,"Congratulations!",eMailTo,completionDetails);
    }

    //----------------------------------------------------------------------
    // Toggle pause on and off. The elapsed time is adjusted to take account of the pause
    public void togglePaused()
    {
        if (paused)
            startTime += System.currentTimeMillis();
        else
            startTime -= System.currentTimeMillis();

        paused = ! paused;
        forceFullPaint();   // Redraw all pieces or blank playing area as appropriate
    }

    //----------------------------------------------------------------------
    // Standard AWT method - over-ride to prevent flicker
    public void update(Graphics g)
    {
        paint(g);
    }

    //----------------------------------------------------------------------
    // Encode a string
    public String encode(String clearText)
    {
        //unpublished encoding method for magic number used to verify time is correct
    }
}