/*
 * Fri Feb 28 07:47:05 1997  Doug Lea  (dl at gee)
 * Based on PolyLineFigure
 */

package CH.ifa.draw.contrib;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.geom.GeneralPath;
import java.io.IOException;
import java.io.Serial;
import java.util.Enumeration;
import java.util.Objects;
import java.util.Vector;

import CH.ifa.draw.DrawPlugin;
import CH.ifa.draw.figures.AttributeFigure;
import CH.ifa.draw.figures.InsertPointHandle;
import CH.ifa.draw.figures.PolyLineable;
import CH.ifa.draw.standard.AbstractLocator;
import CH.ifa.draw.util.Geom;
import de.renew.draw.storables.ontology.Connector;
import de.renew.draw.storables.ontology.Figure;
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;

/**
 * A scalable, rotatable polygon with an arbitrary number of points
 */
public class PolygonFigure extends AttributeFigure implements OutlineFigure, PolyLineable {
    /**
     * Logger for the {@code PolygonFigure} class.
     */
    public static org.apache.log4j.Logger _logger =
        org.apache.log4j.Logger.getLogger(PolygonFigure.class);

    /**
     * Distance threshold for smoothing away or locating points
     **/
    static final int TOO_CLOSE = 2;
    /*
     * Serialization support.
     */
    @Serial
    private static final long serialVersionUID = 6254089689239215026L;
    private static final String SMOOTHINGSTRATEGY = "ch.ifa.draw.polygon.smoothing";
    private static final String SMOOTHING_INLINE = "alignment";
    private static final String SMOOTHING_DISTANCES = "distances";
    /**
     * The version of the serialized data for this figure. Used to maintain
     * backward compatibility during the deserialization process.
     */
    private final int polygonFigureSerializedDataVersion = 1;
    /**
     * Constant to define a polygon shape where vertices are connected by straight lines.
     */
    public final static int LINE_SHAPE = 0;
    /**
     * Constant to define a polygon shape where the vertices are used as
     * control points for a smoothed B-spline curve.
     */
    public final static int BSPLINE_SHAPE = 1;

    /**
     * The polygon to be displayed by this figure.
     * @serial
     **/
    protected Polygon _poly = new Polygon();

    /**
     * Constructs an empty PolygonFigure.
     */
    public PolygonFigure() {
        super();
    }

    /**
     * Constructs a PolygonFigure with an initial point.
     * @param x the x-coordinate of the first point
     * @param y the y-coordinate of the first point
     */
    public PolygonFigure(int x, int y) {
        _poly.addPoint(x, y);
    }

    /**
     * Constructs a PolygonFigure from a java.awt.Polygon.
     * @param p the AWT polygon to create this figure from
     */
    public PolygonFigure(Polygon p) {
        _poly = new Polygon(p.xpoints, p.ypoints, p.npoints);
    }

    @Override
    public Rectangle displayBox() {
        return bounds(_poly);
    }

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

    @Override
    public Vector<FigureHandle> handles() {
        Vector<FigureHandle> handles = new Vector<>(_poly.npoints);
        for (int i = 0; i < _poly.npoints; i++) {
            handles.addElement(new PolygonHandle(this, locator(i), i));
        }
        for (int i = 0; i < _poly.npoints - 1; i++) {
            handles.addElement(new InsertPointHandle(this, i));
        }
        handles.addElement(new PolygonScaleHandle(this));
        //handles.addElement(new PolygonPointAddHandle(this));
        return handles;
    }

    @Override
    public void basicDisplayBox(Point origin, Point corner) {
        Rectangle r = displayBox();
        int dx = origin.x - r.x;
        int dy = origin.y - r.y;
        _poly.translate(dx, dy);
        r = displayBox();
        Point oldCorner = new Point(r.x + r.width, r.y + r.height);
        Polygon p = getPolygon();
        scaleRotate(oldCorner, p, corner, true, true);
    }

    /**
     * return a copy of the raw polygon
     * @return a copy of the raw polygon.
     **/
    public Polygon getPolygon() {
        return new Polygon(_poly.xpoints, _poly.ypoints, _poly.npoints);
    }

