/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Icy is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package plugins.kernel.roi.roi2d;

import icy.painter.Anchor2D;
import icy.painter.PathAnchor2D;
import icy.resource.ResourceUtil;
import icy.roi.ROI;
import icy.type.point.Point5D;
import icy.util.ShapeUtil;
import icy.util.XMLUtil;

import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;

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

/**
 * ROI Path.<br>
 * This ROI can display a Path2D shape.<br>
 * You can modify and remove points (adding new point isn't supported).
 * 
 * @author Stephane
 */
public class ROI2DPath extends ROI2DShape
{
    public static final String ID_POINTS = "points";
    public static final String ID_POINT = "point";
    public static final String ID_WINDING = "winding";

    protected Area closedArea;
    protected Path2D openPath;

    static Path2D initPath(Point2D position)
    {
        final Path2D result = new Path2D.Double();

        result.reset();
        if (position != null)
            result.moveTo(position.getX(), position.getY());

        return result;
    }

    /**
     * Build a new ROI2DPath from the specified path.
     */
    public ROI2DPath(Path2D path, Area closedArea, Path2D openPath)
    {
        super(path);

        rebuildControlPointsFromPath();

        if (closedArea == null)
            this.closedArea = new Area(ShapeUtil.getClosedPath(path));
        else
            this.closedArea = closedArea;
        if (openPath == null)
            this.openPath = ShapeUtil.getOpenPath(path);
        else
            this.openPath = openPath;

        // set icon (default name is defined by getDefaultName())
        setIcon(ResourceUtil.ICON_ROI_POLYLINE);
    }

    /**
     * Build a new ROI2DPath from the specified path.
     */
    public ROI2DPath(Path2D path)
    {
        this(path, null, null);

    }

    /**
     * Build a new ROI2DPath from the specified path.
     */
    public ROI2DPath(Shape shape)
    {
        this(new Path2D.Double(shape), (shape instanceof Area) ? (Area) shape : null, null);
    }

    /**
     * @deprecated
     */
    @Deprecated
    public ROI2DPath(Point2D pt, boolean cm)
    {
        this(pt);
    }

    public ROI2DPath(Point2D position)
    {
        this(initPath(position));
    }

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

    public ROI2DPath()
    {
        this(new Path2D.Double(Path2D.WIND_NON_ZERO));
    }

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

    @Override
    protected Anchor2D createAnchor(Point2D pos)
    {
        return new PathAnchor2D(pos.getX(), pos.getY(), getColor(), getFocusedColor());
    }

    protected void rebuildControlPointsFromPath()
    {
        beginUpdate();
        try
        {
            // remove all point
            removeAllPoint();

            // add path points to the control point list
            for (Anchor2D pt : ShapeUtil.getAnchorsFromShape(getPath(), getColor(), getFocusedColor()))
                addPoint(pt);
        }
        finally
        {
            endUpdate();
        }
    }

    protected Path2D getPath()
    {
        return (Path2D) shape;
    }

    /**
     * Returns the closed area part of the ROI2DPath in {@link Area} shape format
     */
    public Area getClosedArea()
    {
        return closedArea;
    }

    /**
     * Returns the open path part of the ROI2DPath in {@link Path2D} shape format
     */
    public Path2D getOpenPath()
    {
        return openPath;
    }

    @Override
    public boolean canAddPoint()
    {
        // this ROI doesn't support point add
        return false;
    }

    @Override
    public boolean contains(double x, double y)
    {
        // only consider closed path
        return ShapeUtil.getClosedPath(getPath()).contains(x, y);
    }

    @Override
    public boolean contains(Point2D p)
    {
        // only consider closed path
        return ShapeUtil.getClosedPath(getPath()).contains(p);
    }

    @Override
    public boolean contains(double x, double y, double w, double h)
    {
        // only consider closed path
        return ShapeUtil.getClosedPath(getPath()).contains(x, y, w, h);
    }

    @Override
    public boolean contains(Rectangle2D r)
    {
        // only consider closed path
        return ShapeUtil.getClosedPath(getPath()).contains(r);
    }

    @Override
    public boolean contains(ROI roi)
    {
        // not closed --> do not contains anything
        if (!ShapeUtil.isClosed(shape))
            return false;

        return super.contains(roi);
    }

    @Override
    public ROI add(ROI roi, boolean allowCreate) throws UnsupportedOperationException
    {
        if (roi instanceof ROI2DShape)
        {
            final ROI2DShape roiShape = (ROI2DShape) roi;

            // only if on same position
            if ((getZ() == roiShape.getZ()) && (getT() == roiShape.getT()) && (getC() == roiShape.getC()))
            {
                final Path2D path = getPath();

                if (roi instanceof ROI2DPath)
                {
                    final ROI2DPath roiPath = (ROI2DPath) roi;

                    // compute closed area and open path parts
                    closedArea.add(roiPath.closedArea);
                    openPath.append(roiPath.openPath, false);
                }
                else
                {
                    // compute closed area and open path parts
                    if (roiShape.getShape() instanceof Area)
                        closedArea.add((Area) roiShape.getShape());
                    else
                        closedArea.add(new Area(ShapeUtil.getClosedPath(roiShape)));
                    openPath.append(ShapeUtil.getOpenPath(roiShape), false);
                }

                // then rebuild path from closed and open parts
                path.reset();
                path.append(closedArea, false);
                path.append(openPath, false);

                rebuildControlPointsFromPath();
                roiChanged(true);

                return this;
            }
        }

        return super.add(roi, allowCreate);
    }

