/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import { AxisOrder, Geometry } from "../Geometry";
import { GrowableFloat64Array } from "../geometry3d/GrowableFloat64Array";
import { GrowableXYZArray } from "../geometry3d/GrowableXYZArray";
import { Matrix3d } from "../geometry3d/Matrix3d";
import { Plane3d } from "../geometry3d/Plane3d";
import { Plane3dByOriginAndUnitNormal } from "../geometry3d/Plane3dByOriginAndUnitNormal";
import { Point3d, Vector3d } from "../geometry3d/Point3dVector3d";
import { IndexedXYZCollectionPolygonOps } from "../geometry3d/PolygonOps";
import { Range1d, Range3d } from "../geometry3d/Range";
import { Transform } from "../geometry3d/Transform";
import { Point4d } from "../geometry4d/Point4d";
import { AnalyticRoots } from "../numerics/Polynomials";
import { ClipUtilities } from "./ClipUtils";
/**
 * A ClipPlane is a single plane represented as
 * * An inward unit normal (u,v,w)
 * * A signedDistance
 * More details can be found at docs/learning/geometry/Clipping.md
 *
 * Hence
 * * The halfspace function evaluation for "point" (x,y,z) is `(x,y,z) DOT (u,v,w) - signedDistance`.
 * * POSITIVE values of the halfspace function are "inside".
 * * ZERO value of the halfspace function is "on".
 * * NEGATIVE value of the halfspace function is "outside".
 * * A representative point on the plane is (signedDistance * u, signedDistance * v, signedDistance * w).
 * * Given a point on the plane and the inward normal of the plane, `signedDistance = point DOT normal`.
 * @public
 */
