package ij.plugin;
import ij.*;
import ij.process.*;
import ij.gui.*;
import ij.io.*;
import java.awt.*;
import java.io.*;
import java.util.Properties;

/** This plugin opens a multi-page TIFF file, or a set of raw images, as a 
	virtual stack. It implements the File/Import/TIFF Virtual Stack command. */
public class FileInfoVirtualStack extends VirtualStack implements PlugIn {
	private FileInfo[] info;
	private int nImages;
	
	/* Default constructor. */
	public FileInfoVirtualStack() {}

	/* Constructs a FileInfoVirtualStack from a FileInfo object. */
	public FileInfoVirtualStack(FileInfo fi) {
		info = new FileInfo[1];
		info[0] = fi;
		ImagePlus imp = open();
		if (imp!=null)
			imp.show();
	}

	/* Constructs a FileInfoVirtualStack from a FileInfo 
		object and displays it if 'show' is true. */
	public FileInfoVirtualStack(FileInfo fi, boolean show) {
		info = new FileInfo[1];
		info[0] = fi;
		ImagePlus imp = open();
		if (imp!=null && show)
			imp.show();
	}
	
	/* Constructs a FileInfoVirtualStack from an array of FileInfo objects. */
	public FileInfoVirtualStack(FileInfo[] fi) {
		info = fi;
		nImages = info.length;
	}

	/** Opens the specified tiff file as a virtual stack. */
	public static ImagePlus openVirtual(String path) {
		OpenDialog  od = new OpenDialog("Open TIFF", path);
		String name = od.getFileName();
		String  dir = od.getDirectory();
		if (name==null)
			return null;
		FileInfoVirtualStack stack = new FileInfoVirtualStack();
		stack.init(dir, name);
		if (stack.info==null)
			return null;
		else
			return stack.open();
	}

	public void run(String arg) {
		OpenDialog  od = new OpenDialog("Open TIFF", arg);
		String name = od.getFileName();
		String  dir = od.getDirectory();
		if (name==null)
			return;
		init(dir, name);
		if (info==null)
			return;
		ImagePlus imp = open();
		if (imp!=null)
			imp.show();
	}
	
	private void init(String dir, String name) {
		if (name.endsWith(".zip")) {
			IJ.error("Virtual Stack", "ZIP compressed stacks not supported");
			return;
		}
		TiffDecoder td = new TiffDecoder(dir, name);
		if (IJ.debugMode) td.enableDebugging();
		IJ.showStatus("Decoding TIFF header...");
		try {
			info = td.getTiffInfo();
		} catch (IOException e) {
			// try opening using bio-formats 
			dir = IJ.addSeparator(dir);
			IJ.open(dir+name);
			return;
		}
		if (info==null || info.length==0) {
			IJ.error("Virtual Stack", "This does not appear to be a TIFF stack");
			return;
		}
		if (IJ.debugMode)
			IJ.log(info[0].debugInfo);
	}
		
	private ImagePlus open() {
		FileInfo fi = info[0];
		int n = fi.nImages;
		if (info.length==1 && n>1) {
			long bytesPerImage = fi.width*fi.height*fi.getBytesPerPixel();
			if (fi.fileType==FileInfo.GRAY12_UNSIGNED)
				bytesPerImage = (int)(1.5*fi.width)*fi.height;
			else if (fi.fileType==FileInfo.GRAY10_UNSIGNED)
				bytesPerImage = (int)(1.25*fi.width)*fi.height;
			n = validateNImages(fi, bytesPerImage);
			info = new FileInfo[n];
			for (int i=0; i<n; i++) {
				info[i] = (FileInfo)fi.clone();
				info[i].nImages = 1;
				info[i].longOffset = fi.getOffset() + i*(bytesPerImage + fi.getGap());
			}
		}
		nImages = info.length;
		FileOpener fo = new FileOpener(info[0]);
		ImagePlus imp = fo.openImage();
		if (nImages==1 && fi.fileType==FileInfo.RGB48)
			return imp;
		Properties props = fo.decodeDescriptionString(fi);
		ImagePlus imp2 = new ImagePlus(fi.fileName, this);
		imp2.setDisplayRange(imp.getDisplayRangeMin(),imp.getDisplayRangeMax());
		imp2.setFileInfo(fi);
		if (imp!=null) {
			setBitDepth(imp.getBitDepth());
			imp2.setCalibration(imp.getCalibration());
			imp2.setOverlay(imp.getOverlay());
			if (fi.info!=null)
				imp2.setProperty("Info", fi.info);
			if (props!=null) {
				int channels = getInt(props,"channels");
				int slices = getInt(props,"slices");
				int frames = getInt(props,"frames");
				if (channels*slices*frames==nImages) {
					imp2.setDimensions(channels, slices, frames);
					if (getBoolean(props, "hyperstack"))
						imp2.setOpenAsHyperStack(true);
				}
				if (channels>1 && fi.description!=null) {
					int mode = IJ.COMPOSITE;
					if (fi.description.indexOf("mode=color")!=-1)
						mode = IJ.COLOR;
					else if (fi.description.indexOf("mode=gray")!=-1)
						mode = IJ.GRAYSCALE;
					imp2 = new CompositeImage(imp2, mode);
				}
			}
		}
		return imp2;
	}
	
