// SPDX-License-Identifier: Apache-2.0

package chiselTests

import chisel3.stage.ChiselStage
import chisel3.testers.BasicTester

import org.scalacheck.Gen
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks

import scala.collection.immutable.ListMap

// Need separate import to override compile options from Chisel._
object CompatibilityCustomCompileOptions {
  import Chisel.{defaultCompileOptions => _, _}
  implicit val customCompileOptions =
    chisel3.ExplicitCompileOptions.NotStrict.copy(inferModuleReset = true)
  class Foo extends Module {
    val io = new Bundle {}
  }
}

class CompatibilitySpec extends ChiselFlatSpec with ScalaCheckDrivenPropertyChecks with Utils {
  import Chisel._

  behavior.of("Chisel compatibility layer")

  it should "accept direction arguments" in {
    ChiselStage.elaborate(new Module {
      // Choose a random direction
      val directionArgument: Direction = Gen.oneOf(INPUT, OUTPUT, NODIR).sample.get
      val expectedDirection = directionArgument match {
        case NODIR => OUTPUT
        case other => other
      }
      // Choose a random width
      val width = Gen.choose(1, 2048).sample.get
      val io = new Bundle {
        val b = Bool(directionArgument)
        val u = UInt(directionArgument, width)
      }
      io.b shouldBe a[Bool]
      io.b.getWidth shouldEqual 1
      io.b.dir shouldEqual (expectedDirection)
      io.u shouldBe a[UInt]
      io.u.getWidth shouldEqual width
      io.u.dir shouldEqual (expectedDirection)
    })
  }

  it should "accept single argument U/SInt factory methods" in {
    // Choose a random value
    val value: Int = Gen.choose(0, Int.MaxValue).sample.get
    val l = UInt(value)
    l shouldBe a[UInt]
    l shouldBe 'lit
    l.getWidth shouldEqual BigInt(value).bitLength
    l.litValue() shouldEqual value
  }

  it should "map utility objects into the package object" in {
    val value: Int = Gen.choose(2, 2048).sample.get
    log2Up(value) shouldBe (1.max(BigInt(value - 1).bitLength))
    log2Ceil(value) shouldBe (BigInt(value - 1).bitLength)
    log2Down(value) shouldBe ((1.max(BigInt(value - 1).bitLength)) - (if (value > 0 && ((value & (value - 1)) == 0)) 0
                                                                      else 1))
    log2Floor(value) shouldBe (BigInt(value - 1).bitLength - (if (value > 0 && ((value & (value - 1)) == 0)) 0 else 1))
    isPow2(BigInt(1) << value) shouldBe true
    isPow2((BigInt(1) << value) - 1) shouldBe false
  }

  it should "make BitPats available" in {
    val value: Int = Gen.choose(1, Int.MaxValue).sample.get
    val binaryString = value.toBinaryString
    val maskPosition = Gen.choose(0, binaryString.length - 1).sample.get
    val bs = new StringBuilder(binaryString)
    bs(maskPosition) = '?'
    val bitPatString = bs.toString
    val bp = BitPat("b" + bitPatString)
    bp shouldBe a[BitPat]
    bp.getWidth shouldEqual binaryString.length

  }

