import { DefaultRollOptions, RollOptions, RollResult, RollResultStatus } from "./roll-data";
import { DS4RollProvider } from "./roll-provider";
import { calculateRollResult, isDiceSwapNecessary, isSlayingDiceRepetition, separateCriticalHits } from "./roll-utils";

/**
 * Performs a roll against a check target number, e.g. for usage in battle, but not for herbs.
 * @param {number} checkTargetValue the final CTN, including all static modifiers.
 * @param {Partial<RollOptions>} rollOptions optional, final option override that affect the checks outcome, e.g. different values for crits or whether slaying dice are used.
 * @param {Array<number>} dice optional, pass already thrown dice that are used instead of rolling new ones.
 */
export function ds4roll(
    checkTargetValue: number,
    rollOptions: Partial<RollOptions> = {},
    dice: Array<number> = null,
): RollResult {
    if (checkTargetValue <= 20) {
        return rollCheckSingleDie(checkTargetValue, rollOptions, dice);
    } else {
        return rollCheckMultipleDice(checkTargetValue, rollOptions, dice);
    }
}

/**
 * Performs a roll against a single die (CTN less than or equal 20).
 *
 * @internal
 * This is not intended for direct usage. Use
 * {@link ds4roll | the function that is not bound to an amount of Dice} instead.
 *
 * @param {number} checkTargetValue - The target value to check against.
 * @param {RollOptions} rollOptions - Options that affect the checks outcome, e.g. different values for crits or whether slaying dice are used.
 * @param {Array<number>} dice optional, pass already thrown dice that are used instead of rolling new ones.
 *
 * @returns {RollResult} An object containing detailed information on the roll result.
 */
export function rollCheckSingleDie(
    checkTargetValue: number,
    rollOptions: Partial<RollOptions>,
    dice: Array<number> = null,
): RollResult {
    const usedOptions = new DefaultRollOptions().mergeWith(rollOptions);

    if (dice == null || dice.length != 1) {
        console.error(`Rolled a dice. Target: ${checkTargetValue}`);
        dice = [new DS4RollProvider().getNextRoll()];
        console.log(dice);
    }
    const usedDice = dice;
    const rolledDie = usedDice[0];

    if (rolledDie <= usedOptions.maxCritSuccess) {
        return new RollResult(checkTargetValue, RollResultStatus.CRITICAL_SUCCESS, usedDice, true);
    } else if (rolledDie >= usedOptions.minCritFail && !isSlayingDiceRepetition(usedOptions)) {
        return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, usedDice, true);
    } else {
        if (rolledDie <= checkTargetValue) {
            return new RollResult(rolledDie, RollResultStatus.SUCCESS, usedDice, true);
        } else {
            return new RollResult(0, RollResultStatus.FAILURE, usedDice, true);
        }
    }
}

/**
 * Performs a roll against a multitude of die (CTN greater than 20).
 *
 * @internal
 * This is not intended for direct usage. Use
 * {@link ds4roll | the function that is not bound to an amount of Dice} instead.
 *
 * @param {number} targetValue- - The target value to check against.
 * @param {RollOptions} rollOptions - Options that affect the checks outcome, e.g. different values for crits or whether slaying dice are used.
 * @param {Array<number>} dice - Optional array of dice values to consider instead of rolling new ones.
 *
 * @returns {RollResult} An object containing detailed information on the roll result.
 */
export function rollCheckMultipleDice(
    targetValue: number,
    rollOptions: Partial<RollOptions>,
    dice: Array<number> = null,
): RollResult {
    const usedOptions = new DefaultRollOptions().mergeWith(rollOptions);
    const remainderTargetValue = targetValue % 20;
    const numberOfDice = Math.ceil(targetValue / 20);

    if (!dice || dice.length != numberOfDice) {
        dice = new DS4RollProvider().getNextRolls(numberOfDice);
    }
    const usedDice = dice;

    const firstResult = usedDice[0];
    const slayingDiceRepetition = isSlayingDiceRepetition(usedOptions);

    // Slaying Dice require a different handling.
    if (firstResult >= usedOptions.minCritFail && !slayingDiceRepetition) {
        return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, usedDice, true);
    }

    const [critSuccesses, otherRolls] = separateCriticalHits(usedDice, usedOptions);

    const swapLastWithCrit: boolean = isDiceSwapNecessary([critSuccesses, otherRolls], remainderTargetValue);

    let sortedRollResults: Array<number>;

    if (swapLastWithCrit) {
        const diceToMove = critSuccesses[0];
        const remainingSuccesses = critSuccesses.slice(1);
        sortedRollResults = remainingSuccesses.concat(otherRolls).concat([diceToMove]);
    } else {
        sortedRollResults = critSuccesses.concat(otherRolls);
    }

    const evaluationResult = calculateRollResult(sortedRollResults, remainderTargetValue, usedOptions);

    if (firstResult <= usedOptions.maxCritSuccess) {
        return new RollResult(evaluationResult, RollResultStatus.CRITICAL_SUCCESS, usedDice, true);
    } else {
        return new RollResult(evaluationResult, RollResultStatus.SUCCESS, usedDice, true);
    }
}