// SPDX-License-Identifier: Apache-2.0

package chiselTests.experimental.hierarchy

import chiselTests.ChiselFunSpec
import chisel3._
import chisel3.stage.{ChiselCircuitAnnotation, ChiselGeneratorAnnotation, ChiselStage, DesignAnnotation}
import chisel3.experimental.hierarchy.{Definition, Instance}
import chisel3.experimental.hierarchy.ImportDefinitionAnnotation
import firrtl.AnnotationSeq
import firrtl.options.TargetDirAnnotation

import scala.io.Source

class SeparateElaborationSpec extends ChiselFunSpec with Utils {
  import Examples._

  /** Return a [[DesignAnnotation]] from a list of annotations. */
  private def getDesignAnnotation[T <: RawModule](annos: AnnotationSeq): DesignAnnotation[T] = {
    val designAnnos = annos.flatMap { a =>
      a match {
        case a: DesignAnnotation[T] => Some(a)
        case _ => None
      }
    }
    require(designAnnos.length == 1, s"Exactly one DesignAnnotation should exist, but found: $designAnnos.")
    designAnnos.head
  }

  /** Elaborates [[AddOne]] and returns its [[Definition]]. */
  private def getAddOneDefinition(testDir: String): Definition[AddOne] = {
    val dutAnnos = (new ChiselStage).run(
      Seq(
        ChiselGeneratorAnnotation(() => new AddOne),
        TargetDirAnnotation(testDir)
      )
    )

    // Grab DUT definition to pass into testbench
    getDesignAnnotation(dutAnnos).design.asInstanceOf[AddOne].toDefinition
  }

  /** Return [[Definition]]s of all modules in a circuit. */
  private def allModulesToImportedDefs(annos: AnnotationSeq): Seq[ImportDefinitionAnnotation[_]] = {
    annos.flatMap { a =>
      a match {
        case a: ChiselCircuitAnnotation =>
          a.circuit.components.map { c => ImportDefinitionAnnotation(c.id.toDefinition) }
        case _ => Seq.empty
      }
    }
  }

  describe("(0): Name conflicts") {
    it("(0.a): should not occur between a Module and an Instance of a previously elaborated Definition.") {
      val testDir = createTestDirectory(this.getClass.getSimpleName).toString

      val dutDef = getAddOneDefinition(testDir)

      class Testbench(defn: Definition[AddOne]) extends Module {
        val mod = Module(new AddOne)
        val inst = Instance(defn)

        // Tie inputs to a value so ChiselStage does not complain
        mod.in := 0.U
        inst.in := 0.U
        dontTouch(mod.out)
      }

      (new ChiselStage).run(
        Seq(
          ChiselGeneratorAnnotation(() => new Testbench(dutDef)),
          TargetDirAnnotation(testDir),
          ImportDefinitionAnnotation(dutDef)
        )
      )

      val tb_rtl = Source.fromFile(s"$testDir/Testbench.v").getLines.mkString
      tb_rtl should include("module AddOne_1(")
      tb_rtl should include("AddOne_1 mod (")
      (tb_rtl should not).include("module AddOne(")
      tb_rtl should include("AddOne inst (")
    }

    it(
      "(0.b): should not occur between an Instance of a Definition and an Instance of a previously elaborated Definition."
    ) {
      val testDir = createTestDirectory(this.getClass.getSimpleName).toString

      val dutDef = getAddOneDefinition(testDir)

      class Testbench(defn: Definition[AddOne]) extends Module {
        val inst0 = Instance(Definition(new AddOne))
        val inst1 = Instance(defn)

        // Tie inputs to a value so ChiselStage does not complain
        inst0.in := 0.U
        inst1.in := 0.U
        dontTouch(inst0.out)
      }

      (new ChiselStage).run(
        Seq(
          ChiselGeneratorAnnotation(() => new Testbench(dutDef)),
          TargetDirAnnotation(testDir),
          ImportDefinitionAnnotation(dutDef)
        )
      )

      val tb_rtl = Source.fromFile(s"$testDir/Testbench.v").getLines.mkString
      tb_rtl should include("module AddOne_1(")
      tb_rtl should include("AddOne_1 inst0 (")
      (tb_rtl should not).include("module AddOne(")
      tb_rtl should include("AddOne inst1 (")
    }
  }