  it should "successfully compile a complete module" in {
    class Dummy extends Module {
      // The following just checks that we can create objects with nothing more than the Chisel compatibility package.
      val io = new Bundle {}
      val data = UInt(width = 3)
      val wire = Wire(data)
      new ArbiterIO(data, 2) shouldBe a[ArbiterIO[UInt]]
      Module(new LockingRRArbiter(data, 2, 2, None)) shouldBe a[LockingRRArbiter[UInt]]
      Module(new RRArbiter(data, 2)) shouldBe a[RRArbiter[UInt]]
      Module(new Arbiter(data, 2)) shouldBe a[Arbiter[UInt]]
      new Counter(2) shouldBe a[Counter]
      new ValidIO(data) shouldBe a[ValidIO[UInt]]
      new DecoupledIO(data) shouldBe a[DecoupledIO[UInt]]
      new QueueIO(data, 2) shouldBe a[QueueIO[UInt]]
      Module(new Pipe(data, 2)) shouldBe a[Pipe[UInt]]

      FillInterleaved(2, wire) shouldBe a[UInt]
      PopCount(wire) shouldBe a[UInt]
      Fill(2, wire) shouldBe a[UInt]
      Reverse(wire) shouldBe a[UInt]
      Cat(wire, wire) shouldBe a[UInt]
      Log2(wire) shouldBe a[UInt]
      // 'switch' and 'is' are tested below in Risc
      Counter(2) shouldBe a[Counter]
      DecoupledIO(wire) shouldBe a[DecoupledIO[UInt]]
      val dcd = Wire(Decoupled(data))
      dcd shouldBe a[DecoupledIO[UInt]]
      Queue(dcd) shouldBe a[DecoupledIO[UInt]]
      Queue(dcd, 0) shouldBe a[DecoupledIO[UInt]]
      Enum(UInt(), 2) shouldBe a[List[UInt]]
      ListLookup(wire, List(wire), Array((BitPat("b1"), List(wire)))) shouldBe a[List[UInt]]
      Lookup(wire, wire, Seq((BitPat("b1"), wire))) shouldBe a[UInt]
      Mux1H(wire, Seq(wire)) shouldBe a[UInt]
      PriorityMux(Seq(Bool(false)), Seq(data)) shouldBe a[UInt]
      MuxLookup(wire, wire, Seq((wire, wire))) shouldBe a[UInt]
      MuxCase(wire, Seq((Bool(true), wire))) shouldBe a[UInt]
      OHToUInt(wire) shouldBe a[UInt]
      PriorityEncoder(wire) shouldBe a[UInt]
      UIntToOH(wire) shouldBe a[UInt]
      PriorityEncoderOH(wire) shouldBe a[UInt]
      RegNext(wire) shouldBe a[UInt]
      RegInit(wire) shouldBe a[UInt]
      RegEnable(wire, Bool(true)) shouldBe a[UInt]
      ShiftRegister(wire, 2) shouldBe a[UInt]
      Valid(data) shouldBe a[ValidIO[UInt]]
      Pipe(Wire(Valid(data)), 2) shouldBe a[ValidIO[UInt]]
    }
    ChiselStage.elaborate { new Dummy }
  }
  // Verify we can elaborate a design expressed in Chisel2
  class Chisel2CompatibleRisc extends Module {
    val io = new Bundle {
      val isWr = Bool(INPUT)
      val wrAddr = UInt(INPUT, 8)
      val wrData = Bits(INPUT, 32)
      val boot = Bool(INPUT)
      val valid = Bool(OUTPUT)
      val out = Bits(OUTPUT, 32)
    }
    val file = Mem(256, Bits(width = 32))
    val code = Mem(256, Bits(width = 32))
    val pc = Reg(init = UInt(0, 8))

    val add_op :: imm_op :: Nil = Enum(2)

    val inst = code(pc)
    val op = inst(31, 24)
    val rci = inst(23, 16)
    val rai = inst(15, 8)
    val rbi = inst(7, 0)

    val ra = Mux(rai === Bits(0), Bits(0), file(rai))
    val rb = Mux(rbi === Bits(0), Bits(0), file(rbi))
    val rc = Wire(Bits(width = 32))

    io.valid := Bool(false)
    io.out := Bits(0)
    rc := Bits(0)

    when(io.isWr) {
      code(io.wrAddr) := io.wrData
    }.elsewhen(io.boot) {
      pc := UInt(0)
    }.otherwise {
      switch(op) {
        is(add_op) { rc := ra +% rb }
        is(imm_op) { rc := (rai << 8) | rbi }
      }
      io.out := rc
      when(rci === UInt(255)) {
        io.valid := Bool(true)
      }.otherwise {
        file(rci) := rc
      }
      pc := pc +% UInt(1)
    }
  }

  it should "Chisel2CompatibleRisc should elaborate" in {
    ChiselStage.elaborate { new Chisel2CompatibleRisc }
  }

  it should "not try to assign directions to Analog" in {
    ChiselStage.elaborate(new Module {
      val io = new Bundle {
        val port = chisel3.experimental.Analog(32.W)
      }
    })
  }

  class SmallBundle extends Bundle {
    val f1 = UInt(width = 4)
    val f2 = UInt(width = 5)
  }
  class BigBundle extends SmallBundle {
    val f3 = UInt(width = 6)
  }

  "A Module with missing bundle fields when compiled with the Chisel compatibility package" should "not throw an exception" in {

    class ConnectFieldMismatchModule extends Module {
      val io = new Bundle {
        val in = (new SmallBundle).asInput
        val out = (new BigBundle).asOutput
      }
      io.out := io.in
    }
    ChiselStage.elaborate { new ConnectFieldMismatchModule() }
  }

