package gl;

import java.awt.*;
import java.util.*;

/**
 * Extends <code>Canvas</code> with some additional features for interactive
 * two-dimensional graphics.  The <code>Slate</code> class is intended to be
 * subclassed by the user so that a <code>paint</code> method can be provided.
 * <p>
 * This class is loosely based on the OpenGL API.  A <em>scene</em> is
 * composed of <em>primitives</em>, such as lines, polygons, etc., that are
 * constructed and then placed into the scene.
 * <p>
 * Slates can use either a <em>single</em> or <em>double buffer</em>.
 * Single buffering is appropriate for static images while double buffering
 * is usually used for animations.  When double buffering is used, the
 * drawing is actually done to a hidden <em>graphics context</em> that is
 * made visible with a call to <code>swapBuffers</code>.
 * 
 * @author	Jonathan R. Senning, Gordon College.
 * @version	$Id: Slate.java,v 1.5 1998/03/10 15:40:33 senning Exp $.
 */

public class Slate extends Canvas
{
    //=======================================================================
    // Public constants
    //=======================================================================

    /**
     * Indicates that double buffering is desired.
     */

    public static final int	DOUBLE_BUFFER	= 0x00000001;

    /**
     * Identifies the projection transformation matrix.
     */

    public static final int	PROJECTION	= 0x00000002;

    /**
     * Identifies the modelview transformation matrix.
     */

    public static final int	MODELVIEW	= 0x00000004;
    
    /**
     * Indicates that a 3D rectangle is used when the slate is cleared.
     */
    
    public static final int	THREED_SLATE	= 0x00000008;

    //=======================================================================
    // Private variables
    //=======================================================================

    private Matrix3x3	_projectionMatrix;	// current projection matrix
    private Matrix3x3	_modelviewMatrix;	// current transformation
    private Matrix3x3	_currentMatrix;		// current matrix
    private Matrix3x3	_transformMatrix;	// actual transform matrix
    private Stack	_projectionStack;	// stack of projections
    private Stack	_modelviewStack;	// stack of transformations
    private Stack	_currentStack;		// current matrix stack
    private boolean	_doubleBuffering = false;	// DB initially off
    private boolean	_threeDimSlate = false;		// 3D slate init off

    private int		_width;			// slate width in pixels
    private int		_height;		// slate height in pixels
    
    private FontMetrics	_fontMetrics;
    private int		_fontDescent;
    private int		_fontHeight;

    private Image	_backImage;		// used for double buffering
    private Graphics	_backGraphics;		// used for double buffering
    private Graphics	_frontGraphics;		// visible graphics
    private Graphics	_currentGraphics;	// graphics we draw to
    private Color	_backgroundColor;	// clear slate to this color
    
    //=======================================================================
    // Constructors
    //=======================================================================

    /**
     * Class constructor using slate width and height.
     *
     * @param width	slate width in pixels.
     * @param height	slate height in pixels.
     */

    public Slate( int width, int height )
    {
	this( width, height, 0 );	// mode = 0 -> no double buffering
    }

    //-----------------------------------------------------------------------

    /**
     * Class constructor - using slate width and height and the buffering
     * mode.
     * 
     * @param width	slate width in pixels.
     * @param height	slate height in pxiels.
     * @param mode	flag to signal double buffering.
     */

    public Slate( int width, int height, int mode )
    {
	_width  = width;
	_height = height;
	_doubleBuffering  = ( mode & DOUBLE_BUFFER ) != 0;
	_threeDimSlate	  = ( mode & THREED_SLATE ) != 0;
	_backgroundColor  = Color.lightGray;
	_projectionMatrix = new Matrix3x3();
	_modelviewMatrix  = new Matrix3x3();
	_transformMatrix  = new Matrix3x3();
	_projectionStack  = new Stack();
	_modelviewStack   = new Stack();
	_currentMatrix    = _modelviewMatrix;
	_currentStack	  = _modelviewStack;
    }

    //=======================================================================
    // Overloaded methods
    //=======================================================================
    
    /**
     * Overloaded to force call to <code>init</code>.
     */

    public synchronized void addNotify()
    {
        super.addNotify();
        init();
    }

    //-----------------------------------------------------------------------

    /**
     * Overloaded to provide correct dimensions.
     */

    public Dimension preferredSize()
    {
        return new Dimension( _width, _height );
    }

    //-----------------------------------------------------------------------

    /**
     * Overloaded to provide correct dimensions.
     */

    public Dimension minimumSize()
    {
        return new Dimension( _width, _height );
    }

