/*
 * 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.roi5d;

import java.awt.geom.Point2D;
import java.util.Map.Entry;

import icy.roi.BooleanMask4D;
import icy.roi.BooleanMask5D;
import icy.roi.ROI;
import icy.roi.ROI4D;
import icy.type.point.Point5D;
import icy.type.rectangle.Rectangle5D;
import plugins.kernel.roi.roi4d.ROI4DArea;

/**
 * 5D Area ROI.
 * 
 * @author Stephane
 */
public class ROI5DArea extends ROI5DStack<ROI4DArea>
{
    public ROI5DArea()
    {
        super(ROI4DArea.class);

        setName("4D area");
    }

    public ROI5DArea(Point5D pt)
    {
        this();

        addBrush(pt.toPoint2D(), (int) pt.getZ(), (int) pt.getT(), (int) pt.getC());
    }

    /**
     * Create a 3D Area ROI type from the specified {@link BooleanMask5D}.
     */
    public ROI5DArea(BooleanMask5D mask)
    {
        this();

        setAsBooleanMask(mask);
    }

    /**
     * Create a copy of the specified 5D Area ROI.
     */
    public ROI5DArea(ROI5DArea area)
    {
        this();

        // copy the source 4D area ROI
        for (Entry<Integer, ROI4DArea> entry : area.slices.entrySet())
            slices.put(entry.getKey(), new ROI4DArea(entry.getValue()));

        roiChanged(true);
    }

    @Override
    public String getDefaultName()
    {
        return "Area5D";
    }

    /**
     * Adds the specified point to this ROI
     */
    public void addPoint(int x, int y, int z, int t, int c)
    {
        setPoint(x, y, z, t, c, true);
    }

    /**
     * Remove a point from the mask.<br>
     * Don't forget to call optimizeBounds() after consecutive remove operation
     * to refresh the mask bounds.
     */
    public void removePoint(int x, int y, int z, int t, int c)
    {
        setPoint(x, y, z, t, c, false);
    }

    /**
     * Set the value for the specified point in the mask.
     * Don't forget to call optimizeBounds() after consecutive remove point operation
     * to refresh the mask bounds.
     */
    public void setPoint(int x, int y, int z, int t, int c, boolean value)
    {
        final ROI4DArea slice = getSlice(c, value);

        if (slice != null)
            slice.setPoint(x, y, z, t, value);
    }

    /**
     * Add brush point at specified position and for specified Z,T,C slice.
     */
    public void addBrush(Point2D pos, int z, int t, int c)
    {
        getSlice(c, true).addBrush(pos, z, t);
    }

    /**
     * Remove brush point from the mask at specified position and for specified Z,T,C slice.<br>
     * Don't forget to call optimizeBounds() after consecutive remove operation
     * to refresh the mask bounds.
     */
    public void removeBrush(Point2D pos, int z, int t, int c)
    {
        final ROI4DArea slice = getSlice(c, false);

        if (slice != null)
            slice.removeBrush(pos, z, t);
    }

    /**
     * Sets the ROI slice at given C position to this 5D ROI
     * 
     * @param c
     *        the position where the slice must be set
     * @param roiSlice
     *        the 4D ROI to set
     * @param merge
     *        <code>true</code> if the given slice should be merged with the existing slice, or
     *        <code>false</code> to
     *        replace the existing slice.
     */
    public void setSlice(int c, ROI4D roiSlice, boolean merge)
    {
        if (roiSlice == null)
            throw new IllegalArgumentException("Cannot add an empty slice in a 5D ROI");

        final ROI4DArea currentSlice = getSlice(c);
        final ROI newSlice;

        // merge both slice
        if ((currentSlice != null) && merge)
        {
            // we need to modify the C position so we do the merge correctly
            roiSlice.setC(c);
            // do ROI union
            newSlice = currentSlice.getUnion(roiSlice);
        }
        else
            newSlice = roiSlice;

        if (newSlice instanceof ROI4DArea)
            setSlice(c, (ROI4DArea) newSlice);
        else if (newSlice instanceof ROI4D)
            setSlice(c, new ROI4DArea(((ROI4D) newSlice).getBooleanMask(true)));
        else
            throw new IllegalArgumentException(
                    "Can't add the result of the merge operation on 4D slice " + c + ": " + newSlice.getClassName());
    }

    /**
     * Returns true if the ROI is empty (the mask does not contains any point).
     */
    @Override
    public boolean isEmpty()
    {
        for (ROI4DArea area : slices.values())
            if (!area.isEmpty())
                return false;

        return true;
    }

    /**
     * Set the mask from a BooleanMask5D object<br>
     * If specified mask is <i>null</i> then ROI is cleared.
     */
    public void setAsBooleanMask(BooleanMask5D mask)
    {
        // mask empty ? --> just clear the ROI
        if ((mask == null) || mask.isEmpty())
            clear();
        else
        {
            final Rectangle5D.Integer bounds5d = mask.bounds;
            final int startC = bounds5d.c;
            final int sizeC = bounds5d.sizeC;
            final BooleanMask4D masks4d[] = new BooleanMask4D[sizeC];

            for (int c = 0; c < sizeC; c++)
                masks4d[c] = mask.getMask4D(startC + c);

            setAsBooleanMask(bounds5d, masks4d);
        }
    }

    /**
     * Set the 5D mask from a 4D boolean mask array
     * 
     * @param rect
     *        the 5D region defined by 4D boolean mask array
     * @param mask
     *        the 5D mask data (array length should be equals to rect.sizeC)
     */
    public void setAsBooleanMask(Rectangle5D.Integer rect, BooleanMask4D[] mask)
    {
        if (rect.isInfiniteC())
            throw new IllegalArgumentException("Cannot set infinite C dimension on the 5D Area ROI.");

        beginUpdate();
        try
        {
            clear();

            for (int c = 0; c < rect.sizeC; c++)
                setSlice(c + rect.c, new ROI4DArea(mask[c]));
        }
        finally
        {
            endUpdate();
        }
    }

    /**
     * Optimize the bounds size to the minimum surface which still include all mask.<br>
     * You should call it after consecutive remove operations.
     */
    public void optimizeBounds()
    {
        final Rectangle5D.Integer bounds = getBounds();

        beginUpdate();
        try
        {
            for (int c = bounds.c; c < bounds.c + bounds.sizeC; c++)
            {
                final ROI4DArea roi = getSlice(c);

                if (roi != null)
                {
                    if (roi.isEmpty())
                        removeSlice(c);
                    else
                        roi.optimizeBounds();
                }
            }
        }
        finally
        {
            endUpdate();
        }
    }
}