# classes/arc.js

    /**
     * Created by Alex Bol on 3/10/2017.
     */

    "use strict";
    import Flatten from '../flatten';
    import * as Intersection from '../algorithms/intersection';

    /**
     * Class representing a circular arc
     * @type {Arc}
     */
    export class Arc {
        /**
         *
         * @param {Point} pc - arc center
         * @param {number} r - arc radius
         * @param {number} startAngle - start angle in radians from 0 to 2*PI
         * @param {number} endAngle - end angle in radians from 0 to 2*PI
         * @param {boolean} counterClockwise - arc direction, true - clockwise, false - counter clockwise
         */
        constructor(...args) {
            /**
             * Arc center
             * @type {Point}
             */
            this.pc = new Flatten.Point();
            /**
             * Arc radius
             * @type {number}
             */
            this.r = 1;
            /**
             * Arc start angle in radians
             * @type {number}
             */
            this.startAngle = 0;
            /**
             * Arc end angle in radians
             * @type {number}
             */
            this.endAngle = 2 * Math.PI;
            /**
             * Arc orientation
             * @type {boolean}
             */
            this.counterClockwise = Flatten.CCW;

            if (args.length == 0)
                return;

            if (args.length == 1 && args[0] instanceof Object && args[0].name === "arc") {
                let {pc, r, startAngle, endAngle, counterClockwise} = args[0];
                this.pc = new Flatten.Point(pc.x, pc.y);
                this.r = r;
                this.startAngle = startAngle;
                this.endAngle = endAngle;
                this.counterClockwise = counterClockwise;
                return;
            } else {
                let [pc, r, startAngle, endAngle, counterClockwise] = [...args];
                if (pc && pc instanceof Flatten.Point) this.pc = pc.clone();
                if (r !== undefined) this.r = r;
                if (startAngle !== undefined) this.startAngle = startAngle;
                if (endAngle !== undefined) this.endAngle = endAngle;
                if (counterClockwise !== undefined) this.counterClockwise = counterClockwise;
                return;
            }

            throw Flatten.Errors.ILLEGAL_PARAMETERS;
        }

        /**
         * Return new cloned instance of arc
         * @returns {Arc}
         */
        clone() {
            return new Flatten.Arc(this.pc.clone(), this.r, this.startAngle, this.endAngle, this.counterClockwise);
        }

        /**
         * Get sweep angle in radians. Sweep angle is non-negative number from 0 to 2*PI
         * @returns {number}
         */
        get sweep() {
            if (Flatten.Utils.EQ(this.startAngle, this.endAngle))
                return 0.0;
            if (Flatten.Utils.EQ(Math.abs(this.startAngle - this.endAngle), Flatten.PIx2)) {
                return Flatten.PIx2;
            }
            let sweep;
            if (this.counterClockwise) {
                sweep = Flatten.Utils.GT(this.endAngle, this.startAngle) ?
                    this.endAngle - this.startAngle : this.endAngle - this.startAngle + Flatten.PIx2;
            } else {
                sweep = Flatten.Utils.GT(this.startAngle, this.endAngle) ?
                    this.startAngle - this.endAngle : this.startAngle - this.endAngle + Flatten.PIx2;
            }

            if (Flatten.Utils.GT(sweep, Flatten.PIx2)) {
                sweep -= Flatten.PIx2;
            }
            if (Flatten.Utils.LT(sweep, 0)) {
                sweep += Flatten.PIx2;
            }
            return sweep;
        }

        /**
         * Get start point of arc
         * @returns {Point}
         */
        get start() {
            let p0 = new Flatten.Point(this.pc.x + this.r, this.pc.y);
            return p0.rotate(this.startAngle, this.pc);
        }

        /**
         * Get end point of arc
         * @returns {Point}
         */
        get end() {
            let p0 = new Flatten.Point(this.pc.x + this.r, this.pc.y);
            return p0.rotate(this.endAngle, this.pc);
        }

        /**
         * Get center of arc
         * @returns {Point}
         */
        get center() {
            return this.pc.clone();
        }

        get vertices() {
            return [this.start.clone(), this.end.clone()];
        }

        /**
         * Get arc length
         * @returns {number}
         */
        get length() {
            return Math.abs(this.sweep * this.r);
        }

        /**
         * Get bounding box of the arc
         * @returns {Box}
         */
        get box() {
            let func_arcs = this.breakToFunctional();
            let box = func_arcs.reduce((acc, arc) => acc.merge(arc.start.box), new Flatten.Box());
            box = box.merge(this.end.box);
            return box;
        }

        /**
         * Returns true if arc contains point, false otherwise
         * @param {Point} pt - point to test
         * @returns {boolean}
         */
        contains(pt) {
            // first check if  point on circle (pc,r)
            if (!Flatten.Utils.EQ(this.pc.distanceTo(pt)[0], this.r))
                return false;

            // point on circle

            if (pt.equalTo(this.start))
                return true;

            let angle = new Flatten.Vector(this.pc, pt).slope;
            let test_arc = new Flatten.Arc(this.pc, this.r, this.startAngle, angle, this.counterClockwise);
            return Flatten.Utils.LE(test_arc.length, this.length);
        }

        /**
         * When given point belongs to arc, return array of two arcs split by this point. If points is incident
         * to start or end point of the arc, return clone of the arc. If point does not belong to the arcs, return
         * empty array.
         * @param {Point} pt Query point
         * @returns {Arc[]}
         */
        split(pt) {
            if (this.start.equalTo(pt))
                return [null, this.clone()];

            if (this.end.equalTo(pt))
                return [this.clone(), null];

            let angle = new Flatten.Vector(this.pc, pt).slope;

            return [
                new Flatten.Arc(this.pc, this.r, this.startAngle, angle, this.counterClockwise),
                new Flatten.Arc(this.pc, this.r, angle, this.endAngle, this.counterClockwise)
            ]
        }

        /**
         * Return middle point of the arc
         * @returns {Point}
         */
        middle() {
            let endAngle = this.counterClockwise ? this.startAngle + this.sweep / 2 : this.startAngle - this.sweep / 2;
            let arc = new Flatten.Arc(this.pc, this.r, this.startAngle, endAngle, this.counterClockwise);
            return arc.end;
        }

        /**
         * Get point at given length
         * @param {number} length - The length along the arc
         * @returns {Point}
         */
        pointAtLength(length) {
            if (length > this.length || length < 0) return null;
            if (length == 0) return this.start;
            if (length == this.length) return this.end;
            let factor = length / this.length;
            let endAngle = this.counterClockwise ? this.startAngle + this.sweep * factor : this.startAngle - this.sweep * factor;
            let arc = new Flatten.Arc(this.pc, this.r, this.startAngle, endAngle, this.counterClockwise);
            return arc.end;
        }

        /**
         * Returns chord height ("sagitta") of the arc
         * @returns {number}
         */
        chordHeight() {
            return (1.0 - Math.cos(Math.abs(this.sweep / 2.0))) * this.r;
        }

        /**
         * Returns array of intersection points between arc and other shape
         * @param {Shape} shape Shape of the one of supported types <br/>
         * @returns {Points[]}
         */
        intersect(shape) {
            if (shape instanceof Flatten.Point) {
                return this.contains(shape) ? [shape] : [];
            }
            if (shape instanceof Flatten.Line) {
                return Intersection.intersectLine2Arc(shape, this);
            }
            if (shape instanceof Flatten.Circle) {
                return Intersection.intersectArc2Circle(this, shape);
            }
            if (shape instanceof Flatten.Segment) {
                return Intersection.intersectSegment2Arc(shape, this);
            }
            if (shape instanceof Flatten.Box) {
                return Intersection.intersectArc2Box(this, shape);
            }
            if (shape instanceof Flatten.Arc) {
                return Intersection.intersectArc2Arc(this, shape);
            }
            if (shape instanceof Flatten.Polygon) {
                return Intersection.intersectArc2Polygon(this, shape);
            }
        }

        /**
         * Calculate distance and shortest segment from arc to shape and return array [distance, shortest segment]
         * @param {Shape} shape Shape of the one of supported types Point, Line, Circle, Segment, Arc, Polygon or Planar Set
         * @returns {number} distance from arc to shape
         * @returns {Segment} shortest segment between arc and shape (started at arc, ended at shape)

         */
        distanceTo(shape) {
            if (shape instanceof Flatten.Point) {
                let [dist, shortest_segment] = Flatten.Distance.point2arc(shape, this);
                shortest_segment = shortest_segment.reverse();
                return [dist, shortest_segment];
            }

            if (shape instanceof Flatten.Circle) {
                let [dist, shortest_segment] = Flatten.Distance.arc2circle(this, shape);
                return [dist, shortest_segment];
            }

            if (shape instanceof Flatten.Line) {
                let [dist, shortest_segment] = Flatten.Distance.arc2line(this, shape);
                return [dist, shortest_segment];
            }

            if (shape instanceof Flatten.Segment) {
                let [dist, shortest_segment] = Flatten.Distance.segment2arc(shape, this);
                shortest_segment = shortest_segment.reverse();
                return [dist, shortest_segment];
            }

            if (shape instanceof Flatten.Arc) {
                let [dist, shortest_segment] = Flatten.Distance.arc2arc(this, shape);
                return [dist, shortest_segment];
            }

            if (shape instanceof Flatten.Polygon) {
                let [dist, shortest_segment] = Flatten.Distance.shape2polygon(this, shape);
                return [dist, shortest_segment];
            }

            if (shape instanceof Flatten.PlanarSet) {
                let [dist, shortest_segment] = Flatten.Distance.shape2planarSet(this, shape);
                return [dist, shortest_segment];
            }
        }

        /**
         * Breaks arc in extreme point 0, pi/2, pi, 3*pi/2 and returns array of sub-arcs
         * @returns {Arcs[]}
         */
        breakToFunctional() {
            let func_arcs_array = [];
            let angles = [0, Math.PI / 2, 2 * Math.PI / 2, 3 * Math.PI / 2];
            let pts = [
                this.pc.translate(this.r, 0),
                this.pc.translate(0, this.r),
                this.pc.translate(-this.r, 0),
                this.pc.translate(0, -this.r)
            ];

            // If arc contains extreme point,
            // create test arc started at start point and ended at this extreme point
            let test_arcs = [];
            for (let i = 0; i < 4; i++) {
                if (pts[i].on(this)) {
                    test_arcs.push(new Flatten.Arc(this.pc, this.r, this.startAngle, angles[i], this.counterClockwise));
                }
            }

            if (test_arcs.length == 0) {                  // arc does contain any extreme point
                func_arcs_array.push(this.clone());
            } else {                                        // arc passes extreme point
                // sort these arcs by length
                test_arcs.sort((arc1, arc2) => arc1.length - arc2.length);

                for (let i = 0; i < test_arcs.length; i++) {
                    let prev_arc = func_arcs_array.length > 0 ? func_arcs_array[func_arcs_array.length - 1] : undefined;
                    let new_arc;
                    if (prev_arc) {
                        new_arc = new Flatten.Arc(this.pc, this.r, prev_arc.endAngle, test_arcs[i].endAngle, this.counterClockwise);
                    } else {
                        new_arc = new Flatten.Arc(this.pc, this.r, this.startAngle, test_arcs[i].endAngle, this.counterClockwise);
                    }
                    if (!Flatten.Utils.EQ_0(new_arc.length)) {
                        func_arcs_array.push(new_arc.clone());
                    }
                }

                // add last sub arc
                let prev_arc = func_arcs_array.length > 0 ? func_arcs_array[func_arcs_array.length - 1] : undefined;
                let new_arc;
                if (prev_arc) {
                    new_arc = new Flatten.Arc(this.pc, this.r, prev_arc.endAngle, this.endAngle, this.counterClockwise);
                } else {
                    new_arc = new Flatten.Arc(this.pc, this.r, this.startAngle, this.endAngle, this.counterClockwise);
                }
                // It could be 2*PI when occasionally start = 0 and end = 2*PI but this is not valid for breakToFunctional
                if (!Flatten.Utils.EQ_0(new_arc.length) && !Flatten.Utils.EQ(new_arc.sweep, 2*Math.PI)) {
                    func_arcs_array.push(new_arc.clone());
                }
            }
            return func_arcs_array;
        }

        /**
         * Return tangent unit vector in the start point in the direction from start to end
         * @returns {Vector}
         */
        tangentInStart() {
            let vec = new Flatten.Vector(this.pc, this.start);
            let angle = this.counterClockwise ? Math.PI / 2. : -Math.PI / 2.;
            let tangent = vec.rotate(angle).normalize();
            return tangent;
        }

        /**
         * Return tangent unit vector in the end point in the direction from end to start
         * @returns {Vector}
         */
        tangentInEnd() {
            let vec = new Flatten.Vector(this.pc, this.end);
            let angle = this.counterClockwise ? -Math.PI / 2. : Math.PI / 2.;
            let tangent = vec.rotate(angle).normalize();
            return tangent;
        }

        /**
         * Returns new arc with swapped start and end angles and reversed direction
         * @returns {Arc}
         */
        reverse() {
            return new Flatten.Arc(this.pc, this.r, this.endAngle, this.startAngle, !this.counterClockwise);
        }

        /**
         * Returns new arc translated by vector vec
         * @param {Vector} vec
         * @returns {Segment}
         */
        translate(...args) {
            let arc = this.clone();
            arc.pc = this.pc.translate(...args);
            return arc;
        }

        /**
         * Return new segment rotated by given angle around given point
         * If point omitted, rotate around origin (0,0)
         * Positive value of angle defines rotation counter clockwise, negative - clockwise
         * @param {number} angle - rotation angle in radians
         * @param {Point} center - center point, default is (0,0)
         * @returns {Arc}
         */
        rotate(angle = 0, center = new Flatten.Point()) {
            let m = new Flatten.Matrix();
            m = m.translate(center.x, center.y).rotate(angle).translate(-center.x, -center.y);
            return this.transform(m);
        }

        /**
         * Return new arc scaled by scaleX, scaleY.
         * @param {number} scaleX - scale value by X
         * @param {number} scaleY - scale value by Y
         * @returns {Arc}
         */
        scale(scaleX = 1, scaleY = 1) {
            let m = new Flatten.Matrix();
            m = m.scale(scaleX, scaleY);
            return this.transform(m);
        }

        /**
         * Return new arc transformed using affine transformation matrix <br/>
         * Note 1. Non-equal scaling by x and y (abs(matrix[0]) != abs(matrix[3])) produce illegal result because
         * it should create elliptic arc but this package does not support ellipses
         * Note 2. Mirror transformation (matrix[0] * matrix[3] < 0) change direction of the arc to the opposite
         * TODO: support non-equal scaling arc to ellipse or throw exception ?
         * @param {Matrix} matrix - affine transformation matrix
         * @returns {Arc}
         */
        transform(matrix = new Flatten.Matrix()) {
            let newStart = this.start.transform(matrix);
            let newEnd = this.end.transform(matrix);
            let newCenter = this.pc.transform(matrix);
            let newDirection = this.counterClockwise;
            if (matrix.a * matrix.d < 0) {
              newDirection = !newDirection;
            }
            let arc = Flatten.Arc.arcSE(newCenter, newStart, newEnd, newDirection);
            return arc;
        }

        static arcSE(center, start, end, counterClockwise) {
            let {vector} = Flatten;
            let startAngle = vector(center, start).slope;
            let endAngle = vector(center, end).slope;
            if (Flatten.Utils.EQ(startAngle, endAngle)) {
                endAngle += 2 * Math.PI;
                counterClockwise = true;
            }
            let r = vector(center, start).length;

            return new Flatten.Arc(center, r, startAngle, endAngle, counterClockwise);
        }

        definiteIntegral(ymin = 0) {
            let f_arcs = this.breakToFunctional();
            let area = f_arcs.reduce((acc, arc) => acc + arc.circularSegmentDefiniteIntegral(ymin), 0.0);
            return area;
        }

        circularSegmentDefiniteIntegral(ymin) {
            let line = new Flatten.Line(this.start, this.end);
            let onLeftSide = this.pc.leftTo(line);
            let segment = new Flatten.Segment(this.start, this.end);
            let areaTrapez = segment.definiteIntegral(ymin);
            let areaCircularSegment = this.circularSegmentArea();
            let area = onLeftSide ? areaTrapez - areaCircularSegment : areaTrapez + areaCircularSegment;
            return area;
        }

        circularSegmentArea() {
            return (0.5 * this.r * this.r * (this.sweep - Math.sin(this.sweep)))
        }

        /**
         * Sort given array of points from arc start to end, assuming all points lay on the arc
         * @param {Point[]} array of points
         * @returns {Point[]} new array sorted
         */
        sortPoints(pts) {
            let {vector} = Flatten;
            return pts.slice().sort( (pt1, pt2) => {
                let slope1 = vector(this.pc, pt1).slope;
                let slope2 = vector(this.pc, pt2).slope;
                if (slope1 < slope2) {
                    return -1;
                }
                if (slope1 > slope2) {
                    return 1;
                }
                return 0;
            })
        }

        /**
         * This method returns an object that defines how data will be
         * serialized when called JSON.stringify() method
         * @returns {Object}
         */
        toJSON() {
            return Object.assign({}, this, {name: "arc"});
        }

        /**
         * Return string to draw arc in svg
         * @param {Object} attrs - an object with attributes of svg path element,
         * like "stroke", "strokeWidth", "fill" <br/>
         * Defaults are stroke:"black", strokeWidth:"1", fill:"none"
         * @returns {string}
         */
        svg(attrs = {}) {
            let largeArcFlag = this.sweep <= Math.PI ? "0" : "1";
            let sweepFlag = this.counterClockwise ? "1" : "0";
            let {stroke, strokeWidth, fill, id, className} = attrs;
            // let rest_str = Object.keys(rest).reduce( (acc, key) => acc += ` ${key}="${rest[key]}"`, "");
            let id_str = (id && id.length > 0) ? `id="${id}"` : "";
            let class_str = (className && className.length > 0) ? `class="${className}"` : "";

            if (Flatten.Utils.EQ(this.sweep, 2 * Math.PI)) {
                let circle = new Flatten.Circle(this.pc, this.r);
                return circle.svg(attrs);
            } else {
                return `\n<path d="M${this.start.x},${this.start.y}
                                 A${this.r},${this.r} 0 ${largeArcFlag},${sweepFlag} ${this.end.x},${this.end.y}"
                        stroke="${stroke || "black"}" stroke-width="${strokeWidth || 1}" fill="${fill || "none"}" ${id_str} ${class_str} />`
            }
        }

    };

    Flatten.Arc = Arc;
    /**
     * Function to create arc equivalent to "new" constructor
     * @param args
     */
    export const arc = (...args) => new Flatten.Arc(...args);
    Flatten.arc = arc;