// SPDX-License-Identifier: Apache-2.0

package firrtlTests.analyses

import firrtl.PrimOps.AsUInt
import firrtl.analyses.IRLookup
import firrtl.annotations.{CircuitTarget, ModuleTarget, ReferenceTarget}
import firrtl._
import firrtl.ir._
import firrtl.options.Dependency
import firrtl.passes.ExpandWhensAndCheck
import firrtl.stage.{Forms, TransformManager}
import firrtl.testutils.FirrtlFlatSpec

class IRLookupSpec extends FirrtlFlatSpec {

  "IRLookup" should "return declarations" in {
    val input =
      """circuit Test:
        |  module Test :
        |    input in: UInt<8>
        |    input clk: Clock
        |    input reset: UInt<1>
        |    output out: {a: UInt<8>, b: UInt<8>[2]}
        |    input ana1: Analog<8>
        |    output ana2: Analog<8>
        |    out is invalid
        |    reg r: UInt<8>, clk with:
        |      (reset => (reset, UInt(0)))
        |    node x = r
        |    wire y: UInt<8>
        |    y <= x
        |    out.b[0] <= and(y, asUInt(SInt(-1)))
        |    attach(ana1, ana2)
        |    inst child of Child
        |    out.a <= child.out
        |  module Child:
        |    output out: UInt<8>
        |    out <= UInt(1)
        |""".stripMargin

    val circuit = new firrtl.stage.transforms.Compiler(Seq(Dependency[ExpandWhensAndCheck]))
      .runTransform(
        CircuitState(parse(input), UnknownForm)
      )
      .circuit
    val irLookup = IRLookup(circuit)
    val Test = ModuleTarget("Test", "Test")
    val uint8 = UIntType(IntWidth(8))

    irLookup.declaration(Test.ref("in")) shouldBe Port(NoInfo, "in", Input, uint8)
    irLookup.declaration(Test.ref("clk")) shouldBe Port(NoInfo, "clk", Input, ClockType)
    irLookup.declaration(Test.ref("reset")) shouldBe Port(NoInfo, "reset", Input, UIntType(IntWidth(1)))

    val out = Port(
      NoInfo,
      "out",
      Output,
      BundleType(Seq(Field("a", Default, uint8), Field("b", Default, VectorType(uint8, 2))))
    )
    irLookup.declaration(Test.ref("out")) shouldBe out
    irLookup.declaration(Test.ref("out").field("a")) shouldBe out
    irLookup.declaration(Test.ref("out").field("b").index(0)) shouldBe out
    irLookup.declaration(Test.ref("out").field("b").index(1)) shouldBe out

    irLookup.declaration(Test.ref("ana1")) shouldBe Port(NoInfo, "ana1", Input, AnalogType(IntWidth(8)))
    irLookup.declaration(Test.ref("ana2")) shouldBe Port(NoInfo, "ana2", Output, AnalogType(IntWidth(8)))

    val clk = WRef("clk", ClockType, PortKind, SourceFlow)
    val reset = WRef("reset", UIntType(IntWidth(1)), PortKind, SourceFlow)
    val init = UIntLiteral(0)
    val reg = DefRegister(NoInfo, "r", uint8, clk, reset, init)
    irLookup.declaration(Test.ref("r")) shouldBe reg
    irLookup.declaration(Test.ref("r").clock) shouldBe reg
    irLookup.declaration(Test.ref("r").reset) shouldBe reg
    irLookup.declaration(Test.ref("r").init) shouldBe reg
    irLookup.kindFinder(Test, RegKind) shouldBe Seq(Test.ref("r"))
    irLookup.declaration(Test.ref("x")) shouldBe DefNode(NoInfo, "x", WRef("r", uint8, RegKind, SourceFlow))
    irLookup.declaration(Test.ref("y")) shouldBe DefWire(NoInfo, "y", uint8)

    irLookup.declaration(Test.ref("@and#0")) shouldBe
      DoPrim(
        PrimOps.And,
        Seq(WRef("y", uint8, WireKind, SourceFlow), DoPrim(AsUInt, Seq(SIntLiteral(-1)), Nil, UIntType(IntWidth(1)))),
        Nil,
        uint8
      )

    val inst = WDefInstance(NoInfo, "child", "Child", BundleType(Seq(Field("out", Default, uint8))))
    irLookup.declaration(Test.ref("child")) shouldBe inst
    irLookup.declaration(Test.ref("child").field("out")) shouldBe inst
    irLookup.declaration(Test.instOf("child", "Child").ref("out")) shouldBe Port(NoInfo, "out", Output, uint8)

    intercept[IllegalArgumentException] { irLookup.declaration(Test.instOf("child", "Child").ref("missing")) }
    intercept[IllegalArgumentException] { irLookup.declaration(Test.instOf("child", "Missing").ref("out")) }
    intercept[IllegalArgumentException] { irLookup.declaration(Test.instOf("missing", "Child").ref("out")) }
    intercept[IllegalArgumentException] { irLookup.declaration(Test.ref("missing")) }
    intercept[IllegalArgumentException] { irLookup.declaration(Test.ref("out").field("c")) }
    intercept[IllegalArgumentException] {
      irLookup.declaration(Test.instOf("child", "Child").ref("out").field("missing"))
    }
  }