    //-----------------------------------------------------------------------

    /**
     * Overloaded so that the screen is not cleared before painting.
     */

    public void update( Graphics g )
    {
	paint( g );	// user is expected to provide paint() in subclass
    }

    //=======================================================================
    // New methods
    //=======================================================================

    /**
     * Class initializer.  Its primary responsibility is to initialize
     * the appropriate <code>Graphics</code>.
     */

    public void init()
    {
	_frontGraphics = this.getGraphics();
	if ( _doubleBuffering ) {
	    _backImage = createImage( _width, _height );
	    _backGraphics = _backImage.getGraphics();
	    _currentGraphics = _backGraphics;
	} else {
	    _backImage = null;
	    _backGraphics = null;
	    _currentGraphics = _frontGraphics;
	}
	_fontMetrics = getFontMetrics( getFont() );
	_fontDescent = _fontMetrics.getDescent();
	_fontHeight  = _fontMetrics.getHeight();
    }

    //-----------------------------------------------------------------------

    /**
     * Set background color for the slate.
     * 
     * @param c		background color.
     */

    public void clearColor( Color c )
    {
	setBackground( c );
	_backgroundColor = c;
    }

    //-----------------------------------------------------------------------

    /**
     * Set current color for primitives.
     * 
     * @param c		current color.
     */

    public void color( Color c )
    {
	_currentGraphics.setColor( c );
    }

    //-----------------------------------------------------------------------

    /**
     * Resets current matrix to identity.
     */

    public void loadIdentity()
    {
	_currentMatrix.loadIdentity();
	_transformMatrix = _projectionMatrix.times( _modelviewMatrix );
    }

    //-----------------------------------------------------------------------

    /**
     * Replaces the current matrix by a matrix constructed from the
     * supplied values.
     * 
     * @param a--i	elements of matrix in row-wise order.
     */

    public void loadMatrix( double a, double b, double c,
			    double d, double e, double f,
			    double g, double h, double i )
    {
	Matrix3x3 S = new Matrix3x3( a, b, c, d, e, f, g, h, i );
	_currentMatrix.copy( S );
	_transformMatrix = _projectionMatrix.times( _modelviewMatrix );
    }

    //-----------------------------------------------------------------------

    /**
     * Multiplies the current matrix by a matrix constructed from the
     * supplied values.
     * 
     * @param a--i	elements of matrix in row-wise order.
     */

    public void multMatrix( double a, double b, double c,
			    double d, double e, double f,
			    double g, double h, double i )
    {
	Matrix3x3 S = new Matrix3x3( a, b, c, d, e, f, g, h, i );
	_currentMatrix.multiplyOnRightBy( S );
	_transformMatrix = _projectionMatrix.times( _modelviewMatrix );
    }

    //-----------------------------------------------------------------------

    /**
     * Set orthographic projection.
     * 
     * @param x0	x coordinate of lower-left corner.
     * @param x1	x coordinate of upper-right corner.
     * @param y0	y coordinate of lower-left corner.
     * @param y1	y coordinate of upper-right corner.
     */

    public void ortho( double x0, double x1, double y0, double y1 )
    {
        double sx = (double) ( _width  - 1 ) / ( x1 - x0 );
        double sy = (double) ( _height - 1 ) / ( y1 - y0 );

        scale( sx, sy );
        translate( -x0, -y0 );
    }

    //-----------------------------------------------------------------------

    /**
     * Updates current matrix with scaling transformation.
     * 
     * @param sx	scale factor in x direction.
     * @param sy	scale factor in y direction.
     */

    public void scale( double sx, double sy )
    {
	Matrix3x3 S = new Matrix3x3( sx, 0.0, 0.0, 0.0, sy, 0.0, 0.0, 0.0, 1.0 );
	_currentMatrix.multiplyOnRightBy( S );
	_transformMatrix = _projectionMatrix.times( _modelviewMatrix );
    }

    //-----------------------------------------------------------------------

    /**
     * Updates current matrix with translation transformation.
     *
     * @param tx	translation amount in x direction.
     * @param ty	translation amount in y direction.
     */

    public void translate( double tx, double ty )
    {
	Matrix3x3 T = new Matrix3x3( 1.0, 0.0, tx, 0.0, 1.0, ty, 0.0, 0.0, 1.0 );
	_currentMatrix.multiplyOnRightBy( T );
	_transformMatrix = _projectionMatrix.times( _modelviewMatrix );
    }

