/*
 * #%L
 * Fiji distribution of ImageJ for the life sciences.
 * %%
 * Copyright (C) 2007 - 2017 Fiji
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 2 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-2.0.html>.
 * #L%
 */
import static stitching.CommonFunctions.RED_CYAN;
import static stitching.CommonFunctions.addHyperLinkListener;
import static stitching.CommonFunctions.colorList;
import static stitching.CommonFunctions.computeFFT;
import static stitching.CommonFunctions.computePhaseCorrelationMatrix;
import static stitching.CommonFunctions.getPixelValueRGB;
import static stitching.CommonFunctions.methodList;
import static stitching.CommonFunctions.quicksort;
import static stitching.CommonFunctions.startTask;
import static stitching.CommonFunctions.zeroPadImages;
/**
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License 2
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 * 
 * An execption is the FFT implementation of Dave Hale which we use as a library,
 * wich is released under the terms of the Common Public License - v1.0, which is 
 * available at http://www.eclipse.org/legal/cpl-v10.html  
 *
 * @author Stephan Preibisch
 */import ij.IJ;
import ij.ImagePlus;
import ij.WindowManager;
import ij.gui.GenericDialog;
import ij.gui.MultiLineLabel;
import ij.gui.Roi;
import ij.plugin.PlugIn;
import ij.process.ImageProcessor;

import java.awt.Choice;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicInteger;

import stitching.CrossCorrelationResult2D;
import stitching.FloatArray2D;
import stitching.ImageInformation;
import stitching.Point2D;
import stitching.utils.Log;

public class Stitching_2D implements PlugIn
{
	private String myURL = "http://fly.mpi-cbg.de/~preibisch/contact.html";
	public String image1, image2, fusedImageName;
	
	public static String methodStatic = methodList[1];
	public static String handleRGB1Static = colorList[colorList.length - 1];
	public static String handleRGB2Static = colorList[colorList.length - 1];
	public static boolean fuseImagesStatic = true, windowingStatic = true, computeOverlapStatic = true;
	public static int checkPeaksStatic = 5;
	public static double alphaStatic = 1.5;
	public static int xOffsetStatic = 0, yOffsetStatic = 0;
	
	public String method = methodList[1];
	public String handleRGB1= colorList[colorList.length - 1];
	public String handleRGB2= colorList[colorList.length - 1];
	public boolean fuseImages= true, windowing = true, computeOverlap = true;
	public int checkPeaks= 5;
	public double alpha= 1.5;
	public int xOffset = 0, yOffset = 0;
	
	public boolean doLogging = true;
	public ImagePlus imp1 = null, imp2 = null;
	
	public Point2D shift = null;	
	private CrossCorrelationResult2D[] result = null;
	
	public Point2D getTranslation() { return shift.clone(); }
	public CrossCorrelationResult2D[] getCrossCorrelationResults() { return result; }
	public CrossCorrelationResult2D getCrossCorrelationResult() { return result[0].clone(); }
	

	// a macro can call to only fuse two images given a certain shift
	private Point2D translation = null;

	@Override
	public void run(String args)
	{
		// get list of image stacks
		int[] idList = WindowManager.getIDList();

		if (idList == null)
		{
			IJ.error("You need two open images.");
			return;
		}

		int nostacks = 0;
		for (int i = 0; i < idList.length; i++)
			if (WindowManager.getImage(idList[i]).getStackSize() == 1)
				nostacks++;

		if (nostacks < 2)
		{
			IJ.error("You need two open images (no stacks).");
			return;
		}

		String[] nostackList = new String[nostacks];
		int[] nostackIDs = new int[nostacks];
		nostacks = 0;

		for (int i = 0; i < idList.length; i++)
		{
			if (WindowManager.getImage(idList[i]).getStackSize() == 1)
			{
				nostackList[nostacks] = WindowManager.getImage(idList[i]).getTitle();
				nostackIDs[nostacks] = idList[i];
				++nostacks;
			}
		}
		
		// create generic dialog
		GenericDialog gd = new GenericDialog("Stitching of 2D Images");
		gd.addChoice("First_image (reference)", nostackList, nostackList[0]);
		gd.addChoice("Use_Channel_for_First", colorList, handleRGB1Static);
		enableChannelChoice((Choice) gd.getChoices().get(0), (Choice) gd.getChoices().get(1), nostackIDs);

		gd.addChoice("Second_image (to register)", nostackList, nostackList[1]);
		gd.addChoice("Use_Channel_for_Second", colorList, handleRGB2Static);
		enableChannelChoice((Choice) gd.getChoices().get(2), (Choice) gd.getChoices().get(3), nostackIDs);

		gd.addCheckbox("Use_windowing", windowingStatic);
		gd.addNumericField("How_many_peaks should be checked", checkPeaksStatic, 0);
		gd.addMessage("");
		gd.addCheckbox("Create_merged_image (fusion)", fuseImagesStatic);
		gd.addChoice("Fusion_method", methodList, methodStatic);
		gd.addNumericField("Fusion_alpha", alphaStatic, 2);		
		gd.addStringField("Fused_image name: ", "Fused_" + nostackList[0] + "_" + nostackList[1]);
		gd.addCheckbox("compute_overlap", computeOverlapStatic );
		gd.addNumericField("x", xOffset, xOffsetStatic);
		gd.addNumericField("y", yOffset, yOffsetStatic);
		gd.addMessage("");
		gd.addMessage("This Plugin is developed by Stephan Preibisch\n" + myURL);

		MultiLineLabel text = (MultiLineLabel) gd.getMessage();
		addHyperLinkListener(text, myURL);
		
		gd.showDialog();

		if (gd.wasCanceled())
			return;

		this.image1 = gd.getNextChoice();
		handleRGB1Static = gd.getNextChoice();
		this.image2 = gd.getNextChoice();
		handleRGB2Static = gd.getNextChoice();
		this.imp1 = WindowManager.getImage(nostackIDs[((Choice) gd.getChoices().get(0)).getSelectedIndex()]);
		this.imp2 = WindowManager.getImage(nostackIDs[((Choice) gd.getChoices().get(2)).getSelectedIndex()]);
		windowingStatic = gd.getNextBoolean();
		checkPeaksStatic = (int) gd.getNextNumber();
		fuseImagesStatic = gd.getNextBoolean();
		methodStatic = gd.getNextChoice();
		alphaStatic = gd.getNextNumber();
		this.fusedImageName = gd.getNextString();
		computeOverlapStatic = gd.getNextBoolean();
		xOffsetStatic = (int)Math.round( gd.getNextNumber() );
		yOffsetStatic = (int)Math.round( gd.getNextNumber() );

		method = methodStatic;
		handleRGB1= handleRGB1Static;
		handleRGB2= handleRGB2Static;
		fuseImages = fuseImagesStatic;
		windowing = windowingStatic;
		checkPeaks = checkPeaksStatic;
		alpha = alphaStatic;
		computeOverlap = computeOverlapStatic;
		xOffset = xOffsetStatic;
		yOffset = yOffsetStatic;
		
		
		if ( !computeOverlap )
			this.translation = new Point2D( xOffset, yOffset );
		else
			this.translation = null;
		
		//
		// determine wheater a macro called it which limits in determining name
		// clashes
		//
		boolean calledFromMacro = false;

		if (!(imp1.getTitle().equals(image1) && imp2.getTitle().equals(image2)))
		{
			calledFromMacro = true;
			imp1 = WindowManager.getImage(image1);
			imp2 = WindowManager.getImage(image2);
		}

		if (!calledFromMacro && nostackIDs[((Choice) gd.getChoices().get(0)).getSelectedIndex()] == nostackIDs[((Choice) gd.getChoices().get(2)).getSelectedIndex()])
		{
			IJ.error("You selected the same stack twice. Stopping.");
			return;
		}

		if (fuseImages)
		{
			if (imp1.getType() != imp2.getType())
			{
				IJ.error("The Image Stacks are of a different type, it is unclear how to fuse them. Stopping.");
				return;
			}

			if ((imp1.getType() == ImagePlus.COLOR_RGB || imp1.getType() == ImagePlus.COLOR_256) && method.equals(methodList[RED_CYAN]))
			{
				Log.warn("Red-Cyan Overlay is not possible for RGB images, reducing images to Single Channel data.");
			}
		}
		
		work();
	}