    @Override
    public Polygon outline() {
        return getPolygon();
    }

    @Override
    public Point center() {
        return center(_poly);
    }

    /**
     * Gets all the points of the polygon.
     *
     * @return all points of this polygon figure.
     */
    public Enumeration<Point> points() {
        Vector<Point> pts = new Vector<>(_poly.npoints);
        for (int i = 0; i < _poly.npoints; ++i) {
            pts.addElement(new Point(_poly.xpoints[i], _poly.ypoints[i]));
        }
        return pts.elements();
    }

    @Override
    public int pointCount() {
        return _poly.npoints;
    }

    @Override
    public void basicMoveBy(int dx, int dy) {
        _poly.translate(dx, dy);
    }

    @Override
    public void drawBackground(Graphics g) {
        if (!(g instanceof Graphics2D g2d)) {
            throw new IllegalStateException("Graphics type not a Graphics2D");
        }
        g2d.fill(createPath());
    }

    @Override
    public void drawFrame(Graphics g) {
        if (!(g instanceof Graphics2D g2d)) {
            throw new IllegalStateException("Graphics type not a Graphics2D");
        }
        g2d.draw(createPath());
    }

    private GeneralPath createPath() {
        GeneralPath shape = new GeneralPath();
        int i = 0;
        int[] x = _poly.xpoints;
        int[] y = _poly.ypoints;
        int max = _poly.npoints;
        shape.moveTo(x[i], y[i]);
        while (i < max) {
            shape.lineTo(x[i], y[i]);
            i++;
        }
        shape.closePath();
        return shape;
    }

    @Override
    public boolean containsPoint(int x, int y) {
        _logger.debug("Contains Point x=" + x + ", y= " + y + " ? ");
        return _poly.contains(x, y);
    }

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

    /**
     * Adds a node to the list of points.
     * @param x the x coordinate of the point to add.
     * @param y the y coordinate of the point to add.
     */
    public void addPoint(int x, int y) {
        _poly.addPoint(x, y);
        changed();
    }

    /**
     * Changes the position of a node.
     */
    @Override
    public void setPointAt(Point p, int i) {
        willChange();
        _poly.xpoints[i] = p.x;
        _poly.ypoints[i] = p.y;
        //Define a new Polygon as moving points is not allowed see Java Bug 4269933
        _poly = new Polygon(_poly.xpoints, _poly.ypoints, _poly.npoints);
        changed();
    }

    /**
     * Insert a node at the given point.
     */
    @Override
    public void insertPointAt(Point p, int i) {
        willChange();
        int n = _poly.npoints + 1;
        int[] xs = new int[n];
        int[] ys = new int[n];
        for (int j = 0; j < i; ++j) {
            xs[j] = _poly.xpoints[j];
            ys[j] = _poly.ypoints[j];
        }
        xs[i] = p.x;
        ys[i] = p.y;
        for (int j = i; j < _poly.npoints; ++j) {
            xs[j + 1] = _poly.xpoints[j];
            ys[j + 1] = _poly.ypoints[j];
        }

        _poly = new Polygon(xs, ys, n);
        changed();
    }

    @Override
    public void removePointAt(int i) {
        willChange();
        int n = _poly.npoints - 1;
        int[] xs = new int[n];
        int[] ys = new int[n];
        for (int j = 0; j < i; ++j) {
            xs[j] = _poly.xpoints[j];
            ys[j] = _poly.ypoints[j];
        }
        for (int j = i; j < n; ++j) {
            xs[j] = _poly.xpoints[j + 1];
            ys[j] = _poly.ypoints[j + 1];
        }
        _poly = new Polygon(xs, ys, n);
        changed();
    }

