// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.turnlanes.gui;

import static java.lang.Math.PI;
import static java.lang.Math.abs;
import static java.lang.Math.cos;
import static java.lang.Math.hypot;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.lang.Math.signum;
import static java.lang.Math.sin;
import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.angle;
import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.cpf;
import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.minAngleDiff;
import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.relativePoint;

import java.awt.geom.Line2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.util.NoSuchElementException;

/**
 * A path that allows constructing offset curves/parallel curves with a somewhat crude straight
 * skeleton implementation.
 *
 * @author Ben Schulz
 */
abstract class Path {
    private static final class SimplePathIterator implements PathIterator {
        private final SimplePathIterator previous;

        private final int type;
        private final double[] coords;

        private boolean done = false;

        SimplePathIterator(SimplePathIterator previous, int type, double... coords) {
            this.previous = previous;
            this.type = type;
            this.coords = coords;
        }

        SimplePathIterator(int type, double... coords) {
            this(null, type, coords);
        }

        @Override
        public int getWindingRule() {
            return WIND_NON_ZERO;
        }

        @Override
        public boolean isDone() {
            return done;
        }

        @Override
        public void next() {
            if (previous != null && !previous.isDone()) {
                previous.next();
            } else {
                done = true;
            }
        }

        @Override
        public int currentSegment(float[] coords) {
            if (previous != null && !previous.isDone()) {
                return previous.currentSegment(coords);
            } else if (done) {
                throw new NoSuchElementException("Iterator is already done.");
            }

            for (int i = 0; i < 6; ++i) {
                coords[i] = (float) this.coords[i];
            }

            return type;
        }

        @Override
        public int currentSegment(double[] coords) {
            if (previous != null && !previous.isDone()) {
                return previous.currentSegment(coords);
            } else if (done) {
                throw new NoSuchElementException("Iterator is already done.");
            }

            for (int i = 0; i < 6; ++i) {
                coords[i] = this.coords[i];
            }

            return type;
        }

    }

    private static final class Line extends Path {
        private final Path previous;

        private final double endX;
        private final double endY;

        private final double angle;

        private final double length;

        Line(Path previous, double x, double y, double length) {
            this.previous = previous;

            this.endX = x;
            this.endY = y;

            this.angle = angle(previous.getEnd(), getEnd());

            this.length = length;
        }

        @Override
        public Point2D getStart() {
            return previous.getStart();
        }

        @Override
        public Point2D getEnd() {
            return new Point2D.Double(endX, endY);
        }

        @Override
        public double getEndAngle() {
            return angle;
        }

        @Override
        public double getLength() {
            return previous.getLength() + length;
        }

        @Override
        public Path offset(double ws, double m1, double m2, double we) {
            return offsetInternal(ws, m1, m2, we, angle);
        }

        @Override
        Path offsetInternal(double ws, double m1, double m2, double we, double endAngle) {
            final double PL = previous.getLength();
            final double ML = PL + length;

            final Path prev = previous.offsetInternal(ws, m1, m2, we, angle);

            final double wStart = PL <= m1 ? ws : m2 <= PL ? we : ws + (PL - m1) * (we - ws) / (m2 - m1);
            final Point2D from = prev.getEnd();
            final Point2D to = offsetEnd(wStart, endAngle);

            if (abs(minAngleDiff(angle, angle(from, to))) > PI / 100) {
                return previous.offsetInternal(ws, m1, m2, we, endAngle);
            }

            if (ML <= m1) {
                return simpleOffset(prev, ws, endAngle);
            } else if (m2 <= PL) {
                return simpleOffset(prev, we, endAngle);
            }

            final double LL = from.distance(to);

            final Point2D m1o = PL <= m1 ? relativePoint(prev.getEnd(), LL * (m1 - PL) / length, angle) : null;
            final Point2D m2t = m2 <= ML ? relativePoint(getEnd(), LL * (ML - m2) / length, angle + PI) : null;
            final Point2D m2o = m2t == null ? null : relativePoint(m2t, we, (angle + endAngle - PI) / 2);

            if (m1o != null && m2o != null) {
                final Line l1 = new Line(prev, m1o.getX(), m1o.getY(), m1 - PL);
                final Line l2 = new Line(l1, m2o.getX(), m2o.getY(), m2 - m1);

                final Point2D end = offsetEnd(we, endAngle);

                return new Line(l2, end.getX(), end.getY(), ML - m2);
            } else if (m1o != null) {
                final Line l1 = new Line(prev, m1o.getX(), m1o.getY(), m1 - PL);

                final double w = ws + (ML - m1) * (we - ws) / (m2 - m1);
                final Point2D end = offsetEnd(w, endAngle);

                return new Line(l1, end.getX(), end.getY(), ML - m1);
            } else if (m2o != null) {
                final Line l2 = new Line(prev, m2o.getX(), m2o.getY(), m2 - PL);

                final Point2D end = offsetEnd(we, endAngle);

                return new Line(l2, end.getX(), end.getY(), ML - m2);
            } else {
                final double w = ws + (PL - m1 + length) * (we - ws) / (m2 - m1);
                final Point2D end = offsetEnd(w, endAngle);
                return new Line(prev, end.getX(), end.getY(), length);
            }
        }