	private final void enableChannelChoice(final Choice controller, final Choice target, final int[] nostackIDs)
	{
		setRGB(nostackIDs[controller.getSelectedIndex()], target);

		controller.addItemListener(new ItemListener()
		{
			@Override
			public void itemStateChanged(ItemEvent ie)
			{
				setRGB(nostackIDs[controller.getSelectedIndex()], target);
			}
		});
	}

	private final void setRGB(final int imagejID, final Choice target)
	{
		if (WindowManager.getImage(imagejID).getType() == ImagePlus.COLOR_RGB || WindowManager.getImage(imagejID).getType() == ImagePlus.COLOR_256)
			target.setEnabled(true);
		else		
			target.setEnabled(false);
	}
	
	private ImagePlus getImage(String img)
	{
		ImagePlus imp = null;

		try
		{
			imp = WindowManager.getImage(img);
		} catch (Exception e)
		{
			IJ.error("Could not find image: " + e);
		}

		return imp;
	}

	public Stitching_2D()
	{
	}

	public Stitching_2D(boolean windowing1, int checkPeaks1, boolean fuseImages1, String method1, String fusedImageName1)
	{
		windowing = windowing1;
		fuseImages = fuseImages1;
		method = method1;
		fusedImageName = fusedImageName1;
		checkPeaks = checkPeaks1;
	}

	public void work()
	{
		work(null, null);
	}

