package ij.plugin;
import ij.*;
import ij.macro.Interpreter;
import ij.process.*; 
import ij.gui.*;
import java.awt.*;
import ij.measure.*;
import ij.plugin.filter.*;
import ij.plugin.frame.Recorder;
import java.awt.event.*;
import java.util.*;
import java.lang.*;
import java.awt.image.ColorModel;

/** This plugin, which concatenates two or more images or stacks,
 *  implements the Image/Stacks/Tools/Concatenate command.
 *  Has the option of viewing the concatenated stack as a 4D image.
 *  @author Jon Jackson j.jackson # ucl.ac.uk
 */
public class Concatenator implements PlugIn, ItemListener{
    public String pluginName =  "Concatenator";
    public int maxEntries = 18;  // limit number of entries to fit on screen
    private static boolean all_option = false;
    private boolean keep = false;
    private static boolean keep_option = false;
    private boolean batch = false;
    private boolean macro = false;
    private boolean im4D = true;
    private static boolean im4D_option = true;
    private String[] imageTitles;
    private ImagePlus[] images;
    private Vector choices;
    private Checkbox allWindows;
    private final String none = "-- None --";
    private String newtitle = "Untitled";
    private ImagePlus newImp;
    private int stackSize;
    private double min = 0, max = Float.MAX_VALUE;
    private int maxWidth, maxHeight;
    private boolean showingDialog;

    
    /** Optional string argument sets the name dialog boxes if called from another plugin. */
    public void run(String arg) {
        macro = !arg.equals("");
        if (!showDialog())
            return;
        newImp = concatenate(images, keep);
        if (newImp!=null)
            newImp.show();
    }
    
    /** Displays a dialog requiring user to choose images and
        returns ImagePlus of concatenated images. */
    public ImagePlus run() {
        if (!showDialog())
            return null;
        newImp = createHypervol();
        return newImp;
    }
    
    /** Concatenates two images, stacks or hyperstacks. */
    public static ImagePlus run(ImagePlus img1, ImagePlus img2) {
        ImagePlus[] images = new ImagePlus[2];
        images[0]=img1; images[1]=img2;
        return (new Concatenator()).concatenate(images, false);
    }

    /** Concatenates three images, stacks or hyperstacks. */
    public static ImagePlus run(ImagePlus img1, ImagePlus img2, ImagePlus img3) {
        ImagePlus[] images = new ImagePlus[3];
        images[0]=img1; images[1]=img2;  images[2]=img3;
        return (new Concatenator()).concatenate(images, false);
    }

    /** Concatenates four images, stacks or hyperstacks. */
    public static ImagePlus run(ImagePlus img1, ImagePlus img2, ImagePlus img3, ImagePlus img4) {
        ImagePlus[] images = new ImagePlus[4];
        images[0]=img1; images[1]=img2;  images[2]=img3; images[2]=img4;
        return (new Concatenator()).concatenate(images, false);
    }

    /** Concatenates five images, stacks or hyperstacks. */
    public static ImagePlus run(ImagePlus img1, ImagePlus img2, ImagePlus img3, ImagePlus img4, ImagePlus img5) {
        ImagePlus[] images = new ImagePlus[5];
        images[0]=img1; images[1]=img2;  images[2]=img3; images[2]=img4; images[5]=img5;
        return (new Concatenator()).concatenate(images, false);
    }

    /** Concatenates two or more images, stacks or hyperstacks.
     * @param images Array of source images
     * @return Returns the concatenated images as an ImagePlus
    */
    public static ImagePlus run(ImagePlus[] images) {
        return  (new Concatenator()).concatenate(images, false);
    }

    /*
    // Why does this not work with Java 6?
    public static ImagePlus run(ImagePlus... args) {
        return (new Concatenator()).concatenate(args, false);
    }
    */

    /** Concatenates two or more images or stacks. */
    public ImagePlus concatenate(ImagePlus[] ims, boolean keepIms) {
        images = ims;
        imageTitles = new String[ims.length];
        for (int i = 0; i < ims.length; i++) {
            if (ims[i] != null) {
                imageTitles[i] = ims[i].getTitle();
            } else {
                IJ.error(pluginName, "Null ImagePlus passed to concatenate(...) method");
                return null;
            }
        }
        keep = keepIms;
        batch = true;
        ImagePlus imp0 = images[0];
        if (imp0.isComposite() || imp0.getNChannels()>1)
            newImp = concatenateHyperstacks(images, newtitle, keep);
        else
            newImp = createHypervol();
        if (Recorder.scriptMode()) {
            String args = "imp1";
            for (int i=1; i<images.length; i++)
                args += ", imp"+(i+1);
            Recorder.recordCall("imp"+(images.length+1)+" = Concatenator.run("+args+");");
        }
        if (imp0!=null && newImp!=null)
            newImp.setProperty("Info", imp0.getProperty("Info"));
        return newImp;
    }
    
