/*
 * @(#)PolyLineFigure.java 5.1
 *
 */

package CH.ifa.draw.figures;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.GeneralPath;
import java.io.IOException;
import java.io.Serial;
import java.util.Enumeration;
import java.util.Vector;

import CH.ifa.draw.util.BSpline;
import CH.ifa.draw.util.DoublePoint;
import CH.ifa.draw.util.Geom;
import de.renew.draw.storables.ontology.Connector;
import de.renew.draw.storables.ontology.Locator;
import de.renew.draw.storables.ontology.StorableInput;
import de.renew.draw.storables.ontology.StorableOutput;
import de.renew.draw.ui.ontology.FigureHandle;
import de.renew.util.ClassSource;

/**
 * A poly line figure consists of a list of points.
 * It has an optional line decoration at the start and end.
 * <p>
 * {@link LineDecoration}
 */

// The write method assumes that PolyLineFigure inherits
// directly from AttributeFigure.
public class PolyLineFigure extends AttributeFigure implements PolyLineable {

    /**
     * Logger for the class PolyLineFigure.
     */
    public static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(PolyLineFigure.class);

    /**
     * No arrow tip.
     */
    public final static int ARROW_TIP_NONE = 0;

    /**
     * Arrow tip at the start of the line.
     */
    public final static int ARROW_TIP_START = 1;

    /**
     * Arrow tip at the end of the line.
     */
    public final static int ARROW_TIP_END = 2;

    /**
     * Arrow tips at the start and end of the line.
     */
    public final static int ARROW_TIP_BOTH = 3;

    /**
     * Line shape.
     */
    public final static int LINE_SHAPE = 0;

    /**
     * B-spline shape.
     */
    public final static int BSPLINE_SHAPE = 1;

    /*
     * Serialization support.
     */
    @Serial
    private static final long serialVersionUID = -7951352179906577773L;
    private static final Integer BSPLINE_SHAPE_OBJ = BSPLINE_SHAPE;

    /**
     * List of control points which determine the form
     * of the polyline figure.
     *
     * @serial
     **/
    protected Vector<Point> fPoints;

    /**
     * Helper object which transforms a vector of control
     * points into a vector of drawing points. Initially
     * <code>null</code>, will be created when needed.
     * <p>
     * This field is transient because it is sufficient
     * to serialize the control points in <code>fPoints</code>
     * along with the spline-related attributes.
     * </p>
     **/
    protected transient BSpline _spline = null;

    /**
     * Line decoration (like an arrow tip, for example)
     * at the start of the line. May be <code>null</code>.
     *
     * @serial
     **/
    protected LineDecoration _startDecoration = null;

    /**
     * Line decoration (like an arrow tip, for example)
     * at the end of the line. May be <code>null</code>.
     *
     * @serial
     **/
    protected LineDecoration _endDecoration = null;

    /**
     * Determines the type of decoration to be added by the
     * <code>setStart</code>- or -<code>EndDecoration()</code>
     * methods. Defaults to {@link ArrowTip}.
     *
     * @serial
     **/
    protected Class<?> fArrowTipClass = ArrowTip.class;

    /**
     * Serial number for the serialized data format of PolyLineFigures.
     */
    @SuppressWarnings("unused")
    private final int _polyLineFigureSerializedDataVersion = 1;

    /**
     * Constructs an empty PolyLineFigure and initializes the Point Vector with a size of 4.
     */
    public PolyLineFigure() {
        fPoints = new Vector<>(4);
    }

    /**
     * Constructs an empty PolyLineFigure and initializes the Point Vector with the given size.
     *
     * @param size initial Point Vector size
     */
    public PolyLineFigure(int size) {
        fPoints = new Vector<>(size);
    }

    /**
     * Constructs a PolyLineFigure, initializes the Point Vector and adds a Point with the given
     * coordinates (x,y) to it.
     *
     * @param x x-coordinate of the initial Point
     * @param y y-coordinate of the initial Point
     */
    public PolyLineFigure(int x, int y) {
        fPoints = new Vector<>();
        fPoints.addElement(new Point(x, y));
    }

