/**
 * 
 */
package icy.roi;

import icy.type.collection.array.DynamicArray;
import icy.type.point.Point5D;
import icy.type.rectangle.Rectangle3D;
import icy.type.rectangle.Rectangle4D;
import icy.type.rectangle.Rectangle5D;

import java.awt.Rectangle;
import java.util.Map.Entry;
import java.util.TreeMap;

/**
 * Class to define a 5D boolean mask region and make basic boolean operation between masks.<br>
 * The bounds property of this object represents the region defined by the boolean mask.
 * 
 * @author Stephane
 */
public class BooleanMask5D
{
    // Internal use only
    private static BooleanMask4D doUnion4D(BooleanMask4D m1, BooleanMask4D m2)
    {
        if (m1 == null)
        {
            // only use the 3D mask from second mask
            if (m2 != null)
                return (BooleanMask4D) m2.clone();

            return null;
        }
        else if (m2 == null)
            // only use the 3D mask from first mask
            return (BooleanMask4D) m1.clone();

        // process union of 3D mask
        return BooleanMask4D.getUnion(m1, m2);
    }

    // Internal use only
    private static BooleanMask4D doIntersection4D(BooleanMask4D m1, BooleanMask4D m2)
    {
        if ((m1 == null) || (m2 == null))
            return null;

        // process intersection of 3D mask
        return BooleanMask4D.getIntersection(m1, m2);
    }

    // Internal use only
    private static BooleanMask4D doExclusiveUnion4D(BooleanMask4D m1, BooleanMask4D m2)
    {
        if (m1 == null)
        {
            // only use the 3D mask from second mask
            if (m2 != null)
                return (BooleanMask4D) m2.clone();

            return null;
        }
        else if (m2 == null)
            // only use the 3D mask from first mask
            return (BooleanMask4D) m1.clone();

        // process exclusive union of 3D mask
        return BooleanMask4D.getExclusiveUnion(m1, m2);
    }

    // Internal use only
    private static BooleanMask4D doSubtraction4D(BooleanMask4D m1, BooleanMask4D m2)
    {
        if (m1 == null)
            return null;
        // only use the 3D mask from first mask
        if (m2 == null)
            return (BooleanMask4D) m1.clone();

        // process subtraction of 3D mask
        return BooleanMask4D.getSubtraction(m1, m2);
    }

    /**
     * Build resulting mask from union of the mask1 and mask2:
     * 
     * <pre>
     *        mask1          +       mask2        =      result
     * 
     *     ################     ################     ################
     *     ##############         ##############     ################
     *     ############             ############     ################
     *     ##########                 ##########     ################
     *     ########                     ########     ################
     *     ######                         ######     ######    ######
     *     ####                             ####     ####        ####
     *     ##                                 ##     ##            ##
     * </pre>
     */
    public static BooleanMask5D getUnion(BooleanMask5D mask1, BooleanMask5D mask2)
    {
        if ((mask1 == null) && (mask2 == null))
            return new BooleanMask5D();

        if ((mask1 == null) || mask1.isEmpty())
            return (BooleanMask5D) mask2.clone();
        if ((mask2 == null) || mask2.isEmpty())
            return (BooleanMask5D) mask1.clone();

        final Rectangle5D.Integer bounds = (Rectangle5D.Integer) mask1.bounds.createUnion(mask2.bounds);

        if (!bounds.isEmpty())
        {
            final BooleanMask4D[] mask;

            // special case of infinite C dimension
            if (bounds.sizeC == Integer.MAX_VALUE)
            {
                // we can allow merge ROI only if they both has infinite C dimension
                if ((mask1.bounds.sizeC != Integer.MAX_VALUE) || (mask2.bounds.sizeC != Integer.MAX_VALUE))
                    throw new UnsupportedOperationException(
                            "Cannot merge an infinite C dimension ROI with  a finite Z dimension ROI");

                mask = new BooleanMask4D[1];

                final BooleanMask4D m2d1 = mask1.mask.firstEntry().getValue();
                final BooleanMask4D m2d2 = mask2.mask.firstEntry().getValue();

                mask[0] = doUnion4D(m2d1, m2d2);
            }
            else
            {
                mask = new BooleanMask4D[bounds.sizeT];

                for (int c = 0; c < bounds.sizeC; c++)
                {
                    final BooleanMask4D m2d1 = mask1.getMask4D(c + bounds.c);
                    final BooleanMask4D m2d2 = mask2.getMask4D(c + bounds.c);

                    mask[c] = doUnion4D(m2d1, m2d2);
                }
            }

            return new BooleanMask5D(bounds, mask);
        }

        return new BooleanMask5D();
    }