    /** Concatenate two images or stacks. */
    public ImagePlus concatenate(ImagePlus imp1, ImagePlus imp2, boolean keep) {
        images = new ImagePlus[2];
        images[0] = imp1;
        images[1] = imp2;
        return concatenate(images, keep);
    }
    
    private ImagePlus createHypervol() {
        boolean firstImage = true;
        boolean duplicated;
        Properties[] propertyArr = new Properties[images.length];
        ImagePlus currentImp = null;
        ImageStack concat_Stack = null;
        stackSize = 0;
        int dataType = 0, width= 0, height = 0;
        Calibration cal = null;
        int count = 0;
        findMaxDimensions(images);
        for (int i = 0; i < images.length; i++) {
            if (images[i] != null) { // Should only find null imp if user has closed an image after starting plugin (unlikely...)
                currentImp = images[i];
                if (firstImage) { // Initialise based on first image
                    //concat_Imp = images[i];
                    cal = currentImp.getCalibration();
                    width = currentImp.getWidth();
                    height = currentImp.getHeight();
                    stackSize = currentImp.getNSlices();
                    dataType = currentImp.getType();
                    ColorModel cm = currentImp.getProcessor().getColorModel();
                    concat_Stack = new ImageStack(maxWidth, maxHeight, cm);
                    min = currentImp.getProcessor().getMin();
                    max = currentImp.getProcessor().getMax();
                    firstImage = false;
                }
                
                // Safety Checks
                boolean unequalSizes = currentImp.getNSlices()!=stackSize;
                if (unequalSizes)
                    im4D = false;
                if (currentImp.getType() != dataType) {
                    IJ.log("Omitting " + imageTitles[i] + " - image type not matched");
                    continue;
                }

               // concatenate
                duplicated = isDuplicated(currentImp, i);
                concat(concat_Stack, currentImp.getStack(), (keep || duplicated));
                propertyArr[count] = currentImp.getProperties();
                imageTitles[count] = currentImp.getTitle();
                if (! (keep || duplicated)) {
                    currentImp.changes = false;
                    currentImp.hide();
                }
                count++;
            }
        }
        
        // Copy across info fields
        ImagePlus imp = new ImagePlus(newtitle, concat_Stack);
        imp.setCalibration(cal);
        imp.setProperty("Number of Stacks", Integer.valueOf(count));
        imp.setProperty("Stacks Properties", propertyArr);
        imp.setProperty("Image Titles", imageTitles);
        imp.getProcessor().setMinAndMax(min, max);
        if (im4D) {
            imp.setDimensions(1, stackSize, imp.getStackSize()/stackSize);
            imp.setOpenAsHyperStack(true);
        }
        return imp;
    }
    
    // taken from WSR's Concatenator_.java
    private void concat(ImageStack stack3, ImageStack stack1, boolean dup) {
        int slice = 1;
        int size = stack1.getSize();
        for (int i = 1; i <= size; i++) {
            ImageProcessor ip = stack1.getProcessor(slice);
            String label = stack1.getSliceLabel(slice);
            if (dup) {
                ip = ip.duplicate();
                slice++;
            } else
                stack1.deleteSlice(slice);
            ImageProcessor ip2 = ip;
            if (ip.getWidth()!=maxWidth || ip.getHeight()!=maxHeight) {
                ip2 = ip.createProcessor(maxWidth, maxHeight);
                ip2.insert(ip, (maxWidth-ip.getWidth())/2, (maxHeight-ip.getHeight())/2);
            }
            stack3.addSlice(label, ip2);
        }
    } 
    
