/*
 * 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 plugins.kernel.roi.roi4d;

import icy.canvas.IcyCanvas;
import icy.canvas.IcyCanvas2D;
import icy.canvas.IcyCanvas3D;
import icy.painter.OverlayEvent;
import icy.painter.OverlayListener;
import icy.roi.BooleanMask2D;
import icy.roi.ROI;
import icy.roi.ROI3D;
import icy.roi.ROI4D;
import icy.roi.ROIEvent;
import icy.roi.ROIListener;
import icy.sequence.Sequence;
import icy.system.IcyExceptionHandler;
import icy.type.point.Point5D;
import icy.type.rectangle.Rectangle3D;
import icy.type.rectangle.Rectangle4D;
import icy.util.StringUtil;
import icy.util.XMLUtil;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.concurrent.Semaphore;

import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * Abstract class defining a generic 4D ROI as a stack of individual 3D ROI slices.
 * 
 * @author Alexandre Dufour
 * @author Stephane Dallongeville
 * @param <R>
 *        the type of 3D ROI for each slice of this 4D ROI
 */
public class ROI4DStack<R extends ROI3D> extends ROI4D implements ROIListener, OverlayListener, Iterable<R>
{
    public static final String PROPERTY_USECHILDCOLOR = "useChildColor";

    protected final TreeMap<Integer, R> slices = new TreeMap<Integer, R>();

    protected final Class<R> roiClass;
    protected boolean useChildColor;
    protected Semaphore modifyingSlice;
    protected double translateT;

    /**
     * Creates a new 4D ROI based on the given 3D ROI type.
     */
    public ROI4DStack(Class<R> roiClass)
    {
        super();

        this.roiClass = roiClass;
        useChildColor = false;
        modifyingSlice = new Semaphore(1);
        translateT = 0d;
    }
    
    @Override
    public String getDefaultName()
    {
        return "ROI3D stack";
    }

    @Override
    protected ROIPainter createPainter()
    {
        return new ROI4DStackPainter();
    }

    /**
     * Create a new empty 3D ROI slice.
     */
    protected R createSlice()
    {
        try
        {
            return roiClass.newInstance();
        }
        catch (Exception e)
        {
            IcyExceptionHandler.showErrorMessage(e, true, true);
            return null;
        }
    }

    /**
     * Returns <code>true</code> if the ROI directly uses the 3D slice color draw property and <code>false</code> if it
     * uses the global 4D ROI color draw property.
     */
    public boolean getUseChildColor()
    {
        return useChildColor;
    }

    /**
     * Set to <code>true</code> if you want to directly use the 3D slice color draw property and <code>false</code> to
     * keep the global 4D ROI color draw property.
     * 
     * @see #setColor(int, Color)
     */
    public void setUseChildColor(boolean value)
    {
        if (useChildColor != value)
        {
            useChildColor = value;
            propertyChanged(PROPERTY_USECHILDCOLOR);
            // need to redraw it
            getOverlay().painterChanged();
        }
    }

    /**
     * Set the painter color for the specified ROI slice.
     * 
     * @see #setUseChildColor(boolean)
     */
    public void setColor(int t, Color value)
    {
        final ROI3D slice = getSlice(t);

        modifyingSlice.acquireUninterruptibly();
        try
        {
            if (slice != null)
                slice.setColor(value);
        }
        finally
        {
            modifyingSlice.release();
        }
    }

    @Override
    public void setColor(Color value)
    {
        beginUpdate();
        try
        {
            super.setColor(value);

            if (!getUseChildColor())
            {
                modifyingSlice.acquireUninterruptibly();
                try
                {
                    for (R slice : slices.values())
                        slice.setColor(value);
                }
                finally
                {
                    modifyingSlice.release();
                }
            }
        }
        finally
        {
            endUpdate();
        }
    }

    @Override
    public void setOpacity(float value)
    {
        beginUpdate();
        try
        {
            super.setOpacity(value);

            modifyingSlice.acquireUninterruptibly();
            try
            {
                for (R slice : slices.values())
                    slice.setOpacity(value);
            }
            finally
            {
                modifyingSlice.release();
            }
        }
        finally
        {
            endUpdate();
        }
    }

    @Override
    public void setStroke(double value)
    {
        beginUpdate();
        try
        {
            super.setStroke(value);

            modifyingSlice.acquireUninterruptibly();
            try
            {
                for (R slice : slices.values())
                    slice.setStroke(value);
            }
            finally
            {
                modifyingSlice.release();
            }
        }
        finally
        {
            endUpdate();
        }
    }