  describe("(1): Repeat Module definitions") {
    it("(1.a): should not occur when elaborating multiple Instances separately from its Definition.") {
      val testDir = createTestDirectory(this.getClass.getSimpleName).toString

      val dutDef = getAddOneDefinition(testDir)

      class Testbench(defn: Definition[AddOne]) extends Module {
        val inst0 = Instance(defn)
        val inst1 = Instance(defn)

        inst0.in := 0.U
        inst1.in := 0.U
      }

      // If there is a repeat module definition, FIRRTL emission will fail
      (new ChiselStage).emitFirrtl(
        gen = new Testbench(dutDef),
        args = Array("-td", testDir, "--full-stacktrace"),
        annotations = Seq(ImportDefinitionAnnotation(dutDef))
      )
    }
  }

  describe("(2): Multiple imported Definitions of modules without submodules") {
    it(
      "(2.a): should work if a list of imported Definitions is passed between Stages."
    ) {
      val testDir = createTestDirectory(this.getClass.getSimpleName).toString

      val dutAnnos0 = (new ChiselStage).run(
        Seq(
          ChiselGeneratorAnnotation(() => new AddOneParameterized(4)),
          TargetDirAnnotation(s"$testDir/dutDef0")
        )
      )
      val dutDef0 = getDesignAnnotation(dutAnnos0).design.asInstanceOf[AddOneParameterized].toDefinition

      val dutAnnos1 = (new ChiselStage).run(
        Seq(
          ChiselGeneratorAnnotation(() => new AddOneParameterized(8)),
          TargetDirAnnotation(s"$testDir/dutDef1"),
          // pass in previously elaborated Definitions
          ImportDefinitionAnnotation(dutDef0)
        )
      )
      val dutDef1 = getDesignAnnotation(dutAnnos1).design.asInstanceOf[AddOneParameterized].toDefinition

      class Testbench(defn0: Definition[AddOneParameterized], defn1: Definition[AddOneParameterized]) extends Module {
        val inst0 = Instance(defn0)
        val inst1 = Instance(defn1)

        // Tie inputs to a value so ChiselStage does not complain
        inst0.in := 0.U
        inst1.in := 0.U
      }

      (new ChiselStage).run(
        Seq(
          ChiselGeneratorAnnotation(() => new Testbench(dutDef0, dutDef1)),
          TargetDirAnnotation(testDir),
          ImportDefinitionAnnotation(dutDef0),
          ImportDefinitionAnnotation(dutDef1)
        )
      )

      val dutDef0_rtl = Source.fromFile(s"$testDir/dutDef0/AddOneParameterized.v").getLines.mkString
      dutDef0_rtl should include("module AddOneParameterized(")
      val dutDef1_rtl = Source.fromFile(s"$testDir/dutDef1/AddOneParameterized_1.v").getLines.mkString
      dutDef1_rtl should include("module AddOneParameterized_1(")

      val tb_rtl = Source.fromFile(s"$testDir/Testbench.v").getLines.mkString
      tb_rtl should include("AddOneParameterized inst0 (")
      tb_rtl should include("AddOneParameterized_1 inst1 (")
      (tb_rtl should not).include("module AddOneParameterized(")
      (tb_rtl should not).include("module AddOneParameterized_1(")
    }

    it(
      "(2.b): should throw an exception if information is not passed between Stages."
    ) {
      val testDir = createTestDirectory(this.getClass.getSimpleName).toString

      val dutAnnos0 = (new ChiselStage).run(
        Seq(
          ChiselGeneratorAnnotation(() => new AddOneParameterized(4)),
          TargetDirAnnotation(s"$testDir/dutDef0")
        )
      )
      val dutDef0 = getDesignAnnotation(dutAnnos0).design.asInstanceOf[AddOneParameterized].toDefinition

      val dutAnnos1 = (new ChiselStage).run(
        Seq(
          ChiselGeneratorAnnotation(() => new AddOneParameterized(8)),
          TargetDirAnnotation(s"$testDir/dutDef1")
        )
      )
      val dutDef1 = getDesignAnnotation(dutAnnos1).design.asInstanceOf[AddOneParameterized].toDefinition

      class Testbench(defn0: Definition[AddOneParameterized], defn1: Definition[AddOneParameterized]) extends Module {
        val inst0 = Instance(defn0)
        val inst1 = Instance(defn1)

        // Tie inputs to a value so ChiselStage does not complain
        inst0.in := 0.U
        inst1.in := 0.U
      }

      // Because these elaborations have no knowledge of each other, they create
      // modules of the same name
      val dutDef0_rtl = Source.fromFile(s"$testDir/dutDef0/AddOneParameterized.v").getLines.mkString
      dutDef0_rtl should include("module AddOneParameterized(")
      val dutDef1_rtl = Source.fromFile(s"$testDir/dutDef1/AddOneParameterized.v").getLines.mkString
      dutDef1_rtl should include("module AddOneParameterized(")

      val errMsg = intercept[ChiselException] {
        (new ChiselStage).run(
          Seq(
            ChiselGeneratorAnnotation(() => new Testbench(dutDef0, dutDef1)),
            TargetDirAnnotation(testDir),
            ImportDefinitionAnnotation(dutDef0),
            ImportDefinitionAnnotation(dutDef1)
          )
        )
      }
      errMsg.getMessage should include(
        "Expected distinct imported Definition names but found duplicates for: AddOneParameterized"
      )
    }
  }