    /**
     * Build resulting mask from intersection of the mask1 and mask2:
     * 
     * <pre>
     *        mask1     intersect     mask2      =        result
     * 
     *     ################     ################     ################
     *     ##############         ##############       ############
     *     ############             ############         ########
     *     ##########                 ##########           ####
     *     ########                     ########
     *     ######                         ######
     *     ####                             ####
     *     ##                                 ##
     * </pre>
     */
    public static BooleanMask5D getIntersection(BooleanMask5D mask1, BooleanMask5D mask2)
    {
        if ((mask1 == null) || (mask2 == null))
            return new BooleanMask5D();

        final Rectangle5D.Integer bounds = (Rectangle5D.Integer) mask1.bounds.createIntersection(mask2.bounds);

        if (!bounds.isEmpty())
        {
            final BooleanMask4D[] mask;

            // special case of infinite C dimension
            if (bounds.sizeC == Integer.MAX_VALUE)
            {
                // we can allow merge ROI only if they both has infinite C dimension
                if ((mask1.bounds.sizeC != Integer.MAX_VALUE) || (mask2.bounds.sizeC != Integer.MAX_VALUE))
                    throw new UnsupportedOperationException(
                            "Cannot merge an infinite C dimension ROI with  a finite Z dimension ROI");

                mask = new BooleanMask4D[1];

                final BooleanMask4D m2d1 = mask1.mask.firstEntry().getValue();
                final BooleanMask4D m2d2 = mask2.mask.firstEntry().getValue();

                mask[0] = doIntersection4D(m2d1, m2d2);
            }
            else
            {
                mask = new BooleanMask4D[bounds.sizeT];

                for (int c = 0; c < bounds.sizeC; c++)
                {
                    final BooleanMask4D m2d1 = mask1.getMask4D(c + bounds.c);
                    final BooleanMask4D m2d2 = mask2.getMask4D(c + bounds.c);

                    mask[c] = doIntersection4D(m2d1, m2d2);
                }
            }

            return new BooleanMask5D(bounds, mask);
        }

        return new BooleanMask5D();
    }

    /**
     * Build resulting mask from exclusive union of the mask1 and mask2:
     * 
     * <pre>
     *          mask1       xor      mask2        =       result
     * 
     *     ################     ################
     *     ##############         ##############     ##            ##
     *     ############             ############     ####        ####
     *     ##########                 ##########     ######    ######
     *     ########                     ########     ################
     *     ######                         ######     ######    ######
     *     ####                             ####     ####        ####
     *     ##                                 ##     ##            ##
     * </pre>
     */
    public static BooleanMask5D getExclusiveUnion(BooleanMask5D mask1, BooleanMask5D mask2)
    {
        if ((mask1 == null) && (mask2 == null))
            return new BooleanMask5D();

        if ((mask1 == null) || mask1.isEmpty())
            return (BooleanMask5D) mask2.clone();
        if ((mask2 == null) || mask2.isEmpty())
            return (BooleanMask5D) mask1.clone();

        final Rectangle5D.Integer bounds = (Rectangle5D.Integer) mask1.bounds.createUnion(mask2.bounds);

        if (!bounds.isEmpty())
        {
            final BooleanMask4D[] mask;

            // special case of infinite C dimension
            if (bounds.sizeC == Integer.MAX_VALUE)
            {
                // we can allow merge ROI only if they both has infinite C dimension
                if ((mask1.bounds.sizeC != Integer.MAX_VALUE) || (mask2.bounds.sizeC != Integer.MAX_VALUE))
                    throw new UnsupportedOperationException(
                            "Cannot merge an infinite C dimension ROI with  a finite Z dimension ROI");

                mask = new BooleanMask4D[1];

                final BooleanMask4D m2d1 = mask1.mask.firstEntry().getValue();
                final BooleanMask4D m2d2 = mask2.mask.firstEntry().getValue();

                mask[0] = doExclusiveUnion4D(m2d1, m2d2);
            }
            else
            {
                mask = new BooleanMask4D[bounds.sizeT];

                for (int c = 0; c < bounds.sizeC; c++)
                {
                    final BooleanMask4D m2d1 = mask1.getMask4D(c + bounds.c);
                    final BooleanMask4D m2d2 = mask2.getMask4D(c + bounds.c);

                    mask[c] = doExclusiveUnion4D(m2d1, m2d2);
                }
            }

            return new BooleanMask5D(bounds, mask);
        }

        return new BooleanMask5D();
    }

