/*
 * Greedy solver of the coin change problem, based on the following CHICKEN
 * implementation: https://git.sr.ht/~siiky/coin-change
 */

/*
 * prepare_denominations([[d0, count], [d1, count], ...])
 * => [{ denom, count, csum }, ...]
 */
const prepare_denominations = denominations =>
  JSON.parse(JSON.stringify(denominations))
    .sort(([d1], [d2]) => d2 - d1)
    .reduce(
      ([csum, denoms], [denom, count]) => {
        csum += denom * count
        denoms.push({ denom, count, csum })
        return [csum, denoms]
      },
      [0, []],
    )[1] /* ([csum, denoms]) => denoms */

const max_denomination_multiplicity = (denom, count, target) =>
  Math.min(count, Math.floor(target / denom))

/*
 * @returns null if there's no solution set;
 *          false if there's no solution;
 *          solution if there's a solution
 */
const memo_get = (memo, target, denom) => {
  const denom_solutions = memo[target]
  if (denom_solutions === undefined) return null
  const solution = denom_solutions[denom]
  return solution === undefined ? null : solution
}

const memo_set = (memo, target, denom, solution) => {
  let denom_solutions = memo[target]
  if (denom_solutions === undefined) memo[target] = denom_solutions = {}
  return (denom_solutions[denom] = solution)
}

const check = (solution, target) =>
  !solution ||
  target ===
    solution.reduce((sum, [denom, provisioned]) => sum + denom * provisioned, 0)

const model = denominations => ({
  denominations: prepare_denominations(denominations),
  memo: {},
})

/*
 * target :: Int
 * denominations :: [[d0, count], [d1, count], ...]
 *
 * @returns [[d0, provisioned], [d1, provisioned], ... ];
 *          false if there's no solution.
 */
const solve = (model, target) => {
  const { denominations, memo } = model

  const coin_change = (didx, target) => {
    if (target === 0) return []

    for (; didx < denominations.length; didx++) {
      const { denom, count, csum } = denominations[didx]

      /*
       * There's no solution if the target is greater than the cumulative sum
       * of the denominations, or if the target is not divisible by any of the
       * denominations
       */
      if (target > csum) return memo_set(memo, target, denom, false)

      let solution = memo_get(memo, target, denom)
      if (solution === false) continue /* not here, keep looking */
      if (solution) return solution /* we've previously computed a solution */

      /* solution === null */
      for (
        let nd = max_denomination_multiplicity(denom, count, target);
        nd >= 0;
        nd--
      ) {
        solution = coin_change(didx + 1, target - denom * nd)
        if (solution)
          return memo_set(memo, target, denom, [[denom, nd]].concat(solution))
      }

      memo_set(memo, target, denom, false)
    }

    return false
  }

  return coin_change(0, target)
}

module.exports = {
  check,
  model,
  solve,
}
