import _ from 'lodash';
import '../../../assert/js/assert.js';
import '../../../globals.js';
// Copyright 2023-2024, University of Colorado Boulder
/**
 * A ClippableFace based on a set of line segment edges. Should still represent multiple closed loops, but it is not
 * explicit.
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */
import { alpenglow, BinaryClipping, BoundsClipping, CircularClipping, EdgedClippedFace, GridClipping, LinearEdge, PolygonalFace, PolygonBilinear, PolygonMitchellNetravali, StripeClipping } from '../imports.js';
import Bounds2 from '../../../dot/js/Bounds2.js';
import Range from '../../../dot/js/Range.js';
import Vector2 from '../../../dot/js/Vector2.js';
import Utils from '../../../dot/js/Utils.js';
const scratchVectorA = new Vector2(0, 0);
const scratchVectorB = new Vector2(0, 0);
export default class EdgedFace {
    constructor(edges, skipValidation = false) {
        this.edges = edges;
        // Check on validating edges, since our binary clips won't work well if things aren't matched up (can get extra
        // edges).
        assertSlow && !skipValidation && LinearEdge.validateStartEndMatches(edges);
    }
    /**
     * Converts the face to a polygonal face. Epsilon is used to determine whether start/end points match.
     *
     * NOTE: This is likely a low-performance method, and should only be used for debugging.
     */
    toPolygonalFace(epsilon = 1e-8) {
        return new PolygonalFace(LinearEdge.toPolygons(this.edges, epsilon));
    }
    /**
     * Converts the face to an edged face.
     */
    toEdgedFace() {
        return this;
    }
    /**
     * Converts the face to a edged-clipped face (scanning the edges, to convert bounds-side edges to counts)
     */
    toEdgedClippedFace(minX, minY, maxX, maxY) {
        return EdgedClippedFace.fromEdges(this.edges, minX, minY, maxX, maxY);
    }
    /**
     * Converts the face to a edged-clipped face (without scanning the edges to see if they are bounds-side)
     */
    toEdgedClippedFaceWithoutCheck(minX, minY, maxX, maxY) {
        return EdgedClippedFace.fromEdgesWithoutCheck(this.edges, minX, minY, maxX, maxY);
    }
    /**
     * Returns a Shape for the face.
     *
     * NOTE: This is likely a low-performance method, and should only be used for debugging.
     */
    getShape(epsilon = 1e-8) {
        return this.toPolygonalFace(epsilon).getShape();
    }
    /**
     * Returns the bounds of the face (ignoring any "fake" edges, if the type supports them)
     */
    getBounds() {
        const result = Bounds2.NOTHING.copy();
        for (let i = 0; i < this.edges.length; i++) {
            const edge = this.edges[i];
            if (!edge.containsFakeCorner) {
                result.addPoint(edge.startPoint);
                result.addPoint(edge.endPoint);
            }
        }
        return result;
    }
    /**
     * Returns the range of values for the dot product of the given normal with any point contained within the face
     * (for polygons, this is the same as the range of values for the dot product of the normal with any vertex).
     */
    getDotRange(normal) {
        let min = Number.POSITIVE_INFINITY;
        let max = Number.NEGATIVE_INFINITY;
        for (let i = 0; i < this.edges.length; i++) {
            const edge = this.edges[i];
            // TODO: containsFakeCorner should... propagate with clipping operations, no?
            if (!edge.containsFakeCorner) {
                const dotStart = edge.startPoint.dot(normal);
                const dotEnd = edge.endPoint.dot(normal);
                min = Math.min(min, dotStart, dotEnd);
                max = Math.max(max, dotStart, dotEnd);
            }
        }
        return new Range(min, max);
    }
    /**
     * Returns the range of distances from the given point to every point along the edges of the face.
     * For instance, if the face was the unit cube, the range would be 1/2 to sqrt(2), for distances to the middles of
     * the edges and the corners respectively.
     */
    getDistanceRangeToEdges(point) {
        let min = Number.POSITIVE_INFINITY;
        let max = 0;
        // TODO: Use LinearEdge.addDistanceRange if the function-call overhead isn't too much
        for (let i = 0; i < this.edges.length; i++) {
            const edge = this.edges[i];
            const p0x = edge.startPoint.x - point.x;
            const p0y = edge.startPoint.y - point.y;
            const p1x = edge.endPoint.x - point.x;
            const p1y = edge.endPoint.y - point.y;
            min = Math.min(min, LinearEdge.evaluateClosestDistanceToOrigin(p0x, p0y, p1x, p1y));
            max = Math.max(max, Math.sqrt(p0x * p0x + p0y * p0y), Math.sqrt(p1x * p1x + p1y * p1y));
        }
        return new Range(min, max);
    }
    /**
     * Returns the range of distances from the given point to every point inside the face. The upper bound should be
     * the same as getDistanceRangeToEdges, however the lower bound may be 0 if the point is inside the face.
     */
    getDistanceRangeToInside(point) {
        const range = this.getDistanceRangeToEdges(point);
        if (this.containsPoint(point)) {
            return new Range(0, range.max);
        }
        else {
            return range;
        }
    }
    /**
     * Returns the signed area of the face (positive if the vertices are in counter-clockwise order, negative if clockwise)
     */
    getArea() {
        let area = 0;
        for (let i = 0; i < this.edges.length; i++) {
            const edge = this.edges[i];
            const p0 = edge.startPoint;
            const p1 = edge.endPoint;
            // Shoelace formula for the area
            // NOTE NOTE NOTE: Don't change this without changing EdgedClippedFace's getArea()!
            area += (p1.x + p0.x) * (p1.y - p0.y);
        }
        return 0.5 * area;
    }
    /**
     * Returns the partial for the centroid computation. These should be summed up, divided by 6, and divided by the area
     * to give the full centroid
     */
    getCentroidPartial() {
        let x = 0;
        let y = 0;
        for (let i = 0; i < this.edges.length; i++) {
            const edge = this.edges[i];
            const p0 = edge.startPoint;
            const p1 = edge.endPoint;
            // Partial centroid evaluation. NOTE: using the compound version here, for performance/stability tradeoffs
            // NOTE NOTE NOTE: Don't change this without changing EdgedClippedFace's getCentroidPartial()!
            const base = (p0.x * (2 * p0.y + p1.y) + p1.x * (p0.y + 2 * p1.y));
            x += (p0.x - p1.x) * base;
            y += (p1.y - p0.y) * base;
        }
        return new Vector2(x, y);
    }
    /**
     * Returns the centroid of the face (area is required for the typical integral required to evaluate)
     */
    getCentroid(area) {
        return this.getCentroidPartial().timesScalar(1 / (6 * area));
    }
    /**
     * Returns the evaluation of an integral that will be zero if the boundaries of the face are correctly closed.
     * It is designed so that if there is a "gap" and we have open boundaries, the result will likely be non-zero.
     *
     * NOTE: This is only used for debugging, so performance is not a concern.
     */
    getZero() {
        return _.sum(this.edges.map(e => e.getLineIntegralZero()));
    }
    /**
     * Returns the average distance from the given point to every point inside the face. The integral evaluation requires
     * the area (similarly to the centroid computation).
     */
    getAverageDistance(point, area) {
        let sum = 0;
        for (let i = 0; i < this.edges.length; i++) {
            const edge = this.edges[i];
            const p0 = edge.startPoint;
            const p1 = edge.endPoint;
            sum += LinearEdge.evaluateLineIntegralDistance(p0.x - point.x, p0.y - point.y, p1.x - point.x, p1.y - point.y);
        }
        return sum / area;
    }
    /**
     * Returns the average distance from the origin to every point inside the face transformed by the given matrix.
     */
    getAverageDistanceTransformedToOrigin(transform, area) {
        let sum = 0;
        for (let i = 0; i < this.edges.length; i++) {
            const edge = this.edges[i];
            const p0 = transform.multiplyVector2(scratchVectorA.set(edge.startPoint));
            const p1 = transform.multiplyVector2(scratchVectorB.set(edge.endPoint));
            sum += LinearEdge.evaluateLineIntegralDistance(p0.x, p0.y, p1.x, p1.y);
        }
        // We need to account for how much the transform will scale the area
        return sum / (area * transform.getSignedScale());
    }
    /**
     * Returns a copy of the face that is clipped to be within the given axis-aligned bounding box.
     */
    getClipped(minX, minY, maxX, maxY) {
        const edges = [];
        const centerX = (minX + maxX) / 2;
        const centerY = (minY + maxY) / 2;
        for (let i = 0; i < this.edges.length; i++) {
            const edge = this.edges[i];
            BoundsClipping.boundsClipEdge(edge.startPoint, edge.endPoint, minX, minY, maxX, maxY, centerX, centerY, edges);
        }
        return new EdgedFace(edges);
    }
    /**
     * Returns two copies of the face, one that is clipped to be to the left of the given x value, and one that is
     * clipped to be to the right of the given x value.
     *
     * The fakeCornerY is used to determine the "fake" corner that is used for unsorted-edge clipping.
     */
    getBinaryXClip(x, fakeCornerY) {
        const minEdges = [];
        const maxEdges = [];
        for (let i = 0; i < this.edges.length; i++) {
            const edge = this.edges[i];
            BinaryClipping.binaryXClipEdge(edge.startPoint, edge.endPoint, x, fakeCornerY, minEdges, maxEdges);
        }
        assert && assert(minEdges.every(e => e.startPoint.x <= x && e.endPoint.x <= x));
        assert && assert(maxEdges.every(e => e.startPoint.x >= x && e.endPoint.x >= x));
        return {
            minFace: new EdgedFace(minEdges),
            maxFace: new EdgedFace(maxEdges)
        };
    }
    /**
     * Returns two copies of the face, one that is clipped to y values less than the given y value, and one that is
     * clipped to values greater than the given y value.
     *
     * The fakeCornerX is used to determine the "fake" corner that is used for unsorted-edge clipping.
     */
    getBinaryYClip(y, fakeCornerX) {
        const minEdges = [];
        const maxEdges = [];
        for (let i = 0; i < this.edges.length; i++) {
            const edge = this.edges[i];
            BinaryClipping.binaryYClipEdge(edge.startPoint, edge.endPoint, y, fakeCornerX, minEdges, maxEdges);
        }
        assert && assert(minEdges.every(e => e.startPoint.y <= y && e.endPoint.y <= y));
        assert && assert(maxEdges.every(e => e.startPoint.y >= y && e.endPoint.y >= y));
        return {
            minFace: new EdgedFace(minEdges),
            maxFace: new EdgedFace(maxEdges)
        };
    }
    /**
     * Returns two copies of the face, one that is clipped to contain points where dot( normal, point ) < value,
     * and one that is clipped to contain points where dot( normal, point ) > value.
     *
     * The fake corner perpendicular is used to determine the "fake" corner that is used for unsorted-edge clipping
     */
    getBinaryLineClip(normal, value, fakeCornerPerpendicular) {
        const minEdges = [];
        const maxEdges = [];
        for (let i = 0; i < this.edges.length; i++) {
            const edge = this.edges[i];
            BinaryClipping.binaryLineClipEdge(edge.startPoint, edge.endPoint, normal, value, fakeCornerPerpendicular, minEdges, maxEdges);
        }
        assert && assert(minEdges.every(e => normal.dot(e.startPoint) <= value + 1e-8 && normal.dot(e.endPoint) <= value + 1e-8));
        assert && assert(maxEdges.every(e => normal.dot(e.startPoint) >= value - 1e-8 && normal.dot(e.endPoint) >= value - 1e-8));
        return {
            minFace: new EdgedFace(minEdges),
            maxFace: new EdgedFace(maxEdges)
        };
    }
    /**
     * Returns an array of faces, clipped similarly to getBinaryLineClip, but with more than one (parallel) split line at
     * a time. The first face will be the one with dot( normal, point ) < values[0], the second one with
     * values[ 0 ] < dot( normal, point ) < values[1], etc.
     */
    getStripeLineClip(normal, values, fakeCornerPerpendicular) {
        if (values.length === 0) {
            return [this];
        }
        const edgesCollection = _.range(values.length + 1).map(() => []);
        for (let i = 0; i < this.edges.length; i++) {
            const edge = this.edges[i];
            StripeClipping.binaryStripeClipEdge(edge.startPoint, edge.endPoint, normal, values, fakeCornerPerpendicular, edgesCollection);
        }
        if (assert) {
            for (let i = 0; i < edgesCollection.length; i++) {
                const edges = edgesCollection[i];
                const minValue = i > 0 ? values[i - 1] : Number.NEGATIVE_INFINITY;
                const maxValue = i < values.length ? values[i] : Number.POSITIVE_INFINITY;
                assert(edges.every(e => {
                    return normal.dot(e.startPoint) + 1e-8 >= minValue && normal.dot(e.startPoint) - 1e-8 <= maxValue &&
                        normal.dot(e.endPoint) + 1e-8 >= minValue && normal.dot(e.endPoint) - 1e-8 <= maxValue;
                }));
            }
        }
        return edgesCollection.map(edges => new EdgedFace(edges));
    }
    /**
     * Returns two copies of the face, one that is clipped to contain points inside the circle defined by the given
     * center and radius, and one that is clipped to contain points outside the circle.
     *
     * NOTE: maxAngleSplit is used to determine the polygonal approximation of the circle. The returned result will not
     * have a chord with an angle greater than maxAngleSplit.
     */
    getBinaryCircularClip(center, radius, maxAngleSplit) {
        const insideEdges = [];
        const outsideEdges = [];
        CircularClipping.binaryCircularClipEdges(this.edges, center, radius, maxAngleSplit, insideEdges, outsideEdges);
        return {
            insideFace: new EdgedFace(insideEdges),
            outsideFace: new EdgedFace(outsideEdges)
        };
    }
    /**
     * Given an integral bounding box and step sizes (which define the grid), this will clip the face to each cell in the
     * grid, calling the callback for each cell's contributing edges (in order, if we are a PolygonalFace).
     * polygonCompleteCallback will be called whenever a polygon is completed (if we are a polygonal type of face).
     */
    gridClipIterate(minX, minY, maxX, maxY, stepX, stepY, stepWidth, stepHeight, callback, polygonCompleteCallback) {
        for (let i = 0; i < this.edges.length; i++) {
            const edge = this.edges[i];
            GridClipping.gridClipIterate(edge.startPoint, edge.endPoint, minX, minY, maxX, maxY, stepX, stepY, stepWidth, stepHeight, callback);
        }
        if (this.edges.length) {
            polygonCompleteCallback();
        }
    }
    /**
     * Returns the evaluation of the bilinear (tent) filter integrals for the given point, ASSUMING that the face
     * is clipped to the transformed unit square of x: [minX,minX+1], y: [minY,minY+1].
     */
    getBilinearFiltered(pointX, pointY, minX, minY) {
        return PolygonBilinear.evaluateLinearEdges(this.edges, pointX, pointY, minX, minY);
    }
    /**
     * Returns the evaluation of the Mitchell-Netravali (1/3,1/3) filter integrals for the given point, ASSUMING that the
     * face is clipped to the transformed unit square of x: [minX,minX+1], y: [minY,minY+1].
     */
    getMitchellNetravaliFiltered(pointX, pointY, minX, minY) {
        return PolygonMitchellNetravali.evaluateLinearEdges(this.edges, pointX, pointY, minX, minY);
    }
    /**
     * Returns whether the face contains the given point.
     */
    containsPoint(point) {
        return LinearEdge.getWindingNumberEdges(this.edges, point) !== 0;
    }
    /**
     * Returns an affine-transformed version of the face.
     */
    getTransformed(transform) {
        if (transform.isIdentity()) {
            return this;
        }
        else {
            const transformedEdges = [];
            for (let i = 0; i < this.edges.length; i++) {
                const edge = this.edges[i];
                const start = transform.timesVector2(edge.startPoint);
                const end = transform.timesVector2(edge.endPoint);
                if (!start.equals(end)) {
                    transformedEdges.push(new LinearEdge(start, end));
                }
            }
            return new EdgedFace(transformedEdges);
        }
    }
    /**
     * Returns a rounded version of the face, where [-epsilon/2, epsilon/2] rounds to 0, etc.
     */
    getRounded(epsilon) {
        const edges = [];
        for (let i = 0; i < this.edges.length; i++) {
            const edge = this.edges[i];
            const startPoint = new Vector2(Utils.roundSymmetric(edge.startPoint.x / epsilon) * epsilon, Utils.roundSymmetric(edge.startPoint.y / epsilon) * epsilon);
            const endPoint = new Vector2(Utils.roundSymmetric(edge.endPoint.x / epsilon) * epsilon, Utils.roundSymmetric(edge.endPoint.y / epsilon) * epsilon);
            if (!startPoint.equals(endPoint)) {
                edges.push(new LinearEdge(startPoint, endPoint, edge.containsFakeCorner));
            }
        }
        return new EdgedFace(edges);
    }
    /**
     * Returns a version of the face with the orientation of all of the edges swapped.
     */
    withReversedEdges() {
        return new EdgedFace(this.edges.map(edge => new LinearEdge(edge.endPoint, edge.startPoint)).reverse());
    }
    /**
     * Calls the callback with points for each edge in the face.
     */
    forEachEdge(callback) {
        for (let i = 0; i < this.edges.length; i++) {
            const edge = this.edges[i];
            callback(edge.startPoint, edge.endPoint);
        }
    }
    /**
     * Returns a singleton accumulator for this type of face. This will always return the same instance, and should ONLY
     * be used if there will be no reentrant or co-occurring usage of this accumulator (i.e. only use it when you can
     * guarantee nothing else will be clipped at the same time). If two tasks try to use this at the same time, it will
     * likely break.
     *
     * This is a method that can be called on an unknown-type face, to reproduce the same type of face. This is
     * important, since we can't feed unsorted edge data directly to a PolygonalFace's accumulator, and in general this
     * is the safest way to get an accumulator for a face.
     */
    getScratchAccumulator() {
        return scratchAccumulator;
    }
    /**
     * Returns a singleton accumulator for this type of face. This will always return the same instance, and should ONLY
     * be used if there will be no reentrant or co-occurring usage of this accumulator (i.e. only use it when you can
     * guarantee nothing else will be clipped at the same time). If two tasks try to use this at the same time, it will
     * likely break.
     *
     * This should be used directly when you know you want an EdgedFace as output.
     */
    static getScratchAccumulator() {
        return scratchAccumulator;
    }
    /**
     * Returns a new accumulator for this type of face. This should be used when concurrent clipping will need to happen.
     *
     * This is a method that can be called on an unknown-type face, to reproduce the same type of face. This is
     * important, since we can't feed unsorted edge data directly to a PolygonalFace's accumulator, and in general this
     * is the safest way to get an accumulator for a face.
     */
    getAccumulator() {
        return new EdgedFaceAccumulator();
    }
    /**
     * Returns a debugging string.
     */
    toString() {
        return this.edges.map(e => `${e.startPoint.x},${e.startPoint.y} => ${e.endPoint.x},${e.endPoint.y}`).join('\n');
    }
    /**
     * Returns a serialized version of the face, that should be able to be deserialized into the same type of face.
     * See {FaceType}.deserialize.
     *
     * NOTE: If you don't know what type of face this is, use serializeClippableFace instead.
     */
    serialize() {
        return {
            edges: this.edges.map(edge => edge.serialize())
        };
    }
    static deserialize(serialized) {
        return new EdgedFace(serialized.edges.map(edge => LinearEdge.deserialize(edge)));
    }
    static fromBounds(bounds) {
        return EdgedFace.fromBoundsValues(bounds.minX, bounds.minY, bounds.maxX, bounds.maxY);
    }
    static fromBoundsValues(minX, minY, maxX, maxY) {
        const p0 = new Vector2(minX, minY);
        const p1 = new Vector2(maxX, minY);
        const p2 = new Vector2(maxX, maxY);
        const p3 = new Vector2(minX, maxY);
        return new EdgedFace([
            new LinearEdge(p0, p1),
            new LinearEdge(p1, p2),
            new LinearEdge(p2, p3),
            new LinearEdge(p3, p0)
        ]);
    }
}
alpenglow.register('EdgedFace', EdgedFace);
export class EdgedFaceAccumulator {
    constructor() {
        this.edges = [];
        this.usesEndPoint = true;
    }
    addEdge(startX, startY, endX, endY, startPoint, endPoint) {
        assert && assert(startX !== endX || startY !== endY, 'Points should not be identical');
        this.edges.push(new LinearEdge(startPoint || new Vector2(startX, startY), endPoint || new Vector2(endX, endY)));
    }
    markNewPolygon() {
        // no-op, since we're storing unsorted edges!
    }
    setAccumulationBounds(minX, minY, maxX, maxY) {
        // no-op, since we don't use bounds
    }
    // Will reset it to the initial state also
    finalizeFace() {
        if (this.edges.length === 0) {
            return null;
        }
        const edges = this.edges;
        this.edges = [];
        return new EdgedFace(edges);
    }
    // Will reset without creating a face
    reset() {
        this.edges.length = 0;
    }
}
const scratchAccumulator = new EdgedFaceAccumulator();
//# sourceMappingURL=EdgedFace.js.map