    /**
     * Build resulting mask from the subtraction of mask2 from mask1:
     * 
     * <pre>
     *        mask1          -        mask2       =  result
     * 
     *     ################     ################
     *     ##############         ##############     ##
     *     ############             ############     ####
     *     ##########                 ##########     ######
     *     ########                     ########     ########
     *     ######                         ######     ######
     *     ####                             ####     ####
     *     ##                                 ##     ##
     * </pre>
     */
    public static BooleanMask5D getSubtraction(BooleanMask5D mask1, BooleanMask5D mask2)
    {
        if (mask1 == null)
            return new BooleanMask5D();
        if (mask2 == null)
            return (BooleanMask5D) mask1.clone();

        final Rectangle5D.Integer bounds = (Rectangle5D.Integer) mask1.bounds.createIntersection(mask2.bounds);

        // need to subtract something ?
        if (!bounds.isEmpty())
        {
            final BooleanMask4D[] mask;

            // special case of infinite C dimension
            if (bounds.sizeC == Integer.MAX_VALUE)
            {
                // we can allow merge ROI only if they both has infinite C dimension
                if ((mask1.bounds.sizeC != Integer.MAX_VALUE) || (mask2.bounds.sizeC != Integer.MAX_VALUE))
                    throw new UnsupportedOperationException(
                            "Cannot merge an infinite C dimension ROI with  a finite Z dimension ROI");

                mask = new BooleanMask4D[1];

                final BooleanMask4D m2d1 = mask1.mask.firstEntry().getValue();
                final BooleanMask4D m2d2 = mask2.mask.firstEntry().getValue();

                mask[0] = doSubtraction4D(m2d1, m2d2);
            }
            else
            {
                mask = new BooleanMask4D[bounds.sizeC];

                for (int c = 0; c < bounds.sizeC; c++)
                {
                    final BooleanMask4D m2d1 = mask1.getMask4D(c + bounds.c);
                    final BooleanMask4D m2d2 = mask2.getMask4D(c + bounds.c);

                    mask[c] = doSubtraction4D(m2d1, m2d2);
                }
            }

            return new BooleanMask5D(bounds, mask);
        }

        return (BooleanMask5D) mask1.clone();
    }

    /**
     * Region represented by the mask.
     */
    public Rectangle5D.Integer bounds;
    /**
     * Boolean mask 4D array.
     */
    public final TreeMap<Integer, BooleanMask4D> mask;

    /**
     * Build a new 4D boolean mask with specified bounds and 4D mask array.<br>
     * The 4D mask array length should be >= to <code>bounds.getSizeT()</code>.
     */
    public BooleanMask5D(Rectangle5D.Integer bounds, BooleanMask4D[] mask)
    {
        super();

        this.bounds = bounds;
        this.mask = new TreeMap<Integer, BooleanMask4D>();

        // special case of infinite C dim
        if (bounds.sizeC == Integer.MAX_VALUE)
            this.mask.put(Integer.valueOf(Integer.MIN_VALUE), mask[0]);
        else
        {
            for (int c = 0; c < bounds.sizeC; c++)
                if (mask[c] != null)
                    this.mask.put(Integer.valueOf(bounds.c + c), mask[c]);
        }
    }