	private int validateNImages(FileInfo fi, long bytesPerImage) {
		File f = new File(fi.getFilePath());
		if (!f.exists())
			return fi.nImages;
		long fileLength = f.length();
		for (int i=fi.nImages-1; i>=0; i--) {
			long offset =  fi.getOffset() + i*(bytesPerImage+fi.getGap());
			if (offset+bytesPerImage<=fileLength)
				return i+1;
		}
		return fi.nImages;
	}

	int getInt(Properties props, String key) {
		Double n = getNumber(props, key);
		return n!=null?(int)n.doubleValue():1;
	}

	Double getNumber(Properties props, String key) {
		String s = props.getProperty(key);
		if (s!=null) {
			try {
				return Double.valueOf(s);
			} catch (NumberFormatException e) {}
		}	
		return null;
	}

	boolean getBoolean(Properties props, String key) {
		String s = props.getProperty(key);
		return s!=null&&s.equals("true")?true:false;
	}

	/** Deletes the specified image, where {@literal 1<=n<=nImages}. */
	public void deleteSlice(int n) {
		if (n<1 || n>nImages)
			throw new IllegalArgumentException("Argument out of range: "+n);
		if (nImages<1) return;
		for (int i=n; i<nImages; i++)
			info[i-1] = info[i];
		info[nImages-1] = null;
		nImages--;
	}
	
	/** Returns an ImageProcessor for the specified image,
		where {@literal 1<=n<=nImages}. Returns null if the stack is empty.
	*/
	public ImageProcessor getProcessor(int n) {
		n = translate(n);  // update n for hyperstacks not in default CZT order
		if (n<1 || n>nImages)
			throw new IllegalArgumentException("Argument out of range: "+n);
		//if (n>1) IJ.log("  "+(info[n-1].getOffset()-info[n-2].getOffset()));
		info[n-1].nImages = 1; // why is this needed?
		ImageProcessor ip = null;
		if (IJ.debugMode) {
			long t0 = System.currentTimeMillis();
			FileOpener fo = new FileOpener(info[n-1]);
			ip = fo.openProcessor();
			IJ.log("FileInfoVirtualStack: "+n+", offset="+info[n-1].getOffset()+", "+(System.currentTimeMillis()-t0)+"ms");
		} else {
			FileOpener fo = new FileOpener(info[n-1]);
			if (info[n-1].fileType==FileInfo.RGB48) {
				ImagePlus imp = fo.openImage();
				if (info[n-1].sliceNumber>0)
					imp.setSlice(info[n-1].sliceNumber);
				ip = imp.getProcessor();
			} else
				ip = fo.openProcessor();
		}
		if (ip!=null) {
			if (cTable!=null)
				ip.setCalibrationTable(cTable);
			ip.setSliceNumber(n);
			return ip;
		} else {
			int w=getWidth(), h=getHeight();
			IJ.log("Read error or file not found ("+n+"): "+info[n-1].directory+info[n-1].fileName);
			switch (getBitDepth()) {
				case 8: return new ByteProcessor(w, h);
				case 16: return new ShortProcessor(w, h);
				case 24: return new ColorProcessor(w, h);
				case 32: return new FloatProcessor(w, h);
				default: return null;
			}
		}
	 }
 
	/** Returns the number of slices in this stack. */
	public int size() {
		return getSize();
	}

	public int getSize() {
		return nImages;
	}

	/** Returns the label of the Nth image. */
	public String getSliceLabel(int n) {
		if (n<1 || n>nImages)
			throw new IllegalArgumentException("Argument out of range: "+n);
		if (info[0].sliceLabels==null || info[0].sliceLabels.length!=nImages)
			return null;
		else
			return info[0].sliceLabels[n-1];
	}

	public int getWidth() {
		return info[0].width;
	}
	
	public int getHeight() {
		return info[0].height;
	}
	
	/** Adds an image to this stack. */
	public synchronized  void addImage(FileInfo fileInfo) {
		nImages++;
		if (info==null)
			info = new FileInfo[250];
		if (nImages==info.length) {
			FileInfo[] tmp = new FileInfo[nImages*2];
			System.arraycopy(info, 0, tmp, 0, nImages);
			info = tmp;
		}
		info[nImages-1] = fileInfo;
	}
	
	@Override
	public String getDirectory() {
		if (info!=null && info.length>0)
			return info[0].directory;
		else
			return null;
	}
		
	@Override
	public String getFileName(int n) {
		int index = n - 1;
		if (index>=0 && info!=null && info.length>index)
			return info[index].fileName;
		else
			return null;
	}

		
}
