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

import java.util.Vector; 
import java.io.*; 
import java.awt.*;
import java.awt.event.*; 
import java.awt.image.*; 
import java.util.*; 
import java.lang.*; 
import java.text.DecimalFormat;

/** This plug-in defines an Image object generated by a Fluoview TIFF
	file. Such files are produced by the Olympus Fluoview software.

	These TIFF files use the TIFF ImageDescription field to store
	acquisition parameters, version information and color lookup
	tables. As a result, this Image Description field must be parsed
	to extract the corresponding information. In addition, this
	information must be maintained to ensure that if these images are
	later saved the appropriate Fluoview information is propagated to
	the new files.	FluoviewTiff_ is derived from the ExtendedTiff to
	pick up that class's support for reading the ImageDescription
	field from a Fluoview TIFF file.

	Note: Fluoview software can not read files associated with this
	FluoviewTiff_ class. It appears the Fluoview software is using some
	private tags. Need to follow up on this.

@see ij.ImagePlus
@see plugins.ExtendedTiff_
@see plugins.ExtendedTiffEncoder_
@see plugins.SaveExtendedTiff_

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


public class FluoviewTiff_ extends ExtendedTiff_ implements PlugIn {

	// --------------------------------------------------
	//
	// Begin Inner Class Definitions.
	//
	// Inner classes to manage parameters.
	//

	// 
	// Olympus Fluoview system may output information in several
	// channels.
	//
	public class CProps extends Object {
	String sTypeDye = ""; 
	String sIntensityRange = "";   
	
	public int min = 0; 	// min intensity
	public int max = 4095;	// max intensity

	// Support one-level of undo 
	int prevMin = 0; 
	int prevMax = 4095; 
	
	byte[] bReds; 
	byte[] bGreens; 
	byte[] bBlues; 

	int lutSize; 

	int index = -1; 

	final int defaultSize = 256;   //  lut size
	
	CProps(int i) {
		index = i; 
		lutSize = defaultSize; 

		allocate(lutSize); 
	}

	public void setMinAndMax(int min, int max) {
		this.min = min; 
		this.max = max; 
	}

	public void setMin(int min) {
		this.min = min; 
	}
	
	public void setMax(int max) {
		this.max = max; 
	}

	public int getMin() {
		return min;
	}

	public int getMax() {
		return max; 
	}


	private void allocate(int sz) {
		bReds	= new byte[sz]; 
		bGreens = new byte[sz];
		bBlues	= new byte[sz]; 
	}

	public String toString() {
		String s = "Channel "+index+"\n"; 
		s += sIntensityRange;
		return s; 
	}

	void setDefaults() {
		prevMin = min; 
		prevMax = max; 
	}

	void reset() {
		min = prevMin; 
		max = prevMax; 
	}

	} // end CProps inner class

	//
	// Maintain information about the acquistion parameters
	// contained in the ImageDescription field.
	//
	public class AcqParams extends Object {
	String sVersion = ""; 
	String sSource	= ""; 
	String sZoom	= ""; 
	String sMag 	= ""; 
	String sDesc	= ""; 

	// Keep track of entire acquistion parameter string. This
	// makes parsing easier.
	String sap = null; 

	AcqParams(String s) {sap=s;}

	public String toString() {
		String s = sap;
		s+="\n"+"\tVersion: "+sVersion+"\n";
		s+="\tSource: "+sSource+"\n";
		s+="\tZoom: "+sZoom+"\n";
		s+="\tMag: "+sMag+"\n";
		s+="\tDesc: "+sDesc+"\n";

		return s; 
	}
	
	} // end AcqParams inner class


	//
	// Inner class to manage parsing Fluoview TIFF ImageDescription
	// value.  This is something of a hack. Should implement a more
	// formal parsing module using StreamTokenizer or Java equivalent
	// of lex/yacc.
	//
	// This will initialize the ChannelProps and AcqParams
	// objects maintained by associated instance of FluoviewTiff_.
	//
	class FVParser extends Object {
	final String I_MAPPING_START = "[Intensity Mapping]\r\n"; 
	final String I_MAPPING_END	 = "[Intensity Mapping End]\r\n"; 

	final String AP_START		 = "[Acquisition Parameters]\r\n"; 
	final String AP_END 		 = "[Acquisition Parameters End]\r\n"; 

	final String VERSION_START	 = "[Version Info]\r\n"; 
	final String VERSION_END	 = "[Version Info End]\r\n"; 

	final String DESC_START 	 = "[Description]\r\n";
	final String DESC_END		 = "[Description End]\r\n";

	final String CHANNEL		 = "Channel";
	final String SOURCE 		 = "Source";
	final String ZOOM			 = "Zoom Size";
	final String MAG			 = "Magnification";

	final String CHANNEL_TAG	 = "Ch";
	final String MAP_TAG		 = "Map";
	final String SEP			 = ":";
	final String RANGE			 = "Range=";
	final String TO 			 = "to"; 

	final String LUT_START		 = "[LUT Ch";
	final String LUT_END		 = " End]\r\n";

	final String LEFT_BRACKET	 = "[";
	final String RIGHT_BRACKET	 = "]";

	final String RGB_TAG		 = "RGB"; 
	final String EQUALS 		 = "=";
	final String LINE_TERM		 = "\n"; 
	final String LINE_END		 = "\r\n"; 
	final String CARRIAGE_RTN	 = "\r"; 
	final String SPACE			 = " "; 
	final String TAB			 = "\t"; 


	// --------------------------------------------------
	// Initialize a set of AcqParams by parsing input 
	// ImageDescription String. Returns null on failure.
	public	AcqParams imageDesc2AP(String id) {
		if(id==null) return null; 

		int is = id.indexOf(AP_START); 
		int ie = id.indexOf(AP_END); 

		if(is==-1 || ie==-1)
		return null; 

		String sap;
		try {sap = id.substring(is,ie)+AP_END;}  // Add back on ending
		catch(StringIndexOutOfBoundsException e) 
		{return null; }

		ap = new AcqParams(sap);

		// Try to read Source parameter.
		ap.sSource = readParameter(sap,SOURCE,LINE_TERM); 
		// Try to read Zoom parameter.
		ap.sZoom = readParameter(sap,ZOOM,LINE_TERM); 
		// Try to read Magnitude parameter.
		ap.sMag  = readParameter(sap,MAG,LINE_TERM); 

		// Following are not really acquistion parameters but we
		// store them here anyway.	Read these from entire string,
		// not just sap.

		// Try to read version entry.
		ap.sVersion = readEntry(id,VERSION_START,VERSION_END); 

		// Try to read Description entry.
		ap.sDesc = readEntry(id,DESC_START,DESC_END); 

		return ap; 
	}

	// --------------------------------------------------	   
	//
	// Read an entire entry, backeted by the input start/end
	// parameters from the input string.
	//
	private String readEntry(String id, String start, String end) {
		int is = id.indexOf(start); 
		int ie = id.indexOf(end); 
		 
		String s = null; 
		try{s=id.substring(is,ie)+end;}
		catch(StringIndexOutOfBoundsException e)
		{return null;}

		return s; 
	}

	private  String readParameter(String id, String param, String term) {
		String s = ""; 

		int is = id.indexOf(param); 
		int ie = id.indexOf(term,is); 

		try{s = id.substring(is,ie)+term;}
		catch(StringIndexOutOfBoundsException e)
		{return "";}

		return s; 
	}

	// --------------------------------------------------
	//
	// Nested class to make dealing w/ IntensityParameter entry
	// easier.
	//
	public class IM extends Object {
		int min,max; 
		int index; 
		String sMappingRange; 

		IM(String s) {
		sMappingRange = s; 
		}

		public String toString() {
		return sMappingRange; 
		}
	}

	// --------------------------------------------------
	//
	// Read intensity mapping entry from ImageDescription
	// field.
	//
	Hashtable imageDesc2IM(String id) {
		if(id==null) return null; 

		// Select the intensity mapping entry.
		int is = id.indexOf(I_MAPPING_START); 
		int ie = id.indexOf(I_MAPPING_END); 

		if(is==-1 || ie==-1) 
		return null; 

		// Select out interior of intensity map entry.
		is += I_MAPPING_START.length();

		// Intensity mapping entry is present, so parse it.
		String smap;
		try{smap = id.substring(is,ie);}
		catch(StringIndexOutOfBoundsException e)
		{return null;}

		StringTokenizer st = new StringTokenizer(smap,LINE_TERM); 

		Hashtable ht = new Hashtable(); 
		while(st.hasMoreTokens()) {
		String s = st.nextToken(); 

		// Note - add line terminator back into string.
		IM im = new IM(s+LINE_TERM); 

		// Read min, max intensity bounds.
		String sdelim = " \t\n\r"+SEP+EQUALS; 
		StringTokenizer sti = new StringTokenizer(s,sdelim); 

		// Read past Map field.
		String tmp = sti.nextToken(); 

		// Read Ch# field and get number.
		String sindex = sti.nextToken();

		try{im.index = 
			Integer.parseInt(sindex.substring(2,sindex.length()));}
		catch(StringIndexOutOfBoundsException e) {return null;}
		catch(NumberFormatException e) {return null;}
		
		// Read Range info.
		sti.nextToken();

		// Read min value.
		try{im.min = Integer.parseInt(sti.nextToken());}
		catch(NumberFormatException e) {return null;}

		// Read past "to" field.
		sti.nextToken();

		// Read max value.
		try{im.max = Integer.parseInt(sti.nextToken());}
		catch(NumberFormatException e) {return null;}

		// Add this to hash table.
		ht.put(new Integer(im.index),im); 
		}

		return ht; 
	}

	// --------------------------------------------------
	private  CProps[] imageDesc2ChannelProps(String id, Hashtable htim) {
		if(id==null || htim==null) return null; 
		CProps[] acp = new CProps[htim.size()]; 

		Enumeration keys = htim.keys(); 
		for(int j=0; j<htim.size();++j) {
		Integer I = (Integer)(keys.nextElement()); 
		int i = I.intValue(); 

		// Read each intensity mapping entry and use this to 
		// parse lookup table information.
		CProps cp = new CProps(i); 

		// Build lut delimiters for this entry.
		String slutStart = 
			LUT_START+Integer.toString(i)+RIGHT_BRACKET+
			CARRIAGE_RTN+LINE_TERM; 
		String slutEnd = LUT_START+Integer.toString(i)+LUT_END;

		int is = id.indexOf(slutStart); 
		int ie = id.indexOf(slutEnd); 

		// Make sure lut information present.
		if(is==-1 || ie==-1)
			return null; 

		// Strip out LUT begin delimiters as it makes parsing
		// easier.
		is += slutStart.length(); 

		String slut; 
		try{slut = id.substring(is,ie);}
		catch(StringIndexOutOfBoundsException e)
			{return null;}
		
		String sdelim = " \t\n\r"+RGB_TAG+EQUALS;
		StringTokenizer st = new StringTokenizer(slut,sdelim); 

		int index = 0; 
		while(st.hasMoreTokens()) {
			String s = st.nextToken(); // Skip first number
			// Read red entry.
			try {
			cp.bReds[index] = 
				(byte)((Integer.parseInt(st.nextToken()))&0xff);}
			catch(NumberFormatException e)
			{return null;}

			// Next should be green.
			try {
			cp.bGreens[index] = 
				(byte)((Integer.parseInt(st.nextToken()))&0xff);}
			catch(NumberFormatException e)
			{return null;}
			// Finally, we should get blue.

			try {
			cp.bBlues[index] = 
				(byte)((Integer.parseInt(st.nextToken()))&0xff);}
			catch(NumberFormatException e)
			{return null;}

			++index; 
		}

		// Finished reading lut for this channel. So set
		// parameters for this channel and add it to vector.
		IM im = (IM)htim.get(new Integer(i)); 
		if(im==null) return null; 

		cp.min = im.min;
		cp.max = im.max; 

		// Set the intensity mapping string. 
		cp.sIntensityRange = im.sMappingRange; 

		// Finally, get the type and dye parameters for this
		// channel. Note, we assume acquistion parameters have
		// been initialized at this point.
		cp.sTypeDye = readTypeDye(i,ap); 

		//acp[i] = cp; 
		acp[j] = cp; 
		}

		return acp; 
	}

	// --------------------------------------------------
	// Read the type and dye fields corresponding to channel w/
	// specified index from the input AcquistionParameters
	// structure. We also assume that these entries appear in
	// pairs.  So we look for 2 entries.
	//
	private String readTypeDye(int index, AcqParams ap) {
		if(ap==null) return ""; 

		// First increment index. We do this because of weirdness in
		// Fluoview image description.
		++index; 

		String s = ""; 
		 
		// Build channel tag.
		String sChannel = CHANNEL+" "+index; 
		int is = ap.sap.indexOf(sChannel); 

		int ie = is; 
		if(is>-1) {
		ie = ap.sap.indexOf(LINE_TERM,is); 
		try{s = ap.sap.substring(is,ie)+LINE_TERM;}
		catch(StringIndexOutOfBoundsException e) 
			{return null;}
		}

		is = ap.sap.indexOf(sChannel,ie); 
		if(is>-1) {
		ie = ap.sap.indexOf(LINE_TERM,is); 
		try{s += ap.sap.substring(is,ie)+LINE_TERM;}
		catch(StringIndexOutOfBoundsException e)
			{return null;}
		}

		return s; 
	}

	// --------------------------------------------------
	//
	// Public interface to parsing operations.
	//
	public void parseImageDescription(String id) {
		vChannelProps = null; 

		if(id==null) return; 
		

		Hashtable htim = imageDesc2IM(id); 
		if(htim==null)
		return ; 

		vChannelProps = imageDesc2ChannelProps(id,htim); 
		// Enable future changes to intensity mapping bounds to be
		// undone.
		initChannelUndo(); 
	}

	// --------------------------------------------------
	//
	// Convert channel properties and image acquistion parameters
	// maintained by associated FluoviewTiff_ object into an
	// ImageDescription string suitable to output to a TIFF file.
	//
	public String properties2ImageDesc() {
		if(ap==null || vChannelProps==null)
		return null; 
		 
		String sim	= I_MAPPING_START;	// accumulate intensity mapping
		String slut  = "";				// accumulate lookup tables
		String stype = "";				// accumulate type/dye strings

		// Write out channel properties associated w/ each channel
		// in the image.
		for(int i=0; i<vChannelProps.length;++i) {
		CProps cp = vChannelProps[i]; 

		// Accure intensity mapping data for this channel.
		sim += imap2String(cp); 

		slut += lut2String(cp); 

		stype += cp.sTypeDye; 
		}

		// Close up the intensity mapping entry.
		sim += I_MAPPING_END; 

		// Add acquistion parameters to output image description.
		String sap = acq2string(stype); 

		String sid = sap+sim+slut; 

		return sid; 
	}

	// --------------------------------------------------
	private String acq2string(String stype) {
		String sap = AP_START; 
		sap += ap.sSource+ap.sZoom+ap.sMag; 
		sap += stype; 
		sap += AP_END; 

		sap += ap.sVersion+ap.sDesc; 

		return sap; 
	}

	// --------------------------------------------------
	//
	// Convert lut associated w/ input ChannelProperites object
	// into a string.
	//
	private String lut2String(CProps cp) {
		// Build LUT begin tag.
		String slut = LUT_START
		+cp.index
		+RIGHT_BRACKET+LINE_END; 

		for(int i=0; i<cp.lutSize;++i) {
		slut += RGB_TAG+SPACE+i+EQUALS; 
		slut += values2String(cp,i); 
		slut += LINE_END; 
		}

		slut += LUT_START+cp.index+LUT_END; 

		return slut; 
	}

	// --------------------------------------------------
	/** Convert ChannelProperty intensity mapping parameters into
		an appropriately formated string. */
	private String imap2String(CProps cp) {
		String simap = 
		MAP_TAG+SPACE+CHANNEL_TAG
		+cp.index
		+SEP+SPACE+RANGE+EQUALS; 

		DecimalFormat df = new DecimalFormat("0000"); 
		simap += df.format(cp.getMin()); 
		simap += SPACE+TO+SPACE;
		simap += df.format(cp.getMax()); 
		simap += LINE_END; 
		
		return simap; 
	}

	// --------------------------------------------------
	// Convert lut values at a particular index to a string.
	private String values2String(CProps cp, int index) {
		int rvalue = 255 & cp.bReds[index]; 
		int gvalue = 255 & cp.bGreens[index];
		int bvalue = 255 & cp.bBlues[index];

		DecimalFormat df = new DecimalFormat("000"); 
		String svalues = df.format(rvalue); 
		svalues += TAB + df.format(gvalue); 
		svalues += TAB + df.format(bvalue); 

		return svalues; 
	}

	
	} // end FVParser inner class

	//
	// End inner class definitions.
	//
	// --------------------------------------------------

	/** Maintain set of channel property parameters for this image. */
	private CProps[] vChannelProps; 

	/** Active channel indicator. */
	private boolean[] activeChannels = null; 
	private int nActive = 0; 

	/** Maintain acquisition parameters for this image. */
	private AcqParams ap = null; 

	/** Store composited RGB pixels in this array. */
	private int[] rgbPixels; 
	/** Control whether createImage generates an image corresponding
	to single channel or composite image that reflects all
	channels. */
	private boolean composite; 
	/** Specifies whether composite color models use intensity mapping
	parameters provided by Fluoview or default bounds. */
	private boolean usingFVIntensity; 

	/** Set of color models, one for each channel. Used to map index
	values to color Luts to create composite images. */
	private ClampedColorModel[] colorMods; 

	/** All Fluoview files use this version tag. */
	static final String VERSION_TAG = "[Version Info]"; 


	//
	// Ctors
	//

	// --------------------------------------------------
	public FluoviewTiff_() {
	init(); 
	}


	// --------------------------------------------------
	public FluoviewTiff_(String urlString, Component comp) {
	super(urlString,comp); 
	init(); 
	}


	// --------------------------------------------------
	public FluoviewTiff_(String title, Image img) {
	super(title,img); 
	init(); 
	}
	

	// --------------------------------------------------
	public FluoviewTiff_(String title, ImageProcessor ip) {
	super(title,ip); 
	init(); 
	}


	// --------------------------------------------------
	public FluoviewTiff_(String title, ImageStack stack) {
	super(title,stack); 
	init(); 
	}
	

	// --------------------------------------------------
	/** Initialize state of FluoviewTiff_ image instance. */
	protected void init() {
	vChannelProps = null; 
	activeChannels = null; 

	ap = null; 

	colorMods = null; 

	// Default behavior is not composited.
	composite = false; 

	// Default behavior is not to use Fluoview Intensity mapping
	// parameters.
	usingFVIntensity = false; 

	rgbPixels = null; 
	}


	// --------------------------------------------------
	/** Initialize color models used to generate composite images. */
	public void initColorMods() {
	colorMods = new ClampedColorModel[getNumberChannels()]; 

	for(int c=0; c<getNumberChannels();++c) {
		CProps cp = getChannelProps(c); 
		// Note, using default lower, upper.
		colorMods[c]= new ClampedColorModel(256,
						cp.bReds,
						cp.bGreens,
						cp.bBlues,
						12);  // bits in index
	}

	useFVIntensity(true); 
	}


	// --------------------------------------------------
	/** Configures color models so that they use either intensity
	mapping parameters generated by the Fluoview software or
	default bounds. */
	public void useFVIntensity(boolean useFV) {
	usingFVIntensity = useFV; 

	for(int c=0; c<getNumberChannels();++c) {
		ClampedColorModel cm = colorMods[c]; 

		if(usingFVIntensity) {
		// Color models use Fluoview intensity bounds.
		CProps cp = getChannelProps(c); 
		cm.setLowerAndUpper(cp.min,cp.max,true); 
		}
		else
		cm.setLowerAndUpper(0,4095,true);	// MAGIC NUMBER
	}
	}


	// --------------------------------------------------
	/** Returns status indicating whether composite images are
	computed using Fluoview intensity mapping bounds or default
	bounds. */
	public boolean fvIntensity() { 
	return usingFVIntensity; 
	}	


	 // --------------------------------------------------
	 // Override ExtendedTiff run.
	 public void run(String arg) {
	 super.run(arg); 

	 IJ.register(FluoviewTiff_.class); 
	 }


	// --------------------------------------------------
	/** Sets composite flag equal to input value. If setWin input is
	true, updates associated window object with this new composite
	indicator. */
	 public void setComposite(boolean c, boolean setWin) {
	 // Prevent user from setting composite true if this image
	 // does not have Fluoview information.
	 if(!hasFluoviewInfo(imageDescription)) {
		 if(IJ.debugMode)
		 IJ.log("\tCan not change composite on file: "
			  +getTitle()
			  +"\n\tMake sure this is a real Fluoview image."); 
		return; 
	 }

	 composite = c; 

	 if(setWin==true) {
		 if(win instanceof FVStackWindow)
		 ((FVStackWindow)win).setComposite(c); 
	 }

	 updateAndRepaintWindow(); 
	 }

	// --------------------------------------------------
	/** Returns flag indicating whether or not object is generating
	composite or standard images. */
	 public boolean isFVComposite() {
	 return composite; 
	 }


	 // --------------------------------------------------
	 /** Post process TIFF data. Here we parse the ImageDescription
	 field. */
	 protected void postProcess() {
	 // Make sure we have a Fluoview file.
	 if(!hasFluoviewInfo(imageDescription)) return; 

	 parseImageDescription(); 
	 }


	 // --------------------------------------------------
	 // This invokes parse to parse image description.
	 protected void parseImageDescription() {
	 FVParser fp = new FVParser(); 

	 // First read the acquistion parameters.
	 ap = fp.imageDesc2AP(imageDescription); 
	 // Parse remainder of image description.
	 fp.parseImageDescription(imageDescription); 

	 // Order channel properties.
	 orderChannelProps(); 

	 // Initilize internal state data. Must be called after
	 // properties are ordered.
	 initState(); 
	 }


	 // --------------------------------------------------
	 public String props2ImageDesc() {
	 FVParser fp = new FVParser();	
	 return fp.properties2ImageDesc(); 
	 }


	// --------------------------------------------------
	/** Return array of channel properites. */
	 public CProps[] getChannelProps() {
	 return vChannelProps;
	 }


	 // --------------------------------------------------
	 /** Return channel property object associated w/ input index. */
	 public CProps getChannelProps(int i) {
	 try{
		 return vChannelProps[i]; 
	 }
	 catch(ArrayIndexOutOfBoundsException e) {
		 return null; 
	 }
	 }


	 // --------------------------------------------------
	 public CProps[] getChannelProps(int start, int end) {
	 if(vChannelProps==null) 
		 return null;
	 if(start > end) 
		 return null; 
	 if(start < 0 || end >= vChannelProps.length) 
		 return null; 

	 CProps[] acp = new CProps[end-start+1];

	 for(int n=start, index=0; n<=end;++n,++index) 
		 acp[index] = vChannelProps[n]; 

	 return acp; 
	 }

	// --------------------------------------------------
	public boolean isActive(int i) {
	return activeChannels[i]; 
	}

	// --------------------------------------------------
	public void activateChannel(int i) {
	// Make channel active.
	activeChannels[i] = true; 

	updateActiveCount(); 
	}

	// --------------------------------------------------
	public void inactivateChannel(int i) {
	// Ensure that at least one channel is always active.
	//if(nActive == 1)
	//return; 

	activeChannels[i] = false; 

	updateActiveCount(); 
	}

	// --------------------------------------------------
	public int numActiveChannels() {
	return nActive; 
	}


	 // --------------------------------------------------
	 public AcqParams getAcqParams() {
	 return ap; 
	 }


	// --------------------------------------------------
	 public int getNumberChannels() {
	 if(vChannelProps==null) 
		 return 0;
	 else 
		 return vChannelProps.length; 
	 }


	// --------------------------------------------------
	/** Use acquisition parameter and channel property information
	maintained by this object to create a new image description
	string. This method is necessary because users may need to
	create an image description for this object after its
	acquisition and channel property information has been set. */
	 public void initImageDescription() {
	 if(ap==null || vChannelProps==null) 
		 setImageDesc(null); 

	 setImageDesc(props2ImageDesc()); 
	 }


	 // --------------------------------------------------
	 public void setChannelProps(CProps[] acp) {
	 vChannelProps = acp; 

	 // Reinitialize state data here because channel props have
	 // changed.
	 initState(); 

	 // Enable future changes to intensity mapping bounds to be
	 // undone.
	 initChannelUndo(); 
	 }


	 // --------------------------------------------------
	 public void setChannelProps(CProps cp) {
	 vChannelProps = new CProps[] { cp }; 

	 // Reinitialize state data because channel props have
	 // changed.
	 initState(); 

	 // Enable future changes to intensity mapping bounds to be
	 // undone.
	 initChannelUndo(); 
	 }

	// --------------------------------------------------
	/** Allow changes to channel properties to be undone. Only a
	 single level of undo is currently supported. */
	public void initChannelUndo() {
	for(int n=0; n<vChannelProps.length;++n)
		vChannelProps[n].setDefaults(); 
	}

	// --------------------------------------------------
	/** Return channel properties to their default values. This undoes
	previous (one level)change to channel properties. */
	public void resetChannelProps() {
	for(int n=0; n<vChannelProps.length;++n) {
		vChannelProps[n].reset(); 
		colorMods[n].setLowerAndUpper(vChannelProps[n].getMin(),
					  vChannelProps[n].getMax(),
					  true/*recompute*/); 
	}
	}

	// --------------------------------------------------
	/** Return specific channel property to its default value. This
	undoes previous (one level) change to specific channel
	property.*/
	public void resetChannelProps(int n) {
	vChannelProps[n].reset(); 
	colorMods[n].setLowerAndUpper(vChannelProps[n].getMin(),
					  vChannelProps[n].getMax(),
					  true/*recompute*/); 

	}

	// --------------------------------------------------
	public void setAcqParams(AcqParams ap) {
	 this.ap = ap; 
	}


	 // --------------------------------------------------
	 /** Determine if input string contains a fluoview identifier.
	 This is not a very robust way to check for Fluoview
	 files. For instance, the string could contain the Fluoview
	 version info and nothing else. Revisit in later release. */
	 private static boolean hasFluoviewInfo(String sid) {
	 if(sid==null) 
		 return false; 

	 // Is Fluoview identifier present?
	 if(sid.indexOf(VERSION_TAG)>0)
		 return true;
	 else
		 return false; 
	 }


	 // --------------------------------------------------
	 /** Determine if input image is a valid FluoviewTiff_ object.
	 This check first determines if input is an instance of
	 FluoviewTiff_ type. If so, it then checks the image
	 description field in the input image to ensure that it
	 contains valid Fluoview version information.  */
	 public static boolean isFluoviewImage(ImagePlus imp) {
	 if(!(imp instanceof FluoviewTiff_))
		 return false; 

	 return hasFluoviewInfo(imp.getFileInfo().info); 
	 }


	// --------------------------------------------------
	/** Creates displayable image based on composite flag. If display
	is not to show composited images, just behave like a standard
	ImagePlus object and return Image generated by associated
	ImageProcessor. Otherwise return a displayable image that is a
	composite of multiple channels. */
	 protected Image createImage() {
	 if(!composite || getType()!=GRAY16)
		 return ip.createImage();
	 else
		 return compositeChannels();
	 }


	// --------------------------------------------------
	/** Initialize values of pixels used to build composite image. */
	 private void initRGBPixels() {
	 // Get pixels for each channel.
	 short[][] vpixels = getImagePixels(); 

	 int number = getWidth()*getHeight(); 
	 if(rgbPixels==null) 
		 rgbPixels = new int[number]; 

	 int mask = 0xffff; 

	 for(int n=0; n<number; ++n) {
		 int rvalue = 0; 
		 int gvalue = 0; 
		 int bvalue = 0; 

		 for(int c=0; c<getNumberChannels(); ++c) {
		 // Only include active channels in composite.
		 if(activeChannels[c]==false)
			 continue; 

		 ClampedColorModel ccm = colorMods[c]; 
		 short[] spixels = vpixels[c]; 

		 int value = spixels[n]&mask; 

		 rvalue += ccm.getRed(value); 
		 gvalue += ccm.getGreen(value); 
		 bvalue += ccm.getBlue(value); 
		 }

		 // Composite pixels
		 rvalue = (rvalue>255)?255:rvalue; 
		 gvalue = (gvalue>255)?255:gvalue; 
		 bvalue = (bvalue>255)?255:bvalue; 

		 rgbPixels[n] = 0xff << 24;
		 rgbPixels[n] |= rvalue << 16;
		 rgbPixels[n] |= gvalue << 8;
		 rgbPixels[n] |= bvalue; 
	 }
	 }

	// --------------------------------------------------
	/** Initialize values of pixels used to build composite image. */
	 private void initRGBPixels(int slice) {
	 // Get pixels for each channel.
	 short[][] vpixels = getImagePixelsSlice(slice); 

	 int number = getWidth()*getHeight(); 
	 if(rgbPixels==null) 
		 rgbPixels = new int[number]; 

	 int mask = 0xffff; 

	 for(int n=0; n<number; ++n) {
		 int rvalue = 0; 
		 int gvalue = 0; 
		 int bvalue = 0; 

		 for(int c=0; c<getNumberChannels(); ++c) {
		 // Only include active channels in composite.
		 if(activeChannels[c]==false)
			 continue; 

		 ClampedColorModel ccm = colorMods[c]; 
		 short[] spixels = vpixels[c]; 

		 int value = spixels[n]&mask; 

		 rvalue += ccm.getRed(value); 
		 gvalue += ccm.getGreen(value); 
		 bvalue += ccm.getBlue(value); 
		 }

		 // Composite pixels
		 rvalue = (rvalue>255)?255:rvalue; 
		 gvalue = (gvalue>255)?255:gvalue; 
		 bvalue = (bvalue>255)?255:bvalue; 

		 rgbPixels[n] = 0xff << 24;
		 rgbPixels[n] |= rvalue << 16;
		 rgbPixels[n] |= gvalue << 8;
		 rgbPixels[n] |= bvalue; 
	 }
	 }

	// --------------------------------------------------
	/** Creates java.awt.Image object that is a composite of multiple
	channels. This is public so other routines can retrieve a
	composite image, even if this object is not currently
	configured to display composite images. */
	 public Image compositeChannels() {
	 initRGBPixels(); 
	 return createCompositeImage(rgbPixels); 
	 }

	public Image compositeChannels(int slice) {
	initRGBPixels(slice); 
	return createCompositeImage(rgbPixels); 
	}

	// --------------------------------------------------
	/** Does work of creating a RGB java.awt.Image object using input
	array of pixels. */
	 private Image createCompositeImage(int[] rgbPixels) {
	 // Composite image into single RGBA image.
	 ImageProducer p = 
		 new MemoryImageSource(getWidth(), getHeight(),
				   rgbPixels,
				   0,				   // offset
				   getWidth());  // scansize
	 return Toolkit.getDefaultToolkit().createImage(p); 
	 }


	 // --------------------------------------------------
	 /** Retrieve pixels associated with channel c (and currently
	   active slice) from FluoviewTiff_ image. */
	 private short[] getImagePixels(int c) {
	 // Number of composite images.
	 int numImages = getStackSize()/getNumberChannels(); 

	 // Actual slice value.
	 int slice = getSliceOffset() + c*numImages; 

	 // Retrieve pixels to process.
	 if(slice==getCurrentSlice())
		 return (short[])ip.getPixels(); 
	 else
		 return (short[])(getStack().getPixels(slice)); 
	 }

	 private short[] getImagePixels(int c,int cslice) {
	 // Number of composite images.
	 int numImages = getStackSize()/getNumberChannels(); 

	 // Actual slice value.
	 int slice = cslice + c*numImages; 

	 // Retrieve pixels to process.
	 if(slice==getCurrentSlice())
		 return (short[])ip.getPixels(); 
	 else
		 return (short[])(getStack().getPixels(slice)); 
	 }

	 // --------------------------------------------------
	/** Retrieve pixels from all channels associated with current
		slice. */
	 private short[][] getImagePixels() {
	 short[][] spixels = new short[getNumberChannels()][]; 

	 for(int c=0; c<getNumberChannels(); ++c)
		 spixels[c] = getImagePixels(c); 

	 return spixels; 
	 }

	 private short[][] getImagePixelsSlice(int slice) {
	 short[][] spixels = new short[getNumberChannels()][]; 

	 for(int c=0; c<getNumberChannels(); ++c)
		 spixels[c] = getImagePixels(c,slice); 

	 return spixels; 
	 }

	 // --------------------------------------------------
	 /** Determines slice corresponding to input channel index
	 associated with currently active stack slice.	*/
	 private int channel2Slice(int c) {
	 int offset    = getSliceOffset(); 
	 int numImages = getNumberComposites(); 

	 return offset + c*numImages; 
	 }


	 // --------------------------------------------------
	 /** Determines the channel index associated with the currently
	 active stack slice. */
	 private int currentChannel() {
	 int slice	   = getCurrentSlice(); 
	 int offset    = getSliceOffset(); 

	 int delta = slice-offset; 
	 int ch = (int)Math.floor((double)delta/(double)getNumberComposites()); 

	 return ch; 
	 }


	 // --------------------------------------------------
	 /** Returns currently active stack slice. */
	 //private int getCurrentSlice() {
	//	  return (getStack()==null)?1:getStack().getCurrentSlice(); 
	 //}


	 // --------------------------------------------------
	 /** Returns remainder when currently active stack slice is
	 divided by the number of composite images. This remainder
	 gives the index of the composite image the currently active
	 slice contributes to. */
	 private int getSliceOffset() {
	 int slice	   = getCurrentSlice(); 
	 int numImages = getNumberComposites();

	 int tmp = slice; 
	 try{
		 tmp =	(slice-1)%numImages + 1; 
	 }
	 catch(ArithmeticException e) {
		 IJ.log("ArithmeticException in getSliceOffset"); 
	 }
	 
	 // Default just returns current slice.
	 return tmp; 
	 }


	 // ==================================================
	 //
	 // Override ImagePlus methods.
	 //

	 // --------------------------------------------------
	 /** Override ImagePlus.show() to get correct composite display
	 behavior. */
	 public void show() {
	 if(win!=null) return; 

	 img = createImage(); 
	 if ((img!=null) && (width>=0) && (height>=0)) {
		 if (getStackSize()>1)
		 win = new FVStackWindow(this);
		 else
		 win = new ImageWindow(this);

		 draw();
		 IJ.showStatus("");
	 } else
		 IJ.log("<Error loading image>");
	 }


	 // --------------------------------------------------
	/** Override ImagePlus.updateImage() to correctly update
	displayable image data in case of composite display. */
	 public void updateImage() {

	 if(!composite  || getType()!=GRAY16) {
		 super.updateImage(); 
	 }
	 else {
		 if (ip!=null) {
		 //if (img==null)
			 img = createImage();
		 //else {
		 //    img.flush();
		 //    if (img.getSource()!=ip.getImageSource())
		 //    img = createImage();
		//}
		}
	}
	}


	// --------------------------------------------------
	/** Override ImagePlus.getImage() to get correct displayable image
	in case of composite display. */
	public Image getImage() {
	if(!composite || getType()!=GRAY16) {
		img = null;
		super.getImage(); 
	} else {
		// Return composited version
		if(img==null && ip!=null)
		createImage(); 
	}
	return img; 
	}


	// --------------------------------------------------
	/** Override ImagePlus.setProcessor() to account for compositing
	behavior if activated. */
	public void setProcessor(String title, ImageProcessor ip) {
	super.setProcessor(title,ip);
	if(composite)
		createImage(); 
	}

	// --------------------------------------------------
	/** Override ImagePlus.createImagePlus() to copy Fluoview specific 
	information to new image.
	*/
	public ImagePlus createImagePlus() {
	FluoviewTiff_ imp2 = new FluoviewTiff_(); 
	imp2.setCalibration(getCalibration());
	if (vChannelProps==null)
		return imp2;

	// Set type of this image.
	imp2.setType(ImagePlus.GRAY16); 

	// Copy scale information.
	imp2.copyScale(this); 

	// Copy Fluoview specific information.
	imp2.setChannelProps(getChannelProps()); 
	imp2.setAcqParams(getAcqParams()); 
	imp2.setImageDesc(imageDescription); 

	// Set channel property defaults for new image.
	imp2.initChannelUndo(); 

	// Set composite properties for new image.
	imp2.setComposite(isFVComposite(),true); 

	return imp2; 
	}

	// --------------------------------------------------
	/** Retrieve statistcs for slice from channel ch. Not really an
	 override but pretty close. */
	public ImageStatistics getStatistics(int ch) {
	int slice = channel2Slice(ch); 
	if(IJ.debugMode==true) 
		IJ.log("Retrieving statistics for slice: "+slice); 

	ImageProcessor ip = getProcessor(); 
	ShortProcessor sp = new ShortProcessor(ip.getWidth(),
						   ip.getHeight(),
						   true);
	//IJ.log(getStackSize()+" "+slice+" "+getCurrentSlice()); 
	short[] pixels = (short[])getStack().getPixels(getCurrentSlice()); 

	sp.setPixels(pixels); 
	return new ShortStatistics(sp); 
	}

	//
	// End ImagePlus overrides.
	//
	// ==================================================


	// --------------------------------------------------
	/** Sort channel properties in order of increasing channel index.
	Use an insertion sort because number of channel props is typically
	small (1-5). */
	private void orderChannelProps() {
	if(vChannelProps==null)
		return; 

	for(int k=1; k<vChannelProps.length;++k) {
		boolean inserted = false; 
		int i = k; 
		while((i>0) && !inserted) {
		if(vChannelProps[i-1].index > vChannelProps[i].index) {
			// Swap the values.
			CProps tmp	 = vChannelProps[i-1]; 
			vChannelProps[i-1] = vChannelProps[i]; 
			vChannelProps[i]   = tmp; 
			i--; 
		}
		else
			inserted = true; 
		}
	}
	}

	// --------------------------------------------------
	/** Returns number of composite images that can be created using
	stack data.  */
	public int getNumberComposites() {
	if(getNumberChannels()==0)
		return 0; 

	int num = (int)Math.floor((double)getStackSize()/
				  (double)getNumberChannels()); 
	return num; 
	}

	// --------------------------------------------------
	/** Activates particular composite image. */
	public void setCompositeSlice(int c) {
	if(getStackSize()==1)
		return; 
	if(c>=1 && c<=getNumberComposites()) {
		setSlice(c); 
	}
	}


	// --------------------------------------------------
	public void setChannelPropMin(int n, int min) {
	if(n < 0 || n >= getNumberChannels()) return; 
	vChannelProps[n].setMin(min); 
	colorMods[n].setLower(min,true); 
	}

	public void setChannelPropMinMax(int n, int min, int max) {
	if(n<0 || n>=getNumberChannels()) return; 
	vChannelProps[n].setMinAndMax(min,max); 
	colorMods[n].setLowerAndUpper(min,max,true); 
	}

	// --------------------------------------------------
	public void setChannelPropMax(int n, int max) {
	if(n < 0 || n >= getNumberChannels()) return; 
	vChannelProps[n].setMax(max); 
	colorMods[n].setUpper(max,true); 
	}

	// --------------------------------------------------
	private void initActiveChannels() {
	activeChannels = null; 
	activeChannels = new boolean[getNumberChannels()];

	// All channels initially active.
	for(int n=0; n<activeChannels.length;++n)
		activeChannels[n] = true; 
	}

	// --------------------------------------------------
	private void initState() {
	initColorMods();
	initActiveChannels(); 
	}

	// --------------------------------------------------
	private void updateActiveCount() {
	nActive = 0; 
	for(int n=0; n<activeChannels.length;++n)
		nActive++; 
	}

} // end FluoviewTiff_

	
// --------------------------------------------------
//
/** This class is an extended ij.gui.ImageWindow used to display image
  stacks generated by the Olympus Fluoview software. A modified
  version of ij.gui.ImageWindow is needed to support the display
  Fluoview images in color.  */