    /**
     * Build a new 4D boolean mask from the specified array of {@link Point5D}.<br>
     */
    public BooleanMask5D(Point5D.Integer[] points)
    {
        super();

        mask = new TreeMap<Integer, BooleanMask4D>();

        if ((points == null) || (points.length == 0))
            bounds = new Rectangle5D.Integer();
        else
        {
            int minX = Integer.MAX_VALUE;
            int minY = Integer.MAX_VALUE;
            int minZ = Integer.MAX_VALUE;
            int minT = Integer.MAX_VALUE;
            int minC = Integer.MAX_VALUE;
            int maxX = Integer.MIN_VALUE;
            int maxY = Integer.MIN_VALUE;
            int maxZ = Integer.MIN_VALUE;
            int maxT = Integer.MIN_VALUE;
            int maxC = Integer.MIN_VALUE;

            for (Point5D.Integer pt : points)
            {
                final int x = pt.x;
                final int y = pt.y;
                final int z = pt.z;
                final int t = pt.t;
                final int c = pt.c;

                if (x < minX)
                    minX = x;
                if (x > maxX)
                    maxX = x;
                if (y < minY)
                    minY = y;
                if (y > maxY)
                    maxY = y;
                if (z < minZ)
                    minZ = z;
                if (z > maxZ)
                    maxZ = z;
                if (t < minT)
                    minT = t;
                if (t > maxT)
                    maxT = t;
                if (c < minC)
                    minC = c;
                if (c > maxC)
                    maxC = c;
            }

            // define bounds
            bounds = new Rectangle5D.Integer(minX, minY, minZ, minT, minC, (maxX - minX) + 1, (maxY - minY) + 1,
                    (maxZ - minZ) + 1, (maxT - minT) + 1, (maxC - minC) + 1);

            // set mask
            for (Point5D.Integer pt : points)
            {
                BooleanMask4D m4d = mask.get(Integer.valueOf(pt.c));

                // allocate 4D boolean mask if needed
                if (m4d == null)
                {
                    m4d = new BooleanMask4D(new Rectangle4D.Integer(minX, minY, minZ, minT, bounds.sizeX, bounds.sizeY,
                            bounds.sizeZ, bounds.sizeT), new BooleanMask3D[bounds.sizeT]);
                    // set 4D mask for position C
                    mask.put(Integer.valueOf(pt.c), m4d);
                }

                BooleanMask3D m3d = m4d.getMask3D(pt.t);

                // allocate 3D boolean mask if needed
                if (m3d == null)
                {
                    m3d = new BooleanMask3D(new Rectangle3D.Integer(minX, minY, minZ, bounds.sizeX, bounds.sizeY,
                            bounds.sizeZ), new BooleanMask2D[bounds.sizeZ]);
                    // set 3D mask for position T
                    m4d.mask.put(Integer.valueOf(pt.t), m3d);
                }

                BooleanMask2D m2d = m3d.getMask2D(pt.z);

                // allocate 2D boolean mask if needed
                if (m2d == null)
                {
                    m2d = new BooleanMask2D(new Rectangle(minX, minY, bounds.sizeX, bounds.sizeY),
                            new boolean[bounds.sizeX * bounds.sizeY]);
                    // set 2D mask for position Z
                    m3d.mask.put(Integer.valueOf(pt.z), m2d);
                }

                // set mask point
                m2d.mask[((pt.y - minY) * bounds.sizeX) + (pt.x - minX)] = true;
            }

            // optimize mask 4D bounds
            for (BooleanMask4D m : mask.values())
                m.optimizeBounds();
        }
    }

