import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.image.ImageObserver;
import java.awt.image.MemoryImageSource;
import java.awt.image.PixelGrabber;
import java.awt.Rectangle;
import java.awt.Toolkit;

/*
 * This class implements a TurtleGraphics FILL graphics operation.
 *
 * Complexity in performing a FILL has its roots in the AWT's
 * mechanism for manipulating the RGB values of pixels representing
 * a Component.  The Component in TGCanvas' case is the Canvas
 * that has been drawn on.  Access to the pixels comes via the
 * in-memory Image which is maintained by TGCanvas.
 *
 * Due to an early emphasis placed on supporting the WWW (java
 * applets), image manipulation is asynchronous.  Specifically,
 * Graphics.drawImage() schedules/requests that the task is to be
 * done. IT DOESN'T NECESSARILY DO IT.  The situation is the same
 * going in the other direction, but PixelGrabber.grabPixels()
 * waits for completion.
 *
 * Soapbox Opinion: Asynchronous manipulation of an Image makes
 * sense when its source is bits out on the net, it makes no
 * sense when the bits are in memory.  In fact, the M$ Windows
 * implementation appears to treat the in-memory case specially
 * and do it synchronously.
 *
 * Second Complexity... once the FILL has been done on the pixels,
 * the new Image needs to be generated.  This is done with the
 * createImage() method.  The problem is that this is a "peer"
 * method, i.e., is different for each system.  On Apple OS-X
 * and Sun systems, createImage() dithers the pixels in the
 * process.  Nasty!!!  This means that back-to-back FILLs do not
 * work!  Worse yet, the dithering effects all pixels in the
 * rectangle passed to createImage().  So, I've added complexity
 * to limit the rectangle of pixels given to createImage().
 *
 * I'm not even going to comment on how ludicrus the decision
 * to dither supplied pixels was, especially with no alternative.
 *
 * @author Guy Haas
 */

class TGFillOp implements ImageObserver, TGGraphicsOp
{

   // constants
   //
   private static final int EXPAND_SIZE = 10;


   // variables with class-wide scope
   //
   private boolean waitingForImage;
   private Color color;        // FILL Color
   private Image sourceImage;
   private int maxFloodX;
   private int maxFloodY;
   private int minFloodX;
   private int minFloodY;
   private int sourceHeight;
   private int sourceWidth;
   private int subPixHt;
   private int subPixLeftX;
   private int subPixTopY;
   private int subPixWd;
   private int xCenter;
   private int yCenter;
   private int[] pixels;       // holds the RGB pixel representation
                               // of an Image
   private TGPoint point;      // center of the FILL operation


   //
   // constructor
   //
   public TGFillOp( TGPoint point, Color color )
   {
      this.color = color;
      this.point = point;
      waitingForImage = false;
   }


   private void addLeftPixels()
   {
      int addedPixWidth = EXPAND_SIZE;
      if ( subPixLeftX < EXPAND_SIZE )
         addedPixWidth = subPixLeftX;
      subPixLeftX -= addedPixWidth;
      getPixels( subPixLeftX, subPixTopY, addedPixWidth, subPixHt );
      subPixWd += addedPixWidth;

   } // end addLeftPixels()


   private void addLowerPixels()
   {
      int topY = subPixTopY + subPixHt;
      int addedPixHeight = EXPAND_SIZE;
      if ( topY+EXPAND_SIZE > sourceHeight )
         addedPixHeight = sourceHeight - topY;
      getPixels( subPixLeftX, topY, subPixWd, addedPixHeight );
      subPixHt += addedPixHeight;

   } // end addLowerPixels()


   private void addRightPixels()
   {
      int leftX = subPixLeftX + subPixWd;
      int addedPixWidth = EXPAND_SIZE;
      if ( leftX+EXPAND_SIZE > sourceWidth )
         addedPixWidth = sourceWidth - leftX;
      getPixels( leftX, subPixTopY, addedPixWidth, subPixHt );
      subPixWd += addedPixWidth;

   } // end addRightPixels()


   private void addUpperPixels()
   {
      int addedPixHeight = EXPAND_SIZE;
      if ( subPixTopY < EXPAND_SIZE )
         addedPixHeight = subPixTopY;
      subPixTopY -= addedPixHeight;
      getPixels( subPixLeftX, subPixTopY, subPixWd, addedPixHeight );
      subPixHt += addedPixHeight;

   } // end addUpperPixels()


