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

import icy.canvas.IcyCanvas;
import icy.type.point.Point5D;
import icy.type.rectangle.Rectangle3D;
import icy.type.rectangle.Rectangle4D;
import icy.type.rectangle.Rectangle5D;

import java.util.ArrayList;
import java.util.List;

/**
 * 5D ROI base class.
 */
public abstract class ROI5D extends ROI
{
    /**
     * @deprecated Use {@link ROI5D#getROI5DList(List)} instead.
     */
    @Deprecated
    public static ArrayList<ROI5D> getROI5DList(ArrayList<ROI> rois)
    {
        final ArrayList<ROI5D> result = new ArrayList<ROI5D>();

        for (ROI roi : rois)
            if (roi instanceof ROI5D)
                result.add((ROI5D) roi);

        return result;
    }

    /**
     * Return all 5D ROI from the ROI list
     */
    public static List<ROI5D> getROI5DList(List<ROI> rois)
    {
        final List<ROI5D> result = new ArrayList<ROI5D>();

        for (ROI roi : rois)
            if (roi instanceof ROI5D)
                result.add((ROI5D) roi);

        return result;
    }

    public ROI5D()
    {
        super();
    }

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

    @Override
    final public int getDimension()
    {
        return 5;
    }

    @Override
    public boolean isActiveFor(IcyCanvas canvas)
    {
        return true;
    }

    /**
     * Returns an integer {@link Rectangle5D} that completely encloses the <code>ROI</code>. Note
     * that there is no guarantee that the returned <code>Rectangle5D</code> is the smallest
     * bounding box that encloses the <code>ROI</code>, only that the <code>ROI</code> lies entirely
     * within the indicated <code>Rectangle5D</code>. The returned <code>Rectangle5D</code> might
     * also fail to completely enclose the <code>ROI</code> if the <code>ROI</code> overflows the
     * limited range of the integer data type. The <code>getBounds5D</code> method generally returns
     * a tighter bounding box due to its greater flexibility in representation.
     * 
     * @return an integer <code>Rectangle5D</code> that completely encloses the <code>ROI</code>.
     */
    public Rectangle5D.Integer getBounds()
    {
        return getBounds5D().toInteger();
    }

    /**
     * Returns the integer ROI position which normally correspond to the <i>minimum</i> point of the
     * ROI bounds.
     * 
     * @see #getBounds()
     */
    public Point5D.Integer getPosition()
    {
        final Rectangle5D.Integer bounds = getBounds();
        return new Point5D.Integer(bounds.x, bounds.y, bounds.z, bounds.t, bounds.c);
    }

    @Override
    public boolean canSetBounds()
    {
        // default
        return false;
    }

    @Override
    public void setBounds5D(Rectangle5D bounds)
    {
        // do nothing by default (not supported)
    }

    @Override
    public boolean canSetPosition()
    {
        // default implementation use translation if available
        return canTranslate();
    }

    @Override
    public void setPosition5D(Point5D position)
    {
        // use translation operation by default if supported
        if (canTranslate())
        {
            final Point5D oldPos = getPosition5D();
            translate(position.getX() - oldPos.getX(), position.getY() - oldPos.getY(),
                    position.getZ() - oldPos.getZ(), position.getT() - oldPos.getT(), position.getC() - oldPos.getC());
        }
    }

    /**
     * Returns <code>true</code> if the ROI support translate operation.
     * 
     * @see #translate(double, double, double, double, double)
     */
    public boolean canTranslate()
    {
        // by default
        return false;
    }

    /**
     * Translate the ROI position by the specified delta X/Y/Z/T.<br>
     * Note that not all ROI support this operation so you should test it by calling {@link #canTranslate()} first.
     * 
     * @param dx
     *        translation value to apply on X dimension
     * @param dy
     *        translation value to apply on Y dimension
     * @param dz
     *        translation value to apply on Z dimension
     * @param dt
     *        translation value to apply on T dimension
     * @param dc
     *        translation value to apply on C dimension
     * @see #canTranslate()
     * @see #setPosition5D(Point5D)
     */
    public void translate(double dx, double dy, double dz, double dt, double dc)
    {

    }

    /*
     * Generic implementation using the BooleanMask which is not accurate and slow.
     * Override this for specific ROI type.
     */
    @Override
    public boolean contains(ROI roi)
    {
        if (roi instanceof ROI5D)
        {
            final ROI5D roi5d = (ROI5D) roi;

            // special case of ROI Point
            if (roi5d.isEmpty())
                return contains(roi5d.getPosition5D());

            BooleanMask5D mask;
            BooleanMask5D roiMask;

            // take content first
            mask = getBooleanMask(false);
            roiMask = roi5d.getBooleanMask(false);

            // test first only on content
            if (!mask.contains(roiMask))
                return false;

            // take content and edge
            mask = getBooleanMask(true);
            roiMask = roi5d.getBooleanMask(true);

            // then test on content and edge
            if (!mask.contains(roiMask))
                return false;

            // contained
            return true;
        }

        // use default implementation
        return super.contains(roi);
    }