        private Path simpleOffset(Path prev, double w, double endAngle) {
            final Point2D offset = offsetEnd(w, endAngle);
            return new Line(prev, offset.getX(), offset.getY(), length);
        }

        private Point2D offsetEnd(double w, double endAngle) {
            final double da2 = minAngleDiff(angle, endAngle) / 2;
            final double hypotenuse = w / cos(da2);

            return relativePoint(getEnd(), hypotenuse, angle + PI / 2 + da2);
        }

        @Override
        public SimplePathIterator getIterator() {
            return new SimplePathIterator(previous.getIteratorInternal(angle), PathIterator.SEG_LINETO, endX, endY, 0,
                    0, 0, 0);
        }

        @Override
        public Path subpath(double from, double to) {
            final double PL = previous.getLength();
            final double ML = PL + length;

            if (from > ML) {
                throw new IllegalArgumentException("from > length");
            } else if (to > ML) {
                throw new IllegalArgumentException("to > length");
            }

            if (to < PL) {
                return previous.subpath(from, to);
            }

            final Point2D end = to < ML ? getPoint(to) : new Point2D.Double(endX, endY);

            final double EL = min(ML, to);
            if (PL <= from) {
                final Point2D start = getPoint(from);
                return new Line(new Start(start.getX(), start.getY(), angle), end.getX(), end.getY(), EL - from);
            } else {
                return new Line(previous.subpath(from, PL), end.getX(), end.getY(), EL - PL);
            }
        }

        @Override
        public Point2D getPoint(double offset) {
            final double PL = previous.getLength();
            final double ML = PL + length;

            if (offset > ML) {
                throw new IllegalArgumentException("offset > length");
            }

            if (offset <= ML && offset >= PL) {
                final double LL = previous.getEnd().distance(getEnd());
                return relativePoint(getEnd(), LL * (ML - offset) / length, angle + PI);
            } else {
                return previous.getPoint(offset);
            }
        }

        @Override
        SimplePathIterator getIteratorInternal(double endAngle) {
            return getIterator();
        }
    }

    // TODO curves are still somewhat broken
    private static class Curve extends Path {
        private final Path previous;

        private final double height;

        private final double centerX;
        private final double centerY;
        private final double centerToFromAngle;

        private final double endX;
        private final double endY;

        private final double fromAngle;
        private final double fromRadius;
        private final double toRadius;
        private final double angle;

        private final double length;

        private Curve(Path previous, double r1, double r2, double a, double length, double fromAngle) {
            this.previous = previous;
            this.fromAngle = fromAngle;
            this.fromRadius = r1;
            this.toRadius = r2;
            this.angle = a;
            this.length = length;

            final Point2D from = previous.getEnd();
            this.centerToFromAngle = fromAngle - signum(a) * PI / 2;
            final Point2D center = relativePoint(from, r1, centerToFromAngle + PI);

            final double toAngle = centerToFromAngle + a;
            this.endX = center.getX() + r2 * cos(toAngle);
            this.endY = center.getY() - r2 * sin(toAngle);

            this.centerX = center.getX();
            this.centerY = center.getY();

            final double y = new Line2D.Double(center, from).ptLineDist(endX, endY);
            this.height = y / sin(angle);
        }

