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 javax.imageio.*;
import javax.imageio.spi.*;
import javax.imageio.stream.*;
import javax.imageio.metadata.*;

public class BMPImageReader extends ImageReader implements BMPConstants{

  private boolean gotHeader = false;

  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   bitsPerPixel = 24;            // Bits per Pixel, default = RGB 
  private int   compression = 0;              // 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;
  private IndexColorModel icm=null;

  private int   redMask=0;                    // 16bit, 32bit compression
  private int   greenMask=0;
  private int   blueMask=0;

  protected BMPImageReader(ImageReaderSpi originatingProvider){
    super(originatingProvider);
  }

  public BufferedImage read(int imageIndex, ImageReadParam param)throws IOException{
    checkIndex(imageIndex);
    return read((ImageInputStream)getInput());
  }

  public int getHeight(int imageIndex)throws IOException{
    checkIndex(imageIndex);
    readHeader((ImageInputStream)getInput());
    return height;
  }

  public int getWidth(int imageIndex)throws IOException{
    checkIndex(imageIndex);
    readHeader((ImageInputStream)getInput());
    return width;
  }

  public Iterator getImageTypes(int imageIndex)throws IOException{
//System.err.println("BMPImageReader.getImageTypes "+imageIndex);
    checkIndex(imageIndex);
    readHeader((ImageInputStream)getInput());

    java.util.List l = new ArrayList();
//    l.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_BYTE_BINARY));
//    l.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_BYTE_INDEXED));
//    l.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_USHORT_555_RGB));
//    l.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR));
//    l.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB));

    return l.iterator();
  }

  public int getNumImages(boolean allowSearch)throws IOException{
    return 1;
  }

  public IIOMetadata getImageMetadata(int imageIndex)throws IOException{
    BMPMetadata md=new BMPMetadata();
    md.setWidth(getWidth(imageIndex));
    md.setHeight(getHeight(imageIndex));
//    private int   planes = 1;                   // BitPlanes on Target Device
    md.setBitsPerPixel(bitsPerPixel);
    md.setCompression(compression);
    md.setImageSize(sizeImage);
    md.setXPixelsPerMeter(xPelsPerMeter);
    md.setYPixelsPerMeter(yPelsPerMeter);
    md.setColorsUsed(clrUsed);
    md.setIndexColorModel(icm);
    md.setColorsImportant(clrImportant);
    md.setRedMask(redMask);md.setGreenMask(greenMask);md.setBlueMask(blueMask);
    return md;
  }

  public IIOMetadata getStreamMetadata() throws IOException{
    return null;
  }

  private void checkIndex(int imageIndex) {
    if (imageIndex != 0) {
      throw new IndexOutOfBoundsException(getClass().getName()+".checkIndex:\n\tBad index in bmp image reader.");
    }
  }

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

  private void readHeader(ImageInputStream in)throws IOException{
    if (gotHeader) { return; }
    gotHeader = true;

    in.setByteOrder(ByteOrder.LITTLE_ENDIAN);

    byte[] bm = new byte[2];
    in.readFully(bm);
    if((bm[0]!=(byte)'B')||(bm[1]!=(byte)'M')){
      throw new IOException("Invalid BMP 3.0 File.");
    }
    size=in.readInt();
    in.readUnsignedShort();
    in.readUnsignedShort();
    offset=in.readInt();

    headerSize = in.readInt();
    if(headerSize == 12){
      width = in.readUnsignedShort();
      height = in.readUnsignedShort();
      planes = in.readUnsignedShort();
      bitsPerPixel = in.readUnsignedShort();

      compression = 0;                        // BI_RGB
      xPelsPerMeter = 2953;                   // default 75 dpi
      yPelsPerMeter = 2953;
      clrUsed = 0;
      clrImportant = 0;
    }else{
      width = in.readInt();
      height = in.readInt();
      planes = in.readUnsignedShort();        // always = 1; device independent
      bitsPerPixel = in.readUnsignedShort();  // 1,4,8,16,24,32

      compression = in.readInt();             // BI_RGB, BI_RLE8, BI_RLE4, BI_BITFIELDS
      sizeImage = in.readInt();
      xPelsPerMeter = in.readInt();
      yPelsPerMeter = in.readInt();
      clrUsed = in.readInt();
      clrImportant = in.readInt();
    }
    if(sizeImage == 0){
      sizeImage = (((width*bitsPerPixel+31)>>5)<<2)*height;
    }
    int coloursUsed=getColorsInPalette();
    if(coloursUsed > 0){
//      colortable=new int[coloursUsed];
      byte[] redCols  =new byte[coloursUsed];
      byte[] greenCols=new byte[coloursUsed];
      byte[] blueCols =new byte[coloursUsed];
      for (int i=0; i < coloursUsed; i++) {
        blueCols[i] =(byte)in.read();    	      // blue
        greenCols[i]=(byte)in.read();    	      // green
        redCols[i]  =(byte)in.read();    	      // red
        in.read(); 			                        // reserved
//        colortable[i]=in.readInt();
      }
      icm=new IndexColorModel(bitsPerPixel,coloursUsed,redCols,greenCols,blueCols);
    }
    if(compression==BI_BITFIELDS){
      redMask=in.readInt();
      greenMask=in.readInt();
      blueMask=in.readInt();
    }else if(bitsPerPixel==16){
      redMask  =0x00007C00;
      greenMask=0x000003E0;
      blueMask =0x0000001F;
    }else if(bitsPerPixel==32){
      redMask  =0x00FF0000;
      greenMask=0x0000FF00;
      blueMask =0x000000FF;
    }
    int skip=offset-(14+headerSize+coloursUsed*4+((compression==BI_BITFIELDS)?12:0));
    if(skip > 0){ in.skipBytes(skip); }
    if((compression==BI_RLE4)||(compression==BI_RLE8)){
      throw new IOException(getClass().getName()+".readHeader:\n\tCannot read Run Length Encoded BMP Files.");
    }
  }
///*
  private BufferedImage read(ImageInputStream in)throws IOException{
    readHeader(in);
    switch(bitsPerPixel){
    case  1:  return unpack01(in, icm);
    case  4:  return unpack04(in, icm);
    case  8:  return unpack08(in, icm);
    case 16:  return unpack16(in);
    case 24:  return unpack24(in);
    case 32:  return unpack32(in);
    default:  throw new IOException(getClass().getName()+".read:\n    Cannot read BMP with depth="+(1<<bitsPerPixel));
    }
  }

