/*
 * @(#)BezierFigure.java 3.0.1  2007-11-30
 *
 * Copyright (c) 1996-2007 by the original authors of JHotDraw
 * and all its contributors.
 * All rights reserved.
 *
 * The copyright of this software is owned by the authors and  
 * contributors of the JHotDraw project ("the copyright holders").  
 * You may not use, copy or modify this software, except in  
 * accordance with the license agreement you entered into with  
 * the copyright holders. For details see accompanying license terms. 
 */

package org.jhotdraw.draw;

import org.jhotdraw.util.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.*;
import javax.swing.undo.*;
import java.io.*;
import static org.jhotdraw.draw.AttributeKeys.*;
import org.jhotdraw.geom.*;
import org.jhotdraw.xml.DOMInput;
import org.jhotdraw.xml.DOMOutput;
/**
 * A BezierFigure can be used to draw arbitrary shapes using a <code>BezierPath</code>.
 * It can be used to draw an open path or a closed shape.
 * <p>
 * A BezierFigure can have straight path segments and curved segments.
 * A straight path segment can be added by clicking on the drawing area.
 * Curved segments can be added by dragging the mouse pointer over the
 * drawing area.
 * <p> 
 * To creation of the BezierFigure can be finished by adding a segment
 * which closes the path, or by double clicking on the drawing area, or by
 * selecting a different tool in the DrawingEditor.
 * 
 *
 * @see org.jhotdraw.geom.BezierPath
 *
 * @version 3.0.1 2007-11-30 Changed method removeNode from protected to public. 
 * <br>3.0 2007-05-12 Got rid of basic methods.
 * <br>2.2.1 2007-04-22 Method contains did not work as expected for filled
 * unclosed beziers with thick line widths.
 * <br>2.2 2007-04-14 Added BezierContourHandle. We fill now open
 * paths as well.
 * <br>2.1.1 2006-06-08 Fixed caps drawing.
 * <br>2.1 2006-04-21 Improved caps drawing.
 * <br>2.0 2006-01-14 Changed to support double precison coordinates.
 * <br>1.0 March 14, 2004.
 * @author Werner Randelshofer
 */
public class BezierFigure extends AbstractAttributedFigure {
    /**
     * The BezierPath.
     */
    protected BezierPath path;
    /**
     * The cappedPath BezierPath is derived from variable path.
     * We cache it to increase the drawing speed of the figure.
     */
    private transient BezierPath cappedPath;
    
    
    /**
     * Creates an empty <code>BezierFigure</code>, for example without any
     * <code>BezierPath.Node</code>s.
     * The BezierFigure will not draw anything, if at least two nodes
     * are added to it. The <code>BezierPath</code> created by this constructor
     * is not closed.
     */
    public BezierFigure() {
        this(false);
    }
    /**
     * Creates an empty BezierFigure, for example without any
     * <code>BezierPath.Node</code>s.
     * The BezierFigure will not draw anything, unless at least two nodes
     * are added to it.
     *
     * @param isClosed Specifies whether the <code>BezierPath</code> shall
     * be closed.
     */
    public BezierFigure(boolean isClosed) {
        path = new BezierPath();
        CLOSED.basicSet(this, isClosed);
        //path.setClosed(isClosed);
    }
    
    // DRAWING
    // SHAPE AND BOUNDS
    // ATTRIBUTES
    // EDITING
    // CONNECTING
    /**
     * Returns the Figures connector for the specified location.
     * By default a ChopDiamondConnector is returned.
     * @see ChopDiamondConnector
     */
    public Connector findConnector(Point2D.Double p, ConnectionFigure prototype) {
        return new ChopBezierConnector(this);
    }
    
