import ij.*;
import ij.io.*; 
import ij.gui.*;
import ij.process.*;
import ij.plugin.*;
import ij.measure.Calibration;

import java.util.Vector; 
import java.io.*; 
import java.awt.*;
import java.awt.event.*; 
import java.awt.image.*; 
import java.util.Properties;

/** This plug-in defines an "Extended" Tiff file/image. This is an
    image read from a TIFF file that may not use TIFF fields in a way
    specified by the standard, or that stores additional information
    (e.g. color map data) in a nonstandard way (e.g. as part of the
    image descritpion). Such additional information may be useful for
    subsequent analysis activities.  

    The ExtendedTiff_ base class adds the capability to read the TIFF
    ImageDescription field from a TIFF image. It stores this
    information in an internal string and overrides the
    ij.ImagePlus.getFileInfo method to set the FileInfo.info field
    with the value of this internal string. (I would have used the
    FileInfo object managed by ImagePlus but this is a private member
    and ImagePlus.getFileInfo() method does NOT initialize the info
    field of the FileInfo object.)

    In addition, a list of those TIFF image directory entries not
    specifically handled by ImageJ is also maintained. For each the
    tag (2 bytes), type (2 bytes), count (4 bytes) and value/offset (4
    bytes) is also maintained. Currently, we DO NOT maintain a record
    of the value in the case where the value/offset stores an offset
    to the actual value.

    ExtendedTiff_ class also adds a postProcess method that can be used
    after the image data and fields are read to deal w/ any
    non-standard structure in the TIFF image file. For instance, the
    subclass might parse the data in the ImageDescription field (as is
    necessary for Olympus Fluoview TIFF images).


@see ij.ImagePlus
@see ij.io.TiffDecoder
@see plugins.ExtendedTiffEncoder_

@author Patrick Kelly <phkelly@ucsd.edu> */

public class ExtendedTiff_ extends ImagePlus implements PlugIn {

    /** Keep track of "working" directory. */
    private static String defaultDirectory = null; 

    /** Keep track of FileInfo.info field. We do this here because
	FileInfo member of ImagePlus is private and ImagePlus does not
	provide any way of accessing this FileInfo.info field.
	ImagePlus.getFileInfo member DOES NOT set this field.  */
    protected String imageDescription = null; 

    /** Vector of unknown (to ImageJ) image directory entries. */
    Vector ides = new Vector(); 

    /// phkelly - begin: added to deal w/ ImageDescription field.
    public static final int IMAGE_DESCRIPTION = 270;  // 10E
    public static final int ASCII = 2;                // ascii field type
    /// phkelly - end


    // --------------------------------------------------
    // 
    // Ctors
    //
    /** Constructs an uninitialized ExtendedTiff_ image object. */
    public ExtendedTiff_() {
    }

    /** Constructs an ExtendedTiff_ image object from an AWT Image. */
    public ExtendedTiff_(String title, Image img) {
	super(title,img); 
    }

    /** Constructs an ExtendedTiff_ image object from an
        ImageProcessor. */
    public ExtendedTiff_(String title, ImageProcessor ip) {
	super(title,ip); 
    }

    /** Constructs an ExtendedTiff_ image object from an image
	specfied by a URL. 'comp' is Component that will be used to
	display the image. */
    public ExtendedTiff_(String urlString, Component comp) {
	super(urlString); 
    }


    /** Constructs an ExtendedTiff_ image object from a stack. */
    public ExtendedTiff_(String title, ImageStack stack) {
	super(title,stack); 
    }