    /*
     * Generic implementation using the BooleanMask which is not accurate and slow.
     * Override this for specific ROI type.
     */
    @Override
    public boolean intersects(ROI roi)
    {
        if (roi instanceof ROI5D)
            return getBooleanMask(true).intersects(((ROI5D) roi).getBooleanMask(true));

        // use default implementation
        return super.intersects(roi);
    }

    @Override
    public BooleanMask2D getBooleanMask2D(int z, int t, int c, boolean inclusive)
    {
        final BooleanMask2D result = super.getBooleanMask2D(z, t, c, inclusive);

        // optimized bounds to optimize memory usage for this specific Z, T, C slice mask
        result.optimizeBounds();

        return result;
    }

    /**
     * Returns the {@link BooleanMask3D} object representing the XYZ volume content at specified Z,
     * T, C position.<br>
     * It contains the 3D rectangle mask bounds and the associated boolean array mask.<br>
     * 
     * @param z
     *        Z position we want to retrieve the boolean mask or -1 to retrieve the whole Z
     *        dimension
     * @param t
     *        T position we want to retrieve the boolean mask.
     * @param c
     *        C position we want to retrieve the boolean mask.
     * @param inclusive
     *        If true then all partially contained (intersected) pixels are included in the mask.
     */
    public BooleanMask3D getBooleanMask3D(int z, int t, int c, boolean inclusive)
    {
        // whole Z dimension
        if (z == -1)
            return getBooleanMask3D(t, c, inclusive);

        // define bounds
        final Rectangle3D.Integer bounds = getBounds5D().toRectangle3D().toInteger();
        bounds.setZ(z);
        bounds.setSizeZ(1);

        return new BooleanMask3D(bounds, new BooleanMask2D[] {getBooleanMask2D(z, t, c, inclusive)});
    }

    /**
     * Returns the {@link BooleanMask3D} object representing the XYZ volume content at specified T,
     * C position.<br>
     * It contains the 3D rectangle mask bounds and the associated boolean array mask.<br>
     * 
     * @param inclusive
     *        If true then all partially contained (intersected) pixels are included in the mask.
     */
    public BooleanMask3D getBooleanMask3D(int t, int c, boolean inclusive)
    {
        final Rectangle3D.Integer bounds = getBounds5D().toRectangle3D().toInteger();
        final BooleanMask2D masks[] = new BooleanMask2D[bounds.sizeZ];

        for (int z = 0; z < masks.length; z++)
            masks[z] = getBooleanMask2D(bounds.z + z, t, c, inclusive);

        return new BooleanMask3D(bounds, masks);
    }

    /**
     * Returns the {@link BooleanMask4D} object representing the XYZT space content at specified Z
     * and C position.
     * 
     * @param z
     *        Z position we want to retrieve the boolean mask or -1 to retrieve the whole Z
     *        dimension
     * @param t
     *        T position we want to retrieve the boolean mask or -1 to retrieve the whole T
     *        dimension
     * @param c
     *        C position we want to retrieve the boolean mask.
     * @param inclusive
     *        If true then all partially contained (intersected) pixels are included in the mask.
     */
    public BooleanMask4D getBooleanMask4D(int z, int t, int c, boolean inclusive)
    {
        // whole Z dimension
        if (z == -1)
        {
            // whole Z and T dimension
            if (t == -1)
                return getBooleanMask4D(c, inclusive);

            // define bounds
            final Rectangle4D.Integer bounds = getBounds5D().toRectangle4D().toInteger();
            bounds.setT(t);
            bounds.setSizeT(1);

            // whole Z dimension but specific T
            return new BooleanMask4D(bounds, new BooleanMask3D[] {getBooleanMask3D(t, c, inclusive)});
        }

        final Rectangle4D.Integer bounds4d = getBounds5D().toRectangle4D().toInteger();

        // specific Z
        bounds4d.setZ(z);
        bounds4d.setSizeZ(1);

        // specific T dimension ?
        if (t != -1)
        {
            bounds4d.setT(t);
            bounds4d.setSizeT(1);
        }

        final Rectangle3D.Integer bounds3d = (Rectangle3D.Integer) bounds4d.toRectangle3D();
        final BooleanMask3D masks[] = new BooleanMask3D[bounds4d.sizeT];

        for (int i = 0; i < bounds4d.sizeT; i++)
            masks[i] = new BooleanMask3D((Rectangle3D.Integer) bounds3d.clone(), new BooleanMask2D[] {getBooleanMask2D(
                    z, bounds4d.t + i, c, inclusive)});

        return new BooleanMask4D(bounds4d, masks);
    }

    /**
     * Get the {@link BooleanMask4D} object representing the roi for specified C position.<br>
     * It contains the 4D rectangle mask bounds and the associated boolean array mask.<br>
     * 
     * @param inclusive
     *        If true then all partially contained (intersected) pixels are included in the mask.
     */
    public BooleanMask4D getBooleanMask4D(int c, boolean inclusive)
    {
        final Rectangle4D.Integer bounds = getBounds5D().toRectangle4D().toInteger();
        final BooleanMask3D masks[] = new BooleanMask3D[bounds.sizeT];

        for (int t = 0; t < masks.length; t++)
            masks[t] = getBooleanMask3D(bounds.t + t, c, inclusive);

        return new BooleanMask4D(bounds, masks);
    }

