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

export function ds4test(testValue: number, rollOptions: Partial<RollOptions> = {}): RollResult {
    const finalRollValue = testValue;
    if (finalRollValue <= 20) {
        return rollCheckSingleDie(finalRollValue, rollOptions);
    } else {
        return rollCheckMultipleDice(finalRollValue, rollOptions);
    }
}

export function rollCheckSingleDie(
    testValue: number,
    rollOptions: Partial<RollOptions>,
    provider: RollProvider = new DS4RollProvider(),
): RollResult {
    const usedOptions = new DefaultRollOptions().mergeWith(rollOptions);
    const roll = provider.getNextRoll();
    const dice = [roll];

    if (roll <= usedOptions.maxCritSucc) {
        return new RollResult(testValue, RollResultStatus.CRITICAL_SUCCESS, dice);
    } else if (roll >= usedOptions.minCritFail && !isSlayingDiceRepetition(usedOptions)) {
        return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, dice);
    } else {
        if (roll <= testValue) {
            return new RollResult(roll, RollResultStatus.SUCCESS, dice);
        } else {
            return new RollResult(0, RollResultStatus.FAILURE, dice);
        }
    }
}

function separateCriticalHits(dice: Array<number>, usedOptions: RollOptions): [Array<number>, Array<number>] {
    const partitionCallback = (prev: [Array<number>, Array<number>], cur: number) => {
        if (cur <= usedOptions.maxCritSucc) {
            prev[0].push(cur);
        } else {
            prev[1].push(cur);
        }
        return prev;
    };

    const [critSuccesses, otherRolls] = dice
        .reduce(partitionCallback, [[], []])
        .map((a) => a.sort((r1, r2) => r2 - r1));

    return [critSuccesses, otherRolls];
}

export function rollCheckMultipleDice(
    testValue: number,
    rollOptions: Partial<RollOptions>,
    provider: RollProvider = new DS4RollProvider(),
): RollResult {
    const usedOptions = new DefaultRollOptions().mergeWith(rollOptions);
    const finalCheck = testValue % 20;
    const numberOfDice = Math.ceil(testValue / 20);

    const dice = provider.getNextRolls(numberOfDice);

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

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

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

    const swapLastWithCrit: boolean = isDiceSwapNecessary(critSuccesses, otherRolls, finalCheck);

    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 = sortedRollResults
        .map((value, index) => {
            if (index == numberOfDice - 1) {
                if (value <= usedOptions.maxCritSucc) {
                    return finalCheck;
                } else if (value <= finalCheck) {
                    return value;
                } else {
                    return 0;
                }
            } else {
                if (value <= usedOptions.maxCritSucc) {
                    return 20;
                } else {
                    return value;
                }
            }
        })
        .reduce((a, b) => a + b);

    if (usedOptions.useSlayingDice && firstResult <= usedOptions.maxCritSucc) {
        return new RollResult(evaluationResult, RollResultStatus.CRITICAL_SUCCESS, sortedRollResults);
    } else {
        return new RollResult(evaluationResult, RollResultStatus.SUCCESS, sortedRollResults);
    }
}