/* *
 *
 *  (c) 2010-2024 Highsoft AS
 *
 *  License: www.highcharts.com/license
 *
 *  !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
 *
 * */
'use strict';
import Geometry from './GeometryUtilities.js';
const { getAngleBetweenPoints, getCenterOfPoints, getDistanceBetweenPoints } = Geometry;
/* *
 *
 *  Namespace
 *
 * */
var CircleUtilities;
(function (CircleUtilities) {
    /* *
     *
     *  Functions
     *
     * */
    /**
     * @private
     *
     * @param {number} x
     * Number to round
     *
     * @param {number} decimals
     * Number of decimals to round to
     *
     * @return {number}
     * Rounded number
     */
    function round(x, decimals) {
        const a = Math.pow(10, decimals);
        return Math.round(x * a) / a;
    }
    CircleUtilities.round = round;
    /**
     * Calculates the area of a circle based on its radius.
     *
     * @private
     *
     * @param {number} r
     * The radius of the circle.
     *
     * @return {number}
     * Returns the area of the circle.
     */
    function getAreaOfCircle(r) {
        if (r <= 0) {
            throw new Error('radius of circle must be a positive number.');
        }
        return Math.PI * r * r;
    }
    CircleUtilities.getAreaOfCircle = getAreaOfCircle;
    /**
     * Calculates the area of a circular segment based on the radius of the
     * circle and the height of the segment.
     *
     * @see http://mathworld.wolfram.com/CircularSegment.html
     *
     * @private
     *
     * @param {number} r
     * The radius of the circle.
     *
     * @param {number} h
     * The height of the circular segment.
     *
     * @return {number}
     * Returns the area of the circular segment.
     */
    function getCircularSegmentArea(r, h) {
        return (r * r * Math.acos(1 - h / r) -
            (r - h) * Math.sqrt(h * (2 * r - h)));
    }
    CircleUtilities.getCircularSegmentArea = getCircularSegmentArea;
    /**
     * Calculates the area of overlap between two circles based on their
     * radiuses and the distance between them.
     *
     * @see http://mathworld.wolfram.com/Circle-CircleIntersection.html
     *
     * @private
     *
     * @param {number} r1
     * Radius of the first circle.
     *
     * @param {number} r2
     * Radius of the second circle.
     *
     * @param {number} d
     * The distance between the two circles.
     *
     * @return {number}
     * Returns the area of overlap between the two circles.
     */
    function getOverlapBetweenCircles(r1, r2, d) {
        let overlap = 0;
        // If the distance is larger than the sum of the radiuses then the
        // circles does not overlap.
        if (d < r1 + r2) {
            if (d <= Math.abs(r2 - r1)) {
                // If the circles are completely overlapping, then the overlap
                // equals the area of the smallest circle.
                overlap = getAreaOfCircle(r1 < r2 ? r1 : r2);
            }
            else {
                // Height of first triangle segment.
                const d1 = (r1 * r1 - r2 * r2 + d * d) / (2 * d), 
                // Height of second triangle segment.
                d2 = d - d1;
                overlap = (getCircularSegmentArea(r1, r1 - d1) +
                    getCircularSegmentArea(r2, r2 - d2));
            }
            // Round the result to two decimals.
            overlap = round(overlap, 14);
        }
        return overlap;
    }
    CircleUtilities.getOverlapBetweenCircles = getOverlapBetweenCircles;
    /**
     * Calculates the intersection points of two circles.
     *
     * NOTE: does not handle floating errors well.
     *
     * @private
     *
     * @param {Highcharts.CircleObject} c1
     * The first circle.
     *
     * @param {Highcharts.CircleObject} c2
     * The second circle.
     *
     * @return {Array<Highcharts.PositionObject>}
     * Returns the resulting intersection points.
     */
    function getCircleCircleIntersection(c1, c2) {
        const d = getDistanceBetweenPoints(c1, c2), r1 = c1.r, r2 = c2.r;
        let points = [];
        if (d < r1 + r2 && d > Math.abs(r1 - r2)) {
            // If the circles are overlapping, but not completely overlapping,
            // then it exists intersecting points.
            const r1Square = r1 * r1, r2Square = r2 * r2, 
            // `d^2 - r^2 + R^2 / 2d`
            x = (r1Square - r2Square + d * d) / (2 * d), 
            // `y^2 = R^2 - x^2`
            y = Math.sqrt(r1Square - x * x), x1 = c1.x, x2 = c2.x, y1 = c1.y, y2 = c2.y, x0 = x1 + x * (x2 - x1) / d, y0 = y1 + x * (y2 - y1) / d, rx = -(y2 - y1) * (y / d), ry = -(x2 - x1) * (y / d);
            points = [
                { x: round(x0 + rx, 14), y: round(y0 - ry, 14) },
                { x: round(x0 - rx, 14), y: round(y0 + ry, 14) }
            ];
        }
        return points;
    }
    CircleUtilities.getCircleCircleIntersection = getCircleCircleIntersection;
    /**
     * Calculates all the intersection points for between a list of circles.
     *
     * @private
     *
     * @param {Array<Highcharts.CircleObject>} circles
     * The circles to calculate the points from.
     *
     * @return {Array<Highcharts.GeometryObject>}
     * Returns a list of intersection points.
     */
    function getCirclesIntersectionPoints(circles) {
        return circles.reduce((points, c1, i, arr) => {
            const additional = arr
                .slice(i + 1)
                .reduce((points, c2, j) => {
                const indexes = [i, j + i + 1];
                return points.concat(getCircleCircleIntersection(c1, c2).map((p) => {
                    p.indexes = indexes;
                    return p;
                }));
            }, []);
            return points.concat(additional);
        }, []);
    }
    CircleUtilities.getCirclesIntersectionPoints = getCirclesIntersectionPoints;
    /**
     * Tests whether the first circle is completely overlapping the second
     * circle.
     *
     * @private
     *
     * @param {Highcharts.CircleObject} circle1
     * The first circle.
     *
     * @param {Highcharts.CircleObject} circle2
     * The second circle.
     *
     * @return {boolean}
     * Returns true if circle1 is completely overlapping circle2, false if not.
     */
    function isCircle1CompletelyOverlappingCircle2(circle1, circle2) {
        return getDistanceBetweenPoints(circle1, circle2) + circle2.r < circle1.r + 1e-10;
    }
    CircleUtilities.isCircle1CompletelyOverlappingCircle2 = isCircle1CompletelyOverlappingCircle2;
    /**
     * Tests whether a point lies within a given circle.
     * @private
     * @param {Highcharts.PositionObject} point
     * The point to test for.
     *
     * @param {Highcharts.CircleObject} circle
     * The circle to test if the point is within.
     *
     * @return {boolean}
     * Returns true if the point is inside, false if outside.
     */
    function isPointInsideCircle(point, circle) {
        return getDistanceBetweenPoints(point, circle) <= circle.r + 1e-10;
    }
    CircleUtilities.isPointInsideCircle = isPointInsideCircle;
    /**
     * Tests whether a point lies within a set of circles.
     *
     * @private
     *
     * @param {Highcharts.PositionObject} point
     * The point to test.
     *
     * @param {Array<Highcharts.CircleObject>} circles
     * The list of circles to test against.
     *
     * @return {boolean}
     * Returns true if the point is inside all the circles, false if not.
     */
    function isPointInsideAllCircles(point, circles) {
        return !circles.some(function (circle) {
            return !isPointInsideCircle(point, circle);
        });
    }
    CircleUtilities.isPointInsideAllCircles = isPointInsideAllCircles;
    /**
     * Tests whether a point lies outside a set of circles.
     *
     * TODO: add unit tests.
     *
     * @private
     *
     * @param {Highcharts.PositionObject} point
     * The point to test.
     *
     * @param {Array<Highcharts.CircleObject>} circles
     * The list of circles to test against.
     *
     * @return {boolean}
     * Returns true if the point is outside all the circles, false if not.
     */
    function isPointOutsideAllCircles(point, circles) {
        return !circles.some(function (circle) {
            return isPointInsideCircle(point, circle);
        });
    }
    CircleUtilities.isPointOutsideAllCircles = isPointOutsideAllCircles;
    /**
     * Calculates the points for the polygon of the intersection area between
     * a set of circles.
     *
     * @private
     *
     * @param {Array<Highcharts.CircleObject>} circles
     * List of circles to calculate polygon of.
     *
     * @return {Array<Highcharts.GeometryObject>}
     * Return list of points in the intersection polygon.
     */
    function getCirclesIntersectionPolygon(circles) {
        return getCirclesIntersectionPoints(circles)
            .filter(function (p) {
            return isPointInsideAllCircles(p, circles);
        });
    }
    CircleUtilities.getCirclesIntersectionPolygon = getCirclesIntersectionPolygon;
    /**
     * Calculate the path for the area of overlap between a set of circles.
     *
     * @todo handle cases with only 1 or 0 arcs.
     *
     * @private
     *
     * @param {Array<Highcharts.CircleObject>} circles
     * List of circles to calculate area of.
     *
     * @return {Highcharts.GeometryIntersectionObject|undefined}
     * Returns the path for the area of overlap. Returns an empty string if
     * there are no intersection between all the circles.
     */
    function getAreaOfIntersectionBetweenCircles(circles) {
        let intersectionPoints = getCirclesIntersectionPolygon(circles), result;
        if (intersectionPoints.length > 1) {
            // Calculate the center of the intersection points.
            const center = getCenterOfPoints(intersectionPoints);
            intersectionPoints = intersectionPoints
                // Calculate the angle between the center and the points.
                .map(function (p) {
                p.angle = getAngleBetweenPoints(center, p);
                return p;
            })
                // Sort the points by the angle to the center.
                .sort(function (a, b) {
                return b.angle - a.angle;
            });
            const startPoint = intersectionPoints[intersectionPoints.length - 1];
            const arcs = intersectionPoints
                .reduce(function (data, p1) {
                const { startPoint } = data, midPoint = getCenterOfPoints([startPoint, p1]);
                // Calculate the arc from the intersection points and their
                // circles.
                const arc = p1.indexes
                    // Filter out circles that are not included in both
                    // intersection points.
                    .filter(function (index) {
                    return startPoint.indexes.indexOf(index) > -1;
                })
                    // Iterate the circles of the intersection points and
                    // calculate arcs.
                    .reduce(function (arc, index) {
                    const circle = circles[index], angle1 = getAngleBetweenPoints(circle, p1), angle2 = getAngleBetweenPoints(circle, startPoint), angleDiff = angle2 - angle1 +
                        (angle2 < angle1 ? 2 * Math.PI : 0), angle = angle2 - angleDiff / 2;
                    let width = getDistanceBetweenPoints(midPoint, {
                        x: circle.x + circle.r * Math.sin(angle),
                        y: circle.y + circle.r * Math.cos(angle)
                    });
                    const { r } = circle;
                    // Width can sometimes become to large due to
                    // floating point errors
                    if (width > r * 2) {
                        width = r * 2;
                    }
                    // Get the arc with the smallest width.
                    if (!arc || arc.width > width) {
                        arc = {
                            r,
                            largeArc: width > r ? 1 : 0,
                            width,
                            x: p1.x,
                            y: p1.y
                        };
                    }
                    // Return the chosen arc.
                    return arc;
                }, null);
                // If we find an arc then add it to the list and update p2.
                if (arc) {
                    const { r } = arc;
                    data.arcs.push(['A', r, r, 0, arc.largeArc, 1, arc.x, arc.y]);
                    data.startPoint = p1;
                }
                return data;
            }, {
                startPoint: startPoint,
                arcs: []
            }).arcs;
            if (arcs.length === 0) {
                // Empty
            }
            else if (arcs.length === 1) {
                // Empty
            }
            else {
                arcs.unshift(['M', startPoint.x, startPoint.y]);
                result = {
                    center,
                    d: arcs
                };
            }
        }
        return result;
    }
    CircleUtilities.getAreaOfIntersectionBetweenCircles = getAreaOfIntersectionBetweenCircles;
})(CircleUtilities || (CircleUtilities = {}));
/* *
 *
 *  Default Export
 *
 * */
export default CircleUtilities;