  void copyImage(byte[] imgBuf,int bytesPerLine,ImageInputStream in)throws IOException{
    int padding=(((width*bitsPerPixel+31)>>5)<<2)-bytesPerLine;
    for(int pos=(height-1)*bytesPerLine;pos>=0;pos-=bytesPerLine){
      in.read(imgBuf,pos,bytesPerLine);
      in.skipBytes(padding);
    }
  }

  int getColorValue(int pixel,int mask){
    int color=pixel&mask;
    while((mask&0x80000000)==0){
      color<<=1; mask<<=1;
    }
    return (color>>24)&0x000000FF;
  }

  BufferedImage unpack32(ImageInputStream in)throws IOException{
    BufferedImage   image  =new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
    DataBufferInt   buffer =(DataBufferInt)image.getRaster().getDataBuffer();
    int[]           imgBuf =buffer.getData();

    if((redMask==0x00FF0000)&&(greenMask==0x0000FF00)&&(blueMask==0x000000FF)){
      for(int pos=(height-1)*width;pos>=0;pos-=width){
        in.readFully(imgBuf,pos,width);
      }
    }else{
      int   color, r, g, b;
      for(int pos=(height-1)*width;pos>=0;pos-=width){
        for(int x=0;x<width;x++){
          color=in.readInt();
          r=getColorValue(color,redMask);
          g=getColorValue(color,greenMask);
          b=getColorValue(color,blueMask);
          imgBuf[pos+x]=(r<<16)|(g<<8)|b;
        }
      }
    }
    return image;
  }

  BufferedImage unpack24(ImageInputStream in)throws IOException{
    BufferedImage image=new BufferedImage(width,height,BufferedImage.TYPE_3BYTE_BGR);
    WritableRaster  raster=image.getRaster();
    DataBufferByte  buffer=(DataBufferByte)raster.getDataBuffer();
    copyImage(buffer.getData(),width*3,in);
    return image;
  }

  BufferedImage unpack16(ImageInputStream in)throws IOException{
    BufferedImage    image=new BufferedImage(width,height,BufferedImage.TYPE_USHORT_555_RGB);
    DataBufferUShort buffer=(DataBufferUShort)image.getRaster().getDataBuffer();
    short[]          imgBuf=buffer.getData();

    int padding=(((width*bitsPerPixel+31)>>5)<<2)-(width<<1);

    if((redMask==0x00007C00)&&(greenMask==0x000003E0)&&(blueMask==0x0000001F)){
      for(int pos=(height-1)*width;pos>=0;pos-=width){
        in.readFully(imgBuf,pos,width);
        in.skipBytes(padding);
      }
    }else{
      int     color, r, g, b;
      for(int pos=(height-1)*width;pos>=0;pos-=width){
        for(int x=0;x<width;x++){
          color=in.readShort();
          r=getColorValue(color,redMask)  &0x001F;
          g=getColorValue(color,greenMask)&0x001F;
          b=getColorValue(color,blueMask) &0x001F;
          imgBuf[pos+x]=(short)((r<<10)|(g<<5)|b);
        }
        in.skipBytes(padding);
      }
    }
    return image;
  }