    /**
     * Build a new boolean mask from the specified array of {@link Point5D}.<br>
     */
    public BooleanMask5D(Point5D[] points)
    {
        super();

        mask = new TreeMap<Integer, BooleanMask4D>();

        if ((points == null) || (points.length == 0))
            bounds = new Rectangle5D.Integer();
        else
        {
            int minX = Integer.MAX_VALUE;
            int minY = Integer.MAX_VALUE;
            int minZ = Integer.MAX_VALUE;
            int minT = Integer.MAX_VALUE;
            int minC = Integer.MAX_VALUE;
            int maxX = Integer.MIN_VALUE;
            int maxY = Integer.MIN_VALUE;
            int maxZ = Integer.MIN_VALUE;
            int maxT = Integer.MIN_VALUE;
            int maxC = Integer.MIN_VALUE;

            for (Point5D pt : points)
            {
                final int x = (int) pt.getX();
                final int y = (int) pt.getY();
                final int z = (int) pt.getZ();
                final int t = (int) pt.getT();
                final int c = (int) pt.getC();

                if (x < minX)
                    minX = x;
                if (x > maxX)
                    maxX = x;
                if (y < minY)
                    minY = y;
                if (y > maxY)
                    maxY = y;
                if (z < minZ)
                    minZ = z;
                if (z > maxZ)
                    maxZ = z;
                if (t < minT)
                    minT = t;
                if (t > maxT)
                    maxT = t;
                if (c < minC)
                    minC = c;
                if (c > maxC)
                    maxC = c;
            }

            // define bounds
            bounds = new Rectangle5D.Integer(minX, minY, minZ, minT, minC, (maxX - minX) + 1, (maxY - minY) + 1,
                    (maxZ - minZ) + 1, (maxT - minT) + 1, (maxC - minC) + 1);

            // set mask
            for (Point5D pt : points)
            {
                BooleanMask4D m4d = mask.get(Integer.valueOf((int) pt.getC()));

                // allocate 4D boolean mask if needed
                if (m4d == null)
                {
                    m4d = new BooleanMask4D(new Rectangle4D.Integer(minX, minY, minZ, minT, bounds.sizeX, bounds.sizeY,
                            bounds.sizeZ, bounds.sizeT), new BooleanMask3D[bounds.sizeT]);
                    // set 4D mask for position C
                    mask.put(Integer.valueOf((int) pt.getC()), m4d);
                }

                BooleanMask3D m3d = m4d.getMask3D((int) pt.getT());

                // allocate 3D boolean mask if needed
                if (m3d == null)
                {
                    m3d = new BooleanMask3D(new Rectangle3D.Integer(minX, minY, minZ, bounds.sizeX, bounds.sizeY,
                            bounds.sizeZ), new BooleanMask2D[bounds.sizeZ]);
                    // set 3D mask for position T
                    m4d.mask.put(Integer.valueOf((int) pt.getT()), m3d);
                }

                BooleanMask2D m2d = m3d.getMask2D((int) pt.getZ());

                // allocate 2D boolean mask if needed
                if (m2d == null)
                {
                    m2d = new BooleanMask2D(new Rectangle(minX, minY, bounds.sizeX, bounds.sizeY),
                            new boolean[bounds.sizeX * bounds.sizeY]);
                    // set 2D mask for position Z
                    m3d.mask.put(Integer.valueOf((int) pt.getZ()), m2d);
                }

                // set mask point
                m2d.mask[(((int) pt.getY() - minY) * bounds.sizeX) + ((int) pt.getX() - minX)] = true;
            }

            // optimize mask 4D bounds
            for (BooleanMask4D m : mask.values())
                m.optimizeBounds();
        }
    }

    public BooleanMask5D()
    {
        this(new Rectangle5D.Integer(), new BooleanMask4D[0]);
    }

    /**
     * Returns the 4D boolean mask for the specified C position
     */
    public BooleanMask4D getMask4D(int c)
    {
        // special case of infinite C dimension
        if (bounds.sizeC == Integer.MAX_VALUE)
            return mask.firstEntry().getValue();

        return mask.get(Integer.valueOf(c));
    }

    /**
     * Returns the 3D boolean mask for the specified T, C position
     */
    public BooleanMask3D getMask3D(int t, int c)
    {
        final BooleanMask4D m = getMask4D(c);

        if (m != null)
            return m.getMask3D(t);

        return null;
    }

    /**
     * Returns the 2D boolean mask for the specified Z, T, C position
     */
    public BooleanMask2D getMask2D(int z, int t, int c)
    {
        final BooleanMask3D m = getMask3D(t, c);

        if (m != null)
            return m.getMask2D(z);

        return null;
    }

    /**
     * Return <code>true</code> if boolean mask is empty
     */
    public boolean isEmpty()
    {
        return bounds.isEmpty();
    }

    /**
     * Return true if mask contains the specified point
     */
    public boolean contains(int x, int y, int z, int t, int c)
    {
        if (bounds.contains(x, y, z, t, c))
        {
            final BooleanMask4D m4d = getMask4D(c);

            if (m4d != null)
                return m4d.contains(x, y, z, t);
        }

        return false;
    }

    /**
     * Return true if mask contains the specified 2D mask at position Z, T, C
     */
    public boolean contains(BooleanMask2D booleanMask, int z, int t, int c)
    {
        if (isEmpty())
            return false;

        final BooleanMask2D mask2d = getMask2D(z, t, c);

        if (mask2d != null)
            return mask2d.contains(booleanMask);

        return false;
    }

    /**
     * Return true if mask contains the specified 3D mask at position T, C
     */
    public boolean contains(BooleanMask3D booleanMask, int t, int c)
    {
        if (isEmpty())
            return false;

        final BooleanMask3D mask3d = getMask3D(t, c);

        if (mask3d != null)
            return mask3d.contains(booleanMask);

        return false;
    }

    /**
     * Return true if mask contains the specified 4D mask at position C
     */
    public boolean contains(BooleanMask4D booleanMask, int c)
    {
        if (isEmpty())
            return false;

        final BooleanMask4D mask4d = getMask4D(c);

        if (mask4d != null)
            return mask4d.contains(booleanMask);

        return false;
    }

