/* Copyright (C) 2000  Free Software Foundation

   This file is part of libgcj.

This software is copyrighted work licensed under the terms of the
Libgcj License.  Please consult the file "LIBGCJ_LICENSE" for
details.  */

package java.awt.image;

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

import gnu.gcj.awt.ComponentDataBlitOp;

/**
 * A buffered image always starts at coordinates (0, 0).
 *
 * The buffered image is not subdivided into multiple tiles. Instead,
 * the image consists of one large tile (0,0) with the width and
 * height of the image. This tile is always considered to be checked
 * out.
 * 
 * @author Rolf W. Rasmussen <rolfwr@ii.uib.no>
 */
public class BufferedImage extends java.awt.Image
    //implements java.awt.image.WritableRenderedImage
{
  public static final int TYPE_CUSTOM         =  0,
                          TYPE_INT_RGB        =  1,
                          TYPE_INT_ARGB       =  2,
                          TYPE_INT_ARGB_PRE   =  3,
                          TYPE_INT_BGR        =  4,
                          TYPE_3BYTE_BGR      =  5,
                          TYPE_4BYTE_ABGR     =  6,
                          TYPE_4BYTE_ABGR_PRE =  7,
                          TYPE_USHORT_565_RGB =  8,
                          TYPE_USHORT_555_RGB =  9,
                          TYPE_BYTE_GRAY      = 10,
                          TYPE_USHORT_GRAY    = 11,
                          TYPE_BYTE_BINARY    = 12,
                          TYPE_BYTE_INDEXED   = 13;
  
  final static int[] bits3 = { 8, 8, 8 };
  final static int[] bits4 = { 8, 8, 8 };
  final static int[] bits1byte = { 8 };
  final static int[] bits1ushort = { 16 };
  
  final static int[] masks_int = { 0x00ff0000,
				   0x0000ff00,
				   0x000000ff,
				   DataBuffer.TYPE_INT };
  final static int[] masks_565 = { 0xf800,
				   0x07e0,
				   0x001f,
				   DataBuffer.TYPE_USHORT};
  final static int[] masks_555 = { 0x7c00,
				   0x03e0,
				   0x001f,
				   DataBuffer.TYPE_USHORT};
  
  public BufferedImage(int w, int h, int type)
  {
    ColorModel cm = null;
    
    boolean alpha = false;
    boolean premultiplied = false;
    switch (type)
      {
      case TYPE_4BYTE_ABGR_PRE:
      case TYPE_INT_ARGB_PRE:
	premultiplied = true;
	// fall through
      case TYPE_INT_ARGB:
      case TYPE_4BYTE_ABGR:
	alpha = true;
      }
	
    ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
    switch (type)
      {
      case TYPE_INT_RGB:
      case TYPE_INT_ARGB:
      case TYPE_INT_ARGB_PRE:
      case TYPE_USHORT_565_RGB:
      case TYPE_USHORT_555_RGB:
	int[] masks = null;
	switch (type)
	  {
	  case TYPE_INT_RGB:
	  case TYPE_INT_ARGB:
	  case TYPE_INT_ARGB_PRE:
	    masks = masks_int;
	    break;
	  case TYPE_USHORT_565_RGB:
	    masks = masks_565;
	    break;
	  case TYPE_USHORT_555_RGB:
	    masks = masks_555;
	    break;
	  }
	
	cm = new DirectColorModel(cs,
				  32, // 32 bits in an int
				  masks[0], // r
				  masks[1], // g
				  masks[2], // b
				  alpha ? 0xff000000 : 0,
				  premultiplied,
				  masks[3] // data type
				  );
	break;
	
      case TYPE_INT_BGR:
	String msg =
	  "FIXME: Programmer is confused. Why (and how) does a " +
	  "TYPE_INT_BGR image use ComponentColorModel to store " +
	  "8-bit values? Is data type TYPE_INT or TYPE_BYTE. What " +
	  "is the difference between TYPE_INT_BGR and TYPE_3BYTE_BGR?";
	throw new UnsupportedOperationException(msg);
	
      case TYPE_3BYTE_BGR:
      case TYPE_4BYTE_ABGR:
      case TYPE_4BYTE_ABGR_PRE:
      case TYPE_BYTE_GRAY:
      case TYPE_USHORT_GRAY:
	int[] bits = null;
	int dataType = DataBuffer.TYPE_BYTE;
	switch (type) {
	case TYPE_3BYTE_BGR:
	  bits = bits3;
	  break;
	case TYPE_4BYTE_ABGR:
	case TYPE_4BYTE_ABGR_PRE:
	  bits = bits4;
	  break;
	case TYPE_BYTE_GRAY:
	  bits = bits1byte;
	  break;
	case TYPE_USHORT_GRAY:
	  bits = bits1ushort;
	  dataType = DataBuffer.TYPE_USHORT;
	  break;
	}
	cm = new ComponentColorModel(cs, bits, alpha, premultiplied,
				     alpha ?
				     Transparency.TRANSLUCENT:
				     Transparency.OPAQUE,
				     dataType);
	break;
      case TYPE_BYTE_BINARY:
	byte[] vals = { 0, (byte) 0xff };
	cm = new IndexColorModel(8, 2, vals, vals, vals);
	break;
      case TYPE_BYTE_INDEXED:
	String msg2 = "type not implemented yet";
	throw new UnsupportedOperationException(msg2);
	// FIXME: build color-cube and create color model
      }
    
    init(cm,
	 cm.createCompatibleWritableRaster(w, h),
	 premultiplied,
	 null, // no properties
	 type
	 );
  }

  public BufferedImage(int w, int h, int type,
		       IndexColorModel indexcolormodel)
  {
    if ((type != TYPE_BYTE_BINARY) && (type != TYPE_BYTE_INDEXED))
      throw new IllegalArgumentException("type must be binary or indexed");

    init(indexcolormodel,
	 indexcolormodel.createCompatibleWritableRaster(w, h),
	 false, // not premultiplied (guess)
	 null, // no properties
	 type);
  }

  public BufferedImage(ColorModel colormodel, 
		       WritableRaster writableraster,
		       boolean premultiplied,
		       Hashtable properties)
  {
    init(colormodel, writableraster, premultiplied, properties,
	 TYPE_CUSTOM);
    // TODO: perhaps try to identify type?
  }
 
  WritableRaster raster;
  ColorModel colorModel;
  Hashtable properties;
  boolean isPremultiplied;
  int type;
  
  private void init(ColorModel cm,
		    WritableRaster writableraster,
		    boolean premultiplied,
		    Hashtable properties,
		    int type)
  {
    raster = writableraster;
    colorModel = cm;
    this.properties = properties;
    isPremultiplied = premultiplied;
    this.type = type;
  }
    
  //public void addTileObserver(TileObserver tileobserver) {}
  
  public void coerceData(boolean premultiplied)
  {
    colorModel = colorModel.coerceData(raster, premultiplied);
  }

  public WritableRaster copyData(WritableRaster dest)
  {
    if (dest == null)
      dest = raster.createCompatibleWritableRaster();

    int x = dest.getMinX();
    int y = dest.getMinY();
    int w = dest.getWidth();
    int h = dest.getHeight();
    
    // create a src child that has the right bounds...
    WritableRaster src =
      raster.createWritableChild(x, y, w, h, x, y,
				 null  // same bands
				 );
    
    // Refer to ComponentDataBlitOp for optimized data blitting:
    ComponentDataBlitOp.INSTANCE.filter(src, dest);
    return dest;
  }

  public Graphics2D createGraphics()
  {
    throw new UnsupportedOperationException("not implemented");
    // will require a lot of effort to implement
  }

  public void flush() {
  }
  
  public WritableRaster getAlphaRaster()
  {
    return colorModel.getAlphaRaster(raster);
  }
  
  public ColorModel getColorModel()
  {
    return colorModel;
  }
  
  public Raster getData()
  {
    return copyData(null);
    /* TODO: this might be optimized by returning the same
       raster (not writable) as long as image data doesn't change. */
  }

  public Raster getData(Rectangle rectangle)
  {
    WritableRaster dest =
      raster.createCompatibleWritableRaster(rectangle);
    return copyData(dest);
  }
  
  public Graphics getGraphics()
  {
    return createGraphics();
  }

  public int getHeight()
  {
    return raster.getHeight();
  }
  
  public int getHeight(ImageObserver imageobserver)
  {
    return getHeight();
  }
    
  public int getMinTileX()
  {
    return 0;
  }
  
  public int getMinTileY()
  {
    return 0;
  }

  public int getMinX()
  {
    return 0; 
  }

  public int getMinY() 
  {
    return 0;
  }
  
  public int getNumXTiles()
  {
    return 1;
  }

  public int getNumYTiles()
  {
	return 1;
  }

  public Object getProperty(String string)
  {
    if (properties == null)
      return null;
    return properties.get(string);
  }

  public Object getProperty(String string, ImageObserver imageobserver)
  {
    return getProperty(string);
  }

  
  public String[] getPropertyNames()
  {
    // FIXME: implement
    return null;
  }

  public int getRGB(int x, int y)
  {
    Object rgbElem = raster.getDataElements(x, y,
					    null // create as needed
					    );
    return colorModel.getRGB(rgbElem);
  }
    
  public int[] getRGB(int startX, int startY, int w, int h,
		      int[] rgbArray,
		      int offset, int scanlineStride)
  {
    if (rgbArray == null)
    {
      /*
	000000000000000000
	00000[#######-----   [ = start
	-----########-----   ] = end
	-----#######]00000
	000000000000000000  */
      int size = (h-1)*scanlineStride + w;
      rgbArray = new int[size];
    }
	
    int endX = startX + w;
    int endY = startY + h;
    
    /* *TODO*:
       Opportunity for optimization by examining color models...
       
       Perhaps wrap the rgbArray up in a WritableRaster with packed
       sRGB color model and perform optimized rendering into the
       array. */

    Object rgbElem = null;
    for (int y=startY; y<endY; y++)
      {
	int xoffset = offset;
	for (int x=startX; x<endX; x++)
	  {
	    int rgb;
	    rgbElem = raster.getDataElements(x, y, rgbElem);
	    rgb = colorModel.getRGB(rgbElem);
	    rgbArray[xoffset++] = rgb;
	  }
	offset += scanlineStride;
      }
    return rgbArray;
  }

  public WritableRaster getRaster()
  {
    return raster;
  }
  
  public SampleModel getSampleModel()
  {
    return raster.getSampleModel();
  }
    
  public ImageProducer getSource()
  {
    throw new UnsupportedOperationException("not implemented");
  }
  
  public Vector getSources()
  {
    return null;
  }
  
  public BufferedImage getSubimage(int x, int y, int w, int h)
  {
    WritableRaster subRaster = 
      getRaster().createWritableChild(x, y, w, h, 0, 0, null);
    
    return new BufferedImage(getColorModel(),
			     subRaster,
			     isPremultiplied,
			     properties);
  }

  public Raster getTile(int tileX, int tileY)
  {
    return getWritableTile(tileX, tileY);
  }
    
  public int getTileGridXOffset()
  {
    return 0; // according to javadocs
  }

  public int getTileGridYOffset()
  {
    return 0; // according to javadocs
  }

  public int getTileHeight()
  {
    return getHeight(); // image is one big tile
  }

  public int getTileWidth()
  {
    return getWidth(); // image is one big tile
  }

  public int getType()
  {
    return type;
  }

  public int getWidth()
  {
    return raster.getWidth();
  }

  public int getWidth(ImageObserver imageobserver)
  {
    return getWidth();
  }

  public WritableRaster getWritableTile(int tileX, int tileY)
  {
    isTileWritable(tileX, tileY);  // for exception
    return raster;
  }

  private static final Point[] tileIndices = { new Point() };
    
  public Point[] getWritableTileIndices()
  {
    return tileIndices;
  }

  public boolean hasTileWriters()
  {
    return true;
  }
  
  public boolean isAlphaPremultiplied()
  {
    return isPremultiplied;
  }

  public boolean isTileWritable(int tileX, int tileY)
  {
    if ((tileX != 0) || (tileY != 0))
      throw new ArrayIndexOutOfBoundsException("only tile is (0,0)");
    return true;
  }

  public void releaseWritableTile(int tileX, int tileY)
  {
    isTileWritable(tileX, tileY);  // for exception
  }

  //public void removeTileObserver(TileObserver tileobserver) {}

  public void setData(Raster src)
  {
    int x = src.getMinX();
    int y = src.getMinY();
    int w = src.getWidth();
    int h = src.getHeight();
    
    // create a dest child that has the right bounds...
    WritableRaster dest =
      raster.createWritableChild(x, y, w, h, x, y,
				 null  // same bands
				 );
    
    // Refer to ComponentDataBlitOp for optimized data blitting:
    ComponentDataBlitOp.INSTANCE.filter(src, dest);
  }

  public void setRGB(int x, int y, int argb)
  {
    Object rgbElem = colorModel.getDataElements(argb, null);
    raster.setDataElements(x, y, rgbElem);
  }
  
  public void setRGB(int startX, int startY, int w, int h,
		     int[] argbArray, int offset, int scanlineStride)
  {
    int endX = startX + w;
    int endY = startY + h;
    
    Object rgbElem = null;
    for (int y=startY; y<endY; y++)
      {
	int xoffset = offset;
	for (int x=startX; x<endX; x++)
	  {
	    int argb = argbArray[xoffset++];
	    rgbElem = colorModel.getDataElements(argb, rgbElem);
	    raster.setDataElements(x, y, rgbElem);
	  }
	offset += scanlineStride;    
      }
  }
    
  public String toString()
  {
    // FIXME: implement:
    return super.toString();
  }
}