/**
 * @author Alex Goldring 14/05/2018 - Ported to JS using JSweet + manual editing
 * @author Kaspar Fischer (hbf) 09/05/2013
 * @source https://github.com/hbf/miniball
 * @generated Generated from Java with JSweet 2.2.0-SNAPSHOT - http://www.jsweet.org
 */


import { assert } from "../../../assert.js";
import { sqr } from "../../../math/sqr.js";
import { vector_dot } from "../../vec/vector_dot.js";
import { Subspan } from "./Subspan.js";

/**
 * Small number
 * @type {number}
 */
const Epsilon = 1e-14;

/**
 * Limit to how many iterations the algorithm is allowed to perform, this is to cover poorly converging cases
 * @type {number}
 */
const MAX_ITERATIONS = 10000;

/**
 * Computes the miniball of the given point set.
 *
 * Based on paper "Fast Smallest-Enclosing-Ball Computation in High Dimensions" by "Kaspar Fischer" et. al.
 * @copyright Company Named Limited (c) 2026
 */
export class Miniball {

    /**
     *
     * @type {number}
     */
    iteration = 0;

    /**
     *
     * @type {number}
     */
    distToAff = 0;

    /**
     *
     * @type {number}
     */
    distToAffSquare = 0;

    /**
     *
     * The squared radius of the miniball.
     *
     * This is equivalent to `radius() * radius()`.
     *
     * Precondition: `!isEmpty()`
     *
     * @type {number}
     * @private
     */
    __squaredRadius = 0;

    /**
     *
     * @type {number}
     * @private
     */
    __radius = 0;

    /**
     *
     * @type {number}
     */
    stopper = 0;

    /**
     * Notice that the point set `points` is assumed to be immutable during the computation.
     * That is, if you add, remove, or change points in the point set, you have to create a new instance of {@link Miniball}.
     *
     * @param {PointSet} points the point set
     */
    constructor(points) {
        assert.defined(points, 'points');


        /**
         * @type {PointSet}
         */
        this.S = points;

        /**
         *
         * @type {number}
         * @private
         */
        this.__size = this.S.size();

        const dimension_count = this.S.dimension();

        /**
         * Number of dimensions (2 for 2d, 3 for 3d etc.)
         * @type {number}
         */
        this.dim = dimension_count;


        // pre-allocated continuous chunk of memory to make execution faster by reducing cache miss rate
        const buffer = new ArrayBuffer(8 * (dimension_count * 4 + 1));

        /**
         *
         * @type {number[]|Float64Array}
         * @private
         */
        this.__center = new Float64Array(buffer, 0, dimension_count);

        /**
         *
         * @type {number[]|Float64Array}
         */
        this.centerToAff = new Float64Array(buffer, 8 * dimension_count, dimension_count);

        /**
         *
         * @type {number[]|Float64Array}
         */
        this.centerToPoint = new Float64Array(buffer, 8 * 2 * dimension_count, dimension_count);

        /**
         *
         * @type {number[]|Float64Array}
         */
        this.lambdas = new Float64Array(buffer, 8 * 3 * dimension_count, dimension_count + 1);

        /**
         *
         * @type {Subspan}
         * @private
         */
        this.__support = this.initBall();
        this.compute();
    }

    /**
     * Whether the miniball is the empty set, equivalently, whether `points.size() == 0`
     * was true when this miniball instance was constructed.
     *
     * Notice that the miniball of a point set <i>S</i> is empty if and only if <i>S={}</i>.
     *
     * @return {boolean} true iff
     */
    isEmpty() {
        return this.__size === 0;
    }

    /**
     * The radius of the miniball.
     *
     * Precondition: `!isEmpty()`
     *
     * @return {number} the radius of the miniball, a number ≥ 0
     */
    radius() {
        return this.__radius;
    }

    /**
     * The Euclidean coordinates of the center of the miniball.
     *
     * Precondition: `!isEmpty()`
     *
     * @return {number[]} an array holding the coordinates of the center of the miniball
     */
    center() {
        return this.__center;
    }

    /**
     *
     * @returns {Subspan}
     */
    get support() {
        return this.__support;
    }

    /**
     * The number of input points.
     *
     * @return {number} the number of points in the original point set, i.e., `pts.size()` where
     * `pts` was the {@link PointSet} instance passed to the constructor of this
     * instance
     */
    size() {
        return this.__size;
    }

    /**
     * Sets up the search ball with an arbitrary point of <i>S</i> as center and with exactly one of
     * the points farthest from center in the support. So the current ball contains all points of
     * <i>S</i> and has radius at most twice as large as the minball.
     *
     * Precondition: `size > 0`
     * @return {Subspan}
     * @private
     */
    initBall() {
        let i, j;

        const dim = this.dim;

        const center = this.__center;

        const pointSet = this.S;

        for (i = 0; i < dim; ++i) {
            center[i] = pointSet.coord(0, i);
        }

        this.__squaredRadius = 0;

        let farthest = 0;

        const numPoints = pointSet.size();

        for (j = 1; j < numPoints; ++j) {
            let dist = 0;

            for (i = 0; i < dim; ++i) {
                dist += sqr(pointSet.coord(j, i) - center[i]);
            }

            if (dist >= this.__squaredRadius) {
                this.__squaredRadius = dist;
                farthest = j;
            }
        }

        this.__radius = Math.sqrt(this.__squaredRadius);
        return new Subspan(this.dim, pointSet, farthest);
    }

