package uk.co.mmscomputing.imageio.bmp;

import java.io.IOException;
import java.nio.ByteOrder;
import java.util.Iterator;
import java.util.ArrayList;

import java.awt.image.*;
import java.awt.color.*;
import javax.imageio.*;
import javax.imageio.spi.*;
import javax.imageio.stream.*;
import javax.imageio.metadata.*;

public class BMPImageWriter extends ImageWriter implements BMPConstants{

  private int	size=0;
  private int	offset=0;
 
  private int   headerSize = 40;              /* InfoHeader Offset*/
  private int   width;                        /* Width */
  private int   height;                       /* Height */
  private int   planes = 1;                   /* BitPlanes on Target Device */
  private int   bitCount = 24;                /* Bits per Pixel, default = RGB */
  private int   compression = BI_RGB;         /* Bitmap Compression */
  private int   sizeImage;                    /* Bitmap Image Size */
  private int   xPelsPerMeter = 2953;         /* Horiz Pixels Per Meter, default 75 dpi */
  private int   yPelsPerMeter = 2953;         /* Vert Pixels Per Meter, default 75 dpi */
  private int   clrUsed = 0;                  /* Number of ColorMap Entries */
  private int   clrImportant = 0;             /* Number of important colours */
        
  private int[] colortable=null;

  protected BMPImageWriter(ImageWriterSpi originatingProvider){
    super(originatingProvider);
  }

