import {
  DiceUtils,
  DiceContract,
  RollValueContract,
  HelperUtils,
} from "@dndbeyond/character-rules-engine/es";
import {
  RollKinds,
  RollTypes,
} from "@dndbeyond/pocket-dimension-dice/constants";
import { parseDiceNotation } from "@dndbeyond/pocket-dimension-dice/helpers/parseDiceNotation";
import { toDiceNotationString } from "@dndbeyond/pocket-dimension-dice/helpers/toDiceNotationString";
import { RollDicePayload } from "@dndbeyond/pocket-dimension-dice/types";

import { DiceComponentUtils, TypeScriptUtils } from "../index";
import {
  hack__generateDiceRollResultTermLookup,
  hack__generateDiceRollResultValueExcludeTypeLookup,
} from "./hacks";

/**
 * Checks for a critical hit
 */
export function isCriticalRoll(result: RollDicePayload["data"]): boolean {
  // Does this roll have a to-hit roll?
  const hitRoll = result.rolls.find((r) => r.rollType == RollTypes.ToHit);
  if (hitRoll) {
    const minRollForCrit: number = 20;
    let numCritRolls: number =
      hitRoll.result?.values.filter((v) => v >= minRollForCrit).length ?? 0;

    let critRoll: boolean = false;
    switch (hitRoll.rollKind) {
      case RollKinds.Disadvantage:
        critRoll = numCritRolls >= 2;
        break;
      case RollKinds.Advantage:
      default:
        critRoll = numCritRolls > 0;
        break;
    }
    return critRoll;
  }
  return false;
}

/**
 * Returns a dice notation string taking into account the current state
 * (doubles damage dice when in critical hit state)
 *
 * @param isCriticalHit Flags if the dice notation is in a critical hit state
 * @param damage Normally displayed damage
 */
export function getDamageDiceNotation(
  damage: number | DiceContract | null,
  isCriticalHit?: boolean
): string {
  if (typeof damage === "number") {
    return damage.toString();
  }

  if (!isCriticalHit) {
    return DiceUtils.renderDice(damage);
  }

  let dn = parseDiceNotation(DiceUtils.renderDice(damage));

  for (let i = 0; i < dn.set.length; i++) {
    // Double the dice on each term
    if (dn.set[i]) {
      dn.set[i]!.count *= 2;
    }
  }

  return toDiceNotationString(dn.set, dn.constant);
}

/**
 *
 * @param dieType
 */
export function getDieTypeValue(dieType: string): number {
  return Number(dieType.substr(1));
}

/**
 * Returns Array<RollValueContract> given the a rollRequestResult with a single RollResult
 *
 * @param rollRequestResult
 */
export function generateRollValueContracts(
  rollRequestResult: RollDicePayload["data"]
): Array<RollValueContract> | null {
  //these hacks make it easier to grab the data from the dice api until the api is updated
  const hack__diceRollResultTermLookup =
    hack__generateDiceRollResultTermLookup(rollRequestResult);
  const hack__diceRollResultValueExcludeTypeLookup =
    hack__generateDiceRollResultValueExcludeTypeLookup(rollRequestResult);

  //RollResult | undefined - RollResult is not exported from Dice;
  //  only expect to have one single rollRequest.
  const rollResult = rollRequestResult.rolls[0].result ?? null;

  if (!rollResult) {
    return null;
  }

  return rollResult.values
    .map((value, idx) => {
      const dieTerm = HelperUtils.lookupDataOrFallback(
        hack__diceRollResultTermLookup,
        idx
      );
      const excludeType = HelperUtils.lookupDataOrFallback(
        hack__diceRollResultValueExcludeTypeLookup,
        idx
      );

      if (!dieTerm || !excludeType) {
        return null;
      }

      return {
        value,
        dieType: DiceComponentUtils.getDieTypeValue(dieTerm.dieType),
        excludeType,
      };
    })
    .filter(TypeScriptUtils.isNotNullOrUndefined);
}