  "IRLookup" should "return mem declarations" in {
    def commonFields: Seq[String] = Seq("clk", "en", "addr")
    def readerTargets(rt: ReferenceTarget): Seq[ReferenceTarget] = {
      (commonFields ++ Seq("data")).map(rt.field)
    }
    def writerTargets(rt: ReferenceTarget): Seq[ReferenceTarget] = {
      (commonFields ++ Seq("data", "mask")).map(rt.field)
    }
    def readwriterTargets(rt: ReferenceTarget): Seq[ReferenceTarget] = {
      (commonFields ++ Seq("wdata", "wmask", "wmode", "rdata")).map(rt.field)
    }
    val input =
      s"""circuit Test:
         |  module Test :
         |    input in : UInt<8>
         |    input clk: Clock[3]
         |    input dataClk: Clock
         |    input mode: UInt<1>
         |    output out : UInt<8>[2]
         |    mem m:
         |      data-type => UInt<8>
         |      reader => r
         |      writer => w
         |      readwriter => rw
         |      depth => 2
         |      write-latency => 1
         |      read-latency => 0
         |
         |    reg addr: UInt<1>, dataClk
         |    reg en: UInt<1>, dataClk
         |    reg indata: UInt<8>, dataClk
         |
         |    m.r.clk <= clk[0]
         |    m.r.en <= en
         |    m.r.addr <= addr
         |    out[0] <= m.r.data
         |
         |    m.w.clk <= clk[1]
         |    m.w.en <= en
         |    m.w.addr <= addr
         |    m.w.data <= indata
         |    m.w.mask <= en
         |
         |    m.rw.clk <= clk[2]
         |    m.rw.en <= en
         |    m.rw.addr <= addr
         |    m.rw.wdata <= indata
         |    m.rw.wmask <= en
         |    m.rw.wmode <= en
         |    out[1] <= m.rw.rdata
         |""".stripMargin

    val C = CircuitTarget("Test")
    val MemTest = C.module("Test")
    val Mem = MemTest.ref("m")
    val Reader = Mem.field("r")
    val Writer = Mem.field("w")
    val Readwriter = Mem.field("rw")
    val allSignals = readerTargets(Reader) ++ writerTargets(Writer) ++ readwriterTargets(Readwriter)

    val circuit = new firrtl.stage.transforms.Compiler(Seq(Dependency[ExpandWhensAndCheck]))
      .runTransform(
        CircuitState(parse(input), UnknownForm)
      )
      .circuit
    val irLookup = IRLookup(circuit)
    val uint8 = UIntType(IntWidth(8))
    val mem = DefMemory(NoInfo, "m", uint8, 2, 1, 0, Seq("r"), Seq("w"), Seq("rw"))
    allSignals.foreach { at =>
      irLookup.declaration(at) shouldBe mem
    }
  }