	public void work(FloatArray2D inputImage1, FloatArray2D inputImage2)
	{
		FloatArray2D img1 = null, img2 = null, fft1, fft2;
		ImagePlus imp1, imp2;

		Point2D img1Dim = new Point2D(0, 0), img2Dim = new Point2D(0, 0), // (incl. possible ext!!!)
				ext1Dim = new Point2D(0, 0), ext2Dim = new Point2D(0, 0),
				maxDim;

		// make it also executable as non-plugin/macro
		if (inputImage1 == null || inputImage2 == null)
		{
			// get images
			if (this.imp1 == null) imp1 = getImage(image1);
			else imp1 = this.imp1;

			if (this.imp2 == null) imp2 = getImage(image2);
			else imp2 = this.imp2;

			if (imp1 == null || imp2 == null)
				return;

			// check for ROIs in images and whether they are valid
			if (!checkRoi(imp1, imp2))
				return;

			// apply ROIs if they are there and save dimensions of original images and size increases
			// images and size increases
			if (this.translation == null)
			{
				img1 = applyROI(imp1, img1Dim, ext1Dim, handleRGB1, windowing);
				img2 = applyROI(imp2, img2Dim, ext2Dim, handleRGB2, windowing);
			}
		}
		else
		{
			img1 = inputImage1;
			img2 = inputImage2;

			imp1 = FloatArrayToImagePlus(img1, "Image1", 0, 0);
			imp2 = FloatArrayToImagePlus(img2, "Image2", 0, 0);

			img1Dim.x = img1.width;
			img1Dim.y = img1.height;
			img2Dim.x = img2.width;
			img2Dim.y = img2.height;
		}

		if (this.translation == null)
		{

			// apply windowing
			if (windowing)
			{
				exponentialWindow(img1);
				exponentialWindow(img2);
			}
	
			// zero pad images to fft-able size
			FloatArray2D[] zeropadded = zeroPadImages(img1, img2);
			img1 = zeropadded[0];
			img2 = zeropadded[1];
	
			/*
			   FloatArrayToImagePlus(img1, "1", 0, 0).show();
			   FloatArrayToImagePlus(img2, "2", 0, 0).show();
			 */
	
			// save dimensions of zeropadded image
			maxDim = new Point2D(img1.width, img1.height);
	
			// compute FFT's
			fft1 = computeFFT(img1);
			fft2 = computeFFT(img2);
	
			// do the phase correlation
			FloatArray2D invPCM = computePhaseCorrelationMatrix(fft1, fft2, maxDim.x);
			//FloatArrayToImagePlus(invPCM, "invpcm", 0, 0).show();
	
			// find the peaks
			ArrayList<Point2D> peaks = findPeaks(invPCM, img1Dim, img2Dim, ext1Dim, ext2Dim, checkPeaks);
	
			// get the original images
			img1 = applyROI(imp1, img1Dim, ext1Dim, handleRGB1, false /*no windowing of course*/);
			img2 = applyROI(imp2, img2Dim, ext2Dim, handleRGB2, false /*no windowing of course*/);
	
			// test peaks
			result = testCrossCorrelation(invPCM, peaks, img1, img2);
	
			// get shift of images relative to each other
			shift = getImageOffset(result[0], imp1, imp2);
		}
		else
		{
			shift = translation;
		}
		
		if (fuseImages)
		{
			// merge if wanted
			ImagePlus fused = fuseImages(imp1, imp2, shift, method, fusedImageName);
			fused.show();
		}

		if (doLogging)
		{
			Log.info("Translation Parameters:");
			Log.info("(second stack relative to first stack)");
			
			if ( this.translation == null )
				Log.info("x= " + shift.x + " y= " + shift.y + " R= " + result[0].R);
			else
				Log.info("x= " + shift.x + " y= " + shift.y);
		}
	}

	private Point2D getImageOffset(CrossCorrelationResult2D result, ImagePlus imp1, ImagePlus imp2)
	{
		// see "ROI shift to Image Shift.ppt" for details of nomenclature
		Point2D r1, r2, sr, sir, si;

		// relative shift between rois (all shifts relative to upper left front corner)
		sr = result.shift;

		if (imp1.getRoi() == null || imp2.getRoi() == null)
		{
			// there are no rois....so we already have the relative shift between the images
			si = sr;
		}
		else
		{
			Roi roi1 = imp1.getRoi();
			Roi roi2 = imp2.getRoi();

			int x1 = roi1.getBoundingRect().x;
			int y1 = roi1.getBoundingRect().y;
			int x2 = roi2.getBoundingRect().x;
			int y2 = roi2.getBoundingRect().y;

			r1 = new Point2D(x1, y1);
			r2 = new Point2D(x2, y2);

			sir = add(r1, sr);
			si = subtract(sir, r2);
		}

		return si;
	}

	private Point2D subtract(Point2D a, Point2D b)
	{
		return new Point2D(a.x - b.x, a.y - b.y);
	}

	private Point2D add(Point2D a, Point2D b)
	{
		return new Point2D(a.x + b.x, a.y + b.y);
	}

	/*private float getPixelMin(ImageProcessor ip, int width, int height, int x, int y, double min)
	{
		if (x < 0 || y < 0 || x >= width || y >= height)
			return (float) min;

		if (ip.getPixels() instanceof byte[])
		{
			byte[] pixelTmp = (byte[]) ip.getPixels();
			return (float) (pixelTmp[x + y * width] & 0xff);
		}
		else if (ip.getPixels() instanceof short[])
		{
			short[] pixelTmp = (short[]) ip.getPixels();
			return (float) (pixelTmp[x + y * width] & 0xffff);
		}
		else // instance of float[]
		{
			float[] pixelTmp = (float[]) ip.getPixels();
			return pixelTmp[x + y * width];
		}

	}*/

	private ImagePlus fuseImages(final ImagePlus imp1, final ImagePlus imp2, Point2D shift, final String fusionMethod, final String name)
	{		
		final int dim = 2;
		
		ImageInformation i1 = new ImageInformation(dim, 1, null);
		i1.closeAtEnd = false;
		i1.imp = imp1;
		i1.size[0] = imp1.getWidth();
		i1.size[1] = imp1.getHeight();
		i1.position = new float[2];
		i1.position[0] = 0;
		i1.position[1] = 0;
		i1.imageName = imp1.getTitle();
		i1.invalid = false;
		i1.imageType = imp1.getType();

		ImageInformation i2 = new ImageInformation(dim, 2, null);
		i2.closeAtEnd = false;
		i2.imp = imp2;
		i2.size[0] = imp2.getWidth();
		i2.size[1] = imp2.getHeight();
		i2.position = new float[2];
		i2.position[0] = shift.x;
		i2.position[1] = shift.y;
		i2.imageName = imp2.getTitle();
		i2.invalid = false;
		i2.imageType = imp2.getType();

		ArrayList<ImageInformation> imageInformationList = new ArrayList<ImageInformation>();
		imageInformationList.add(i1);
		imageInformationList.add(i2);
		
		final float[] max = Stitch_Image_Collection.getAndApplyMinMax(imageInformationList, dim);
		return Stitch_Image_Collection.fuseImages(imageInformationList, max, name, fusionMethod, "rgb", dim, alpha, true );
	}

