// SPDX-License-Identifier: Apache-2.0

package firrtlTests.annotationTests

import firrtl._
import firrtl.annotations._
import firrtl.annotations.analysis.DuplicationHelper
import firrtl.annotations.transforms.{NoSuchTargetException}
import firrtl.transforms.{DedupedResult, DontTouchAnnotation}
import firrtl.testutils.{FirrtlMatchers, FirrtlPropSpec}

object EliminateTargetPathsSpec {

  case class DummyAnnotation(target: Target) extends SingleTargetAnnotation[Target] {
    override def duplicate(n: Target): Annotation = DummyAnnotation(n)
  }
  class DummyTransform() extends Transform with ResolvedAnnotationPaths {
    override def inputForm:  CircuitForm = LowForm
    override def outputForm: CircuitForm = LowForm

    override val annotationClasses: Traversable[Class[_]] = Seq(classOf[DummyAnnotation])

    override def execute(state: CircuitState): CircuitState = state
  }

}

class EliminateTargetPathsSpec extends FirrtlPropSpec with FirrtlMatchers {
  import EliminateTargetPathsSpec._

  val input =
    """circuit Top:
      |  module Leaf:
      |    input i: UInt<1>
      |    output o: UInt<1>
      |    o <= i
      |    node a = i
      |  module Middle:
      |    input i: UInt<1>
      |    output o: UInt<1>
      |    inst l1 of Leaf
      |    inst l2 of Leaf
      |    l1.i <= i
      |    l2.i <= l1.o
      |    o <= l2.o
      |  module Top:
      |    input i: UInt<1>
      |    output o: UInt<1>
      |    inst m1 of Middle
      |    inst m2 of Middle
      |    m1.i <= i
      |    m2.i <= m1.o
      |    o <= m2.o
    """.stripMargin

  val TopCircuit = CircuitTarget("Top")
  val Top = TopCircuit.module("Top")
  val Middle = TopCircuit.module("Middle")
  val Leaf = TopCircuit.module("Leaf")

  val Top_m1_l1_a = Top.instOf("m1", "Middle").instOf("l1", "Leaf").ref("a")
  val Top_m2_l1_a = Top.instOf("m2", "Middle").instOf("l1", "Leaf").ref("a")
  val Top_m1_l2_a = Top.instOf("m1", "Middle").instOf("l2", "Leaf").ref("a")
  val Top_m2_l2_a = Top.instOf("m2", "Middle").instOf("l2", "Leaf").ref("a")
  val Middle_l1_a = Middle.instOf("l1", "Leaf").ref("a")
  val Middle_l2_a = Middle.instOf("l2", "Leaf").ref("a")
  val Leaf_a = Leaf.ref("a")

  val customTransforms = Seq(new DummyTransform())

  val inputState = CircuitState(parse(input), ChirrtlForm)
  property("Hierarchical tokens should be expanded properly") {
    val dupMap = DuplicationHelper(inputState.circuit.modules.map(_.name).toSet)

    // Only a few instance references
    dupMap.expandHierarchy(Top_m1_l1_a)
    dupMap.expandHierarchy(Top_m2_l1_a)
    dupMap.expandHierarchy(Middle_l1_a)

    dupMap.makePathless(Top_m1_l1_a).foreach { Set(TopCircuit.module("Leaf___Top_m1_l1").ref("a")) should contain(_) }
    dupMap.makePathless(Top_m2_l1_a).foreach { Set(TopCircuit.module("Leaf___Top_m2_l1").ref("a")) should contain(_) }
    dupMap.makePathless(Top_m1_l2_a).foreach { Set(Leaf_a) should contain(_) }
    dupMap.makePathless(Top_m2_l2_a).foreach { Set(Leaf_a) should contain(_) }
    dupMap.makePathless(Middle_l1_a).foreach {
      Set(
        TopCircuit.module("Leaf___Top_m1_l1").ref("a"),
        TopCircuit.module("Leaf___Top_m2_l1").ref("a"),
        TopCircuit.module("Leaf___Middle_l1").ref("a")
      ) should contain(_)
    }
    dupMap.makePathless(Middle_l2_a).foreach { Set(Leaf_a) should contain(_) }
    dupMap.makePathless(Leaf_a).foreach {
      Set(
        TopCircuit.module("Leaf___Top_m1_l1").ref("a"),
        TopCircuit.module("Leaf___Top_m2_l1").ref("a"),
        TopCircuit.module("Leaf___Middle_l1").ref("a"),
        Leaf_a
      ) should contain(_)
    }
    dupMap.makePathless(Top).foreach { Set(Top) should contain(_) }
    dupMap.makePathless(Middle).foreach {
      Set(
        TopCircuit.module("Middle___Top_m1"),
        TopCircuit.module("Middle___Top_m2"),
        Middle
      ) should contain(_)
    }
    dupMap.makePathless(Leaf).foreach {
      Set(
        TopCircuit.module("Leaf___Top_m1_l1"),
        TopCircuit.module("Leaf___Top_m2_l1"),
        TopCircuit.module("Leaf___Middle_l1"),
        Leaf
      ) should contain(_)
    }
  }

