package com.profcon.tini.stikclik;


import java.io.DataOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.DataInputStream;

  /** Convert a camera bitmap into a BMP image. This class converts
   *  the raw rasters from the camera into a BMP image. Understanding
   *  it propbably requires a fairly detailed knowledge of both.
   *  See http://www.chipcenter.com/circuitcellar/august00/c0800bl1.htm
   *  for the camera's format.
   */

public
class BmpBuilder {

  /** Convert from rggb into bmp. */

private byte[] rggb, bmp;

  /** bmp[at] is the next byte of bmp to write. */

private int at;

private ByteArrayOutputStream baos = new ByteArrayOutputStream();

private DataOutputStream dos = new DataOutputStream(baos);

private
void flush() {
  byte[] buf = baos.toByteArray();
  baos.reset();
  System.arraycopy(buf, 0, bmp, at, buf.length);
  at += buf.length;
}


// Remember:  BMP files have data in the "wrong" (Intel-style) byte order.
private void writeSwappedInt(int i) throws Exception {

  dos.write(i);
  dos.write(i>>>8);
  dos.write(i>>>16);
  dos.write(i>>>24);
}

  /** Translate the raw rggb from the camera into BMP format. */

public
synchronized
void translate(byte[] rggb, byte[] bmp) throws Exception {
  this.rggb = rggb;
  this.bmp = bmp;
  at = 0;
  writeHeader();
  writePixels();
}



private
void writeHeader() throws Exception {

  dos.write(new byte[] { 0x42, 0x4d }); // "BM"

  //dos.writeInt(swapInt(BMP_TOTAL_SIZE));
  writeSwappedInt(BMP_TOTAL_SIZE);

  dos.writeShort(0);  		// Reserved
  dos.writeShort(0);  		// Reserved

  dos.writeInt(0x36000000); 	// Swapped int: pixel offset

  dos.writeInt(0x28000000); 	// Swapped int: info header size

  writeSwappedInt(BMP_WIDTH);	// Swapped int: width
  writeSwappedInt(BMP_HEIGHT);	// Swapped int: height

  dos.writeShort(0x0100);    	// Swapped short: 1 plane of pixels
  dos.writeShort(0x1800);    	// Swapped short: 24 bits/pixel

  dos.writeInt(0);    		// compression = 0

  writeSwappedInt(BMP_DATA_SIZE);	// Swapped int: image size

  writeSwappedInt(2000);    	// X pixels/meter
  writeSwappedInt(2000);    	// Y pixels/meter
  dos.writeInt(0);    		// Unused field
  dos.writeInt(0);    		// Unused field


  flush();

  // The pixel data follows:  Bottom row first, each line padded to
  // a 4-byte boundary.
}


private
void writePixels() throws Exception {
  flush();
  int pixelsAt = at;        // starting pos for BMP pixel data
  Diagnostics.startTimer("rgb transform");
  monochrome_transform();
  //balanceBmp(pixelsAt);
  Diagnostics.endTimer("rgb transform");
  flush();
}

  /* The G1-RR-BB-G2 values are Bayer pixels. The X values are BMP pixels.
     Note that the BMP image is smaller by one pixel in each dimension.
     This picture does not show how the Bayer pixels are physically layed out.

      G1 RR G1 RR G1 RR
        X  X  X  X  X  
      BB G2 BB G2 BB G2  
        X  X  X  X  X  
      G1 RR G1 RR G1 RR  
        X  X  X  X  X      
      BB G2 BB G2 BB G2  
        X  X  X  X  X      
      G1 RR G1 RR G1 RR
  */

//  Read from camera: A series of 2-row blocks consisting of:
//
//  (1) One row of red pixels values (0-255)
//  (2) One row of green1 pixels values (0-255)
//  (3) One row of green2 pixels values (0-255)
//  (4) One row of blue pixels values (0-255)
    

/** A property of the BMP format */
private final static int BMP_HEADER_LEN = 0x36;

private static final int
  RGGB_WIDTH = CameraController.RGGB_WIDTH,
  RGGB_HEIGHT = CameraController.RGGB_HEIGHT-1;

public static final int
  BMP_WIDTH = RGGB_WIDTH,  
  BMP_HEIGHT = RGGB_HEIGHT,