    /**
     * Returns the {@link BooleanMask5D} object representing the XYZTC space content at specified Z,
     * T, C position.
     * 
     * @param z
     *        Z position we want to retrieve the boolean mask or -1 to retrieve the whole Z
     *        dimension
     * @param t
     *        T position we want to retrieve the boolean mask or -1 to retrieve the whole T
     *        dimension
     * @param c
     *        C position we want to retrieve the boolean mask or -1 to retrieve the whole C
     *        dimension
     * @param inclusive
     *        If true then all partially contained (intersected) pixels are included in the mask.
     */
    public BooleanMask5D getBooleanMask5D(int z, int t, int c, boolean inclusive)
    {
        // whole Z dimension
        if (z == -1)
        {
            // whole Z and T dimension
            if (t == -1)
            {
                // whole Z, T and C dimension
                if (c == -1)
                    return getBooleanMask(inclusive);

                // define bounds
                final Rectangle5D.Integer bounds = getBounds5D().toInteger();
                bounds.setC(c);
                bounds.setSizeC(1);

                // whole Z and T dimension but specific C
                return new BooleanMask5D(bounds, new BooleanMask4D[] {getBooleanMask4D(c, inclusive)});
            }

            final Rectangle5D.Integer bounds5d = getBounds();

            // specific T
            bounds5d.setT(t);
            bounds5d.setSizeT(1);
            // specific C dimension ?
            if (c != -1)
            {
                bounds5d.setC(c);
                bounds5d.setSizeC(1);
            }

            final Rectangle4D.Integer bounds4d = (Rectangle4D.Integer) bounds5d.toRectangle4D();
            final BooleanMask4D masks[] = new BooleanMask4D[bounds5d.sizeC];

            for (int i = 0; i < bounds5d.sizeC; i++)
                masks[i] = new BooleanMask4D((Rectangle4D.Integer) bounds4d.clone(),
                        new BooleanMask3D[] {getBooleanMask3D(t, bounds5d.c + i, inclusive)});

            return new BooleanMask5D(bounds5d, masks);
        }

        final Rectangle5D.Integer bounds5d = getBounds();

        // specific Z
        bounds5d.setZ(z);
        bounds5d.setSizeZ(1);
        // specific T dimension ?
        if (t != -1)
        {
            bounds5d.setT(t);
            bounds5d.setSizeT(1);
        }
        // specific C dimension ?
        if (c != -1)
        {
            bounds5d.setC(c);
            bounds5d.setSizeC(1);
        }

        final Rectangle4D.Integer bounds4d = (Rectangle4D.Integer) bounds5d.toRectangle4D();
        final Rectangle3D.Integer bounds3d = (Rectangle3D.Integer) bounds4d.toRectangle3D();
        final BooleanMask4D masks[] = new BooleanMask4D[bounds5d.sizeC];

        for (int i = 0; i < bounds5d.sizeC; i++)
        {
            final BooleanMask3D masks3d[] = new BooleanMask3D[bounds4d.sizeT];

            for (int j = 0; j < bounds5d.sizeT; j++)
                masks3d[i] = new BooleanMask3D((Rectangle3D.Integer) bounds3d.clone(),
                        new BooleanMask2D[] {getBooleanMask2D(z, bounds5d.t + j, bounds5d.c + i, inclusive)});

            masks[i] = new BooleanMask4D((Rectangle4D.Integer) bounds4d.clone(), masks3d);
        }

        return new BooleanMask5D(bounds5d, masks);
    }

    /**
     * Get the {@link BooleanMask5D} object representing the roi.<br>
     * It contains the 5D rectangle mask bounds and the associated boolean array mask.<br>
     * 
     * @param inclusive
     *        If true then all partially contained (intersected) pixels are included in the mask.
     */
    public BooleanMask5D getBooleanMask(boolean inclusive)
    {
        final Rectangle5D.Integer bounds = getBounds();
        final BooleanMask4D masks[] = new BooleanMask4D[bounds.sizeC];

        for (int c = 0; c < masks.length; c++)
            masks[c] = getBooleanMask4D(bounds.c + c, inclusive);

        return new BooleanMask5D(bounds, masks);
    }

    /*
     * Generic implementation for ROI5D using the BooleanMask object so
     * the result is just an approximation.
     * Override to optimize for specific ROI.
     */
    @Override
    public double computeNumberOfContourPoints()
    {
        // approximation by using number of point of the edge of boolean mask
        return getBooleanMask(true).getContourPointsAsIntArray().length / getDimension();
    }

    /*
     * Generic implementation for ROI5D using the BooleanMask object so
     * the result is just an approximation.
     * Override to optimize for specific ROI.
     */
    @Override
    public double computeNumberOfPoints()
    {
        double numPoints = 0;

        // approximation by using number of point of boolean mask with and without border
        numPoints += getBooleanMask(true).getNumberOfPoints();
        numPoints += getBooleanMask(false).getNumberOfPoints();
        numPoints /= 2d;

        return numPoints;
    }

}