    /** Obsolete, replaced by concatenate(images,keep) and Concatenator.run(images). */
    public ImagePlus concatenateHyperstacks(ImagePlus[] images, String newTitle, boolean keep) {
        int n = images.length;
        int width = images[0].getWidth();
        int height = images[0].getHeight();
        int bitDepth = images[0].getBitDepth();
        int channels = images[0].getNChannels();
        int slices =  images[0].getNSlices();
        int frames = images[0].getNFrames();
        boolean concatSlices = slices>1 && frames==1;
        boolean keepCalibration = true;
        Calibration cal = images[0].getCalibration();
        maxWidth = width;
        maxHeight = height;
        
        for (int i=1; i<n; i++) {
            if (images[i].getNFrames()>1)
                concatSlices = false;
            if (images[i].getBitDepth()!=bitDepth
            || images[i].getNChannels()!=channels
            || (!concatSlices && images[i].getNSlices()!=slices)) {
                IJ.error(pluginName, "Images do not all have the same dimensions or type");
                return null;
            }
            Calibration cal2 = images[i].getCalibration();
            if (cal2.pixelWidth!=cal.pixelWidth 
            || cal2.pixelHeight!=cal.pixelHeight 
            || cal2.pixelDepth != cal.pixelDepth)
                keepCalibration = false;
            if (images[i].getWidth()>maxWidth)
                maxWidth = images[i].getWidth();
            if (images[i].getHeight()>maxHeight)
                maxHeight = images[i].getHeight();
        }
        ImageStack stack2 = new ImageStack(maxWidth, maxHeight);
        int slices2=0, frames2=0;
        for (int i=0;i<n;i++) {
            ImageStack stack = images[i].getStack();
            slices = images[i].getNSlices();
            if (concatSlices) {
                slices = images[i].getNSlices();
                slices2 += slices;
                frames2 = frames;
            } else {
                frames = images[i].getNFrames();
                frames2 += frames;
                slices2 = slices;
            }
            for (int f=1; f<=frames; f++) {
                for (int s=1; s<=slices; s++) {
                    for (int c=1; c<=channels; c++) {
                        int index = (f-1)*channels*slices + (s-1)*channels + c;
                        ImageProcessor ip = stack.getProcessor(index);
                        if (keep)
                            ip = ip.duplicate();
                        String label = stack.getSliceLabel(index);
                        ImageProcessor ip2 = ip;
                        if (ip.getWidth()!=maxWidth || ip.getHeight()!=maxHeight) {
                            ip2 = ip.createProcessor(maxWidth, maxHeight);
                            ip2.insert(ip, (maxWidth-ip.getWidth())/2, (maxHeight-ip.getHeight())/2);
                        }
                        stack2.addSlice(label, ip2);
                    }
                }
            }
        }
        ImagePlus imp2 = new ImagePlus(newTitle, stack2);
        imp2.setDimensions(channels, slices2, frames2);
        if (channels>1) {
            int mode = 0;
            if (images[0].isComposite())
                mode = ((CompositeImage)images[0]).getMode();
            imp2 = new CompositeImage(imp2, mode);
            ((CompositeImage)imp2).copyLuts(images[0]);
        }
        if (channels>1 && frames2>1)
            imp2.setOpenAsHyperStack(true);
        if (keepCalibration)
            imp2.setCalibration(cal);
        if (!keep) {
            for (int i=0; i<n; i++) {
                images[i].changes = false;
                images[i].close();
            }
        }
        return imp2;
    }   
    