    @Override
    public void setCreating(boolean value)
    {
        beginUpdate();
        try
        {
            super.setCreating(value);

            modifyingSlice.acquireUninterruptibly();
            try
            {
                for (R slice : slices.values())
                    slice.setCreating(value);
            }
            finally
            {
                modifyingSlice.release();
            }
        }
        finally
        {
            endUpdate();
        }
    }

    @Override
    public void setReadOnly(boolean value)
    {
        beginUpdate();
        try
        {
            super.setReadOnly(value);

            modifyingSlice.acquireUninterruptibly();
            try
            {
                for (R slice : slices.values())
                    slice.setReadOnly(value);
            }
            finally
            {
                modifyingSlice.release();
            }
        }
        finally
        {
            endUpdate();
        }
    }

    @Override
    public void setFocused(boolean value)
    {
        beginUpdate();
        try
        {
            super.setFocused(value);

            modifyingSlice.acquireUninterruptibly();
            try
            {
                for (R slice : slices.values())
                    slice.setFocused(value);
            }
            finally
            {
                modifyingSlice.release();
            }
        }
        finally
        {
            endUpdate();
        }
    }

    @Override
    public void setSelected(boolean value)
    {
        beginUpdate();
        try
        {
            super.setSelected(value);

            modifyingSlice.acquireUninterruptibly();
            try
            {
                for (R slice : slices.values())
                    slice.setSelected(value);
            }
            finally
            {
                modifyingSlice.release();
            }
        }
        finally
        {
            endUpdate();
        }
    }

    @Override
    public void setC(int value)
    {
        beginUpdate();
        try
        {
            super.setC(value);

            modifyingSlice.acquireUninterruptibly();
            try
            {
                for (R slice : slices.values())
                    slice.setC(value);
            }
            finally
            {
                modifyingSlice.release();
            }
        }
        finally
        {
            endUpdate();
        }
    }

    /**
     * Returns <code>true</code> if the ROI stack is empty.
     */
    public boolean isEmpty()
    {
        return slices.isEmpty();
    }

    /**
     * @return The size of this ROI stack along T.<br>
     *         Note that the returned value indicates the difference between upper and lower bounds
     *         of this ROI, but doesn't guarantee that all slices in-between exist ( {@link #getSlice(int)} may still
     *         return <code>null</code>.<br>
     */
    public int getSizeT()
    {
        if (slices.isEmpty())
            return 0;

        return (slices.lastKey().intValue() - slices.firstKey().intValue()) + 1;
    }

    /**
     * Returns the ROI slice at given T position.
     */
    public R getSlice(int t)
    {
        return slices.get(Integer.valueOf(t));
    }

    /**
     * Returns the ROI slice at given T position.
     */
    public R getSlice(int t, boolean createIfNull)
    {
        R result = getSlice(t);

        if ((result == null) && createIfNull)
        {
            result = createSlice();
            if (result != null)
                setSlice(t, result);
        }

        return result;
    }

    /**
     * Sets the slice for the given T position.
     */
    public void setSlice(int t, R roi3d)
    {
        if (roi3d == null)
            throw new IllegalArgumentException("Cannot set an empty slice in a 4D ROI");

        // set T and C position
        roi3d.setT(t);
        roi3d.setC(getC());
        // listen events from this ROI and its overlay
        roi3d.addListener(this);
        roi3d.getOverlay().addOverlayListener(this);

        slices.put(Integer.valueOf(t), roi3d);

        // notify ROI changed
        roiChanged(true);
    }

    /**
     * Removes slice at the given T position and returns it.
     */
    public R removeSlice(int t)
    {
        // remove the current slice (if any)
        final R result = slices.remove(Integer.valueOf(t));

        // remove listeners
        if (result != null)
        {
            result.removeListener(this);
            result.getOverlay().removeOverlayListener(this);
        }

        // notify ROI changed
        roiChanged(true);

        return result;
    }

    /**
     * Removes all slices.
     */
    public void clear()
    {
        // nothing to do
        if (isEmpty())
            return;

        for (R slice : slices.values())
        {
            slice.removeListener(this);
            slice.getOverlay().removeOverlayListener(this);
        }

        slices.clear();
        roiChanged(true);
    }