	/*private ImagePlus fuseImages(ImagePlus imp1, ImagePlus imp2, Point2D shift, String method, String name)
	{
		int type = 0;

		if (method.equals("Average"))
			type = AVG;
		else if (method.equals("Linear Blending"))
			type = LIN_BLEND;
		else if (method.equals("Max. Intensity"))
			type = MAX;
		else if (method.equals("Min. Intensity"))
			type = MIN;
		else if (method.equals("Red-Cyan Overlay"))
			type = RED_CYAN;
		
		ImageProcessor ip1 = imp1.getProcessor();
		int w1 = ip1.getWidth();
		int h1 = ip1.getHeight();

		ImageProcessor ip2 = imp2.getProcessor();
		int w2 = ip2.getWidth();
		int h2 = ip2.getHeight();

		int sx = shift.x;
		int sy = shift.y;

		int imgW, imgH;

		if (sx >= 0)
			imgW = Math.max(w1, w2 + sx);
		else
			imgW = Math.max(w1 - sx, w2); // equals max(w1 + Math.abs(sx), w2);

		if (sy >= 0)
			imgH = Math.max(h1, h2 + sy);
		else
			imgH = Math.max(h1 - sy, h2);

		int offsetImg1X = Math.max(0, -sx); // + max(0, max(0, -sx) - max(0, (w1 - w2)/2));
		int offsetImg1Y = Math.max(0, -sy); // + max(0, max(0, -sy) - max(0, (h1 - h2)/2));
		int offsetImg2X = Math.max(0, sx); // + max(0, max(0, sx) - max(0, (w2 - w1)/2));
		int offsetImg2Y = Math.max(0, sy); // + max(0, max(0, sy) - max(0, (h2 - h1)/2));

		// get size of min enclosing rectangle
		Point2D[] result = getCommonRectangle(new ImagePlus[]{imp1, imp2}, new Point2D[]{shift}, true);
		Point2D size = result[1];

		// now compute the beginning of the enclosing rectangle relative to the big resulting image
		Point2D offset = new Point2D(Math.max(offsetImg2X, offsetImg1X), Math.max(offsetImg2Y, offsetImg1Y));
		
		final int up = 0;
		final int dn = 1;
		final int img1 = 0;
		final int img2 = 1;		
		
		// get the weightening directions
		double xw[][] = new double[2][2]; // [img1, img2][up, down]
		double yw[][] = new double[2][2]; // [img1, img2][up, down]
			
		if (type == LIN_BLEND)
		{
			if (offsetImg1X == offsetImg2X)
			{
				xw[img1][up] = xw[img2][up] = 0;
			}
			else if (offsetImg1X < offsetImg2X)
			{
				xw[img1][up] = 1;
				xw[img2][up] = 0;
			}
			else
			{
				xw[img1][up] = 0;
				xw[img2][up] = 1;
			}
	
			if (offsetImg1Y == offsetImg2Y)
			{
				yw[img1][up] = yw[img2][up] = 0;
			}
			else if (offsetImg1Y < offsetImg2Y)
			{
				yw[img1][up] = 1;
				yw[img2][up] = 0;
			}
			else
			{
				yw[img1][up] = 0;
				yw[img2][up] = 1;
			}
	
			if (offsetImg1X + w1 == offsetImg2X + w2)
			{
				xw[img1][dn] = xw[img2][dn] = 0;
			}
			else if (offsetImg1X + w1 > offsetImg2X + w2)
			{
				xw[img1][dn] = 1;
				xw[img2][dn] = 0;
			}
			else
			{
				xw[img1][dn] = 0;
				xw[img2][dn] = 1;
			}
	
			if (offsetImg1Y + h1 == offsetImg2Y + h2)
			{
				yw[img1][dn] = yw[img2][dn] = 0;
			}
			else if (offsetImg1Y + h1 > offsetImg2Y + h2)
			{
				yw[img1][dn] = 1;
				yw[img2][dn] = 0;
			}
			else
			{
				yw[img1][dn] = 0;
				yw[img2][dn] = 1;
			}
		}
				
		FloatArray2D fused = null;
		ImagePlus fusedImp = null;
		ImageProcessor fusedIp = null;
		float pixel1, pixel2;

		double min1 = ip1.getMin();
		double max1 = ip1.getMax();
		double min2 = ip2.getMin();
		double max2 = ip2.getMax();

		if (type == RED_CYAN)
		{
			fusedImp = IJ.createImage(name, "RGB", imgW, imgH, 1);
			fusedIp = fusedImp.getProcessor();
		}
		else
		{
			fused = new FloatArray2D(imgW, imgH);
		}

		float min = Float.MAX_VALUE;
		float max = Float.MIN_VALUE;
		float pixel3 = 0;
		int[] iArray = new int[3];

		int count = 0;

		for (int y = 0; y < imgH; y++)
			for (int x = 0; x < imgW; x++)
			{
				pixel1 = getPixelMin(ip1, w1, h1, x - offsetImg1X, y - offsetImg1Y, min1);
				pixel2 = getPixelMin(ip2, w2, h2, x - offsetImg2X, y - offsetImg2Y, min2);

				if (type != RED_CYAN)
				{
					// combine images if overlapping
					if (x >= offsetImg1X && x >= offsetImg2X &&
						x < offsetImg1X + w1 && x < offsetImg2X + w2 &&
						y >= offsetImg1Y && y >= offsetImg2Y &&
						y < offsetImg1Y + h1 && y < offsetImg2Y + h2)
					{
						// compute w1(weight Image1) and w2(weight image2)
						
						// first we need the relative positions in the enclosing rectangle
						double weight1 = 0.5;
						double weight2 = 0.5; 
						
						if (type == LIN_BLEND)
						{
							final double px = (x - offset.x)/(double)(size.x-1);
							final double py = (y - offset.y)/(double)(size.y-1);
							
							final double wx1, wx2, wy1, wy2;
							
							if (px < 0.5)
							{
								wx1 = 0.5 * 2*px + xw[img1][up] * (1 - 2*px);
								wx2 = 0.5 * 2*px + xw[img2][up] * (1 - 2*px);
							}
							else if (px > 0.5)
							{
								wx1 = 0.5 * (1 - 2*(px-0.5)) + xw[img1][dn] * 2*(px-0.5);
								wx2 = 0.5 * (1 - 2*(px-0.5)) + xw[img2][dn] * 2*(px-0.5);
							}
							else
							{
								wx1 = wx2 = 0.5;
							}
	
							if (py < 0.5)
							{
								wy1 = 0.5 * 2*py + yw[img1][up] * (1 - 2*py);
								wy2 = 0.5 * 2*py + yw[img2][up] * (1 - 2*py);
							}
							else if (py > 0.5)
							{
								wy1 = 0.5 * (1 - 2*(py-0.5)) + yw[img1][dn] * 2*(py-0.5);
								wy2 = 0.5 * (1 - 2*(py-0.5)) + yw[img2][dn] * 2*(py-0.5);
							}
							else
							{
								wy1 = wy2 = 0.5;
							}
														
							weight1 = wx1 * wy1;
							weight2 = wx2 * wy2;
							
							if (weight1 == 0 && weight2 == 0)
							{
								weight1 = weight2 = 0.5;
							}
							else
							{
								double norm = weight1 + weight2; 
								weight1 /= norm;
								weight2 /= norm;
							}
						}
												
						pixel3 = 0;
						if (type == AVG)
							pixel3 = (pixel1 + pixel2) / 2f;
						else if (type == LIN_BLEND)
							pixel3 = (float)(pixel1*weight1 + pixel2*weight2);							
						else if (type == MAX)
							pixel3 = Math.max(pixel1, pixel2);
						else if (type == MIN)
							pixel3 = Math.min(pixel1, pixel2);
					}
					else
					{
						pixel3 = Math.max(pixel1, pixel2);

					}

					fused.data[count++] = pixel3;

					if (pixel3 < min)
						min = pixel3;
					else if (pixel3 > max)
						max = pixel3;
				}
				else
				{
					iArray[0] = (int) (((pixel1 - min1) / (max1 - min1)) * 255D);
					iArray[1] = iArray[2] = (int) (((pixel2 - min2) / (max2 - min2)) * 255D);

					fusedIp.putPixel(x, y, iArray);
				}
			}

		if (type != RED_CYAN)
		{
			fusedImp = FloatArrayToImagePlus(fused, name, min, max);
			fused.data = null;
			fused = null;
		}

		return fusedImp;
	}*/
	
