/*
 * Copyright 2010-2013 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.roi2d;

import icy.canvas.IcyCanvas;
import icy.canvas.IcyCanvas2D;
import icy.canvas.IcyCanvas3D;
import icy.image.ImageUtil;
import icy.resource.ResourceUtil;
import icy.roi.BooleanMask2D;
import icy.roi.ROI;
import icy.roi.ROI2D;
import icy.roi.edit.Area2DChangeROIEdit;
import icy.sequence.Sequence;
import icy.type.point.Point5D;
import icy.type.point.Point5D.Double;
import icy.util.EventUtil;
import icy.util.GraphicsUtil;
import icy.util.ShapeUtil;
import icy.util.XMLUtil;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.awt.image.IndexColorModel;

import org.w3c.dom.Node;

import plugins.kernel.canvas.VtkCanvas;

/**
 * ROI Area type.<br>
 * Use a bitmap mask internally for fast boolean mask operation.<br>
 * 
 * @author Stephane
 */
public class ROI2DArea extends ROI2D
{
    protected static final float DEFAULT_CURSOR_SIZE = 15f;

    // we want to keep a static brush
    protected static final Ellipse2D brush = new Ellipse2D.Double();
    // protected static final Point2D.Double cursorPosition = new Point2D.Double();
    protected static Color brushColor = Color.red;
    protected static float brushSize = DEFAULT_CURSOR_SIZE;

    public class ROI2DAreaPainter extends ROI2DPainter
    {
        /**
         * @deprecated Use {@link #getOpacity()} instead.
         */
        @Deprecated
        public static final float CONTENT_ALPHA = 0.3f;

        private static final float MIN_CURSOR_SIZE = 0.6f;
        private static final float MAX_CURSOR_SIZE = 500f;

        protected final Point2D brushPosition;

        public ROI2DAreaPainter()
        {
            super();

            brushPosition = new Point2D.Double();
        }

        void updateCursor()
        {
            final double x = brushPosition.getX();
            final double y = brushPosition.getY();

            brush.setFrameFromDiagonal(x - brushSize, y - brushSize, x + brushSize, y + brushSize);

            // if roi selected (cursor displayed) --> painter changed
            if (isSelected())
                painterChanged();
        }

        /**
         * Returns the brush position.
         */
        public Point2D getBrushPosition()
        {
            return (Point) brushPosition.clone();
        }

        /**
         * Set the brush position.
         */
        public void setBrushPosition(Point2D position)
        {
            if (!brushPosition.equals(position))
            {
                brushPosition.setLocation(position);
                updateCursor();
            }
        }

        /**
         * @deprecated Use {@link #getBrushPosition()} instead.
         */
        @Deprecated
        public Point2D getCursorPosition()
        {
            return getBrushPosition();
        }

        /**
         * @deprecated Use {@link #setBrushPosition(Point2D)} instead.
         */
        @Deprecated
        public void setCursorPosition(Point2D position)
        {
            setBrushPosition(position);
        }

        /**
         * Returns the brush size.
         */
        public float getBrushSize()
        {
            return brushSize;
        }

        /**
         * Sets the brush size.
         */
        public void setBrushSize(float value)
        {
            final float adjValue = Math.max(Math.min(value, MAX_CURSOR_SIZE), MIN_CURSOR_SIZE);

            if (brushSize != adjValue)
            {
                brushSize = adjValue;
                updateCursor();
            }
        }

        /**
         * @deprecated Use {@link #getBrushSize()} instead
         */
        @Deprecated
        public float getCursorSize()
        {
            return getBrushSize();
        }

        /**
         * @deprecated Use {@link #setBrushSize(float)} instead
         */
        @Deprecated
        public void setCursorSize(float value)
        {
            setBrushSize(value);
        }

        /**
         * Returns the brush color
         */
        public Color getBrushColor()
        {
            return brushColor;
        }

        /**
         * Sets the brush color
         */
        public void setBrushColor(Color value)
        {
            if (!brushColor.equals(value))
            {
                brushColor = value;
                painterChanged();
            }
        }

        /**
         * @deprecated Use {@link #getBrushColor()} instead
         */
        @Deprecated
        public Color getCursorColor()
        {
            return getBrushColor();
        }

        /**
         * @deprecated Use {@link #setBrushColor(Color)} instead
         */
        @Deprecated
        public void setCursorColor(Color value)
        {
            setBrushColor(value);
        }

        public void addToMask(Point2D pos)
        {
            setBrushPosition(pos);
            updateMask(brush, false);
        }