  describe("(3): Multiple imported Definitions of modules with submodules") {
    it(
      "(3.a): should work if a list of imported Definitions for all modules is passed between Stages."
    ) {
      val testDir = createTestDirectory(this.getClass.getSimpleName).toString

      val dutAnnos0 = (new ChiselStage).run(
        Seq(
          ChiselGeneratorAnnotation(() => new AddTwoMixedModules),
          TargetDirAnnotation(s"$testDir/dutDef0")
        )
      )
      val dutDef0 = getDesignAnnotation(dutAnnos0).design.asInstanceOf[AddTwoMixedModules].toDefinition
      val importDefinitionAnnos0 = allModulesToImportedDefs(dutAnnos0)

      val dutAnnos1 = (new ChiselStage).run(
        Seq(
          ChiselGeneratorAnnotation(() => new AddTwoMixedModules),
          TargetDirAnnotation(s"$testDir/dutDef1")
        ) ++ importDefinitionAnnos0
      )
      val dutDef1 = getDesignAnnotation(dutAnnos1).design.asInstanceOf[AddTwoMixedModules].toDefinition
      val importDefinitionAnnos1 = allModulesToImportedDefs(dutAnnos1)

      class Testbench(defn0: Definition[AddTwoMixedModules], defn1: Definition[AddTwoMixedModules]) extends Module {
        val inst0 = Instance(defn0)
        val inst1 = Instance(defn1)

        // Tie inputs to a value so ChiselStage does not complain
        inst0.in := 0.U
        inst1.in := 0.U
      }

      val dutDef0_rtl = Source.fromFile(s"$testDir/dutDef0/AddTwoMixedModules.v").getLines.mkString
      dutDef0_rtl should include("module AddOne(")
      dutDef0_rtl should include("module AddTwoMixedModules(")
      val dutDef1_rtl = Source.fromFile(s"$testDir/dutDef1/AddTwoMixedModules_1.v").getLines.mkString
      dutDef1_rtl should include("module AddOne_2(")
      dutDef1_rtl should include("module AddTwoMixedModules_1(")

      (new ChiselStage).run(
        Seq(
          ChiselGeneratorAnnotation(() => new Testbench(dutDef0, dutDef1)),
          TargetDirAnnotation(testDir)
        ) ++ importDefinitionAnnos0 ++ importDefinitionAnnos1
      )

      val tb_rtl = Source.fromFile(s"$testDir/Testbench.v").getLines.mkString
      tb_rtl should include("AddTwoMixedModules inst0 (")
      tb_rtl should include("AddTwoMixedModules_1 inst1 (")
      (tb_rtl should not).include("module AddTwoMixedModules(")
      (tb_rtl should not).include("module AddTwoMixedModules_1(")
    }
  }