	public static Point2D[] getCommonRectangle(ImagePlus img[], Point2D t[], boolean min)
	{
		Point2D size = new Point2D(0, 0);

		// defines where the output image starts relative to the first image
		Point2D startOffset = new Point2D(0, 0);

		// minimum size
		Point2D start = new Point2D(0, 0, 0);

		Point2D minEnd = new Point2D(img[0].getWidth(), img[0].getHeight());
		Point2D maxStart = new Point2D(0, 0, 0);

		for (int i = 1; i < img.length; i++)
		{
		start.x += t[i - 1].x;
		start.y += t[i - 1].y;

		maxStart.x = Math.max(start.x, maxStart.x);
		maxStart.y = Math.max(start.y, maxStart.y);

		minEnd.x = Math.min(minEnd.x, start.x + img[i].getWidth());
		minEnd.y = Math.min(minEnd.y, start.y + img[i].getHeight());
		}

		size.x = minEnd.x - maxStart.x;
		size.y = minEnd.y - maxStart.y;

		startOffset.x = maxStart.x;
		startOffset.y = maxStart.y;
		 
		 Point2D[] result = new Point2D[2];
		 
		 result[0] = startOffset;
		 result[1] = size;
		 
		 return result;
	}
	

	private CrossCorrelationResult2D[] testCrossCorrelation(FloatArray2D invPCM, ArrayList<Point2D> peaks,
			final FloatArray2D img1, final FloatArray2D img2)
	{
		final int numBestHits = peaks.size();
		final int numCases = 4;
		final CrossCorrelationResult2D result[] = new CrossCorrelationResult2D[numBestHits * numCases];

		int count = 0;
		int w = invPCM.width;
		int h = invPCM.height;

		final Point2D[] points = new Point2D[numCases];

		for (int hit = 0; count < numBestHits * numCases; hit++)
		{
			points[0] = peaks.get(hit);

			if (points[0].x < 0)
				points[1] = new Point2D(points[0].x + w, points[0].y, points[0].value);
			else
				points[1] = new Point2D(points[0].x - w, points[0].y, points[0].value);

			if (points[0].y < 0)
				points[2] = new Point2D(points[0].x, points[0].y + h, points[0].value);
			else
				points[2] = new Point2D(points[0].x, points[0].y - h, points[0].value);

			points[3] = new Point2D(points[1].x, points[2].y, points[0].value);

			final AtomicInteger entry = new AtomicInteger(count);
			final AtomicInteger shift = new AtomicInteger(0);

			Runnable task = new Runnable()
			{
				@Override
				public void run()
				{
					try
					{
						int myEntry = entry.getAndIncrement();
						int myShift = shift.getAndIncrement();
						result[myEntry] = testCrossCorrelation(points[myShift], img1, img2);
					} catch (Exception e)
					{
						Log.error(e);
					}
				}
			};

			startTask(task, numCases);

			count += numCases;
		}

		quicksort(result, 0, result.length - 1);

		if (doLogging)
			for (int i = 0; i < result.length; i++)
				Log.info("x:" + result[i].shift.x + " y:" + result[i].shift.y + " overlap:" + result[i].overlappingPixels + " R:" + result[i].R + " Peak:" + result[i].PCMValue);

		return result;
	}

