/*
 * #%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%
 */
/**
 * 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
 */
package stitching;

import edu.mines.jtk.dsp.FftComplex;
import edu.mines.jtk.dsp.FftReal;
import ij.IJ;
import ij.ImagePlus;
import ij.ImageStack;
import ij.gui.MultiLineLabel;
import ij.io.Opener;
import ij.plugin.BrowserLauncher;
import ij.process.ByteProcessor;
import ij.process.ColorProcessor;
import ij.process.ShortProcessor;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicInteger;

import ome.units.quantity.Length;
import loci.formats.ChannelSeparator;
import loci.formats.FormatException;
import loci.formats.FormatTools;
import loci.formats.IFormatReader;
import loci.formats.meta.MetadataRetrieve;
import stitching.utils.Log;

public class CommonFunctions
{
	public static String[] methodList = {"Average", "Linear Blending", "Max. Intensity", "Min. Intensity", "Red-Cyan Overlay"};	
	public final static int AVG = 0, LIN_BLEND = 1,  MAX = 2, MIN = 3, RED_CYAN = 4, NONE = 5;

	public static String[] methodListCollection = {"Average", "Linear Blending", "Max. Intensity", "Min. Intensity", "None"};	
	public static String[] rgbTypes = {"rgb", "rbg", "grb", "gbr", "brg", "bgr"}; 
	public static String[] colorList = { "Red", "Green", "Blue", "Red and Green", "Red and Blue", "Green and Blue", "Red, Green and Blue" };

	public static String[] fusionMethodList = { "Linear Blending", "Average", "Median", "Max. Intensity", "Min. Intensity", "Intensity of random input tile", "Overlay into composite image", "Do not fuse images" };	
	public static String[] fusionMethodListSimple = { "Overlay into composite image", "Do not fuse images" };	
	public static String[] fusionMethodListGrid = { "Linear Blending", "Average", "Median", "Max. Intensity", "Min. Intensity", "Intensity of random input tile", /* "Overlay into composite image", */ "Do not fuse images (only write TileConfiguration)" };	
	public static String[] timeSelect = { "Apply registration of first time-point to all other time-points", "Register images adjacently over time", "Register all images over all time-points globally (expensive!)" };
	public static String[] cpuMemSelect = { "Save memory (but be slower)", "Save computation time (but use more RAM)" };
	
	public static ImagePlus loadImage(String directory, String file, int seriesNumber) { return loadImage(directory, file, seriesNumber, "rgb"); }
	public static ImagePlus loadImage(String directory, String file, int seriesNumber, String rgb)
	{
		ImagePlus imp = null;
		
		String smallFile = file.toLowerCase();
		
		if (smallFile.endsWith("tif") || smallFile.endsWith("tiff") || smallFile.endsWith("jpg") || smallFile.endsWith("png") || smallFile.endsWith("bmp") || 
			smallFile.endsWith("gif") || smallFile.endsWith("jpeg"))
		{
			imp = new Opener().openImage((new File(directory, file)).getPath());
		}
		else
		{
			imp = openLOCIImagePlus(directory, file, seriesNumber, rgb);
			if (imp == null)
				imp = new Opener().openImage((new File(directory, file)).getPath());
		}

		
		return imp;
	}

	public static final void addHyperLinkListener(final MultiLineLabel text, final String myURL)
	{
		if ( text != null && myURL != null )
		{
			text.addMouseListener(new MouseAdapter()
			{
				@Override
				public void mouseClicked(MouseEvent e)
				{
					try
					{
						BrowserLauncher.openURL(myURL);
					}
					catch (Exception ex)
					{
						IJ.error("" + ex);
					}
				}
	
				@Override
				public void mouseEntered(MouseEvent e)
				{
					text.setForeground(Color.BLUE);
					text.setCursor(new Cursor(Cursor.HAND_CURSOR));
				}
	
				@Override
				public void mouseExited(MouseEvent e)
				{
					text.setForeground(Color.BLACK);
					text.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
				}
			});
		}
	}

	public static ImagePlus openLOCIImagePlus(String path, String fileName, int seriesNumber, String rgb) 
	{
		return openLOCIImagePlus(path, fileName, seriesNumber, rgb, -1, -1);
	}

	public static ImagePlus openLOCIImagePlus(String path, String fileName, int seriesNumber) 
	{
		return openLOCIImagePlus(path, fileName, seriesNumber, "rgb", -1, -1);
	}