    /**
     * Determines whether the polyline should be drawn as a spline.
     *
     * @return if the polyline should be drawn as a spline
     */
    protected boolean drawAsSpline() {
        Object lineShape = getAttribute("LineShape");
        return BSPLINE_SHAPE_OBJ.equals(lineShape);
    }

    //NOTICEredundant
    private Vector<Point> drawPointsVector() {
        if (drawAsSpline()) {
            if (_spline == null) {
                // We only need to set the degree, segments and points in
                // this case since this should be done immediately when the
                // parameter change occurs (e.g. in setAttribute or
                // insert/delete/movePoint).
                updateSpline();
            }
            return _spline.getCurvepointsInt();
        }
        return fPoints;
    }

    //NOTICEredundant
    private Vector<DoublePoint> drawDoublePointsVector() {
        if (drawAsSpline()) {
            if (_spline == null) {
                // We only need to set the degree, segments and points in
                // this case since this should be done immediately when the
                // parameter change occurs (e.g. in setAttribute or
                // insert/delete/movePoint).
                updateSpline();
            }
            return _spline.getCurvepointsDouble();
        }
        return DoublePoint.convertPointVector(fPoints);
    }

    private void updateSpline() {
        Object splineDegree = getAttribute("BSplineDegree");
        Object splineSegments = getAttribute("BSplineSegments");
        _spline = new BSpline(fPoints, (Integer) splineSegments, (Integer) splineDegree);
    }

    /**
     * Returns a rectangle which contains the drawn figure.
     */
    @Override
    public Rectangle displayBox() {
        Enumeration<Point> k = points();
        Rectangle r = new Rectangle(k.nextElement());

        while (k.hasMoreElements()) {
            r.add(k.nextElement());
        }

        return r;
    }

    @Override
    public boolean isEmpty() {
        return (size().width < 3) && (size().height < 3);
    }

    /**
     * Adds to the given vector a <code>PolylineHandle</code>
     * for each intermediate control point in the line and a
     * <code>InsertPointHandle</code> for each line segment.
     * Start and end control points are not included!
     *
     * @param handles the given Vector
     **/
    protected void addIntermediateHandles(Vector<FigureHandle> handles) {
        for (int i = 1; i < fPoints.size() - 1; i++) {
            handles.addElement(new PolyLineHandle(this, locator(i), i));
        }
        for (int i = 0; i < fPoints.size() - 1; i++) {
            handles.addElement(new InsertPointHandle(this, i));
        }
    }

    /**
     * Returns a set of handles: <br>
     * - one handle for each control point, including start and end <br>
     * - one handle in the middle of each segment to add new points.
     **/
    @Override
    public Vector<FigureHandle> handles() {
        int n = fPoints.size();
        Vector<FigureHandle> handles = new Vector<>(2 * n);
        handles.addElement(new PolyLineHandle(this, locator(0), 0));
        addIntermediateHandles(handles);
        handles.addElement(new PolyLineHandle(this, locator(n - 1), n - 1));
        return handles;
    }

    @Override
    public void basicDisplayBox(Point origin, Point corner) {}

    /**
     * Adds a Point to the Point Vector. If a spline is present, updates its points to reflect the change.
     * Also informs listeners of the change.
     *
     * @param x x-coordinate of the added Point
     * @param y y-coordinate of the added Point
     */
    public void addPoint(int x, int y) {
        fPoints.addElement(new Point(x, y));
        if (_spline != null) {
            _spline.setPoints(fPoints);
        }
        changed();
    }

    /**
     * Returns all Points of the Point Vector in an Enumeration.
     *
     * @return Enumeration containing all points of the Point Vector
     */
    public Enumeration<Point> points() {
        return fPoints.elements();
    }

    @Override
    public int pointCount() {
        return fPoints.size();
    }

