/* *
 *
 *  !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
 *
 * */

/**
 * Internal types
 * @private
 */
declare global {
    namespace Highcharts {
        interface NelderMeadMixin {
            getCentroid(simplex: Array<NelderMeadPointArray>): Array<number>;
            nelderMead(
                fn: NelderMeadTestFunction,
                initial: NelderMeadPointArray
            ): NelderMeadPointArray;
        }
        interface NelderMeadPointArray extends Array<number> {
            fx: number;
        }
        interface NelderMeadTestFunction {
            (point: NelderMeadPointArray): number;
        }
    }
}
interface NelderMeadCentroidObject {
    i: number;
    sum: number;
}

/* eslint-disable valid-jsdoc */

const getCentroid = function (
    simplex: Array<Highcharts.NelderMeadPointArray>
): Array<number> {
    const arr = simplex.slice(0, -1),
        length = arr.length,
        result = [] as Array<number>,
        sum = function (
            data: NelderMeadCentroidObject,
            point: Array<number>
        ): NelderMeadCentroidObject {
            data.sum += point[data.i];
            return data;
        };

    for (let i = 0; i < length; i++) {
        result[i] = arr.reduce(sum, { sum: 0, i: i }).sum / length;
    }
    return result;
};

/**
 * Finds an optimal position for a given point.
 * @todo add unit tests.
 * @todo add constraints to optimize the algorithm.
 * @private
 * @param {Highcharts.NelderMeadTestFunction} fn
 *        The function to test a point.
 * @param {Highcharts.NelderMeadPointArray} initial
 *        The initial point to optimize.
 * @return {Highcharts.NelderMeadPointArray}
 *         Returns the opimized position of a point.
 */
const nelderMead = function nelderMead(
    fn: Highcharts.NelderMeadTestFunction,
    initial: Highcharts.NelderMeadPointArray
): Highcharts.NelderMeadPointArray {
    const maxIterations = 100,
        sortByFx = function (
            a: Highcharts.NelderMeadPointArray,
            b: Highcharts.NelderMeadPointArray
        ): number {
            return a.fx - b.fx;
        },
        pRef = 1, // Reflection parameter
        pExp = 2, // Expansion parameter
        pCon = -0.5, // Contraction parameter
        pOCon = pCon * pRef, // Outwards contraction parameter
        pShrink = 0.5; // Shrink parameter

    /**
     * @private
     */
    const weightedSum = function weightedSum(
        weight1: number,
        v1: Array<number>,
        weight2: number,
        v2: Array<number>
    ): Array<number> {
        return v1.map(function (x: number, i: number): number {
            return weight1 * x + weight2 * v2[i];
        });
    };

    /**
     * @private
     */
    const getSimplex = function getSimplex(
        initial: Highcharts.NelderMeadPointArray
    ): Array<Highcharts.NelderMeadPointArray> {
        const n = initial.length,
            simplex: Array<Highcharts.NelderMeadPointArray> =
                new Array(n + 1);

        // Initial point to the simplex.
        simplex[0] = initial;
        simplex[0].fx = fn(initial);

        // Create a set of extra points based on the initial.
        for (let i = 0; i < n; ++i) {
            const point = initial.slice() as Highcharts.NelderMeadPointArray;

            point[i] = point[i] ? point[i] * 1.05 : 0.001;
            point.fx = fn(point);
            simplex[i + 1] = point;
        }
        return simplex;
    };

    const updateSimplex = function (
        simplex: Array<Highcharts.NelderMeadPointArray>,
        point: Highcharts.NelderMeadPointArray
    ): Array<Highcharts.NelderMeadPointArray> {
        point.fx = fn(point);
        simplex[simplex.length - 1] = point;
        return simplex;
    };

    const shrinkSimplex = function (
        simplex: Array<Highcharts.NelderMeadPointArray>
    ): Array<Highcharts.NelderMeadPointArray> {
        const best = simplex[0];

        return simplex.map(function (
            point: Highcharts.NelderMeadPointArray
        ): Highcharts.NelderMeadPointArray {
            const p = weightedSum(1 - pShrink, best, pShrink, point) as (
                Highcharts.NelderMeadPointArray
            );

            p.fx = fn(p);
            return p;
        });
    };

    const getPoint = function (
        centroid: Array<number>,
        worst: Array<number>,
        a: number,
        b: number
    ): Highcharts.NelderMeadPointArray {
        const point = weightedSum(a, centroid, b, worst) as (
            Highcharts.NelderMeadPointArray
        );

        point.fx = fn(point);
        return point;
    };

    // Create a simplex
    let simplex = getSimplex(initial);

    // Iterate from 0 to max iterations
    for (let i = 0; i < maxIterations; i++) {
        // Sort the simplex
        simplex.sort(sortByFx);

        // Create a centroid from the simplex
        const worst = simplex[simplex.length - 1];
        const centroid = getCentroid(simplex);

        // Calculate the reflected point.
        const reflected = getPoint(centroid, worst, 1 + pRef, -pRef);

        if (reflected.fx < simplex[0].fx) {
            // If reflected point is the best, then possibly expand.
            const expanded = getPoint(centroid, worst, 1 + pExp, -pExp);

            simplex = updateSimplex(
                simplex,
                (expanded.fx < reflected.fx) ? expanded : reflected
            );
        } else if (reflected.fx >= simplex[simplex.length - 2].fx) {
            // If the reflected point is worse than the second worse, then
            // contract.
            let contracted;

            if (reflected.fx > worst.fx) {
                // If the reflected is worse than the worst point, do a
                // contraction
                contracted = getPoint(centroid, worst, 1 + pCon, -pCon);
                if (contracted.fx < worst.fx) {
                    simplex = updateSimplex(simplex, contracted);
                } else {
                    simplex = shrinkSimplex(simplex);
                }
            } else {
                // Otherwise do an outwards contraction
                contracted = getPoint(centroid, worst, 1 - pOCon, pOCon);
                if (contracted.fx < reflected.fx) {
                    simplex = updateSimplex(simplex, contracted);
                } else {
                    simplex = shrinkSimplex(simplex);
                }
            }
        } else {
            simplex = updateSimplex(simplex, reflected);
        }
    }

    return simplex[0];
};

const nelderMeadMixin: Highcharts.NelderMeadMixin = {
    getCentroid: getCentroid,
    nelderMead: nelderMead
};

export default nelderMeadMixin;