  property("Hierarchical donttouch should be resolved properly") {
    val inputState = CircuitState(parse(input), ChirrtlForm, Seq(DontTouchAnnotation(Top_m1_l1_a)))
    val customTransforms = Seq(new LowFirrtlOptimization())
    val outputState = new LowFirrtlCompiler().compile(inputState, customTransforms)
    val check =
      """circuit Top :
        |  module Leaf :
        |    input i : UInt<1>
        |    output o : UInt<1>

        |    node a = i
        |    o <= i
        |
        |  module Middle :
        |    input i : UInt<1>
        |    output o : UInt<1>

        |    inst l1 of Leaf
        |    inst l2 of Leaf
        |    o <= l2.o
        |    l1.i <= i
        |    l2.i <= l1.o

        |  module Top :
        |    input i : UInt<1>
        |    output o : UInt<1>

        |    inst m1 of Middle
        |    inst m2 of Middle
        |    o <= m2.o
        |    m1.i <= i
        |    m2.i <= m1.o""".stripMargin

    canonicalize(outputState.circuit).serialize should be(canonicalize(parse(check)).serialize)
    outputState.annotations.collect {
      case x: DontTouchAnnotation => x.target
    } should be(Seq(Top.circuitTarget.module("Top").instOf("m1", "Middle").instOf("l1", "Leaf").ref("a")))
  }

  property("No name conflicts between old and new modules") {
    val input =
      """circuit Top:
        |  module Middle:
        |    input i: UInt<1>
        |    output o: UInt<1>
        |    o <= i
        |  module Top:
        |    input i: UInt<1>
        |    output o: UInt<1>
        |    inst m1 of Middle
        |    inst m2 of Middle
        |    inst x of Middle___Top_m1
        |    x.i <= i
        |    m1.i <= i
        |    m2.i <= m1.o
        |    o <= m2.o
        |  module Middle___Top_m1:
        |    input i: UInt<1>
        |    output o: UInt<1>
        |    o <= i
        |    node a = i
      """.stripMargin
    val checks =
      """circuit Top :
        |  module Middle :
        |  module Top :
        |  module Middle___Top_m1 :
        |  module Middle____Top_m1 :""".stripMargin.split("\n")
    val Top_m1 = Top.instOf("m1", "Middle")
    val inputState = CircuitState(parse(input), ChirrtlForm, Seq(DummyAnnotation(Top_m1)))
    val outputState = new LowFirrtlCompiler().compile(inputState, customTransforms)
    val outputLines = outputState.circuit.serialize.split("\n")
    checks.foreach { line =>
      outputLines should contain(line)
    }
  }

  property("Previously unused modules should remain, but newly unused modules should be eliminated") {
    val input =
      """circuit Top:
        |  module Leaf:
        |    input i: UInt<1>
        |    output o: UInt<1>
        |    o <= i
        |    node a = i
        |  module Middle:
        |    input i: UInt<1>
        |    output o: UInt<1>
        |    o <= i
        |  module Top:
        |    input i: UInt<1>
        |    output o: UInt<1>
        |    inst m1 of Middle
        |    inst m2 of Middle
        |    m1.i <= i
        |    m2.i <= m1.o
        |    o <= m2.o
      """.stripMargin

    val checks =
      """circuit Top :
        |  module Leaf :
        |  module Top :
        |  module Middle___Top_m1 :
        |  module Middle___Top_m2 :""".stripMargin.split("\n")

    val Top_m1 = Top.instOf("m1", "Middle")
    val Top_m2 = Top.instOf("m2", "Middle")
    val inputState = CircuitState(parse(input), ChirrtlForm, Seq(DummyAnnotation(Top_m1), DummyAnnotation(Top_m2)))
    val outputState = new LowFirrtlCompiler().compile(inputState, customTransforms)
    val outputLines = outputState.circuit.serialize.split("\n")

    checks.foreach { line =>
      outputLines should contain(line)
    }
    checks.foreach { line =>
      outputLines should not contain ("  module Middle :")
    }
  }