    @Override
    protected void basicMoveBy(int dx, int dy) {
        Enumeration<Point> k = fPoints.elements();
        while (k.hasMoreElements()) {
            k.nextElement().translate(dx, dy);
        }
        if (_spline != null) {
            _spline.setPoints(fPoints);
        }
    }

    /**
     * Changes the position of a node.
     */
    @Override
    public void setPointAt(Point p, int i) {
        willChange();
        fPoints.setElementAt(p, i);
        if (_spline != null) {
            _spline.setPoints(fPoints);
        }
        changed();
    }

    /* (non-Javadoc)
     * @see CH.ifa.draw.figures.PolyLineable#insertPointAt(java.awt.Point, int)
     */
    @Override
    public void insertPointAt(Point p, int i) {
        fPoints.insertElementAt(p, i);
        if (_spline != null) {
            _spline.setPoints(fPoints);
        }
        changed();
    }

    @Override
    public void removePointAt(int i) {
        // Don't remove the last two points.
        if (fPoints.size() > 2) {
            willChange();
            fPoints.removeElementAt(i);
            if (_spline != null) {
                _spline.setPoints(fPoints);
            }
            changed();
        }
    }

    /**
     * Splits the segment at the given point if a segment was hit.
     *
     * @param x x-coordinate of the Point
     * @param y y-coordinate of the Point
     * @return the index of the segment or 0 if no segment was hit.
     */
    public int splitSegment(int x, int y) {
        int i = findSegment(x, y);
        if (i != -1) {
            insertPointAt(new Point(x, y), i + 1);
        }

        // logger.debug("splitsegment returns: "+(i+1));
        return i + 1;
    }

    @Override
    public Point pointAt(int i) {
        return fPoints.elementAt(i);
    }

    /**
     * Joins two segments into one if the given point hits a node of the polyline.
     *
     * @param x x-coordinate of the Point
     * @param y y-coordinate of the Point
     * @return true if the two segments were joined.
     */
    public boolean joinSegments(int x, int y) {
        for (int i = 1; i < fPoints.size() - 1; i++) {
            Point p = pointAt(i);
            if (Geom.length(x, y, p.x, p.y) < 3) {
                removePointAt(i);
                return true;
            }
        }
        return false;
    }

    @Override
    public Connector connectorAt(int x, int y) {
        return new PolyLineConnector(this);
    }

    /**
     * Sets the start decoration.
     *
     * @param l LineDecoration to which the start decoration will be set
     */
    public void setStartDecoration(LineDecoration l) {
        _startDecoration = l;
    }

    /**
     * Sets the default start decoration.
     */
    protected void setStartDecoration() {
        try {
            setStartDecoration(
                (LineDecoration) fArrowTipClass.getDeclaredConstructor().newInstance());
        } catch (Exception e) {
            LOGGER.error("Could not set arrow tip:\n" + e);
        }
    }

    /**
     * Sets the end decoration.
     *
     * @param l LineDecoration to which the end decoration will be set
     */
    public void setEndDecoration(LineDecoration l) {
        _endDecoration = l;
    }

    /**
     * Calculates the total distance of the PolyLineFigure by adding up the distances
     * between each pair of consecutive points.
     *
     * @return total distance of the PolyLineFigure
     */
    public float distance() {
        Enumeration<Point> points = points();
        Point first = points.nextElement();
        float distance = 0;
        while (points.hasMoreElements()) {
            Point second = points.nextElement();
            distance += (float) first.distance(second);
            first = second;
        }
        return distance;
    }

    /**
     * Sets the default end decoration.
     */
    protected void setEndDecoration() {
        try {
            setEndDecoration(
                (LineDecoration) fArrowTipClass.getDeclaredConstructor().newInstance());
        } catch (Exception e) {
            LOGGER.error("Could not set arrow tip:\n" + e);
        }
    }

