import { randomFromArray } from "../../../core/math/random/randomFromArray.js";
import { seededRandom } from "../../../core/math/random/seededRandom.js";

/**
 * Hill climbing optimizer based on testing random moves one move at a time, if overall fitness improves - that move is committed, otherwise it's rejected
 * Very dumb, but quite effective. Requires very little code to write a competent optimizer, especially when state space is relatively small
 * @template S
 * @class
 */
export class RandomOptimizer {


    /**
     *
     * @type {S|null}
     */
    state = null;

    /**
     *
     * @type {function(S):S}
     */
    cloneState = null;

    /**
     *
     * @type {function(S):Function[]}
     */
    computeValidActions = null;

    /**
     *
     * @type {function(S):number}
     */
    scoreFunction = null;

    /**
     *
     * @type {function(S,random:function():number)|null}
     */
    randomAction = null;


    /**
     *
     * @type {function():number}
     */
    random = seededRandom(0);


    /**
     *
     * @param {S} state
     * @param {function(S):Function[]} [computeValidActions]
     * @param {function(S):S} cloneState
     * @param {function(S):number} scoreFunction
     * @param {function(S,random:function():number)} [randomAction]
     */
    initialize(
        {
            state,
            computeValidActions = null,
            cloneState,
            scoreFunction,
            randomAction = null
        }
    ) {
        if (computeValidActions === null && randomAction === null) {
            throw new Error(`Either computeValidActions or randomAction must be supplied`);
        }

        this.state = state;

        this.computeValidActions = computeValidActions;

        this.cloneState = cloneState;

        this.scoreFunction = scoreFunction;

        this.randomAction = randomAction;
    }

    /**
     * Perform a single optimization step
     * @returns {boolean} True if state was improved, false if no change has occurred
     */
    step() {
        const tempState = this.cloneState(this.state);

        const currentScore = this.scoreFunction(this.state);

        //mutate state
        if (this.randomAction !== null) {
            this.randomAction(tempState, this.random);
        } else {

            const actions = this.computeValidActions(tempState);

            const numActions = actions.length;

            if (numActions === 0) {
                return false;
            }


            const action = randomFromArray(this.random, actions);

            action(tempState);
        }

        const newScore = this.scoreFunction(tempState);

        if (Number.isNaN(newScore)) {
            console.error('Score function returned NaN');

            return false;
        }

        if (newScore <= currentScore) {
            //score not improved
            return false;
        }

        //swap current state with the new one
        this.state = tempState;

        return true;
    }

    /**
     *
     * @param {number} tries
     * @returns {boolean}
     */
    stepThrough(tries) {
        for (let i = 0; i < tries; i++) {
            if (this.step()) {
                return true;
            }
        }

        return false;
    }
}
