import { log, LogTypes as LT } from '@Log4Deno';

import { CustomDiceShapes, RollConf } from 'artigen/dice/dice.d.ts';

import { DiceOptions, NumberlessDiceOptions } from 'artigen/dice/rollOptions.ts';

import { getLoopCount, loopCountCheck } from 'artigen/managers/loopManager.ts';

import { loggingEnabled } from 'artigen/utils/logFlag.ts';
import { addToRange, gtrAddToRange, ltAddToRange } from 'artigen/utils/rangeAdder.ts';

const throwDoubleSepError = (sep: string): void => {
  throw new Error(`DoubleSeparator_${sep}`);
};

// Converts a rollStr into a machine readable rollConf
export const getRollConf = (rollStr: string, customTypes: CustomDiceShapes = new Map<string, number[]>()): RollConf => {
  // Split the roll on the die size (and the drop if its there)
  const dPts = rollStr.split('d');

  // Initialize the configuration to store the parsed data
  const rollConf: RollConf = {
    type: '',
    customType: null,
    dieCount: 0,
    dieSize: 0,
    dPercent: {
      on: false,
      sizeAdjustment: 0,
      critVal: 0,
    },
    drop: {
      on: false,
      count: 0,
    },
    keep: {
      on: false,
      count: 0,
    },
    dropHigh: {
      on: false,
      count: 0,
    },
    keepLow: {
      on: false,
      count: 0,
    },
    reroll: {
      on: false,
      once: false,
      nums: [],
    },
    critScore: {
      on: false,
      range: [],
    },
    critFail: {
      on: false,
      range: [],
    },
    exploding: {
      on: false,
      once: false,
      compounding: false,
      penetrating: false,
      nums: [],
    },
    match: {
      on: false,
      minCount: 2,
      returnTotal: false,
    },
    sort: {
      on: false,
      direction: '',
    },
    success: {
      on: false,
      range: [],
    },
    fail: {
      on: false,
      range: [],
    },
  };

  // If the dPts is not long enough, throw error
  if (dPts.length < 2) {
    throw new Error(`YouNeedAD_${rollStr}`);
  }

  // Fill out the die count, first item will either be an int or empty string, short circuit execution will take care of replacing the empty string with a 1
  const rawDC = dPts.shift() || '1';
  if (rawDC.includes('.')) {
    throw new Error('WholeDieCountSizeOnly');
  }
  const tempDC = rawDC.replace(/\D/g, '');
  const numberlessRawDC = rawDC.replace(/\d/g, '');
  if (!tempDC && !numberlessRawDC) {
    throw new Error(`CannotParseDieCount_${rawDC}`);
  }
  // Rejoin all remaining parts
  let remains = dPts.join('d');

  loggingEnabled && log(LT.LOG, `Initial breaking of rollStr ${rawDC} ${tempDC} ${dPts} ${remains}`);

  // Manual Parsing for custom roll types
  if (rawDC.endsWith('cwo')) {
    // CWOD dice parsing
    rollConf.type = 'cwod';

    // Get CWOD parts, setting count and getting difficulty
    const cwodParts = rollStr.split('cwod');
    rollConf.dieCount = parseInt(cwodParts[0] || '1');
    rollConf.dieSize = 10;

    // Use success to set the difficulty
    rollConf.success.on = true;
    rollConf.fail.on = true;
    addToRange('cwod', rollConf.fail.range, 1);
    const tempDifficulty = (cwodParts[1] ?? '').search(/\d/) === 0 ? cwodParts[1] : '';
    let afterDifficultyIdx = tempDifficulty.search(/[^\d]/);
    if (afterDifficultyIdx === -1) {
      afterDifficultyIdx = tempDifficulty.length;
    }
    const difficulty = parseInt(tempDifficulty.slice(0, afterDifficultyIdx) || '10');

    for (let i = difficulty; i <= rollConf.dieSize; i++) {
      loopCountCheck('getRollConf.ts - setting cwod difficulty');

      loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling cwod ${rollStr} | Parsing difficulty ${i}`);
      rollConf.success.range.push(i);
    }

    // Remove any garbage from the remains
    remains = remains.slice(afterDifficultyIdx);
  } else if (rawDC.endsWith('ova')) {
    // OVA dice parsing
    rollConf.type = 'ova';

    // Get OVA parts, setting count and getting difficulty
    const ovaParts = rollStr.split('ovad');
    const tempOvaPart1 = (ovaParts[1] ?? '').search(/\d/) === 0 ? ovaParts[1] : '';
    if (tempOvaPart1.search(/\d+\.\d/) === 0) {
      throw new Error('WholeDieCountSizeOnly');
    }
    rollConf.dieCount = parseInt(ovaParts[0] || '1');

    let afterOvaSizeIdx = tempOvaPart1.search(/[^\d]/);
    if (afterOvaSizeIdx === -1) {
      afterOvaSizeIdx = tempOvaPart1.length;
    }
    rollConf.dieSize = parseInt(tempOvaPart1.slice(0, afterOvaSizeIdx) || '6');

    // Remove any garbage from the remains
    remains = remains.slice(afterOvaSizeIdx);
  } else if (remains.startsWith('f')) {
    // fate dice setup
    rollConf.type = 'fate';
    rollConf.dieCount = parseInt(tempDC);
    // dieSize set to 1 as 1 is max face value, a six sided die is used internally
    rollConf.dieSize = 1;

    // remove F from the remains
    remains = remains.slice(1);
  } else if (customTypes.has(numberlessRawDC)) {
    // custom dice setup
    rollConf.type = 'custom';
    rollConf.customType = numberlessRawDC;
    rollConf.dieCount = isNaN(parseInt(tempDC ?? '1')) ? 1 : parseInt(tempDC ?? '1');
    rollConf.dieSize = Math.max(...(customTypes.get(numberlessRawDC) ?? []));
  } else {
    // roll20 dice setup
    rollConf.type = 'roll20';
    rollConf.dieCount = parseInt(tempDC);

    // Finds the end of the die size/beginning of the additional options
    let afterDieIdx = dPts[0].search(/[^%\d]/);
    if (afterDieIdx === -1) {
      afterDieIdx = dPts[0].length;
    }

    // Get the die size out of the remains and into the rollConf
    const rawDS = remains.slice(0, afterDieIdx);
    remains = remains.slice(afterDieIdx);

    if (rawDS.startsWith('%')) {
      rollConf.dieSize = 10;
      rollConf.dPercent.on = true;
      const percentCount = rawDS.match(/%/g)?.length ?? 1;
      rollConf.dPercent.sizeAdjustment = Math.pow(10, percentCount - 1);
      rollConf.dPercent.critVal = Math.pow(10, percentCount) - rollConf.dPercent.sizeAdjustment;
    } else {
      rollConf.dieSize = parseInt(rawDS);
    }

    if (remains.search(/\.\d/) === 0) {
      throw new Error('WholeDieCountSizeOnly');
    }
  }

  loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Parsed Die Count: ${rollConf.dieCount}`);
  loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Parsed Die Size: ${rollConf.dieSize}`);
  loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | remains: ${remains}`);

  if (!rollConf.dieCount || !rollConf.dieSize) {
    throw new Error(`YouNeedAD_${rollStr}`);
  }

  // Finish parsing the roll
  if (remains.length > 0) {
    // Determine if the first item is a drop, and if it is, add the d back in
    if (remains.search(/\D/) > 0 || remains.indexOf('l') === 0 || remains.indexOf('h') === 0) {
      remains = `d${remains}`;
    }

    // Loop until all remaining args are parsed
    while (remains.length > 0) {
      loopCountCheck('getRollConf.ts - parsing rollConf');

      loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Parsing remains ${remains}`);
      // Find the next number in the remains to be able to cut out the rule name
      let afterSepIdx = remains.search(/[-\d]/);
      if (afterSepIdx < 0) {
        afterSepIdx = remains.length;
      }

      // Determine if afterSepIdx needs to be moved up (cases like mt! or !mt)
      const tempSep = remains.slice(0, afterSepIdx);
      loggingEnabled && log(LT.LOG, `tempSep: ${tempSep}`);

      let noNumberAfter = false;
      if (!(Object.values(DiceOptions) as string[]).includes(tempSep)) {
        NumberlessDiceOptions.some((opt) => {
          loopCountCheck('getRollConf.ts - parsing numberlessDiceOptions');
          loggingEnabled && log(LT.LOG, `In NumberlessDiceOptions ${opt} ${tempSep.startsWith(opt) && tempSep !== opt}`);
          if (tempSep.startsWith(opt) && tempSep !== opt) {
            afterSepIdx = opt.length;
            noNumberAfter = true;
            return true;
          }
          return tempSep === opt;
        });
      }

      // Save the rule name to tSep and remove it from remains
      const tSep = remains.slice(0, afterSepIdx);
      remains = remains.slice(afterSepIdx);
      loggingEnabled && log(LT.LOG, `tSep: ${tSep}, remains: ${remains}`);

      // Find the next non-number in the remains to be able to cut out the count/num
      let afterNumIdx = noNumberAfter ? 0 : remains.search(/(?![-\d])/);
      if (afterNumIdx < 0) {
        afterNumIdx = remains.length;
      }
      // Save the count/num to tNum leaving it in remains for the time being
      const tNum = parseInt(remains.slice(0, afterNumIdx));

      loggingEnabled && log(LT.LOG, `${getLoopCount()} tSep: ${tSep} ${afterSepIdx}, tNum: ${tNum} ${afterNumIdx}`);

      // Switch on rule name
      switch (tSep) {
        case DiceOptions.Drop:
        case DiceOptions.DropLow:
          if (rollConf.drop.on) {
            // Ensure we do not override existing settings
            throwDoubleSepError(tSep);
          }
          // Configure Drop (Lowest)
          rollConf.drop.on = true;
          rollConf.drop.count = tNum;
          break;
        case DiceOptions.Keep:
        case DiceOptions.KeepHigh:
          if (rollConf.keep.on) {
            // Ensure we do not override existing settings
            throwDoubleSepError(tSep);
          }
          // Configure Keep (Highest)
          rollConf.keep.on = true;
          rollConf.keep.count = tNum;
          break;
        case DiceOptions.DropHigh:
          if (rollConf.dropHigh.on) {
            // Ensure we do not override existing settings
            throwDoubleSepError(tSep);
          }
          // Configure Drop (Highest)
          rollConf.dropHigh.on = true;
          rollConf.dropHigh.count = tNum;
          break;
        case DiceOptions.KeepLow:
          if (rollConf.keepLow.on) {
            // Ensure we do not override existing settings
            throwDoubleSepError(tSep);
          }
          // Configure Keep (Lowest)
          rollConf.keepLow.on = true;
          rollConf.keepLow.count = tNum;
          break;
        case DiceOptions.RerollOnce:
        case DiceOptions.RerollOnceEqu:
          rollConf.reroll.once = true;
        // falls through as ro/ro= functions the same as r/r= in this context
        case DiceOptions.Reroll:
        case DiceOptions.RerollEqu:
          // Configure Reroll (this can happen multiple times)
          rollConf.reroll.on = true;
          addToRange(tSep, rollConf.reroll.nums, tNum);
          break;
        case DiceOptions.RerollOnceGtr:
          rollConf.reroll.once = true;
        // falls through as ro> functions the same as r> in this context
        case DiceOptions.RerollGtr:
          // Configure reroll for all numbers greater than or equal to tNum (this could happen multiple times, but why)
          rollConf.reroll.on = true;
          gtrAddToRange(tSep, rollConf.reroll.nums, tNum, rollConf.dieSize);
          break;
        case DiceOptions.RerollOnceLt:
          rollConf.reroll.once = true;
        // falls through as ro< functions the same as r< in this context
        case DiceOptions.RerollLt:
          // Configure reroll for all numbers less than or equal to tNum (this could happen multiple times, but why)
          rollConf.reroll.on = true;
          ltAddToRange(tSep, rollConf.reroll.nums, tNum, rollConf.type);
          break;
        case DiceOptions.CritSuccess:
        case DiceOptions.CritSuccessEqu:
          // Configure CritScore for one number (this can happen multiple times)
          rollConf.critScore.on = true;
          addToRange(tSep, rollConf.critScore.range, tNum);
          break;
        case DiceOptions.CritSuccessGtr:
          // Configure CritScore for all numbers greater than or equal to tNum (this could happen multiple times, but why)
          rollConf.critScore.on = true;
          gtrAddToRange(tSep, rollConf.critScore.range, tNum, rollConf.dieSize);
          break;
        case DiceOptions.CritSuccessLt:
          // Configure CritScore for all numbers less than or equal to tNum (this could happen multiple times, but why)
          rollConf.critScore.on = true;
          ltAddToRange(tSep, rollConf.critScore.range, tNum, rollConf.type);
          break;
        case DiceOptions.CritFail:
        case DiceOptions.CritFailEqu:
          // Configure CritFail for one number (this can happen multiple times)
          rollConf.critFail.on = true;
          addToRange(tSep, rollConf.critFail.range, tNum);
          break;
        case DiceOptions.CritFailGtr:
          // Configure CritFail for all numbers greater than or equal to tNum (this could happen multiple times, but why)
          rollConf.critFail.on = true;
          gtrAddToRange(tSep, rollConf.critFail.range, tNum, rollConf.dieSize);
          break;
        case DiceOptions.CritFailLt:
          // Configure CritFail for all numbers less than or equal to tNum (this could happen multiple times, but why)
          rollConf.critFail.on = true;
          ltAddToRange(tSep, rollConf.critFail.range, tNum, rollConf.type);
          break;
        case DiceOptions.Exploding:
        case DiceOptions.ExplodeOnce:
        case DiceOptions.PenetratingExplosion:
        case DiceOptions.CompoundingExplosion:
          // Configure Exploding
          rollConf.exploding.on = true;
          if (afterNumIdx > 0) {
            // User gave a number to explode on, save it
            addToRange(tSep, rollConf.exploding.nums, tNum);
          }
          break;
        case DiceOptions.ExplodingEqu:
        case DiceOptions.ExplodeOnceEqu:
        case DiceOptions.PenetratingExplosionEqu:
        case DiceOptions.CompoundingExplosionEqu:
          // Configure Exploding (this can happen multiple times)
          rollConf.exploding.on = true;
          addToRange(tSep, rollConf.exploding.nums, tNum);
          break;
        case DiceOptions.ExplodingGtr:
        case DiceOptions.ExplodeOnceGtr:
        case DiceOptions.PenetratingExplosionGtr:
        case DiceOptions.CompoundingExplosionGtr:
          // Configure Exploding for all numbers greater than or equal to tNum (this could happen multiple times, but why)
          rollConf.exploding.on = true;
          gtrAddToRange(tSep, rollConf.exploding.nums, tNum, rollConf.dieSize);
          break;
        case DiceOptions.ExplodingLt:
        case DiceOptions.ExplodeOnceLt:
        case DiceOptions.PenetratingExplosionLt:
        case DiceOptions.CompoundingExplosionLt:
          // Configure Exploding for all numbers less than or equal to tNum (this could happen multiple times, but why)
          rollConf.exploding.on = true;
          ltAddToRange(tSep, rollConf.exploding.nums, tNum, rollConf.type);
          break;
        case DiceOptions.MatchingTotal:
          rollConf.match.returnTotal = true;
        // falls through as mt functions the same as m in this context
        case DiceOptions.Matching:
          if (rollConf.match.on) {
            // Ensure we do not override existing settings
            throwDoubleSepError(tSep);
          }
          rollConf.match.on = true;
          if (afterNumIdx > 0) {
            // User gave a number to work with, save it
            rollConf.match.minCount = tNum;
          }
          break;
        case DiceOptions.Sort:
        case DiceOptions.SortAsc:
          if (rollConf.sort.on) {
            // Ensure we do not override existing settings
            throwDoubleSepError(tSep);
          }
          rollConf.sort.on = true;
          rollConf.sort.direction = 'a';
          break;
        case DiceOptions.SortDesc:
          if (rollConf.sort.on) {
            // Ensure we do not override existing settings
            throwDoubleSepError(tSep);
          }
          rollConf.sort.on = true;
          rollConf.sort.direction = 'd';
          break;
        case DiceOptions.SuccessEqu:
          // Configure success (this can happen multiple times)
          rollConf.success.on = true;
          addToRange(tSep, rollConf.success.range, tNum);
          break;
        case DiceOptions.SuccessGtr:
          // Configure success for all numbers greater than or equal to tNum (this could happen multiple times, but why)
          rollConf.success.on = true;
          gtrAddToRange(tSep, rollConf.success.range, tNum, rollConf.dieSize);
          break;
        case DiceOptions.SuccessLt:
          // Configure success for all numbers less than or equal to tNum (this could happen multiple times, but why)
          rollConf.success.on = true;
          ltAddToRange(tSep, rollConf.success.range, tNum, rollConf.type);
          break;
        case DiceOptions.Fail:
        case DiceOptions.FailEqu:
          // Configure fail (this can happen multiple times)
          rollConf.fail.on = true;
          addToRange(tSep, rollConf.fail.range, tNum);
          break;
        case DiceOptions.FailGtr:
          // Configure fail for all numbers greater than or equal to tNum (this could happen multiple times, but why)
          rollConf.fail.on = true;
          gtrAddToRange(tSep, rollConf.fail.range, tNum, rollConf.dieSize);
          break;
        case DiceOptions.FailLt:
          // Configure fail for all numbers less than or equal to tNum (this could happen multiple times, but why)
          rollConf.fail.on = true;
          ltAddToRange(tSep, rollConf.fail.range, tNum, rollConf.type);
          break;
        default:
          // Throw error immediately if unknown op is encountered
          throw new Error(`UnknownOperation_${tSep}`);
      }

      // Followup switch to avoid weird duplicated code
      switch (tSep) {
        case DiceOptions.ExplodeOnce:
        case DiceOptions.ExplodeOnceLt:
        case DiceOptions.ExplodeOnceGtr:
        case DiceOptions.ExplodeOnceEqu:
          rollConf.exploding.once = true;
          break;
        case DiceOptions.PenetratingExplosion:
        case DiceOptions.PenetratingExplosionLt:
        case DiceOptions.PenetratingExplosionGtr:
        case DiceOptions.PenetratingExplosionEqu:
          rollConf.exploding.penetrating = true;
          break;
        case DiceOptions.CompoundingExplosion:
        case DiceOptions.CompoundingExplosionLt:
        case DiceOptions.CompoundingExplosionGtr:
        case DiceOptions.CompoundingExplosionEqu:
          rollConf.exploding.compounding = true;
          break;
      }

      // Finally slice off everything else parsed this loop
      remains = remains.slice(afterNumIdx);
    }
  }

  loggingEnabled && log(LT.LOG, `RollConf before cleanup: ${JSON.stringify(rollConf)}`);

  // Verify the parse, throwing errors for every invalid config
  if (rollConf.dieCount < 0) {
    throw new Error('NoZerosAllowed_base');
  }
  if (rollConf.dieCount === 0 || rollConf.dieSize === 0) {
    throw new Error('NoZerosAllowed_base');
  }

  // Since only one drop or keep option can be active, count how many are active to throw the right error
  let dkdkCnt = 0;
  [rollConf.drop.on, rollConf.keep.on, rollConf.dropHigh.on, rollConf.keepLow.on].forEach((e) => {
    loggingEnabled && log(LT.LOG, `Handling ${rollConf.type} ${rollStr} | Checking if drop/keep is on ${e}`);
    if (e) {
      dkdkCnt++;
    }
  });
  if (dkdkCnt > 1) {
    throw new Error('FormattingError_dk');
  }

  if (rollConf.match.on && (rollConf.success.on || rollConf.fail.on)) {
    throw new Error('FormattingError_mtsf');
  }

  if (rollConf.drop.on && rollConf.drop.count === 0) {
    throw new Error('NoZerosAllowed_drop');
  }
  if (rollConf.keep.on && rollConf.keep.count === 0) {
    throw new Error('NoZerosAllowed_keep');
  }
  if (rollConf.dropHigh.on && rollConf.dropHigh.count === 0) {
    throw new Error('NoZerosAllowed_dropHigh');
  }
  if (rollConf.keepLow.on && rollConf.keepLow.count === 0) {
    throw new Error('NoZerosAllowed_keepLow');
  }

  // Filter rollConf num lists to only include valid numbers
  const validNumFilter = (curNum: number) => {
    if (rollConf.type === 'fate') {
      return [-1, 0, 1].includes(curNum);
    }
    return curNum <= rollConf.dieSize && curNum > (rollConf.dPercent.on ? -1 : 0);
  };
  rollConf.reroll.nums = rollConf.reroll.nums.filter(validNumFilter);
  rollConf.critScore.range = rollConf.critScore.range.filter(validNumFilter);
  rollConf.critFail.range = rollConf.critFail.range.filter(validNumFilter);
  rollConf.exploding.nums = rollConf.exploding.nums.filter(validNumFilter);
  rollConf.success.range = rollConf.success.range.filter(validNumFilter);
  rollConf.fail.range = rollConf.fail.range.filter(validNumFilter);

  if (rollConf.reroll.on && rollConf.reroll.nums.length === (rollConf.type === 'fate' ? 3 : rollConf.dieSize)) {
    throw new Error('NoRerollOnAllSides');
  }

  loggingEnabled && log(LT.LOG, `RollConf after cleanup: ${JSON.stringify(rollConf)}`);

  return rollConf;
};