    @Override
    public void drawFrame(Graphics g) {
        Vector<DoublePoint> drawPoints = drawDoublePointsVector();
        int size = drawPoints.size();

        double[] xPoints = new double[size];
        double[] yPoints = new double[size];

        for (int i = 0; i < size; i++) {
            DoublePoint p = (drawPoints.elementAt(i));
            xPoints[i] = p.x;
            yPoints[i] = p.y;
        }

        GeneralPath shape = new GeneralPath();
        int i = 0;

        //TODO We have to cast to float here as Java 1.5 only supports float
        //Remove the cast when we switch to Java 1.6
        shape.moveTo((float) xPoints[i], (float) yPoints[i]);
        while (i < size) {
            shape.lineTo((float) xPoints[i], (float) yPoints[i]);
            i++;
        }

        Graphics2D g2 = (Graphics2D) g;
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        Integer width = (Integer) getAttribute(LINE_WIDTH_KEY);
        BasicStroke str = (BasicStroke) g2.getStroke();
        if (width != null) {
            //}else{
            //            g2.setStroke(new BasicStroke((float)width.intValue()));
            BasicStroke s = new BasicStroke(
                width, str.getEndCap(), str.getLineJoin(), str.getMiterLimit(), str.getDashArray(),
                str.getDashPhase());
            g2.setStroke(s);
        }
        g2.draw(shape);
        //        Stroke stroke = g2.getStroke();
        if (width == null) {
            g2.setStroke(new BasicStroke());
        } else {
            g2.setStroke(new BasicStroke(width));
        }
        decorate(g);
        g2.setStroke(str);
    }