export class ClipPlane extends Plane3d {
    _inwardNormal;
    /**
     * Construct a parallel plane through the origin.
     * * Move it to the actual position.
     * * _distanceFromOrigin is the distance it moved, with the (inward) normal direction as positive
     */
    _distanceFromOrigin;
    _invisible;
    _interior;
    constructor(normal, distance, invisible, interior) {
        super();
        this._invisible = invisible;
        this._interior = interior;
        this._inwardNormal = normal;
        this._distanceFromOrigin = distance;
    }
    /*
      private safeSetXYZDistance(nx: number, ny: number, nz: number, d: number) {
        this._inwardNormal.set(nx, ny, nz);
        this._distanceFromOrigin = d;
      }
    */
    /**
     * Return true if all members are almostEqual to corresponding members of other.
     * @param other clip plane to compare
     */
    isAlmostEqual(other) {
        return Geometry.isSameCoordinate(this._distanceFromOrigin, other._distanceFromOrigin)
            && this._inwardNormal.isAlmostEqual(other._inwardNormal)
            && this._interior === other._interior
            && this._invisible === other._invisible;
    }
    /** Return a cloned plane */
    clone() {
        const result = new ClipPlane(this._inwardNormal.clone(), this._distanceFromOrigin, this._invisible, this._interior);
        return result;
    }
    /** Return a cloned plane with coordinate data negated. */
    cloneNegated() {
        const plane = new ClipPlane(this._inwardNormal.clone(), this._distanceFromOrigin, this._invisible, this._interior);
        plane.negateInPlace();
        return plane;
    }
    /** Create a ClipPlane from Plane3dByOriginAndUnitNormal. */
    static createPlane(plane, invisible = false, interior = false, result) {
        const distance = plane.getNormalRef().dotProduct(plane.getOriginRef());
        if (result) {
            result._invisible = invisible;
            result._interior = interior;
            result._inwardNormal = plane.getNormalRef().clone();
            result._distanceFromOrigin = distance;
            return result;
        }
        return new ClipPlane(plane.getNormalRef().clone(), distance, invisible, interior);
    }
    /**
     * Create a ClipPlane with direct normal and signedDistance.
     * * The vector is normalized for storage.
     */
    static createNormalAndDistance(normal, distance, invisible = false, interior = false, result) {
        const normalized = normal.normalize();
        if (normalized) {
            if (result) {
                result._invisible = invisible;
                result._interior = interior;
                result._inwardNormal = normalized;
                result._distanceFromOrigin = distance;
            }
            return new ClipPlane(normalized, distance, invisible, interior);
        }
        return undefined;
    }
    /**
     * Create a ClipPlane
     * * "normal" is the inward normal of the plane (it is internally normalized).
     * * "point" is any point of the plane.
     * * The stored distance for the plane is the dot product of the point with the normal (i.e. treat the point's xyz as
     * a vector from the origin.)
     */
    static createNormalAndPoint(normal, point, invisible = false, interior = false, result) {
        const normalized = normal.normalize();
        if (normalized) {
            const distance = normalized.dotProduct(point);
            if (result) {
                result._invisible = invisible;
                result._interior = interior;
                result._inwardNormal = normalized;
                result._distanceFromOrigin = distance;
            }
            return new ClipPlane(normalized, distance, invisible, interior);
        }
        return undefined;
    }
    /**
     * Create a clip plane
     * @param origin any point on the plane.
     * @param vectorA any vector in the plane
     * @param vectorB any vector in the plane
     * @return undefined if the vectors are not independent.
     * * The stored inward normal is vectorB.crossProduct(vectorA).
     * * That is, the vectors are considered as a right-handed pair when viewed from the outside.
     */
    static createOriginAndVectors(origin, vectorA, vectorB, invisible = false, interior = false, result) {
        const normalized = vectorB.crossProduct(vectorA);
        return this.createNormalAndPoint(normalized, origin, invisible, interior, result);
    }
    /**
     * Create a ClipPlane
     * * "normal" (normalX, normalY, normalZ) is the inward normal of the plane.
     * * "point" (originX,originY,originZ) is any point of the plane.
     * * The stored distance for the plane is the dot product of the point with the normal (i.e. treat the point's xyz
     * as a vector from the origin.)
     */
    static createNormalAndPointXYZXYZ(normalX, normalY, normalZ, originX, originY, originZ, invisible = false, interior = false, result) {
        const q = Geometry.hypotenuseXYZ(normalX, normalY, normalZ);
        const r = Geometry.conditionalDivideFraction(1, q);
        if (r !== undefined) {
            if (result) {
                result._inwardNormal.set(normalX * r, normalY * r, normalZ * r);
                result._distanceFromOrigin = result._inwardNormal.dotProductXYZ(originX, originY, originZ);
                result._invisible = invisible;
                result._interior = interior;
                return result;
            }
            const normal = Vector3d.create(normalX * r, normalY * r, normalZ * r);
            return new ClipPlane(normal, normal.dotProductXYZ(originX, originY, originZ), invisible, interior);
        }
        return undefined;
    }
    /**
     * Return a json object of the form
     * `{"normal":[u,v,w],"dist":signedDistanceValue,"interior":true,"invisible":true}`
     */
    toJSON() {
        const props = {
            normal: this.inwardNormalRef.toJSON(),
            dist: this.distance,
        };
        if (this.interior)
            props.interior = true;
        if (this.invisible)
            props.invisible = true;
        return props;
    }
    /** Parse json object to ClipPlane instance */
    static fromJSON(json, result) {
        if (json && json.normal && undefined !== json.dist && Number.isFinite(json.dist))
            return ClipPlane.createNormalAndDistance(Vector3d.fromJSON(json.normal), json.dist, !!json.invisible, !!json.interior);
        return ClipPlane.createNormalAndDistance(Vector3d.unitZ(), 0, false, false, result);
    }
    /** Set both the invisible and interior flags. */
    setFlags(invisible, interior) {
        this._invisible = invisible;
        this._interior = interior;
    }
    /** Return the stored distanceFromOrigin property. */
    get distance() {
        return this._distanceFromOrigin;
    }
    /** * Return the stored inward normal property. */
    get inwardNormalRef() {
        return this._inwardNormal;
    }
    /**  Return the "interior" property flag. Interpretation of this flag is algorithm-specific. */
    get interior() {
        return this._interior;
    }
    /**  Return the "invisible" property flag. Interpretation of this flag is algorithm-specific. */
    get invisible() {
        return this._invisible;
    }
    /**
     * Create a plane defined by two points, an up vector, and a tilt angle relative to the up vector.
     * @param point0 start point of the edge
     * @param point1 end point of the edge
     * @param upVector vector perpendicular to the plane
     * @param tiltAngle angle to tilt the plane around the edge in the direction of the up vector.
     * @param result optional preallocated plane
     */
    static createEdgeAndUpVector(point0, point1, upVector, tiltAngle, result) {
        const edgeVector = Vector3d.createFrom(point1.minus(point0));
        let normal = (upVector.crossProduct(edgeVector)).normalize();
        if (normal) {
            if (tiltAngle !== undefined && !tiltAngle.isAlmostZero) {
                const tiltNormal = Vector3d.createRotateVectorAroundVector(normal, edgeVector, tiltAngle);
                if (tiltNormal) {
                    normal = tiltNormal.clone();
                }
            }
            normal.negate(normal);
            return ClipPlane.createNormalAndPoint(normal, point0, false, false, result);
        }
        return undefined;
    }
    /** Create a plane perpendicular to the edge between the xy parts of point0 and point1. */
    static createEdgeXY(point0, point1, result) {
        const normal = Vector3d.create(point0.y - point1.y, point1.x - point0.x);
        if (normal.normalizeInPlace())
            return ClipPlane.createNormalAndPoint(normal, point0, false, false, result);
        return undefined;
    }
    /**
     * Variant of [[createEdgeXY]] that computes the plane using the edge midpoint instead of its start point.
     * * This is more stable for creating a pair of clip planes from a long edge and its reversal, as is commonly found
     * in a [[UnionOfConvexClipPlaneSets]].
     */
    static createMidPointEdgeXY(point0, point1, result) {
        return ClipPlane.createNormalAndPointXYZXYZ(point0.y - point1.y, point1.x - point0.x, 0, 0.5 * (point0.x + point1.x), 0.5 * (point0.y + point1.y), 0, false, false, result);
    }
    /**
     * Return the Plane3d form of the plane.
     * * The plane origin is the point `distance * inwardNormal`
     * * The plane normal is the inward normal of the ClipPlane.
     */
    getPlane3d() {
        const d = this._distanceFromOrigin;
        // normal should be normalized, will not return undefined
        return Plane3dByOriginAndUnitNormal.create(Point3d.create(this._inwardNormal.x * d, this._inwardNormal.y * d, this._inwardNormal.z * d), this._inwardNormal);
    }
    /**
     * Return the Point4d d form of the plane.
     * * The homogeneous xyz are the inward normal xyz.
     * * The homogeneous weight is the negated ClipPlane distance.
     */
    getPlane4d() {
        return Point4d.create(this._inwardNormal.x, this._inwardNormal.y, this._inwardNormal.z, -this._distanceFromOrigin);
    }
    /**
     * Set the plane from DPoint4d style plane.
     * * The saved plane has its direction normalized.
     * * This preserves the plane itself as a zero set but make plane evaluations act as true distances (even if the
     * plane coefficients are scaled otherwise).
     * @param plane the DPoint4d style plane.
     */
    setPlane4d(plane) {
        const a = Math.sqrt(plane.x * plane.x + plane.y * plane.y + plane.z * plane.z);
        const r = a === 0.0 ? 1.0 : 1.0 / a;
        this._inwardNormal.x = r * plane.x;
        this._inwardNormal.y = r * plane.y;
        this._inwardNormal.z = r * plane.z;
        this._distanceFromOrigin = -r * plane.w;
    }
    /**
     * Evaluate the altitude in weighted space, i.e. (dot product with inward normal) minus distance, with point.w
     * scale applied to distance.
     * @param point space point to test.
     */
    weightedAltitude(point) {
        return point.x * this._inwardNormal.x
            + point.y * this._inwardNormal.y
            + point.z * this._inwardNormal.z
            - point.w * this._distanceFromOrigin;
    }
    /**
     * Evaluate the distance from the plane to a point in space, i.e. (dot product with inward normal) minus distance.
     * @param point space point to test.
     */
    altitude(point) {
        return point.x * this._inwardNormal.x
            + point.y * this._inwardNormal.y
            + point.z * this._inwardNormal.z
            - this._distanceFromOrigin;
    }
    /**
     * Evaluate the distance from the plane to a point in space with point given as x,y,z, i.e. (dot product with
     * inward normal) minus distance.
     * @param point space point to test.
     */
    altitudeXYZ(x, y, z) {
        return x * this._inwardNormal.x
            + y * this._inwardNormal.y
            + z * this._inwardNormal.z
            - this._distanceFromOrigin;
    }
    /** Return the x component of the normal used to evaluate altitude. */
    normalX() {
        return this._inwardNormal.x;
    }
    /** Return the x component of the normal used to evaluate altitude. */
    normalY() {
        return this._inwardNormal.y;
    }
    /** Return the z component of the normal used to evaluate altitude. */
    normalZ() {
        return this._inwardNormal.z;
    }
    /** Return the dot product of the plane normal with the vector (NOT using the plane's distanceFromOrigin). */
    velocity(vector) {
        return vector.x * this._inwardNormal.x + vector.y * this._inwardNormal.y + vector.z * this._inwardNormal.z;
    }
    /**
     * Return the dot product of the plane normal with the x,yz, vector components (NOT using the plane's
     * distanceFromOrigin).
     */
    velocityXYZ(x, y, z) {
        return x * this._inwardNormal.x + y * this._inwardNormal.y + z * this._inwardNormal.z;
    }
    /**
     * Return the dot product of the plane normal with the point (treating the point xyz as a vector, and NOT
     * using the plane's distanceFromOrigin).
     */
    dotProductPlaneNormalPoint(point) {
        return point.x * this._inwardNormal.x + point.y * this._inwardNormal.y + point.z * this._inwardNormal.z;
    }
    /**
     * Return true if spacePoint is inside or on the plane, with tolerance applied to "on".
     * @param spacePoint point to test.
     * @param tolerance tolerance for considering "near plane" to be "on plane"
     */
    isPointOnOrInside(spacePoint, tolerance = Geometry.smallMetricDistance) {
        let value = this.altitude(spacePoint);
        if (tolerance) {
            value += tolerance;
        }
        return value >= 0.0;
    }
    /**
     * Return true if spacePoint is strictly inside the halfspace, with tolerance applied to "on".
     * @param spacePoint point to test.
     * @param tolerance tolerance for considering "near plane" to be "on plane"
     */
    isPointInside(point, tolerance = Geometry.smallMetricDistance) {
        let value = this.altitude(point);
        if (tolerance) {
            value -= tolerance;
        }
        return value > 0.0;
    }
    /**
     * Return true if spacePoint is strictly on the plane, within tolerance
     * @param spacePoint point to test.
     * @param tolerance tolerance for considering "near plane" to be "on plane"
     */
    isPointOn(point, tolerance = Geometry.smallMetricDistance) {
        return Math.abs(this.altitude(point)) <= tolerance;
    }
    /**
     * Compute intersections of an (UNBOUNDED) arc with the plane.  Append them (as radians) to a growing array.
     * @param arc arc to test.  The angle limits of the arc are NOT considered.
     * @param intersectionRadians array to receive results
     */
    appendIntersectionRadians(arc, intersectionRadians) {
        const arcVectors = arc.toVectors();
        const alpha = this.altitude(arc.center);
        const beta = this.velocity(arcVectors.vector0);
        const gamma = this.velocity(arcVectors.vector90);
        AnalyticRoots.appendImplicitLineUnitCircleIntersections(alpha, beta, gamma, undefined, undefined, intersectionRadians);
    }
    static _clipArcFractionArray = new GrowableFloat64Array();
    /**
     * Announce fractional intervals of arc clip.
     * * Each call to `announce(fraction0, fraction1, arc)` announces one interval that is inside the clip plane.
     */
    announceClippedArcIntervals(arc, announce) {
        const breaks = ClipPlane._clipArcFractionArray;
        breaks.clear();
        this.appendIntersectionRadians(arc, breaks);
        arc.sweep.radiansArrayToPositivePeriodicFractions(breaks);
        return ClipUtilities.selectIntervals01(arc, breaks, this, announce);
    }
    /**
     * Compute intersection of (unbounded) segment with the plane.
     * * If the ends are on the same side of the plane, return undefined.
     * * If the intersection is an endpoint or interior to the segment return the fraction.
     * * If both ends are on, return undefined.
     */
    getBoundedSegmentSimpleIntersection(pointA, pointB) {
        const h0 = this.altitude(pointA);
        const h1 = this.altitude(pointB);
        if (h0 * h1 > 0.0)
            return undefined;
        if (h0 === 0.0 && h1 === 0.0) {
            return undefined;
        }
        return -h0 / (h1 - h0);
    }
    /** Apply transform to the origin. Apply inverse transpose of the matrix part to th normal vector. */
    transformInPlace(transform) {
        const plane = this.getPlane3d();
        const matrix = transform.matrix;
        const newPoint = transform.multiplyPoint3d(plane.getOriginRef());
        // Normal transforms as the inverse transpose of the matrix part
        // BTW: If the matrix is orthogonal, this is a long way to multiply by the matrix part (mumble grumble)
        const newNormal = matrix.multiplyInverseTranspose(plane.getNormalRef());
        if (!newNormal)
            return false;
        plane.set(newPoint, newNormal);
        const normalized = (plane.getNormalRef()).normalize();
        if (!normalized)
            return false;
        this._inwardNormal = normalized;
        this._distanceFromOrigin = this._inwardNormal.dotProduct(plane.getOriginRef());
        return true;
    }
    /** Set the invisible flag. Interpretation of this flag is algorithm-specific. */
    setInvisible(invisible) {
        this._invisible = invisible;
    }
    /** Reverse the sign of all coefficients, so outside and inside reverse */
    negateInPlace() {
        this._inwardNormal = this._inwardNormal.negate();
        this._distanceFromOrigin = -this._distanceFromOrigin;
    }
    /**
     * Move the plane INWARD by given distance
     * @param offset distance of shift inwards
     */
    offsetDistance(offset) {
        this._distanceFromOrigin += offset;
    }
    /**
     * Clip a polygon to the inside or outside of the plane.
     * * Results with 2 or fewer points are ignored.
     * * Other than ensuring capacity in the arrays, there are no object allocations during execution of this function.
     * * For a convex input polygon, the output polygon is also convex.
     * * For non-convex input, the output polygon may have double-back edges along plane intersections. This is still a
     * valid clip in a parity sense (overlapping regions cancel).
     * @param xyz input points.
     * @param work optional work buffer
     * @param inside whether the positive side of the plane survives (true, default), or negative side (false).
     * @param tolerance distance tolerance for "on plane" decision. Default value is [[Geometry.smallMetricDistance]].
     * @return the number of crossings. If this is larger than 2, the input polygon was non-convex.
     * @see appendPolygonClip
     */
    clipConvexPolygonInPlace(xyz, work, inside = true, tolerance = Geometry.smallMetricDistance) {
        return IndexedXYZCollectionPolygonOps.clipConvexPolygonInPlace(this, xyz, work, inside, tolerance);
    }
    /**
     * Multiply the ClipPlane's DPoint4d by matrix.
     * @param matrix matrix to apply.
     * @param invert if true, use in verse of the matrix.
     * @param transpose if true, use the transpose of the matrix (or inverse, per invert parameter)
     * @param matrix matrix to apply
     * @return false if unable to invert
     * * Note that if matrixA is applied to all of space, the matrix to send to this method to get a corresponding
     * effect on the plane is the inverse transpose of matrixA
     * * Callers that will apply the same matrix to many planes should pre-invert the matrix for efficiency.
     * * Both params default to true to get the full effect of transforming space.
     */
    multiplyPlaneByMatrix4d(matrix, invert = true, transpose = true) {
        const plane = this.getPlane4d();
        if (invert) {
            const inverse = matrix.createInverse();
            if (inverse)
                return this.multiplyPlaneByMatrix4d(inverse, false, transpose);
            return false;
        }
        if (transpose)
            matrix.multiplyTransposePoint4d(plane, plane);
        else
            matrix.multiplyPoint4d(plane, plane);
        this.setPlane4d(plane);
        return true;
    }
    /** Announce the interval (if any) where a line is within the clip plane half space. */
    announceClippedSegmentIntervals(f0, f1, pointA, pointB, announce) {
        if (f1 < f0)
            return false;
        const h0 = -this.altitude(pointA);
        const h1 = -this.altitude(pointB);
        const delta = h1 - h0;
        const f = Geometry.conditionalDivideFraction(-h0, delta);
        if (f === undefined) { // The segment is parallel to the plane.
            if (h0 <= 0.0) {
                if (announce)
                    announce(f0, f1);
                return true;
            }
            return false;
        }
        if (delta > 0) { // segment aims OUT
            if (f < f1)
                f1 = f;
        }
        else {
            // segment aims IN
            if (f > f0)
                f0 = f;
        }
        if (f1 < f0)
            return false;
        if (announce)
            announce(f0, f1);
        return true;
    }
    /**
     * Return a coordinate frame with
     * * origin at closest point to global origin
     * * z axis points in
     * * x and y are "in plane"
     */
    getFrame() {
        const d = this._distanceFromOrigin;
        const origin = Point3d.create(this._inwardNormal.x * d, this._inwardNormal.y * d, this._inwardNormal.z * d);
        const matrix = Matrix3d.createRigidHeadsUp(this._inwardNormal, AxisOrder.ZXY);
        return Transform.createOriginAndMatrix(origin, matrix);
    }
    /**
     * Return the intersection of the plane with a range cube.
     * @param range
     * @param xyzOut intersection polygon.  This is convex.
     */
    intersectRange(range, addClosurePoint = false) {
        if (range.isNull)
            return undefined;
        const corners = range.corners();
        const frameOnPlane = this.getFrame();
        frameOnPlane.multiplyInversePoint3dArrayInPlace(corners);
        const localRange = Range3d.createArray(corners);
        if (localRange.low.z * localRange.high.z > 0.0)
            return undefined;
        // oversized polygon on local z= 0
        const xyzOut = new GrowableXYZArray();
        xyzOut.pushXYZ(localRange.low.x, localRange.low.y, 0);
        xyzOut.pushXYZ(localRange.high.x, localRange.low.y, 0);
        xyzOut.pushXYZ(localRange.high.x, localRange.high.y, 0);
        xyzOut.pushXYZ(localRange.low.x, localRange.high.y, 0);
        xyzOut.multiplyTransformInPlace(frameOnPlane);
        IndexedXYZCollectionPolygonOps.intersectRangeConvexPolygonInPlace(range, xyzOut);
        if (xyzOut.length === 0)
            return undefined;
        if (addClosurePoint)
            xyzOut.pushWrap(1);
        return xyzOut;
    }
    /**
     * Implement appendPolygonClip, as defined in interface PolygonClipper.
     * @param xyz convex polygon. This is not changed.
     * @param insideFragments Array to receive "inside" fragments. Each fragment is a GrowableXYZArray grabbed
     * from the cache. This is NOT cleared.
     * @param outsideFragments Array to receive "outside" fragments. Each fragment is a GrowableXYZArray grabbed
     * from the cache. This is NOT cleared.
     * @param arrayCache cache for reusable GrowableXYZArray.
     * @see clipConvexPolygonInPlace
     */
    appendPolygonClip(xyz, insideFragments, outsideFragments, arrayCache) {
        const perpendicularRange = Range1d.createNull();
        const newInside = arrayCache.grabFromCache();
        const newOutside = arrayCache.grabFromCache();
        IndexedXYZCollectionPolygonOps.splitConvexPolygonInsideOutsidePlane(this, xyz, newInside, newOutside, perpendicularRange);
        ClipUtilities.captureOrDrop(newInside, 3, insideFragments, arrayCache);
        ClipUtilities.captureOrDrop(newOutside, 3, outsideFragments, arrayCache);
    }
    /** Project a point in space to the plane. */
    projectPointToPlane(spacePoint, result) {
        return this.projectXYZToPlane(spacePoint.x, spacePoint.y, spacePoint.z, result);
    }
    /** Return the projection of (x,y,z) onto the plane. */
    projectXYZToPlane(x, y, z, result) {
        const scale = -this.altitudeXYZ(x, y, z);
        return Point3d.create(x + scale * this._inwardNormal.x, y + scale * this._inwardNormal.y, z + scale * this._inwardNormal.z, result);
    }
}
//# sourceMappingURL=ClipPlane.js.map