    /**
     * Scale and rotate relative to anchor
     *
     * @param anchor the anchor for the transformation.
     * @param originalPolygon the original polygon.
     *
     * @param p the difference between p and the anchor relative to the
     *         center of the polygon define the scale factor and the angle
     *         of the transformations.
     *
     * @param scale if the polygon should be scaled.
     * @param rotate if the polygon should be rotated.
     **/
    public void scaleRotate(
        Point anchor, Polygon originalPolygon, Point p, boolean scale, boolean rotate)
    {
        willChange();

        // use center to determine relative angles and lengths
        Point ctr = center(originalPolygon);
        double anchorLen = Geom.length(ctr.x, ctr.y, anchor.x, anchor.y);

        if (anchorLen > 0.0) {
            double newLen = Geom.length(ctr.x, ctr.y, p.x, p.y);
            double ratio = newLen / anchorLen;

            double anchorAngle = Math.atan2(anchor.y - ctr.y, anchor.x - ctr.x);
            double newAngle = Math.atan2(p.y - ctr.y, p.x - ctr.x);
            double rotation = newAngle - anchorAngle;

            if (!scale) {
                ratio = 1;
            }
            if (!rotate) {
                rotation = 0;
            }
            int n = originalPolygon.npoints;
            int[] xs = new int[n];
            int[] ys = new int[n];

            for (int i = 0; i < n; ++i) {
                int x = originalPolygon.xpoints[i];
                int y = originalPolygon.ypoints[i];
                double l = Geom.length(ctr.x, ctr.y, x, y) * ratio;
                double a = Math.atan2(y - ctr.y, x - ctr.x) + rotation;
                xs[i] = (int) (ctr.x + l * Math.cos(a) + 0.5);
                ys[i] = (int) (ctr.y + l * Math.sin(a) + 0.5);
            }
            _poly = new Polygon(xs, ys, n);
        }
        changed();
    }

    /**
     * Remove points that are nearly collinear with others
     **/
    public void smoothPoints() {
        willChange();
        boolean removed;
        int n = _poly.npoints;
        do {
            removed = false;
            int i = 0;
            while (i < n && n >= 3) {
                int nxt = (i + 1) % n;
                int prv = (i - 1 + n) % n;
                String strategy = Objects.requireNonNull(DrawPlugin.getCurrent()).getProperties()
                    .getProperty(SMOOTHINGSTRATEGY);
                boolean doremove = false;
                if (strategy == null || strategy.isEmpty() || SMOOTHING_INLINE.equals(strategy)) {
                    if ((distanceFromLine(
                        _poly.xpoints[prv], _poly.ypoints[prv], _poly.xpoints[nxt],
                        _poly.ypoints[nxt], _poly.xpoints[i], _poly.ypoints[i]) < TOO_CLOSE)) {
                        doremove = true;
                    }
                } else if (SMOOTHING_DISTANCES.equals(strategy)) {
                    if (Math.abs(_poly.xpoints[prv] - _poly.xpoints[i]) < 5
                        && Math.abs(_poly.ypoints[prv] - _poly.ypoints[i]) < 5) {
                        doremove = true;
                    }
                }
                if (doremove) {
                    removed = true;
                    --n;
                    for (int j = i; j < n; ++j) {
                        _poly.xpoints[j] = _poly.xpoints[j + 1];
                        _poly.ypoints[j] = _poly.ypoints[j + 1];
                    }
                } else {
                    ++i;
                }
            }
        } while (removed);
        if (n != _poly.npoints) {
            _poly = new Polygon(_poly.xpoints, _poly.ypoints, n);
        }
        changed();
    }

    /**
     * Splits the segment at the given point if a segment was hit.
     *
     * @param x the x coordinate of the given point.
     * @param y the y coordinate of the given point.
     * @return the index of the segment or -1 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);
            return i + 1;
        } else {
            return -1;
        }
    }

    @Override
    public Point pointAt(int i) {
        return new Point(_poly.xpoints[i], _poly.ypoints[i]);
    }

    /**
     * Return the point on the polygon that is farthest from the center
     *
     * @return the point on the polygon that is farthest from the center.
     **/
    public Point outermostPoint() {
        Point ctr = center();
        int outer = 0;
        long dist = 0;

        for (int i = 0; i < _poly.npoints; ++i) {
            long d = Geom.length2(ctr.x, ctr.y, _poly.xpoints[i], _poly.ypoints[i]);
            if (d > dist) {
                dist = d;
                outer = i;
            }
        }

        return new Point(_poly.xpoints[outer], _poly.ypoints[outer]);
    }