    @Override
    public boolean containsPoint(int x, int y) {
        Rectangle bounds = displayBox();
        bounds.grow(4, 4);
        if (!bounds.contains(x, y)) {
            return false;
        }

        Vector<Point> drawPoints = drawPointsVector();
        Point p1;
        Point p2;
        for (int i = 0; i < drawPoints.size() - 1; i++) {
            p1 = (drawPoints.elementAt(i));
            p2 = (drawPoints.elementAt(i + 1));
            if (Geom.lineContainsPoint(p1.x, p1.y, p2.x, p2.y, x, y)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Return line segment number i. In case of a spline, the returned
     * value indicates a segment between two handles.
     */
    private int lineSegment(int i) {
        if (drawAsSpline()) {
            return _spline.lineSegment(i);
        } else {
            return i;
        }
    }

    /**
     * Gets the segment of the polyline that is hit by
     * the given point.
     *
     * @param x x-coordinate of the Point
     * @param y y-coordinate of the Point
     * @return the index of the segment or -1 if no segment was hit.
     */
    public int findSegment(int x, int y) {
        Vector<Point> drawPoints = drawPointsVector();
        Point p1;
        Point p2;
        for (int i = 0; i < drawPoints.size() - 1; i++) {
            p1 = (drawPoints.elementAt(i));
            p2 = (drawPoints.elementAt(i + 1));
            if (Geom.lineContainsPoint(p1.x, p1.y, p2.x, p2.y, x, y)) {
                // logger.debug("findsegment: i="+i);
                return lineSegment(i);
            }
        }
        return -1;
    }

    private void decorate(Graphics g) {
        if (_startDecoration != null) {
            Point p1 = fPoints.elementAt(0);
            Point p2 = fPoints.elementAt(1);
            _startDecoration.draw(g, p1.x, p1.y, p2.x, p2.y, getFillColor(), getFrameColor());
        }
        if (_endDecoration != null) {
            Point p3 = fPoints.elementAt(fPoints.size() - 2);
            Point p4 = fPoints.elementAt(fPoints.size() - 1);
            _endDecoration.draw(g, p4.x, p4.y, p3.x, p3.y, getFillColor(), getFrameColor());
        }
    }

    /**
     * Gets the attribute with the given name.
     * PolyLineFigure maps "ArrowMode" and "ArrowTip" to a
     * line decoration.
     */
    @Override
    public Object getAttribute(String name) {
        if (name.equals("ArrowMode")) {
            int value = 0;
            if (_startDecoration != null) {
                value |= ARROW_TIP_START;
            }
            if (_endDecoration != null) {
                value |= ARROW_TIP_END;
            }
            return value;
        } else if (name.equals("ArrowTip")) {
            return fArrowTipClass.getName();
        }
        return super.getAttribute(name);
    }

    /**
     * Sets the attribute with the given name.
     * PolyLineFigure interprets "ArrowMode" and "ArrowTip" to set
     * the line decoration.
     */
    @Override
    public void setAttribute(String name, Object value) {
        // Reasons for the case distinction in this method:
        // - ArrowMode and ArrowTip
        //     are not stored in the usual attribute management
        //     of the superclass, but interpreted directly and
        //     applied to the appropriate fields.
        // - LineStyle
        //     is checked for validity before it is stored by
        //     the usual attribute mechanism.
        // - BSplineDegree and BSplineSegments
        //     are stored the normal way, but the caching spline
        //     object must be informed immediately, too.
        switch (name) {
            case "ArrowMode" -> {
                Integer intObj = (Integer) value;
                if (intObj != null) {
                    int decoration = intObj;

                    if ((decoration & ARROW_TIP_START) != 0) {
                        setStartDecoration();
                    } else {
                        setStartDecoration(null);
                    }

                    if ((decoration & ARROW_TIP_END) != 0) {
                        setEndDecoration();
                    } else {
                        setEndDecoration(null);
                    }
                }
            }
            case "ArrowTip" -> {
                String decorationClass = (String) value;
                try {
                    fArrowTipClass = ClassSource.classForName(decorationClass);
                    if (_startDecoration != null) {
                        setStartDecoration();
                    }
                    if (_endDecoration != null) {
                        setEndDecoration();
                    }
                } catch (ClassNotFoundException e) {
                    LOGGER.error("Trying to use " + decorationClass + " for arrow tips:\n" + e);
                }
            }
            case "LineStyle" -> {
                String lineStyle = (String) value;
                if (value != null) {
                    super.setAttribute("LineStyle", lineStyle);
                }
            }
            default -> {
                if (_spline != null) {
                    if (name.equals("BSplineDegree")) {
                        int degree = (Integer) value;
                        _spline.setDegree(degree);
                    } else if (name.equals("BSplineSegments")) {
                        int segments = (Integer) value;
                        _spline.setSegments(segments);
                    }
                }
                super.setAttribute(name, value);
            }
        }

        // To be safe, I always claim that I have changed.
        changed();
    }

    @Override
    public void write(StorableOutput dw) {
        super.write(dw);
        dw.writeInt(fPoints.size());
        Enumeration<Point> k = fPoints.elements();
        while (k.hasMoreElements()) {
            Point p = k.nextElement();
            dw.writeInt(p.x);
            dw.writeInt(p.y);
        }
        dw.writeStorable(_startDecoration);
        dw.writeStorable(_endDecoration);
        dw.writeString(fArrowTipClass.getName());
    }

    @Override
    public void read(StorableInput dr) throws IOException {
        if (dr.getVersion() >= 1) {
            // We are reading a recent file where
            // attributes have been saved.
            super.read(dr);
        }

        int size = dr.readInt();
        fPoints = new Vector<>(size);
        for (int i = 0; i < size; i++) {
            int x = dr.readInt();
            int y = dr.readInt();
            fPoints.addElement(new Point(x, y));
        }
        _startDecoration = (LineDecoration) dr.readStorable();
        _endDecoration = (LineDecoration) dr.readStorable();

        if (dr.getVersion() >= 8) {
            String arrowName = dr.readString();
            setAttribute("ArrowTip", arrowName);
        }

        if (dr.getVersion() == 0) {
            // We are reading an ancient file where the
            // color was saved explicitly.
            Color color = new Color(dr.readInt(), dr.readInt(), dr.readInt());
            setAttribute("FrameColor", color);
        }
    }

    /**
     * Creates a locator for the point with the given index.
     *
     * @param pointIndex given index
     * @return PolyLineLocator for the Point at the index
     */
    public static Locator locator(int pointIndex) {
        return new PolyLineLocator(pointIndex);
    }

}