// SPDX-License-Identifier: Apache-2.0

package chiselTests

import circt.stage.ChiselStage
import chisel3._
import chisel3.util._
import chisel3.experimental.dataview._

import org.scalactic.source.Position

object ReadOnlySpec {
  class SimpleBundle extends Bundle {
    val a = UInt(8.W)
  }
  object SimpleBundle {
    implicit val toOther: DataView[SimpleBundle, OtherBundle] = DataView(
      _ => new OtherBundle,
      _.a -> _.foo
    )
    implicit val toBigger: DataView[(SimpleBundle, UInt), BiggerBundle] = DataView(
      _ => new BiggerBundle,
      _._1.a -> _.fizz,
      _._2 -> _.buzz
    )
  }
  class OtherBundle extends Bundle {
    val foo = UInt(8.W)
  }
  class BiggerBundle extends Bundle {
    val fizz = UInt(8.W)
    val buzz = UInt(8.W)
  }
}

class ReadOnlySpec extends ChiselFlatSpec with Utils {
  import ReadOnlySpec._

  behavior.of("readOnly")

  def check(m: => RawModule)(implicit pos: Position): Unit = {
    val e = the[ChiselException] thrownBy {
      ChiselStage.emitCHIRRTL(m, Array("--throw-on-first-error"))
    }
    e.getMessage should include("Cannot assign to read-only value")
  }

  val rightToLeftConnectOps: Seq[(Data, Data) => Unit] = Seq(
    _ := _,
    _ :<= _,
    _ :#= _,
    _ :<>= _
  )

  it should "be an error to assign to a read-only UInt wire" in {
    for (op <- rightToLeftConnectOps) {
      check(new RawModule {
        val foo = Wire(UInt(8.W)).readOnly
        op(foo, 1.U)
      })
    }
  }

  it should "be an error to assign to a field of a read-only Bundle wire" in {
    for (op <- rightToLeftConnectOps) {
      check(new RawModule {
        val foo = Wire(new SimpleBundle).readOnly
        op(foo.a, 1.U)
      })
    }
  }

  it should "be an error to assign to a read-only Bundle wire" in {
    for (op <- rightToLeftConnectOps) {
      check(new RawModule {
        val in = IO(Input(new SimpleBundle))
        val foo = Wire(new SimpleBundle).readOnly
        op(foo, in)
      })
    }
  }

  it should "be an error to assign to a read-only field of a Bundle wire" in {
    for (op <- rightToLeftConnectOps) {
      check(new RawModule {
        val foo = Wire(new SimpleBundle)
        val foo_a = foo.a.readOnly
        op(foo_a, 1.U)
      })
    }
  }

  it should "be an error to assign to a read-only view of a UInt wire" in {
    for (op <- rightToLeftConnectOps) {
      check(new RawModule {
        val foo = Wire(UInt(8.W))
        val x = foo.viewAs[UInt].readOnly
        op(x, 1.U)
      })
    }
  }

  it should "be an error to assign to a field of a read-only view of a Bundle wire" in {
    for (op <- rightToLeftConnectOps) {
      check(new RawModule {
        val w = Wire(new SimpleBundle)
        val x = w.viewAs[OtherBundle].readOnly
        op(x.foo, 1.U)
      })
    }
  }

  it should "be an error to assign to a read-only view of a Bundle wire" in {
    for (op <- rightToLeftConnectOps) {
      check(new RawModule {
        val in = IO(Input(new OtherBundle))
        val w = Wire(new SimpleBundle)
        val x = w.viewAs[OtherBundle].readOnly
        op(x, in)
      })
    }
  }

  it should "be an error to assign to a field of a view of a read-only Bundle wire" in {
    for (op <- rightToLeftConnectOps) {
      check(new RawModule {
        val w = Wire(new SimpleBundle).readOnly
        val x = w.viewAs[OtherBundle]
        op(x.foo, 1.U)
      })
    }
  }

  it should "be an error to assign to a view of a read-only Bundle wire" in {
    for (op <- rightToLeftConnectOps) {
      check(new RawModule {
        val in = IO(Input(new OtherBundle))
        val w = Wire(new SimpleBundle).readOnly
        val x = w.viewAs[OtherBundle]
        op(x, in)
      })
    }
  }

  it should "be an error to assign to a field of read-only non-1-1 view" in {
    for (op <- rightToLeftConnectOps) {
      check(new RawModule {
        val w = Wire(new SimpleBundle)
        val x = Wire(UInt(8.W))
        val y = (w, x).viewAs[BiggerBundle].readOnly
        op(y.fizz, 1.U)
      })
    }
  }

  it should "be an error to assign to a read-only non-1-1 view" in {
    for (op <- rightToLeftConnectOps) {
      check(new RawModule {
        val in = IO(Input(new BiggerBundle))
        val w = Wire(new SimpleBundle)
        val x = Wire(UInt(8.W))
        val y = (w, x).viewAs[BiggerBundle].readOnly
        op(y, in)
      })
    }
  }

  it should "be an error to assign to a field of a non-1-1 view targeting a read-only Bundle wire" in {
    for (op <- rightToLeftConnectOps) {
      check(new RawModule {
        val w = Wire(new SimpleBundle).readOnly
        val x = Wire(UInt(8.W))
        val y = (w, x).viewAs[BiggerBundle]
        op(y.fizz, 1.U)
      })
    }
  }

  it should "NOT be an error to assign to a field of a non-1-1 view targeting a normal Bundle wire (where another targeted field is read-only)" in {
    for (op <- rightToLeftConnectOps) {
      val chirrtl = ChiselStage.emitCHIRRTL(new RawModule {
        val w = Wire(new SimpleBundle)
        val x = Wire(UInt(8.W)).readOnly
        val y = (w, x).viewAs[BiggerBundle]
        op(y.fizz, 1.U)
      })
      chirrtl should include("connect w.a, UInt<1>(0h1)")
    }
  }

  it should "be an error to assign to a non-1-1 view targeting a read-only Bundle wire (where another targeted field is normal)" in {
    for (op <- rightToLeftConnectOps) {
      check(new RawModule {
        val in = IO(Input(new BiggerBundle))
        val w = Wire(new SimpleBundle).readOnly
        val x = Wire(UInt(8.W))
        val y = (w, x).viewAs[BiggerBundle]
        op(y, in)
      })
    }
  }
}