    /**
     * Called when a ROI slice has changed.
     */
    protected void sliceChanged(ROIEvent event)
    {
        if (modifyingSlice.availablePermits() <= 0)
            return;

        final ROI source = event.getSource();

        switch (event.getType())
        {
            case ROI_CHANGED:
                // position change of a slice can change global bounds --> transform to 'content changed' event type
                roiChanged(true);
//                roiChanged(StringUtil.equals(event.getPropertyName(), ROI_CHANGED_ALL));
                break;

            case FOCUS_CHANGED:
                setFocused(source.isFocused());
                break;

            case SELECTION_CHANGED:
                setSelected(source.isSelected());
                break;

            case PROPERTY_CHANGED:
                final String propertyName = event.getPropertyName();

                if ((propertyName == null) || propertyName.equals(PROPERTY_READONLY))
                    setReadOnly(source.isReadOnly());
                if ((propertyName == null) || propertyName.equals(PROPERTY_CREATING))
                    setCreating(source.isCreating());
                break;
        }
    }

    /**
     * Called when a ROI slice overlay has changed.
     */
    protected void sliceOverlayChanged(OverlayEvent event)
    {
        switch (event.getType())
        {
            case PAINTER_CHANGED:
                // forward the event to ROI stack overlay
                getOverlay().painterChanged();
                break;

            case PROPERTY_CHANGED:
                // forward the event to ROI stack overlay
                getOverlay().propertyChanged(event.getPropertyName());
                break;
        }
    }

    @Override
    public Rectangle4D computeBounds4D()
    {
        Rectangle3D xyzBounds = null;

        for (R slice : slices.values())
        {
            final Rectangle3D bnd3d = slice.getBounds3D();

            // only add non empty bounds
            if (!bnd3d.isEmpty())
            {
                if (xyzBounds == null)
                    xyzBounds = (Rectangle3D) bnd3d.clone();
                else
                    xyzBounds.add(bnd3d);
            }
        }

        // create empty 3D bounds
        if (xyzBounds == null)
            xyzBounds = new Rectangle3D.Double();

        final int t;
        final int sizeT;

        if (!slices.isEmpty())
        {
            t = slices.firstKey().intValue();
            sizeT = getSizeT();
        }
        else
        {
            t = 0;
            sizeT = 0;
        }

        return new Rectangle4D.Double(xyzBounds.getX(), xyzBounds.getY(), xyzBounds.getZ(), t, xyzBounds.getSizeX(),
                xyzBounds.getSizeY(), xyzBounds.getSizeZ(), sizeT);
    }

    @Override
    public boolean contains(double x, double y, double z, double t)
    {
        final R roi3d = getSlice((int) Math.floor(t));

        if (roi3d != null)
            return roi3d.contains(x, y, z);

        return false;
    }

    @Override
    public boolean contains(double x, double y, double z, double t, double sizeX, double sizeY, double sizeZ,
            double sizeT)
    {
        final Rectangle4D bounds = getBounds4D();

        // easy discard
        if (!bounds.contains(x, y, z, t, sizeX, sizeY, sizeZ, sizeT))
            return false;

        final int lim = (int) Math.floor(t + sizeT);
        for (int tc = (int) Math.floor(t); tc < lim; tc++)
        {
            final R roi3d = getSlice(tc);
            if ((roi3d == null) || !roi3d.contains(x, y, z, sizeX, sizeY, sizeZ))
                return false;
        }

        return true;
    }

    @Override
    public boolean intersects(double x, double y, double z, double t, double sizeX, double sizeY, double sizeZ,
            double sizeT)
    {
        final Rectangle4D bounds = getBounds4D();

        // easy discard
        if (!bounds.intersects(x, y, z, t, sizeX, sizeY, sizeZ, sizeT))
            return false;

        final int lim = (int) Math.floor(t + sizeT);
        for (int tc = (int) Math.floor(t); tc < lim; tc++)
        {
            final R roi3d = getSlice(tc);
            if ((roi3d != null) && roi3d.intersects(x, y, z, sizeX, sizeY, sizeZ))
                return true;
        }

        return false;
    }

    @Override
    public boolean hasSelectedPoint()
    {
        // default
        return false;
    }
    
    @Override
    public void unselectAllPoints()
    {
        beginUpdate();
        try
        {
            modifyingSlice.acquireUninterruptibly();
            try
            {
                for (R slice : slices.values())
                    slice.unselectAllPoints();
            }
            finally
            {
                modifyingSlice.release();
            }
        }
        finally
        {
            endUpdate();
        }
    }