   private void floodFill(int x, int y, int wd, int ht, int curRGB, int newRGB)
   {
      int pixIdx;

      //System.out.println("floodFill: x="+x+", y="+y);
      int leftLimit = x, rightLimit = x;
      if ( y < minFloodY )
         minFloodY = y;
      if ( y > maxFloodY )
         maxFloodY = y;
      for ( int i = x; i >= 0; i-- )
      {
         if ( i < subPixLeftX )
            addLeftPixels();
         pixIdx = y * wd + i;
         if ( (pixels[pixIdx] & 0xFFFFFF) != curRGB )
            break;
         pixels[pixIdx] = (pixels[pixIdx] & 0xFF000000) | newRGB;
         leftLimit = i;
      }
      if ( leftLimit < minFloodX )
         minFloodX = leftLimit;
      for ( int i = x+1; i < wd; i++ )
      {
         if ( i == (subPixLeftX + subPixWd) )
            addRightPixels();
         pixIdx = y * wd + i;
         if ( (pixels[pixIdx] & 0xFFFFFF) != curRGB )
            break;
         pixels[pixIdx] = (pixels[pixIdx] & 0xFF000000) | newRGB;
         rightLimit = i;
      }
      if ( rightLimit > maxFloodX )
         maxFloodX = rightLimit;
      if ( y > 0 )
      {
         for ( int i = leftLimit; i <= rightLimit; i++ )
         {
            int newY = y - 1;
            if ( newY < subPixTopY )
               addUpperPixels();
            pixIdx = newY * wd + i;
            if ( (pixels[pixIdx] & 0xFFFFFF) == curRGB )
               floodFill( i, newY, wd, ht, curRGB, newRGB );
         }
      }
      if ( y < ht-1 )
      {
         for ( int i = leftLimit; i <= rightLimit; i++ )
         {
            int newY = y + 1;
            if ( newY == (subPixTopY + subPixHt) )
               addLowerPixels();
            pixIdx = newY * wd + i;
            if ( (pixels[pixIdx] & 0xFFFFFF) == curRGB )
               floodFill( i, newY, wd, ht, curRGB, newRGB );
         }
      }

   } // end floodFill


   // print pixels in TG-coordinate-based rectangle,
   // i.e., x and y origins are at center of the graphicsImage.
   // x=leftmostEdge, y=bottomEdge
   private void printPixels( int x, int y, int wd, int ht )
   {
      int lin = yCenter - (y+(ht-1));
      System.out.print( "printPixels: sourceHeight="+sourceHeight+", YCenter=");
      System.out.println( yCenter+", lines "+lin+" .. "+(yCenter-y) );
      while ( lin <= yCenter-y )
      {
         System.out.print( x + "," + (yCenter - lin) + ":" );
         int col = x + xCenter;
         while ( col < x+xCenter+wd )
         {
            System.out.print( " " );
            int pixel = pixels[lin*sourceWidth + col];
            pixel &= 0xFFFFFF;
            System.out.print( (pixel >>> 16) + ".");
            pixel &= 0xFFFF;
            System.out.print( (pixel >>> 8) + ".");
            pixel &= 0xFF;
            System.out.print( pixel );
            col++;
         }
         System.out.println("");
         lin++;
      }
   
   } // end printPixels()


   // get color under my point and flood fill it and all its
   // neighbors, and their neighbors, etc... that are the
   // same color, with this operation's color
   //
   // *NOTE* the point where the fill operation is to start
   //        may not be within current bounds of the graphics
   //        Image.  in this case, the operation can not be
   //        performed.
   //        
   public Rectangle doIt( Image graphicsImage )
   {
      sourceImage = graphicsImage;
      sourceWidth = graphicsImage.getWidth( this );
      if ( sourceWidth == -1 )
         return null;
      int imageX = point.imageX( sourceWidth );
      if ( imageX < 0 || imageX >= sourceWidth )
         return null;
      xCenter = sourceWidth / 2;
      sourceHeight = graphicsImage.getHeight( this );
      if ( sourceHeight == -1 )
         return null;
      int imageY = point.imageY( sourceHeight );
      if ( imageY < 0 || imageY >= sourceHeight )
         return null;
      pixels = new int[sourceWidth * sourceHeight];
      yCenter = sourceHeight / 2;
      subPixLeftX = imageX - EXPAND_SIZE;
      if ( subPixLeftX < 0 )
         subPixLeftX = 0;
      subPixTopY = imageY - EXPAND_SIZE;
      if ( subPixTopY < 0 )
         subPixTopY = 0;
      subPixHt = EXPAND_SIZE * 2;
      if ( subPixHt > sourceHeight )
         subPixHt = sourceHeight;
      subPixWd = EXPAND_SIZE * 2;
      if ( subPixWd > sourceWidth )
         subPixWd = sourceWidth;
      if ( ! getPixels(subPixLeftX, subPixTopY, subPixWd, subPixHt) )
         return null;
      int curRGB = pixels[ imageY * sourceWidth + imageX ];
      curRGB &= 0xFFFFFF;
      int newRGB = color.getRGB();
      newRGB &= 0xFFFFFF;
      if ( curRGB == newRGB )
         return null;
      maxFloodX = minFloodX = imageX;
      maxFloodY = minFloodY = imageY;
      floodFill( imageX, imageY, sourceWidth, sourceHeight, curRGB, newRGB );
      //printPixels( -2, -2, 10, 20 );
      // create an ImageProducer, based on the new pixel data
      int pixelOffset = (sourceWidth * minFloodY) + minFloodX;
      int floodWidth = (maxFloodX+1) - minFloodX;
      int floodHeight = (maxFloodY+1) - minFloodY;
      MemoryImageSource mis = new MemoryImageSource( floodWidth, floodHeight,
                                                     pixels, pixelOffset, sourceWidth
                                                   );
      Image newImage = Toolkit.getDefaultToolkit().createImage( mis );
      drawImage( newImage, minFloodX, minFloodY );
      return new Rectangle( minFloodX, minFloodY, floodWidth, floodHeight );

   } // end doIt()


