import ij.*; 
import ij.io.*; 
import ij.plugin.*; 

import java.io.*; 


/** A version of ij.io.TiffEncoder that can write the image
    description and additional image directory entries associated with
    an ExtendedTiff image. Currently, only writes out tiff image
    directory entries w/ values greater than 32768, the starting value
    reserved for private tags.

    This is a simple modification of the ij.io.TiffEncoder class with
    processing to handle outputing the FileInfo.info field associated
    with an ExtendedTiff object into the TIFF ImageDescription field of
    the output TIFF file. 

@see ij.io.TiffEncoder
@see plugins.ExtendedTiff_

@author Patrick Kelly <phkelly@ucsd.edu>

*/

public class ExtendedTiffEncoder {
    /// Have to redfine these here because they are not public in
    //ij.io.TiffEncoder.
    //
    static final int IMAGE_START = 768;
    static final int HDR_SIZE = 8;
    static final int MAP_SIZE = 768;
    static final int BPS_DATA_SIZE = 6;
    static final int SCALE_DATA_SIZE = 16;

    private FileInfo fi;
    private int bitsPerSample;
    private int photoInterp;
    private int samplesPerPixel;
    private int nEntries;
    private int ifdSize;
    private int imageOffset = IMAGE_START;
    private int imageSize;
    private int stackSize;

    /// phkelly - begin: added to deal w/ ImageDescription field output.
    private int idOffset;   // address at which to write ImageDescription
    private int idCount;    // count of ImageDescription value
    private boolean firstIFD;  
    private int firstIfdSize; 
    private int nEntriesFirst; 
    /// phkelly - end
	
    public ExtendedTiffEncoder (FileInfo fi) {
	this.fi = fi;
	bitsPerSample = 8;
	samplesPerPixel = 1;
	nEntries = 9;
	int bytesPerPixel = 1;
	switch (fi.fileType) {
	case FileInfo.GRAY8:
	    photoInterp = fi.whiteIsZero?0:1;
	    break;
	case FileInfo.GRAY16_SIGNED:
	    bitsPerSample = 16;
	    photoInterp = fi.whiteIsZero?0:1;
	    bytesPerPixel = 2;
	    break;
	case FileInfo.GRAY32_FLOAT:
	    bitsPerSample = 32;
	    photoInterp = fi.whiteIsZero?0:1;
	    bytesPerPixel = 4;
	    break;
	case FileInfo.RGB:
	    photoInterp = 2;
	    samplesPerPixel = 3;
	    bytesPerPixel = 3;
	    break;
	case FileInfo.COLOR8:
	    photoInterp = 3;
	    nEntries = 10;
	    break;
	default:
	    photoInterp = 0;
	}
	if (fi.unit!=null && fi.pixelWidth!=0 && fi.pixelHeight!=0)
	    nEntries += 3; // XResolution, YResolution and ResolutionUnit

	ifdSize = 2 + nEntries*12 + 4;

	/// phkelly - begin: added to deal w/ image description
	firstIfdSize = ifdSize; 
	nEntriesFirst = nEntries; 
	/// phkelly - end

	imageSize = fi.width*fi.height*bytesPerPixel;
	stackSize = imageSize*fi.nImages;

	firstIFD = true; 

	/// phkelly - begin: added to deal w/ ImageDescription offset.
	if(fi.info!=null) {
	    // Image information exists so compute offset to save it
	    // in TIFF ImageDescription field. Write this as last item
	    // in image. This is ok as according to TIFF 6.0 standard:
	    // "The values to which directory entries point need not
	    // be in and particular order in the file."
	    idOffset = 
		IMAGE_START   // start of image data
		+stackSize    // size of image data
		+((fi.fileType==FileInfo.COLOR8)?MAP_SIZE*2:0) // cmap
		+ifdSize*(fi.nImages-1);     // remainig ifds

	    // String.lenth() returns number of 16-bit unicode
	    // characters in the string. This should be same as number
	    // of bytes if all ascii. One extra for terminating NUL.
	    idCount = fi.info.length()+1; 

	    // Increase first IFD size since this is where we store
	    // ImageDescription entry.
	    nEntriesFirst = nEntries+1; 
	    firstIfdSize = 2 + nEntriesFirst*12 + 4; 
	}
	/// phkelly - end.
    }
	