	private CrossCorrelationResult2D testCrossCorrelation(Point2D shift, FloatArray2D img1, FloatArray2D img2)
	{
		// init Result Datastructure
		CrossCorrelationResult2D result = new CrossCorrelationResult2D();

		// compute values that will not change during testing
		result.PCMValue = shift.value;
		result.shift = shift;

		int w1 = img1.width;
		int h1 = img1.height;

		int w2 = img2.width;
		int h2 = img2.height;

		int sx = shift.x;
		int sy = shift.y;

		int imgW, imgH;

		if (sx >= 0)
			imgW = Math.max(w1, w2 + sx);
		else
			imgW = Math.max(w1 - sx, w2); // equals max(w1 + Math.abs(sx), w2);

		if (sy >= 0)
			imgH = Math.max(h1, h2 + sy);
		else
			imgH = Math.max(h1 - sy, h2);

		int offsetImg1X = Math.max(0, -sx); // + max(0, max(0, -sx) - max(0, (w1 - w2)/2));
		int offsetImg1Y = Math.max(0, -sy); // + max(0, max(0, -sy) - max(0, (h1 - h2)/2));
		int offsetImg2X = Math.max(0, sx); // + max(0, max(0, sx) - max(0, (w2 - w1)/2));
		int offsetImg2Y = Math.max(0, sy); // + max(0, max(0, sy) - max(0, (h2 - h1)/2));

		int count = 0;
		float pixel1, pixel2;

		double avg1 = 0, avg2 = 0;

		for (int y = 0; y < imgH; y++)
			for (int x = 0; x < imgW; x++)
			{
				// compute errors only if the images overlap
				if (x >= offsetImg1X && x >= offsetImg2X &&
					x < offsetImg1X + w1 && x < offsetImg2X + w2 &&
					y >= offsetImg1Y && y >= offsetImg2Y &&
					y < offsetImg1Y + h1 && y < offsetImg2Y + h2)
				{
					pixel1 = img1.getZero(x - offsetImg1X, y - offsetImg1Y);
					pixel2 = img2.getZero(x - offsetImg2X, y - offsetImg2Y);

					avg1 += pixel1;
					avg2 += pixel2;
					count++;

				}
			}

		// if less than 1% is overlapping
		if (count <= (Math.min(w1, w2) * Math.min(h1, h2)) * 0.01)
		{
			//Log.info("lower than 1%");
			result.R = 0;
			result.SSQ = Float.MAX_VALUE;
			result.overlappingPixels = count;
			return result;
		}

		avg1 /= count;
		avg2 /= count;

		double var1 = 0, var2 = 0;
		double coVar = 0;

		double dist1, dist2;

		double SSQ = 0;
		double pixelSSQ;
		count = 0;

		for (int y = 0; y < imgH; y++)
			for (int x = 0; x < imgW; x++)
			{
				// compute errors only if the images overlap
				if (x >= offsetImg1X && x >= offsetImg2X &&
					x < offsetImg1X + w1 && x < offsetImg2X + w2 &&
					y >= offsetImg1Y && y >= offsetImg2Y &&
					y < offsetImg1Y + h1 && y < offsetImg2Y + h2)
				{
					pixel1 = img1.getZero(x - offsetImg1X, y - offsetImg1Y);
					pixel2 = img2.getZero(x - offsetImg2X, y - offsetImg2Y);

					pixelSSQ = Math.pow(pixel1 - pixel2, 2);
					SSQ += pixelSSQ;
					count++;

					dist1 = pixel1 - avg1;
					dist2 = pixel2 - avg2;

					coVar += dist1 * dist2;
					var1 += Math.pow(dist1, 2);
					var2 += Math.pow(dist2, 2);

				}
			}

		SSQ /= count;
		var1 /= count;
		var2 /= count;
		coVar /= count;

		double stDev1 = Math.sqrt(var1);
		double stDev2 = Math.sqrt(var2);

		// all pixels had the same color....
		if (stDev1 == 0 || stDev2 == 0)
		{
			result.R = 0;
			result.SSQ = Float.MAX_VALUE;
			result.overlappingPixels = count;
			return result;
		}

		// compute correlation coeffienct
		result.R = coVar / (stDev1 * stDev2);
		result.SSQ = SSQ;
		result.overlappingPixels = count;

		return result;
	}

	private ArrayList<Point2D> findPeaks(FloatArray2D invPCM, Point2D img1, Point2D img2, Point2D ext1, Point2D ext2, int checkPeaks)
	{
		int w = invPCM.width;
		int h = invPCM.height;

		int xs, ys, xt, yt;
		float value;

		ArrayList<Point2D>peaks = new ArrayList<Point2D>();

		for (int j = 0; j < checkPeaks; j++)
			peaks.add(new Point2D(0, 0, Float.MIN_VALUE));

		for (int y = 0; y < h; y++)
			for (int x = 0; x < w; x++)
				if (isLocalMaxima(invPCM, x, y))
				{
					value = invPCM.get(x, y);
					Point2D insert = null;
					int insertPos = -1;

					Iterator<Point2D> i = peaks.iterator();
					boolean wasBigger = true;

					while (i.hasNext() && wasBigger)
					{
						if (value > i.next().value)
						{
							if (insert == null)
								insert = new Point2D(0, 0, value);

							insertPos++;
						}
						else
							wasBigger = false;
					}

					if (insertPos >= 0)
						peaks.add(insertPos + 1, insert);

					// remove lowest peak
					if (peaks.size() > checkPeaks)
						peaks.remove(0);

					if (insert != null)
					{
						// find relative to the left upper front corners of both images
						xt = x + (img1.x - img2.x) / 2 - (ext1.x - ext2.x) / 2;

						if (xt >= w / 2)
						{
							xs = xt - w;
						}
						else
							xs = xt;

						yt = y + (img1.y - img2.y) / 2 - (ext1.y - ext2.y) / 2;

						if (yt >= h / 2)
						{
							ys = yt - h;
						}
						else
							ys = yt;

						insert.x = xs;
						insert.y = ys;
					}
				}

		return peaks;
	}