    /**
     * Gets the segment that is hit by the given point.
     * @param x the x coordinate of the given point.
     * @param y the y coordinate of the given point.
     * @return the index of the segment or -1 if no segment was hit.
     */
    public int findSegment(int x, int y) {
        double dist = TOO_CLOSE;
        int best = -1;

        for (int i = 0; i < _poly.npoints; i++) {
            int n = (i + 1) % _poly.npoints;
            double d = distanceFromLine(
                _poly.xpoints[i], _poly.ypoints[i], _poly.xpoints[n], _poly.ypoints[n], x, y);
            if (d < dist) {
                dist = d;
                best = i;
            }
        }
        return best;
    }

    @Override
    public void write(StorableOutput dw) {
        super.write(dw);
        dw.writeInt(_poly.npoints);
        for (int i = 0; i < _poly.npoints; ++i) {
            dw.writeInt(_poly.xpoints[i]);
            dw.writeInt(_poly.ypoints[i]);
        }
    }

    @Override
    public void read(StorableInput dr) throws IOException {
        super.read(dr);
        int size = dr.readInt();
        int[] xs = new int[size];
        int[] ys = new int[size];
        for (int i = 0; i < size; i++) {
            xs[i] = dr.readInt();
            ys[i] = dr.readInt();
        }
        _poly = new Polygon(xs, ys, size);
    }

    /**
     * Creates a locator for the point with the given index.
     *
     * @param pointIndex the index of the point.
     *
     * @return the locator for the point with the given index or (-1,-1)
     *          if the index is invalid.
     */
    public static Locator locator(final int pointIndex) {
        return new AbstractLocator() {
            @Override
            public Point locate(Figure owner) {
                PolygonFigure plf = (PolygonFigure) owner;

                // guard against changing PolygonFigures -> temporary hack
                if (pointIndex < plf.pointCount()) {
                    return ((PolygonFigure) owner).pointAt(pointIndex);
                }
                return new Point(-1, -1);
            }
        };
    }

    /**
     * Compute the distance of point c from line segment form point a to point b.
     * if perpendicular projection is outside segment it, returns Double.MAX_VALUE.
     * If a and b are the same, returns the distance between that point and c.
     *
     * @param xa the x coordinate of the first point on the line.
     * @param ya the y coordinate of the first point on the line.
     * @param xb the x coordinate of the second point on the line.
     * @param yb the y coordinate of the second point on the line.
     * @param xc the x coordinate of the point c,
     *         whose distance should be computed.
     * @param yc the y coordinate of the point c,
     *         whose distance should be computed.
     *
     * @return the distance of point c from line segment form point a to point b.
     *          if perpendicular projection is outside segment it, returns Double.MAX_VALUE.
     *          If a and b are the same, returns the distance between that point and c.
     **/
    public static double distanceFromLine(int xa, int ya, int xb, int yb, int xc, int yc) {
        // source:http://vision.dai.ed.ac.uk/andrewfg/c-g-a-faq.html#q7
        //Let the point be C (XC,YC) and the line be AB (XA,YA) to (XB,YB).
        //The length of the
        //      line segment AB is L:
        //
        //                    ___________________
        //                   |        2         2
        //              L = \| (XB-XA) + (YB-YA)
        //and
        //
        //                  (YA-YC)(YA-YB)-(XA-XC)(XB-XA)
        //              r = -----------------------------
        //                              L**2
        //
        //                  (YA-YC)(XB-XA)-(XA-XC)(YB-YA)
        //              s = -----------------------------
        //                              L**2
        //
        //      Let I be the point of perpendicular projection of C onto AB, the
        //
        //              XI=XA+r(XB-XA)
        //              YI=YA+r(YB-YA)
        //
        //      Distance from A to I = r*L
        //      Distance from C to I = s*L
        //
        //      If r < 0 I is on backward extension of AB
        //      If r>1 I is on ahead extension of AB
        //      If 0<=r<=1 I is on AB
        //
        //      If s < 0 C is left of AB (you can just check the numerator)
        //      If s>0 C is right of AB
        //      If s=0 C is on AB
        int xdiff = xb - xa;
        int ydiff = yb - ya;
        long l2 = (long) xdiff * xdiff + (long) ydiff * ydiff;

        if (l2 == 0) {
            return Geom.length(xa, ya, xc, yc);
        }

        double rnum = (ya - yc) * (ya - yb) - (xa - xc) * (xb - xa);
        double r = rnum / l2;

        if (r < 0.0 || r > 1.0) {
            return Double.MAX_VALUE;
        }

        double xi = xa + r * xdiff;
        double yi = ya + r * ydiff;
        double xd = xc - xi;
        double yd = yc - yi;
        return Math.sqrt(xd * xd + yd * yd);

        /*
          for directional version, instead use
          double snum =  (ya-yc) * (xb-xa) - (xa-xc) * (yb-ya);
          double s = snum / l2;
        
          double l = Math.sqrt((double)l2);
          return = s * l;
          */
    }