        Curve(Path previous, double r1, double r2, double a, double length) {
            this(previous, r1, r2, a, length, previous.getEndAngle());
        }

        @Override
        public Point2D getStart() {
            return previous.getStart();
        }

        @Override
        public Point2D getEnd() {
            return new Point2D.Double(endX, endY);
        }

        @Override
        public double getEndAngle() {
            return fromAngle + angle;
        }

        @Override
        public double getLength() {
            return previous.getLength() + length;
        }

        @Override
        public Path offset(double ws, double m1, double m2, double we) {
            return offsetInternal(ws, m1, m2, we, previous.getEndAngle() + angle);
        }

        @Override
        Path offsetInternal(double ws, double m1, double m2, double we, double endAngle) {
            final double PL = previous.getLength();
            final double ML = PL + length;

            final Path prev = previous.offsetInternal(ws, m1, m2, we, fromAngle);

            if (ML <= m1) {
                return simpleOffset(prev, ws);
            } else if (m2 <= PL) {
                return simpleOffset(prev, we);
            }

            final double s = signum(angle);

            if (PL < m1 && m2 < ML) {
                final double l1 = m1 - PL;
                final double a1 = angle(l1);
                final double r1 = radius(a1) - s * ws;

                final Curve c1 = new Curve(prev, fromRadius - ws, r1, a1, l1, fromAngle);

                final double l2 = m2 - m1;
                final double a2 = angle(l2);
                final double r2 = radius(a2) - s * we;

                final Curve c2 = new Curve(c1, r1, r2, a2 - a1, l2);

                return new Curve(c2, r2, toRadius - s * we, angle - a2, ML - m2);
            } else if (PL < m1) {
                final double l1 = m1 - PL;
                final double a1 = angle(l1);
                final double r1 = radius(a1) - s * ws;

                final Curve c1 = new Curve(prev, fromRadius - s * ws, r1, a1, l1, fromAngle);

                final double w = ws + (ML - m1) * (we - ws) / (m2 - m1);

                return new Curve(c1, r1, toRadius - s * w, angle - a1, ML - m1);
            } else if (m2 < ML) {
                final double w = ws + (PL - m1) * (we - ws) / (m2 - m1);

                final double l2 = m2 - PL;
                final double a2 = angle(l2);
                final double r2 = radius(a2) - s * we;

                final Curve c2 = new Curve(prev, fromRadius - s * w, r2, a2, l2, fromAngle);

                return new Curve(c2, r2, toRadius - s * we, angle - a2, ML - m2);
            } else {
                final double w1 = ws + (PL - m1) * (we - ws) / (m2 - m1);
                final double w2 = we - (m2 - ML) * (we - ws) / (m2 - m1);

                return new Curve(prev, fromRadius - s * w1, toRadius - s * w2, angle, length,
                        fromAngle);
            }
        }

        private double angle(double l) {
            return l * angle / length;
        }

        private double radius(double a) {
            return hypot(fromRadius * cos(a), height * sin(a));
        }

        private Path simpleOffset(Path prev, double w) {
            final double s = signum(angle);
            return new Curve(prev, fromRadius - s * w, toRadius - s * w, angle, length, fromAngle);
        }

        @Override
        public SimplePathIterator getIterator() {
            return getIteratorInternal(previous.getEndAngle() + angle);
        }

        @Override
        public Path subpath(double from, double to) {
            final double PL = previous.getLength();
            final double ML = PL + length;

            if (from > ML) {
                throw new IllegalArgumentException("from > length");
            } else if (to > ML) {
                throw new IllegalArgumentException("to > length");
            }

            if (to < PL) {
                return previous.subpath(from, to);
            }

            final double toA = to < ML ? angle(to - PL) : angle;
            final double toR = to < ML ? radius(toA) : toRadius;

            final double fromA = from > PL ? angle(from - PL) : 0;
            final double fromR = from > PL ? radius(fromA) : fromRadius;

            final double a = toA - fromA;
            final double l = min(ML, to) - max(PL, from);

            if (from >= PL) {
                final Point2D start = getPoint(from);
                final double fa = fromAngle + fromA;

                return new Curve(new Start(start.getX(), start.getY(), fa), fromR, toR, a, l, fa);
            } else {
                return new Curve(previous.subpath(from, PL), fromR, toR, a, l, fromAngle);
            }
        }