    // --------------------------------------------------
    /** Reads tiff image data from a file. */
    public void run(String arg) {
	FileInfo fi = null; 
	String fileName, directory; 

	if(arg.equals("")) {
	    // No file name specified so prompt user for file name.
	    FileDialog fd = new FileDialog(IJ.getInstance(),"Open TIFF..."); 
	    if(defaultDirectory!=null) 
		fd.setDirectory(defaultDirectory); 
	    fd.show();
	    fileName = fd.getFile();
	    directory = fd.getDirectory(); 
	    defaultDirectory = directory; 
	    fd.dispose(); 
	    if(fileName==null)
		return; 
	}
	else {
	    // File name is specified so read from this file.
	    int i = arg.lastIndexOf('/'); 
	    if(i==-1)
		i = arg.lastIndexOf('\\'); 
	    if(i>0) {
		directory = arg.substring(0,i+1); 
		fileName = arg.substring(i+1); 
	    }
	    else {
		directory = ""; 
		fileName = arg; 
	    }
	}
	if(IJ.debugMode) IJ.log("Opening: "+directory+fileName+"\n"); 

	// Use a "full" TIFF decoder. This is one that reads all
	// fields that TIFF directory entries.
	ETDecoder etd = 
	    new ETDecoder(directory,fileName); 

	FileInfo[] info = null; 

	try {info = etd.getTiffInfo();}
	catch(IOException e) {
	    IJ.log("ExtendedTiff_.run - IOException: "+e.getMessage());
	    return; 
	}

	// Get unknown (by ImageJ) image directory entries.
	ides = etd.getIDEs(); 

	ImagePlus imp = null; 
	if(info!=null) {
	    // Keep track of the ImageDescription field. Assumes this
	    // only shows up in first directory.
	    imageDescription = info[0].info; 

	    Opener opener = new Opener(); 
	    if(info.length>1){
		// Try to open as stack.
		imp = opener.openTiffStack(info); 
		if(imp!=null) {
		    // Successfully read image as a stack.
		    setResolution(info[0],imp); 
		}
	    }
	    if(imp==null) {
		// Didn't open yet so try to read as single image.
		FileOpener fo = new FileOpener(info[0]); 
		imp = fo.open(false); 
		if(info.length>1) 
		    IJ.log((info.length-1)+
			     " unopened images in this file.\n"); 
		if(imp==null)  // Still a problem, so give up.
		    return; 
	    }
	    if(imp.getStackSize()>1) 
		setStack(fileName,imp.getStack());  // sets processor
	    else
		setProcessor(fileName,imp.getProcessor()); 

	    setFileInfo(imp.getFileInfo()); 

	    // Let subclasses execute additional processing on the
	    // data.
	    postProcess(); 

	    if(arg.equals("")) show(); 
	}
	else {
	    // Problem reading file.
	    String errStr = "Problem reading file: " 
		+ directory+"/"+fileName+"\n"
		+"Make sure it is a TIFF file."; 
	    IJ.error(errStr); 
	    return; 
	}

	IJ.showStatus(""); 
	IJ.register(ExtendedTiff_.class); 

	if(IJ.debugMode) {
	    if(ides.size()==0)
		IJ.log("No unknown image directory entires."); 
	    else {
		IJ.log("Unknown image directory entries"); 
		for(int n=0; n<ides.size();++n) {
		    IDE ide = (IDE)ides.elementAt(n);
		    IJ.log("Entry number: "+n+":\t"+ide); 
		}
	    }
	}

    } // end run


    // --------------------------------------------------
    /** Override the ImagePlus.getFileInfo() method to initialize the
	FileInfo.info field w/ the TIFF ImageDescription field.  */
    public FileInfo getFileInfo() {
	FileInfo fi = super.getFileInfo();
	// Only difference is we set the info here.
	fi.info = imageDescription; 
	return fi; 
    }

    // --------------------------------------------------
    /** Retrieve unknown (to ImageJ) TIFF image directory entries.
     */
    public Vector getIDEs() {
	return ides; 
    }

    // --------------------------------------------------
    /** This method gives subclasses a chance to perform additional
    processing on the input data. Subclasses should handle nonstandard
    aspects of input TIFF file here.*/
    protected void postProcess() {
    }


    // --------------------------------------------------
    /** We need this functionality.  As currently defined in the
	ij.io.FileOpener class, it has default package visibility and
	thus is accessible only to FileOpener objects and other
	classes in the ij.io package.  */
    private void setResolution(FileInfo fi, ImagePlus imp) {
	if (fi.pixelWidth>0.0 && fi.unit!=null) {
	    Calibration cal = imp.getCalibration();
	    cal.pixelWidth = fi.pixelWidth;
	    cal.pixelHeight = fi.pixelHeight;
	    cal.setUnit(fi.unit);
	}
    }



    // --------------------------------------------------
    /** Let subclasses set image description. */
    protected void setImageDesc(String sid) {
	imageDescription = sid; 
    }



// --------------------------------------------------
/** Decodes uncompressed, multi-image TIFF files. This is a slightly
    modified version of the ij.io.TiffDecoder class. It has been
    modified to read the ImageDescription Tiff field.

    It would be better if this class could simply extend TiffDecoder
    as most of the processing is the same. But currently
    ij.io.TiffDecoder interface does not support subclass access to
    routines such as OpenIFD that we need to extend.  */

class ETDecoder {
    // tags - get these from ij.io.TiffEncoder


    //field types
    final int SHORT = 3;
    final int LONG = 4;

    private String directory, name;
    private File f;
    RandomAccessFile in;
    private boolean littleEndian;
    private boolean debugMode;