    public Connector findCompatibleConnector(Connector c, boolean isStart) {
        return new ChopBezierConnector(this);
    }
    // COMPOSITE FIGURES
    // CLONING
    // EVENT HANDLING
    protected void drawStroke(Graphics2D g) {
        if (isClosed()) {
            double grow = AttributeKeys.getPerpendicularDrawGrowth(this);
            if (grow == 0d) {
                g.draw(path);
            } else {
                GrowStroke gs = new GrowStroke((float) grow,
                        (float) (AttributeKeys.getStrokeTotalWidth(this) *
                        STROKE_MITER_LIMIT.get(this))
                        );
                g.draw(gs.createStrokedShape(path));
            }
        } else {
            g.draw(getCappedPath());
        }
        drawCaps(g);
    }
    
    protected void drawCaps(Graphics2D g) {
        if (getNodeCount() > 1) {
            if (START_DECORATION.get(this) != null) {
                BezierPath cp = getCappedPath();
                Point2D.Double p1 = path.get(0,0);
                Point2D.Double p2 = cp.get(0,0);
                if (p2.equals(p1)) {
                    p2 = path.get(1,0);
                }
                START_DECORATION.get(this).draw(g, this, p1, p2);
            }
            if (END_DECORATION.get(this) != null) {
                BezierPath cp = getCappedPath();
                Point2D.Double p1 = path.get(path.size()-1,0);
                Point2D.Double p2 = cp.get(path.size()-1,0);
                if (p2.equals(p1)) {
                    p2 = path.get(path.size()-2,0);
                }
                END_DECORATION.get(this).draw(g, this, p1, p2);
            }
        }
    }
    
    protected void drawFill(Graphics2D g) {
        if (isClosed() || FILL_OPEN_PATH.get(this)) {
            double grow = AttributeKeys.getPerpendicularFillGrowth(this);
            if (grow == 0d) {
                g.fill(path);
            } else {
                GrowStroke gs = new GrowStroke((float) grow,
                        (float) (AttributeKeys.getStrokeTotalWidth(this) *
                        STROKE_MITER_LIMIT.get(this))
                        );
                g.fill(gs.createStrokedShape(path));
            }
        }
    }
    
    public boolean contains(Point2D.Double p) {
        double tolerance = Math.max(2f, AttributeKeys.getStrokeTotalWidth(this) / 2d);
        if (isClosed() || FILL_COLOR.get(this) != null && FILL_OPEN_PATH.get(this)) {
            if (path.contains(p)) {
                return true;
            }
            double grow = AttributeKeys.getPerpendicularHitGrowth(this) * 2d;
            GrowStroke gs = new GrowStroke((float) grow,
                    (float) (AttributeKeys.getStrokeTotalWidth(this) *
                    STROKE_MITER_LIMIT.get(this))
                    );
            if (gs.createStrokedShape(path).contains(p)) {
                return true;
            } else {
                if (isClosed()) {
                    return false;
                }
            }
        }
        if (! isClosed()) {
            if (getCappedPath().outlineContains(p, tolerance)) {
                return true;
            }
            if (START_DECORATION.get(this) != null) {
                BezierPath cp = getCappedPath();
                Point2D.Double p1 = path.get(0,0);
                Point2D.Double p2 = cp.get(0,0);
                // FIXME - Check here, if caps path contains the point
                if (Geom.lineContainsPoint(p1.x,p1.y,p2.x,p2.y, p.x, p.y, tolerance)) {
                    return true;
                }
            }
            if (END_DECORATION.get(this) != null) {
                BezierPath cp = getCappedPath();
                Point2D.Double p1 = path.get(path.size()-1,0);
                Point2D.Double p2 = cp.get(path.size()-1,0);
                // FIXME - Check here, if caps path contains the point
                if (Geom.lineContainsPoint(p1.x,p1.y,p2.x,p2.y, p.x, p.y, tolerance)) {
                    return true;
                }
            }
        }
        return false;
    }
    /**
     * Checks if this figure can be connected. By default
     * filled BezierFigures can be connected.
     */
    public boolean canConnect() {
        return isClosed();
    }
    public Collection<Handle> createHandles(int detailLevel) {
        LinkedList<Handle> handles = new LinkedList<Handle>();
        switch (detailLevel % 2) {
            case 0 :
                handles.add(new BezierOutlineHandle(this));
                for (int i=0, n = path.size(); i < n; i++) {
                    handles.add(new BezierNodeHandle(this, i));
                }
                break;
            case 1 :
                TransformHandleKit.addTransformHandles(this, handles);
                handles.add(new BezierScaleHandle(this));
                break;
        }
        return handles;
    }
    