    @Override
    public ROI intersect(ROI roi, boolean allowCreate) throws UnsupportedOperationException
    {
        if (roi instanceof ROI2DShape)
        {
            final ROI2DShape roiShape = (ROI2DShape) roi;

            // only if on same position
            if ((getZ() == roiShape.getZ()) && (getT() == roiShape.getT()) && (getC() == roiShape.getC()))
            {
                final Path2D path = getPath();

                if (roi instanceof ROI2DPath)
                {
                    final ROI2DPath roiPath = (ROI2DPath) roi;

                    // compute closed area intersection and clear open path
                    closedArea.intersect(roiPath.closedArea);
                    openPath.reset();
                }
                else
                {
                    // compute closed area intersection and clear open path
                    if (roiShape.getShape() instanceof Area)
                        closedArea.intersect((Area) roiShape.getShape());
                    else
                        closedArea.intersect(new Area(ShapeUtil.getClosedPath(roiShape)));
                    openPath.reset();
                }

                // then rebuild path from closed area (open part is empty)
                path.reset();
                path.append(closedArea, false);

                rebuildControlPointsFromPath();
                roiChanged(true);

                return this;
            }
        }

        return super.intersect(roi, allowCreate);
    }

    @Override
    public ROI exclusiveAdd(ROI roi, boolean allowCreate) throws UnsupportedOperationException
    {
        if (roi instanceof ROI2DShape)
        {
            final ROI2DShape roiShape = (ROI2DShape) roi;

            // only if on same position
            if ((getZ() == roiShape.getZ()) && (getT() == roiShape.getT()) && (getC() == roiShape.getC()))
            {
                final Path2D path = getPath();

                if (roi instanceof ROI2DPath)
                {
                    final ROI2DPath roiPath = (ROI2DPath) roi;

                    // compute exclusive union on closed area and simple append for open path
                    closedArea.exclusiveOr(roiPath.closedArea);
                    openPath.append(roiPath.openPath, false);
                }
                else
                {
                    // compute exclusive union on closed area and simple append for open path
                    if (roiShape.getShape() instanceof Area)
                        closedArea.exclusiveOr((Area) roiShape.getShape());
                    else
                        closedArea.exclusiveOr(new Area(ShapeUtil.getClosedPath(roiShape)));
                    openPath.append(ShapeUtil.getOpenPath(roiShape), false);
                }

                // then rebuild path from closed and open parts
                path.reset();
                path.append(closedArea, false);
                path.append(openPath, false);

                rebuildControlPointsFromPath();
                roiChanged(true);

                return this;
            }
        }

        return super.exclusiveAdd(roi, allowCreate);
    }

    @Override
    public ROI subtract(ROI roi, boolean allowCreate) throws UnsupportedOperationException
    {
        if (roi instanceof ROI2DShape)
        {
            final ROI2DShape roiShape = (ROI2DShape) roi;

            // only if on same position
            if ((getZ() == roiShape.getZ()) && (getT() == roiShape.getT()) && (getC() == roiShape.getC()))
            {
                final Path2D path = getPath();

                if (roi instanceof ROI2DPath)
                {
                    final ROI2DPath roiPath = (ROI2DPath) roi;

                    // compute closed area intersection and clear open path parts
                    closedArea.exclusiveOr(roiPath.closedArea);
                    if (!roiPath.closedArea.isEmpty())
                        openPath.reset();
                }
                else
                {
                    final Area area;

                    // compute closed area and open path parts
                    if (roiShape.getShape() instanceof Area)
                        area = (Area) roiShape.getShape();
                    else
                        area = new Area(ShapeUtil.getClosedPath(roiShape));
                    if (!area.isEmpty())
                    {
                        closedArea.exclusiveOr(area);
                        openPath.reset();
                    }
                }

                // then rebuild path from closed and open parts
                path.reset();
                path.append(closedArea, false);
                path.append(openPath, false);

                rebuildControlPointsFromPath();
                roiChanged(true);

                return this;
            }
        }

        return super.subtract(roi, allowCreate);
    }

    /**
     * Return the list of control points for this ROI.
     */
    public List<PathAnchor2D> getPathAnchors()
    {
        final List<PathAnchor2D> result = new ArrayList<PathAnchor2D>();

        synchronized (controlPoints)
        {
            for (Anchor2D pt : controlPoints)
                result.add((PathAnchor2D) pt);
        }

        return result;
    }

    protected void updateCachedStructures()
    {
        closedArea = new Area(ShapeUtil.getClosedPath(getPath()));
        openPath = ShapeUtil.getOpenPath(getPath());
    }

    @Override
    protected void updateShape()
    {
        ShapeUtil.buildPathFromAnchors(getPath(), getPathAnchors(), false);
        // update internal closed area and open path
        updateCachedStructures();

        // call super method after shape has been updated
        super.updateShape();
    }

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

            removeAllPoint();

            final List<Node> nodesPoint = XMLUtil.getChildren(XMLUtil.getElement(node, ID_POINTS), ID_POINT);
            if (nodesPoint != null)
            {
                for (Node n : nodesPoint)
                {
                    final PathAnchor2D pt = (PathAnchor2D) createAnchor(new Point2D.Double());
                    pt.loadPositionFromXML(n);
                    addPoint(pt);
                }
            }

            getPath().setWindingRule(XMLUtil.getElementIntValue(node, ID_WINDING, Path2D.WIND_NON_ZERO));
        }
        finally
        {
            endUpdate();
        }

        return true;
    }

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

        final Element points = XMLUtil.setElement(node, ID_POINTS);

        synchronized (controlPoints)
        {
            for (Anchor2D pt : controlPoints)
                pt.savePositionToXML(XMLUtil.addElement(points, ID_POINT));
        }

        XMLUtil.setElementIntValue(node, ID_WINDING, getPath().getWindingRule());

        return true;
    }
}