    //-----------------------------------------------------------------------

    /**
     * Updates current matrix with rotation transformation.
     *
     * @param theta	angle (in degrees) of (counterclockwise) rotation.
     */

    public void rotate( double theta )
    {
	double angle = theta * Math.PI / 180.0;
	double c = Math.cos( angle );
	double s = Math.sin( angle );
	Matrix3x3 R = new Matrix3x3( c, -s, 0.0, s, c, 0.0, 0.0, 0.0, 1.0 );
	_currentMatrix.multiplyOnRightBy( R );
	_transformMatrix = _projectionMatrix.times( _modelviewMatrix );
    }

    //-----------------------------------------------------------------------

    /**
     * Clears slate graphics area.
     */

    public void clear()
    {
	Color c = _currentGraphics.getColor();
	_currentGraphics.setColor( _backgroundColor );
	if ( _threeDimSlate ) {
	    _currentGraphics.fill3DRect( 0, 0, _width, _height, true );
	} else {
	    _currentGraphics.fillRect( 0, 0, _width, _height );
	}
	_currentGraphics.setColor( c );
    }

    //-----------------------------------------------------------------------
    
    /**
     * Selects the current matrix mode.
     * 
     * @param mode	matrix mode: either PROJECTION or MODELVIEW.
     */

    public void matrixMode( int mode )
    {
	if ( ( mode & PROJECTION ) != 0 ) {
	    _currentMatrix = _projectionMatrix;
	    _currentStack  = _projectionStack;
	} else if ( ( mode & MODELVIEW ) != 0 ) {
	    _currentMatrix = _modelviewMatrix;
	    _currentStack  = _modelviewStack;
	}
    }

    //-----------------------------------------------------------------------
    
    /**
     * Save copy of current matrix on current matrix stack.
     */

    public void pushMatrix()
    {
	_currentStack.push( new Matrix3x3( _currentMatrix ) );
    }

    //-----------------------------------------------------------------------
    
    /**
     * Replace current transformation matrix by popping one off the stack.
     */

    public void popMatrix()
    {
	_currentMatrix.copy( (Matrix3x3) _currentStack.pop() );
	_transformMatrix = _projectionMatrix.times( _modelviewMatrix );
    }

    //-----------------------------------------------------------------------

    /**
     * Add a primitive to the current graphics scene.
     * 
     * @param p		the Primitive to add.
     */

    public void addPrimitive( Primitive p )
    {
	drawPrimitive( _currentGraphics, p );	
    }

    //-----------------------------------------------------------------------

    /**
     * Add a string to the current graphics scene.
     * 
     * @param s		the string to add.
     * @param v		the location for the string to start.
     */

    public void addString( String s, Vertex v )
    {
	drawString( _currentGraphics, s, v );	
    }

    //-----------------------------------------------------------------------

    /**
     * Copies the back buffer (if double buffering is enabled) to the front
     * buffer.  Actually, the buffers are not swapped -- the contents of the
     * front buffer are merely replaced with the contents of the back buffer.
     * The original front buffer contents are destroyed.
     */

    public void swapBuffers()
    {
	if ( _doubleBuffering ) {
	    _frontGraphics.drawImage( _backImage, 0, 0, this );
	}
    }

    //=======================================================================
    // Protected methods
    //=======================================================================

    //-----------------------------------------------------------------------

    /**
     * Draws a primitive in the provided graphics context.  Determines the
     * type of <code>Primitive</code> and uses <code>Graphics</code> methods
     * to carry out the work.
     * 
     * @param g		the <code>Graphics</code> context to draw in.
     * @param p		the <code>Primitive</code> to draw.
     */