  "A Module in which a Reg is created with a bound type when compiled with the Chisel compatibility package" should "not throw an exception" in {

    class CreateRegFromBoundTypeModule extends Module {
      val io = new Bundle {
        val in = (new SmallBundle).asInput
        val out = (new BigBundle).asOutput
      }
      val badReg = Reg(UInt(7, width = 4))
    }
    ChiselStage.elaborate { new CreateRegFromBoundTypeModule() }
  }

  "A Module with unwrapped IO when compiled with the Chisel compatibility package" should "not throw an exception" in {

    class RequireIOWrapModule extends Module {
      val io = new Bundle {
        val in = UInt(width = 32).asInput
        val out = Bool().asOutput
      }
      io.out := io.in(1)
    }
    ChiselStage.elaborate { new RequireIOWrapModule() }
  }

  "A Module without val io" should "throw an exception" in {
    class ModuleWithoutValIO extends Module {
      val foo = IO(new Bundle {
        val in = UInt(width = 32).asInput
        val out = Bool().asOutput
      })
      foo.out := foo.in(1)
    }
    val e = intercept[Exception](
      ChiselStage.elaborate { new ModuleWithoutValIO }
    )
    e.getMessage should include("must have a 'val io' Bundle")
  }

  "A Module connecting output as source to input as sink when compiled with the Chisel compatibility package" should "not throw an exception" in {

    class SimpleModule extends Module {
      val io = new Bundle {
        val in = (UInt(width = 3)).asInput
        val out = (UInt(width = 4)).asOutput
      }
    }
    class SwappedConnectionModule extends SimpleModule {
      val child = Module(new SimpleModule)
      io.in := child.io.out
    }
    ChiselStage.elaborate { new SwappedConnectionModule() }
  }

  "Vec ports" should "give default directions to children so they can be used in chisel3.util" in {
    import Chisel._
    ChiselStage.elaborate(new Module {
      val io = new Bundle {
        val in = Vec(1, UInt(width = 8)).flip
        val out = UInt(width = 8)
      }
      io.out := RegEnable(io.in(0), true.B)
    })
  }

  "Reset" should "still walk, talk, and quack like a Bool" in {
    import Chisel._
    ChiselStage.elaborate(new Module {
      val io = new Bundle {
        val in = Bool(INPUT)
        val out = Bool(OUTPUT)
      }
      io.out := io.in && reset
    })
  }

  "Data.dir" should "give the correct direction for io" in {
    import Chisel._
    ChiselStage.elaborate(new Module {
      val io = (new Bundle {
        val foo = Bool(OUTPUT)
        val bar = Bool().flip
      }).flip
      Chisel.assert(io.foo.dir == INPUT)
      Chisel.assert(io.bar.dir == OUTPUT)
    })
  }

  // Note: This is a regression (see https://github.com/freechipsproject/chisel3/issues/668)
  it should "fail for Chisel types" in {
    import Chisel._
    an[chisel3.ExpectedHardwareException] should be thrownBy extractCause[chisel3.ExpectedHardwareException] {
      ChiselStage.elaborate(new Module {
        val io = new Bundle {}
        UInt(INPUT).dir
      })
    }
  }

  "Mux return value" should "be able to be used on the RHS" in {
    import Chisel._
    ChiselStage.elaborate(new Module {
      val gen = new Bundle { val foo = UInt(width = 8) }
      val io = new Bundle {
        val a = Vec(2, UInt(width = 8)).asInput
        val b = Vec(2, UInt(width = 8)).asInput
        val c = gen.asInput
        val d = gen.asInput
        val en = Bool(INPUT)
        val y = Vec(2, UInt(width = 8)).asOutput
        val z = gen.asOutput
      }
      io.y := Mux(io.en, io.a, io.b)
      io.z := Mux(io.en, io.c, io.d)
    })
  }

  "Chisel3 IO constructs" should "be useable in Chisel2" in {
    import Chisel._
    ChiselStage.elaborate(new Module {
      val io = IO(new Bundle {
        val in = Input(Bool())
        val foo = Output(Bool())
        val bar = Flipped(Bool())
      })
      Chisel.assert(io.in.dir == INPUT)
      Chisel.assert(io.foo.dir == OUTPUT)
      Chisel.assert(io.bar.dir == INPUT)
    })
  }