class FVStackWindow extends StackWindow 
	implements Runnable, AdjustmentListener, ActionListener{

	/** Number of channels in associated Fluoview image. Each channel
	is treated as a separate color. */
	protected int numChannels;
	/** Number of separate composite (color) images in associated
	Fluoview image. This will typically be smaller (by a factor of
	numChannels) than the number of slices in the stack. */
	protected int numImages; 
	/** Controls whether window displays composited (color) or
	standard view of image data. */
	protected boolean composite; 
	private int activeSlice; 


	// --------------------------------------------------
	public FVStackWindow(FluoviewTiff_ imp, boolean composite) {
	this(imp); 
	setComposite(composite); 
	}


	// --------------------------------------------------
	public FVStackWindow(FluoviewTiff_ imp) {
	super(imp);
	init(); 
	setComposite(false); 
	}


	// --------------------------------------------------
	// Synchronize this method because we modify slice value
	public synchronized void setComposite(boolean c) {
	composite = c; 
	updateSlider(); 

	// Update display.
	if(!running) {
		slice = sliceSelector.getValue();
		notify(); 
	}
	}


	// --------------------------------------------------
	protected void init() {
	int stackSize = imp.getStackSize(); 
	if(FluoviewTiff_.isFluoviewImage(imp)) {
		numImages = stackSize/
		((FluoviewTiff_)imp).getNumberChannels(); 
	}
	activeSlice = 1; 
	}

	// --------------------------------------------------
	/** Adjust slider based on value of composite. */
	protected void updateSlider() {
	int blockIncrement,maxvalue; 
	int value = sliceSelector.getValue(); 
	if(composite==true) {
		// Slider bounds should be smaller to reflect fewer images
		// in composite stack.
		blockIncrement = numImages/5; 
		maxvalue = numImages+1; 
		try {
		value = ((value-1) % numImages) + 1; 
		}
		catch(ArithmeticException e) {
		IJ.log("ArithmeticException in getSliceOffset"); 
		}
	}
	else {
		// Slider bounds should reflect full stack size.
		int stackSize = imp.getStack().getSize(); 
		blockIncrement = stackSize/10; 
		maxvalue = stackSize+1; 
	}

	if(blockIncrement<1) blockIncrement = 1; 
	sliceSelector.setBlockIncrement(blockIncrement); 
	sliceSelector.setValues(value,1,1,maxvalue); 
	}


	// --------------------------------------------------
	//
	// Override ij.gui.StackWindow methods.
	//
	// --------------------------------------------------
	/** Displays the specified slice and updates the stack scrollbar. */
	public void showSlice(int index) {
	activeSlice = index; 
	super.showSlice(index); 
	}


	// --------------------------------------------------
	public synchronized void adjustmentValueChanged(AdjustmentEvent e) {
	if(!running) {
		// Get scrollbar value to modify display.
		ScrollbarWithLabel sb = (ScrollbarWithLabel)e.getSource(); 
		activeSlice = sb.getValue(); 
	}

	super.adjustmentValueChanged(e); 
	}

	//
	// End ij.gui.StackWindow methods overrides.
	//
	// --------------------------------------------------

}  // end FVStackWindow
