import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.image.PixelGrabber;
import java.awt.Rectangle;
import java.lang.Math;
import java.util.Vector;

/**
 * This class is an implementation of a graphics window, a Java
 * AWT Canvas.  All graphics is saved for re-display in the event
 * of resizing. Other features are:
 *
 * - the coordinate space is the traditional mathematics; [0,0] is
 *   in the middle of the graphics window.
 *
 * - KeyEvents; keyPressed() is captured and can be passed on.
 *
 * - MouseEvents; mouseReleased() is captured and can be passed on.
 *
 * - higher-level functionality (i.e., flood-fill provided). 
 *
 * - in-memory Image maintained with buffered operations to improve
 *   graphics display performance. 
 *
 * @author Guy Haas
 */
public class TGCanvas extends Component
                      implements FocusListener, KeyListener,
                                 MouseListener, MouseMotionListener
{

   // global constants
   //
   public static final int MAX_TURTLES = 64;
   public static final int MINIMUM_HEIGHT = 40;
   public static final int MINIMUM_WIDTH = 40;


   // constants
   //
   private static final String CLASS_NAME = "TGCanvas";

   private static final Color INITIAL_BACKGROUND = Color.white;
   private static final Color INITIAL_PEN_COLOR = Color.black;
   private static final int GI_HEIGHT = 1201;  // needs to be odd to allow for
   private static final int GI_WIDTH = 1601;   // zero at center and equal num
                                               // of neg/pos ints above/below
   private static final int INITIAL_FONT_SIZE = 14;
   private static final int INITIAL_FONT_STYLE = Font.PLAIN;
   private static final int INITIAL_PEN_SIZE = 2;
   private static final String INITIAL_FONT_NAME = "Courier";
   
   // the following STATES are needed by paint() due to the
   // async model provided by drawImage()
   private static final int PAINT_REFRESH = 0;
   private static final int PAINT_DRAW_GRAPHICS = 1;
   private static final int PAINT_ERASE_TURTLES = 2;
   private static final int PAINT_DRAW_TURTLES = 3;


   // variables with class-wide scope
   //
   private boolean gotFocus;            // used to determine when to pass on
                                        // mouseMoved Events to TGDriver
   private int canvasHeight;
   private int canvasWidth;
   private int mouseX, mouseY;          // position of mouse when last clicked
   private int paintState;              // used in paint() to determine what
                                        // needs to be done. drawImage() does
                                        // not necessarily complete and thus
                                        // forces follow-up passes of paint()
   private int paintTurtleNum;          // used in paint() to determine which
                                        // turtle is being erased/painted
   private int xCenter, yCenter;        // these AWT graphics coordinates will
                                        // be [0,0] for the TGCanvas
                                        // coordinate space. *NOTE* i tried
                                        // making these "float" but when they
                                        // had a .5 fractional part, pixel
                                        // choice by AWT was poorer. i ended
                                        // up with lines with endpoints one
                                        // pixel apart instead of straight
   private Color background;
   private Image graphicsImage;         // in-memory Image for the composite
                                        // graphics - all the stuff on the
                                        // display except for the turtle(s)
   private Rectangle[] turtleClipRect;  // clipRects used to draw turtle images
   private Turtle[] turtles;            // array of turtles that want to be
                                        // displayed 
   private Vector graphicsOps;          // a list of Graphics operations
                                        // pending processing
   private Vector keyHandlers;          // Objects that want their keyPressed()
                                        // method invoked when our KeyListener
                                        // interface: keyPressed() method
                                        // is invoked, propagating key stuff
   private Vector mouseHandlers;        // Objects that want their mouseClicked()
                                        // method invoked when our MouseListener
                                        // interface: mouseReleased() method
                                        // is invoked, propagating mouse stuff


   //
   // TGCanvas Constructors
   //
   public TGCanvas() { this( 700, 400 ); }

   public TGCanvas( int width, int height )
   {
      if ( width > MINIMUM_WIDTH )
         canvasWidth = width;
      else
         canvasWidth = MINIMUM_WIDTH;
      if ( height > MINIMUM_HEIGHT )
         canvasHeight = height;
      else
         canvasHeight = MINIMUM_HEIGHT;
      super.setSize( canvasWidth, canvasHeight );
      addFocusListener(this);
      addKeyListener(this);
      addMouseListener(this);
      addMouseMotionListener(this);
      xCenter = canvasWidth / 2;
      yCenter = canvasHeight / 2;
      Font font =  new Font( INITIAL_FONT_NAME,
                             INITIAL_FONT_STYLE,
                             INITIAL_FONT_SIZE
                           );
      setFont( font );
      background = INITIAL_BACKGROUND;
      graphicsOps = new Vector( 200, 100 );
      keyHandlers = new Vector( 5, 5 );
      mouseHandlers = new Vector( 5, 5 );
      turtleClipRect = new Rectangle[ MAX_TURTLES ];
      turtles = new Turtle[ MAX_TURTLES ];
      paintState = PAINT_REFRESH;
      gotFocus = false;

   } // end TGCanvas()


   //
   // FocusListener methods
   //
   public void focusGained(FocusEvent e)
   { gotFocus = true; }

   public void focusLost(FocusEvent e)
   { gotFocus = false; }


   //
   // KeyListener  Interface Support
   //
   public void keyPressed(KeyEvent ke)
   {
      int numHandlers = keyHandlers.size(); 
      if ( numHandlers == 0 )
         return;
      int keyCode = ke.getKeyCode();
      int tgKeyNum = 0;
      switch ( keyCode )
      {
         case KeyEvent.VK_DOWN:
            tgKeyNum = TGKeyHandler.DOWN;
            break;
         case KeyEvent.VK_ENTER:
            tgKeyNum = TGKeyHandler.ENTER;
            break;
         case KeyEvent.VK_LEFT:
            tgKeyNum = TGKeyHandler.LEFT;
            break;
         case KeyEvent.VK_RIGHT:
            tgKeyNum = TGKeyHandler.RIGHT;
            break;
         case KeyEvent.VK_SPACE:
            tgKeyNum = TGKeyHandler.SPACE;
            break;
         case KeyEvent.VK_UP:
            tgKeyNum = TGKeyHandler.UP;
            break;
      }
      if ( tgKeyNum == 0 )
         return;
      for (int idx=0; idx < numHandlers; idx++)
      {
         TGKeyHandler kh = (TGKeyHandler) keyHandlers.elementAt(idx);
         kh.keyPressed( tgKeyNum );
      }

   } // end keyPressed()

   public void keyReleased(KeyEvent ke) {}
   public void keyTyped(KeyEvent ke)
   {
      int numHandlers = keyHandlers.size(); 
      if ( numHandlers == 0 )
         return;
      char ch = ke.getKeyChar();
      //System.out.println( "keyTyped: " + ch + ", " + (int) ch );
      //System.out.println( "          " + ke.getKeyCode() );
      for (int idx=0; idx < numHandlers; idx++)
      {
         TGKeyHandler kh = (TGKeyHandler) keyHandlers.elementAt(idx);
         kh.keyPressed( ch );
      }

   } // end keyTyped()


   // MouseListener Interface Support
   //
   // I intended to only support mouseClicked(), but too many clicks
   // of the mouse never got to the program.  Searching Sun's Java
   // Developer web site turned up an explanation: if the mouse is
   // moved at all between the press and the release, it is considered
   // a mouse-stroke instead of a mouse-click.  i was not able to find
   // any details on how one could adjust a threshold differentiating
   // the two, so the solution is to use mouseReleased() instead of
   // mouseClicked()
   //
   // Currently, TGCanvas only supports capturing and propagating a
   // left-button release (with no modifiers, e.g. [Shift] key down).
   // So, if a MouseEvent is received that isn't for TGCanvas, the
   // AWT tree is walked and if a MouseListener is found in a parent
   // component - the MouseEvent is given to it.
   //
   private void fwdMouseEvent( int id, MouseEvent me )
   {
      boolean popupTrigger = me.isPopupTrigger();
      long when = me.getWhen();
      int clickCount = me.getClickCount();
      int modifiers = me.getModifiers();
      int x = me.getX();
      int y = me.getY();
      Rectangle r = getBounds();
      x += r.x;
      y += r.y;
      Container parent = getParent();
      while ( parent != null )
      {
         Class c = parent.getClass();
         Class[] interfaces = c.getInterfaces();
         for ( int i=0; i < interfaces.length; i++)
         {
            String name = interfaces[i].getName();
            if ( name.equals("java.awt.event.MouseListener") )
            {
               MouseEvent nme = new MouseEvent(parent, id, when, modifiers,
                                               x, y, clickCount, popupTrigger);
               switch ( id )
               {
                 case MouseEvent.MOUSE_PRESSED:
                     ((MouseListener)parent).mousePressed( nme );
                     return;
                 case MouseEvent.MOUSE_RELEASED:
                     ((MouseListener)parent).mouseReleased( nme );
                     return;
                 case MouseEvent.MOUSE_CLICKED:
                     ((MouseListener)parent).mouseClicked( nme );
                     return;
                 case MouseEvent.MOUSE_ENTERED:
                     ((MouseListener)parent).mouseEntered( nme );
                     return;
                 case MouseEvent.MOUSE_EXITED:
                     ((MouseListener)parent).mouseExited( nme );
                     return;
                 case MouseEvent.MOUSE_MOVED:
                     ((MouseMotionListener)parent).mouseMoved( nme );
                     return;
                 case MouseEvent.MOUSE_DRAGGED:
                     ((MouseMotionListener)parent).mouseDragged( nme );
                     return;
                 default:
                     System.err.println(CLASS_NAME+".fwdMouseEvent: bad id");
                     return;
               }
            }
         }
         r = parent.getBounds();
         x += r.x;
         y += r.y;
         parent = parent.getParent();
      }

   } // end fwdMouseEvent()


   // MouseListener interface methods 
   //

   public void mouseClicked(MouseEvent me)
   { fwdMouseEvent( MouseEvent.MOUSE_CLICKED, me ); }
   public void mouseEntered(MouseEvent me)
   { fwdMouseEvent( MouseEvent.MOUSE_ENTERED, me ); }
   public void mouseExited(MouseEvent me)
   { fwdMouseEvent( MouseEvent.MOUSE_EXITED, me ); }
   public void mousePressed(MouseEvent me)
   { fwdMouseEvent( MouseEvent.MOUSE_PRESSED, me ); }

   public void mouseReleased(MouseEvent me)
   {
      int modifiersMask = me.getModifiers();
      if ( modifiersMask == InputEvent.BUTTON1_MASK )
      {
         mouseX = me.getX();
         mouseY = me.getY();
         int numHandlers = mouseHandlers.size(); 
         for (int idx=0; idx < numHandlers; idx++)
         {
            TGMouseHandler mh = (TGMouseHandler) mouseHandlers.elementAt(idx);
            mh.mouseClicked();
         }
         this.requestFocus();
      }
      else
         fwdMouseEvent( MouseEvent.MOUSE_RELEASED, me );

   } // end mouseReleased()


   // MouseMotionListener interface methods 

   public void mouseDragged(MouseEvent me) { }

   public void mouseMoved(MouseEvent me)
   {
      if ( gotFocus )
      {
         mouseX = me.getX();
         mouseY = me.getY();
         //System.out.println(CLASS_NAME + ".mouseMoved: " + mouseX + "," + mouseY);
         int numHandlers = mouseHandlers.size(); 
         for (int idx=0; idx < numHandlers; idx++)
         {
            TGMouseHandler mh = (TGMouseHandler) mouseHandlers.elementAt(idx);
            mh.mouseMoved();
         }
      }

   } // end mouseMoved()



   //
   // support methods only used in this class
   //

   private void addGraphObj( Object obj )
   {
      //System.out.println("TGCanvas.addGraphObj: obj="+obj);
      synchronized (graphicsOps)
      { graphicsOps.addElement( obj ); }

   } // end addGraphObj()


   private void clearGraphicsImage()
   {
      if ( graphicsImage != null )
      {
         Graphics giGraphics = graphicsImage.getGraphics();
         giGraphics.setClip( 0, 0, GI_WIDTH-1, GI_HEIGHT-1 );
         giGraphics.setColor( background );
         giGraphics.fillRect( 0, 0, GI_WIDTH-1, GI_HEIGHT-1 );
         giGraphics.setColor( INITIAL_PEN_COLOR );
         giGraphics.dispose();
      }

   } // end clearGraphicsImage()


   private void initGraphicsImage()
   {
      graphicsImage = createImage( GI_WIDTH, GI_HEIGHT );
      clearGraphicsImage();

   } // end initGraphicsImage()


   // apply all outstanding graphics operations to graphicsImage.
   // return a clipRect for area of me (TGCanvas extends Component)
   // that is to be painted into, based on bits changed in the
   // graphicsImage
   private Rectangle renderGraphics()
   {
      int giLeftX = GI_WIDTH;
      int giRightX = -1;
      int giUpperY = GI_HEIGHT;
      int giLowerY = -1;
      if ( graphicsImage == null )
      {
         initGraphicsImage();
         giLeftX = 0;
         giRightX = GI_WIDTH-1;
         giUpperY = 0;
         giLowerY = GI_HEIGHT-1;
      }
      synchronized ( graphicsOps )
      {
         for (int opsCount = graphicsOps.size(); opsCount > 0; opsCount--)
         {
            TGGraphicsOp op = (TGGraphicsOp) graphicsOps.firstElement();
            //System.out.println("render: op="+op);
            Rectangle clipRect = op.doIt( graphicsImage );
            if ( clipRect != null )
            {
               if ( clipRect.x < giLeftX )
                  giLeftX = clipRect.x;
               if ( clipRect.y < giUpperY )
                  giUpperY = clipRect.y;
               int coord = clipRect.x + clipRect.width - 1;
               if ( coord > giRightX )
                  giRightX = coord;
               coord = clipRect.y + clipRect.height - 1;
               if ( coord > giLowerY )
                  giLowerY = coord;
            }
            graphicsOps.removeElementAt( 0 );
         }
      }
      //System.out.print("render: left="+giLeftX+", right="+giRightX);
      //System.out.println(", upper="+giUpperY+", lower="+giLowerY);
      int width = (giRightX + 1) - giLeftX;
      int widthInset = (GI_WIDTH - canvasWidth) / 2;
      int canvasLeftX = giLeftX - widthInset;
      if ( canvasLeftX < 0 )    // if negative, at least some of the
      {                         // painted pixels are to the left of
         width += canvasLeftX;  // the canvas, so adjust width
         canvasLeftX = 0;       // appropriately and reset left-most
      }                         // pixel number
      if ( width > canvasWidth )
         width = canvasWidth;
      int height = (giLowerY + 1) - giUpperY;
      int heightInset = (GI_HEIGHT - canvasHeight) / 2;
      int canvasUpperY = giUpperY - heightInset;
      if ( canvasUpperY < 0 )
      {
         height += canvasUpperY;
         canvasUpperY = 0;
      }
      if ( height > canvasHeight )
         height = canvasHeight;
      if ( height > 0 && width > 0 )
         return new Rectangle( canvasLeftX, canvasUpperY, width, height );
      return null;

   } // end renderGraphics()


   //
   // Overridden Canvas/Component Methods
   //

   public Dimension getMinimumSize()
   { return new Dimension ( MINIMUM_WIDTH, MINIMUM_HEIGHT ); }

   public Dimension getPreferredSize()
   { return new Dimension ( canvasWidth, canvasHeight ); }

   public Dimension getSize()
   { return new Dimension ( canvasWidth, canvasHeight ); }


   // either TGGraphicsOps have been queued to be performed or the
   // AWT has decided we need to redraw at least some part of the
   // Canvas, e.g., it was partially covered by some other window
   // that has moved/gone away.
   //
   public void paint(Graphics g)
   {
      //System.out.println("TGCanvas.paint: got here!");
      Rectangle rect = g.getClipBounds();
      int heightDiff = (GI_HEIGHT - canvasHeight) / 2;
      int widthDiff = (GI_WIDTH - canvasWidth) / 2;
      switch ( paintState )
      {
         case PAINT_REFRESH:
            if ( graphicsImage == null )
            {
               g.setColor( background );
               g.fillRect( 0, 0, canvasWidth, canvasHeight );
            }
            else
               if ( ! g.drawImage(graphicsImage, -widthDiff, -heightDiff, this) )
                  return;
            paintState = PAINT_DRAW_GRAPHICS;
         case PAINT_DRAW_GRAPHICS:
            rect = renderGraphics();
            if ( rect != null )
            {
               g.setClip( rect );
               if ( ! g.drawImage(graphicsImage, -widthDiff, -heightDiff, this) )
                  return;
            }
            paintState = PAINT_ERASE_TURTLES;
            paintTurtleNum = 0;
         case PAINT_ERASE_TURTLES:
            while ( paintTurtleNum < turtleClipRect.length )
            {
               if ( (rect = turtleClipRect[paintTurtleNum]) != null )
               {
                  g.setClip( rect );
                  if ( ! g.drawImage(graphicsImage, -widthDiff, -heightDiff, this) )
                     return;
                  turtleClipRect[paintTurtleNum] = null;
               }
               paintTurtleNum++;
            }
            paintState = PAINT_DRAW_TURTLES;
            paintTurtleNum = 0;
         case PAINT_DRAW_TURTLES:
            while ( paintTurtleNum < turtles.length )
            {
               Turtle turtle = turtles[paintTurtleNum];
               if ( turtle != null )
               {
                  int turtleX = (int) Math.rint( turtle.xcor() + xCenter );
                  int turtleY = (int) Math.rint( yCenter - turtle.ycor() );
                  int imgSz = turtle.getImageSideSize();
                  int imgLeftX = turtleX - imgSz/2;
                  int imgTopY = turtleY - imgSz/2;
                  g.setClip( imgLeftX, imgTopY, imgSz, imgSz);
                  if ( ! g.drawImage (turtle.getImage(), imgLeftX, imgTopY, this) )
                     return;
                  turtleClipRect[paintTurtleNum] = new Rectangle(imgLeftX, imgTopY, imgSz, imgSz);
               }
               paintTurtleNum++;
            }
            paintState = PAINT_REFRESH;
      }

   } //end paint()


   public void setBounds( int x, int y, int width, int height )
   {
      super.setBounds( x, y, width, height );
      canvasWidth = width;
      canvasHeight = height;
      xCenter = width / 2;
      yCenter = height / 2;
      repaint();

   } // end setBounds()


   public void setSize( int width, int height )
   {
      super.setSize( width, height );
      canvasWidth = width;
      canvasHeight = height;
      xCenter = width / 2;
      yCenter = height / 2;
      repaint();

   } // end setSize()


   // override update() to eliminate its invocation of clear()
   //
   public void update(Graphics g) { paint(g); }


   // -------------------------------------------------------
   // methods available outside the class - exposed interface
   // -------------------------------------------------------


   public void addKeyHandler( TGKeyHandler kh )
   {
      if ( ! keyHandlers.contains(kh) )
         keyHandlers.addElement( kh );

   } // end addKeyHandler


   public void addMouseHandler( TGMouseHandler mh )
   {
      if ( ! mouseHandlers.contains(mh) )
         mouseHandlers.addElement( mh );

   } // end addMouseHandler()


   public void addTurtle( Turtle turtle )
   {
      int openIdx = -1;
      for (int i=turtles.length-1; i >= 0; i--)
      {
         if ( turtles[i] != null )
         {
            if ( turtles[i] == turtle )
               return;
         }
         else
            openIdx = i;
      }
      if ( openIdx < 0 )
      {
         System.err.println(CLASS_NAME+".addTurtle: no room!");
         return;
      }
      turtles[openIdx] = turtle;
      repaint();

   } // end addTurtle()


   public void setbg( int rgbValue )
   {
      synchronized ( graphicsOps )
      { graphicsOps.removeAllElements(); }
      background = Turtle.rgbToColor( rgbValue );
      clearGraphicsImage();
      repaint();
   }

   public int canvasHeight()
   { return canvasHeight; }

   public int canvasWidth()
   { return canvasWidth; }


   /**
    * Clean graphics off of the display.
    */
   public void clean()
   {
      synchronized ( graphicsOps )
      { graphicsOps.removeAllElements(); }
      clearGraphicsImage();
      repaint();

   } // end clean()


   public int colorunder( TGPoint curXY )
   {
      String me = CLASS_NAME + ".colorunder: ";
      int imageX = curXY.imageX( GI_WIDTH );
      if ( imageX < 0 || imageX > GI_WIDTH )
         return background.getRGB() & 0xffffff;
      int imageY = curXY.imageY( GI_HEIGHT );
      if ( imageY < 0 || imageY > GI_HEIGHT )
         return background.getRGB() & 0xffffff;
      int[] pixel = new int[1];
      if ( graphicsImage == null )
         return background.getRGB() & 0xffffff;
      PixelGrabber pg = new PixelGrabber( graphicsImage,
                                          imageX, imageY,
                                          1, 1,
                                          pixel,
                                          0,
                                          GI_WIDTH
                                        );
      try { pg.grabPixels(); }
      catch (InterruptedException e)
      {
         System.err.println( me + "grabPixels interrupted" );
         return background.getRGB() & 0xffffff;
      }
      int status = pg.getStatus();
      //if ( (status & ImageObserver.ALLBITS ) == 0)
      //{
      //   System.err.print( me + "grabPixels ALLBITS not set" );
      //   System.err.println( ", status=" + status );
      //   //return background.getRGB() & 0xffffff;
      //}
      return pixel[0] & 0xFFFFFF;

   } // end colorunder()


   public TGPoint drawLine(TGPoint p1, double steps, double hd, int wd, Color cl )
   {
      if ( steps < 0 )
      {
         hd -= Math.PI;
         if ( hd < 0 )
            hd += (2 * Math.PI);
         steps = -steps;
      }
      TGPoint p2 = p1.otherEndPoint( hd, steps );
      addGraphObj( new TGLineOp(p1, p2, hd, cl, wd) );
      return p2;
   }

   public void drawLine( TGPoint p1, TGPoint p2, double hd, int wd, Color cl )
   { addGraphObj( new TGLineOp(p1, p2, hd, cl, wd) ); }


   public void fill( TGPoint point, Color color )
   { addGraphObj( new TGFillOp(point, color) ); }


   public void label( String text, TGPoint p, Font font, Color color )
   { addGraphObj( new TGLabelOp(text, p, font, color) ); }


   /**
    * Return a TurtleSpace x-coordinate of last mouse click
    *
    * @see #mousey
    */
   public int mousex()
   {
      return mouseX - xCenter;
   }

   /**
    * Return a TurtleSpace y-coordinate of last mouse click
    *
    * @see #mousex
    */
   public int mousey()
   {
      return -(mouseY - yCenter);
   }


   public void removeKeyHandler( TGKeyHandler kh )
   { keyHandlers.removeElement( kh ); }

   public void removeMouseHandler( TGMouseHandler mh )
   { mouseHandlers.removeElement( mh ); }


   public void removeTurtle( Turtle turtle )
   {
      for (int i=turtles.length-1; i >= 0; i--)
         if ( turtles[i] == turtle )
         {
            turtles[i] = null;
            return;
         }
      System.err.println(CLASS_NAME+".removeTurtle: turtle missing!");

   } // end removeTurtle()

} // end class TGCanvas