    private int currentIFD; 

    Vector ides = new Vector(); 

    // --------------------------------------------------	
    public ETDecoder(String directory, String name) {
	this.directory = directory;
	this.name = name;
    }

    // --------------------------------------------------
    protected final int getInt() throws IOException {
	int b1 = in.read();
	int b2 = in.read();
	int b3 = in.read();
	int b4 = in.read();
	if (littleEndian)
	    return ((b4 << 24) + (b3 << 16) + (b2 << 8) + (b1 << 0));
	else
	    return ((b1 << 24) + (b2 << 16) + (b3 << 8) + b4);
    }


    // --------------------------------------------------
    protected int getShort() throws IOException {
	int b1 = in.read();
	int b2 = in.read();
	if (littleEndian)
	    return ((b2 << 8) + b1);
	else
	    return ((b1 << 8) + b2);
    }

    // --------------------------------------------------
    protected int OpenImageFileHeader() throws IOException {
	// Open 8-byte Image File Header at start of file.
	// Returns the offset in bytes to the first IFD or -1
	// if this is not a valid tiff file.
	int byteOrder = in.readShort();
	if (byteOrder==0x4949) // "II"
	    littleEndian = true;
	else if (byteOrder==0x4d4d) // "MM"
	    littleEndian = false;
	else {
	    in.close();
	    return -1;
	}
	int magicNumber = getShort(); // 42
	int offset = getInt();
	return offset;
    }
	

    // --------------------------------------------------	
    protected int getValue(int fieldType, int count) throws IOException {
	int value = 0;
	int unused;
	if (fieldType==SHORT && count==1) {
	    value = getShort();
	    unused = getShort();
	}
	else
	    value = getInt();
	return value;
    }
	
	
    // --------------------------------------------------	
    protected void getColorMap(int offset, FileInfo fi) throws IOException {
	byte[] colorTable16 = new byte[768*2];
	long saveLoc = in.getFilePointer();
	in.seek(offset);
	int bytesRead = in.read(colorTable16);
	in.seek(saveLoc);
	if (bytesRead!=768*2)
	    return;
	fi.lutSize = 256;
	fi.reds = new byte[256];
	fi.greens = new byte[256];
	fi.blues = new byte[256];
	int j = 0;
	if (littleEndian)
	    j++;
	for (int i=0; i<256; i++) {
	    fi.reds[i] = colorTable16[j];
	    fi.greens[i] = colorTable16[512+j];
	    fi.blues[i] = colorTable16[1024+j];
	    j += 2;
	}
	fi.fileType = FileInfo.COLOR8;
    }
	
    // --------------------------------------------------	
    protected void decodeNIHImageHeader(int offset, FileInfo fi) 
	throws IOException {
	long saveLoc = in.getFilePointer();
		
	in.seek(offset+12);
	int version = in.readShort();
		
	in.seek(offset+160);
	double scale = in.readDouble();
	if (version>106 && scale!=0.0) {
	    fi.pixelWidth = 1.0/scale;
	    fi.pixelHeight = fi.pixelWidth;
	} 

	in.seek(offset+172);
	int units = in.readShort();
	if (version<=153) units += 5;
	switch (units) {
	case 5: fi.unit = "nanometer"; break;
	case 6: fi.unit = "micrometer"; break;
	case 7: fi.unit = "mm"; break;
	case 8: fi.unit = "cm"; break;
	case 9: fi.unit = "meter"; break;
	case 10: fi.unit = "km"; break;
	case 11: fi.unit = "inch"; break;
	case 12: fi.unit = "ft"; break;
	case 13: fi.unit = "mi"; break;
	}

	in.seek(offset+260);
	int nImages = in.readShort();
	if(nImages>=2)
	    fi.nImages = nImages;

	in.seek(offset+272);
	float aspectRatio = in.readFloat();
	if (version>140 && aspectRatio!=0.0)
	    fi.pixelHeight = fi.pixelWidth/aspectRatio;
		
	in.seek(saveLoc);
	//fi.info += "littleEndian: "+littleEndian+"\n";
	//fi.info += version+" "+scale+" "+units+" "+aspectRatio+"\n";;
    } // end decodeNIHImageHeader
	