        public void removeFromMask(Point2D pos)
        {
            setBrushPosition(pos);
            updateMask(brush, true);
        }

        @Override
        public void painterChanged()
        {
            updateMaskColor(true);

            super.painterChanged();
        }

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

            // not yet consumed and ROI editable...
            if (!e.isConsumed() && !isReadOnly())
            {
                // then process it here
                if (isActiveFor(canvas))
                {
                    // check we can do the action
                    if (!(canvas instanceof VtkCanvas) && (imagePoint != null))
                    {
                        ROI2DArea.this.beginUpdate();
                        try
                        {
                            switch (e.getKeyChar())
                            {
                                case '+':
                                    if (isSelected())
                                    {
                                        setBrushSize(getBrushSize() * 1.1f);
                                        e.consume();
                                    }
                                    break;

                                case '-':
                                    if (isSelected())
                                    {
                                        setBrushSize(getBrushSize() * 0.9f);
                                        e.consume();
                                    }
                                    break;
                            }
                        }
                        finally
                        {
                            ROI2DArea.this.endUpdate();
                        }
                    }
                }
            }
        }

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

            // not yet consumed, ROI editable, selected and not focused...
            if (!e.isConsumed() && !isReadOnly() && isSelected() && !isFocused())
            {
                // then process it here
                if (isActiveFor(canvas))
                {
                    // check we can do the action
                    if (!(canvas instanceof VtkCanvas) && (imagePoint != null))
                    {
                        // keep trace of roi changes from user mouse action
                        roiModifiedByMouse = false;
                        // save current ROI
                        undoSave = getBooleanMask(true);

                        ROI2DArea.this.beginUpdate();
                        try
                        {
                            // left button action
                            if (EventUtil.isLeftMouseButton(e))
                            {
                                // add point first
                                addToMask(imagePoint.toPoint2D());
                                roiModifiedByMouse = true;
                                e.consume();
                            }
                            // right button action
                            else if (EventUtil.isRightMouseButton(e))
                            {
                                // remove point
                                removeFromMask(imagePoint.toPoint2D());
                                roiModifiedByMouse = true;
                                e.consume();
                            }
                        }
                        finally
                        {
                            ROI2DArea.this.endUpdate();
                        }
                    }
                }
            }
        }

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

            // update only on release as it can be long
            if (!isReadOnly())
            {
                if (boundsNeedUpdate)
                {
                    optimizeBounds();
                    // empty ? delete ROI
                    if (bounds.isEmpty())
                    {
                        ROI2DArea.this.remove();
                        // nothing more to do
                        return;
                    }
                }

                if (roiModifiedByMouse)
                {
                    final Sequence sequence = canvas.getSequence();

                    // add undo operation
                    try
                    {
                        if ((sequence != null) && (undoSave != null))
                            sequence.addUndoableEdit(new Area2DChangeROIEdit(ROI2DArea.this, undoSave));
                    }
                    catch (OutOfMemoryError err)
                    {
                        // can't create undo operation, show message and clear undo manager
                        System.out.println("Warning: not enough memory to create undo point for ROI area change");
                        sequence.clearUndoManager();
                    }

                    // release save
                    undoSave = null;
                    roiModifiedByMouse = false;
                }
            }
        }

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

            // not yet consumed, ROI editable and selected...
            if (!e.isConsumed() && !isReadOnly() && isSelected())
            {
                // then process it here
                if (isActiveFor(canvas))
                {
                    // check we can do the action
                    if (!(canvas instanceof VtkCanvas) && (imagePoint != null))
                    {
                        setBrushPosition(imagePoint.toPoint2D());
                    }
                }
            }
        }

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

            // not yet consumed, ROI editable and selected...
            if (!e.isConsumed() && !isReadOnly() && isSelected())
            {
                // then process it here
                if (isActiveFor(canvas))
                {
                    // check we can do the action
                    if (!(canvas instanceof VtkCanvas) && (imagePoint != null))
                    {
                        ROI2DArea.this.beginUpdate();
                        try
                        {
                            // left button action
                            if (EventUtil.isLeftMouseButton(e))
                            {
                                // add point first
                                addToMask(imagePoint.toPoint2D());
                                roiModifiedByMouse = true;
                                e.consume();
                            }
                            // right button action
                            else if (EventUtil.isRightMouseButton(e))
                            {
                                // remove point
                                removeFromMask(imagePoint.toPoint2D());
                                roiModifiedByMouse = true;
                                e.consume();
                            }
                        }
                        finally
                        {
                            ROI2DArea.this.endUpdate();
                        }
                    }
                }
            }
        }

        @Override
        public void paint(Graphics2D g, Sequence sequence, IcyCanvas canvas)
        {
            super.paint(g, sequence, canvas);

            if (isActiveFor(canvas))
            {
                // ROI selected ? draw cursor
                if (isSelected() && !isFocused() && !isReadOnly())
                    drawCursor(g, sequence, canvas);
            }
        }

        /**
         * Draw the ROI itself
         */
        @Override
        protected void drawROI(Graphics2D g, Sequence sequence, IcyCanvas canvas)
        {
            if (canvas instanceof IcyCanvas2D)
            {
                // not supported
                if (g == null)
                    return;

                final Rectangle bounds = getBounds();
                // trivial paint optimization
                final boolean shapeVisible = GraphicsUtil.isVisible(g, bounds);

                if (shapeVisible)
                {
                    final Graphics2D g2 = (Graphics2D) g.create();
                    final boolean small;

                    // disable LOD when creating the ROI
                    if (isCreating())
                        small = false;
                    else
                    {
                        final double scale = Math.max(Math.abs(canvas.getScaleX()), Math.abs(canvas.getScaleY()));
                        small = Math.max(scale * bounds.getWidth(), scale * bounds.getHeight()) < LOD_SMALL;
                    }

                    // simplified draw
                    if (small)
                    {
                        g2.setColor(getDisplayColor());
                        g2.drawImage(imageMask, null, bounds.x, bounds.y);
                    }
                    // normal draw
                    else
                    {
                        final AlphaComposite prevAlpha = (AlphaComposite) g2.getComposite();
                        float newAlpha = prevAlpha.getAlpha() * getOpacity();
                        newAlpha = Math.min(1f, newAlpha);
                        newAlpha = Math.max(0f, newAlpha);

                        // show content with an alpha factor
                        g2.setComposite(prevAlpha.derive(newAlpha));

                        // draw mask
                        g2.drawImage(imageMask, null, bounds.x, bounds.y);

                        // restore alpha
                        g2.setComposite(prevAlpha);

                        // draw border
                        if (isSelected())
                        {
                            g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke + 1d)));
                            g2.setColor(getDisplayColor());
                            g2.draw(bounds);
                        }
                        else
                        {
                            // outside border
                            g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke + 1d)));
                            g2.setColor(Color.black);
                            g2.draw(bounds);
                            // internal border
                            g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke)));
                            g2.setColor(getDisplayColor());
                            g2.draw(bounds);
                        }
                    }

                    g2.dispose();
                }

                // for (Point2D pt : getBooleanMask().getEdgePoints())
                // g2.drawRect((int) pt.getX(), (int) pt.getY(), 1, 1);
            }

            if (canvas instanceof IcyCanvas3D)
            {
                // not yet supported

            }
        }

        /**
         * draw the ROI cursor
         */
        protected void drawCursor(Graphics2D g, Sequence sequence, IcyCanvas canvas)
        {
            if (canvas instanceof IcyCanvas2D)
            {
                // not supported
                if (g == null)
                    return;

                final Rectangle bounds = brush.getBounds();
                // trivial paint optimization
                final boolean shapeVisible = GraphicsUtil.isVisible(g, bounds);

                if (shapeVisible)
                {
                    final Graphics2D g2 = (Graphics2D) g.create();
                    final boolean tiny;

                    // disable LOD when creating the ROI
                    if (isCreating())
                        tiny = false;
                    else
                    {
                        final double scale = Math.max(canvas.getScaleX(), canvas.getScaleY());
                        tiny = Math.max(scale * bounds.getWidth(), scale * bounds.getHeight()) < LOD_TINY;
                    }

                    // simplified draw
                    if (tiny)
                    {
                        // cursor color
                        g2.setColor(brushColor);
                        // draw cursor
                        g2.fill(brush);
                    }
                    // normal draw
                    else
                    {
                        final AlphaComposite prevAlpha = (AlphaComposite) g2.getComposite();
                        float newAlpha = prevAlpha.getAlpha() * getOpacity() * 2f;
                        newAlpha = Math.min(1f, newAlpha);
                        newAlpha = Math.max(0f, newAlpha);

                        // show cursor with an alpha factor
                        g2.setComposite(prevAlpha.derive(newAlpha));

                        // draw cursor border
                        g2.setColor(Color.black);
                        g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke)));
                        g2.draw(brush);
                        // draw cursor
                        g2.setColor(brushColor);
                        g2.fill(brush);
                    }

                    g2.dispose();
                }
            }

            if (canvas instanceof IcyCanvas3D)
            {
                // not yet supported

            }
        }
    }

    public static final String ID_BOUNDS_X = "boundsX";
    public static final String ID_BOUNDS_Y = "boundsY";
    public static final String ID_BOUNDS_W = "boundsW";
    public static final String ID_BOUNDS_H = "boundsH";
    // protected static final String ID_BOOLMASK_LEN = "boolMaskLen";
    public static final String ID_BOOLMASK_DATA = "boolMaskData";

    /**
     * image containing the mask
     */
    BufferedImage imageMask;
    /**
     * rectangle bounds
     */
    final Rectangle bounds;

    /**
     * internals
     */
    protected final byte[] red;
    protected final byte[] green;
    protected final byte[] blue;
    protected IndexColorModel colorModel;
    protected byte[] maskData; // 0 = false, 1 = true
    protected double translateX, translateY;
    protected Color previousColor;
    protected boolean boundsNeedUpdate;
    protected boolean roiModifiedByMouse;
    protected BooleanMask2D undoSave;

    /**
     * Create a ROI2D Area type from the specified {@link BooleanMask2D}.
     */
    public ROI2DArea()
    {
        super();

        bounds = new Rectangle();
        boundsNeedUpdate = false;
        roiModifiedByMouse = false;
        undoSave = null;
        translateX = 0d;
        translateY = 0d;

        // prepare indexed image
        red = new byte[256];
        green = new byte[256];
        blue = new byte[256];

        // keep trace of previous color
        previousColor = getDisplayColor();

        // set colormap
        red[1] = (byte) previousColor.getRed();
        green[1] = (byte) previousColor.getGreen();
        blue[1] = (byte) previousColor.getBlue();

        // classic 8 bits indexed with one transparent color (index = 0)
        colorModel = new IndexColorModel(8, 256, red, green, blue, 0);
        // create default image
        imageMask = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED, colorModel);
        // get data pointer
        maskData = ((DataBufferByte) imageMask.getRaster().getDataBuffer()).getData();

        // set name and icon
        setName("Area2D");
        setIcon(ResourceUtil.ICON_ROI_AREA);
    }

    /**
     * @deprecated Use {@link #ROI2DArea(Point5D)} instead
     */
    @Deprecated
    public ROI2DArea(Point2D position, boolean cm)
    {
        this(position);
    }

    /**
     * Create a ROI2D Area type with a single point.
     */
    public ROI2DArea(Point2D position)
    {
        this();

        // add current point to mask
        addBrush(position);
    }

    /**
     * Generic constructor for interactive mode.
     */
    public ROI2DArea(Point5D position)
    {
        this(position.toPoint2D());
    }

    /**
     * Create a ROI2D Area type from the specified {@link BooleanMask2D}.
     */
    public ROI2DArea(BooleanMask2D mask)
    {
        this();

        setAsBooleanMask(mask);
    }

    /**
     * Create a copy of the specified 2D Area ROI
     */
    public ROI2DArea(ROI2DArea area)
    {
        this();

        final BufferedImage newImageMask = new BufferedImage(area.bounds.width, area.bounds.height,
                BufferedImage.TYPE_BYTE_INDEXED, colorModel);
        final byte[] newMaskData = ((DataBufferByte) newImageMask.getRaster().getDataBuffer()).getData();

        System.arraycopy(area.maskData, 0, newMaskData, 0, newMaskData.length);

        imageMask = newImageMask;
        maskData = newMaskData;
        bounds.setBounds(area.bounds);
    }

    void addToBounds(Rectangle bnd)
    {
        final Rectangle newBounds;

        if (bounds.isEmpty())
            newBounds = new Rectangle(bnd);
        else
        {
            newBounds = new Rectangle(bounds);
            newBounds.add(bnd);
        }

        try
        {
            // update image to the new bounds
            updateImage(newBounds);
        }
        catch (Error E)
        {
            // maybe a "out of memory" error, restore back old bounds
            System.err.println("can't enlarge ROI, no enough memory !");
        }
    }

    /**
     * @deprecated Use {@link #optimizeBounds()} instead.
     */
    @Deprecated
    public void optimizeBounds(boolean removeIfEmpty)
    {
        optimizeBounds();
        if (removeIfEmpty && bounds.isEmpty())
            remove();
    }

    /**
     * Returns true if the ROI is empty (the mask does not contains any point).
     */
    public boolean isEmpty()
    {
        if (bounds.isEmpty())
            return true;

        for (byte b : maskData)
            if (b != 0)
                return false;

        return true;
    }

    /**
     * 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()
    {
        // bounds are being updated
        boundsNeedUpdate = false;

        // recompute bound from the mask data
        final int sizeX = imageMask.getWidth();
        final int sizeY = imageMask.getHeight();

        int minX, minY, maxX, maxY;
        minX = maxX = minY = maxY = 0;
        boolean empty = true;
        int offset = 0;
        for (int y = 0; y < sizeY; y++)
        {
            for (int x = 0; x < sizeX; x++)
            {
                if (maskData[offset++] != 0)
                {
                    if (empty)
                    {
                        minX = maxX = x;
                        minY = maxY = y;
                        empty = false;
                    }
                    else
                    {
                        if (x < minX)
                            minX = x;
                        else if (x > maxX)
                            maxX = x;
                        if (y < minY)
                            minY = y;
                        else if (y > maxY)
                            maxY = y;
                    }
                }
            }
        }

        if (!empty)
        {
            // update image to the new bounds
            updateImage(new Rectangle(bounds.x + minX, bounds.y + minY, (maxX - minX) + 1, (maxY - minY) + 1));
            // notify changed
            roiChanged();
        }
        else
        {
            // update to empty bounds
            updateImage(new Rectangle(bounds.x, bounds.y, 0, 0));
            // notify changed
            roiChanged();
        }
    }

    /**
     * @deprecated Use {@link #getDisplayColor()} instead.
     */
    @Deprecated
    public Color getMaskColor()
    {
        return getPainter().getDisplayColor();
    }

    void updateMaskColor(boolean rebuildImage)
    {
        final Color color = getPainter().getDisplayColor();

        // roi color changed ?
        if (!previousColor.equals(color))
        {
            // update colormap
            red[1] = (byte) color.getRed();
            green[1] = (byte) color.getGreen();
            blue[1] = (byte) color.getBlue();

            colorModel = new IndexColorModel(8, 256, red, green, blue, 0);

            // recreate image (so the new colormodel takes effect)
            if (rebuildImage)
                imageMask = ImageUtil.createIndexedImage(imageMask.getWidth(), imageMask.getHeight(), colorModel,
                        maskData);

            // set to new color
            previousColor = color;
        }
    }

    /**
     * Returns the internal image mask.
     */
    public BufferedImage getImageMask()
    {
        return imageMask;
    }

    void updateImage(Rectangle newBnd)
    {
        // copy rectangle
        final Rectangle oldBounds = new Rectangle(bounds);
        final Rectangle newBounds = new Rectangle(newBnd);

        // replace to oldBounds origin
        oldBounds.translate(-bounds.x, -bounds.y);
        newBounds.translate(-bounds.x, -bounds.y);

        // dimension changed ?
        if ((oldBounds.width != newBounds.width) || (oldBounds.height != newBounds.height))
        {
            if (!newBounds.isEmpty())
            {
                // new bounds not empty
                final BufferedImage newImageMask = new BufferedImage(newBounds.width, newBounds.height,
                        BufferedImage.TYPE_BYTE_INDEXED, colorModel);
                final byte[] newMaskData = ((DataBufferByte) newImageMask.getRaster().getDataBuffer()).getData();

                final Rectangle intersect = newBounds.intersection(oldBounds);

                if (!intersect.isEmpty())
                {
                    int offSrc = 0;
                    int offDst = 0;

                    // adjust offset in source mask
                    if (intersect.x > 0)
                        offSrc += intersect.x;
                    if (intersect.y > 0)
                        offSrc += intersect.y * oldBounds.width;
                    // adjust offset in destination mask
                    if (newBounds.x < 0)
                        offDst += -newBounds.x;
                    if (newBounds.y < 0)
                        offDst += -newBounds.y * newBounds.width;

                    // preserve data
                    for (int j = 0; j < intersect.height; j++)
                    {
                        System.arraycopy(maskData, offSrc, newMaskData, offDst, intersect.width);

                        offSrc += oldBounds.width;
                        offDst += newBounds.width;
                    }
                }

                // set new image and maskData
                imageMask = newImageMask;
                maskData = newMaskData;
            }
            else
            {
                // new bounds empty --> use single pixel image to avoid NPE
                imageMask = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED, colorModel);
                maskData = ((DataBufferByte) imageMask.getRaster().getDataBuffer()).getData();
            }

            bounds.setBounds(newBnd);
        }
    }

    /**
     * Set the value of the specified point.<br>
     * Don't forget to call optimizeBounds() after consecutive remove operation to refresh the mask
     * bounds.
     */
    public void setPoint(int x, int y, boolean value)
    {
        if (value)
        {
            // set point in mask
            addToBounds(new Rectangle(x, y, 1, 1));
            // set color depending remove or adding to mask
            maskData[(x - bounds.x) + ((y - bounds.y) * bounds.width)] = 1;
        }
        else
        {
            // remove point from mask
            maskData[(x - bounds.x) + ((y - bounds.y) * bounds.width)] = 0;
            // mark that bounds need to be updated
            boundsNeedUpdate = true;
        }

        // notify roi changed
        roiChanged();
    }

    /**
     * @deprecated Use {@link #setPoint(int, int, boolean)} instead.
     */
    @Deprecated
    public void updateMask(int x, int y, boolean remove)
    {
        setPoint(x, y, !remove);
    }

    /**
     * Update mask from specified shape
     */
    public void updateMask(Shape shape, boolean remove)
    {
        if (remove)
        {
            // outside bounds ? --> nothing to remove so nothing to do...
            if (!bounds.intersects(shape.getBounds2D()))
                return;

            // mark that bounds need to be updated
            boundsNeedUpdate = true;
        }
        else
            // update bounds (this update the image dimension if needed)
            addToBounds(shape.getBounds());

        // get image graphics object
        final Graphics2D g = imageMask.createGraphics();

        g.setComposite(AlphaComposite.Src);
        // set color depending remove or adding to mask
        if (remove)
            g.setColor(new Color(colorModel.getRGB(0), true));
        else
            g.setColor(new Color(colorModel.getRGB(1), true));
        // translate to origin of image and pixel center
        g.translate(-(bounds.x + 0.5d), -(bounds.y + 0.5d));
        // draw cursor in the mask
        g.fill(shape);

        g.dispose();

        // notify roi changed
        roiChanged();
    }

    @Override
    public ROI2DAreaPainter getPainter()
    {
        return (ROI2DAreaPainter) super.painter;
    }

    @Override
    protected ROI2DAreaPainter createPainter()
    {
        return new ROI2DAreaPainter();
    }

    @Override
    public boolean hasSelectedPoint()
    {
        return false;
    }

    /**
     * @deprecated useless method.
     */
    @Deprecated
    public boolean canAddPoint()
    {
        return true;
    }

    /**
     * @deprecated useless method.
     */
    @Deprecated
    public boolean canRemovePoint()
    {
        return true;
    }

    /**
     * @deprecated Use {@link #addBrush(Point2D)} instead.
     */
    @Deprecated
    public boolean addPointAt(Point2D pos, boolean ctrl)
    {
        addBrush(pos);
        return true;
    }

    /**
     * @deprecated Use {@link #removeBrush(Point2D)} instead.
     */
    @Deprecated
    public boolean removePointAt(IcyCanvas canvas, Point2D pos)
    {
        removeBrush(pos);
        return true;
    }

    /**
     * @deprecated Useless method.
     */
    @Deprecated
    protected boolean removeSelectedPoint(IcyCanvas canvas, Point2D imagePoint)
    {
        // no selected point for this ROI
        return false;
    }

    /**
     * Add brush point at specified position.
     */
    public void addBrush(Point2D pos)
    {
        getPainter().addToMask(pos);
    }

    /**
     * Remove brush point from the mask at specified position.<br>
     * Don't forget to call optimizeBounds() after consecutive remove operation
     * to refresh the mask bounds.
     */
    public void removeBrush(Point2D pos)
    {
        getPainter().removeFromMask(pos);
    }

    /**
     * Add a point to the mask
     */
    public void addPoint(Point pos)
    {
        addPoint(pos.x, pos.y);
    }

    /**
     * Add a point to the mask
     */
    public void addPoint(int x, int y)
    {
        setPoint(x, y, 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(Point pos)
    {
        removePoint(pos.x, pos.y);
    }

    /**
     * Remove a point to 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)
    {
        setPoint(x, y, false);
    }

    /**
     * Add a rectangle to the mask
     */
    public void addRect(Rectangle r)
    {
        updateMask(r, false);
    }

    /**
     * Add a rectangle to the mask
     */
    public void addRect(int x, int y, int w, int h)
    {
        addRect(new Rectangle(x, y, w, h));
    }

    /**
     * Remove a rectangle from the mask.<br>
     * Don't forget to call optimizeBounds() after consecutive remove operation<br>
     * to refresh the mask bounds.
     */
    public void removeRect(Rectangle r)
    {
        updateMask(r, true);
    }

    /**
     * Remove a rectangle from the mask.<br>
     * Don't forget to call optimizeBounds() after consecutive remove operation<br>
     * to refresh the mask bounds.
     */
    public void removeRect(int x, int y, int w, int h)
    {
        removeRect(new Rectangle(x, y, w, h));
    }

    /**
     * Add a shape to the mask
     */
    public void addShape(Shape s)
    {
        updateMask(s, false);
    }

    /**
     * Remove a shape to the mask.<br>
     * Don't forget to call optimizeBounds() after consecutive remove operation<br>
     * to refresh the mask bounds.
     */
    public void removeShape(Shape s)
    {
        updateMask(s, true);
    }

    /**
     * Return true if bounds need to be updated by calling optimizeBounds() method.
     */
    public boolean getBoundsNeedUpdate()
    {
        return boundsNeedUpdate;
    }

    /**
     * Clear the mask
     */
    public void clear()
    {
        // reset image with new rectangle
        updateImage(new Rectangle());
    }

    @Override
    public boolean isOverEdge(IcyCanvas canvas, double x, double y)
    {
        // use bigger stroke for isOverEdge test for easier intersection
        final double strk = getAdjustedStroke(canvas) * 3;
        final Rectangle2D rect = new Rectangle2D.Double(x - (strk * 0.5), y - (strk * 0.5), strk, strk);

        // fast intersect test to start with
        if (getBounds2D().intersects(rect))
            // use flatten path, intersects on curved shape return incorrect result
            return ShapeUtil.pathIntersects(bounds.getPathIterator(null, 0.1), rect);

        return false;
    }

    @Override
    public boolean contains(double x, double y)
    {
        // fast discard
        if (!bounds.contains(x, y))
            return false;

        // replace to origin
        final int xi = (int) x - bounds.x;
        final int yi = (int) y - bounds.y;

        return (maskData[(yi * imageMask.getWidth()) + xi] != 0);
    }

    @Override
    public boolean contains(double x, double y, double w, double h)
    {
        // fast discard
        if (!bounds.contains(x, y, w, h))
            return false;

        // replace to origin
        final int xi = (int) x - bounds.x;
        final int yi = (int) y - bounds.y;
        final int wi = (int) (x + w) - (int) x;
        final int hi = (int) (y + h) - (int) y;

        // scan all pixels, can take sometime if mask is large
        int offset = (yi * bounds.width) + xi;
        for (int j = 0; j < hi; j++)
        {
            for (int i = 0; i < wi; i++)
                if (maskData[offset++] == 0)
                    return false;

            offset += bounds.width - wi;
        }

        return true;
    }

    /*
     * already calculated
     */
    @Override
    public Rectangle2D computeBounds2D()
    {
        return bounds;
    }

    /*
     * We can override directly this method as we use our own bounds calculation method here
     */
    @Override
    public Rectangle2D getBounds2D()
    {
        return bounds;
    }

    @Override
    public boolean intersects(double x, double y, double w, double h)
    {
        // fast discard
        if (!bounds.intersects(x, y, w, h))
            return false;

        // replace to origin
        int xi = (int) x - bounds.x;
        int yi = (int) y - bounds.y;
        int wi = (int) (x + w) - (int) x;
        int hi = (int) (y + h) - (int) y;

        // adjust box to mask size
        if (xi < 0)
        {
            wi += xi;
            xi = 0;
        }
        if (yi < 0)
        {
            hi += yi;
            yi = 0;
        }
        if ((xi + wi) > bounds.width)
            wi -= (xi + wi) - bounds.width;
        if ((yi + hi) > bounds.height)
            hi -= (yi + hi) - bounds.height;

        // scan all pixels, can take sometime if mask is large
        int offset = (yi * bounds.width) + xi;
        for (int j = 0; j < hi; j++)
        {
            for (int i = 0; i < wi; i++)
                if (maskData[offset++] != 0)
                    return true;

            offset += bounds.width - wi;
        }

        return false;
    }

    @Override
    public boolean[] getBooleanMask(int x, int y, int w, int h, boolean inclusive)
    {
        final boolean[] result = new boolean[w * h];

        // calculate intersection
        final Rectangle intersect = bounds.intersection(new Rectangle(x, y, w, h));

        // no intersection between mask and specified rectangle
        if (intersect.isEmpty())
            return result;

        // this ROI doesn't take care of inclusive parameter as intersect = contains
        int offSrc = 0;
        int offDst = 0;

        // adjust offset in source mask
        if (intersect.x > bounds.x)
            offSrc += (intersect.x - bounds.x);
        if (intersect.y > bounds.y)
            offSrc += (intersect.y - bounds.y) * bounds.width;
        // adjust offset in destination mask
        if (bounds.x > x)
            offDst += (bounds.x - x);
        if (bounds.y > y)
            offDst += (bounds.y - y) * w;

        for (int j = 0; j < intersect.height; j++)
        {
            for (int i = 0; i < intersect.width; i++)
                result[offDst++] = (maskData[offSrc++] != 0);

            offSrc += bounds.width - intersect.width;
            offDst += w - intersect.width;
        }

        return result;
    }

    @Override
    public double computeNumberOfPoints()
    {
        // just count the number of point contained in the mask
        double result = 0d;

        for (byte b : maskData)
            if (b != 0)
                result += 1d;

        return result;
    }

    @Override
    public boolean canTranslate()
    {
        return true;
    }

    @Override
    public void translate(double dx, double dy)
    {
        translateX += dx;
        translateY += dy;
        // convert to integer
        final int dxi = (int) translateX;
        final int dyi = (int) translateY;
        // keep trace of not used floating part
        translateX -= dxi;
        translateY -= dyi;

        bounds.translate(dxi, dyi);

        roiChanged();
    }

    @Override
    public boolean canSetPosition()
    {
        return true;
    }

    @Override
    public void setPosition2D(Point2D newPosition)
    {
        bounds.setLocation((int) newPosition.getX(), (int) newPosition.getY());

        roiChanged();
    }

    /**
     * Set the mask from a BooleanMask2D object
     */
    public void setAsBooleanMask(BooleanMask2D mask)
    {
        if ((mask != null) && !mask.isEmpty())
            setAsBooleanMask(mask.bounds, mask.mask);
    }

    /**
     * Set the mask from a boolean array.<br>
     * r represents the region defined by the boolean array.
     * 
     * @param r
     * @param booleanMask
     */
    protected void setAsByteMask(Rectangle r, byte[] mask)
    {
        // reset image with new rectangle
        updateImage(r);

        final int len = r.width * r.height;

        for (int i = 0; i < len; i++)
            maskData[i] = mask[i];

        optimizeBounds();
    }

    /**
     * Set the mask from a boolean array.<br>
     * r represents the region defined by the boolean array.
     * 
     * @param r
     * @param booleanMask
     */
    public void setAsBooleanMask(Rectangle r, boolean[] booleanMask)
    {
        // reset image with new rectangle
        updateImage(r);

        final int len = r.width * r.height;

        for (int i = 0; i < len; i++)
            maskData[i] = (byte) (booleanMask[i] ? 1 : 0);

        optimizeBounds();
    }

    public void setAsBooleanMask(int x, int y, int w, int h, boolean[] booleanMask)
    {
        setAsBooleanMask(new Rectangle(x, y, w, h), booleanMask);
    }

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

            final Rectangle rect = new Rectangle();

            // retrieve mask bounds
            rect.x = XMLUtil.getElementIntValue(node, ID_BOUNDS_X, 0);
            rect.y = XMLUtil.getElementIntValue(node, ID_BOUNDS_Y, 0);
            rect.width = XMLUtil.getElementIntValue(node, ID_BOUNDS_W, 0);
            rect.height = XMLUtil.getElementIntValue(node, ID_BOUNDS_H, 0);

            // retrieve mask data
            final byte[] data = XMLUtil.getElementBytesValue(node, ID_BOOLMASK_DATA, new byte[0]);
            // set the ROI from the unpacked boolean mask
            setAsByteMask(rect, data);
        }
        finally
        {
            endUpdate();
        }

        return true;
    }

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

        // retrieve mask bounds
        XMLUtil.setElementIntValue(node, ID_BOUNDS_X, bounds.x);
        XMLUtil.setElementIntValue(node, ID_BOUNDS_Y, bounds.y);
        XMLUtil.setElementIntValue(node, ID_BOUNDS_W, bounds.width);
        XMLUtil.setElementIntValue(node, ID_BOUNDS_H, bounds.height);

        // set mask data as byte array
        XMLUtil.setElementBytesValue(node, ID_BOOLMASK_DATA, maskData);

        return true;
    }
}