    public Rectangle2D.Double getBounds() {
        Rectangle2D.Double bounds =path.getBounds2D();
        // Make sure, bounds are not empty
        bounds.width = Math.max(1, bounds.width);
        bounds.height = Math.max(1, bounds.height);
        return bounds;
    }
    public Rectangle2D.Double getDrawingArea() {
        Rectangle2D.Double r = super.getDrawingArea();
        
        if (getNodeCount() > 1) {
            if (START_DECORATION.get(this) != null) {
                Point2D.Double p1 = getPoint(0, 0);
                Point2D.Double p2 = getPoint(1, 0);
                r.add(START_DECORATION.get(this).getDrawingArea(this, p1, p2));
            }
            if (END_DECORATION.get(this) != null) {
                Point2D.Double p1 = getPoint(getNodeCount() - 1, 0);
                Point2D.Double p2 = getPoint(getNodeCount() - 2, 0);
                r.add(END_DECORATION.get(this).getDrawingArea(this, p1, p2));
            }
        }
        
        return r;
    }
    
    protected void validate() {
        super.validate();
        path.invalidatePath();
        cappedPath = null;
    }
    
    
    
    /**
     * Returns a clone of the bezier path of this figure.
     */
    public BezierPath getBezierPath() {
        return (BezierPath) path.clone();
    }
    public void setBezierPath(BezierPath newValue) {
        path = (BezierPath) newValue.clone();
        this.setClosed(newValue.isClosed());
    }
    
    public Point2D.Double getPointOnPath(float relative, double flatness) {
        return path.getPointOnPath(relative, flatness);
    }
    
    public boolean isClosed() {
        return (Boolean) getAttribute(CLOSED);
    }
    public void setClosed(boolean newValue) {
        CLOSED.set(this, newValue);
    }
    public void setAttribute(AttributeKey key, Object newValue) {
        if (key == CLOSED) {
            path.setClosed((Boolean) newValue);
        } else if (key == WINDING_RULE) {
            path.setWindingRule(newValue == AttributeKeys.WindingRule.EVEN_ODD ? GeneralPath.WIND_EVEN_ODD : GeneralPath.WIND_NON_ZERO);
        }
        super.setAttribute(key, newValue);
        invalidate();
    }
    
    /**
     * Sets the location of the first and the last <code>BezierPath.Node</code>
     * of the BezierFigure.
     * If the BezierFigure has not at least two nodes, nodes are added
     * to the figure until the BezierFigure has at least two nodes.
     */
    public void setBounds(Point2D.Double anchor, Point2D.Double lead) {
        setStartPoint(anchor);
        setEndPoint(lead);
        invalidate();
    }
    public void transform(AffineTransform tx) {
        path.transform(tx);
        invalidate();
    }
    public void invalidate() {
        super.invalidate();
        path.invalidatePath();
        cappedPath = null;
    }
    