    // default approximated implementation for ROI4DStack
    @Override
    public double computeNumberOfContourPoints()
    {
        // 4D contour points = first slice points + inter slices contour points + last slice points
        double result = 0;

        if (slices.size() <= 2)
        {
            for (R slice : slices.values())
                result += slice.getNumberOfPoints();
        }
        else
        {
            final Entry<Integer, R> firstEntry = slices.firstEntry();
            final Entry<Integer, R> lastEntry = slices.lastEntry();
            final Integer firstKey = firstEntry.getKey();
            final Integer lastKey = lastEntry.getKey();

            result = firstEntry.getValue().getNumberOfPoints();

            for (R slice : slices.subMap(firstKey, false, lastKey, false).values())
                result += slice.getNumberOfContourPoints();

            result += lastEntry.getValue().getNumberOfPoints();
        }

        return result;
    }

    @Override
    public double computeNumberOfPoints()
    {
        double volume = 0;

        for (R slice : slices.values())
            volume += slice.getNumberOfPoints();

        return volume;
    }

    @Override
    public boolean canTranslate()
    {
        // only need to test the first entry
        if (!slices.isEmpty())
            return slices.firstEntry().getValue().canTranslate();

        return false;
    }

    /**
     * Translate the stack of specified T position.
     */
    public void translate(int t)
    {
        // easy optimizations
        if ((t == 0) || isEmpty())
            return;

        final Map<Integer, R> map = new HashMap<Integer, R>(slices);

        slices.clear();
        for (Entry<Integer, R> entry : map.entrySet())
        {
            final R roi = entry.getValue();
            final int newT = roi.getT() + t;

            // only positive value accepted
            if (newT >= 0)
            {
                roi.setT(newT);
                slices.put(Integer.valueOf(newT), roi);
            }
        }

        // notify ROI changed
        roiChanged(false);
    }

    @Override
    public void translate(double dx, double dy, double dz, double dt)
    {
        beginUpdate();
        try
        {
            translateT += dt;
            // convert to integer
            final int dti = (int) translateT;
            // keep trace of not used floating part
            translateT -= dti;

            translate(dti);

            modifyingSlice.acquireUninterruptibly();
            try
            {
                for (R slice : slices.values())
                    slice.translate(dx, dy, dz);
            }
            finally
            {
                modifyingSlice.release();
            }

            // notify ROI changed because we modified slice 'internally'
            if ((dx != 0d) || (dy != 0d) || (dz != 0d))
                roiChanged(false);
        }
        finally
        {
            endUpdate();
        }
    }

    @Override
    public boolean[] getBooleanMask2D(int x, int y, int width, int height, int z, int t, boolean inclusive)
    {
        final R roi3d = getSlice(t);

        if (roi3d != null)
            return roi3d.getBooleanMask2D(x, y, width, height, z, inclusive);

        return new boolean[width * height];
    }

    @Override
    public BooleanMask2D getBooleanMask2D(int z, int t, boolean inclusive)
    {
        final R roi3d = getSlice(t);

        if (roi3d != null)
            return roi3d.getBooleanMask2D(z, inclusive);

        return new BooleanMask2D(new Rectangle(), new boolean[0]);
    }

    // called when one of the slice ROI changed
    @Override
    public void roiChanged(ROIEvent event)
    {
        // propagate children change event
        sliceChanged(event);
    }

    // called when one of the slice ROI overlay changed
    @Override
    public void overlayChanged(OverlayEvent event)
    {
        // propagate children overlay change event
        sliceOverlayChanged(event);
    }

    @Override
    public Iterator<R> iterator()
    {
        return slices.values().iterator();
    }

    @Override
    public boolean loadFromXML(Node node)
    {
        beginUpdate();
        try
        {
            if (!super.loadFromXML(node))
                return false;

            // we don't need to save the 3D ROI class as the parent class already do it
            clear();

            for (Element e : XMLUtil.getElements(node, "slice"))
            {
                // faster than using complete XML serialization
                final R slice = createSlice();

                // error while reloading the ROI from XML
                if ((slice == null) || !slice.loadFromXML(e))
                    return false;

                setSlice(slice.getT(), slice);
            }
        }
        finally
        {
            endUpdate();
        }

        return true;
    }

    @Override
    public boolean saveToXML(Node node)
    {
        if (!super.saveToXML(node))
            return false;

        for (R slice : slices.values())
        {
            Element sliceNode = XMLUtil.addElement(node, "slice");

            if (!slice.saveToXML(sliceNode))
                return false;
        }

        return true;
    }