    /**
     * Replacement for built-in Polygon.getBounds that doesn't always update?
     *
     * @param p the polygon, which bounds are returned.
     * @return the bounds of the given polygon.
     */
    public static Rectangle bounds(Polygon p) {
        int minx = Integer.MAX_VALUE;
        int miny = Integer.MAX_VALUE;
        int maxx = Integer.MIN_VALUE;
        int maxy = Integer.MIN_VALUE;
        int n = p.npoints;
        for (int i = 0; i < n; i++) {
            int x = p.xpoints[i];
            int y = p.ypoints[i];
            if (x > maxx) {
                maxx = x;
            }
            if (x < minx) {
                minx = x;
            }
            if (y > maxy) {
                maxy = y;
            }
            if (y < miny) {
                miny = y;
            }
        }

        return new Rectangle(minx, miny, maxx - minx, maxy - miny);
    }

    /**
     * Computes the center of the given polygon.
     *
     * @param p the given polygon.
     * @return the center of the given polygon.
     */
    public static Point center(Polygon p) {
        long sx = 0;
        long sy = 0;
        int n = p.npoints;
        for (int i = 0; i < n; i++) {
            sx += p.xpoints[i];
            sy += p.ypoints[i];
        }

        return new Point((int) (sx / n), (int) (sy / n));
    }

    /**
     * Calculates the point on the polygon's border that intersects with a line
     * drawn from the center of the polygon to a given external point.
     * If no such intersection can be found (e.g., if the point is inside
     * the polygon), it returns the polygon vertex closest to the given point.
     *
     * @param poly the polygon to find the connection point on
     * @param p    the external point to connect from
     * @return the closest point on the polygon's outline for a connection.
     */
    public static Point chop(Polygon poly, Point p) {
        Point ctr = center(poly);
        int cx = -1;
        int cy = -1;
        long len = Long.MAX_VALUE;

        // Try for points along edge
        for (int i = 0; i < poly.npoints; ++i) {
            int nxt = (i + 1) % poly.npoints;
            Point chop = Geom.intersect(
                poly.xpoints[i], poly.ypoints[i], poly.xpoints[nxt], poly.ypoints[nxt], p.x, p.y,
                ctr.x, ctr.y);
            if (chop != null) {
                long cl = Geom.length2(chop.x, chop.y, p.x, p.y);
                if (cl < len) {
                    len = cl;
                    cx = chop.x;
                    cy = chop.y;
                }
            }
        }

        // if none found, pick the closest vertex
        if (len == Long.MAX_VALUE) {
            for (int i = 0; i < poly.npoints; ++i) {
                long l = Geom.length2(poly.xpoints[i], poly.ypoints[i], p.x, p.y);
                if (l < len) {
                    len = l;
                    cx = poly.xpoints[i];
                    cy = poly.ypoints[i];
                }
            }
        }
        return new Point(cx, cy);
    }
}