	public static ImagePlus openLOCIImagePlus(String path, String fileName, int seriesNumber, String rgb, int from, int to) 
	{
		if (path.length() > 1) 
		{
			path = path.replace('\\', '/');
			if (!path.endsWith("/"))
				path = path + "/";
		}
		
		// parse howto assign channels
		rgb = rgb.toLowerCase().trim();
		final int colorAssign[][] = new int[rgb.length()][];
		final int colorWeight[] = new int[3];
		
		for (int i = 0; i < colorAssign.length; i++)
		{
			if (rgb.charAt(i) == 'r')
			{
				colorAssign[i] = new int[1]; 
				colorAssign[i][0] = 0;
				colorWeight[0]++;
			}
			else if (rgb.charAt(i) == 'b')
			{
				colorAssign[i] = new int[1]; 
				colorAssign[i][0] = 2;
				colorWeight[2]++;
			}
			else if (rgb.charAt(i) == 'g')
			{
				colorAssign[i] = new int[1]; 
				colorAssign[i][0] = 1;
				colorWeight[1]++;
			}
			else //leave out
			{
				colorAssign[i] = new int[0]; 
			}
		}
		
		for (int i = 0; i < colorWeight.length; i++)
			if (colorWeight[i] == 0)
				colorWeight[i] = 1;
		
		ImagePlus imp = null;
		
		final String id = path + fileName;
		final IFormatReader r = new ChannelSeparator();
		
		try 
		{
			r.setId(id);

			// if loaded from a multiple series file (like LSM 710) select the correct series
			if ( seriesNumber >= 0 )
				r.setSeries( seriesNumber );

			//final int num = r.getImageCount();
			final int width = r.getSizeX();
			final int height = r.getSizeY();
			final int depth = r.getSizeZ();
			final int timepoints = r.getSizeT();
			final int channels;
			//final String formatType = r.getFormat();
			final int pixelType = r.getPixelType();
			final int bytesPerPixel = FormatTools.getBytesPerPixel(pixelType); 
			final String pixelTypeString = FormatTools.getPixelTypeString(pixelType);
			//final String dimensionOrder = r.getDimensionOrder();
			
			if (timepoints > 1)
				Log.warn("More than one timepoint. Not implemented yet. Returning first timepoint");
			
			if (r.getSizeC() > 3)
			{
				Log.warn("More than three channels. ImageJ supports only 3 channels, returning the first three channels.");
				channels = 3;
			}
			else
			{
				channels = r.getSizeC();
			}				
			
			if (!(pixelType == FormatTools.UINT8 || pixelType == FormatTools.UINT16))
			{
				Log.error("PixelType " + pixelTypeString + " not supported yet, returning. ");
				return null;
			}
			
			final int start, end;			
			if (from < 0 || to < 0 || to < from)
			{
				start = 0; end = depth;
			}
			else 
			{
				start = from;
				if (to > depth)
					end = depth;
				else 
					end = to;
			}
			
			/*Log.debug("width: " + width);
			Log.debug("height: " + height);
			Log.debug("depth: " + depth);
			Log.debug("timepoints: " + timepoints);
			Log.debug("channels: " + channels);
			Log.debug("images: " + num);
			Log.debug("image format: " + formatType);
			Log.debug("bytes per pixel: " + bytesPerPixel);
			Log.debug("pixel type: " + pixelTypeString);			
			Log.debug("dimensionOrder: " + dimensionOrder);*/

			final ImageStack stack = new ImageStack(width, height);	
			final int t = 0;			
			
			for (int z = start; z < end; z++)
			{				
				byte[][] b = new byte[channels][width * height * bytesPerPixel];
				
				for (int c = 0; c < channels; c++)
				{
					final int index = r.getIndex(z, c, t);
					r.openBytes(index, b[c]);					
					//Log.debug(index);
				}
				
				if (channels == 1)
				{
					if (pixelType == FormatTools.UINT8)
					{
						final ByteProcessor bp = new ByteProcessor(width, height, b[0], null);
						stack.addSlice("" + (z + 1), bp);
					}	
					else if (pixelType == FormatTools.UINT16)
					{
						final short[] data = new short[width * height];
						
						for (int i = 0; i < data.length; i++)
							data[i] = getShortValue(b[0], i * 2);
													
						final ShortProcessor sp = new ShortProcessor(width, height, data, null);
						
						stack.addSlice("" + (z + 1), sp);						
					}						
				}
				else
				{
					final ColorProcessor cp = new ColorProcessor(width, height);
					final int color[] = new int[3];
					                            
					if (pixelType == FormatTools.UINT8)
						for (int y = 0; y < height; y++)
							for (int x = 0; x < width; x++)
							{
								color[0] = color[1] = color[2] = 0;
								
								for (int c = 0; c < channels; c++)
									for (int e = 0; e < colorAssign[c].length; e++)
									color[colorAssign[c][e]] += b[c][x + y*width] & 0xff;
								
								color[0] /= colorWeight[0]; 
								color[1] /= colorWeight[1]; 
								color[2] /= colorWeight[2]; 
									
								cp.putPixel(x, y, color);
							}
					else if (pixelType == FormatTools.UINT16)
						for (int y = 0; y < height; y++)
							for (int x = 0; x < width; x++)
							{
								color[0] = color[1] = color[2] = 0;
								
								for (int c = 0; c < channels; c++)
									color[c] = (byte)(getShortValue(b[c], 2 * (x + y*width))/256);
								
								cp.putPixel(x, y, color);
							}
					stack.addSlice("" + (z + 1), cp);						
				}
			}
			
			imp = new ImagePlus(fileName, stack);
		}
		catch (IOException exc) { IJ.handleException(exc); return null;}
		catch (FormatException exc) { IJ.handleException(exc); return null;}
	                
		return imp;
	}

