/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Curve
 */
import { TransitionSpiral3d } from "./TransitionSpiral3d";
import { Segment1d } from "../../geometry3d/Segment1d";
import { AngleSweep } from "../../geometry3d/AngleSweep";
import { Transform } from "../../geometry3d/Transform";
import { LineString3d } from "../LineString3d";
import { NormalizedTransition } from "./NormalizedTransition";
import { TransitionConditionalProperties } from "./TransitionConditionalProperties";
import { Quadrature } from "../../numerics/Quadrature";
import { Point3d } from "../../geometry3d/Point3dVector3d";
import { Matrix3d } from "../../geometry3d/Matrix3d";
import { AxisOrder, Geometry } from "../../Geometry";
import { StrokeOptions } from "../StrokeOptions";
import { Ray3d } from "../../geometry3d/Ray3d";
import { Plane3dByOriginAndVectors } from "../../geometry3d/Plane3dByOriginAndVectors";
/**
 * An IntegratedSpiral3d is a curve defined by integrating its curvature.
 * * The first integral of curvature (with respect to distance along the curve) is the bearing angle (in radians).
 * * Integrating (cos(theta), sin(theta)) gives displacement from the start point, and thus the actual curve position.
 * * The curvature functions of interest are all symmetric snap functions in the [[NormalizedTransition]] class.
 * * [[TransitionConditionalProperties]] implements the computations of the interrelationship of radii, bearing, and length.
 * @public
 */