	private boolean isLocalMaxima(FloatArray2D invPCM, int x, int y)
	{
		int width = invPCM.width;
		int height = invPCM.height;

		boolean isMax = true;
		float value = invPCM.get(x, y);

		if (x > 0 && y > 0 && x < width - 1 && y < height - 1)
		{
			for (int xs = x - 1; xs <= x + 1 && isMax; xs++)
				for (int ys = y - 1; ys <= y + 1 && isMax; ys++)
					if (!(x == xs && y == ys))
						if (invPCM.get(xs, ys) > value)
							isMax = false;
		}
		else
		{
			int xt, yt;

			for (int xs = x - 1; xs <= x + 1 && isMax; xs++)
				for (int ys = y - 1; ys <= y + 1 && isMax; ys++)
					if (!(x == xs && y == ys))
					{
						xt = xs;
						yt = ys;

						if (xt == -1) xt = width - 1;
						if (yt == -1) yt = height - 1;

						if (xt == width) xt = 0;
						if (yt == height) yt = 0;

						if (invPCM.get(xt, yt) > value)
							isMax = false;
					}
		}

		return isMax;
	}
	
	private FloatArray2D applyROI(ImagePlus imp, Point2D imgDim, Point2D extDim, String handleRGB, boolean windowing)
	{
		FloatArray2D img;

		if (imp.getRoi() == null)
		{
			// there are no rois....
			img = ImageToFloatArray(imp.getProcessor(), handleRGB);
		}
		else
		{
			Roi r = imp.getRoi();

			int x = r.getBoundingRect().x;
			int y = r.getBoundingRect().y;
			int w = r.getBoundingRect().width;
			int h = r.getBoundingRect().height;

			img = ImageToFloatArray(imp.getProcessor(), handleRGB, x, y, w, h);
		}

		imgDim.x = img.width;
		imgDim.y = img.height;

		extDim.x = extDim.y = 0;

		if (windowing)
		{
			int imgW = img.width;
			int imgH = img.height;
			int extW = imgW / 4;
			int extH = imgH / 4;

			// add an even number so that both sides extend equally
			if (extW % 2 != 0) extW++;
			if (extH % 2 != 0) extH++;

			extDim.x = extW;
			extDim.y = extH;

			imgDim.x += extDim.x;
			imgDim.y += extDim.y;

			// extend images
			img = extendImageMirror(img, imgW + extW, imgH + extH);
		}

		return img;
	}

	private boolean checkRoi(ImagePlus imp1, ImagePlus imp2)
	{
		boolean img1HasROI, img2HasROI;

		if (imp1.getRoi() != null)
			img1HasROI = true;
		else
			img1HasROI = false;

		if (imp2.getRoi() != null)
			img2HasROI = true;
		else
			img2HasROI = false;

		if (img1HasROI && !img2HasROI || img2HasROI && !img1HasROI)
		{
			IJ.error("Eigher both images should have a ROI or none of them.");

			return false;
		}

		if (img1HasROI)
		{
			int type1 = imp1.getRoi().getType();
			int type2 = imp2.getRoi().getType();

			if (type1 != Roi.RECTANGLE)
			{
				IJ.error(imp1.getTitle() + " has a ROI which is no rectangle.");
			}

			if (type2 != Roi.RECTANGLE)
			{
				IJ.error(imp2.getTitle() + " has a ROI which is no rectangle.");
			}
		}

		return true;
	}


	/*
	   -----------------------------------------------------------------------------------------
	   Here are the general static methods needed by the plugin
	   Just to pronouce that they are not directly related, I put them in a seperate child class
	   -----------------------------------------------------------------------------------------
	 */

	private void exponentialWindow(FloatArray2D img)
	{
		double a = 1000;

		// create lookup table
		double weightsX[] = new double[img.width];
		double weightsY[] = new double[img.height];

		for (int x = 0; x < img.width; x++)
		{
			double relPos = (double) x / (double) (img.width - 1);

			if (relPos <= 0.5)
				weightsX[x] = 1.0 - (1.0 / (Math.pow(a, (relPos * 2))));
			else
				weightsX[x] = 1.0 - (1.0 / (Math.pow(a, ((1 - relPos) * 2))));
		}

		for (int y = 0; y < img.height; y++)
		{
			double relPos = (double) y / (double) (img.height - 1);

			if (relPos <= 0.5)
				weightsY[y] = 1.0 - (1.0 / (Math.pow(a, (relPos * 2))));
			else
				weightsY[y] = 1.0 - (1.0 / (Math.pow(a, ((1 - relPos) * 2))));
		}

		for (int y = 0; y < img.height; y++)
			for (int x = 0; x < img.width; x++)
				img.set((float) (img.get(x, y) * weightsX[x] * weightsY[y]), x, y);
	}

	private FloatArray2D extendImageMirror(FloatArray2D ip, int width, int height)
	{
		FloatArray2D image = new FloatArray2D(width, height);

		int offsetX = (width - ip.width) / 2;
		int offsetY = (height - ip.height) / 2;

		if (offsetX < 0)
		{
			IJ.error("Stitching_3D.extendImageMirror(): Extended size in X smaller than image! " + width + " < " + ip.width);
			return null;
		}

		if (offsetY < 0)
		{
			IJ.error("Stitching_3D.extendImageMirror(): Extended size in Y smaller than image! " + height + " < " + ip.height);
			return null;
		}

		for (int x = 0; x < width; x++)
			for (int y = 0; y < height; y++)
				image.set(ip.getMirror(x - offsetX, y - offsetY), x, y);

		return image;
	}