    public class ROI4DStackPainter extends ROIPainter
    {
        R getSliceForCanvas(IcyCanvas canvas)
        {
            final int t = canvas.getPositionT();

            if (t >= 0)
                return getSlice(t);

            return null;
        }

        @Override
        public void paint(Graphics2D g, Sequence sequence, IcyCanvas canvas)
        {
            if (isActiveFor(canvas))
            {
                if (canvas instanceof IcyCanvas3D)
                {
                    // TODO

                }
                else if (canvas instanceof IcyCanvas2D)
                {
                    // forward event to current slice
                    final R slice = getSliceForCanvas(canvas);

                    if (slice != null)
                        slice.getOverlay().paint(g, sequence, canvas);
                }
            }
        }

        @Override
        public void keyPressed(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // send event to parent first
            super.keyPressed(e, imagePoint, canvas);

            // then send it to active slice
            if (isActiveFor(canvas))
            {
                // forward event to current slice
                final R slice = getSliceForCanvas(canvas);

                if (slice != null)
                    slice.getOverlay().keyPressed(e, imagePoint, canvas);
            }
        }

        @Override
        public void keyReleased(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // send event to parent first
            super.keyReleased(e, imagePoint, canvas);

            // then send it to active slice
            if (isActiveFor(canvas))
            {
                // forward event to current slice
                final R slice = getSliceForCanvas(canvas);

                if (slice != null)
                    slice.getOverlay().keyReleased(e, imagePoint, canvas);
            }
        }

        @Override
        public void mouseEntered(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // send event to parent first
            super.mouseEntered(e, imagePoint, canvas);

            // then send it to active slice
            if (isActiveFor(canvas))
            {
                // forward event to current slice
                final R slice = getSliceForCanvas(canvas);

                if (slice != null)
                    slice.getOverlay().mouseEntered(e, imagePoint, canvas);
            }
        }

        @Override
        public void mouseExited(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // send event to parent first
            super.mouseExited(e, imagePoint, canvas);

            // then send it to active slice
            if (isActiveFor(canvas))
            {
                // forward event to current slice
                final R slice = getSliceForCanvas(canvas);

                if (slice != null)
                    slice.getOverlay().mouseExited(e, imagePoint, canvas);
            }
        }

        @Override
        public void mouseMove(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // send event to parent first
            super.mouseMove(e, imagePoint, canvas);

            // then send it to active slice
            if (isActiveFor(canvas))
            {
                // forward event to current slice
                final R slice = getSliceForCanvas(canvas);

                if (slice != null)
                    slice.getOverlay().mouseMove(e, imagePoint, canvas);
            }
        }

        @Override
        public void mouseDrag(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // send event to parent first
            super.mouseDrag(e, imagePoint, canvas);

            // then send it to active slice
            if (isActiveFor(canvas))
            {
                // forward event to current slice
                final R slice = getSliceForCanvas(canvas);

                if (slice != null)
                    slice.getOverlay().mouseDrag(e, imagePoint, canvas);
            }
        }

        @Override
        public void mousePressed(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // send event to parent first
            super.mousePressed(e, imagePoint, canvas);

            // then send it to active slice
            if (isActiveFor(canvas))
            {
                // forward event to current slice
                final R slice = getSliceForCanvas(canvas);

                if (slice != null)
                    slice.getOverlay().mousePressed(e, imagePoint, canvas);
            }
        }

        @Override
        public void mouseReleased(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // send event to parent first
            super.mouseReleased(e, imagePoint, canvas);

            // then send it to active slice
            if (isActiveFor(canvas))
            {
                // forward event to current slice
                final R slice = getSliceForCanvas(canvas);

                if (slice != null)
                    slice.getOverlay().mouseReleased(e, imagePoint, canvas);
            }
        }

        @Override
        public void mouseClick(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // send event to parent first
            super.mouseClick(e, imagePoint, canvas);

            // then send it to active slice
            if (isActiveFor(canvas))
            {
                // forward event to current slice
                final R slice = getSliceForCanvas(canvas);

                if (slice != null)
                    slice.getOverlay().mouseClick(e, imagePoint, canvas);
            }
        }

        @Override
        public void mouseWheelMoved(MouseWheelEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // send event to parent first
            super.mouseWheelMoved(e, imagePoint, canvas);

            // then send it to active slice
            if (isActiveFor(canvas))
            {
                // forward event to current slice
                final R slice = getSliceForCanvas(canvas);

                if (slice != null)
                    slice.getOverlay().mouseWheelMoved(e, imagePoint, canvas);
            }
        }
    }
}