/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy 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 3 of the License, or (at your option) any later version.
 * 
 * Icy 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 Icy. If not, see
 * <http://www.gnu.org/licenses/>.
 */
package icy.imagej;

import icy.common.listener.ProgressListener;
import icy.image.IcyBufferedImage;
import icy.math.ArrayMath;
import icy.roi.ROI;
import icy.roi.ROI2D;
import icy.sequence.Sequence;
import icy.system.thread.ThreadUtil;
import icy.type.DataType;
import icy.type.collection.array.Array1DUtil;
import icy.type.collection.array.Array2DUtil;
import icy.type.collection.array.ArrayUtil;
import ij.CompositeImage;
import ij.ImagePlus;
import ij.ImageStack;
import ij.LookUpTable;
import ij.gui.Line;
import ij.gui.OvalRoi;
import ij.gui.PointRoi;
import ij.gui.PolygonRoi;
import ij.gui.Roi;
import ij.gui.ShapeRoi;
import ij.measure.Calibration;
import ij.plugin.frame.RoiManager;
import ij.process.FloatPolygon;
import ij.process.ImageProcessor;

import java.awt.Color;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.Area;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;

import plugins.kernel.roi.roi2d.ROI2DArea;
import plugins.kernel.roi.roi2d.ROI2DEllipse;
import plugins.kernel.roi.roi2d.ROI2DLine;
import plugins.kernel.roi.roi2d.ROI2DPath;
import plugins.kernel.roi.roi2d.ROI2DPoint;
import plugins.kernel.roi.roi2d.ROI2DPolyLine;
import plugins.kernel.roi.roi2d.ROI2DPolygon;
import plugins.kernel.roi.roi2d.ROI2DRectangle;
import plugins.kernel.roi.roi2d.ROI2DShape;

/**
 * ImageJ utilities class.
 * 
 * @author Stephane
 */
public class ImageJUtil
{
    /**
     * Convert the specified native 1D array to supported ImageJ native data array.
     */
    private static Object convertToIJType(Object array, boolean signed)
    {
        // double[] not supported in ImageJ
        if (array instanceof double[])
            return Array1DUtil.arrayToFloatArray(array, signed);
        // long[] not supported in ImageJ
        if (array instanceof long[])
            return Array1DUtil.arrayToShortArray(array, signed);
        // int[] means Color image for ImageJ
        if (array instanceof int[])
            return Array1DUtil.arrayToShortArray(array, signed);

        return Array1DUtil.copyOf(array);
    }

    /**
     * Append the specified {@link IcyBufferedImage} to the given ImageJ {@link ImageStack}.<br>
     * If input {@link ImageStack} is <code>null</code> then a new {@link ImageStack} is returned.
     */
    private static ImageStack appendToStack(IcyBufferedImage img, ImageStack stack)
    {
        final ImageStack result;

        if (stack == null)
            result = new ImageStack(img.getSizeX(), img.getSizeY(), LookUpTable.createGrayscaleColorModel(false));
        else
            result = stack;

        for (int c = 0; c < img.getSizeC(); c++)
            result.addSlice(null, convertToIJType(img.getDataXY(c), img.isSignedDataType()));

        return result;
    }

    /**
     * Convert the specified Icy {@link Sequence} object to {@link ImagePlus}.
     */
    private static ImagePlus createImagePlus(Sequence sequence, ProgressListener progressListener)
    {
        final int sizeZ = sequence.getSizeZ();
        final int sizeT = sequence.getSizeT();
        final int len = sizeZ * sizeT;

        int position = 0;
        ImageStack stack = null;

        for (int t = 0; t < sizeT; t++)
        {
            for (int z = 0; z < sizeZ; z++)
            {
                if (progressListener != null)
                    progressListener.notifyProgress(position, len);

                stack = appendToStack(sequence.getImage(t, z), stack);

                position++;
            }
        }

        // return the image
        return new ImagePlus(sequence.getName(), stack);
    }

    /**
     * Calibrate the specified Icy {@link Sequence} from the specified ImageJ {@link Calibration} object.
     */
    private static void calibrateIcySequence(Sequence sequence, Calibration cal)
    {
        if (cal != null)
        {
            if (cal.scaled())
            {
                // TODO : apply unit conversion
                sequence.setPixelSizeX(cal.pixelWidth);
                sequence.setPixelSizeY(cal.pixelHeight);
                sequence.setPixelSizeZ(cal.pixelDepth);
            }

            // TODO : apply unit conversion
            sequence.setTimeInterval(cal.frameInterval);
        }
    }