	private static final short getShortValue(final byte[] b, final int i)
	{
		return (short)getShortValueInt(b, i);
	}

	private static final int getShortValueInt(final byte[] b, final int i)
	{
		return ((((b[i] & 0xff) << 8)) + (b[i+1] & 0xff));
	}
	
	public static float getPixelValueRGB( final int rgb, final int rgbType )
	{
		final int r = (rgb & 0xff0000) >> 16;
		final int g = (rgb & 0xff00) >> 8;
		final int b = rgb & 0xff;

		// colorList = {"Red", "Green", "Blue", "Red and Green", "Red and Blue", "Green and Blue", "Red, Green and Blue"};

		if (rgbType == 0) return r;
		else if (rgbType == 1) return g;
		else if (rgbType == 2) return b;
		else if (rgbType == 3) return (r + g) / 2.0f;
		else if (rgbType == 4) return (r + b) / 2.0f;
		else if (rgbType == 5) return (g + b) / 2.0f;
		else return (r + g + b) / 3.0f;
	}

	public static FloatArray3D zeroPad(final FloatArray3D ip, final int width, final int height, final int depth)
	{
		final FloatArray3D image = new FloatArray3D(width, height, depth);

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

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

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

		if (offsetZ < 0)
		{
			IJ.error("Stitching_3D.ZeroPad(): Zero-Padding size in Z smaller than image! " + depth + " < " + ip.depth);
			return null;
		}

		for (int z = 0; z < ip.depth; z++)
			for (int y = 0; y < ip.height; y++)
				for (int x = 0; x < ip.width; x++)
					image.set(ip.get(x, y, z), x + offsetX, y + offsetY, z + offsetZ);

		return image;
	}

	public static FloatArray3D[] zeroPadImages(final FloatArray3D img1, final FloatArray3D img2)
	{
		final int width = Math.max(img1.width, img2.width);
		final int height = Math.max(img1.height, img2.height);
		final int depth = Math.max(img1.depth, img2.depth);

		final int widthFFT = FftReal.nfftFast(width);
		final int heightFFT = FftComplex.nfftFast(height);
		final int depthFFT = FftComplex.nfftFast(depth);

		final FloatArray3D[] result = new FloatArray3D[2];

		result[0] = zeroPad(img1, widthFFT, heightFFT, depthFFT);
		img1.data = null;

		result[1] = zeroPad(img2, widthFFT, heightFFT, depthFFT);
		img2.data = null;

		return result;
	}
	
	public static FloatArray2D zeroPad(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.ZeroPad(): Zero-Padding size in X smaller than image! " + width + " < " + ip.width);
			return null;
		}

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

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

