// The canvas supporting the drawing window.
// (c) 1997 duane a. bailey

package element;

import java.awt.*;
import java.awt.event.*;

/**
 * The private extension to the Canvas class that is used to support
 * the DrawingWindow.
 *
 * @author duane a. bailey
 */
class DrawingCanvas extends Canvas
implements MouseListener, MouseMotionListener, KeyListener,
WindowListener
{
    /**
     * The last place that the mouse was seen.  Since the mouse movement
     * events are tracked, this is a good estimate of the current mouse 
     * location.
     */
    Pt mouse;		// where the mouse was last seen
    /**
     * Whether or not the mouse was last seen in pressed position.
     * for multibutton mouse, this is the leftmost mouse button.
     */
    boolean mousePressed;	// whether the mouse is currently pressed
    int buttonMask;		// event modifiers for button presses
    /**
     * A list of off-screen images that are used for backing store.
     * One is used to manage the current screen, another is used to 
     * keep track of current updates.  They switch roles only when the
     * secondary image is in an acceptable state.  The user can control
     * this process with hold and release calls.
     */
    Image osi[];		// off screen image
    /**
     * The offscreen pixmaps that are targets for graphics operations.
     * They are derived from the images, above.
     */
    Graphics osg[];		// off screen pixmap
    /**
     * The index of the current offscreen bitmap.  osi[cbi] is stable
     * and represents the current contents of the screen.  It is never
     * written to directly.
     */
    int cbi;			// which image is being used for double buffer
    /**
     * The number of outstanding hold() calls performed by the user.
     * When this count reaches zero, the offscreen bitmaps are allowed
     * to swap.
     */
    int holdDepth;		// the level of hold-release calls
    /**
     * Current estimates of the width and height of the bitmaps.
     * May be poor estimates if the container is smaller.
     */
    int width, height;
    /**
     * This barrier keeps track of whether or not a DrawingWindow has
     * been constructed.  Since many resources are allocated only when
     * an initial drawing window has made contact with the current graphics
     * device, we must be careful not to attempt font allocation before
     * the first window is constructed.  This code should be replaced
     * with a default allocation that is performed at startup.
     */
    Barrier started;		// signals startup
    /**
     * This barrier is freed when the mouse gets pressed next.  It is 
     * used to asynchronously communicate between the user's thread and the
     * Canvas' event monitor thread.
     */
    Barrier mousePressLock;
    /**
     * This barrier is freed when the mouse gets released next.  It is 
     * used to asynchronously communicate between the user's thread and the
     * Canvas' event monitor thread.
     */
    Barrier mouseReleaseLock;
    /**
     * This barrier is freed when the next key is pressed.  Allows for
     * asynchronous communication with the user process on a key event.
     */
    Barrier keyLock;
    /**
     * This character is the key that was pressed when the key event 
     * occured.
     */
    char keyChar;

    /**
     * Construct a 200x200 canvas.
     */
    public DrawingCanvas()
    // post: construct a 200x200 canvas
    {
	this(200,200);
    }

    /**
     * Construct a drawing canvas with desired dimensions.
     * @param width the desired width of the canvas
     * @param height the desired height of the canvas
     */
    public DrawingCanvas(int width, int height)
    // pre: 0 <= width, height
    // post: construct a canvas with the desired width and height
    {
	buttonMask = InputEvent.BUTTON1_MASK|
	    InputEvent.BUTTON2_MASK|InputEvent.BUTTON3_MASK;
	setBackground(Color.white);
	setForeground(Color.black);
	setSize(width,height);
	this.width = width;
	this.height = height;

	// create the off-screen pixmap associated with window
	osi = null;
	osg = null;
	cbi = 0;
	holdDepth = 0;

	// listen to basic events
	addMouseListener(this);
	addMouseMotionListener(this);
	addKeyListener(this);

	// allocate locations
	mouse = new Pt();
	started = new Barrier();
	mousePressLock = new Barrier();
	mouseReleaseLock = new Barrier();
	keyLock = new Barrier();
    }

    /**
     * Fetch the current writeable offscreen Graphics.
     * 
     * @return the current writeable offscreen Graphics.
     */
    public Graphics getOffscreenGraphics()
    // post: returns the offscreen graphics associated with
    //       the canvas
    {
	if (osi == null)
	{
	    started.P(); // wait for offscreen graphics to be created
	}
	return osg[0];
    }

    /**
     * Fetch the current writeable offscreen image.  This
     * generated the offscreen Graphics.
     */
    public Image getImage(java.awt.image.ImageProducer ip)
    {
	if (osi == null)
	{
	    started.P(); // wait for offscreen graphics to be created
	}
	return createImage(ip);
    }

    /**
     * Hold the double buffering in its current state.  This allows
     * updating of the offscreen map is a timely and consistent manner.
     * Holds may be nested.
     * @see #release
     */
    public void hold()
    // post: hold the screen updating process
    {
	if (cbi != 1)
	{
	    cbi = 1;
	    osg[cbi].drawImage(osi[0], 0, 0, null);
	}
	holdDepth++;
    }

    /**
     * Release one hold on the double buffering.  When all holds are 
     * released, the screen may be updated from a new secondary buffer.
     * @see #hold
     */
    public void release()
    // pre: a hold has been placed on the screen updating process
    // post: a hold is released; screen will be updated
    {
	holdDepth--;
	if (holdDepth <= 0)
	{
	    cbi = 0;
	    repaint();
	}
    }	

    /**
     * Gets the current estimate of the mouse position.
     * @return the current estimated mouse position
     */
    public Pt getMouse()
    // post: the current mouse position
    {
	return new Pt(mouse);
    }

    private void updateMouse(MouseEvent e)
    // pre: e is a MouseEvent, but not MouseReleased
    // post: mouse coordinates and press state is handled.
    {
	boolean mouseWasPressed = mousePressed;
	mousePressed = 0 != (e.getModifiers()&buttonMask);
	if (mouseWasPressed != mousePressed)
	{
	    // System.out.println("Mouse state changed with event "+e);
	    if (mousePressed) mousePressLock.V(); // notify anyone holding on the event.
	    else mouseReleaseLock.V(); // notify anyone holding on the event.
	} else {
	    //System.out.println("Mouse state DIDN'T changed with event "+e);
	}
    }

    private void updateMouseP(MouseEvent e)
    // pre: e is a MousePress event
    // post: mouse coordinates and press state is handled.
    {
	boolean mouseWasPressed = mousePressed;
	mousePressed = true;
	if (mouseWasPressed != mousePressed)
	{
	    mousePressLock.V(); // notify anyone holding on the event.
	}
    }

    private void updateMouseR(MouseEvent e)
    // pre: e is a MouseReleased or MouseClicked event
    // post: mouse coordinates and press state is handled.
    {
	boolean mouseWasPressed = mousePressed;
	mousePressed = false;
	if (mouseWasPressed != mousePressed)
	{
	    // System.out.println("Mouse state changed with event "+e);
	    mouseReleaseLock.V(); // notify anyone holding on the event.
	}
    }


    /**
     * Returns true iff the mouse is currently pressed.
     * On multibutton mice, the leftmost button is the one queried.
     * @return true iff the mouse is currently pressed
     */
    public boolean mousePressed()
    // post: returns true iff the mouse is currently pressed
    {
	return mousePressed;
    }

    /**
     * The event handler for a mouse pressed event.
     * @param e the mousePressed event
     */
    public void mousePressed(MouseEvent e)
    // pre: the mouse was pressed
    // post: the event is recorded
    {
	mouse.moveTo(e.getX(),e.getY());
	updateMouseP(e);
    }

    /**
     * The mouseReleased event handler.
     * @param e the mouseReleased event handler
     */
    public void mouseReleased(MouseEvent e)
    // pre: the mouseReleased event handler
    // post: the event is recorded
    {
	mouse.moveTo(e.getX(),e.getY());
	updateMouseR(e);
    }

    /**
     * The mouse entered window event handler
     * @param e the current mouse position is updated.
     */
    public void mouseEntered(MouseEvent e)
	// pre: the mouse has just entered the window
	// post: the mouse position is updated
	{
	    mouse.moveTo(e.getX(),e.getY());
	}

    /**
     * The mouse left window event handler
     * @param e the current mouse position is updated
     */
    public void mouseExited(MouseEvent e)
	// pre: the mouse just left the window
	// post: the mouse position is updated
	{
	    mouse.moveTo(e.getX(),e.getY());
	}

    /**
     * The mouse "clicked" event handler
     * @param e the mouseClicked event
     */
    public void mouseClicked(MouseEvent e)
	// pre: the mouse was just clicked
	// post: the mouse position and state is updated
	{
	    mouse.moveTo(e.getX(),e.getY());
	    updateMouseR(e);
	}

    /**
     * The mouse movement event handler
     * @param e the mouseDragged event
     */
    public void mouseDragged(MouseEvent e)
	// pre: the mouse has moved
	// post: the mouse position and state is updated in the window
	{
	    mouse.moveTo(e.getX(),e.getY());
	    updateMouseP(e);
	}

    /**
     * The mouse movement event handler
     * @param e the mouseMoved event
     */
    public void mouseMoved(MouseEvent e)
    // pre: the mouse has moved
    // post: the mouse position and state is updated in the window
	{
	    mouse.moveTo(e.getX(),e.getY());
	    updateMouseR(e);
	}

    /**
     * Wait until the mouse is pressed.
     * @return the position of the mouse press
     */
    public Pt awaitMousePress()
    // post: returns the point of next mouse press; blocks until then
	{
	    if (!mousePressed) mousePressLock.P();
	    return getMouse();
	}

    /**
     * Wait until the mouse is released
     * @return the position of the mouse release
     */
    public Pt awaitMouseRelease()
    // post: returns the point of the next mouse release; blocks until then
	{
	    if (mousePressed) mouseReleaseLock.P();
	    return getMouse();
	}

    /**
     * Wait until the key is pressed
     * @return returns the key pressed
     */
    public char awaitKey()
    // post: returns the next key pressed
	{
	    keyLock.P();
	    return keyChar;
	}

    /**
     * Key type event handler (currently not used)
     * @param evt the keyTyped event
     */
    public void keyTyped(java.awt.event.KeyEvent evt)
	{
	    return;
	}

    /**
     * The key pressed event handler
     * @param evt the keyPressed event
     */
    public void keyPressed(java.awt.event.KeyEvent evt)
    // pre: the keyPressed event has taken place
    // post: the key is recorded and returned to awaiting processes
	{
	    keyChar = evt.getKeyChar();
	    if (keyChar >= 0 && keyChar <= '~') {
		keyLock.V();
	    }
	    return;
	}

    /**
     * The key released event handler (currently not used)
     * @param evt the key released event
     */
    public void keyReleased(java.awt.event.KeyEvent evt)
	{
	    return;
	}

    /**
     * Update a portion of the canvas.
     * 
     * @param g the Graphics associated with the canvas to be updated
     */
    public void update(Graphics g)
	// post: updates a portion of the canvas from osi
	{
	    paint(g);
	}

    /**
     * Selective repaint of an area of the canvas.
     * (Selective portion of code must yet be implemented.)
     * Only active when there are no outstanding holds.
     * Repaints a selected are of the screen
     * @param left the left side of repaint area
     * @param top the top side of the repaint area
     * @param width the width of the repaint area
     * @param height the height of the repaint area
     */
    public void repaint(int left, int top, int width, int height)
	{
	    if (cbi == 1) return;
	    repaint();
	}

    /**
     * Update the canvas from offscreen bitmap.
     * 
     * @param g the Graphics to be updated
     */
    public void paint(Graphics g)
	// post: updates all of canvas from osg
	{
	    if (osi == null) // first call; allocate bitmaps/images
	    {
		osi = new Image[2];
		osi[0] = createImage(width,height);
		osi[1] = createImage(width,height);

		osg = new Graphics[2];
		osg[0] = osi[0].getGraphics();
		osg[1] = osi[1].getGraphics();
		cbi = 0;		// current buffer index
		started.V();
	    }
	    g.drawImage(osi[cbi], 0, 0, null);
	}

    // Window listening methods:
    public void windowOpened(WindowEvent e)
    {
	System.out.println("windowOpened");
    }
    public void windowClosing(WindowEvent e)
    {
	System.out.println("windowClosing");
    }
    public void windowClosed(WindowEvent e)
    {
	System.out.println("windowClosed");
    }
    public void windowIconified(WindowEvent e)
    {
	System.out.println("windowIconified");
    }
    public void windowDeiconified(WindowEvent e)
    {
	System.out.println("windowDeiconified");
    }
    public void windowActivated(WindowEvent e)
    {
	System.out.println("windowActivated");
    }
    public void windowDeactivated(WindowEvent e)
    {
	System.out.println("windowDeactivated");
    }
}


