import { Curve } from './curve';
import { Rectangle } from './rectangle';
import { ParallelogramNode } from './parallelogramNode';
import { Point } from './point';
import { GeomConstants } from './geomConstants';
import { ClosestPointOnCurve } from './closestPointOnCurve';
import { closeDistEps } from '../../utils/compare';
export class Ellipse {
    // The point on the ellipse corresponding to the parameter t is calculated by
    // the formula center + cos(t)*axis0 + sin(t) * axis1.
    // To get an ellipse rotating clockwise use, for example,
    // axis0=(-1,0) and axis1=(0,1)
    constructor(parStart, parEnd, axis0, axis1, center) {
        //    assert(parStart <= parEnd);
        this.parStart = parStart;
        this.parEnd = parEnd;
        this.aAxis = axis0;
        this.bAxis = axis1;
        this.center = center;
        this.pNode = null;
        this.setBoundingBox();
    }
    // offsets the curve in the given direction
    offsetCurve(offset, dir) {
        //is dir inside or outside of the ellipse
        const d = dir.sub(this.center);
        const angle = Point.angle(this.aAxis, d);
        const s = this.aAxis.mul(Math.cos(angle)).add(this.bAxis.mul(Math.sin(angle)));
        if (s.length < d.length) {
            const al = this.aAxis.length;
            const bl = this.bAxis.length;
            return Ellipse.mkEllipsePPP(this.aAxis.normalize().mul(al + offset), this.bAxis.normalize().mul(bl + offset), this.center);
        }
        {
            const al = this.aAxis.length;
            const bl = this.bAxis.length;
            return Ellipse.mkEllipsePPP(this.aAxis.normalize().mul(al - offset), this.bAxis.normalize().mul(bl - offset), this.center);
        }
    }
    // Reverse the ellipe: not implemented.
    reverse() {
        return null; // throw new Exception("not implemented");
    }
    static mkEllipsePPP(a, b, center) {
        return new Ellipse(0, Math.PI * 2, a, b, center);
    }
    get start() {
        return this.value(this.parStart);
    }
    get end() {
        return this.value(this.parEnd);
    }
    // Trims the curve
    trim(start, end) {
        // Assert.assert(start <= end);
        // Assert.assert(start >= ParStart - GeomConstants.tolerance);
        // Assert.assert(end <= ParEnd + GeomConstants.tolerance);
        return new Ellipse(Math.max(start, this.parStart), Math.min(end, this.parEnd), this.aAxis, this.bAxis, this.center);
    }
    // Not Implemented: Returns the trimmed curve, wrapping around the end if start is greater than end.
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    trimWithWrap(start, end) {
        return null;
    }
    // The bounding box of the ellipse
    get boundingBox() {
        return this.box;
    }
    // Returns the point on the curve corresponding to parameter t
    value(t) {
        return this.center.add(Point.mkPoint(Math.cos(t), this.aAxis, Math.sin(t), this.bAxis));
    }
    // first derivative
    derivative(t) {
        return Point.mkPoint(-Math.sin(t), this.aAxis, Math.cos(t), this.bAxis);
    }
    // second derivative
    secondDerivative(t) {
        return Point.mkPoint(-Math.cos(t), this.aAxis, -Math.sin(t), this.bAxis);
    }
    // third derivative
    thirdDerivative(t) {
        return Point.mkPoint(Math.sin(t), this.aAxis, -Math.cos(t), this.bAxis);
    }
    // a tree of ParallelogramNodes covering the edge
    pNodeOverICurve() {
        if (this.pNode != null)
            return this.pNode;
        return (this.pNode = ParallelogramNode.createParallelogramNodeForCurveSegDefaultOffset(this));
    }
    setBoundingBox() {
        if (closeDistEps(this.parStart, 0) && closeDistEps(this.parEnd, Math.PI * 2))
            this.box = this.fullBox();
        else {
            //the idea is that the box of an arc staying in one quadrant is just the box of the start and the end point of the arc
            this.box = Rectangle.mkPP(this.start, this.end);
            //now Start and End are in the box, we need just add all k*P/2 that are in between
            let t;
            for (let i = Math.ceil(this.parStart / (Math.PI / 2)); (t = (i * Math.PI) / 2) < this.parEnd; i++)
                if (t > this.parStart)
                    this.box.add(this.value(t));
        }
    }
    static mkEllipse(parStart, parEnd, axis0, axis1, centerX, centerY) {
        return new Ellipse(parStart, parEnd, axis0, axis1, new Point(centerX, centerY));
    }
    // Construct a full ellipse by two axes
    static mkFullEllipsePPP(axis0, axis1, center) {
        return new Ellipse(0, Math.PI * 2, axis0, axis1, center);
    }
    // Constructs a full ellipse with axes aligned to X and Y directions
    static mkFullEllipseNNP(axisA, axisB, center) {
        return new Ellipse(0, Math.PI * 2, new Point(axisA, 0), new Point(0, axisB), center);
    }
    static mkCircle(radius, center) {
        return Ellipse.mkFullEllipseNNP(radius, radius, center);
    }
    // Moves the ellipse to the delta vector
    translate(delta) {
        this.center = this.center.add(delta);
        this.box.center = this.box.center.add(delta);
        this.pNode = null;
    }
    // Scales the ellipse by x and by y
    scaleFromOrigin(xScale, yScale) {
        return new Ellipse(this.parStart, this.parEnd, this.aAxis.mul(xScale), this.bAxis.mul(yScale), this.center.scale(xScale, yScale));
    }
    //
    getParameterAtLength(length) {
        //todo: slow version!
        const eps = 0.001;
        let l = this.parStart;
        let u = this.parEnd;
        const lenplus = length + eps;
        const lenminsu = length - eps;
        while (u - l > GeomConstants.distanceEpsilon) {
            const m = 0.5 * (u + l);
            const len = this.lengthPartial(this.parStart, m);
            if (len > lenplus)
                u = m;
            else if (len < lenminsu)
                l = m;
            else
                return m;
        }
        return (u + l) / 2;
    }
    // Transforms the ellipse
    transform(transformation) {
        if (transformation != null) {
            const ap = transformation.multiplyPoint(this.aAxis).sub(transformation.offset());
            const bp = transformation.multiplyPoint(this.bAxis).sub(transformation.offset());
            return new Ellipse(this.parStart, this.parEnd, ap, bp, transformation.multiplyPoint(this.center));
        }
        return this;
    }
    // returns a parameter t such that the distance between curve[t] and targetPoint is minimal
    // and t belongs to the closed segment [low,high]
    closestParameterWithinBounds(targetPoint, low, high) {
        const numberOfTestPoints = 8;
        const t = (high - low) / (numberOfTestPoints + 1);
        let closest = low;
        let minDist = Number.MAX_VALUE;
        for (let i = 0; i <= numberOfTestPoints; i++) {
            const par = low + i * t;
            const p = targetPoint.sub(this.value(par));
            const d = p.dot(p);
            if (d < minDist) {
                minDist = d;
                closest = par;
            }
        }
        if (closest == 0 && high == Math.PI * 2)
            low = -Math.PI;
        let ret = ClosestPointOnCurve.closestPoint(this, targetPoint, closest, low, high);
        if (ret < 0)
            ret += 2 * Math.PI;
        return ret;
    }
    // return length of the curve segment [start,end] : not implemented
    lengthPartial(start, end) {
        return Curve.lengthWithInterpolationAndThreshold(this.trim(start, end), GeomConstants.lineSegmentThreshold / 100);
    }
    get length() {
        return Curve.lengthWithInterpolation(this);
    }
    // clones the ellipse .
    clone() {
        return new Ellipse(this.parStart, this.parEnd, this.aAxis.clone(), this.bAxis.clone(), this.center.clone());
    }
    // returns a parameter t such that the distance between curve[t] and a is minimal
    closestParameter(targetPoint) {
        let savedParStart = 0;
        const numberOfTestPoints = 8;
        const t = (this.parEnd - this.parStart) / (numberOfTestPoints + 1);
        let closest = this.parStart;
        let minDist = Number.MAX_VALUE;
        for (let i = 0; i <= numberOfTestPoints; i++) {
            const par = this.parStart + i * t;
            const p = targetPoint.sub(this.value(par));
            const d = p.dot(p);
            if (d < minDist) {
                minDist = d;
                closest = par;
            }
        }
        let parStartWasChanged = false;
        if (closest == 0 && this.parEnd == Math.PI * 2) {
            parStartWasChanged = true;
            savedParStart = this.parStart;
            this.parStart = -Math.PI;
        }
        let ret = ClosestPointOnCurve.closestPoint(this, targetPoint, closest, this.parStart, this.parEnd);
        if (ret < 0)
            ret += 2 * Math.PI;
        if (parStartWasChanged)
            this.parStart = savedParStart;
        return ret;
    }
    // left derivative at t
    leftDerivative(t) {
        return this.derivative(t);
    }
    // right derivative at t
    rightDerivative(t) {
        return this.derivative(t);
    }
    //
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    curvature(t) {
        throw 'NotImplementedException()';
        return 0;
    }
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    curvatureDerivative(t) {
        throw 'NotImplementedException();';
        return 0;
    }
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    curvatureSecondDerivative(t) {
        throw 'NotImplementedException()';
        return 0;
    }
    // returns true if the ellipse goes counterclockwise
    orientedCounterclockwise() {
        return Point.crossProduct(this.aAxis, this.bAxis) > 0;
    }
    //returns the box of the ellipse that this ellipse is a part of
    fullBox() {
        const del = this.aAxis.add(this.bAxis);
        return Rectangle.mkPP(this.center.add(del), this.center.sub(del));
    }
    //is it a proper arc?
    isArc() {
        return this.aAxis.x == this.bAxis.y && this.aAxis.y == -this.bAxis.x;
    }
}
//# sourceMappingURL=ellipse.js.map