    /** Saves the image as a TIFF file. */
    public void write(DataOutputStream out) throws IOException {
	writeHeader(out);
	int nextIFD = 0;
	if (fi.nImages>1) {
	    nextIFD = IMAGE_START+stackSize;
	    /// phkelly - bug?  Shouldn't this be MAP_SIZE*2 because
	    //writeColorMap outputs MAP_SIZE*2 bytes. TIFF colormap
	    //data is SHORT.
	    if (fi.fileType==FileInfo.COLOR8) nextIFD += MAP_SIZE;
	    /// phkelly - end
	}
	writeIFD(out, imageOffset, nextIFD);
	int bpsSize=0, scaleSize=0;;
	if (fi.fileType==FileInfo.RGB)
	    bpsSize = writeBitsPerPixel(out);
	if (fi.unit!=null && fi.pixelWidth!=0 && fi.pixelHeight!=0)
	    scaleSize = writeScale(out);

	/// phkelly - begin: use firstIfdSize here.
	byte[] filler = new byte[IMAGE_START - 
				(HDR_SIZE+firstIfdSize+bpsSize+scaleSize)];
	/// phkelly - end

	out.write(filler); // force image to start at offset 768
	new ImageWriter(fi).write(out);
	if (fi.fileType==FileInfo.COLOR8)
	    writeColorMap(out);

	/// phkelly - begin
	firstIFD=false; 
	/// phkelly - end

	for (int i=2; i<=fi.nImages; i++) {
	    if (i==fi.nImages)
		nextIFD = 0;
	    else
		nextIFD += ifdSize;

	    imageOffset += imageSize;
	    writeIFD(out, imageOffset, nextIFD);
	}

	/// phkelly - begin. write out image description data if
	//present
	if(fi.info!=null)
	    writeImageDescription(out); 
	/// phkelly - end
    }
	
    /** Writes the 8-byte image file header. */
    void writeHeader(DataOutputStream out) throws IOException {
	byte[] hdr = new byte[8];
	hdr[0] = 77; // "MM" (Motorola byte order)
	hdr[1] = 77;
	hdr[2] = 0;  // 42 (magic number)
	hdr[3] = 42;
	hdr[4] = 0;  // 8 (offset to first IFD)
	hdr[5] = 0;
	hdr[6] = 0;
	hdr[7] = 8;
	out.write(hdr);
    }
	
    /** Writes one 12-byte IFD entry. */
    void writeEntry(DataOutputStream out, int tag, int fieldType, int count, int value) throws IOException {
	out.writeShort(tag);
	out.writeShort(fieldType);
	out.writeInt(count);
	if (count==1 && fieldType==3)
	    value <<= 16;
	out.writeInt(value);
    }
	