  behavior.of("BitPat")

  it should "support old operators" in {
    class Foo extends Module {
      val io = IO(new Bundle {})

      info("Deprecated method DC hasn't been removed")
      val bp = BitPat.DC(4)
    }

    ChiselStage.elaborate(new Foo)
  }

  behavior.of("Enum")

  it should "support apply[T <: Bits](nodeType: T, n: Int): List[T]" in {
    class Foo extends Module {
      val io = IO(new Bundle {})

      info("works for a UInt")
      Enum(UInt(), 4) shouldBe a[List[UInt]]

      info("throw an exception for non-UInt types")
      intercept[IllegalArgumentException] {
        Enum(SInt(), 4)
      }.getMessage should include("Only UInt supported for enums")

      info("throw an exception if the bit width is specified")
      intercept[IllegalArgumentException] {
        Enum(UInt(width = 8), 4)
      }.getMessage should include("Bit width may no longer be specified for enums")
    }

    ChiselStage.elaborate(new Foo)
  }

  behavior.of("Queue")

  it should "support deprecated constructors" in {
    class Foo extends Module {
      val io = IO(new Bundle {})

      info("reset: Option[Bool] constructor works")
      val option = Module(new Queue(UInt(), 4, false, false, Some(Bool(true))))

      info("reset: Bool constructor works")
      val explicit = Module(new Queue(UInt(), 4, false, false, Bool(true)))
    }

    ChiselStage.elaborate(new Foo)
  }

  behavior.of("LFSR16")

  it should "still exist" in {
    class Foo extends Module {
      val io = IO(new Bundle {})

      info("Still exists")
      val lfsr = LFSR16()

      info("apply method returns a UInt")
      lfsr shouldBe a[UInt]

      info("returned UInt has a width of 16")
      lfsr.getWidth should be(16)
    }

    ChiselStage.elaborate(new Foo)
  }

  behavior.of("Mem")

  it should "support deprecated apply methods" in {
    class Foo extends Module {
      val io = IO(new Bundle {})

      info("apply[T <: Data](t: T, size: BigInt): Mem[T] works")
      val memBigInt = Mem(UInt(), 8: BigInt)
      memBigInt shouldBe a[Mem[UInt]]

      info("apply[T <: Data](t: T, size: Int): Mem[T] works")
      val memInt = Mem(SInt(), 16: Int)
      memInt shouldBe a[Mem[SInt]]
    }

    ChiselStage.elaborate(new Foo)
  }

  behavior.of("SeqMem")

  it should "support deprecated apply methods" in {
    class Foo extends Module {
      val io = IO(new Bundle {})

      info("apply[T <: Data](t: T, size: BigInt): SeqMem[T] works")
      val seqMemBigInt = SeqMem(UInt(), 8: BigInt)
      seqMemBigInt shouldBe a[SeqMem[UInt]]

      info("apply[T <: Data](t: T, size: Int): SeqMem[T] works")
      val seqMemInt = SeqMem(UInt(), 16: Int)
      seqMemInt shouldBe a[SeqMem[UInt]]
    }

    ChiselStage.elaborate(new Foo)
  }

  it should "support data-types of mixed directionality" in {
    class Foo extends Module {
      val io = IO(new Bundle {})
      val tpe = new Bundle { val foo = UInt(OUTPUT, width = 4); val bar = UInt(width = 4) }
      // NOTE for some reason, the old bug this hit did not occur when `tpe` is inlined
      val mem = SeqMem(tpe, 8)
      mem(3.U)

    }
    ChiselStage.elaborate((new Foo))
  }

  behavior.of("debug")

  it should "still exist" in {
    class Foo extends Module {
      val io = IO(new Bundle {})

      val data = UInt(width = 2)
      debug(data)
    }

    ChiselStage.elaborate(new Foo)
  }

  behavior.of("Data methods")

  behavior.of("Wire")

  it should "support legacy methods" in {
    class Foo extends Module {
      val io = IO(new Bundle {})

      info("apply[T <: Data](dummy: Int = 0, init: T): T works")
      val first = Wire(init = UInt("hdeadbeef"))
      first shouldBe a[UInt]

      info("apply[T <: Data](t: T, init: T): T works")
      val second = Wire(SInt(), SInt(-100))
      second shouldBe a[SInt]

      info("apply[T <: Data](t: T, init: DontCare.type): T works")
      val third = Wire(UInt(), chisel3.DontCare)
      third shouldBe a[UInt]
    }

    ChiselStage.elaborate(new Foo)
  }