  public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param){
    return null;
  }

  public IIOMetadata convertStreamMetadata(IIOMetadata inData,ImageWriteParam param){
    return null;
  }

  public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType,ImageWriteParam param){
    return null;
  }

  public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param){
    return null;
  }

  public boolean canInsertImage(int imageIndex)throws IOException{
    super.canInsertImage(imageIndex);
    return (imageIndex==0);		//	can deal only with one image
  }

  int getColorsInPalette(){
    if(clrUsed!=0){                  // as in info header
      return clrUsed;
    }else if(bitCount<16){           // 2, 16, 256 colours
      return 1<<bitCount;
    }else{                           // 0 for 16,24,32 bits per pixel
      return 0;
    }
  }

  int getHeaderSize(){
    int size=14+40;                          // fileheader + infoheader
    size+=getColorsInPalette()*4;            // either colour table
    size+=(compression==BI_BITFIELDS)?12:0;  // or colour masks or none of them
    return size;
  }

  private void writeHeader(ImageOutputStream out,BufferedImage image,IIOMetadata md)throws IOException{
    width=image.getWidth();
    height=image.getHeight();

    if(md!=null){
      if(md instanceof BMPMetadata){
        BMPMetadata bmd=(BMPMetadata)md;
        xPelsPerMeter=bmd.getXPixelsPerMeter();
        yPelsPerMeter=bmd.getYPixelsPerMeter();
      }
    }

    offset=getHeaderSize();				      //	file header

    sizeImage=((((width*bitCount)+31)>>5)<<2)*height;
    size=offset+sizeImage;

    out.setByteOrder(ByteOrder.LITTLE_ENDIAN);
    out.writeByte('B');			//	file header
    out.writeByte('M');
    out.writeInt(size);     //  size of 
    out.writeShort(0);			//	reserved
    out.writeShort(0);			//	reserved
    out.writeInt(offset);

    out.writeInt(headerSize);		//	info header
    out.writeInt(width);
    out.writeInt(height);
    out.writeShort(planes);
    out.writeShort(bitCount);
    out.writeInt(compression);
    out.writeInt(sizeImage);
    out.writeInt(xPelsPerMeter);
    out.writeInt(yPelsPerMeter);
    out.writeInt(clrUsed);
    out.writeInt(clrImportant);
  }

  public void write(ImageOutputStream out,BufferedImage image,IIOMetadata md)throws IOException{
    writeHeader(out,image,md);

    int rawData[] = new int[width*height];
    PixelGrabber grabber = new PixelGrabber(image, 0, 0, width, height, rawData, 0, width);
    try{
      grabber.grabPixels();
    }catch(InterruptedException e){ 
      throw new IOException(getClass().getName()+".write : Couldn't grab pixels from image !");
    }

    ColorModel model = grabber.getColorModel();

    int x=0,y=0,i=0,j=0,k=0;
    int w=(((width*bitCount)+31)>>5)<<2;        		// dword align width
    int h=height-1;

    byte[] bitmap = new byte[w*height];
    for (y=0;y<height;y++) {
      i=(h-y)*width;                   
      j=y*w;                    
      for (x=0;x<width;x++) {
        k=i+x;
        bitmap[j++]=(byte)((model.getBlue(rawData[k]))&0xFF);
        bitmap[j++]=(byte)((model.getGreen(rawData[k]))&0xFF);
        bitmap[j++]=(byte)((model.getRed(rawData[k]))&0xFF);
      }
    }                        
    out.write(bitmap);            
  }

  void writeColorTable(ImageOutputStream out,IndexColorModel icm)throws IOException{
    byte[] reds=new byte[clrUsed],greens=new byte[clrUsed],blues=new byte[clrUsed];
    icm.getReds(reds);icm.getGreens(greens);icm.getBlues(blues);
    for(int i=0;i<reds.length;i++){
      out.write(blues[i]);out.write(greens[i]);out.write(reds[i]);out.write(0);
    }
  }

  void writeImage(ImageOutputStream out,byte[] buffer,int bytesPerLine)throws IOException{
    int padding=((((width*bitCount)+31)>>5)<<2)-bytesPerLine;
    for(int pos=(height-1)*bytesPerLine;pos>=0;pos-=bytesPerLine){
      out.write(buffer,pos,bytesPerLine);
      for(int p=0;p<padding;p++){out.write(0);}
    }
  }

  private void write01bit(ImageOutputStream out,BufferedImage image,IIOMetadata md,IndexColorModel icm)throws IOException{

    System.out.println("Save as 1 bit BMP");

    bitCount=1;			             //	Save as 1 bit BMP
    compression=BI_RGB;          // no colour masks
    clrUsed=icm.getMapSize();    // two colour table
    writeHeader(out,image,md);
    writeColorTable(out,icm);
    WritableRaster  raster=image.getRaster();
    DataBufferByte  buffer=(DataBufferByte)raster.getDataBuffer();
    writeImage(out,buffer.getData(),(width+7)>>3);
  }

  private void write04bit(ImageOutputStream out,BufferedImage image,IIOMetadata md,IndexColorModel icm)throws IOException{

    System.out.println("Save as 4 bit BMP");

    bitCount=4;			             //	Save as 4 bit BMP
    compression=BI_RGB;          // no colour masks
    clrUsed=icm.getMapSize();    // max 16 colour table
    writeHeader(out,image,md);
    writeColorTable(out,icm);
    WritableRaster  raster=image.getRaster();
    DataBufferByte  buffer=(DataBufferByte)raster.getDataBuffer();
    writeImage(out,buffer.getData(),(width+1)>>1);
  }

  private void write08bit(ImageOutputStream out,BufferedImage image,IIOMetadata md,IndexColorModel icm)throws IOException{

    System.out.println("Save as 8 bit BMP");

    bitCount=8;			             //	Save as 8 bit BMP
    compression=BI_RGB;          // no colour masks
    clrUsed=icm.getMapSize();    // max 256 colour table
    writeHeader(out,image,md);
    writeColorTable(out,icm);
    WritableRaster  raster=image.getRaster();
    DataBufferByte  buffer=(DataBufferByte)raster.getDataBuffer();
    writeImage(out,buffer.getData(),width);
  }

  private void write16bit(ImageOutputStream out,BufferedImage image,IIOMetadata md)throws IOException{

    System.out.println("Save as 8 bit BMP");

    bitCount=16;			        //	Save as 16 bit BMP
    compression=BI_BITFIELDS; //  colour masks
    clrUsed=0;                //  no colour table
    writeHeader(out,image,md);

    out.writeInt(0x00007C00); //  default colour masks
    out.writeInt(0x000003E0);
    out.writeInt(0x0000001F);

    WritableRaster   raster=image.getRaster();
    DataBufferUShort buffer=(DataBufferUShort)raster.getDataBuffer();
    short[]          imgBuf=buffer.getData();

    int padding=(((width*bitCount+31)>>5)<<2)-(width<<1);
    for(int pos=(height-1)*width;pos>=0;pos-=width){
      out.writeShorts(imgBuf,pos,width);
      if(padding!=0){out.write(0);out.write(0);}
    }
  }

  private void write24bit(ImageOutputStream out,BufferedImage image,IIOMetadata md)throws IOException{
    bitCount=24;			      //	Save as 24 bit BMP
    compression=BI_RGB;     //  no colour masks
    clrUsed=0;              //  no colour table
    writeHeader(out,image,md);
    WritableRaster  raster=image.getRaster();
    DataBufferByte  buffer=(DataBufferByte)raster.getDataBuffer();
    writeImage(out,buffer.getData(),width*3);
  }

  private void write32bit(ImageOutputStream out,BufferedImage image,IIOMetadata md)throws IOException{
    bitCount=32;			        //	Save as 32 bit BMP
    compression=BI_BITFIELDS; //  colour masks
    clrUsed=0;                //  no colour table
    writeHeader(out,image,md);
    out.writeInt(0x00FF0000); //  default colour masks
    out.writeInt(0x0000FF00);
    out.writeInt(0x000000FF);

    WritableRaster  raster=image.getRaster();
    DataBufferInt   buffer=(DataBufferInt)raster.getDataBuffer();
    int[]           imgBuf=buffer.getData();

    for(int pos=(height-1)*width;pos>=0;pos-=width){
      out.writeInts(imgBuf,pos,width);
    }
  }

  public void write(IIOMetadata streamMetadata,IIOImage img,ImageWriteParam param)throws IOException{
    ImageOutputStream out=(ImageOutputStream)getOutput();

    if(!(img.getRenderedImage() instanceof BufferedImage)){
      throw new IOException(getClass().getName()+"write:\n\tCan only write BufferedImage objects");
    }
    IIOMetadata   md      = img.getMetadata();
    BufferedImage image   = (BufferedImage)img.getRenderedImage();
    ColorModel    cm      = image.getColorModel();
    if(cm instanceof IndexColorModel){
      IndexColorModel icm =(IndexColorModel)cm;
      if((image.getType()==BufferedImage.TYPE_BYTE_BINARY)&&(cm.getPixelSize()==1)){
        write01bit(out,image,md,icm);
      }else if((image.getType()==BufferedImage.TYPE_BYTE_BINARY)&&(cm.getPixelSize()==4)){
        write04bit(out,image,md,icm);
      }else if((image.getType()==BufferedImage.TYPE_BYTE_INDEXED)&&(cm.getPixelSize()<=8)){
        write08bit(out,image,md,icm);
      }else{
        write(out,image,md);
      }
    }else{
      switch(image.getType()){
      case BufferedImage.TYPE_USHORT_555_RGB:  write16bit(out,image,md);break;
      case BufferedImage.TYPE_3BYTE_BGR:       write24bit(out,image,md);break;
      case BufferedImage.TYPE_INT_RGB:         write32bit(out,image,md);break;
      default:                                 write(out,image,md);break;
      }
    }
  }

  public ImageWriteParam getDefaultWriteParam(){
    return new ImageWriteParam(getLocale());
  }
}