    /** Writes one IFD (Image File Directory). */
    void writeIFD(DataOutputStream out, int imageOffset, int nextIFD) throws IOException {	
	/// phkelly - begin: deal w/ different IFD sizes.
	int ifdsz = ifdSize; 
	int nEnt = nEntries; 

	if(firstIFD==true) {
	    ifdsz = firstIfdSize; 
	    nEnt  = nEntriesFirst; 
	}

	int tagDataOffset = HDR_SIZE + ifdsz;

	out.writeShort(nEnt);
	/// phkelly - end

	writeEntry(out, TiffDecoder.NEW_SUBFILE_TYPE, 4, 1, 0);
	writeEntry(out, TiffDecoder.IMAGE_WIDTH,      3, 1, fi.width);
	writeEntry(out, TiffDecoder.IMAGE_LENGTH,     3, 1, fi.height);
	if (fi.fileType==FileInfo.RGB) {
	    writeEntry(out, TiffDecoder.BITS_PER_SAMPLE,  3, 3, tagDataOffset);
	    tagDataOffset += BPS_DATA_SIZE;
	} else
	    writeEntry(out, TiffDecoder.BITS_PER_SAMPLE,  3, 1, bitsPerSample);
	writeEntry(out, TiffDecoder.PHOTO_INTERP,     3, 1, photoInterp);

	/// phkelly - begin: write out ImageDescription here if
	//present. Note, we only write image description entry in
	//first IFD.
	if(fi.info!=null && firstIFD==true)
	    writeEntry(out, ExtendedTiff_.IMAGE_DESCRIPTION,
		       ExtendedTiff_.ASCII,idCount,idOffset); 
	/// phkelly - end.

	writeEntry(out, TiffDecoder.STRIP_OFFSETS,    4, 1, imageOffset);
	writeEntry(out, TiffDecoder.SAMPLES_PER_PIXEL,3, 1, samplesPerPixel);
	writeEntry(out, TiffDecoder.ROWS_PER_STRIP,   3, 1, fi.height);
	writeEntry(out, TiffDecoder.STRIP_BYTE_COUNT, 4, 1, imageSize);
	if (fi.unit!=null && fi.pixelWidth!=0 && fi.pixelHeight!=0) {
	    writeEntry(out, TiffDecoder.X_RESOLUTION, 5, 1, tagDataOffset);
	    writeEntry(out, TiffDecoder.Y_RESOLUTION, 5, 1, tagDataOffset+8);
	    tagDataOffset += SCALE_DATA_SIZE;
	    int unit = 1;
	    if (fi.unit.equals("inch"))
		unit = 2;
	    else if (fi.unit.equals("cm"))
		unit = 3;
	    writeEntry(out, TiffDecoder.RESOLUTION_UNIT, 3, 1, unit);
	}
	if (fi.fileType==FileInfo.COLOR8)
	    writeEntry(out, TiffDecoder.COLOR_MAP, 3, MAP_SIZE, IMAGE_START+stackSize);
	out.writeInt(nextIFD);
    }
	
    /** Writes the 6 bytes of data required by RGB BitsPerSample tag. */
    int writeBitsPerPixel(DataOutputStream out) throws IOException {
	out.writeShort(8);
	out.writeShort(8);
	out.writeShort(8);
	return BPS_DATA_SIZE;
    }

    /** Writes the 16 bytes of data required by the XResolution and YResolution tags. */
    int writeScale(DataOutputStream out) throws IOException {
	double xscale = 1.0/fi.pixelWidth;
	double yscale = 1.0/fi.pixelHeight;
	double scale = 1000000.0;
	if (xscale>1000.0) scale = 1000.0;
	out.writeInt((int)(xscale*scale));
	out.writeInt((int)scale);
	out.writeInt((int)(yscale*scale));
	out.writeInt((int)scale);
	return SCALE_DATA_SIZE;
    }

    /** Writes color palette following the image. */
    void writeColorMap(DataOutputStream out) throws IOException {
	byte[] colorTable16 = new byte[MAP_SIZE*2];
	int j=0;
	for (int i=0; i<fi.lutSize; i++) {
	    colorTable16[j] = fi.reds[i];
	    colorTable16[512+j] = fi.greens[i];
	    colorTable16[1024+j] = fi.blues[i];
	    j += 2;
	}
	out.write(colorTable16);
    }

    /// phkelly - begin: added to deal w/ ImageDescription.
    private void writeImageDescription(DataOutputStream out) throws IOException {
	// Get bytes associated w/ ASCII encoding so 1 byte per char.
	//byte[] bytes = fi.info.getBytes("ASCII"); 
	byte[] bytes = fi.info.getBytes(); 

	out.write(bytes); 

	// Write out terminating null byte.
	byte b = 0x00; 
	out.write(b); 
    }
    /// phkelly - end
	
}  // ExtendedTiffEncoder