    // --------------------------------------------------	
    protected void dumpTag(int tag, int count, int value, FileInfo fi) {
	String name;
	switch (tag) {
	case TiffDecoder.NEW_SUBFILE_TYPE: name="NewSubfileType"; break;
	case TiffDecoder.IMAGE_WIDTH: name="ImageWidth"; break;
	case TiffDecoder.IMAGE_LENGTH: name="ImageLength"; break;
	case TiffDecoder.STRIP_OFFSETS: name="StripOffsets"; break;
	case TiffDecoder.PHOTO_INTERP: name="PhotoInterp"; break;
	case TiffDecoder.BITS_PER_SAMPLE: name="BitsPerSample"; break;
	case TiffDecoder.SAMPLES_PER_PIXEL: name="SamplesPerPixel"; break;
	case TiffDecoder.ROWS_PER_STRIP: name="RowsPerStrip"; break;
	case TiffDecoder.STRIP_BYTE_COUNT: name="StripByteCount"; break;
	case TiffDecoder.X_RESOLUTION: name="XResolution"; break;
	case TiffDecoder.Y_RESOLUTION: name="YResolution"; break;
	case TiffDecoder.RESOLUTION_UNIT: name="ResolutionUnit"; break;
	case TiffDecoder.PLANAR_CONFIGURATION: name="PlanarConfiguration"; break;
	case TiffDecoder.COMPRESSION: name="Compression"; break; 
	case TiffDecoder.COLOR_MAP: name="ColorMap"; break; 
	case TiffDecoder.SAMPLE_FORMAT: name="SampleFormat"; break; 
	case TiffDecoder.NIH_IMAGE_HDR: name="NIHImageHeader"; break; 
	    /// phkelly - begin: added to deal w/ TIFF ImageDescription field.
	case IMAGE_DESCRIPTION: name = "ImageDescription"; break; 
	    /// phkelly - end
	default: name="???"; break;
	}
	String cs = (count==1)?"":", count=" + count;
	fi.info += "    " + tag + ", \"" + name + "\", value=" + value + cs + "\n";
    }

    // --------------------------------------------------
    double getRational(int loc) throws IOException {
	long saveLoc = in.getFilePointer();
	in.seek(loc);
	int numerator = getInt();
	int denominator = getInt();
	in.seek(saveLoc);
	//System.out.println("numerator: "+numerator);
	//System.out.println("denominator: "+denominator);
	if (denominator!=0)
	    return (double)numerator/denominator;
	else
	    return 0.0;
    }
	
    // --------------------------------------------------	
    protected FileInfo OpenIFD() throws IOException {
	// Get Image File Directory data
	
	int tag, fieldType, count, value;
	int nEntries = getShort();

	if (nEntries<1)
	    return null;
	FileInfo fi = new FileInfo();
	if (debugMode) fi.info = "\n  " + name + ": opening\n";
	for (int i=0; i<nEntries; i++) {
	    tag = getShort();
	    fieldType = getShort();
	    count = getInt();
	    value = getValue(fieldType, count);
	    if (debugMode) dumpTag(tag, count, value, fi);
	    if (tag==0) return null;
	    switch (tag) {
	    case TiffDecoder.IMAGE_WIDTH: 
		fi.width = value;
		break;
	    case TiffDecoder.IMAGE_LENGTH: 
		fi.height = value;
		break;
	    case TiffDecoder.STRIP_OFFSETS: 
		if (count==1)
		    fi.offset = value;
		else {
		    long saveLoc = in.getFilePointer();
		    in.seek(value);
		    fi.offset = getInt(); // Assumes contiguous strips
		    in.seek(saveLoc);
		}
		break;
	    case TiffDecoder.PHOTO_INTERP:
		fi.whiteIsZero = value==0;
		break;
	    case TiffDecoder.BITS_PER_SAMPLE:
		if (count==1) {
		    if (value==8)
			fi.fileType = FileInfo.GRAY8;
		    else if (value==16) {
			fi.fileType = FileInfo.GRAY16_UNSIGNED;
			fi.intelByteOrder = littleEndian;
		    }
		    else if (value==32) {
			fi.fileType = FileInfo.GRAY32_FLOAT;
			fi.intelByteOrder = littleEndian;
		    }
		    else
			throw new IOException("Unsupported BitsPerSample: " + value);
		}
		break;
	    case TiffDecoder.SAMPLES_PER_PIXEL:
		if (value==3)
		    fi.fileType = FileInfo.RGB;
		else if (value!=1)
		    throw new IOException("Unsupported SamplesPerPixel: " + value);
		break;
	    case TiffDecoder.X_RESOLUTION:
		double xScale = getRational(value); 
		if (xScale!=0.0) fi.pixelWidth = 1.0/xScale; 
		break;
	    case TiffDecoder.Y_RESOLUTION:
		double yScale = getRational(value); 
		if (yScale!=0.0) fi.pixelHeight = 1.0/yScale; 
		break;
	    case TiffDecoder.RESOLUTION_UNIT:
		if (value==1)
		    fi.unit = " ";
		else if (value==2)
		    fi.unit = "inch";
		else if (value==3)
		    fi.unit = "cm";
		break;
	    case TiffDecoder.PLANAR_CONFIGURATION:
		if (value==2 && fi.fileType==FileInfo.RGB)
		    fi.fileType = FileInfo.RGB_PLANAR;
		break;
	    case TiffDecoder.COMPRESSION: 
		if (value!=1)
		    throw new IOException("TIFF compression is not supported");
	    case TiffDecoder.COLOR_MAP: 
		if (count==768 && fi.fileType==fi.GRAY8)
		    getColorMap(value, fi);
		break;
	    case TiffDecoder.SAMPLE_FORMAT:
		if (fi.fileType==FileInfo.GRAY32_FLOAT && value!=3)
		    fi.fileType = FileInfo.GRAY32_INT;
		break;
	    case TiffDecoder.NIH_IMAGE_HDR: 
		if (count==256)
		    decodeNIHImageHeader(value, fi);
		break;
		/// phkelly - begin: added to deal w/ image desc.
	    case IMAGE_DESCRIPTION:
		//if(fieldType==ASCII) {
		String id = readImageDescriptor(value,count); 
		fi.info = id; 
		//}
		break; 
		/// phkelly - end
	    default:
		/// phkelly - begin: add to ife vector

		// Add to Vector of unknown image directory entries.
		IDE ide = new IDE();
		ide.nDirectory = currentIFD; 
		ide.ideTag = tag; 
		ide.ideType = fieldType; 
		ide.ideCount = count; 
		ide.ideValueOffset = value; 
		ides.addElement((Object)ide); 
		/// phkelly - end
	    }
	}
	fi.fileFormat = fi.TIFF;
	fi.fileName = name;
	fi.directory = directory;
	return fi;
    }