        @Override
        public Point2D getPoint(double offset) {
            final double PL = previous.getLength();
            final double ML = PL + length;

            if (offset <= ML && offset >= PL) {
                final double a = abs(angle(offset - PL));
                final double w = fromRadius * cos(a);
                final double h = -height * sin(a);

                final double r = centerToFromAngle; // rotation angle
                final double x = w * cos(r) + h * sin(r);
                final double y = -w * sin(r) + h * cos(r);

                return new Point2D.Double(centerX + x, centerY + y);
            } else {
                return previous.getPoint(offset);
            }
        }

        @Override
        SimplePathIterator getIteratorInternal(double endAngle) {
            final Point2D cp1 = relativePoint(previous.getEnd(), cpf(angle, fromRadius), previous.getEndAngle());
            final Point2D cp2 = relativePoint(getEnd(), cpf(angle, toRadius), endAngle + PI);

            return new SimplePathIterator(previous.getIteratorInternal(getEndAngle()), PathIterator.SEG_CUBICTO, //
                    cp1.getX(), cp1.getY(), cp2.getX(), cp2.getY(), endX, endY //
            );

        }
    }

    private static class Start extends Path {
        private final double x;
        private final double y;

        private final double endAngle;

        Start(double x, double y, double endAngle) {
            this.x = x;
            this.y = y;
            this.endAngle = endAngle;
        }

        Start(double x, double y) {
            this(x, y, Double.NaN);
        }

        @Override
        public Point2D getStart() {
            return new Point2D.Double(x, y);
        }

        @Override
        public Point2D getEnd() {
            return new Point2D.Double(x, y);
        }

        @Override
        public double getEndAngle() {
            if (Double.isNaN(endAngle)) {
                throw new UnsupportedOperationException();
            }

            return endAngle;
        }

        @Override
        public double getLength() {
            return 0;
        }

        @Override
        public Path offset(double ws, double m1, double m2, double we) {
            throw new UnsupportedOperationException();
        }

        @Override
        Path offsetInternal(double ws, double m1, double m2, double we, double endAngle) {
            final Point2D offset = relativePoint(getStart(), ws, endAngle + PI / 2);
            return new Start(offset.getX(), offset.getY(), endAngle);
        }

        @Override
        public SimplePathIterator getIterator() {
            return new SimplePathIterator(PathIterator.SEG_MOVETO, x, y, 0, 0, 0, 0);
        }

        @Override
        public Path subpath(double from, double to) {
            if (from > to) {
                throw new IllegalArgumentException("from > to");
            }
            if (from < 0) {
                throw new IllegalArgumentException("from < 0");
            }

            return this;
        }

        @Override
        public Point2D getPoint(double offset) {
            if (offset == 0) {
                return getEnd();
            } else {
                throw new IllegalArgumentException(Double.toString(offset));
            }
        }

        @Override
        SimplePathIterator getIteratorInternal(double endAngle) {
            return new SimplePathIterator(PathIterator.SEG_MOVETO, x, y, 0, 0, 0, 0);
        }
    }

    public static Path create(double x, double y) {
        return new Start(x, y);
    }

    public Path lineTo(double x, double y, double length) {
        return new Line(this, x, y, length);
    }

    public Path curveTo(double r1, double r2, double a, double length) {
        return new Curve(this, r1, r2, a, length);
    }

    public abstract Path offset(double ws, double m1, double m2, double we);

    abstract Path offsetInternal(double ws, double m1, double m2, double we, double endAngle);

    public abstract double getLength();

    public abstract double getEndAngle();

    public abstract Point2D getStart();

    public abstract Point2D getEnd();

    public abstract SimplePathIterator getIterator();

    abstract SimplePathIterator getIteratorInternal(double endAngle);

    public abstract Path subpath(double from, double to);

    public Path subpath(double from, double to, boolean fixArgs) {
        if (fixArgs) {
            from = min(max(from, 0), getLength());
            to = min(max(to, 0), getLength());
        }

        return subpath(from, to);
    }

    public abstract Point2D getPoint(double offset);

    public Point2D getPoint(double offset, boolean fixArgs) {
        if (fixArgs) {
            offset = min(max(offset, 0), getLength());
        }

        return getPoint(offset);
    }
}
