package ij.plugin;
import ij.*;
import ij.gui.*;
import ij.process.*;
import ij.measure.*;
import ij.util.Tools;
import java.awt.*;
import java.awt.event.*;
import java.util.*;

/** This plugin implements the Edit/Crop and Image/Adjust/Size commands. */
public class Resizer implements PlugIn, TextListener, ItemListener  {
	public static final int IN_PLACE=16, SCALE_T=32;
    private static int newWidth;
    private static int newHeight;
    private static boolean constrain = true;
    private static boolean averageWhenDownsizing = true;
	private static int interpolationMethod = ImageProcessor.BILINEAR;
	private String[] methods = ImageProcessor.getInterpolationMethods();
    private Vector fields, checkboxes;
	private double origWidth, origHeight;
	private boolean sizeToHeight;
 
	public void run(String arg) {
		boolean crop = arg.equals("crop");
		ImagePlus imp = IJ.getImage();
		ImageProcessor ip = imp.getProcessor();
		Roi roi = imp.getRoi();
		if ((roi==null||!roi.isArea()) && crop) {
			IJ.error(crop?"Crop":"Resize", "Area selection required");
			return;
		}
		if (!imp.lock()) {
			IJ.log("<<Resizer: image is locked ("+imp+")>>");
			return;
		}
		Rectangle r = ip.getRoi();
		origWidth = r.width;;
		origHeight = r.height;
		sizeToHeight=false;
	    boolean restoreRoi = crop && roi!=null && roi.getType()!=Roi.RECTANGLE;
		if (roi!=null) {
			Rectangle b = roi.getBounds();
			int w = ip.getWidth();
			int h = ip.getHeight();
			if (b.x<0 || b.y<0 || b.x+b.width>w || b.y+b.height>h) {
				ShapeRoi shape1 = new ShapeRoi(roi);
				ShapeRoi shape2 = new ShapeRoi(new Roi(0, 0, w, h));
				roi = shape2.and(shape1);
				if (roi.getBounds().width==0 || roi.getBounds().height==0)
					throw new IllegalArgumentException("Selection is outside the image");
				if (restoreRoi) imp.setRoi(roi);
			}
		}
		int stackSize= imp.getStackSize();
		int z1 = imp.getStackSize();
		int t1 = 0;
		int z2=0, t2=0;
		int saveMethod = interpolationMethod;
		if (crop) {
			Rectangle bounds = roi.getBounds();
			newWidth = bounds.width;
			newHeight = bounds.height;
			interpolationMethod = ImageProcessor.NONE;
		} else {
			if (newWidth==0 || newHeight==0) {
				newWidth = (int)origWidth/2;
				newHeight = (int)origHeight/2;
			}
			if (constrain) newHeight = (int)(newWidth*(origHeight/origWidth));
			if (stackSize>1) {
				newWidth = (int)origWidth;
				newHeight = (int)origHeight;
			}
			GenericDialog gd = new GenericDialog("Resize");
			gd.addNumericField("Width (pixels):", newWidth, 0);
			gd.addNumericField("Height (pixels):", newHeight, 0);
			if (imp.isHyperStack()) {
				z1 = imp.getNSlices();
				t1 = imp.getNFrames();
			}
			if (z1>1 && z1==stackSize)
				gd.addNumericField("Depth (images):", z1, 0);
			else if (z1>1 && z1<stackSize)
				gd.addNumericField("Depth (slices):", z1, 0);
			if (t1>1)
				gd.addNumericField("Time (frames):", t1, 0);
			gd.addCheckbox("Constrain aspect ratio", constrain);
			gd.addCheckbox("Average when downsizing", averageWhenDownsizing);
			gd.addChoice("Interpolation:", methods, methods[interpolationMethod]);
			fields = gd.getNumericFields();
			if (!IJ.macroRunning()) {
				for (int i=0; i<2; i++)
					((TextField)fields.elementAt(i)).addTextListener(this);
			}
			checkboxes = gd.getCheckboxes();
			if (!IJ.macroRunning())
				((Checkbox)checkboxes.elementAt(0)).addItemListener(this);
			gd.showDialog();
			if (gd.wasCanceled()) {
				imp.unlock();
				return;
			}
			newWidth = (int)gd.getNextNumber();
			newHeight = (int)gd.getNextNumber();
			if (z1>1)
				z2 = (int)gd.getNextNumber();
			if (t1>1)
				t2 = (int)gd.getNextNumber();
			if (gd.invalidNumber()) {
				IJ.error("Width or height are invalid.");
				imp.unlock();
				return;
			}
			constrain = gd.getNextBoolean();
			averageWhenDownsizing = gd.getNextBoolean();
			interpolationMethod = gd.getNextChoiceIndex();
			if (constrain && newWidth==0)
				sizeToHeight = true;
			if (newWidth<=0.0 && !constrain)  newWidth = 50;
			if (newHeight<=0.0) newHeight = 50;
		}
		
		if (!crop && constrain) {
			if (sizeToHeight)
				newWidth = (int)Math.round(newHeight*(origWidth/origHeight));
			else
				newHeight = (int)Math.round(newWidth*(origHeight/origWidth));
		}
		ip.setInterpolationMethod(interpolationMethod);
		if (stackSize==1)
			Undo.setup(crop?Undo.TRANSFORM:Undo.TYPE_CONVERSION, imp);
			    	
		if (roi!=null || newWidth!=origWidth || newHeight!=origHeight) {
			try {
				StackProcessor sp = new StackProcessor(imp.getStack(), ip);
				ImageStack s2 = sp.resize(newWidth, newHeight, averageWhenDownsizing);
				int newSize = s2.getSize();
				if (s2.getWidth()>0 && newSize>0) {
					if (restoreRoi)
						imp.deleteRoi();
					Calibration cal = imp.getCalibration();
					if (cal.scaled()) {
						cal.pixelWidth *= origWidth/newWidth;
						cal.pixelHeight *= origHeight/newHeight;
					}
					if (crop&&roi!=null&&(cal.xOrigin!=0.0||cal.yOrigin!=0.0)) {
						cal.xOrigin -= roi.getBounds().x;
						cal.yOrigin -= roi.getBounds().y;
					}
					imp.setStack(null, s2);
					if (crop && roi!=null) {
						Overlay overlay = imp.getOverlay();
						if (overlay!=null && !imp.getHideOverlay()) {
							Overlay overlay2 = overlay.crop(roi.getBounds());
							imp.setOverlay(overlay2);
						}
					} else {
						Overlay overlay = imp.getOverlay();
						Overlay overlay2 = new Overlay();
						if (overlay!=null && !imp.getHideOverlay()) {
							for (int i=0; i<overlay.size(); i++) {
								Roi roi2 = overlay.get(i);
								Rectangle bounds = roi2.getBounds();
								if (roi2 instanceof ImageRoi && bounds.x==0 && bounds.y==0) {
									ImageRoi iroi = (ImageRoi)roi2;
									ImageProcessor ip2 = iroi.getProcessor();
									ip2.setInterpolationMethod(interpolationMethod);
									ip2 = ip2.resize(newWidth, newHeight, averageWhenDownsizing);
									iroi.setProcessor(ip2);
									overlay2.add(iroi);
								}
							}
							if (overlay2.size()>0)
								imp.setOverlay(overlay2);
							else
								imp.setOverlay(null);
						} else
							imp.setOverlay(null);
					}
					if (restoreRoi && roi!=null) {
						roi.setLocation(0, 0);
						imp.setRoi(roi);
						imp.draw();
					}
				}
				if (stackSize>1 && newSize<stackSize)
					IJ.error("ImageJ ran out of memory causing \nthe last "+(stackSize-newSize)+" slices to be lost.");
			} catch(OutOfMemoryError o) {
				IJ.outOfMemory("Resize");
			}
			imp.changes = true;
			if (crop)
				interpolationMethod = saveMethod;
		}
		
		ImagePlus imp2 = null;
		if (z2>0 && z2!=z1)
			imp2 = zScale(imp, z2, interpolationMethod+IN_PLACE);
		if (t2>0 && t2!=t1)
			imp2 = zScale(imp2!=null?imp2:imp, t2, interpolationMethod+IN_PLACE+SCALE_T);
		imp.unlock();
		if (imp2!=null && imp2!=imp) {
			imp.changes = false;
			imp.close();
			imp2.show();
		}
	}