    /**
     * Returns a path which is cappedPath at the ends, to prevent
     * it from drawing under the end caps.
     */
    protected BezierPath getCappedPath() {
        if (cappedPath == null) {
            cappedPath = (BezierPath) path.clone();
            if (isClosed()) {
                cappedPath.setClosed(true);
            } else {
                if (cappedPath.size() > 1) {
                    if (START_DECORATION.get(this) != null) {
                        BezierPath.Node p0 = cappedPath.get(0);
                        BezierPath.Node p1 = cappedPath.get(1);
                        Point2D.Double pp;
                        if ((p0.getMask() & BezierPath.C2_MASK) != 0) {
                            pp = p0.getControlPoint(2);
                        } else if ((p1.getMask() & BezierPath.C1_MASK) != 0) {
                            pp = p1.getControlPoint(1);
                        } else {
                            pp = p1.getControlPoint(0);
                        }
                        double radius = START_DECORATION.get(this).getDecorationRadius(this);
                        double lineLength = Geom.length(p0.getControlPoint(0), pp);
                        cappedPath.set(0,0, Geom.cap(pp, p0.getControlPoint(0), - Math.min(radius, lineLength)));
                    }
                    if (END_DECORATION.get(this) != null) {
                        BezierPath.Node p0 = cappedPath.get(cappedPath.size() - 1);
                        BezierPath.Node p1 = cappedPath.get(cappedPath.size() - 2);
                        
                        Point2D.Double pp;
                        if ((p0.getMask() & BezierPath.C1_MASK) != 0) {
                            pp = p0.getControlPoint(1);
                        } else if ((p1.getMask() & BezierPath.C2_MASK) != 0) {
                            pp = p1.getControlPoint(2);
                        } else {
                            pp = p1.getControlPoint(0);
                        }
                        
                        
                        double radius = END_DECORATION.get(this).getDecorationRadius(this);
                        double lineLength = Geom.length(p0.getControlPoint(0), pp);
                        cappedPath.set(cappedPath.size() - 1, 0, Geom.cap(pp, p0.getControlPoint(0), -Math.min(radius, lineLength)));
                    }
                    cappedPath.invalidatePath();
                }
            }
        }
        return cappedPath;
    }
    public void layout() {
    }
    
    /**
     * Adds a control point.
     */
    public void addNode(BezierPath.Node p) {
        addNode(getNodeCount(), p);
    }
    /**
     * Adds a node to the list of points.
     */
    public void addNode(final int index, BezierPath.Node p) {
        final BezierPath.Node newPoint = new BezierPath.Node(p);
        path.add(index, p);
        invalidate();
    }
    /**
     * Sets a control point.
     */
    public void setNode(int index, BezierPath.Node p) {
        path.set(index, p);
        invalidate();
    }
    