  BufferedImage unpack08(ImageInputStream in, IndexColorModel icm)throws IOException{
    BufferedImage   image=new BufferedImage(width,height,BufferedImage.TYPE_BYTE_INDEXED,icm);
    WritableRaster  raster=image.getRaster();
    DataBufferByte  buffer=(DataBufferByte)raster.getDataBuffer();
    copyImage(buffer.getData(),width,in);
    return image;
  }

  BufferedImage unpack04(ImageInputStream in, IndexColorModel icm)throws IOException{
    BufferedImage   image=new BufferedImage(width,height,BufferedImage.TYPE_BYTE_BINARY,icm);
    WritableRaster  raster=image.getRaster();
    DataBufferByte  buffer=(DataBufferByte)raster.getDataBuffer();
    copyImage(buffer.getData(),(width+1)>>1,in);
    return image;
  }

  BufferedImage unpack01(ImageInputStream in, IndexColorModel icm)throws IOException{
    BufferedImage   image=new BufferedImage(width,height,BufferedImage.TYPE_BYTE_BINARY,icm);
    WritableRaster  raster=image.getRaster();
    DataBufferByte  buffer=(DataBufferByte)raster.getDataBuffer();
    copyImage(buffer.getData(),(width+7)>>3,in);
    return image;
  }

//*/
/*  
  private BufferedImage read(ImageInputStream in)throws IOException{
    readHeader(in);
    int scanlineSize = ((width*bitsPerPixel+31)/32)*4;
    byte[] rawData = new byte[scanlineSize*height];
    in.readFully(rawData);
    if(rawData != null){
      switch(bitsPerPixel){
      case  1:  return unpack01(rawData, colortable);
      case  4:  return unpack04(rawData, colortable);
      case  8:  return unpack08(rawData, colortable);
      case 24:  return unpack24(rawData);
      default:  throw new IOException(getClass().getName()+".read:\n    Cannot read BMP with depth="+(1<<bitsPerPixel));
      }
    }
    return null;
  }

  BufferedImage unpack24(byte[] data){
    BufferedImage image=new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
    int           padding      = (((width*3)&3)!=0)?4-((width*3)&3):0;
    int x,y,k=0,len=data.length-3;
    for(y=height-1;y>=0;y--){
      for(x=0;(x<width)&&(k<len);x++){
        image.setRGB(
            x,y,0xFF000000 | 
                (((int)(data[k++])) & 0xFF) |            // blue
                (((int)(data[k++])) & 0xFF) << 8 |       // green
                (((int)(data[k++])) & 0xFF) << 16        // red
        );
      }
      k+=padding;
    }
    return image;
  }

  BufferedImage unpack08(byte[] data, int[] colortable){
    BufferedImage image=new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
    int           padding      = ((width&3)!=0)?4-(width&3):0;
    int x,y,k=0,len=data.length;
    for(y=height-1;y>=0;y--){
      for(x=0;(x<width)&&(k<len);x++){
        image.setRGB(x,y,colortable[data[k++]&0x00FF]);
      }
      k+=padding;
    }
    return image;
  }

  BufferedImage unpack04(byte[] data, int[] colortable) {
    BufferedImage image=new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
    int           bytesPerLine = (width+1)>>1;
    int           padding      = ((bytesPerLine&3)!=0)?4-(bytesPerLine&3):0;
    int x,xx,y,k=0,len=data.length;byte b;
    for(y=height-1;y>=0;y--){
      xx=0;
      for(x=0;(x<bytesPerLine)&&(k<len);x++){
        b=data[k++];
        if(xx<width){                                            // last byte in line may have padding bits
          image.setRGB(xx++,y,colortable[(b>>4)&0x0F]);
          if(xx<width){                                          // last byte in line may have padding bits
            image.setRGB(xx++,y,colortable[b&0x0F]);
          }                                                      
        }                                                        // else ignore padding bytes
      }
      k+=padding;
    }
    return image;
  }

  BufferedImage unpack01(byte[] data, int[] colortable) {
    BufferedImage image        = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
    int           bytesPerLine = (width+7)>>3;
    int           padding      = ((bytesPerLine&3)!=0)?4-(bytesPerLine&3):0;
    int x,xx,y,k=0,bit,len=data.length;byte b;

    for(y=height-1;y>=0;y--){
      xx=0;
      for(x=0;(x<bytesPerLine)&&(k<len);x++){
        b=data[k++];
        for(bit=0;(bit<8)&&(xx<width);bit++){
          image.setRGB(xx++,y,colortable[(b>>(7-bit))&0x01]);
        }
      }
      k+=padding;
    }
    return image;
  }
*/
}