    protected void drawPrimitive( Graphics g, Primitive p )
    {
	int type = p.getType();
	int size = p.size();

	if ( type == Primitive.POINTS ) {
	    for ( int i = 0; i < size; i++ )
	    {
		Vertex v = (Vertex) p.elementAt( i );
		Point pt = computePoint( v );
		g.drawLine( pt.x, pt.y, pt.x, pt.y );
	    }
	} else if ( type == Primitive.LINES ) {
	    for ( int i = 0; i < size; i += 2 )
	    {
		Vertex v0 = (Vertex) p.elementAt( i );
		Vertex v1 = (Vertex) p.elementAt( i + 1 );
		Point pt0 = computePoint( v0 );
		Point pt1 = computePoint( v1 );
		g.drawLine( pt0.x, pt0.y, pt1.x, pt1.y );
	    }
	} else if ( type == Primitive.LINE_STRIP ) {
	    Polygon poly = new Polygon();
	    for ( int i = 0; i < size; i++ )
	    {
		Vertex v = (Vertex) p.elementAt( i );
		Point pt = computePoint( v );
		poly.addPoint( pt.x, pt.y );
	    }
	    g.drawPolygon( poly );
	} else if ( type == Primitive.LINE_LOOP ) {
	    Polygon poly = new Polygon();
	    for ( int i = 0; i < size; i++ )
	    {
		Vertex v = (Vertex) p.elementAt( i );
		Point pt = computePoint( v );
		poly.addPoint( pt.x, pt.y );
	    }
	    poly.addPoint( poly.xpoints[0], poly.ypoints[0] );
	    g.drawPolygon( poly );
	} else if ( type == Primitive.POLYGON ) {
	    Polygon poly = new Polygon();
	    for ( int i = 0; i < size; i++ )
	    {
		Vertex v = (Vertex) p.elementAt( i );
		Point pt = computePoint( v );
		poly.addPoint( pt.x, pt.y );
	    }
	    poly.addPoint( poly.xpoints[0], poly.ypoints[0] );
	    g.fillPolygon( poly );
	} else if ( type == Primitive.CIRCLE ) {
	    Vertex v1 = (Vertex) p.elementAt( 0 );	// center
	    Vertex v2 = (Vertex) p.elementAt( 1 );	// point on circle
	    double r = (v1.minus( v2 )).radius();
	    Point p0 = computePoint( v1.plus( -r,  r ) );
	    Point p1 = computePoint( v1.plus(  r, -r ) );
	    g.drawOval( p0.x, p0.y, p1.x - p0.x + 1, p1.y - p0.y + 1 );
	} else if ( type == Primitive.STRING ) {
	    Vertex v = (Vertex) p.elementAt( 0 );
	    String s = (String) p.elementAt( 1 );
	    Point pt = computePoint( v );
	    g.drawString( s, pt.x, pt.y );
	} else {
	    System.err.println( "Slate.addPrimitive(): unknown primitive: " +
			       	type );
	}
    }

    //-----------------------------------------------------------------------

    /**
     * Draw a string to the current graphics scene.
     * 
     * @param g		the <code>Graphics</code> context to draw in.
     * @param s		the string to draw.
     * @param v		the location for the string to start.
     */

    protected void drawString( Graphics g, String s, Vertex v )
    {
	Point p = computePoint( v );
	g.drawString( s, p.x, p.y );
    }
    
    //-----------------------------------------------------------------------

    /**
     * Apply the current transformation to a vertex to produce a point in
     * the graphics context.
     * 
     * @param v		the <code>Vertex</code> to transform.
     * @return		a <code>Point</code> (location in graphics context).
     */

    protected Point computePoint( Vertex v )
    {
//	Matrix3x3 M = _projectionMatrix.times( _modelviewMatrix );
	Vertex u = matrixTimesVertex( _transformMatrix, v );

	return new Point( (int) ( u.x + 0.5 ),
			     _height - 1 - (int) ( u.y + 0.5 ) );
    }

    //-----------------------------------------------------------------------

    /**
     * Invert the current transformation on a point to produce a vertex.
     * 
     * @param p		the <code>Point</code> to transform.
     * @return		a <code>Vertex</code> (location in world coordinates).
     */

    protected Vertex computeVertex( Point p )
    {
//	Matrix3x3 M = _projectionMatrix.times( _modelviewMatrix );
	Vertex v = new Vertex( (double) p.x, (double) ( _height - 1 - p.y ) );
	return matrixTimesVertex( _transformMatrix.inverse(), v );
    }

    //-----------------------------------------------------------------------

    /**
     * Multiply a vertex by a matrix (on the left).
     * 
     * @param M		the <code>Matrix</code>.
     * @param v		the <code>Vertex</code>.
     * @return		a <code>Vertex</code> (product of matrix and vertex).
     */

    protected Vertex matrixTimesVertex( Matrix3x3 M, Vertex v )
    {
	Vertex u = new Vertex(
	    ( M.elem[0][0] * v.x + M.elem[0][1] * v.y + M.elem[0][2] * v.z ),
	    ( M.elem[1][0] * v.x + M.elem[1][1] * v.y + M.elem[1][2] * v.z ),
	    ( M.elem[2][0] * v.x + M.elem[2][1] * v.y + M.elem[2][2] * v.z ) );
	return u;
    }

    //-----------------------------------------------------------------------
}