    /**
     * Calibrate the specified ImageJ {@link ImagePlus} from the specified Icy {@link Sequence}.
     */
    private static void calibrateImageJImage(ImagePlus image, Sequence seq)
    {
        final Calibration cal = image.getCalibration();

        final double psx = seq.getPixelSizeX();
        final double psy = seq.getPixelSizeY();
        final double psz = seq.getPixelSizeZ();

        // different from defaults values ?
        if ((psx != 1d) || (psy != 1d) || (psz != 1d))
        {
            cal.pixelWidth = psx;
            cal.pixelHeight = psy;
            cal.pixelDepth = psz;
            // default unit size icy
            cal.setUnit("µm");
        }

        final double ti = seq.getTimeInterval();
        // different from default value
        if (ti != 0.1d)
        {
            cal.frameInterval = ti;
            cal.setTimeUnit("sec");
        }

        image.setDimensions(seq.getSizeC(), seq.getSizeZ(), seq.getSizeT());
        image.setOpenAsHyperStack(image.getNDimensions() > 3);

        // final ImageProcessor ip = image.getProcessor();
        // ip.setMinAndMax(seq.getChannelMin(0) displayMin, displayMax);
    }

    /**
     * Convert the ImageJ {@link ImagePlus} image at position [Z,T] into an Icy image
     */
    public static IcyBufferedImage convertToIcyBufferedImage(ImagePlus image, int z, int t, int sizeX, int sizeY,
            int sizeC, int type, boolean signed16)
    {
        // set position
        image.setPosition(1, z + 1, t + 1);

        // directly use the buffered image to do the conversion...
        if ((sizeC == 1) && ((type == ImagePlus.COLOR_256) || (type == ImagePlus.COLOR_RGB)))
            return IcyBufferedImage.createFrom(image.getBufferedImage());

        final ImageProcessor ip = image.getProcessor();
        final Object data = Array1DUtil.copyOf(ip.getPixels());
        final DataType dataType = ArrayUtil.getDataType(data);
        final Object[] datas = Array2DUtil.createArray(dataType, sizeC);

        // first channel data (get a copy)
        datas[0] = data;
        // special case of 16 bits signed data --> subtract 32768
        if (signed16)
            datas[0] = ArrayMath.subtract(datas[0], Double.valueOf(32768));

        // others channels data
        for (int c = 1; c < sizeC; c++)
        {
            image.setPosition(c + 1, z + 1, t + 1);
            datas[c] = Array1DUtil.copyOf(image.getProcessor().getPixels());
            // special case of 16 bits signed data --> subtract 32768
            if (signed16)
                datas[c] = ArrayMath.subtract(datas, Double.valueOf(32768));
        }

        // create a single image from all channels
        return new IcyBufferedImage(sizeX, sizeY, datas, signed16);
    }

    /**
     * Convert the ImageJ {@link ImagePlus} image at position [Z,T] into an Icy image
     */
    public static IcyBufferedImage convertToIcyBufferedImage(ImagePlus image, int z, int t)
    {
        final int[] dim = image.getDimensions(true);

        return convertToIcyBufferedImage(image, z, t, dim[0], dim[1], dim[2], image.getType(), image
                .getLocalCalibration().isSigned16Bit());
    }

    /**
     * Convert the specified ImageJ {@link ImagePlus} object to Icy {@link Sequence}
     */
    public static Sequence convertToIcySequence(ImagePlus image, ProgressListener progressListener)
    {
        final Sequence result = new Sequence(image.getTitle());
        final int[] dim = image.getDimensions(true);

        final int sizeX = dim[0];
        final int sizeY = dim[1];
        final int sizeC = dim[2];
        final int sizeZ = dim[3];
        final int sizeT = dim[4];
        final int type = image.getType();
        // only integer signed type allowed in ImageJ is 16 bit signed
        final boolean signed16 = image.getLocalCalibration().isSigned16Bit();

        final int len = sizeZ * sizeT;
        int position = 0;

        result.beginUpdate();
        try
        {
            // convert image
            for (int t = 0; t < sizeT; t++)
            {
                for (int z = 0; z < sizeZ; z++)
                {
                    if (progressListener != null)
                        progressListener.notifyProgress(position, len);

                    result.setImage(t, z, convertToIcyBufferedImage(image, z, t, sizeX, sizeY, sizeC, type, signed16));

                    position++;
                }
            }

            // convert ROI(s)
            final RoiManager roiManager = RoiManager.getInstance();
            final Roi[] rois;

            if (roiManager != null)
                rois = roiManager.getRoisAsArray();
            else
                rois = new Roi[] {};

            if (rois.length > 0)
            {
                for (Roi ijRoi : rois)
                {
                    // can happen
                    if (ijRoi != null)
                        for (ROI icyRoi : convertToIcyRoi(ijRoi))
                            result.addROI(icyRoi);
                }
            }
            else
            {
                final Roi roi = image.getRoi();

                if (roi != null)
                    for (ROI icyRoi : convertToIcyRoi(roi))
                        result.addROI(icyRoi);
            }

            // calibrate
            calibrateIcySequence(result, image.getCalibration());
        }
        finally
        {
            result.endUpdate();
        }

        return result;
    }