  // Row length is 3*BMP_WIDTH padded up to a multiple of 4
  BMP_ROW_LEN = ((3*BMP_WIDTH + 3) / 4) * 4,

  BMP_ROW_PADDING = BMP_ROW_LEN - (3*BMP_WIDTH),

  // Total file size of bitmap
  BMP_DATA_SIZE = BMP_HEIGHT*BMP_ROW_LEN,

  BMP_TOTAL_SIZE = BMP_DATA_SIZE + BMP_HEADER_LEN;


private static final int
  WIDTHBY2 = CameraController.RGGB_WIDTH/2,
  RGGB_BLOCK = WIDTHBY2*4;



  /** Transform a raw Bayer bitmap from the camera into raw,
   *  unfiltered BMP rows. Don't mess with this method unless
   *  you have a detailed understanding of both formats.
   */

// BROKEN
private
void orig_transform() {

  byte[] result = bmp, rggb = this.rggb;
  int at = this.at;
  for( int block=0; block<RGGB_HEIGHT/2; ++block ) {
    for( int row=0; row<2; ++row ) {          // 2 rows per RGGB block
      int r  = block*RGGB_BLOCK;
      int g1 = r+WIDTHBY2;
      int g2 = g1+WIDTHBY2;
      int b  = g2+WIDTHBY2;
      if( row==1 ) {
        if( block==RGGB_HEIGHT/2-1 )
          break;
        g1 += RGGB_BLOCK;
        r += RGGB_BLOCK;
      }
      for( int x=0; ; x+=2 ) {
        byte rggb_r  = rggb[r];
        result[at++] = rggb_r;
        int rggb_g2  = rggb[g2]&0xFF;
        result[at++] = (byte)(((rggb[g1++]&0xFF)+rggb_g2)/2);
        result[at++] = rggb[b++];
        
        if( x==162 )         /// magic number
          break;

        result[at++] = rggb_r; ++r;
        result[at++] = (byte)(((rggb[g1]&0xFF)+rggb_g2)/2); ++g2;
        result[at++] = rggb[b];        
      }
    }
  }
  this.at = at;
}

///////////////////////////////////////////////////////////////////////
private static byte scaleByte(byte b, double s) {
  int n = (int)b & 0xff;
  n *= s;
  if (n > 255)
    n=255;
  return (byte)n;
}

///////////////////////////////////////////////////////////////////////

private void balanceBmp(int startAt) {

  byte[] bmp = this.bmp;

  final int BMP_PIXELS = BMP_HEIGHT*BMP_WIDTH;

  int avgRed=0, avgGreen=0, avgBlue=0;

  int at = startAt;
  for(int row=0; row<BMP_HEIGHT; ++row ) {
    for( int col=0; col<BMP_WIDTH; ++col) {
      avgRed += (((int)bmp[at++]) & 0xff);
      avgGreen += (((int)bmp[at++]) & 0xff);
      avgBlue += (((int)bmp[at++]) & 0xff);
    }
    // Add padding (if any) to make row length a multiple of 4 bytes.
    at += BMP_ROW_PADDING;
  }

  avgRed /= BMP_PIXELS;
  avgGreen /= BMP_PIXELS;
  avgBlue /= BMP_PIXELS;
  
  System.out.println("RGB Averages: "+avgRed+"  "+avgGreen+"  "+avgBlue);

  double redScale = 256.0 / avgRed;
  double greenScale = 256.0 / avgGreen;
  double blueScale = 256.0 / avgBlue;

  System.out.println("RGB Scales: "+redScale+"  "+greenScale+"  "+blueScale);
      
  at = startAt;
  for(int row=0; row<BMP_HEIGHT; ++row ) {
    for( int col=0; col<BMP_WIDTH; ++col) {
      int p;
      p = (int)bmp[at]&0xff;
      p *= redScale;
      bmp[at++] = (byte)p;

      p = (int)bmp[at]&0xff;
      p *= greenScale;
      bmp[at++] = (byte)p;

      p = (int)bmp[at]&0xff;
      p *= blueScale;
      bmp[at++] = (byte)p;
    }
    // Add padding (if any) to make row length a multiple of 4 bytes.
    at += BMP_ROW_PADDING;
  }

}


///////////////////////////////////////////////////////////////////////