    private boolean showDialog() {
        boolean all_windows = false;
        batch = Interpreter.isBatchMode();
        macro = macro || (IJ.isMacro()&&Macro.getOptions()!=null);
        if (Menus.commandInUse("Stack to Image5D") && !batch)
            im4D = true;
        showingDialog = Macro.getOptions()==null;
        if (macro) {
            String options = Macro.getOptions();
            if (options.contains("stack1")&&options.contains("stack2"))
                Macro.setOptions(options.replaceAll("stack", "image"));
            int macroImageCount = 0;
            options = Macro.getOptions();
            while (true) {
                if (options.contains("image"+(macroImageCount+1)))
                    macroImageCount++;
                else
                    break;
            }
            maxEntries = macroImageCount;
        }
        
        // Checks
        int[] wList = WindowManager.getIDList();
        if (wList==null) {
            IJ.error("No windows are open.");
            return false;
        } else if (wList.length < 2) {
            IJ.error("Two or more windows must be open");
            return false;
        }
        int nImages = wList.length;
        
        String[] titles = new String[nImages];
        String[] titles_none = new String[nImages + 1];
        for (int i=0; i<nImages; i++) {
            ImagePlus imp = WindowManager.getImage(wList[i]);
            if (imp!=null) {
                titles[i] = imp.getTitle();
                titles_none[i] = imp.getTitle();
            } else {
                titles[i] = "";
                titles_none[i] = "";
            }
        }
        titles_none[nImages] = none;
        
        GenericDialog gd = new GenericDialog(pluginName);
        gd.addCheckbox("All_open windows", all_option);
        gd.addChoice("Image1:", titles, titles[0]);
        gd.addChoice("Image2:", titles, titles[1]);
        for (int i = 2; i < ((nImages+1)<maxEntries?(nImages+1):maxEntries); i++)
            gd.addChoice("Image" + (i+1)+":", titles_none, titles_none[i]);
        gd.addStringField("Title:", newtitle, 16);
        gd.addCheckbox("Keep original images", keep_option);
        gd.addCheckbox("Open as 4D_image", im4D_option);
        if (!macro) { // Monitor user selections
            choices = gd.getChoices();
            for (Enumeration e = choices.elements() ; e.hasMoreElements() ;)
                ((Choice)e.nextElement()).addItemListener(this);
            Vector v = gd.getCheckboxes();
            allWindows = (Checkbox)v.firstElement();
            allWindows.addItemListener(this);
            if (all_option) itemStateChanged(new ItemEvent(allWindows, ItemEvent.ITEM_STATE_CHANGED, null, ItemEvent.SELECTED));
        }
        gd.showDialog();
        
        if (gd.wasCanceled())
            return false;
        all_windows = gd.getNextBoolean();
        all_option = all_windows;
        gd.setSmartRecording(true);
        newtitle = gd.getNextString();
        gd.setSmartRecording(false);
        keep = gd.getNextBoolean();
        keep_option = keep;
        im4D = gd.getNextBoolean();
        im4D_option = im4D;
        ImagePlus[] tmpImpArr = new ImagePlus[nImages+1];
        String[] tmpStrArr = new String[nImages+1];
        int index=0, count = 0;
        for (int i=0; i<=nImages; i++) { // compile a list of images to concatenate from user selection
            if (all_windows) { // Useful to not have to specify images in batch mode
                index = i;
            } else {
                if (i == ((nImages+1)<maxEntries?(nImages+1):maxEntries) ) break;
                gd.setSmartRecording(i==nImages);
                index = gd.getNextChoiceIndex();
            }
            if (index >= nImages) break; // reached the 'none' string or handled all images (in case of all_windows)
            if (! titles[index].equals("")) {
                tmpStrArr[count] = titles[index];
                tmpImpArr[count] = WindowManager.getImage(wList[index]);
                count++;
            }
        }
        if (count<2) {
            IJ.error(pluginName, "Please select at least 2 images");
            return false;
        }
        
        imageTitles = new String[count];
        images = new ImagePlus[count];
        System.arraycopy(tmpStrArr, 0, imageTitles, 0, count);
        System.arraycopy(tmpImpArr, 0, images, 0, count);
        return true;
    }
    
    // test if this imageplus appears again in the list
    boolean isDuplicated(ImagePlus imp, int index) {
        int length = images.length;
        if (index >= length - 1) return false;
        for (int i = index + 1; i < length; i++) {
            if (imp == images[i]) return true;
        }
        return false;
    }
    
    public void itemStateChanged(ItemEvent ie) {
        Choice c;
        if (ie.getSource() == allWindows) { // User selected / unselected 'all windows' button
            int count = 0;
            if (allWindows.getState()) {
                for (Enumeration e = choices.elements() ; e.hasMoreElements() ;) {
                    c = (Choice)e.nextElement();
                    c.select(count++);
                    c.setEnabled(false);
                }
            } else {
                for (Enumeration e = choices.elements() ; e.hasMoreElements() ;) {
                    c = (Choice)e.nextElement();
                    c.setEnabled(true);
                }
            }
        } else { // User image selection triggered event
            boolean foundNone = false;
            // All image choices after an occurance of 'none' are reset to 'none'
            for (Enumeration e = choices.elements() ; e.hasMoreElements() ;) {
                c = (Choice)e.nextElement();
                if (! foundNone) {
                    c.setEnabled(true);
                    if (c.getSelectedItem().equals(none)) foundNone = true;
                } else { // a previous choice was 'none'
                    c.select(none);
                    c.setEnabled(false);
                }
            }
        }
    }
    
    public void setIm5D(boolean bool) {
        im4D_option = bool;
        im4D = bool;
    }
    
    private void findMaxDimensions(ImagePlus[] images) {
        boolean first = true;
        ImagePlus currentImp = null;
        int dataType = 0;
        maxWidth = maxHeight = 0;
        for (int i = 0; i < images.length; i++) {
            if (images[i] != null) {
                currentImp = images[i];
                if (first) {
                    dataType = currentImp.getType();
                    first = false;
                }
                if (currentImp.getType() != dataType)
                    continue;
                if (currentImp.getWidth()>maxWidth)
                    maxWidth = currentImp.getWidth();
                if (currentImp.getHeight()>maxHeight)
                    maxHeight = currentImp.getHeight();
            }
        }
    }
    
}