    /**
     * Return true if mask contains the specified 5D mask.
     */
    public boolean contains(BooleanMask5D booleanMask)
    {
        if (isEmpty())
            return false;

        final int sizeC = booleanMask.bounds.sizeC;

        // check for special MAX_INTEGER case (infinite C dim)
        if (sizeC == Integer.MAX_VALUE)
        {
            // we cannot contains it if we are not on infinite C dim too
            if (bounds.sizeC != Integer.MAX_VALUE)
                return false;

            return booleanMask.mask.firstEntry().getValue().contains(mask.firstEntry().getValue());
        }

        final int offC = booleanMask.bounds.c;

        for (int c = offC; c < offC + sizeC; c++)
            if (!contains(booleanMask.getMask4D(c), c))
                return false;

        return true;
    }

    /**
     * Return true if mask intersects (contains at least one point) the specified 2D mask at
     * position Z, T, C
     */
    public boolean intersects(BooleanMask2D booleanMask, int z, int t, int c)
    {
        if (isEmpty())
            return false;

        final BooleanMask2D mask2d = getMask2D(z, t, c);

        if (mask2d != null)
            return mask2d.intersects(booleanMask);

        return false;
    }

    /**
     * Return true if mask intersects (contains at least one point) the specified 3D mask at
     * position T, C
     */
    public boolean intersects(BooleanMask3D booleanMask, int t, int c)
    {
        if (isEmpty())
            return false;

        final BooleanMask3D mask3d = getMask3D(t, c);

        if (mask3d != null)
            return mask3d.intersects(booleanMask);

        return false;
    }

    /**
     * Return true if mask intersects (contains at least one point) the specified 4D mask at
     * position C
     */
    public boolean intersects(BooleanMask4D booleanMask, int c)
    {
        if (isEmpty())
            return false;

        final BooleanMask4D mask4d = getMask4D(c);

        if (mask4d != null)
            return mask4d.intersects(booleanMask);

        return false;
    }

    /**
     * Return true if mask intersects (contains at least one point) the specified 5D mask region
     */
    public boolean intersects(BooleanMask5D booleanMask)
    {
        if (isEmpty())
            return false;

        final int sizeC = booleanMask.bounds.sizeC;

        // check for special MAX_INTEGER case (infinite C dim)
        if (sizeC == Integer.MAX_VALUE)
        {
            // get the single T slice
            final BooleanMask4D mask4d = booleanMask.mask.firstEntry().getValue();

            // test with every slice
            for (BooleanMask4D m : mask.values())
                if (m.intersects(mask4d))
                    return true;

            return false;
        }

        // check for special MAX_INTEGER case (infinite C dim)
        if (bounds.sizeC == Integer.MAX_VALUE)
        {
            // get the single T slice
            final BooleanMask4D mask4d = mask.firstEntry().getValue();

            // test with every slice
            for (BooleanMask4D m : booleanMask.mask.values())
                if (m.intersects(mask4d))
                    return true;

            return false;
        }

        final int offC = booleanMask.bounds.c;

        for (int c = offC; c < offC + sizeC; c++)
            if (intersects(booleanMask.getMask4D(c), c))
                return true;

        return false;
    }

    /**
     * Optimize mask bounds so it fits mask content.
     */
    public Rectangle5D.Integer getOptimizedBounds(boolean compute4DBounds)
    {
        final Rectangle5D.Integer result = new Rectangle5D.Integer();

        if (mask.isEmpty())
            return result;

        Rectangle4D.Integer bounds4D = null;

        for (BooleanMask4D m4d : mask.values())
        {
            // get optimized 4D bounds for each C
            final Rectangle4D.Integer optB4d;

            if (compute4DBounds)
                optB4d = m4d.getOptimizedBounds();
            else
                optB4d = new Rectangle4D.Integer(m4d.bounds);

            // only add non empty bounds
            if (!optB4d.isEmpty())
            {
                if (bounds4D == null)
                    bounds4D = optB4d;
                else
                    bounds4D.add(optB4d);
            }
        }

        // empty ?
        if ((bounds4D == null) || bounds4D.isEmpty())
            return result;

        int minC = mask.firstKey().intValue();
        int maxC = mask.lastKey().intValue();

        // set 4D bounds to start with
        result.setX(bounds4D.x);
        result.setY(bounds4D.y);
        result.setZ(bounds4D.z);
        result.setT(bounds4D.t);
        result.setSizeX(bounds4D.sizeX);
        result.setSizeY(bounds4D.sizeY);
        result.setSizeZ(bounds4D.sizeZ);
        result.setSizeT(bounds4D.sizeT);

        // single C --> check for special MAX_INTEGER case
        if ((minC == maxC) && (bounds.sizeC == Integer.MAX_VALUE))
        {
            result.setC(Integer.MIN_VALUE);
            result.setSizeC(Integer.MAX_VALUE);
        }
        else
        {
            result.setC(minC);
            result.setSizeC((maxC - minC) + 1);
        }

        return result;
    }