	public ImagePlus zScale(ImagePlus imp, int newDepth, int interpolationMethod) {
		ImagePlus imp2 = null;
		if (imp.isHyperStack())
			imp2 = zScaleHyperstack(imp, newDepth, interpolationMethod);
		else {
			boolean inPlace = (interpolationMethod&IN_PLACE)!=0;
			interpolationMethod = interpolationMethod&15;
			int stackSize = imp.getStackSize();
			int bitDepth = imp.getBitDepth();
			imp2 = resizeZ(imp, newDepth, interpolationMethod);
			if (imp2==null)
				return null;
			ImageProcessor ip = imp.getProcessor();
			double min = ip.getMin();
			double max = ip.getMax();
			if (bitDepth==16||bitDepth==32)
				imp2.getProcessor().setMinAndMax(min, max);
		}
		if (imp2==null)
			return null;
		if (imp2!=imp && imp.isComposite()) {
			imp2 = new CompositeImage(imp2, ((CompositeImage)imp).getMode());
			((CompositeImage)imp2).copyLuts(imp);
		}
		imp2.setCalibration(imp.getCalibration());
		Calibration cal = imp2.getCalibration();
		if (cal.scaled()) cal.pixelDepth *= (double)imp.getNSlices()/imp2.getNSlices();
		Object info = imp.getProperty("Info");
		if (info!=null) imp2.setProperty("Info", info);
		if (imp.isHyperStack())
			imp2.setOpenAsHyperStack(imp.isHyperStack());
		return imp2;
	}

