/************************************************}
{                                                }
{   JDK 1.1, 1.2, 1.3, 1.4                       }
{   Extensions graphiques compatibles QuickDraw  }
{   P.Chassignet, Ecole Polytechnique, 1998-2004 }
{                                                }
{   dernie`re mise a` jour:  22/3/04             }
{                                                }
{************************************************/

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

/**
 * A <code>DrawingStuff</code> object implements an off-screen image for 
 * backing store and provides automatic repaint for a 
 * <code>DrawingApplet</code> panel.
 * The drawings are to be done on the off-screen image, using the graphic 
 * context that is obtained by a call to the <code>getGraphics()</code> 
 * method.
 * New drawings have to be explicitely exposed by invoking the 
 * <code>doExpose</code> method.
 * When the <code>DrawingApplet</code> panel needs to be repaint, its 
 * <code>paint</code> method calls the <code>DrawingStuff</code>'s one.
 * <p>
 * The <code>DrawingStuff</code> also implements listeners for handling 
 * keyboard and mouse events that concern the <code>DrawingApplet</code>.
 *
 * @version  22 mar 2004
 * @author   Philippe Chassignet, Ecole Polytechnique
 * @see      MacLib
 * @see      DrawingFrame
 * @see      DrawingApplet
 */
public final class DrawingStuff 
  implements MouseListener, MouseMotionListener, KeyListener {

  /**
   * Used to specify no modifier in the button state.
   *
   * @see      #getButtonState()
   */
  public static final int NONE = 0;

  /**
   * Used to encode the shift modifier in the button state.
   *
   * @see      #getButtonState()
   */
  public static final int SHIFT = Event.SHIFT_MASK;

  /**
   * Used to encode the control modifier in the button state.
   *
   * @see      #getButtonState()
   */
  public static final int CONTROL = Event.CTRL_MASK;

  /**
   * Used to encode the meta modifier in the button state.
   *
   * @see      #getButtonState()
   */
  public static final int META = Event.META_MASK;

  /**
   * The default final width for an off-screen image.
   *
   * @see     #DrawingStuff
   */
  public static final int DEFAULT_SCREEN_WIDTH  = 576;

  /**
   * The default final height for an off-screen image.
   *
   * @see     #DrawingStuff
   */
  public static final int DEFAULT_SCREEN_HEIGHT = 720;

  private static int screenW = DEFAULT_SCREEN_WIDTH;
  private static int screenH = DEFAULT_SCREEN_HEIGHT;

  private Panel visibleDrawing;
  private Image pixmap;
  private java.awt.Point mouseLocation;
  private Object mouseState;
  private Object charLock;
  private boolean buttonDown;
  private int buttonState;
  private char lastChar;

  private String verbose;  // for debug purpose, determines trace_if_verbose

  private void init_stuff() {
    mouseLocation = new java.awt.Point();
    mouseState = new Object();
    charLock = new Object();
    buttonDown = false;
    buttonState = NONE;
    lastChar = 0;
    trace_if_verbose("MacLib messages enabled");
    trace_if_verbose("MacLib version 1.1.3");
  }

  /**
   * Constructs and initializes a <code>DrawingStuff</code> attached to the 
   * specified <code>DrawingApplet</code>.
   *
   * @param       panel   the <code>DrawingApplet</code>
   * @param       width   the final width of the off-screen image
   * @param       height  the final height of the off-screen image
   * @param       debug   a string indicating the debug level
   */
  public DrawingStuff(DrawingApplet panel, int width, int height, String debug) {
    verbose = debug;
    // verbose = "ON";
    visibleDrawing = panel;
    screenW = width;
    screenH = height;
    pixmap = visibleDrawing.createImage(screenW, screenH);
    init_stuff();
  }

  /**
   * Disposes of this <code>DrawingStuff</code> and releases its resources.
   * This method is called by the <code>dispose()</code> method for 
   * <code>DrawingApplet</code>.
   */
  public synchronized void dispose() {
    charLock = null;
    mouseState = null;
    mouseLocation = null;
    if ( pixmap != null ) {
      pixmap.flush();
      pixmap = null;
    }
  }

  /**
   * Called by the garbage collector when there are no more reference to 
   * this <code>DrawingStuff</code>.
   */
  public synchronized void finalize() {
    if ( pixmap != null )
      dispose();
    // else dispose() is assumed to have been already explicitely called 
  }

  /**
   * Returns a graphic context for drawing onto the off-screen image 
   * that is associated to this <code>DrawingStuff</code>.
   * This context should be explicitely disposed when finished.
   *
   * @return      the <code>Graphics</code> object.
   */
  public Graphics getGraphics() {
    return pixmap.getGraphics();
  }

  /**
   * Paints the specified rectangular area onto the visible panel with the 
   * content of the associated off-screen image. Coordinates are relative 
   * to both the off-screen image and the drawing panel.
   *
   * @param       left    the <em>left</em> coordinate of the filled area.
   * @param       top     the <em>top</em> coordinate of the filled area.
   * @param       right   the <em>right</em> coordinate of the filled area.
   * @param       bottom  the <em>bottom</em> coordinate of the filled area.
   */
  public void doExpose(int left, int top, int right, int bottom) {
    if ( left < 0 )  left = 0;
    if ( top < 0 )   top = 0;
    if ( right > screenW )   right = screenW;
    if ( bottom > screenH )  bottom = screenH;
    visibleDrawing.repaint(left, top, right-left, bottom-top);
  }

  /**
   * Invoked by the <code>DrawingApplet.paint(Graphics)</code> method when 
   * an exposed area needs to be repaint.
   * It results in copying the content of the associated off-screen image 
   * onto the panel. 
   */
  public void paint(Graphics g) {
    if ( pixmap != null )
      g.drawImage(pixmap, 0, 0, null);
  }

  /**
   * Clears the associated off-screen image and updates the visible panel.
   */
  private void clear() {
    Graphics g = pixmap.getGraphics();
    g.setPaintMode();
    g.setColor(Color.white);
    g.fillRect(0, 0, screenW, screenH); // clearRect doesn't seem to work
    g.dispose();
    visibleDrawing.repaint();
  }

  /**
   * Resets the drawing and event context of this <code>DrawingStuff</code>.
   */
  public synchronized void reset() {
    lastChar = 0;
    clear();
  }

// MouseListener

  private void getMouseLocation(MouseEvent e) {
    mouseLocation.x = e.getX();
    mouseLocation.y = e.getY();
  }

  /**
   * Invoked by the event handler when the mouse has been clicked 
   * on the drawing panel.
   */
  public void mouseClicked(MouseEvent e) {
  }

  /**
   * Invoked by the event handler when a mouse button has been pressed 
   * on the drawing panel.
   */
  public void mousePressed(MouseEvent e) {
    buttonDown = true;
    synchronized(mouseLocation) {
      getMouseLocation(e);
    }
    synchronized(mouseState) {
      buttonState = NONE;
      if ( e.isShiftDown() )
        buttonState |= SHIFT;
      if ( e.isControlDown() )
        buttonState |= CONTROL;
      if ( e.isAltDown() )
        buttonState |= META;
      mouseState.notify();
    }
  }

  /**
   * Invoked by the event handler when a mouse button has been released 
   * on the drawing panel.
   */
  public void mouseReleased(MouseEvent e) {
    buttonDown = false;
    synchronized(mouseLocation) {
      getMouseLocation(e);
      mouseLocation.notify();
    }
    synchronized(mouseState) {
      mouseState.notify();
    }
  }

  /**
   * Invoked by the event handler when the mouse enters the drawing panel.
   */
  public void mouseEntered(MouseEvent e) {
    visibleDrawing.requestFocus();
    synchronized(mouseLocation) {
      getMouseLocation(e);
    }
  }

  /**
   * Invoked by the event handler when the mouse exits the drawing panel.
   */
  public void mouseExited(MouseEvent e) {
  }

// MouseMotionListener

  /**
   * Invoked by the event handler when a mouse button is pressed  
   * on the drawing panel and then dragged.
   * Mouse drag events will continue to be delivered until the mouse button 
   * is released (regardless of whether the mouse position is within the 
   * bounds of the drawing panel).
   */
  public void mouseDragged(MouseEvent e) {
    synchronized(mouseLocation) {
      getMouseLocation(e);
      mouseLocation.notify();
    }
  }

  /**
   * Invoked by the event handler when the mouse has been moved 
   * on the drawing panel (with no button down).
   */
  public void mouseMoved(MouseEvent e) {
    synchronized(mouseLocation) {
      getMouseLocation(e);
    }
  }

// Mouse user interface

  /**
   * Returns the state of the mouse button.
   *
   * @return      <code>true</code> if the button is currently down;
   *              <code>false</code> otherwise.
   */
  public boolean getButton() {
    return buttonDown;
  }

  /**
   * Returns the current mouse location (relative to the corresponding 
   * drawing panel) as a <code>java.awt.Point</code>.
   *
   * @return      the <code>java.awt.Point</code> object
   */
  public java.awt.Point getMouse() {
    synchronized(mouseLocation) {
      return mouseLocation;
    }
  }

  /**
   * Waits until the mouse button is pressed or released, then returns the 
   * state of the mouse button.
   * 
   * @return      <code>true</code> if the button is currently down;
   *              <code>false</code> otherwise.
   */
  public boolean waitClick() {
    synchronized(mouseState) {
      try {
        mouseState.wait();
      }
      catch( InterruptedException it ) {}
    }
    return buttonDown;
  }

  /**
   * Waits until the mouse button is released, after being pressed, or until 
   * the mouse location is dragged.
   * Returns the issued mouse location (relative to the corresponding 
   * drawing panel) as a <code>java.awt.Point</code>.
   *
   * @return      the <code>java.awt.Point</code> object
   */
  public java.awt.Point waitMouse() {
    synchronized(mouseLocation) {
      try {
        mouseLocation.wait();
      }
      catch( InterruptedException it ) {}
      return mouseLocation;
    }
  }

  /**
   * Returns an integer value that describes the modifier keys state at the 
   * time of the last mouse pressed event.
   * The returned value is either <code>NONE</code>, or a combination of 
   * the <code>SHIFT</code>, <code>CONTROL</code> and <code>META</code> 
   * (for the <em>Alt</em> key) constant flags.
   *
   * @return      the modifier keys state as an int value.
   * @see         #NONE
   * @see         #SHIFT
   * @see         #CONTROL
   * @see         #META
   */
  public int getButtonState() {
    synchronized(mouseState) {
      return buttonState;
    }
  }

// KeyListener

  /**
   * Invoked by the event handler when a key has been typed.
   * This event occurs when a key press is followed by a key release.
   */
  public void keyTyped(KeyEvent e) {
  }

  /**
   * Invoked by the event handler when a key has been pressed.
   */
  public synchronized void keyPressed(KeyEvent e) {
    synchronized(charLock) {
      lastChar = e.getKeyChar();
      charLock.notify();
    }
  }

  /**
   * Invoked by the event handler when a key has been released.
   */
  public void keyReleased(KeyEvent e) {
  }

// Keyboard user interface

  /**
   * Returns the char corresponding to the last keyboard event, if any occurs 
   * and the char is still kept available.
   * This char remains available for further calls.
   *
   * @return      the char, if any, <code>0</code> otherwise
   * @see         #getKey()
   */
  public char peekKey() {
    return lastChar;
  }

  /**
   * Returns the char corresponding to the last keyboard event.
   * If no char is available, will block until a new keyboard event occurs.
   * The char is made unavailable for further calls.
   *
   * @return      the char
   * @see         #peekKey()
   */
  public char getKey() {
    char last;
    synchronized(charLock) {
      if( lastChar == 0 )
        try {
          charLock.wait();
        }
        catch( InterruptedException it ) {}
      last = lastChar;
      lastChar = 0;
    }
    return last;
  }

  /**
   * Returns a string representation of the state of this 
   * <code>DrawingStuff</code>.
   *
   * @return      the string representation of internal parameters
   */
  public String toString() {
    String str = getClass().getName()+"{";
    str += "pixmap=" + pixmap.toString();
    str += ",button=" + buttonDown;
    str += ",mouse=[" + mouseLocation.x + "," + mouseLocation.y + "]";
    str += ",key=['" + lastChar + "'(0x" + Integer.toString(lastChar, 16) + ")]";
    return str + "}";
  }

  /**
   * Prints a string when verbose mode is enabled.
   * For internal debug purpose only.
   */
  void trace_if_verbose(String str) {
    if ( verbose != null )
      System.err.println("**"+verbose+"** "+visibleDrawing.getName()+" : "+str);
  }

}  // end class DrawingStuff