    /**
     * Convert the specified Icy {@link Sequence} object to ImageJ {@link ImagePlus}
     */
    public static ImagePlus convertToImageJImage(Sequence sequence, boolean useRoiManager,
            ProgressListener progressListener)
    {
        // create the image
        final ImagePlus result = createImagePlus(sequence, progressListener);
        // calibrate
        calibrateImageJImage(result, sequence);

        // convert ROI
        final List<Roi> ijRois = new ArrayList<Roi>();
        for (ROI2D roi : sequence.getROI2Ds())
            ijRois.add(convertToImageJRoi(roi));

        if (ijRois.size() > 0)
        {
            if ((ijRois.size() > 1) && useRoiManager)
            {
                RoiManager roiManager = RoiManager.getInstance();
                if (roiManager == null)
                {
                    ThreadUtil.invokeNow(new Runnable()
                    {
                        @Override
                        public void run()
                        {
                            // need to do it on EDT
                            new RoiManager();
                        }
                    });
                }

                roiManager = RoiManager.getInstance();
                int n = 0;
                for (Roi roi : ijRois)
                    roiManager.add(result, roi, n++);
            }

            result.setRoi(ijRois.get(0));
        }

        if (result.getNChannels() > 4)
            return new CompositeImage(result, CompositeImage.COLOR);
        else if (result.getNChannels() > 1)
            return new CompositeImage(result, CompositeImage.COMPOSITE);

        return result;
    }

    /**
     * Convert the specified Icy {@link Sequence} object to ImageJ {@link ImagePlus}
     */
    public static ImagePlus convertToImageJImage(Sequence sequence, ProgressListener progressListener)
    {
        return convertToImageJImage(sequence, false, progressListener);
    }

    /**
     * Convert the specified ImageJ {@link Roi} object to Icy {@link ROI}.
     */
    public static List<ROI2D> convertToIcyRoi(Roi roi)
    {
        final List<ROI2D> result = new ArrayList<ROI2D>();
        final List<Point2D> pts = new ArrayList<Point2D>();
        final FloatPolygon fp;

        switch (roi.getType())
        {
            default:
                result.add(new ROI2DRectangle(roi.getFloatBounds()));
                break;

            case Roi.OVAL:
                result.add(new ROI2DEllipse(roi.getFloatBounds()));
                break;

            case Roi.LINE:
                final Rectangle2D rect = roi.getFloatBounds();
                final double x = rect.getX();
                final double y = rect.getY();
                result.add(new ROI2DLine(new Point2D.Double(x, y), new Point2D.Double(x + rect.getWidth(), y
                        + rect.getHeight())));
                break;

            case Roi.TRACED_ROI:
            case Roi.POLYGON:
            case Roi.FREEROI:
                fp = ((PolygonRoi) roi).getFloatPolygon();
                for (int p = 0; p < fp.npoints; p++)
                    pts.add(new Point2D.Float(fp.xpoints[p], fp.ypoints[p]));

                final ROI2DPolygon roiPolygon = new ROI2DPolygon();
                roiPolygon.setPoints(pts);

                // TRACED_ROI should be converted to ROI2DArea
                if (roi.getType() == Roi.TRACED_ROI)
                    result.add(new ROI2DArea(roiPolygon.getBooleanMask(true)));
                else
                    result.add(roiPolygon);
                break;

            case Roi.FREELINE:
            case Roi.POLYLINE:
            case Roi.ANGLE:
                fp = ((PolygonRoi) roi).getFloatPolygon();
                for (int p = 0; p < fp.npoints; p++)
                    pts.add(new Point2D.Float(fp.xpoints[p], fp.ypoints[p]));

                final ROI2DPolyLine roiPolyline = new ROI2DPolyLine();
                roiPolyline.setPoints(pts);

                result.add(roiPolyline);
                break;

            case Roi.COMPOSITE:
                final ROI2DPath roiPath = new ROI2DPath(((ShapeRoi) roi).getShape());
                final Rectangle2D.Double roiBounds = roi.getFloatBounds();
                // we have to adjust position as Shape do not contains it
                if (roiPath.canSetPosition())
                    roiPath.setPosition2D(new Point2D.Double(roiBounds.x, roiBounds.y));
                result.add(roiPath);
                break;

            case Roi.POINT:
                fp = ((PolygonRoi) roi).getFloatPolygon();
                for (int p = 0; p < fp.npoints; p++)
                    pts.add(new Point2D.Float(fp.xpoints[p], fp.ypoints[p]));

                for (Point2D pt : pts)
                    result.add(new ROI2DPoint(pt));
                break;
        }

        int ind = 0;
        for (ROI2D r : result)
        {
            r.setC(roi.getCPosition() - 1);
            r.setZ(roi.getZPosition() - 1);
            r.setT(roi.getTPosition() - 1);
            r.setSelected(false);
            if (result.size() > 1)
                r.setName(roi.getName() + " " + ind);
            else
                r.setName(roi.getName());
            Color c = roi.getStrokeColor();
            if (c == null)
                c = roi.getFillColor();
            if (c != null)
                r.setColor(c);
        }

        return result;
    }