  property("Paths with incorrect names should error") {
    val input =
      """circuit Top:
        |  module Leaf:
        |    input i: UInt<1>
        |    output o: UInt<1>
        |    o <= i
        |    node a = i
        |  module Middle:
        |    input i: UInt<1>
        |    output o: UInt<1>
        |    o <= i
        |  module Top:
        |    input i: UInt<1>
        |    output o: UInt<1>
        |    inst m1 of Middle
        |    inst m2 of Middle
        |    m1.i <= i
        |    m2.i <= m1.o
        |    o <= m2.o
      """.stripMargin
    val e1 = the[CustomTransformException] thrownBy {
      val Top_m1 = Top.instOf("m1", "MiddleX")
      val inputState = CircuitState(parse(input), ChirrtlForm, Seq(DummyAnnotation(Top_m1)))
      new LowFirrtlCompiler().compile(inputState, customTransforms)
    }
    e1.cause shouldBe a[NoSuchTargetException]

    val e2 = the[CustomTransformException] thrownBy {
      val Top_m2 = Top.instOf("x2", "Middle")
      val inputState = CircuitState(parse(input), ChirrtlForm, Seq(DummyAnnotation(Top_m2)))
      new LowFirrtlCompiler().compile(inputState, customTransforms)
    }
    e2.cause shouldBe a[NoSuchTargetException]
  }

  property("No name conflicts between two new modules") {
    val input =
      """circuit Top:
        |  module Top:
        |    input i: UInt<1>
        |    output o: UInt<1>
        |    inst m1 of Middle_
        |    inst m2 of Middle
        |    m1.i <= i
        |    m2.i <= m1.o
        |    o <= m2.o
        |  module Middle:
        |    input i: UInt<1>
        |    output o: UInt<1>
        |    inst _l of Leaf
        |    _l.i <= i
        |    o <= _l.o
        |  module Middle_:
        |    input i: UInt<1>
        |    output o: UInt<1>
        |    inst l of Leaf
        |    l.i <= i
        |    node x = i
        |    o <= l.o
        |  module Leaf:
        |    input i: UInt<1>
        |    output o: UInt<1>
        |    o <= i
      """.stripMargin
    val checks =
      """circuit Top :
        |  module Middle :
        |  module Middle_ :
        |  module Top :
        |  module Leaf___Middle__l :
        |  module Leaf____Middle__l :""".stripMargin.split("\n")
    val Middle_l1 = CircuitTarget("Top").module("Middle").instOf("_l", "Leaf")
    val Middle_l2 = CircuitTarget("Top").module("Middle_").instOf("l", "Leaf")
    val inputState =
      CircuitState(parse(input), ChirrtlForm, Seq(DummyAnnotation(Middle_l1), DummyAnnotation(Middle_l2)))
    val outputState = new LowFirrtlCompiler().compile(inputState, customTransforms)
    val outputLines = outputState.circuit.serialize.split("\n")
    checks.foreach { line =>
      outputLines should contain(line)
    }
  }

  property("Keep annotations of modules not instantiated") {
    val input =
      """circuit Top:
        |  module Top:
        |    input i: UInt<1>
        |    output o: UInt<1>
        |    inst m1 of Middle
        |    inst m2 of Middle
        |    m1.i <= i
        |    m2.i <= m1.o
        |    o <= m2.o
        |  module Middle:
        |    input i: UInt<1>
        |    output o: UInt<1>
        |    inst _l of Leaf
        |    _l.i <= i
        |    o <= _l.o
        |  module Middle_:
        |    input i: UInt<1>
        |    output o: UInt<1>
        |    o <= UInt(0)
        |  module Leaf:
        |    input i: UInt<1>
        |    output o: UInt<1>
        |    o <= i
      """.stripMargin
    val checks =
      """circuit Top :
        |  module Middle_ :""".stripMargin.split("\n")
    val Middle_ = CircuitTarget("Top").module("Middle_").ref("i")
    val inputState = CircuitState(parse(input), ChirrtlForm, Seq(DontTouchAnnotation(Middle_)))
    val outputState = new VerilogCompiler().compile(inputState, customTransforms)
    val outputLines = outputState.circuit.serialize.split("\n")
    checks.foreach { line =>
      outputLines should contain(line)
    }
  }

  property("It should remove ResolvePaths annotations") {
    val input =
      """|circuit Foo:
         |  module Bar:
         |    skip
         |  module Foo:
         |    inst bar of Bar
         |""".stripMargin

    CircuitState(passes.ToWorkingIR.run(Parser.parse(input)), UnknownForm, Nil)
      .resolvePaths(Seq(CircuitTarget("Foo").module("Foo").instOf("bar", "Bar")))
      .annotations
      .collect { case a: firrtl.annotations.transforms.ResolvePaths => a } should be(empty)
  }

