// SPDX-License-Identifier: Apache-2.0

package examples

import chiselTests.ChiselFlatSpec
import chisel3.testers.BasicTester
import chisel3._
import chisel3.util._

import VendingMachineUtils._

class VendingMachineIO(val legalCoins: Seq[Coin]) extends Bundle {
  require(legalCoins.size >= 1, "The vending machine must accept at least 1 coin!")
  // Order of coins by value
  val coins: Seq[Coin] = legalCoins.sortBy(_.value)
  // Map of coin names to their relative position in value (ie. index in inputs)
  val indexMap: Map[String, Int] = coins.map(_.name).zipWithIndex.toMap

  require(
    coins.map(_.value % coins.head.value == 0).reduce(_ && _),
    "All coins must be a multiple of the lowest value coin!"
  )

  val inputs = Input(Vec(legalCoins.size, Bool()))
  val dispense = Output(Bool())

  def apply(coin: String): Unit = {
    val idx = indexMap(coin)
    inputs(idx) := true.B
  }
}

// Superclass for parameterized vending machines
abstract class ParameterizedVendingMachine(legalCoins: Seq[Coin], val sodaCost: Int) extends Module {
  val io = IO(new VendingMachineIO(legalCoins))
  // Enforce one hot
  if (io.inputs.size > 1) {
    for (input <- io.inputs) {
      when(input) {
        assert(io.inputs.filterNot(_ == input).map(!_).reduce(_ && _), "Only 1 coin can be input in a given cycle!")
      }
    }
  }
}

class VendingMachineGenerator(
  legalCoins: Seq[Coin],
  sodaCost:   Int)
    extends ParameterizedVendingMachine(legalCoins, sodaCost) {
  require(sodaCost > 0, "Sodas must actually cost something!")

  // All coin values are normalized to a multiple of the minimum coin value
  val minCoin = io.coins.head.value
  val maxCoin = io.coins.last.value
  val maxValue = (sodaCost + maxCoin - minCoin) / minCoin // normalize to minimum value

  val width = log2Ceil(maxValue + 1).W
  val value = RegInit(0.asUInt(width))
  val incValue = WireDefault(0.asUInt(width))
  val doDispense = value >= (sodaCost / minCoin).U

  when(doDispense) {
    value := 0.U // No change given
  }.otherwise {
    value := value + incValue
  }

  for ((coin, index) <- io.coins.zipWithIndex) {
    when(io.inputs(index)) { incValue := (coin.value / minCoin).U }
  }
  io.dispense := doDispense
}

class ParameterizedVendingMachineTester(
  mod:        => ParameterizedVendingMachine,
  testLength: Int)
    extends BasicTester {
  require(testLength > 0, "Test length must be positive!")

  // Construct the module
  val dut = Module(mod)
  val coins = dut.io.coins

  // Inputs and expected results
  // Do random testing
  private val _rand = scala.util.Random
  val inputs:   Seq[Option[Coin]] = Seq.fill(testLength)(coins.lift(_rand.nextInt(coins.size + 1)))
  val expected: Seq[Boolean] = getExpectedResults(inputs, dut.sodaCost)

  val inputVec: Vec[UInt] = VecInit(inputs.map {
    case Some(coin) => (1 << dut.io.indexMap(coin.name)).asUInt(coins.size.W)
    case None       => 0.asUInt(coins.size.W)
  })
  val expectedVec: Vec[Bool] = VecInit(expected.map(_.B))

  val (idx, done) = Counter(true.B, testLength + 1)
  when(done) { stop(); stop() } // Two stops for Verilator

  dut.io.inputs := inputVec(idx).asBools
  assert(dut.io.dispense === expectedVec(idx))
}

class VendingMachineGeneratorSpec extends ChiselFlatSpec {
  behavior.of("The vending machine generator")

  it should "generate a vending machine that accepts only nickels and dimes and costs $0.20" in {
    val coins = Seq(Nickel, Dime)
    assertTesterPasses {
      new ParameterizedVendingMachineTester(new VendingMachineGenerator(coins, 20), 100)
    }
  }
  it should "generate a vending machine that only accepts one kind of coin" in {
    val coins = Seq(Nickel)
    assertTesterPasses {
      new ParameterizedVendingMachineTester(new VendingMachineGenerator(coins, 30), 100)
    }
  }
  it should "generate a more realistic vending machine that costs $1.50" in {
    val coins = Seq(Penny, Nickel, Dime, Quarter)
    assertTesterPasses {
      new ParameterizedVendingMachineTester(new VendingMachineGenerator(coins, 150), 100)
    }
  }
  it should "generate a Harry Potter themed vending machine" in {
    val coins = Seq(Knut, Sickle) // Galleons are worth too much
    assertTesterPasses {
      new ParameterizedVendingMachineTester(new VendingMachineGenerator(coins, Galleon.value), 100)
    }
  }
}
