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 TIFF image. This class converts
   *  the raw rasters from the camera into a TIFF 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 TiffBuilder {

  /** Convert from rggb into tiff. */

private byte[] rggb, tiff;

  /** tiff[at] is the next byte of tiff 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, tiff, at, buf.length);
  at += buf.length;
}

  /** swapShort(0x1234)==0x3412. */

private
short swapShort(short s) {
  return (short) (((s>>>8)&0xFF)+(s<<8));
}

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

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


// Some TIFF magic numbers
private static final short TIFF_BYTE = 1;
private static final short TIFF_SHORT = 3;
private static final short TIFF_LONG = 4;

private
void writeHeader() throws Exception {


  // Write TIFF ID bytes and image file directory offset (follows immediately).
  dos.write(new byte[] { 0x4d, 0x4d });
  dos.writeShort(0x2a);
  dos.writeInt(8);

  // 9 tagged entries in the first and only IFD.
  dos.writeShort(9);

  // Write the 9 tagged entries

  // Tag 1: SubfileType
  dos.writeShort(0xff);  	// Tag
  dos.writeShort(TIFF_SHORT);   // data type
  dos.writeInt(1);    		// data len
  dos.writeShort(1);    	// data offset or value
  dos.writeShort(0);		// padding

  // Tag 2: ImageWidth
  dos.writeShort(0x100);	// Tag
  dos.writeShort(TIFF_LONG);	// data type
  dos.writeInt(1);		// data len
  dos.writeInt(TIFF_WIDTH);	// data offset or value


  // Tag 3: ImageLength
  dos.writeShort(0x101);	// Tag
  dos.writeShort(TIFF_LONG);	// data type
  dos.writeInt(1);		// data len
  dos.writeInt(TIFF_HEIGHT);	// data offset or value

  // Tag 4: BitsPerSample
  dos.writeShort(0x102);	// Tag
  dos.writeShort(TIFF_SHORT);	// data type
  dos.writeInt(1);		// data len
  dos.writeShort(8);		// data offset or value
  dos.writeShort(0);		// padding

  // Tag 5: PhotometricInterpretation
  dos.writeShort(0x106);	// Tag
  dos.writeShort(TIFF_SHORT);	// data type
  dos.writeInt(1);		// data len
  dos.writeShort(2);		// data offset or value
  dos.writeShort(0);		// padding

  // Tag 6: StripOffset
  dos.writeShort(0x111);	// Tag
  dos.writeShort(TIFF_LONG);	// data type
  dos.writeInt(1);		// data len
  dos.writeInt(TIFF_HEADER_LEN);// data offset or value

  // Tag 7: SamplesPerPixel
  dos.writeShort(0x115);	// Tag
  dos.writeShort(TIFF_SHORT);	// data type
  dos.writeInt(1);		// data len
  dos.writeShort(3);		// data offset or value
  dos.writeShort(0);		// padding

  // Tag 8: StripByteCount
  dos.writeShort(0x117);	// Tag
  dos.writeShort(TIFF_LONG);	// data type
  dos.writeInt(1);		// data len
  dos.writeInt(TIFF_HEIGHT*TIFF_WIDTH*3);	// data offset or value

  // Tag 9: PlanarConfiguration
  dos.writeShort(0x11c);	// Tag
  dos.writeShort(TIFF_SHORT);	// data type
  dos.writeInt(1);		// data len
  dos.writeShort(1);		// data offset or value
  dos.writeShort(0);		// padding


  // Mark the end of the IFD
  dos.write(new byte[] { 0, 0, 0, 0 });

  flush();

  // The pixel data follows
}


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

  /* The G1-RR-BB-G2 values are Bayer pixels. The X values are TIFF pixels.
     Note that the TIFF 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 TIFF format */
private final static int TIFF_HEADER_LEN = 122;  // Room for 9 tags

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

public static final int
  TIFF_WIDTH = RGGB_WIDTH,  
  TIFF_HEIGHT = RGGB_HEIGHT,
  TIFF_SIZE = (TIFF_HEIGHT)*(TIFF_WIDTH)*3 + TIFF_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 TIFF rows. Don't mess with this method unless
   *  you have a detailed understanding of both formats.
   */

private
void orig_transform() {

  byte[] result = tiff, 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 balanceTiff(int startAt) {

  byte[] tiff = this.tiff;

  final int TIFF_PIXELS = TIFF_HEIGHT*TIFF_WIDTH;

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

  int at = startAt;
  for(int row=0; row<TIFF_HEIGHT; ++row ) {
    for( int col=0; col<TIFF_WIDTH; ++col) {
      avgRed += (((int)tiff[at++]) & 0xff);
      avgGreen += (((int)tiff[at++]) & 0xff);
      avgBlue += (((int)tiff[at++]) & 0xff);
    }
  }

  avgRed /= TIFF_PIXELS;
  avgGreen /= TIFF_PIXELS;
  avgBlue /= TIFF_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<TIFF_HEIGHT; ++row ) {
    for( int col=0; col<TIFF_WIDTH; ++col) {
      int p;
      p = (int)tiff[at]&0xff;
      p *= redScale;
      tiff[at++] = (byte)p;

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

      p = (int)tiff[at]&0xff;
      p *= blueScale;
      tiff[at++] = (byte)p;
    }
  }

}


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

  /** Transform a raw Bayer bitmap from the camera into raw,
   *  unfiltered TIFF 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 TIFF pixel
   // will be saturated R, G, or B.

private
void transform() {
  Diagnostics.assert(RGGB_WIDTH==TIFF_WIDTH && RGGB_HEIGHT == TIFF_HEIGHT,
		      "TiffBuilder.new transform function");

  byte[] tiff = this.tiff, 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<TIFF_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);

	  tiff[at++] = 0;  // Red = 0
	  tiff[at++] = p;  // Green 
	  tiff[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);

	  tiff[at++] = p;  // Red 
	  tiff[at++] = 0;  // Green = 0
	  tiff[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);

	  tiff[at++] = 0;  // Red = 0
	  tiff[at++] = 0;  // Green = 0
	  tiff[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);

	  tiff[at++] = 0;  // Red = 0
	  tiff[at++] = p;  // Green 
	  tiff[at++] = 0;  // Blue = 0
	}
      }
    }
  }
  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==TIFF_WIDTH && RGGB_HEIGHT == TIFF_HEIGHT,
		      "TiffBuilder.new transform function");

  byte[] tiff = this.tiff, 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<TIFF_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 

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

	  tiff[at++] = p;  // Red 
	  tiff[at++] = p;  // Green = 0
	  tiff[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);

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

	  tiff[at++] = p;  // Red = 0
	  tiff[at++] = p;  // Green 
	  tiff[at++] = p;  // Blue = 0
	}
      }
    }
  }
  this.at = at;

}

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

  /** Unit test. */

public
static
void main(String[] argv) throws Exception {
  Auditor.activate();
  Diagnostics.activate();
  Auditor.register(60);
  System.out.println("TiffBuilder 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[TIFF_SIZE];
  new TiffBuilder().translate(rggb, computed);

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

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

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

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

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

}