    /**
     * Convert the specified Icy {@link ROI} object to ImageJ {@link Roi}.
     */
    public static Roi convertToImageJRoi(ROI2D roi)
    {
        final Roi result;

        if (roi instanceof ROI2DShape)
        {
            final List<Point2D> pts = ((ROI2DShape) roi).getPoints();

            if (roi instanceof ROI2DPoint)
            {
                final Point2D p = pts.get(0);
                result = new PointRoi(p.getX(), p.getY());
            }
            else if (roi instanceof ROI2DLine)
            {
                final Point2D p1 = pts.get(0);
                final Point2D p2 = pts.get(1);
                result = new Line(p1.getX(), p1.getY(), p2.getX(), p2.getY());
            }
            else if (roi instanceof ROI2DRectangle)
            {
                final Rectangle2D r = roi.getBounds2D();
                result = new Roi(r.getX(), r.getY(), r.getWidth(), r.getHeight(), 0);
            }
            else if (roi instanceof ROI2DEllipse)
            {
                final Rectangle2D r = roi.getBounds2D();
                result = new OvalRoi(r.getX(), r.getY(), r.getWidth(), r.getHeight());
            }
            else if ((roi instanceof ROI2DPolyLine) || (roi instanceof ROI2DPolygon))
            {
                final FloatPolygon fp = new FloatPolygon();
                for (Point2D p : pts)
                    fp.addPoint(p.getX(), p.getY());
                if (roi instanceof ROI2DPolyLine)
                    result = new PolygonRoi(fp, Roi.POLYLINE);
                else
                    result = new PolygonRoi(fp, Roi.POLYGON);
            }
            else
                // create compatible shape ROI
                result = new ShapeRoi(((ROI2DPath) roi).getShape());
        }
        else if (roi instanceof ROI2DArea)
        {
            final ROI2DArea roiArea = (ROI2DArea) roi;
            final Point[] points = roiArea.getBooleanMask(true).getPoints();

            final Area area = new Area();
            for (Point pt : points)
                area.add(new Area(new Rectangle(pt.x, pt.y, 1, 1)));

            result = new ShapeRoi(area);
        }
        else
        {
            // create standard ROI
            final Rectangle2D r = roi.getBounds2D();
            result = new Roi(r.getX(), r.getY(), r.getWidth(), r.getHeight());
        }

        result.setPosition(roi.getC() + 1, roi.getZ() + 1, roi.getT() + 1);
        result.setName(roi.getName());
        result.setStrokeColor(roi.getColor());
        // result.setFillColor(roi.getColor());
        // result.setStrokeWidth(roi.getStroke());

        return result;
    }

    /**
     * @deprecated Use {@link #convertToImageJRoi(ROI2D)} instead.
     */
    @Deprecated
    public static PointRoi convertToImageJRoiPoint(List<ROI2DPoint> points)
    {
        final int size = points.size();
        final float x[] = new float[size];
        final float y[] = new float[size];

        for (int i = 0; i < points.size(); i++)
        {
            final ROI2DPoint point = points.get(i);

            x[i] = (float) point.getPoint().getX();
            y[i] = (float) point.getPoint().getY();
        }

        return new PointRoi(x, y, size);
    }
}