	private ImagePlus zScaleHyperstack(ImagePlus imp, int depth2, int interpolationMethod) {
		boolean inPlace = (interpolationMethod&IN_PLACE)!=0;
		boolean scaleT = (interpolationMethod&SCALE_T)!=0;
		interpolationMethod = interpolationMethod&15;
		int channels = imp.getNChannels();
		int slices = imp.getNSlices();
		int frames = imp.getNFrames();
		int slices2 = slices;
		int frames2 = frames;
		int bitDepth = imp.getBitDepth();
		if (slices==1 && frames>1)
			scaleT = true;
		if (scaleT)
			frames2 = depth2;
		else
			slices2 = depth2;
		double scale = (double)(depth2-1)/slices;
		if (scaleT) scale = (double)(depth2-1)/frames;
		ImageStack stack1 = imp.getStack();
		int width = stack1.getWidth();
		int height = stack1.getHeight();
		ImagePlus imp2 = IJ.createImage(imp.getTitle(), bitDepth+"-bit", width, height, channels*slices2*frames2);
		if (imp2==null) return null;
		imp2.setDimensions(channels, slices2, frames2);
		ImageStack stack2 = imp2.getStack();
		ImageProcessor ip = imp.getProcessor();
		int count = 0;
		if (scaleT) {
			IJ.showStatus("T Scaling...");
			ImageProcessor xtPlane1 = ip.createProcessor(width, frames);
			xtPlane1.setInterpolationMethod(interpolationMethod);
			ImageProcessor xtPlane2;		
			Object xtpixels1 = xtPlane1.getPixels();
			int last = slices*channels*height-1;
			for (int z=1; z<=slices; z++) {
				for (int c=1; c<=channels; c++) {
					for (int y=0; y<height; y++) {
						IJ.showProgress(count++, last);
						for (int t=1; t<=frames; t++) {
							int index = imp.getStackIndex(c, z, t);
							//IJ.log("1: "+c+" "+z+" "+t+" "+index+" "+xzPlane1);
							Object pixels1 = stack1.getPixels(index);
							System.arraycopy(pixels1, y*width, xtpixels1, (t-1)*width, width);
						}
						xtPlane2 = xtPlane1.resize(width, depth2, averageWhenDownsizing);
						Object xtpixels2 = xtPlane2.getPixels();
						for (int t=1; t<=frames2; t++) {
							int index = imp2.getStackIndex(c, z, t);
							//IJ.log("2: "+c+" "+z+" "+t+" "+index+" "+xzPlane2);
							Object pixels2 = stack2.getPixels(index);
							System.arraycopy(xtpixels2, (t-1)*width, pixels2, y*width, width);
						}
					}
				}
			}
		} else {
			IJ.showStatus("Z Scaling...");
			ImageProcessor xzPlane1 = ip.createProcessor(width, slices);
			xzPlane1.setInterpolationMethod(interpolationMethod);
			ImageProcessor xzPlane2;		
			Object xypixels1 = xzPlane1.getPixels();
			int last = frames*channels*height-1;
			for (int t=1; t<=frames; t++) {
				for (int c=1; c<=channels; c++) {
					for (int y=0; y<height; y++) {
						IJ.showProgress(count++, last);
						for (int z=1; z<=slices; z++) {
							int index = imp.getStackIndex(c, z, t);
							Object pixels1 = stack1.getPixels(index);
							System.arraycopy(pixels1, y*width, xypixels1, (z-1)*width, width);
						}
						xzPlane2 = xzPlane1.resize(width, depth2, averageWhenDownsizing);
						Object xypixels2 = xzPlane2.getPixels();
						for (int z=1; z<=slices2; z++) {
							int index = imp2.getStackIndex(c, z, t);
							Object pixels2 = stack2.getPixels(index);
							System.arraycopy(xypixels2, (z-1)*width, pixels2, y*width, width);
						}
					}
				}
			}
		}
		imp2.setDimensions(channels, slices2, frames2);
		return imp2;
	}