	public FloatArray2D ImageToFloatArray(ImageProcessor ip, String handleRGB)
	{
		int rgbType = -1;

		if (ip == null )
		{
			Log.error("Image Stack is empty.");
			return null;
		}

		if (ip.getPixels() instanceof int[])
		{
			if (handleRGB == null || handleRGB.trim().length() == 0) handleRGB = colorList[colorList.length - 1];

			for (int i = 0; i < colorList.length; i++)
			{
				if (handleRGB.toLowerCase().trim().equals(colorList[i].toLowerCase())) rgbType = i;
			}

			if (rgbType == -1)
			{
				Log.warn("Unrecognized command to handle RGB: " + handleRGB + ". Assuming Average of Red, Green and Blue.");
				rgbType = colorList.length - 1;
			}
		}
		
		int width = ip.getWidth();
		int height = ip.getHeight();

		if (width * height == 0)
		{
			Log.error("Image is empty.");
			return null;
		}

		FloatArray2D pixels = new FloatArray2D(width, height);
		int count;

		if (ip.getPixels() instanceof byte[])
		{
			byte[] pixelTmp = (byte[]) ip.getPixels();
			count = 0;

			for (int y = 0; y < height; y++)
				for (int x = 0; x < width; x++)
					pixels.data[pixels.getPos(x, y)] = pixelTmp[count++] & 0xff;
		}
		else if (ip.getPixels() instanceof short[])
		{
			short[] pixelTmp = (short[]) ip.getPixels();
			count = 0;

			for (int y = 0; y < height; y++)
				for (int x = 0; x < width; x++)
					pixels.data[pixels.getPos(x, y)] = pixelTmp[count++] & 0xffff;
		}
		else if (ip.getPixels() instanceof int[]) 
		{
			int[] pixelTmp = (int[]) ip.getPixels();
			count = 0;

			for (int y = 0; y < height; y++)
				for (int x = 0; x < width; x++)
					pixels.data[pixels.getPos(x, y)] = getPixelValueRGB(pixelTmp[count++], rgbType);
		}
		else// instance of float[]
		{
			float[] pixelTmp = (float[]) ip.getPixels();
			count = 0;

			for (int y = 0; y < height; y++)
				for (int x = 0; x < width; x++)
					pixels.data[pixels.getPos(x, y)] = pixelTmp[count++];
		}

		return pixels;
	}

	private FloatArray2D ImageToFloatArray(ImageProcessor ip, String handleRGB, int xCrop, int yCrop, int wCrop, int hCrop)
	{
		int rgbType = -1;

		if (ip == null )
		{
			Log.error("Image Stack is empty.");
			return null;
		}

		if (ip.getPixels() instanceof int[])
		{
			if (handleRGB == null || handleRGB.trim().length() == 0) handleRGB = colorList[colorList.length - 1];

			for (int i = 0; i < colorList.length; i++)
			{
				if (handleRGB.toLowerCase().trim().equals(colorList[i].toLowerCase())) rgbType = i;
			}

			if (rgbType == -1)
			{
				Log.warn("Unrecognized command to handle RGB: " + handleRGB + ". Assuming Average of Red, Green and Blue.");
				rgbType = colorList.length - 1;
			}
		}

		int width = ip.getWidth();
		int height = ip.getHeight();

		if (width * height == 0)
		{
			Log.error("Image is empty.");
			return null;
		}

		FloatArray2D pixels = new FloatArray2D(wCrop, hCrop);

		if (ip.getPixels() instanceof byte[])
		{
			byte[] pixelTmp = (byte[]) ip.getPixels();

			for (int y = yCrop; y < yCrop + hCrop; y++)
				for (int x = xCrop; x < xCrop + wCrop; x++)
					pixels.data[pixels.getPos(x - xCrop, y - yCrop)] = pixelTmp[x + y * width] & 0xff;
		}
		else if (ip.getPixels() instanceof short[])
		{
			short[] pixelTmp = (short[]) ip.getPixels();

			for (int y = yCrop; y < yCrop + hCrop; y++)
				for (int x = xCrop; x < xCrop + wCrop; x++)
					pixels.data[pixels.getPos(x - xCrop, y - yCrop)] = pixelTmp[x + y * width] & 0xffff;
		}
		else if (ip.getPixels() instanceof int[])
		{
			int[] pixelTmp = (int[]) ip.getPixels();

			for (int y = yCrop; y < yCrop + hCrop; y++)
				for (int x = xCrop; x < xCrop + wCrop; x++)
					pixels.data[pixels.getPos(x - xCrop, y - yCrop)] = getPixelValueRGB(pixelTmp[x + y * width], rgbType);
		}
		else // instance of float[]
		{
			float[] pixelTmp = (float[]) ip.getPixels();

			for (int y = yCrop; y < yCrop + hCrop; y++)
				for (int x = xCrop; x < xCrop + wCrop; x++)
					pixels.data[pixels.getPos(x - xCrop, y - yCrop)] = pixelTmp[x + y * width];
		}

		return pixels;
	}

	private ImagePlus FloatArrayToImagePlus(FloatArray2D image, String name, float min, float max)
	{
		int width = image.width;
		int height = image.height;

		ImagePlus impResult = IJ.createImage(name, "32-Bit Black", width, height, 1);
		ImageProcessor ipResult = impResult.getProcessor();
		float[] sliceImg = new float[width * height];

		for (int x = 0; x < width; x++)
			for (int y = 0; y < height; y++)
				sliceImg[y * width + x] = image.get(x, y);

		ipResult.setPixels(sliceImg);

		if (min == max)
			ipResult.resetMinAndMax();
		else
			ipResult.setMinAndMax(min, max);

		impResult.updateAndDraw();

		return impResult;
	}
}