   private synchronized void drawImage( Image newImg, int x, int y )
   {
      //System.out.println("TGFillOp.drawImage: got here!" );
      Graphics g = sourceImage.getGraphics();
      waitingForImage = true;
      boolean retVal = g.drawImage( newImg, x, y, this );
      //System.out.println("TGFillOp.drawImage: retVal=" + retVal );
      if ( retVal )
         while ( waitingForImage )
         {
            try  { wait(); }
            catch (InterruptedException ie ) { }
            //System.out.println("TGFillOp.drawImage: wait() returned" );
         }
      waitingForImage = false;
      pixels = null;
      g.dispose();

   } // end drawImage()


   public Color getColor()
   { return color; }


   private boolean getPixels( int x, int y, int width, int height )
   {
      //System.out.print("getPixels: x="+x+", y="+y);
      //System.out.println(", wd="+width+", ht="+height);
      String me = "TGFillOp.getPixels: ";
      int pixelOffset = (sourceWidth * y) + x;
      PixelGrabber pg = new PixelGrabber( sourceImage, x, y, width, height,
                                          pixels, pixelOffset, sourceWidth
                                        );
      try { pg.grabPixels(); }
      catch (InterruptedException e)
      {
         System.err.println( me + "grabPixels interrupted" );
         return false;
      }
      if ( (pg.getStatus() & ImageObserver.ABORT) != 0 )
      {
         System.err.println( me + "grabPixels ImageObserver.ABORT" );
         return false;
      }
      return true;

   } // end getPixels()


   /*
    * This method is called when information about an image which was
    * previously requested using an asynchronous interface becomes available.
    *
    * This method should return true if further updates are needed or false
    * if the required information has been acquired. The image which was being
    * tracked is passed in using the img argument. Various constants are
    * combined to form the infoflags argument which indicates what information
    * about the image is now available. The interpretation of the x, y, width,
    * and height arguments depends on the contents of the infoflags argument. 
    */
   public synchronized boolean imageUpdate(Image img, int flags, int x, int y, int wd, int ht)
   {
      //System.out.print("TGFillOp.imageUpdate: flags=" );
      //if ( (flags & ImageObserver.ABORT) != 0 )
      //   System.out.print(" ABORT" );
      //if ( (flags & ImageObserver.ALLBITS) != 0 )
      //   System.out.print(" ALLBITS" );
      //if ( (flags & ImageObserver.ERROR) != 0 )
      //   System.out.print(" ERROR" );
      //if ( (flags & ImageObserver.FRAMEBITS) != 0 )
      //   System.out.print(" FRAMEBITS" );
      //if ( (flags & ImageObserver.HEIGHT) != 0 )
      //   System.out.print(" HEIGHT" );
      //if ( (flags & ImageObserver.PROPERTIES) != 0 )
      //   System.out.print(" PROPERTIES" );
      //if ( (flags & ImageObserver.SOMEBITS) != 0 )
      //   System.out.print(" SOMEBITS" );
      //if ( (flags & ImageObserver.WIDTH) != 0 )
      //   System.out.print(" WIDTH" );
      //System.out.print("\n                      x=" + x + ", y=" + y );
      //System.out.println( ", width=" + wd + ", height=" + ht );
      if ( ! waitingForImage )
         return false;
      if ( (flags & (ImageObserver.ABORT | ImageObserver.ALLBITS | ImageObserver.ERROR)) != 0 )
      {
         waitingForImage = false;
         notifyAll();
         return false;
      }
      return true;


   } // end imageUpdate()


} // end class TGFillOp