  it(
    "(3.b): should throw an exception if submodules are not passed between Definition elaborations."
  ) {
    val testDir = createTestDirectory(this.getClass.getSimpleName).toString

    val dutAnnos0 = (new ChiselStage).run(
      Seq(
        ChiselGeneratorAnnotation(() => new AddTwoMixedModules),
        TargetDirAnnotation(s"$testDir/dutDef0")
      )
    )
    val dutDef0 = getDesignAnnotation(dutAnnos0).design.asInstanceOf[AddTwoMixedModules].toDefinition
    val importDefinitionAnnos0 = allModulesToImportedDefs(dutAnnos0)

    val dutAnnos1 = (new ChiselStage).run(
      Seq(
        ChiselGeneratorAnnotation(() => new AddTwoMixedModules),
        ImportDefinitionAnnotation(dutDef0),
        TargetDirAnnotation(s"$testDir/dutDef1")
      )
    )
    val dutDef1 = getDesignAnnotation(dutAnnos1).design.asInstanceOf[AddTwoMixedModules].toDefinition
    val importDefinitionAnnos1 = allModulesToImportedDefs(dutAnnos1)

    class Testbench(defn0: Definition[AddTwoMixedModules], defn1: Definition[AddTwoMixedModules]) extends Module {
      val inst0 = Instance(defn0)
      val inst1 = Instance(defn1)

      // Tie inputs to a value so ChiselStage does not complain
      inst0.in := 0.U
      inst1.in := 0.U
    }

    val dutDef0_rtl = Source.fromFile(s"$testDir/dutDef0/AddTwoMixedModules.v").getLines.mkString
    dutDef0_rtl should include("module AddOne(")
    dutDef0_rtl should include("module AddTwoMixedModules(")
    val dutDef1_rtl = Source.fromFile(s"$testDir/dutDef1/AddTwoMixedModules_1.v").getLines.mkString
    dutDef1_rtl should include("module AddOne(")
    dutDef1_rtl should include("module AddTwoMixedModules_1(")

    val errMsg = intercept[ChiselException] {
      (new ChiselStage).run(
        Seq(
          ChiselGeneratorAnnotation(() => new Testbench(dutDef0, dutDef1)),
          TargetDirAnnotation(testDir)
        ) ++ importDefinitionAnnos0 ++ importDefinitionAnnos1
      )
    }
    errMsg.getMessage should include(
      "Expected distinct imported Definition names but found duplicates for: AddOne"
    )
  }

  describe("(4): With ExtMod Names") {
    it("(4.a): should pick correct ExtMod names when passed") {
      val testDir = createTestDirectory(this.getClass.getSimpleName).toString

      val dutDef = getAddOneDefinition(testDir)

      class Testbench(defn: Definition[AddOne]) extends Module {
        val mod = Module(new AddOne)
        val inst = Instance(defn)

        // Tie inputs to a value so ChiselStage does not complain
        mod.in := 0.U
        inst.in := 0.U
        dontTouch(mod.out)
      }

      (new ChiselStage).run(
        Seq(
          ChiselGeneratorAnnotation(() => new Testbench(dutDef)),
          TargetDirAnnotation(testDir),
          ImportDefinitionAnnotation(dutDef, Some("CustomPrefix_AddOne_CustomSuffix"))
        )
      )

      val tb_rtl = Source.fromFile(s"$testDir/Testbench.v").getLines.mkString

      tb_rtl should include("module AddOne_1(")
      tb_rtl should include("AddOne_1 mod (")
      (tb_rtl should not).include("module AddOne(")
      tb_rtl should include("CustomPrefix_AddOne_CustomSuffix inst (")
    }
  }