		return image;
	}

	public static FloatArray2D[] zeroPadImages(FloatArray2D img1, FloatArray2D img2)
	{
		int width = Math.max(img1.width, img2.width);
		int height = Math.max(img1.height, img2.height);

		int widthFFT = FftReal.nfftFast(width);
		int heightFFT = FftComplex.nfftFast(height);

		FloatArray2D[] result = new FloatArray2D[2];

		result[0] = zeroPad(img1, widthFFT, heightFFT);
		img1.data = null;
		img1 = null;

		result[1] = zeroPad(img2, widthFFT, heightFFT);
		img2.data = null;
		img2 = null;

		return result;
	}
	
	public static void quicksort(final Quicksortable[] data, final int left, final int right)
	{
		if (data == null || data.length < 2) return;
		int i = left, j = right;

		double x = data[(left + right) / 2].getQuicksortValue();

		do
		{
			while (data[i].getQuicksortValue() < x)
				i++;
			while (x < data[j].getQuicksortValue())
				j--;
			if (i <= j)
			{
				Quicksortable temp = data[i];
				data[i] = data[j];
				data[j] = temp;
				i++;
				j--;
			}
		} while (i <= j);
		if (left < j) quicksort(data, left, j);
		if (i < right) quicksort(data, i, right);
	}

	public static FloatArray2D pffft2D(FloatArray2D values, boolean scale)
	{
		int height = values.height;
		int width = values.width;
		int complexWidth = (width / 2 + 1) * 2;

		FloatArray2D result = new FloatArray2D(complexWidth, height);

		//do fft's in x direction
		float[] tempIn = new float[width];
		float[] tempOut;

		FftReal fft = new FftReal(width);

		for (int y = 0; y < height; y++)
		{
			tempOut = new float[complexWidth];

			for (int x = 0; x < width; x++)
				tempIn[x] = values.get(x, y);

			fft.realToComplex( -1, tempIn, tempOut);

			if (scale)
				fft.scale(width, tempOut);

			for (int x = 0; x < complexWidth; x++)
				result.set(tempOut[x], x, y);
		}

		// do fft's in y-direction on the complex numbers
		tempIn = new float[height * 2];

		FftComplex fftc = new FftComplex(height);

		for (int x = 0; x < complexWidth / 2; x++)
		{
			tempOut = new float[height * 2];

			for (int y = 0; y < height; y++)
			{
				tempIn[y * 2] = result.get(x * 2, y);
				tempIn[y * 2 + 1] = result.get(x * 2 + 1, y);
			}

			fftc.complexToComplex( -1, tempIn, tempOut);

			for (int y = 0; y < height; y++)
			{
				result.set(tempOut[y * 2], x * 2, y);
				result.set(tempOut[y * 2 + 1], x * 2 + 1, y);
			}
		}

		return result;
	}

	public static FloatArray2D pffftInv2D(FloatArray2D values, int nfft)
	{
		int height = values.height;
		int width = nfft;
		int complexWidth = (width / 2 + 1) * 2;

		FloatArray2D result = new FloatArray2D(width, height);

		// do inverse fft's in y-direction on the complex numbers
		float[] tempIn = new float[height * 2];
		float[] tempOut;

		FftComplex fftc = new FftComplex(height);

		for (int x = 0; x < complexWidth / 2; x++)
		{
			tempOut = new float[height * 2];

			for (int y = 0; y < height; y++)
			{
				tempIn[y * 2] = values.get(x * 2, y);
				tempIn[y * 2 + 1] = values.get(x * 2 + 1, y);
			}

			fftc.complexToComplex(1, tempIn, tempOut);

			for (int y = 0; y < height; y++)
			{
				values.set(tempOut[y * 2], x * 2, y);
				values.set(tempOut[y * 2 + 1], x * 2 + 1, y);
			}
		}

		//do inverse fft's in x direction
		tempIn = new float[complexWidth];

		FftReal fft = new FftReal(width);

		for (int y = 0; y < height; y++)
		{
			tempOut = new float[width];

			for (int x = 0; x < complexWidth; x++)
				tempIn[x] = values.get(x, y);

			fft.complexToReal(1, tempIn, tempOut);

			fft.scale(width, tempOut);

			for (int x = 0; x < width; x++)
				result.set(tempOut[x], x, y);
		}

		return result;
	}
	
	public static FloatArray3D pffft3DMT(final FloatArray3D values, final boolean scale)
	{
		final int height = values.height;
		final int width = values.width;
		final int depth = values.depth;
		final int complexWidth = (width / 2 + 1) * 2;

		final FloatArray3D result = new FloatArray3D(complexWidth, height, depth);

		// do fft's in x direction
		final AtomicInteger ai = new AtomicInteger(0);
		Thread[] threads = newThreads();
		final int numThreads = threads.length;

		for (int ithread = 0; ithread < threads.length; ++ithread)
			threads[ithread] = new Thread(new Runnable()
			{
				@Override
				public void run()
				{
					int myNumber = ai.getAndIncrement();

					float[] tempIn = new float[width];
					float[] tempOut;
					FftReal fft = new FftReal(width);

					for (int z = 0; z < depth; z++)
						if (z % numThreads == myNumber) for (int y = 0; y < height; y++)
						{
							tempOut = new float[complexWidth];

							for (int x = 0; x < width; x++)
								tempIn[x] = values.get(x, y, z);

							fft.realToComplex(-1, tempIn, tempOut);

							if (scale) fft.scale(width, tempOut);

							for (int x = 0; x < complexWidth; x++)
								result.set(tempOut[x], x, y, z);
						}
				}
			});
		startAndJoin(threads);

		// do fft's in y direction
		ai.set(0);
		threads = newThreads();

		for (int ithread = 0; ithread < threads.length; ++ithread)
			threads[ithread] = new Thread(new Runnable()
			{
				@Override
				public void run()
				{
					float[] tempIn = new float[height * 2];
					float[] tempOut;
					FftComplex fftc = new FftComplex(height);

					int myNumber = ai.getAndIncrement();

					for (int z = 0; z < depth; z++)
						if (z % numThreads == myNumber) for (int x = 0; x < complexWidth / 2; x++)
						{
							tempOut = new float[height * 2];

							for (int y = 0; y < height; y++)
							{
								tempIn[y * 2] = result.get(x * 2, y, z);
								tempIn[y * 2 + 1] = result.get(x * 2 + 1, y, z);
							}

							fftc.complexToComplex(-1, tempIn, tempOut);

							for (int y = 0; y < height; y++)
							{
								result.set(tempOut[y * 2], x * 2, y, z);
								result.set(tempOut[y * 2 + 1], x * 2 + 1, y, z);
							}
						}
				}
			});

		startAndJoin(threads);

		// do fft's in z direction
		ai.set(0);
		threads = newThreads();

		for (int ithread = 0; ithread < threads.length; ++ithread)
			threads[ithread] = new Thread(new Runnable()
			{
				@Override
				public void run()
				{
					float[] tempIn = new float[depth * 2];
					float[] tempOut;
					FftComplex fftc = new FftComplex(depth);

					int myNumber = ai.getAndIncrement();

					for (int y = 0; y < height; y++)
						if (y % numThreads == myNumber) for (int x = 0; x < complexWidth / 2; x++)
						{
							tempOut = new float[depth * 2];

							for (int z = 0; z < depth; z++)
							{
								tempIn[z * 2] = result.get(x * 2, y, z);
								tempIn[z * 2 + 1] = result.get(x * 2 + 1, y, z);
							}

							fftc.complexToComplex(-1, tempIn, tempOut);

							for (int z = 0; z < depth; z++)
							{
								result.set(tempOut[z * 2], x * 2, y, z);
								result.set(tempOut[z * 2 + 1], x * 2 + 1, y, z);
							}
						}
				}
			});

		startAndJoin(threads);

		return result;
	}

	public static FloatArray2D computePhaseCorrelationMatrix(FloatArray2D fft1, FloatArray2D fft2, int width)
	{
		//
		// Do Phase Correlation
		//

		FloatArray2D pcm = new FloatArray2D(computePhaseCorrelationMatrix(fft1.data, fft2.data, false), fft1.width, fft1.height);

		fft1.data = fft2.data = null;
		fft1 = fft2 = null;

		FloatArray2D ipcm = pffftInv2D(pcm, width);

		pcm.data = null;
		pcm = null;

		return ipcm;
	}

	public static FloatArray2D computeFFT(FloatArray2D img)
	{
		FloatArray2D fft = pffft2D(img, false);
		//img.data = null; img = null;

		return fft;
	}

	public static FloatArray3D computePhaseCorrelationMatrix(FloatArray3D fft1, FloatArray3D fft2, int width)
	{
		//
		// Do Phase Correlation
		//

		FloatArray3D pcm = new FloatArray3D(computePhaseCorrelationMatrix(fft1.data, fft2.data, false), fft1.width, fft1.height, fft1.depth);

		fft1.data = fft2.data = null;
		fft1 = fft2 = null;

		FloatArray3D ipcm = pffftInv3DMT(pcm, width);

		pcm.data = null;
		pcm = null;

		return ipcm;
	}

	public static FloatArray3D computeFFT(FloatArray3D img)
	{
		FloatArray3D fft = pffft3DMT(img, false);
		// img.data = null; img = null;

		return fft;
	}

	
	public static float[] computePhaseCorrelationMatrix(final float[] fft1, final float[] fft2, boolean inPlace)
	{
		normalizeComplexVectorsToUnitVectors(fft1);
		normalizeComplexVectorsToUnitVectors(fft2);

		float[] fftTemp1 = fft1;
		float[] fftTemp2 = fft2;

		// do complex conjugate
		if (inPlace) complexConjugate(fft2);
		else complexConjugate(fftTemp2);

		// multiply both complex arrays elementwise
		if (inPlace) multiply(fft1, fft2, true);
		else fftTemp1 = multiply(fftTemp1, fftTemp2, false);

		return inPlace ? null : fftTemp1;
	}

	public static void normalizeComplexVectorsToUnitVectors(float[] complex)
	{
		int wComplex = complex.length / 2;

		double length;

		for (int pos = 0; pos < wComplex; pos++)
		{
			length = Math.sqrt(Math.pow(complex[pos * 2], 2) + Math.pow(complex[pos * 2 + 1], 2));

			if (length > 1E-5)
			{
				complex[pos * 2 + 1] /= length;
				complex[pos * 2] /= length;
			}
			else
			{
				complex[pos * 2 + 1] = complex[pos * 2] = 0;
			}
		}

	}

	public static void complexConjugate(float[] complex)
	{
		int wComplex = complex.length / 2;

		for (int pos = 0; pos < wComplex; pos++)
			complex[pos * 2 + 1] = -complex[pos * 2 + 1];
	}

	public static float multiplyComplexReal(float a, float b, float c, float d)
	{
		return a * c - b * d;
	}

	public static float multiplyComplexImg(float a, float b, float c, float d)
	{
		return a * d + b * c;
	}

	public static float[] multiply(float[] complexA, float[] complexB, boolean overwriteA)
	{
		if (complexA.length != complexB.length) return null;

		float[] complexResult = null;

		if (!overwriteA) complexResult = new float[complexA.length];

		// this is the amount of complex numbers
		// the actual array size is twice as high
		int wComplex = complexA.length / 2;

		// we compute: (a + bi) * (c + di)
		float a, b, c, d;

		if (!overwriteA) for (int pos = 0; pos < wComplex; pos++)
		{
			a = complexA[pos * 2];
			b = complexA[pos * 2 + 1];
			c = complexB[pos * 2];
			d = complexB[pos * 2 + 1];

			// compute new real part
			complexResult[pos * 2] = multiplyComplexReal(a, b, c, d);

			// compute new imaginary part
			complexResult[pos * 2 + 1] = multiplyComplexImg(a, b, c, d);
		}
		else for (int pos = 0; pos < wComplex; pos++)
		{
			a = complexA[pos * 2];
			b = complexA[pos * 2 + 1];
			c = complexB[pos * 2];
			d = complexB[pos * 2 + 1];

			// compute new real part
			complexA[pos * 2] = multiplyComplexReal(a, b, c, d);

			// compute new imaginary part
			complexA[pos * 2 + 1] = multiplyComplexImg(a, b, c, d);
		}

		if (overwriteA) return complexA;
		return complexResult;
	}

	public static FloatArray3D pffftInv3DMT(final FloatArray3D values, final int nfft)
	{
		final int depth = values.depth;
		final int height = values.height;
		final int width = nfft;
		final int complexWidth = (width / 2 + 1) * 2;

		final FloatArray3D result = new FloatArray3D(width, height, depth);

		// do inverse fft's in z-direction on the complex numbers
		final AtomicInteger ai = new AtomicInteger(0);
		Thread[] threads = newThreads();
		final int numThreads = threads.length;

		for (int ithread = 0; ithread < threads.length; ++ithread)
			threads[ithread] = new Thread(new Runnable()
			{
				@Override
				public void run()
				{
					int myNumber = ai.getAndIncrement();

					float[] tempIn = new float[depth * 2];
					float[] tempOut;
					FftComplex fftc = new FftComplex(depth);

					for (int y = 0; y < height; y++)
						if (y % numThreads == myNumber) for (int x = 0; x < complexWidth / 2; x++)
						{
							tempOut = new float[complexWidth];

							tempOut = new float[depth * 2];

							for (int z = 0; z < depth; z++)
							{
								tempIn[z * 2] = values.get(x * 2, y, z);
								tempIn[z * 2 + 1] = values.get(x * 2 + 1, y, z);
							}

							fftc.complexToComplex(1, tempIn, tempOut);

							for (int z = 0; z < depth; z++)
							{
								values.set(tempOut[z * 2], x * 2, y, z);
								values.set(tempOut[z * 2 + 1], x * 2 + 1, y, z);
							}
						}
				}
			});
		startAndJoin(threads);

		// do inverse fft's in y-direction on the complex numbers
		ai.set(0);
		threads = newThreads();

		for (int ithread = 0; ithread < threads.length; ++ithread)
			threads[ithread] = new Thread(new Runnable()
			{
				@Override
				public void run()
				{
					float[] tempIn = new float[height * 2];
					float[] tempOut;
					FftComplex fftc = new FftComplex(height);

					int myNumber = ai.getAndIncrement();

					for (int z = 0; z < depth; z++)
						if (z % numThreads == myNumber) for (int x = 0; x < complexWidth / 2; x++)
						{
							tempOut = new float[height * 2];

							for (int y = 0; y < height; y++)
							{
								tempIn[y * 2] = values.get(x * 2, y, z);
								tempIn[y * 2 + 1] = values.get(x * 2 + 1, y, z);
							}

							fftc.complexToComplex(1, tempIn, tempOut);

							for (int y = 0; y < height; y++)
							{
								values.set(tempOut[y * 2], x * 2, y, z);
								values.set(tempOut[y * 2 + 1], x * 2 + 1, y, z);
							}
						}
				}
			});
		startAndJoin(threads);

		// do inverse fft's in x direction
		ai.set(0);
		threads = newThreads();

		for (int ithread = 0; ithread < threads.length; ++ithread)
			threads[ithread] = new Thread(new Runnable()
			{
				@Override
				public void run()
				{
					float[] tempIn = new float[complexWidth];
					float[] tempOut;
					FftReal fft = new FftReal(width);

					int myNumber = ai.getAndIncrement();

					for (int z = 0; z < depth; z++)
						if (z % numThreads == myNumber) for (int y = 0; y < height; y++)
						{
							tempOut = new float[width];

							for (int x = 0; x < complexWidth; x++)
								tempIn[x] = values.get(x, y, z);

							fft.complexToReal(1, tempIn, tempOut);

							for (int i = 0; i < tempOut.length; i++)
								tempOut[i] /= width * height * depth;

							// fft.scale(width, tempOut);

							for (int x = 0; x < width; x++)
								result.set(tempOut[x], x, y, z);
						}
				}
			});

		startAndJoin(threads);

		return result;
	}
	
	public static void startTask(Runnable run, int numThreads)
	{
		Thread[] threads = newThreads(numThreads);

		for (int ithread = 0; ithread < threads.length; ++ithread)
			threads[ithread] = new Thread(run);

		startAndJoin(threads);
	}

	public static Thread[] newThreads()
	{
		int nthread = Runtime.getRuntime().availableProcessors();
		return new Thread[nthread];
	}

	public static Thread[] newThreads(int numThreads)
	{
		return new Thread[numThreads];
	}

	public static void startAndJoin(Thread[] threads)
	{
		for (int ithread = 0; ithread < threads.length; ++ithread)
		{
			threads[ithread].setPriority(Thread.NORM_PRIORITY);
			threads[ithread].start();
		}

		try
		{
			for (int ithread = 0; ithread < threads.length; ++ithread)
				threads[ithread].join();
		}
		catch (InterruptedException ie)
		{
			throw new RuntimeException(ie);
		}
	}
	
	final public static int[] getPixelMinRGB(final Object[] imageStack, final int width, final int height, final int depth, final int x, final int y, final int z, final double min)
	{
		final int[] rgb = new int[3];

		if (x < 0 || y < 0 || z < 0 || x >= width || y >= height || z >= depth)
		{
			rgb[0] = rgb[1] = rgb[2] = (int) min;
			return rgb;
		}

		int[] pixelTmp = (int[]) imageStack[z];
		int color = (pixelTmp[x + y * width] & 0xffffff);

		rgb[0] = (color & 0xff0000) >> 16;
		rgb[1] = (color & 0xff00) >> 8;
		rgb[2] = color & 0xff;

		return rgb;
	}
	
	final public static float getPixelMin(final int imageType, final Object[] imageStack, final int width, final int height, final int depth, final int x, final int y, final int z, final double min)
	{
		if (x < 0 || y < 0 || z < 0 || x >= width || y >= height || z >= depth) return (float) min;

		if (imageType == ImagePlus.GRAY8)
		{
			byte[] pixelTmp = (byte[]) imageStack[z];
			return pixelTmp[x + y * width] & 0xff;
		}
		else if (imageType == ImagePlus.GRAY16)
		{
			short[] pixelTmp = (short[]) imageStack[z];
			return pixelTmp[x + y * width] & 0xffff;
		}
		else
		// instance of float[]
		{
			float[] pixelTmp = (float[]) imageStack[z];
			return pixelTmp[x + y * width];
		}
	}

	public static final double[] getPlanePosition( final IFormatReader r, final MetadataRetrieve retrieve, int series, int t )
	{
		return getPlanePosition(r, retrieve, series, t, false, false, false);
	}

	public static final double[] getPlanePosition( final IFormatReader r, final MetadataRetrieve retrieve, int series, int t, boolean invertX, boolean invertY, boolean ignoreZStage)
	{
		// generate a mapping from native indices to Plane element indices
		final HashMap< Integer, Integer > planeMap = new HashMap< Integer, Integer >();
		final int planeCount = retrieve.getPlaneCount( series );
		for ( int p = 0; p < planeCount; ++p )
		{
			final int theZ = retrieve.getPlaneTheZ( series, p ).getValue();
			final int theC = retrieve.getPlaneTheC( series, p ).getValue();
			final int theT = retrieve.getPlaneTheT( series, p ).getValue();
			final int index = r.getIndex( theZ, theC, theT );
			planeMap.put( index, p );
		}

		// get reader index of time point t
		final int index = r.getIndex( 0, 0, t );

		// convert reader index to plane element index
		final int planeIndex = planeMap.containsKey( index ) ? planeMap.get( index ) : 0;
		final boolean hasPlane = planeIndex < retrieve.getPlaneCount( series );

		// CTR HACK: Recover gracefully when StageLabel element is missing.
		// This avoids a problem with the OMEXMLMetadataImpl implementation,
		// which currently does not check for null on the StageLabel object.
		Length stageLabelX = null, stageLabelY = null, stageLabelZ = null;
		try
		{
			stageLabelX = retrieve.getStageLabelX( series );
			stageLabelY = retrieve.getStageLabelY( series );
			stageLabelZ = retrieve.getStageLabelZ( series );
		}
		catch (final NullPointerException exc)
		{
			// ignore
		}

		// stage coordinates (for the given series and plane)
		final double locationX = getPosition( hasPlane ? retrieve.getPlanePositionX( series, planeIndex ) : null, stageLabelX, invertX );
		final double locationY = getPosition( hasPlane ? retrieve.getPlanePositionY( series, planeIndex ) : null, stageLabelY, invertY );
		final double locationZ = ignoreZStage ? 0 : getPosition( hasPlane ? retrieve.getPlanePositionZ( series, planeIndex ) : null, stageLabelZ, false );

		Log.debug( "locationX:  " + locationX );
		Log.debug( "locationY:  " + locationY );
		Log.debug( "locationZ:  " + locationZ );

		return new double[] { locationX, locationY, locationZ };
	}

	private static double getPosition( final Length planePos, final Length stageLabel, final boolean invert )
	{
		// check plane position
		if ( planePos != null )
			return invert ? -planePos.value().doubleValue() : planePos.value().doubleValue();

		// check global stage label
		if ( stageLabel != null )
			return invert ? -stageLabel.value().doubleValue() : stageLabel.value().doubleValue();

		return 0;
	}

}