    /**
     * Optimize mask bounds so it fits mask content.
     */
    public Rectangle5D.Integer getOptimizedBounds()
    {
        return getOptimizedBounds(true);
    }

    /**
     * Optimize mask bounds so it fits mask content.
     */
    public void optimizeBounds()
    {
        // start by optimizing 4D bounds
        for (BooleanMask4D m : mask.values())
            m.optimizeBounds();

        moveBounds(getOptimizedBounds(false));
    }

    /**
     * Change the bounds of BooleanMask.<br>
     * Keep mask data intersecting from old bounds.
     */
    public void moveBounds(Rectangle5D.Integer value)
    {
        // bounds changed ?
        if (!bounds.equals(value))
        {
            // changed to empty mask
            if (value.isEmpty())
            {
                // clear bounds and mask
                bounds = new Rectangle5D.Integer();
                mask.clear();
                return;
            }

            final Rectangle4D.Integer bounds4D = new Rectangle4D.Integer(value.x, value.y, value.z, value.t,
                    value.sizeX, value.sizeY, value.sizeZ, value.sizeT);

            // it was infinite C dim ?
            if (bounds.sizeC == Integer.MAX_VALUE)
            {
                // get the single 4D mask
                final BooleanMask4D m4d = mask.firstEntry().getValue();

                // adjust 4D bounds if needed to the single 4D mask
                m4d.moveBounds(bounds4D);

                // we passed from infinite C to defined C range
                if (value.sizeC != Integer.MAX_VALUE)
                {
                    // assign the same 4D mask for all C position
                    mask.clear();
                    for (int c = 0; c <= value.sizeC; c++)
                        mask.put(Integer.valueOf(c + value.c), (BooleanMask4D) m4d.clone());
                }
            }
            // we pass to infinite C dim
            else if (value.sizeT == Integer.MAX_VALUE)
            {
                // try to use the 4D mask at C position
                BooleanMask4D mask4D = getMask4D(value.c);

                // otherwise we use the first found 2D mask
                if ((mask4D == null) && !mask.isEmpty())
                    mask4D = mask.firstEntry().getValue();

                // set new mask
                mask.clear();
                if (mask4D != null)
                    mask.put(Integer.valueOf(Integer.MIN_VALUE), mask4D);
            }
            else
            {
                // create new mask array
                final BooleanMask4D[] newMask = new BooleanMask4D[value.sizeC];

                for (int c = 0; c < value.sizeC; c++)
                {
                    final BooleanMask4D mask4D = getMask4D(value.c + c);

                    if (mask4D != null)
                        // adjust 4D bounds
                        mask4D.moveBounds(bounds4D);

                    newMask[c] = mask4D;
                }

                // set new mask
                mask.clear();
                for (int c = 0; c < value.sizeC; c++)
                    mask.put(Integer.valueOf(value.c + c), newMask[c]);
            }

            bounds = value;
        }
    }

    /**
     * Transforms the specified 4D coordinates int array [x,y,z,t] in 5D coordinates int array [x,y,z,t,c] with the
     * specified C value.
     */
    public static int[] toInt5D(int[] source4D, int c)
    {
        final int[] result = new int[(source4D.length * 5) / 4];

        int pt = 0;
        for (int i = 0; i < source4D.length; i += 4)
        {
            result[pt++] = source4D[i + 0];
            result[pt++] = source4D[i + 1];
            result[pt++] = source4D[i + 2];
            result[pt++] = source4D[i + 3];
            result[pt++] = c;
        }

        return result;
    }

    /**
     * Return an array of {@link icy.type.point.Point5D.Integer} containing the contour/surface
     * points
     * of the 5D mask.<br>
     * Points are returned in ascending XYZTC order. <br>
     * <br>
     * WARNING: The basic implementation is not totally accurate.<br>
     * It returns all points from the first and the last C slices + contour points for intermediate
     * C
     * slices.
     * 
     * @see #getContourPointsAsIntArray()
     */
    public Point5D.Integer[] getContourPoints()
    {
        return Point5D.Integer.toPoint5D(getContourPointsAsIntArray());
    }