    /**
     * Gets a control point.
     */
    public BezierPath.Node getNode(int index) {
        return (BezierPath.Node) path.get(index).clone();
    }
    /**
     * Convenience method for getting the point coordinate of
     * the first control point of the specified node.
     */
    public Point2D.Double getPoint(int index) {
        return path.get(index).getControlPoint(0);
    }
    /**
     * Gets the point coordinate of a control point.
     */
    public Point2D.Double getPoint(int index, int coord) {
        return path.get(index).getControlPoint(coord);
    }
    /**
     * Sets the point coordinate of control point 0 at the specified node.
     */
    public void setPoint(int index, Point2D.Double p) {
        BezierPath.Node node = path.get(index);
        double dx = p.x - node.x[0];
        double dy = p.y - node.y[0];
        for (int i=0; i < node.x.length; i++) {
            node.x[i] += dx;
            node.y[i] += dy;
        }
        invalidate();
    }
    /**
     * Sets the point coordinate of a control point.
     */
    public void setPoint(int index, int coord, Point2D.Double p) {
        BezierPath.Node cp = new BezierPath.Node(path.get(index));
        cp.setControlPoint(coord, p);
        setNode(index, cp);
    }
    /**
     * Convenience method for setting the point coordinate of the start point.
     * If the BezierFigure has not at least two nodes, nodes are added
     * to the figure until the BezierFigure has at least two nodes.
     */
    public void setStartPoint(Point2D.Double p) {
        // Add two nodes if we haven't at least two nodes
        for (int i=getNodeCount(); i < 2; i++) {
            addNode(0, new BezierPath.Node(p.x, p.y));
        }
        setPoint(0, p);
    }
    /**
     * Convenience method for setting the point coordinate of the end point.
     * If the BezierFigure has not at least two nodes, nodes are added
     * to the figure until the BezierFigure has at least two nodes.
     */
    public void setEndPoint(Point2D.Double p) {
        // Add two nodes if we haven't at least two nodes
        for (int i=getNodeCount(); i < 2; i++) {
            addNode(0, new BezierPath.Node(p.x, p.y));
        }
        setPoint(getNodeCount() - 1, p);
    }
    /**
     * Convenience method for getting the start point.
     */
    public Point2D.Double getStartPoint() {
        return getPoint(0, 0);
    }
    /**
     * Convenience method for getting the end point.
     */
    public Point2D.Double getEndPoint() {
        return getPoint(getNodeCount() - 1, 0);
    }
    /**
     * Finds a control point index.
     * Returns -1 if no control point could be found.
     * FIXME - Move this to BezierPath
     */
    public int findNode(Point2D.Double p) {
        BezierPath tp = path;
        for (int i=0; i < tp.size(); i++) {
            BezierPath.Node p2 = tp.get(i);
            if (p2.x[0] == p.x && p2.y[0] == p.y) {
                return i;
            }
        }
        return -1;
    }
    /**
     * Gets the segment of the polyline that is hit by
     * the given Point2D.Double.
     * @return the index of the segment or -1 if no segment was hit.
     *
     * XXX - Move this to BezierPath
     */
    public int findSegment(Point2D.Double find) {
        // Fixme - use path iterator
        
        Point2D.Double p1, p2;
        for (int i = 0, n = getNodeCount() - 1; i < n; i++) {
            p1 = path.get(i, 0);
            p2 = path.get(i+1, 0);
            if (Geom.lineContainsPoint(p1.x, p1.y, p2.x, p2.y, find.x, find.y, 3d)) {
                return i;
            }
        }
        return -1;
    }
    /**
     * Joins two segments into one if the given Point2D.Double hits a node
     * of the polyline.
     * @return true if the two segments were joined.
     *
     * XXX - Move this to BezierPath
     */
    public boolean joinSegments(Point2D.Double join) {
        int i = findSegment(join);
        if (i != -1 && i > 1) {
            removeNode(i);
            return true;
        }
        return false;
    }
    /**
     * Splits the segment at the given Point2D.Double if a segment was hit.
     * @return the index of the segment or -1 if no segment was hit.
     *
     * XXX - Move this to BezierPath
     */
    public int splitSegment(Point2D.Double split) {
        int i = findSegment(split);
        if (i != -1) {
            addNode(i + 1, new BezierPath.Node(split));
        }
        return i+1;
    }
    /**
     * Removes the Node at the specified index.
     */
    public BezierPath.Node removeNode(int index) {
       return path.remove(index);
    }
    /**
     * Removes the Point2D.Double at the specified index.
     */
    protected void removeAllNodes() {
        path.clear();
    }
    /**
     * Gets the node count.
     */
    public int getNodeCount() {
        return path.size();
    }
    
    public BezierFigure clone() {
        BezierFigure that = (BezierFigure) super.clone();
        that.path = (BezierPath) this.path.clone();
        that.invalidate();
        return that;
    }
    
    public void restoreTransformTo(Object geometry) {
        path.setTo((BezierPath) geometry);
    }
    
    public Object getTransformRestoreData() {
        return path.clone();
    }
    
    public Point2D.Double chop(Point2D.Double p) {
        if (isClosed()) {
            double grow = AttributeKeys.getPerpendicularHitGrowth(this);
            if (grow == 0d) {
                return path.chop(p);
            } else {
                GrowStroke gs = new GrowStroke((float) grow,
                        (float) (AttributeKeys.getStrokeTotalWidth(this) *
                        STROKE_MITER_LIMIT.get(this))
                        );
                return Geom.chop(gs.createStrokedShape(path), p);
            }
        } else {
            return path.chop(p);
        }
    }
    