  property("It should rename module annotations") {
    val input =
      """|circuit Foo:
         |  module Bar:
         |    node x = UInt<1>(0)
         |  module Foo:
         |    inst bar of Bar
         |    inst baz of Bar""".stripMargin
    val check =
      """|circuit Foo:
         |  module Bar___Foo_bar:
         |    node x = UInt<1>(0)
         |  module Bar:
         |    node x = UInt<1>(0)
         |  module Foo:
         |    inst bar of Bar___Foo_bar
         |    inst baz of Bar""".stripMargin
    val Bar_x = CircuitTarget("Foo").module("Bar").ref("x")
    val output = CircuitState(passes.ToWorkingIR.run(Parser.parse(input)), UnknownForm, Seq(DontTouchAnnotation(Bar_x)))
      .resolvePaths(Seq(CircuitTarget("Foo").module("Foo").instOf("bar", "Bar")))

    val parsedCheck = Parser.parse(check)
    info(output.circuit.serialize)
    (output.circuit.serialize) should be(parsedCheck.serialize)

    val newBar_x = CircuitTarget("Foo").module("Bar___Foo_bar").ref("x")

    (output.annotations.filter {
      case _: DeletedAnnotation => false
      case _ => true
    } should contain).allOf(DontTouchAnnotation(newBar_x), DontTouchAnnotation(Bar_x))
  }

  property("It should not rename lone instances") {
    val input =
      """|circuit Foo:
         |  module Baz:
         |    node foo = UInt<1>(0)
         |  module Bar:
         |    node foo = UInt<1>(0)
         |    inst baz of Baz
         |  module Foo:
         |    node foo = UInt<1>(0)
         |    inst bar of Bar
         |""".stripMargin
    val targets = Seq(
      CircuitTarget("Foo").module("Foo").instOf("bar", "Bar").instOf("baz", "Baz"),
      CircuitTarget("Foo").module("Foo").instOf("bar", "Bar"),
      CircuitTarget("Foo").module("Foo")
    )
    val dontTouches = targets.map(t => DontTouchAnnotation(t.ref("foo")))
    val inputCircuit = Parser.parse(input)
    val output = CircuitState(passes.ToWorkingIR.run(inputCircuit), UnknownForm, dontTouches)
      .resolvePaths(targets)

    info(output.circuit.serialize)

    output.circuit.serialize should be(inputCircuit.serialize)
    (output.annotations.collect {
      case a: DontTouchAnnotation => a
    } should contain).allOf(
      DontTouchAnnotation(ModuleTarget("Foo", "Foo").ref("foo")),
      DontTouchAnnotation(ModuleTarget("Foo", "Bar").ref("foo")),
      DontTouchAnnotation(ModuleTarget("Foo", "Baz").ref("foo"))
    )
  }

  property("It should not rename parent lone instances but still rename children") {
    val input =
      """|circuit FooBar:
         |  module Bar:
         |    node baz = UInt<1>(0)
         |  module FooBar:
         |    inst foo of Foo
         |  module Foo:
         |    inst bar of Bar
         |    inst barBar of Bar
         |""".stripMargin

    // modules Foo and FooBar should not be renamed
    val checks =
      """circuit FooBar :
        |  module Foo :
        |  module FooBar :
        |    inst foo of Foo""".stripMargin.split("\n")

    val targets = Seq(
      CircuitTarget("FooBar").module("FooBar").instOf("foo", "Foo").instOf("bar", "Bar"),
      CircuitTarget("FooBar").module("FooBar").instOf("foo", "Foo").instOf("barBar", "Bar")
    )
    val dontTouches = targets.map(t => DontTouchAnnotation(t.ref("baz")))
    val output = CircuitState(passes.ToWorkingIR.run(Parser.parse(input)), UnknownForm, dontTouches)
      .resolvePaths(targets)

    info(output.circuit.serialize)

    val outputLines = output.circuit.serialize.split("\n")
    checks.foreach { line =>
      outputLines should contain(line)
    }

    (output.annotations.collect {
      case a: DontTouchAnnotation => a
    } should contain).allOf(
      DontTouchAnnotation(ModuleTarget("FooBar", "Bar___Foo_bar").ref("baz")),
      DontTouchAnnotation(ModuleTarget("FooBar", "Bar___Foo_barBar").ref("baz"))
    )
  }