	private ImagePlus resizeZ(ImagePlus imp, int newDepth, int interpolationMethod) {
		ImageStack stack1 = imp.getStack();
		int width = stack1.getWidth();
		int height = stack1.getHeight();
		int depth = stack1.getSize();
		int bitDepth = imp.getBitDepth();
		ImagePlus imp2 = IJ.createImage(imp.getTitle(), bitDepth+"-bit", width, height, newDepth);
		if (imp2==null) return null;
		ImageStack stack2 = imp2.getStack();
		ImageProcessor ip = imp.getProcessor();
		ImageProcessor xzPlane1 = ip.createProcessor(width, depth);
		xzPlane1.setInterpolationMethod(interpolationMethod);
		ImageProcessor xzPlane2;		
		Object xzpixels1 = xzPlane1.getPixels();
		IJ.showStatus("Z Scaling...");
		for (int y=0; y<height; y++) {
			IJ.showProgress(y, height-1);
			for (int z=0; z<depth; z++) { // get xz plane at y
				Object pixels1 = stack1.getPixels(z+1);
				System.arraycopy(pixels1, y*width, xzpixels1, z*width, width);
			}
			xzPlane2 = xzPlane1.resize(width, newDepth, averageWhenDownsizing);
			Object xypixels2 = xzPlane2.getPixels();
			for (int z=0; z<newDepth; z++) {
				Object pixels2 = stack2.getPixels(z+1);
				System.arraycopy(xypixels2, z*width, pixels2, y*width, width);
			}
		}
		return imp2;
	}

    public void textValueChanged(TextEvent e) {
    	TextField widthField = (TextField)fields.elementAt(0);
    	TextField heightField = (TextField)fields.elementAt(1);
        int width = (int)Tools.parseDouble(widthField.getText(),-99);
        int height = (int)Tools.parseDouble(heightField.getText(),-99);
        if (width==-99 || height==-99)
        	return;
        if (constrain) {
        	if (width!=newWidth) {
         		sizeToHeight = false;
        		newWidth = width;
				updateFields();
         	} else if (height!=newHeight) {
         		sizeToHeight = true;
        		newHeight = height;
				updateFields();
			}
        }
    }
    
    void updateFields() {
		if (sizeToHeight) {
			newWidth = (int)Math.round(newHeight*(origWidth/origHeight));
			TextField widthField = (TextField)fields.elementAt(0);
			widthField.setText(""+newWidth);
		} else {
			newHeight = (int)Math.round(newWidth*(origHeight/origWidth));
			TextField heightField = (TextField)fields.elementAt(1);
			heightField.setText(""+newHeight);
		}
   }

	public void itemStateChanged(ItemEvent e) {
		Checkbox cb = (Checkbox)checkboxes.elementAt(0);
        boolean newConstrain = cb.getState();
        if (newConstrain && newConstrain!=constrain)
        	updateFields();
        constrain = newConstrain;
	}
	
	public void setAverageWhenDownsizing(boolean averageWhenDownsizing) {
		this. averageWhenDownsizing = averageWhenDownsizing;
	}

}