    public Point2D.Double getCenter() {
        return path.getCenter();
    }
    public Point2D.Double getOutermostPoint() {
        return path.get(path.indexOfOutermostNode()).getControlPoint(0);
    }
    /**
     * Joins two segments into one if the given Point2D.Double hits a node
     * of the polyline.
     * @return true if the two segments were joined.
     */
    public int joinSegments(Point2D.Double join, float tolerance) {
        return path.joinSegments(join, tolerance);
    }
    /**
     * Splits the segment at the given Point2D.Double if a segment was hit.
     * @return the index of the segment or -1 if no segment was hit.
     */
    public int splitSegment(Point2D.Double split, float tolerance) {
        return path.splitSegment(split, tolerance);
    }
    /**
     * Handles a mouse click.
     */
    @Override public boolean handleMouseClick(Point2D.Double p, MouseEvent evt, DrawingView view) {
        if (evt.getClickCount() == 2 && view.getHandleDetailLevel() % 2 == 0) {
            willChange();
            final int index = splitSegment(p, (float) (5f / view.getScaleFactor()));
            if (index != -1) {
                final BezierPath.Node newNode = getNode(index);
                fireUndoableEditHappened(new AbstractUndoableEdit() {
                    public String getPresentationName() {
                        ResourceBundleUtil labels = ResourceBundleUtil.getLAFBundle("org.jhotdraw.draw.Labels");
                        return labels.getString("bezierPath.splitSegment");
                    }
                    public void redo() throws CannotRedoException {
                        super.redo();
                        willChange();
                        addNode(index, newNode);
                        changed();
                    }
                    
                    public void undo() throws CannotUndoException {
                        super.undo();
                        willChange();
                        removeNode(index);
                        changed();
                    }
                    
                });
                changed();
                evt.consume();
                return true;
            }
        }
        return false;
    }
    
    public void write(DOMOutput out) throws IOException {
        writePoints(out);
        writeAttributes(out);
    }
    protected void writePoints(DOMOutput out) throws IOException {
        out.openElement("points");
        if (isClosed()) {
            out.addAttribute("closed", true);
        }
        for (int i=0, n = getNodeCount(); i < n; i++) {
            BezierPath.Node node = getNode(i);
            out.openElement("p");
            out.addAttribute("mask", node.mask, 0);
            out.addAttribute("colinear", true);
            out.addAttribute("x", node.x[0]);
            out.addAttribute("y", node.y[0]);
            out.addAttribute("c1x", node.x[1], node.x[0]);
            out.addAttribute("c1y", node.y[1], node.y[0]);
            out.addAttribute("c2x", node.x[2], node.x[0]);
            out.addAttribute("c2y", node.y[2], node.y[0]);
            out.closeElement();
        }
        out.closeElement();
    }
    
    @Override public void read(DOMInput in) throws IOException {
        readPoints(in);
        readAttributes(in);
    }
    protected void readPoints(DOMInput in) throws IOException {
        path.clear();
        in.openElement("points");
        setClosed(in.getAttribute("closed", false));
        
        for (int i=0, n = in.getElementCount("p"); i < n; i++) {
            in.openElement("p", i);
            BezierPath.Node node = new BezierPath.Node(
                    in.getAttribute("mask", 0),
                    in.getAttribute("x", 0d),
                    in.getAttribute("y", 0d),
                    in.getAttribute("c1x", in.getAttribute("x", 0d)),
                    in.getAttribute("c1y", in.getAttribute("y", 0d)),
                    in.getAttribute("c2x", in.getAttribute("x", 0d)),
                    in.getAttribute("c2y", in.getAttribute("y", 0d))
                    );
            node.keepColinear = in.getAttribute("colinear", true);
            path.add(node);
            path.invalidatePath();
            in.closeElement();
        }
        in.closeElement();
    }
}