    /**
     * @private
     */
    computeDistToAff() {
        this.distToAffSquare = this.__support.shortestVectorToSpan(this.__center, this.centerToAff);
        this.distToAff = Math.sqrt(this.distToAffSquare);
    }

    /**
     * @private
     */
    updateRadius() {
        const any = this.__support.anyMember();
        this.__squaredRadius = 0;

        const dim = this.dim;

        const center = this.__center;
        const points = this.S;

        for (let i = 0; i < dim; ++i) {
            this.__squaredRadius += sqr(points.coord(any, i) - center[i]);
        }

        this.__radius = Math.sqrt(this.__squaredRadius);
    }

    /**
     * The main function containing the main loop.
     *
     * Iteratively, we compute the point in support that is closest to the current center and then
     * walk towards this target as far as we can, i.e., we move until some new point touches the
     * boundary of the ball and must thus be inserted into support.
     *
     * In each of these two alternating phases, we always have to check whether some point must be dropped from support,
     * which is the case when the center lies in <i>aff(support)</i>.
     * If such an attempt to drop fails, we are done; because then the center lies even <i>conv(support)</i>.
     * @private
     */
    compute() {
        const center = this.__center;
        const support = this.__support;
        const dim = this.dim;
        const centerToAff = this.centerToAff;

        for (this.iteration = 0; this.iteration < MAX_ITERATIONS; this.iteration++) {

            this.computeDistToAff();

            while (
                this.distToAff <= Epsilon * this.__radius
                || support.size() === dim + 1
                ) {
                if (!this.successfulDrop()) {
                    // done
                    return;
                }
                this.computeDistToAff();
            }

            const scale = this.findStopFraction();

            if (this.stopper >= 0) {

                for (let i = 0; i < dim; ++i) {
                    center[i] += scale * centerToAff[i];
                }

                this.updateRadius();
                support.add(this.stopper);
            } else {

                for (let i = 0; i < dim; ++i) {
                    center[i] += centerToAff[i];
                }

                this.updateRadius();
                if (!this.successfulDrop()) {
                    return;
                }
            }
        }

    }

    /**
     * If center doesn't already lie in <i>conv(support)</i> and is thus not optimal yet,
     * {@link successfulDrop()} elects a suitable point <i>k</i> to be removed from the support and
     * returns true. If the center lies in the convex hull, however, false is returned (and the
     * support remains unaltered).
     *
     * Precondition: center lies in <i>aff(support)</i>.
     * @return {boolean}
     */
    successfulDrop() {
        const lambdas = this.lambdas;
        const support = this.__support;

        support.findAffineCoefficients(this.__center, lambdas);

        let smallest = 0;
        let minimum = 1;

        const support_set_size = support.size();

        for (let i = 0; i < support_set_size; ++i) {
            const lambda = lambdas[i];

            if (lambda < minimum) {
                minimum = lambda;
                smallest = i;
            }
        }

        if (minimum <= 0) {
            support.remove(smallest);
            return true;
        }

        return false;
    }

    /**
     * Given the center of the current enclosing ball and the walking direction `centerToAff`,
     * determine how much we can walk into this direction without losing a point from <i>S</i>.
     *
     * The (positive) factor by which we can walk along `centerToAff` is returned.
     * Further, `stopper` is set to the index of the most restricting point and to -1 if no such point was found.
     * @return {number}
     * @private
     */
    findStopFraction() {
        let scale = 1;

        this.stopper = -1;

        let i, j;

        const dim = this.dim;

        const center = this.__center;
        const pointSet = this.S;

        const support = this.__support;

        const centerToPoint = this.centerToPoint;
        const centerToAff = this.centerToAff;

        const distToAffSquare = this.distToAffSquare;
        const squaredRadius = this.__squaredRadius;

        const size = this.__size;
        const distance_error = Epsilon * this.__radius * this.distToAff;

        for (j = 0; j < size; ++j) {

            if (support.isMember(j)) {
                continue;
            }

            for (i = 0; i < dim; ++i) {
                // read out a single point, offset by center
                centerToPoint[i] = pointSet.coord(j, i) - center[i];
            }

            const dirPointProd = vector_dot(centerToAff, centerToPoint, dim);

            if ((distToAffSquare - dirPointProd) < distance_error) {
                continue;
            }

            const bound_sq = vector_dot(centerToPoint, centerToPoint, dim);

            const bound = (squaredRadius - bound_sq) / 2 / (distToAffSquare - dirPointProd);

            if (bound > 0 && bound < scale) {
                //if (com.dreizak.miniball.highdim.Logging.log)
                //   com.dreizak.miniball.highdim.Logging.debug("found stopper " + j + " bound=" + bound + " scale=" + scale);
                scale = bound;
                this.stopper = j;
            }

        }

        return scale;
    }


    /**
     * Outputs information about the Miniball
     * @return {string}
     */
    toString() {
        let s = "Miniball [";

        if (this.isEmpty()) {

            s += "isEmpty=true";

        } else {

            s += "center=(";

            for (let i = 0; i < this.dim; ++i) {
                s += (this.__center[i]);
                if (i < this.dim - 1)
                    s += (", ");
            }

            s += `), radius=${this.__radius}, squaredRadius=${this.__squaredRadius}`;

        }

        s += "]";

        return s;
    }
}