  "IRLookup" should "return expressions, types, kinds, and flows" in {
    val input =
      """circuit Test:
        |  module Test :
        |    input in: UInt<8>
        |    input clk: Clock
        |    input reset: UInt<1>
        |    output out: {a: UInt<8>, b: UInt<8>[2]}
        |    input ana1: Analog<8>
        |    output ana2: Analog<8>
        |    out is invalid
        |    reg r: UInt<8>, clk with:
        |      (reset => (reset, UInt(0)))
        |    node x = r
        |    wire y: UInt<8>
        |    y <= x
        |    out.b[0] <= and(y, asUInt(SInt(-1)))
        |    attach(ana1, ana2)
        |    inst child of Child
        |    out.a <= child.out
        |  module Child:
        |    output out: UInt<8>
        |    out <= UInt(1)
        |""".stripMargin

    val circuit = new firrtl.stage.transforms.Compiler(Seq(Dependency[ExpandWhensAndCheck]))
      .runTransform(
        CircuitState(parse(input), UnknownForm)
      )
      .circuit
    val irLookup = IRLookup(circuit)
    val Test = ModuleTarget("Test", "Test")
    val uint8 = UIntType(IntWidth(8))
    val uint1 = UIntType(IntWidth(1))

    def check(rt: ReferenceTarget, e: Expression): Unit = {
      irLookup.expr(rt) shouldBe e
      irLookup.tpe(rt) shouldBe e.tpe
      irLookup.kind(rt) shouldBe Utils.kind(e)
      irLookup.flow(rt) shouldBe Utils.flow(e)
    }

    check(Test.ref("in"), WRef("in", uint8, PortKind, SourceFlow))
    check(Test.ref("clk"), WRef("clk", ClockType, PortKind, SourceFlow))
    check(Test.ref("reset"), WRef("reset", uint1, PortKind, SourceFlow))

    val out = Test.ref("out")
    val outExpr =
      WRef(
        "out",
        BundleType(Seq(Field("a", Default, uint8), Field("b", Default, VectorType(uint8, 2)))),
        PortKind,
        SinkFlow
      )
    check(out, outExpr)
    check(out.field("a"), WSubField(outExpr, "a", uint8, SinkFlow))
    val outB = out.field("b")
    val outBExpr = WSubField(outExpr, "b", VectorType(uint8, 2), SinkFlow)
    check(outB, outBExpr)
    check(outB.index(0), WSubIndex(outBExpr, 0, uint8, SinkFlow))
    check(outB.index(1), WSubIndex(outBExpr, 1, uint8, SinkFlow))

    check(Test.ref("ana1"), WRef("ana1", AnalogType(IntWidth(8)), PortKind, SourceFlow))
    check(Test.ref("ana2"), WRef("ana2", AnalogType(IntWidth(8)), PortKind, SinkFlow))

    val clk = WRef("clk", ClockType, PortKind, SourceFlow)
    val reset = WRef("reset", UIntType(IntWidth(1)), PortKind, SourceFlow)
    val init = UIntLiteral(0)
    check(Test.ref("r"), WRef("r", uint8, RegKind, DuplexFlow))
    check(Test.ref("r").clock, clk)
    check(Test.ref("r").reset, reset)
    check(Test.ref("r").init, init)

    check(Test.ref("x"), WRef("x", uint8, ExpKind, SourceFlow))

    check(Test.ref("y"), WRef("y", uint8, WireKind, DuplexFlow))

    check(
      Test.ref("@and#0"),
      DoPrim(
        PrimOps.And,
        Seq(WRef("y", uint8, WireKind, SourceFlow), DoPrim(AsUInt, Seq(SIntLiteral(-1)), Nil, UIntType(IntWidth(1)))),
        Nil,
        uint8
      )
    )

    val child = WRef("child", BundleType(Seq(Field("out", Default, uint8))), InstanceKind, SourceFlow)
    check(Test.ref("child"), child)
    check(Test.ref("child").field("out"), WSubField(child, "out", uint8, SourceFlow))
  }

  "IRLookup" should "cache expressions" in {
    def mkType(i: Int): String = {
      if (i == 0) "UInt<8>" else s"{x: ${mkType(i - 1)}}"
    }

    val depth = 500

    val input =
      s"""circuit Test:
         |  module Test :
         |    input in: ${mkType(depth)}
         |    output out: ${mkType(depth)}
         |    out <= in
         |""".stripMargin

    val circuit = new firrtl.stage.transforms.Compiler(Seq(Dependency[ExpandWhensAndCheck]))
      .runTransform(
        CircuitState(parse(input), UnknownForm)
      )
      .circuit
    val Test = ModuleTarget("Test", "Test")
    val irLookup = IRLookup(circuit)
    def mkReferences(parent: ReferenceTarget, i: Int): Seq[ReferenceTarget] = {
      if (i == 0) Seq(parent)
      else {
        val newParent = parent.field("x")
        newParent +: mkReferences(newParent, i - 1)
      }
    }

    // Check caching from root to leaf
    val inRefs = mkReferences(Test.ref("in"), depth)
    val (inStartTime, _) = Utils.time(irLookup.expr(inRefs.head))
    inRefs.tail.foreach { r =>
      val (ms, _) = Utils.time(irLookup.expr(r))
      require(inStartTime > ms)
    }
    val outRefs = mkReferences(Test.ref("out"), depth).reverse
    val (outStartTime, _) = Utils.time(irLookup.expr(outRefs.head))
    outRefs.tail.foreach { r =>
      val (ms, _) = Utils.time(irLookup.expr(r))
      require(outStartTime > ms)
    }
  }
}
