# classes/arc.js

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

    "use strict";

    module.exports = function(Flatten) {
        /**
         * Class representing a circular arc
         * @type {Arc}
         */
        Flatten.Arc = 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 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.contains(pt))
                    return [];

                if (Flatten.Utils.EQ_0(this.sweep))
                    return [this.clone()];

                if (this.start.equalTo(pt) || this.end.equalTo(pt))
                    return [this.clone()];

                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;
            }

            /**
             * 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 shape.intersect(this);
                }
                if (shape instanceof Flatten.Circle) {
                    return Arc.intersectArc2Circle(this, shape);
                }
                if (shape instanceof Flatten.Segment) {
                    return shape.intersect(this);
                }
                if (shape instanceof Flatten.Arc) {
                    return Arc.intersectArc2Arc(this, shape);
                }
                if (shape instanceof Flatten.Polygon) {
                    return Flatten.Polygon.intersectShape2Polygon(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) {
                let {Distance} = Flatten;

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

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

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

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

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

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

                if (shape instanceof Flatten.PlanarSet) {
                    let [dist, shortest_segment] = 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);
                    }
                    if (!Flatten.Utils.EQ_0(new_arc.length)) {
                        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 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 transformed using affine transformation matrix <br/>
             * Note, that non-equal scaling by x and y (matrix[0] != matrix[3]) produce illegal result
             * 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 arc = Arc.arcSE(newCenter, newStart, newEnd, this.counterClockwise);
                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 Arc(center, r, startAngle, endAngle, counterClockwise);
            }

            static intersectArc2Arc(arc1, arc2) {
                var ip = [];

                if (arc1.box.not_intersect(arc2.box)) {
                    return ip;
                }

                // Special case: overlapping arcs
                // May return up to 4 intersection points
                if (arc1.pc.equalTo(arc2.pc) && Flatten.Utils.EQ(arc1.r, arc2.r)) {
                    let pt;

                    pt = arc1.start;
                    if (pt.on(arc2))
                        ip.push(pt);

                    pt = arc1.end;
                    if (pt.on(arc2))
                        ip.push(pt);

                    pt = arc2.start;
                    if (pt.on(arc1)) ip.push(pt);

                    pt = arc2.end;
                    if (pt.on(arc1)) ip.push(pt);

                    return ip;
                }

                // Common case
                let circle1 = new Flatten.Circle(arc1.pc, arc1.r);
                let circle2 = new Flatten.Circle(arc2.pc, arc2.r);
                let ip_tmp =  circle1.intersect(circle2);
                for (let pt of ip_tmp) {
                    if (pt.on(arc1) && pt.on(arc2)) {
                        ip.push(pt);
                    }
                }
                return ip;
            }

            static intersectArc2Circle(arc, circle) {
                let ip = [];

                if (arc.box.not_intersect(circle.box)) {
                    return ip;
                }

                // Case when arc center incident to circle center
                // Return arc's end points as 2 intersection points
                if (circle.pc.equalTo(arc.pc) && Flatten.Utils.EQ(circle.r, arc.r)) {
                    ip.push(arc.start);
                    ip.push(arc.end);
                    return ip;
                }

                // Common case
                let circle1 = circle;
                let circle2 = new Flatten.Circle(arc.pc, arc.r);
                let ip_tmp = circle1.intersect(circle2);
                for (let pt of ip_tmp) {
                    if (pt.on(arc)) {
                        ip.push(pt);
                    }
                }
                return ip;
            }

            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)))
            }

            /**
             * 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} />`
                }
            }

            /**
             * 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"});
            }
        };

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