    /**
     * Return an array of integer containing the contour/surface points of the 5D mask.<br>
     * <code>result.length</code> = number of point * 4<br>
     * <code>result[(pt * 4) + 0]</code> = X coordinate for point <i>pt</i>.<br>
     * <code>result[(pt * 4) + 1]</code> = Y coordinate for point <i>pt</i>.<br>
     * <code>result[(pt * 4) + 2]</code> = Z coordinate for point <i>pt</i>.<br>
     * <code>result[(pt * 4) + 3]</code> = T coordinate for point <i>pt</i>.<br>
     * <code>result[(pt * 5) + 4]</code> = C coordinate for point <i>pt</i>.<br>
     * Points are returned in ascending XYZTC order.<br>
     * <br>
     * WARNING: The basic implementation is not totally accurate.<br>
     * It returns all points from the first and the last C slices + contour points for intermediate
     * C
     * slices.
     * 
     * @see #getContourPoints()
     */
    public int[] getContourPointsAsIntArray()
    {
        final DynamicArray.Int result = new DynamicArray.Int(8);

        // perimeter = first slice volume + inter slices perimeter + last slice volume
        // TODO: fix this method and use real 5D contour point
        if (mask.size() <= 2)
        {
            for (Entry<Integer, BooleanMask4D> entry : mask.entrySet())
                result.add(toInt5D(entry.getValue().getPointsAsIntArray(), entry.getKey().intValue()));
        }
        else
        {
            final Entry<Integer, BooleanMask4D> firstEntry = mask.firstEntry();
            final Entry<Integer, BooleanMask4D> lastEntry = mask.lastEntry();
            final Integer firstKey = firstEntry.getKey();
            final Integer lastKey = lastEntry.getKey();

            result.add(toInt5D(firstEntry.getValue().getPointsAsIntArray(), firstKey.intValue()));

            for (Entry<Integer, BooleanMask4D> entry : mask.subMap(firstKey, false, lastKey, false).entrySet())
                result.add(toInt5D(entry.getValue().getContourPointsAsIntArray(), entry.getKey().intValue()));

            result.add(toInt5D(lastEntry.getValue().getPointsAsIntArray(), lastKey.intValue()));
        }

        return result.asArray();
    }

    /**
     * Return the number of points contained in this boolean mask.
     */
    public int getNumberOfPoints()
    {
        int result = 0;

        for (BooleanMask4D mask4d : mask.values())
            result += mask4d.getNumberOfPoints();

        return result;
    }

    /**
     * Return an array of {@link icy.type.point.Point5D.Integer} representing all points of the
     * current 5D mask.<br>
     * Points are returned in ascending XYZTC order.
     */
    public Point5D.Integer[] getPoints()
    {
        return Point5D.Integer.toPoint5D(getPointsAsIntArray());
    }

    /**
     * Return an array of integer representing all points of the current 5D mask.<br>
     * <code>result.length</code> = number of point * 5<br>
     * <code>result[(pt * 5) + 0]</code> = X coordinate for point <i>pt</i>.<br>
     * <code>result[(pt * 5) + 1]</code> = Y coordinate for point <i>pt</i>.<br>
     * <code>result[(pt * 5) + 2]</code> = Z coordinate for point <i>pt</i>.<br>
     * <code>result[(pt * 5) + 3]</code> = T coordinate for point <i>pt</i>.<br>
     * <code>result[(pt * 5) + 4]</code> = C coordinate for point <i>pt</i>.<br>
     * Points are returned in ascending XYZTC order.
     */
    public int[] getPointsAsIntArray()
    {
        final DynamicArray.Int result = new DynamicArray.Int(8);

        for (Entry<Integer, BooleanMask4D> entry : mask.entrySet())
            result.add(toInt5D(entry.getValue().getPointsAsIntArray(), entry.getKey().intValue()));

        return result.asArray();
    }

    @Override
    public Object clone()
    {
        final BooleanMask5D result = new BooleanMask5D();

        result.bounds = new Rectangle5D.Integer(bounds);
        for (Entry<Integer, BooleanMask4D> entry : mask.entrySet())
            result.mask.put(entry.getKey(), (BooleanMask4D) entry.getValue().clone());

        return result;
    }
}