  it(
    "(4.b): should work if a list of imported Definitions is passed between Stages with ExtModName."
  ) {
    val testDir = createTestDirectory(this.getClass.getSimpleName).toString

    val dutAnnos0 = (new ChiselStage).run(
      Seq(
        ChiselGeneratorAnnotation(() => new AddOneParameterized(4)),
        TargetDirAnnotation(s"$testDir/dutDef0")
      )
    )
    val dutDef0 = getDesignAnnotation(dutAnnos0).design.asInstanceOf[AddOneParameterized].toDefinition

    val dutAnnos1 = (new ChiselStage).run(
      Seq(
        ChiselGeneratorAnnotation(() => new AddOneParameterized(8)),
        TargetDirAnnotation(s"$testDir/dutDef1"),
        // pass in previously elaborated Definitions
        ImportDefinitionAnnotation(dutDef0)
      )
    )
    val dutDef1 = getDesignAnnotation(dutAnnos1).design.asInstanceOf[AddOneParameterized].toDefinition

    class Testbench(defn0: Definition[AddOneParameterized], defn1: Definition[AddOneParameterized]) extends Module {
      val inst0 = Instance(defn0)
      val inst1 = Instance(defn1)

      // Tie inputs to a value so ChiselStage does not complain
      inst0.in := 0.U
      inst1.in := 0.U
    }

    (new ChiselStage).run(
      Seq(
        ChiselGeneratorAnnotation(() => new Testbench(dutDef0, dutDef1)),
        TargetDirAnnotation(testDir),
        ImportDefinitionAnnotation(dutDef0, Some("Inst1_Prefix_AddOnePramaterized_Inst1_Suffix")),
        ImportDefinitionAnnotation(dutDef1, Some("Inst2_Prefix_AddOnePrameterized_1_Inst2_Suffix"))
      )
    )

    val dutDef0_rtl = Source.fromFile(s"$testDir/dutDef0/AddOneParameterized.v").getLines.mkString
    dutDef0_rtl should include("module AddOneParameterized(")
    val dutDef1_rtl = Source.fromFile(s"$testDir/dutDef1/AddOneParameterized_1.v").getLines.mkString
    dutDef1_rtl should include("module AddOneParameterized_1(")

    val tb_rtl = Source.fromFile(s"$testDir/Testbench.v").getLines.mkString
    tb_rtl should include("Inst1_Prefix_AddOnePramaterized_Inst1_Suffix inst0 (")
    tb_rtl should include("Inst2_Prefix_AddOnePrameterized_1_Inst2_Suffix inst1 (")
    (tb_rtl should not).include("module AddOneParameterized(")
    (tb_rtl should not).include("module AddOneParameterized_1(")
  }

  it(
    "(4.c): should throw an exception  if a list of imported Definitions is passed between Stages with same ExtModName."
  ) {
    val testDir = createTestDirectory(this.getClass.getSimpleName).toString

    val dutAnnos0 = (new ChiselStage).run(
      Seq(
        ChiselGeneratorAnnotation(() => new AddOneParameterized(4)),
        TargetDirAnnotation(s"$testDir/dutDef0")
      )
    )
    val importDefinitionAnnos0 = allModulesToImportedDefs(dutAnnos0)
    val dutDef0 = getDesignAnnotation(dutAnnos0).design.asInstanceOf[AddOneParameterized].toDefinition

    val dutAnnos1 = (new ChiselStage).run(
      Seq(
        ChiselGeneratorAnnotation(() => new AddOneParameterized(8)),
        TargetDirAnnotation(s"$testDir/dutDef1"),
        // pass in previously elaborated Definitions
        ImportDefinitionAnnotation(dutDef0)
      )
    )
    val importDefinitionAnnos1 = allModulesToImportedDefs(dutAnnos1)
    val dutDef1 = getDesignAnnotation(dutAnnos1).design.asInstanceOf[AddOneParameterized].toDefinition

    class Testbench(defn0: Definition[AddOneParameterized], defn1: Definition[AddOneParameterized]) extends Module {
      val inst0 = Instance(defn0)
      val inst1 = Instance(defn1)

      // Tie inputs to a value so ChiselStage does not complain
      inst0.in := 0.U
      inst1.in := 0.U
    }

    val dutDef0_rtl = Source.fromFile(s"$testDir/dutDef0/AddOneParameterized.v").getLines.mkString
    dutDef0_rtl should include("module AddOneParameterized(")
    val dutDef1_rtl = Source.fromFile(s"$testDir/dutDef1/AddOneParameterized_1.v").getLines.mkString
    dutDef1_rtl should include("module AddOneParameterized_1(")

    val errMsg = intercept[ChiselException] {
      (new ChiselStage).run(
        Seq(
          ChiselGeneratorAnnotation(() => new Testbench(dutDef0, dutDef1)),
          TargetDirAnnotation(testDir),
          ImportDefinitionAnnotation(dutDef0, Some("Inst1_Prefix_AddOnePrameterized_Inst1_Suffix")),
          ImportDefinitionAnnotation(dutDef1, Some("Inst1_Prefix_AddOnePrameterized_Inst1_Suffix"))
        )
      )
    }
    errMsg.getMessage should include(
      "Expected distinct overrideDef names but found duplicates for: Inst1_Prefix_AddOnePrameterized_Inst1_Suffix"
    )
  }
}