// see internaldocs/Spiral.md for more info
export class IntegratedSpiral3d extends TransitionSpiral3d {
    /** String name for schema properties. */
    curvePrimitiveType = "transitionSpiral";
    /** Start and end radii as a Segment1d. */
    radius01;
    /** Start and end bearings as an AngleSweep. */
    bearing01;
    /** Stroked approximation of entire spiral. */
    _globalStrokes;
    /**
     * Stroked approximation of active spiral.
     * * Same count as global -- possibly overly fine, but it gives some consistency between same clothoid constructed
     * as partial versus complete.
     * * If no trimming, this points to the same place as the _globalStrokes.  DO NOT double transform.
     */
    _activeStrokes;
    /** Return the internal stroked form of the (possibly partial) spiral. */
    get activeStrokes() {
        return this._activeStrokes !== undefined ? this._activeStrokes : this._globalStrokes;
    }
    /** Evaluator for transition. */
    _evaluator;
    /** Total curve arc length (computed). */
    _arcLength01;
    /** Curvatures (inverse radii) at start and end. */
    _curvature01;
    // constructor demands all bearing, radius, and length data.
    // caller determines usual dependency of "any 4 determine the 5th".
    constructor(spiralType, evaluator, radius01, bearing01, activeFractionInterval, localToWorld, arcLength, properties) {
        super(spiralType, localToWorld, activeFractionInterval, properties);
        this._evaluator = evaluator;
        this.radius01 = radius01;
        this.bearing01 = bearing01;
        this._arcLength01 = arcLength;
        this._globalStrokes = LineString3d.create();
        // initialize for compiler; but this will be recomputed in refreshComputeProperties
        this._curvature01 = Segment1d.create(0, 1);
        this.refreshComputedProperties();
    }
    /** Default spiral type name: clothoid. */
    static defaultSpiralType = "clothoid";
    /** Use the integrated function to return an angle at fractional position. */
    globalFractionToBearingRadians(fraction) {
        // calculate area under curvature curve from 0 to fraction and add it to start angle to get angle at the given fraction
        // see internaldocs/Spiral.md for more info
        const areaFraction = this._evaluator.fractionToArea(fraction);
        const arcLength = this._arcLength01;
        return this.bearing01.startRadians
            + fraction * arcLength * this._curvature01.x0
            + areaFraction * arcLength * this._curvature01.signedDelta();
    }
    /** Use the integrated function to return an angle at fractional position. */
    globalFractionToCurvature(fraction) {
        const f = this._evaluator.fractionToCurvatureFraction(fraction);
        return this._curvature01.fractionToPoint(f);
    }
    /** Return the bearing at given fraction of the active interval. */
    fractionToBearingRadians(activeFraction) {
        const fraction = this.activeFractionInterval.fractionToPoint(activeFraction);
        return this.bearing01.startRadians
            + fraction * this._arcLength01 * (this._curvature01.x0 + 0.5 * fraction * (this._curvature01.x1 - this._curvature01.x0));
    }
    /**
     * Return the curvature at given fraction of the active interval.
     * * The `undefined` result is to match the abstract class -- it cannot actually occur.
     */
    fractionToCurvature(activeFraction) {
        // BUG? active interval
        return this._curvature01.fractionToPoint(this.activeFractionInterval.fractionToPoint(activeFraction));
    }
    // These static variables are reused on calls to integrateFromStartFraction
    static _gaussFraction;
    static _gaussWeight;
    static _gaussMapper;
    /** Initialize class level work arrays. */
    static initWorkSpace() {
        IntegratedSpiral3d._gaussFraction = new Float64Array(5);
        IntegratedSpiral3d._gaussWeight = new Float64Array(5);
        IntegratedSpiral3d._gaussMapper = (xA, xB, xMapped, wMapped) => Quadrature.setupGauss5(xA, xB, xMapped, wMapped);
    }
    /**
     * Evaluate and sum the gauss quadrature formulas to integrate cos(theta), sin(theta) fractional subset of a reference
     * length (recall that theta is a nonlinear function of the fraction).
     * * This is a single interval of gaussian integration.
     * * The fraction is on the full spiral (not in the mapped active interval).
     * @param xyz advancing integrated point.
     * @param fractionA fraction at start of interval.
     * @param fractionB fraction at end of interval.
     * @param applyMatrix if true, apply the localToWorld matrix to the computed delta before adding to xyz.
     */
    fullSpiralIncrementalIntegral(xyz, fractionA, fractionB, applyMatrix) {
        // see internaldocs/Spiral.md for more info
        const gaussFraction = IntegratedSpiral3d._gaussFraction;
        const gaussWeight = IntegratedSpiral3d._gaussWeight;
        const numEval = IntegratedSpiral3d._gaussMapper(fractionA, fractionB, gaussFraction, gaussWeight);
        const deltaL = this._arcLength01;
        let w = 0;
        let dx = 0.0;
        let dy = 0.0;
        for (let k = 0; k < numEval; k++) {
            const radians = this.globalFractionToBearingRadians(gaussFraction[k]);
            w = gaussWeight[k] * deltaL;
            dx += w * Math.cos(radians);
            dy += w * Math.sin(radians);
        }
        if (applyMatrix)
            Matrix3d.xyzPlusMatrixTimesXYZ(xyz, this.localToWorld.matrix, { x: dx, y: dy, z: 0.0 }, xyz);
        else
            xyz.addXYZInPlace(dx, dy, 0.0);
    }
    /** Recompute strokes. */
    refreshComputedProperties() {
        this._curvature01 = Segment1d.create(TransitionSpiral3d.radiusToCurvature(this.radius01.x0), TransitionSpiral3d.radiusToCurvature(this.radius01.x1));
        this._globalStrokes.clear();
        const currentPoint = Point3d.create();
        this._globalStrokes.appendStrokePoint(currentPoint);
        const numInterval = 16;
        const fractionStep = 1.0 / numInterval;
        for (let i = 1; i <= numInterval; i++) {
            const fraction0 = (i - 1) * fractionStep;
            const fraction1 = i * fractionStep;
            this.fullSpiralIncrementalIntegral(currentPoint, fraction0, fraction1, false);
            this._globalStrokes.appendStrokePoint(currentPoint);
        }
        this._globalStrokes.tryTransformInPlace(this.localToWorld);
        if (!this.activeFractionInterval.isExact01) {
            if (this._activeStrokes === undefined)
                this._activeStrokes = LineString3d.create();
            this._activeStrokes.clear();
            // The active interval has finer strokes: it's the same fraction step but mapped to a sub-interval.
            // Below assumes fractionToPoint depends upon the global strokes we just computed, and not
            // on the active strokes we are computing.
            for (let i = 0; i <= numInterval; i++) {
                const localFraction = i * fractionStep;
                this._activeStrokes.addPoint(this.fractionToPoint(localFraction));
            }
        }
    }
    /**
     * Create a transition spiral with radius and bearing conditions.
     * @param radius01 radius (inverse curvature) at start and end (radius of zero means straight line).
     * @param bearing01 bearing angles at start and end. Bearings are measured from the x axis, positive clockwise
     * towards y axis.
     * @param activeFractionInterval fractional limits of the active portion of the spiral.
     * @param localToWorld placement frame. Fractional coordinate 0 is at the origin.
     */
    static createRadiusRadiusBearingBearing(radius01, bearing01, activeFractionInterval, localToWorld, typeName) {
        const arcLength = TransitionSpiral3d.radiusRadiusSweepRadiansToArcLength(radius01.x0, radius01.x1, bearing01.sweepRadians);
        if (typeName === undefined)
            typeName = "clothoid";
        const evaluator = NormalizedTransition.findEvaluator(typeName);
        if (!evaluator)
            return undefined;
        return new IntegratedSpiral3d(typeName, evaluator, radius01.clone(), bearing01.clone(), activeFractionInterval.clone(), localToWorld.clone(), arcLength, new TransitionConditionalProperties(radius01.x0, radius01.x1, bearing01.startAngle.clone(), bearing01.endAngle.clone(), undefined));
    }
    /**
     * Create a transition spiral.
     * * Inputs must provide exactly 4 of the 5 values `[radius0,radius1,bearing0,bearing1,length]`.
     * @param spiralType one of "clothoid", "bloss", "biquadratic", "cosine", "sine".  If undefined, "clothoid" is used.
     * @param radius0 radius (or 0 for tangent to line) at start.
     * @param radius1 radius (or 0 for tangent to line) at end.
     * @param bearing0 bearing, measured CCW from x axis at start.
     * @param bearing1 bearing, measured CCW from x axis at end.
     * @param fractionInterval optional fractional interval for an "active" portion of the curve. If omitted, the full
     * [0,1] is used.
     * @param localToWorld placement transform.
     */
    static createFrom4OutOf5(spiralType, radius0, radius1, bearing0, bearing1, arcLength, fractionInterval, localToWorld) {
        if (spiralType === undefined)
            spiralType = "clothoid";
        const evaluator = NormalizedTransition.findEvaluator(spiralType);
        if (!evaluator)
            return undefined;
        const data = new TransitionConditionalProperties(radius0, radius1, bearing0, bearing1, arcLength);
        const data1 = data.clone();
        if (!data.tryResolveAnySingleUnknown())
            return undefined;
        if (fractionInterval === undefined)
            fractionInterval = Segment1d.create(0, 1);
        return new IntegratedSpiral3d(spiralType, evaluator, Segment1d.create(data.radius0, data.radius1), AngleSweep.createStartEnd(data.bearing0, data.bearing1), fractionInterval ? fractionInterval.clone() : Segment1d.create(0, 1), localToWorld, data.curveLength, data1);
    }
    /** Copy all defining data from another spiral. */
    setFrom(other) {
        this.localToWorld.setFrom(other.localToWorld);
        this.radius01.setFrom(other.radius01);
        this._curvature01.setFrom(other._curvature01);
        this.bearing01.setFrom(other.bearing01);
        this.localToWorld.setFrom(other.localToWorld);
        this.activeFractionInterval.setFrom(other.activeFractionInterval);
        this._arcLength01 = other._arcLength01;
        return this;
    }
    /** Deep clone of this spiral. */
    clone() {
        return new IntegratedSpiral3d(this._spiralType, this._evaluator, this.radius01.clone(), this.bearing01.clone(), this.activeFractionInterval.clone(), this.localToWorld.clone(), this._arcLength01, this._designProperties?.clone());
    }
    /** Apply `transform` to this spiral's local to world transform. */
    tryTransformInPlace(transformA) {
        const rigidData = this.applyRigidPartOfTransform(transformA);
        if (rigidData !== undefined) {
            this._curvature01.x0 /= rigidData.scale;
            this._curvature01.x1 /= rigidData.scale;
            this.radius01.x0 *= rigidData.scale;
            this.radius01.x1 *= rigidData.scale;
            this._arcLength01 *= rigidData.scale;
        }
        this.refreshComputedProperties();
        return true;
    }
    /** Return the spiral start point. */
    startPoint(result) {
        return this.activeStrokes.startPoint(result);
    }
    /** Return the spiral end point. */
    endPoint(result) {
        return this.activeStrokes.endPoint(result);
    }
    /** Test if the local to world transform places the spiral xy plane into `plane`. */
    isInPlane(plane) {
        return plane.isPointInPlane(this.localToWorld.origin)
            && Geometry.isSameCoordinate(0.0, this.localToWorld.matrix.dotColumnX(plane.getNormalRef()))
            && Geometry.isSameCoordinate(0.0, this.localToWorld.matrix.dotColumnY(plane.getNormalRef()));
    }
    /**
     * Return length of the spiral.
     * * Because TransitionSpiral is parameterized directly in terms of distance along, this is a simple return value.
     */
    quickLength() {
        return this.curveLength();
    }
    /**
     * Return length of the spiral.
     * * Because TransitionSpiral is parameterized directly in terms of distance along, this is a simple return value.
     */
    curveLength() {
        return this._arcLength01 * (this._activeFractionInterval.absoluteDelta());
    }
    /**
     * Return (unsigned) length of the spiral between fractions.
     * * Because TransitionSpiral is parameterized directly in terms of distance along, this is a simple return value.
     */
    curveLengthBetweenFractions(fraction0, fraction1) {
        return this._arcLength01 * (this._activeFractionInterval.absoluteDelta() * Math.abs(fraction1 - fraction0));
    }
    /** Test if `other` is an instance of `TransitionSpiral3d`. */
    isSameGeometryClass(other) {
        return other instanceof TransitionSpiral3d;
    }
    /**
     * Add strokes from this spiral to `dest`.
     * * Linestrings will usually stroke as just their points.
     * * If maxEdgeLength is given, this will sub-stroke within the linestring -- not what we want.
     */
    emitStrokes(dest, options) {
        this.activeStrokes.emitStrokes(dest, options);
    }
    /** Emit stroke fragments to `dest` handler. */
    emitStrokableParts(dest, options) {
        const n = this.computeStrokeCountForOptions(options);
        dest.startParentCurvePrimitive(this);
        const activeStrokes = this.activeStrokes;
        const preferPrimary = dest.needPrimaryGeometryForStrokes === undefined ? false : dest.needPrimaryGeometryForStrokes();
        if (!preferPrimary && n <= activeStrokes.numPoints()) {
            this.activeStrokes.emitStrokableParts(dest, options);
        }
        else {
            dest.announceIntervalForUniformStepStrokes(this, n, 0.0, 1.0);
        }
        dest.endParentCurvePrimitive(this);
    }
    /**
     * Return the stroke count required for given options.
     * @param options StrokeOptions that determine count.
     */
    computeStrokeCountForOptions(options) {
        let numStroke;
        if (options) {
            const rMin = Math.min(Math.abs(this.radius01.x0), Math.abs(this.radius01.x1));
            numStroke = options.applyTolerancesToArc(rMin, this.bearing01.sweepRadians);
            numStroke = options.applyMaxEdgeLength(numStroke, this.curveLength());
            numStroke = options.applyMinStrokesPerPrimitive(numStroke);
        }
        else {
            numStroke = StrokeOptions.applyAngleTol(undefined, 4, this.bearing01.sweepRadians);
        }
        return numStroke;
    }
    /**
     * Reverse the active interval and active strokes.
     * * Primary defining data remains unchanged.
     */
    reverseInPlace() {
        this.activeFractionInterval.reverseInPlace();
        if (this._activeStrokes === undefined)
            this._activeStrokes = this._globalStrokes.clone();
        this._activeStrokes.reverseInPlace();
    }
    /** Evaluate curve point with respect to fraction. */
    fractionToPoint(activeFraction, result) {
        const targetGlobalFraction = this.activeFractionInterval.fractionToPoint(activeFraction);
        const numStrokes = this._globalStrokes.packedPoints.length - 1;
        if (activeFraction > 1.0) {
            result = this._globalStrokes.packedPoints.back(result);
            const integrationStep = 1.0 / numStrokes;
            let currentGlobalFraction = 1.0;
            let nextGlobalFraction = currentGlobalFraction + integrationStep;
            while (nextGlobalFraction < targetGlobalFraction) {
                this.fullSpiralIncrementalIntegral(result, currentGlobalFraction, nextGlobalFraction, true);
                currentGlobalFraction = nextGlobalFraction;
                nextGlobalFraction += integrationStep;
            }
            this.fullSpiralIncrementalIntegral(result, currentGlobalFraction, targetGlobalFraction, true);
        }
        else if (activeFraction < 0.0) {
            result = this._globalStrokes.packedPoints.front(result);
            const integrationStep = 1.0 / numStrokes;
            let currentGlobalFraction = 0.0;
            let nextGlobalFraction = currentGlobalFraction - integrationStep;
            while (nextGlobalFraction > targetGlobalFraction) {
                this.fullSpiralIncrementalIntegral(result, currentGlobalFraction, nextGlobalFraction, true);
                currentGlobalFraction = nextGlobalFraction;
                nextGlobalFraction -= integrationStep;
            }
            this.fullSpiralIncrementalIntegral(result, currentGlobalFraction, targetGlobalFraction, true);
        }
        else {
            const clampedGlobalFraction = Geometry.clampToStartEnd(targetGlobalFraction, 0, 1);
            const index0 = Math.trunc(clampedGlobalFraction * numStrokes); // this indexes the point to the left of the query
            const globalFraction0 = index0 / numStrokes;
            result = this._globalStrokes.packedPoints.getPoint3dAtUncheckedPointIndex(index0, result);
            // GeometryCoreTestIO.consoleLog("fractionToPoint ", activeFraction, this.activeFractionInterval, "( global integration " + globalFraction0 + " to " + globalFraction + ")", index0);
            this.fullSpiralIncrementalIntegral(result, globalFraction0, targetGlobalFraction, true);
        }
        return result;
    }
    /** Evaluate curve point and derivative with respect to fraction. */
    fractionToPointAndDerivative(activeFraction, result) {
        const globalFraction = this.activeFractionInterval.fractionToPoint(activeFraction);
        result = result ? result : Ray3d.createZero();
        this.fractionToPoint(activeFraction, result.origin);
        const radians = this.globalFractionToBearingRadians(globalFraction);
        const a = this._arcLength01 * this.activeFractionInterval.signedDelta();
        this.localToWorld.matrix.multiplyXY(a * Math.cos(radians), a * Math.sin(radians), result.direction);
        return result;
    }
    /** Return the frenet frame at fractional position. */
    fractionToFrenetFrame(activeFraction, result) {
        const globalFraction = this.activeFractionInterval.fractionToPoint(activeFraction);
        result = result ? result : Transform.createIdentity();
        result.origin.setFrom(this.fractionToPoint(activeFraction));
        Matrix3d.createRigidFromMatrix3d(this.localToWorld.matrix, AxisOrder.XYZ, result.matrix);
        const radians = this.globalFractionToBearingRadians(globalFraction);
        const c = Math.cos(radians);
        const s = Math.sin(radians);
        result.matrix.applyGivensColumnOp(0, 1, c, s);
        return result;
    }
    /**
     * Return a plane with
     * * origin at fractional position along the curve.
     * * vectorU is the first derivative, i.e. tangent vector with length equal to the rate of change with respect to
     * the fraction.
     * * vectorV is the second derivative, i.e.derivative of vectorU.
     */
    fractionToPointAnd2Derivatives(activeFraction, result) {
        const globalFraction = this.activeFractionInterval.fractionToPoint(activeFraction);
        const origin = this.fractionToPoint(activeFraction);
        const radians = this.globalFractionToBearingRadians(globalFraction);
        const c = Math.cos(radians);
        const s = Math.sin(radians);
        const delta = this.activeFractionInterval.signedDelta();
        const a = delta;
        const b = a * delta;
        const vectorX = this.localToWorld.matrix.multiplyXY(a * c, a * s);
        const vectorY = this.localToWorld.matrix.multiplyXY(-b * s, b * c);
        vectorY.scaleInPlace(this.globalFractionToCurvature(globalFraction));
        return Plane3dByOriginAndVectors.createCapture(origin, vectorX, vectorY, result);
    }
    /** Second step of double dispatch:  call `handler.handleTransitionSpiral(this)`. */
    dispatchToGeometryHandler(handler) {
        return handler.handleTransitionSpiral(this);
    }
    /** Compare various coordinate quantities. */
    isAlmostEqual(other) {
        if (other instanceof IntegratedSpiral3d) {
            return this.radius01.isAlmostEqual(other.radius01)
                && this.bearing01.isAlmostEqualAllowPeriodShift(other.bearing01)
                && this.localToWorld.isAlmostEqual(other.localToWorld)
                && Geometry.isSameCoordinate(this._arcLength01, other._arcLength01)
                && this.activeFractionInterval.isAlmostEqual(other.activeFractionInterval)
                && this._curvature01.isAlmostEqual(other._curvature01);
        }
        return false;
    }
}
// at load time, initialize gauss quadrature workspace
IntegratedSpiral3d.initWorkSpace();
//# sourceMappingURL=IntegratedSpiral3d.js.map