// SPDX-License-Identifier: Apache-2.0

package firrtlTests.execution

import firrtl._
import firrtl.ir._

object DUTRules {
  val dutName = "dut"
  val clock = Reference("clock", ClockType)
  val reset = Reference("reset", Utils.BoolType)
  val counter = Reference("step", UnknownType)

  // Need a flat name for the register that latches poke values
  val illegal = raw"[\[\]\.]".r
  val pokeRegSuffix = "_poke"
  def pokeRegName(e: Expression) = illegal.replaceAllIn(e.serialize, "_") + pokeRegSuffix

  // Naming patterns are static, so DUT has to be checked for proper form + collisions
  def hasNameConflicts(c: Circuit): Boolean = {
    val top = c.modules.find(_.name == c.main).get
    val names = Namespace(top).cloneUnderlying
    names.contains(counter.name) || names.exists(_.contains(pokeRegSuffix))
  }
}

object ExecutionTestHelper {
  val counterType = UIntType(IntWidth(32))
  def apply(body: String): ExecutionTestHelper = {
    // Parse input and check that it complies with test syntax rules
    val c = ParseStatement.makeDUT(body)
    require(!DUTRules.hasNameConflicts(c), "Avoid using 'step' or 'poke' in DUT component names")

    // Generate test step counter, create ExecutionTestHelper that represents initial test state
    val cnt = DefRegister(NoInfo, DUTRules.counter.name, counterType, DUTRules.clock, DUTRules.reset, Utils.zero)
    val inc =
      Connect(NoInfo, DUTRules.counter, DoPrim(PrimOps.Add, Seq(DUTRules.counter, UIntLiteral(1)), Nil, UnknownType))
    ExecutionTestHelper(c, Seq(cnt, inc), Map.empty[Expression, Expression], Nil, Nil)
  }
}

case class ExecutionTestHelper(
  dut:            Circuit,
  setup:          Seq[Statement],
  pokeRegs:       Map[Expression, Expression],
  completedSteps: Seq[Conditionally],
  activeStep:     Seq[Statement]) {

  def step(n: Int): ExecutionTestHelper = {
    require(n > 0, "Step length must be positive")
    (0 until n).foldLeft(this) { case (eth, int) => eth.next }
  }

  def poke(expString: String, value: Literal): ExecutionTestHelper = {
    val pokeExp = ParseExpression(expString)
    val pokeable = ensurePokeable(pokeExp)
    pokeable.addStatements(Connect(NoInfo, pokeExp, value), Connect(NoInfo, pokeable.pokeRegs(pokeExp), value))
  }

  def invalidate(expString: String): ExecutionTestHelper = {
    addStatements(IsInvalid(NoInfo, ParseExpression(expString)))
  }

  def expect(expString: String, value: Literal): ExecutionTestHelper = {
    val peekExp = ParseExpression(expString)
    val neq = DoPrim(PrimOps.Neq, Seq(peekExp, value), Nil, Utils.BoolType)
    addStatements(Stop(NoInfo, 1, DUTRules.clock, neq))
  }

  def finish(): ExecutionTestHelper = {
    addStatements(Stop(NoInfo, 0, DUTRules.clock, Utils.one)).next
  }

  // Private helper methods

  private def t = completedSteps.length

  private def addStatements(stmts: Statement*) = copy(activeStep = activeStep ++ stmts)

  private def next: ExecutionTestHelper = {
    val count = Reference(DUTRules.counter.name, DUTRules.counter.tpe)
    val ifStep = DoPrim(PrimOps.Eq, Seq(count, UIntLiteral(t)), Nil, Utils.BoolType)
    val onThisStep = Conditionally(NoInfo, ifStep, Block(activeStep), EmptyStmt)
    copy(completedSteps = completedSteps :+ onThisStep, activeStep = Nil)
  }

  private def top: Module = {
    dut.modules.collectFirst({ case m: Module if m.name == dut.main => m }).get
  }

  private[execution] def emit: Circuit = {
    val finished = finish()
    val modulesX = dut.modules.collect {
      case m: Module if m.name == dut.main =>
        m.copy(body = Block(m.body +: (setup ++ finished.completedSteps)))
      case m => m
    }
    dut.copy(modules = modulesX)
  }

  private def ensurePokeable(pokeExp: Expression): ExecutionTestHelper = {
    if (pokeRegs.contains(pokeExp)) {
      this
    } else {
      val pName = DUTRules.pokeRegName(pokeExp)
      val pRef = Reference(pName, UnknownType)
      val pReg = DefRegister(NoInfo, pName, UIntType(UnknownWidth), DUTRules.clock, Utils.zero, pRef)
      val defaultConn = Connect(NoInfo, pokeExp, pRef)
      copy(setup = setup ++ Seq(pReg, defaultConn), pokeRegs = pokeRegs + (pokeExp -> pRef))
    }
  }
}