  /** Transform a raw Bayer bitmap from the camera into raw,
   *  unfiltered BMP rows. Don't mess with this method unless
   *  you have a detailed understanding of both formats.
   */

   // DB: This does a literal 1-1 translation.  Every BMP pixel
   // will be saturated R, G, or B.

private
void transform() {
  Diagnostics.assert(RGGB_WIDTH==BMP_WIDTH && RGGB_HEIGHT == BMP_HEIGHT,
		      "BmpBuilder.new transform function");

  byte[] bmp = this.bmp, rggb = this.rggb;
  int at = this.at;

  int avgRed=0, avgGreen=0, avgBlue=0;

  for(int row=0; row<RGGB_HEIGHT; ++row ) {

    // Index of start of first half of rggb row (red or green2 pixels)
    int rggb_lower_rowbase = row*RGGB_WIDTH;

    // Index of start of second half of rggb row (green1 or blue pixels)
    int rggb_upper_rowbase = rggb_lower_rowbase+WIDTHBY2+2;

    boolean even_row = ((row&1) == 0);

    for( int col=0; col<BMP_WIDTH; ++col) {
      
      int rggb_col = col/2;  // We want integral division:
			 // e.g. for col=3, rggb_col=1
      byte p;

      if (even_row) {
	// even-numbered rows

	if ((col&1) == 0) {
	  // Even-numbered columns: Green1
	  p = rggb[rggb_upper_rowbase+rggb_col];  // Green 
	  avgGreen += (((int)p)&0xff);
	  p = scaleByte(p, 0.7);

	  bmp[at++] = 0;  // Red = 0
	  bmp[at++] = p;  // Green 
	  bmp[at++] = 0;  // Blue = 0
	} else {
	  // Odd-numbered columns: Red
	  p = rggb[rggb_lower_rowbase+rggb_col];  // Red 
	  avgRed += (((int)p)&0xff);
	  p = scaleByte(p, 0.7);

	  bmp[at++] = p;  // Red 
	  bmp[at++] = 0;  // Green = 0
	  bmp[at++] = 0;  // Blue = 0
	}

      } else {
	// odd-numbered rows

	if ((col&1) == 0) {
	  // Even-numbered columns: Blue
	  p = rggb[rggb_upper_rowbase+rggb_col];  // Blue 
	  avgBlue += (((int)p)&0xff);
	  p = scaleByte(p, 1.4);

	  bmp[at++] = 0;  // Red = 0
	  bmp[at++] = 0;  // Green = 0
	  bmp[at++] = p;  // Blue 
	} else {
	  // Odd-numbered columns: Green2
	  p = rggb[rggb_lower_rowbase+rggb_col];  // Green 
	  avgGreen += (((int)p)&0xff);
	  p = scaleByte(p, 0.7);

	  bmp[at++] = 0;  // Red = 0
	  bmp[at++] = p;  // Green 
	  bmp[at++] = 0;  // Blue = 0
	}
      }
    }

    // Add padding (if any) to make row length a multiple of 4 bytes.
    at += BMP_ROW_PADDING;
  }
  this.at = at;

  avgRed /= RGGB_HEIGHT*RGGB_WIDTH/4;
  avgGreen /= RGGB_HEIGHT*RGGB_WIDTH/2;
  avgBlue /= RGGB_HEIGHT*RGGB_WIDTH/4;

  Diagnostics.display("RGB Averages: "+avgRed+"  "+avgGreen+"  "+avgBlue);

}

///////////////////////////////////////////////////////////////////////

private
void monochrome_transform() {
  Diagnostics.assert(RGGB_WIDTH==BMP_WIDTH && RGGB_HEIGHT == BMP_HEIGHT,
		      "BmpBuilder.new transform function");

  byte[] bmp = this.bmp, rggb = this.rggb;
  int at = this.at;


  for(int row=0; row<RGGB_HEIGHT; ++row ) {

    // Index of start of first half of rggb row (red or green2 pixels)
    int rggb_lower_rowbase = row*RGGB_WIDTH;

    // Index of start of second half of rggb row (green1 or blue pixels)
    int rggb_upper_rowbase = rggb_lower_rowbase+WIDTHBY2+2;

    boolean even_row = ((row&1) == 0);

    for( int col=0; col<BMP_WIDTH; ++col) {
      
      int rggb_col = col/2;  // We want integral division:
			 // e.g. for col=3, rggb_col=1
      byte p;

      if (even_row) {
	// even-numbered rows

	if ((col&1) == 0) {
	  // Even-numbered columns: Green1
	  p = rggb[rggb_upper_rowbase+rggb_col];  // Green 

	  bmp[at++] = p;  // Red = 0
	  bmp[at++] = p;  // Green 
	  bmp[at++] = p;  // Blue = 0
	} else {
	  // Odd-numbered columns: Red
	  p = rggb[rggb_lower_rowbase+rggb_col];  // Red 
	  p = scaleByte(p, 0.7);

	  bmp[at++] = p;  // Red 
	  bmp[at++] = p;  // Green = 0
	  bmp[at++] = p;  // Blue = 0
	}

      } else {
	// odd-numbered rows

	if ((col&1) == 0) {
	  // Even-numbered columns: Blue
	  p = rggb[rggb_upper_rowbase+rggb_col];  // Blue 
	  p = scaleByte(p, 1.4);

	  bmp[at++] = p;  // Red = 0
	  bmp[at++] = p;  // Green = 0
	  bmp[at++] = p;  // Blue 
	} else {
	  // Odd-numbered columns: Green2
	  p = rggb[rggb_lower_rowbase+rggb_col];  // Green 

	  bmp[at++] = p;  // Red = 0
	  bmp[at++] = p;  // Green 
	  bmp[at++] = p;  // Blue = 0
	}
      }
    }

    // Add padding (if any) to make row length a multiple of 4 bytes.
    at += BMP_ROW_PADDING;
  }

  this.at = at;

}

///////////////////////////////////////////////////////////////////////

  /** Unit test. */

public
static
void main(String[] argv) throws Exception {
  Auditor.activate();
  Diagnostics.activate();
  Auditor.register(60);
  System.out.println("BmpBuilder main");
  byte[] rggb = new byte[CameraController.RGGB_SIZE];
  DataInputStream dis =
    new DataInputStream(
      new FileInputStream("./test.rggb"));
  dis.readFully(rggb);
  dis.close();
  byte[] computed = new byte[BMP_TOTAL_SIZE];
  new BmpBuilder().translate(rggb, computed);

  FileOutputStream fos = new FileOutputStream("./test.bmp");
  fos.write(computed);
  fos.close();

  /* ************

  byte[] expected = new byte[BMP_TOTAL_SIZE];
  dis = new DataInputStream(
          new FileInputStream("./regression/expectedoutput.bmp"));
  dis.readFully(expected);
  dis.close();

  for( int i=0; i<BMP_TOTAL_SIZE; ++i )
    Diagnostics.assert(computed[i]==expected[i], "BmpBuilder.main unit test");
  ********* */

  System.out.println("OK");
}

}

