const DEFAULT_EPSILON = 0.01;

/**
 * @param {Number[]} steps
 *
 * @returns {String}
 */
function create_key(steps) {
    return steps.join(':');
}

class Daemon {
    /**
     * @type {Number}
     */
    base;

    /**
     * @type {Number}
     */
    humanCount;

    /**
     * @type {Number}
     */
    robotCount;

    /**
     * @type {Number}
     */
    epsilon;

    /**
     * @type {Object}
     */
    weights = {};

    /**
     * @param {Number} base
     * @param {Number} humanCount
     * @param {Number} robotCount
     * @param {Number} epsilon
     */
    constructor(base, humanCount, robotCount, epsilon = DEFAULT_EPSILON) {
        this.base = base;
        this.humanCount = humanCount;
        this.robotCount = robotCount;
        this.epsilon = epsilon;
    }

    /**
     * @returns {Number}
     */
    get power() {
        return this.humanCount + this.robotCount;
    }

    /**
     * @param {Journal} journal
     *
     * @returns {Number}
     */
    predict(journal) {
        const steps = this._getStepSlice(journal);

        const proposals = [];
        for (let i = 0; i < this.base; ++i) {
            proposals[i] = this._getWeight([...steps, i]);
        }

        const maxWeight = Math.max(...proposals);

        return proposals.indexOf(maxWeight);
    }

    /**
     * @param {Journal} journal
     * @param {Number} humanValue
     */
    adjust(journal, humanValue) {
        const steps = this._getStepSlice(journal);
        const adjustmentWeight = this._getAdjustmentWeight(journal.length);
        this._adjustWeight([...steps, humanValue], adjustmentWeight);
    }

    /**
     * @param {Journal} journal
     *
     * @returns {Number[]}
     */
    _getStepSlice(journal) {
        return journal.getLastMovements(this.humanCount, this.robotCount);
    }

    /**
     * @param {Number} stepNumber
     *
     * @returns {Number}
     *
     * @private
     */
    _getAdjustmentWeight(stepNumber) {
        return Math.pow(1 + this.epsilon, stepNumber);
    }

    /**
     * @param {Number[]} steps
     *
     * @returns {Number}
     *
     * @private
     */
    _getWeight(steps) {
        const key = create_key(steps);
        const weight = this.weights[key];
        return weight === undefined ? 0 : weight;
    }

    /**
     * @param {Number[]} steps
     * @param {Number} value
     *
     * @returns {Number}
     *
     * @private
     */
    _setWeight(steps, value) {
        const key = create_key(steps);
        this.weights[key] = value;
    }

    /**
     * @param {Number[]} steps
     * @param {Number} weight
     *
     * @private
     */
    _adjustWeight(steps, weight) {
        const currentWeight = this._getWeight(steps);
        const newWeight = currentWeight + weight;
        this._setWeight(steps, newWeight);
    }
}

export default Daemon;