    // --------------------------------------------------	
    public void enableDebugging() {
	debugMode = true;
    }
	

    // --------------------------------------------------	
    public FileInfo[] getTiffInfo() throws IOException {
	int ifdOffset;
	Vector info;
				
	info = new Vector();
	f = new File(directory + name);
	in = new RandomAccessFile(f, "r");
	ifdOffset = OpenImageFileHeader();
	if (ifdOffset<0) {
	    in.close();
	    return null;
	}

	currentIFD = 0; 
	while (ifdOffset>0) {

	    in.seek(ifdOffset);
	    FileInfo fi = OpenIFD();
	    if (fi!=null)
		info.addElement(fi);
	    ifdOffset = getInt();

	    currentIFD++; 
	    
	    if (fi!=null && fi.nImages>1) // ignore extra IFDs in NIH Image stack
		ifdOffset = 0;
	}
	in.close();
	if (info.size()==0)
	    return null;
	else {
	    FileInfo[] fi = new FileInfo[info.size()];
	    info.copyInto((Object[])fi);
	    return fi;
	}
    }

    public Vector getIDEs() {
	return ides; 
    }


    /// phkelly - begin: added to deal w/ Tiff ImageDescriptor field.

    //
    // Assume that if there is no image description field, the count
    // will be 0. Also, assume that if ImageDescriptor field is
    // present, the value is ALWAYS an offset and the ImageDescriptor
    // is NEVER stored in the value field (this would correspond to a
    // 4-byte (character) descriptor - one of which should be null -
    // which is unlikely).
    //
    protected String readImageDescriptor(int offset, int count) throws IOException {
	if(count==0)
	    return null; 

	long saveLoc = in.getFilePointer(); 
	in.seek(offset); 

	// Read bytes into FileInfo info string. Don't include
	// terminating nul.
	byte[] b = new byte[count-1]; 
	in.readFully(b); 

	// Convert using platform encoding.
	String id = new String(b); 

	// Reset
	in.seek(saveLoc); 

	return id; 
    }
    /// phkelly - end

}  // ETDecoder




    // --------------------------------------------------
    /** Maintain information on Tiff image directory entry.  */

    class IDE extends Object {
	public int ideTag; 
	public int ideType; 
	public int ideCount; 
	public int ideValueOffset; 
	public int nDirectory; 

	public IDE() {}

	public String toString() {
	    String str = 
		"Dir: "+nDirectory +
		"\tTag: "+ideTag +
		"\tType: "+ideType +
		"\tCount: "+ideCount+
		"\tValue/offset: "+ideValueOffset; 

	    return str; 
	}

    }  // IDE


}   // end ExtendedTiff_ class