  behavior.of("Vec")

  it should "support legacy methods" in {
    class Foo extends BasicTester {
      val seq = Seq(Wire(UInt(0, width = 4)), Wire(UInt(1, width = 4)), Wire(UInt(2, width = 4)))
      val vec = Vec(seq)

      info("read works")
      chisel3.assert(vec.read(UInt(0)) === UInt(0))

      info("write works")
      vec.write(UInt(1), UInt(3))
      chisel3.assert(vec.read(UInt(1)) === UInt(3))

      val (_, done) = Counter(Bool(true), 4)
      when(done) { stop }
    }

    assertTesterPasses(new Foo)
  }

  behavior.of("Bits methods")

  it should "support legacy methods" in {
    class Foo extends Module {
      val io = new Bundle {}

      val u = UInt(8)
      val s = SInt(-4)

      info("asBits works")
      s.asBits shouldBe a[Bits]

      info("toSInt works")
      u.toSInt shouldBe a[SInt]

      info("toUInt works")
      s.toUInt shouldBe a[UInt]

      info("toBools works")
      s.toBools shouldBe a[Seq[Bool]]
    }

    ChiselStage.elaborate(new Foo)
  }

  it should "properly propagate custom compileOptions in Chisel.Module" in {
    import CompatibilityCustomCompileOptions._
    var result: Foo = null
    ChiselStage.elaborate({ result = new Foo; result })
    (result.compileOptions should be).theSameInstanceAs(customCompileOptions)
  }

  it should "properly set the refs of Records" in {
    class MyRecord extends Record {
      val foo = Vec(1, Bool()).asInput
      val bar = Vec(1, Bool())
      val elements = ListMap("in" -> foo, "out" -> bar)
      def cloneType = (new MyRecord).asInstanceOf[this.type]
    }
    class Foo extends Module {
      val io = IO(new MyRecord)
      io.bar := io.foo
    }
    val verilog = ChiselStage.emitVerilog(new Foo)
    // Check that the names are correct (and that the FIRRTL is valid)
    verilog should include("assign io_out_0 = io_in_0;")
  }

  it should "ignore .suggestName on field io" in {
    class MyModule extends Module {
      val io = new Bundle {
        val foo = UInt(width = 8).asInput
        val bar = UInt(width = 8).asOutput
      }
      io.suggestName("potato")
      io.bar := io.foo
    }
    val verilog = ChiselStage.emitVerilog(new MyModule)
    verilog should include("input  [7:0] io_foo")
    verilog should include("output [7:0] io_bar")
  }

  it should "properly name field io" in {
    class MyModule extends Module {
      val io = new Bundle {
        val foo = UInt(width = 8).asInput
        val bar = UInt(width = 8).asOutput
      }
      val wire = Wire(init = io.foo)
      io.bar := wire
    }
    val verilog = ChiselStage.emitVerilog(new MyModule)
    verilog should include("input  [7:0] io_foo")
    verilog should include("output [7:0] io_bar")
  }

  it should "properly handle hardware construction before val io is initialized" in {
    class MyModule extends Module {
      val foo = Wire(init = UInt(8))
      val io = new Bundle {
        val in = UInt(INPUT, width = 8)
        val en = Bool(INPUT)
        val out = UInt(OUTPUT, width = 8)
      }
      io.out := foo
      when(io.en) {
        io.out := io.in
      }
    }
    // Just check that this doesn't crash during elaboration. For more info see:
    // https://github.com/chipsalliance/chisel3/issues/1802
    //
    ChiselStage.elaborate(new MyModule)
  }

  behavior.of("BlackBox")

  it should "have invalidated ports in a compatibility context" in {
    class ExtModuleInvalidatedTester extends Module {
      val io = IO(new Bundle {
        val in = Input(UInt(8.W))
        val out = Output(UInt(8.W))
      })
      val inst = Module(new BlackBox {
        val io = IO(new Bundle {
          val in = Input(UInt(8.W))
          val out = Output(UInt(8.W))
        })
      })
      inst.io.in := io.in
      io.out := inst.io.out
    }

    val chirrtl = ChiselStage.emitChirrtl(new ExtModuleInvalidatedTester)
    chirrtl should include("inst.in is invalid")
    chirrtl should include("inst.out is invalid")
  }
}