  property("It should use DedupedResult names") {
    val input =
      """|circuit Top:
         |  module Baz:
         |    skip
         |  module Bar:
         |    inst baz of Baz
         |    inst bazzz of Baz
         |    skip
         |  module Top:
         |    inst bar of Bar
         |""".stripMargin
    val checks =
      """|circuit Top :
         |  module Baz_0 :
         |  module Baz_1 :
         |    inst baz of Baz_0
         |    inst bazzz of Baz_1
         |""".stripMargin.split("\n")
    val baz = CircuitTarget("Top").module("Top").instOf("bar", "Bar").instOf("baz", "Baz")
    val bazzz = CircuitTarget("Top").module("Top").instOf("bar", "Bar").instOf("bazzz", "Baz")
    val annos = Seq(
      DedupedResult(ModuleTarget("Top", "Baz_0"), Some(baz), 0),
      DedupedResult(ModuleTarget("Top", "Baz_1"), Some(bazzz), 1),
      DontTouchAnnotation(baz.ref("foo")),
      DontTouchAnnotation(bazzz.ref("foo"))
    )
    val inputCircuit = Parser.parse(input)
    val output = CircuitState(passes.ToWorkingIR.run(inputCircuit), UnknownForm, annos)
      .resolvePaths(Seq(baz, bazzz))

    info(output.circuit.serialize)

    val outputLines = output.circuit.serialize.split("\n")
    checks.foreach { line =>
      outputLines should contain(line)
    }
    (output.annotations.collect {
      case a: DontTouchAnnotation => a
    } should contain).allOf(
      DontTouchAnnotation(ModuleTarget("Top", "Baz_0").ref("foo")),
      DontTouchAnnotation(ModuleTarget("Top", "Baz_1").ref("foo"))
    )
  }

  property("It should not rename untouched modules") {
    val input =
      """|circuit Top:
         |  module Baz:
         |    node foo = UInt<1>(0)
         |  module Bar:
         |    inst lkj of Baz
         |    inst asdf of Baz
         |  module Top:
         |    inst bar of Bar
         |    inst baz of Baz
         |""".stripMargin
    val asdf = ModuleTarget("Top", "Top").instOf("bar", "Bar").instOf("asdf", "Baz")
    val lkj = ModuleTarget("Top", "Top").instOf("bar", "Bar").instOf("lkj", "Baz")
    val baz = ModuleTarget("Top", "Top").instOf("baz", "Baz")
    val annos = Seq(
      DontTouchAnnotation(asdf.ref("foo")),
      DontTouchAnnotation(lkj.ref("foo")),
      DontTouchAnnotation(baz.ref("foo"))
    )
    val inputCircuit = Parser.parse(input)
    val output = CircuitState(passes.ToWorkingIR.run(inputCircuit), UnknownForm, annos)
      .resolvePaths(Seq(asdf, lkj))

    info(output.circuit.serialize)

    output.annotations.collect { case a: DontTouchAnnotation => a } should be(
      Seq(
        DontTouchAnnotation(ModuleTarget("Top", "Baz___Bar_asdf").ref("foo")),
        DontTouchAnnotation(ModuleTarget("Top", "Baz___Bar_lkj").ref("foo")),
        DontTouchAnnotation(baz.ref("foo"))
      )
    )
  }

  property("It should properly rename modules with multiple instances") {
    val input =
      """|circuit Top:
         |  module Core:
         |    node clock = UInt<1>(0)
         |  module System:
         |    inst core_1 of Core
         |    inst core_2 of Core
         |    inst core_3 of Core
         |    inst core_4 of Core
         |  module Top:
         |    inst system of System
         |""".stripMargin
    val absCoreInstances = (1 to 4).map { i =>
      ModuleTarget("Top", "Top").instOf("system", "System").instOf(s"core_$i", "Core")
    }
    val relCoreInstances = (1 to 4).map { i =>
      ModuleTarget("Top", "System").instOf(s"core_$i", "Core")
    }
    val coreModule = ModuleTarget("Top", "Core")
    val annos = (coreModule +: (relCoreInstances ++ absCoreInstances)).map(DummyAnnotation(_))
    val inputCircuit = Parser.parse(input)
    val output = CircuitState(passes.ToWorkingIR.run(inputCircuit), UnknownForm, annos)
      .resolvePaths(relCoreInstances ++ absCoreInstances)

    info(output.circuit.serialize)

    val checkDontTouches = (1 to 4).map { i =>
      DummyAnnotation(ModuleTarget("Top", s"Core___System_core_$i"))
    }
    output.annotations.collect { case a: DummyAnnotation => a } should be(checkDontTouches)
  }
}
