// See LICENSE for license details.

package firrtlTests

import firrtl.FirrtlProtos.Firrtl
import firrtl._
import firrtl.ir._
import firrtl.testutils._

class ProtoBufSpec extends FirrtlFlatSpec {

  /** Tests in src/test/resource/ in .fir format
    *
    * @note These tests rely on the ANTLR Parser
    */
  case class FirrtlResourceTest(name: String, resourceDir: String)

  val firrtlResourceTests = List(
    FirrtlResourceTest("GCDTester", "/integration"),
    FirrtlResourceTest("RightShiftTester", "/integration"),
    FirrtlResourceTest("MemTester", "/integration"),
    FirrtlResourceTest("PipeTester", "/integration"),
    FirrtlResourceTest("Rob", "/regress"),
    FirrtlResourceTest("RocketCore", "/regress"),
    FirrtlResourceTest("ICache", "/regress"),
    FirrtlResourceTest("FPU", "/regress"),
    FirrtlResourceTest("AsyncResetTester", "/features")
  )

  for (FirrtlResourceTest(name, dir) <- firrtlResourceTests) {
    s"$name" should "work with Protobuf serialization and deserialization" in {
      val circuit = parse(FileUtils.getTextResource(s"$dir/$name.fir"))

      // Test ToProto and FromProto
      val protobuf = proto.ToProto.convert(circuit)
      val pcircuit = proto.FromProto.convert(protobuf)
      canonicalize(circuit).serialize should equal(canonicalize(pcircuit).serialize)

      // Test ProtoBuf generated serialization and deserialization
      val ostream = new java.io.ByteArrayOutputStream()
      protobuf.writeTo(ostream)
      val istream = new java.io.ByteArrayInputStream(ostream.toByteArray)
      val cistream = com.google.protobuf.CodedInputStream.newInstance(istream)
      cistream.setRecursionLimit(Integer.MAX_VALUE)
      val protobuf2 = firrtl.FirrtlProtos.Firrtl.parseFrom(cistream)
      protobuf2 should equal (protobuf)

      // Test that our faster serialization matches generated serialization
      val ostream2 = new java.io.ByteArrayOutputStream
      proto.ToProto.writeToStream(ostream2, circuit)
      ostream2.toByteArray.toList should equal (ostream.toByteArray.toList)
    }
  }

  // ********** Focused Tests **********
  // The goal is to fill coverage holes left after the above

  behavior of "ProtoBuf serialization and deserialization"
  import firrtl.proto._

  it should "support UnknownWidth" in {
    // Note that this has to be handled in the parent object so we need to test everything that has a width
    val uint = ir.UIntType(ir.UnknownWidth)
    FromProto.convert(ToProto.convert(uint).build) should equal (uint)

    val sint = ir.SIntType(ir.UnknownWidth)
    FromProto.convert(ToProto.convert(sint).build) should equal (sint)

    val ftpe = ir.FixedType(ir.UnknownWidth, ir.UnknownWidth)
    FromProto.convert(ToProto.convert(ftpe).build) should equal (ftpe)

    val atpe = ir.AnalogType(ir.UnknownWidth)
    FromProto.convert(ToProto.convert(atpe).build) should equal (atpe)

    val ulit = ir.UIntLiteral(123, ir.UnknownWidth)
    FromProto.convert(ToProto.convert(ulit).build) should equal (ulit)

    val slit = ir.SIntLiteral(-123, ir.UnknownWidth)
    FromProto.convert(ToProto.convert(slit).build) should equal (slit)

    val flit = ir.FixedLiteral(-123, ir.UnknownWidth, ir.UnknownWidth)
    FromProto.convert(ToProto.convert(flit).build) should equal (flit)
  }

  it should "support all Primops" in {
    val builtInOps = PrimOps.listing.map(PrimOps.fromString(_))
    for (op <- builtInOps) {
      val expr = DoPrim(op, List.empty, List.empty, ir.UnknownType)
      FromProto.convert(ToProto.convert(expr).build) should equal (expr)
    }
  }

  it should "support all ExtModule features (except info which isn't yet supported by Chisel)" in {
    val ports = Seq(
      Port(ir.NoInfo, "port1", ir.Input, ir.UIntType(ir.IntWidth(8))),
      Port(ir.NoInfo, "port2", ir.Output, ir.SIntType(ir.IntWidth(8)))
    )
    val params = Seq(
      IntParam("param1", BigInt(Long.MinValue)),
      DoubleParam("param2", Double.NegativeInfinity),
      StringParam("param3", ir.StringLit("quite the string!")),
      RawStringParam("param4", "get some raw strings")
    )
    val ext = ir.ExtModule(ir.NoInfo, "MyModule", ports, "DefNameHere", params)
    FromProto.convert(ToProto.convert(ext).build) should equal (ext)
  }

  it should "support FixedType" in {
    val ftpe = ir.FixedType(IntWidth(8), IntWidth(4))
    FromProto.convert(ToProto.convert(ftpe).build) should equal (ftpe)
  }

  it should "support FixedLiteral" in {
    val flit = ir.FixedLiteral(3, IntWidth(8), IntWidth(4))
    FromProto.convert(ToProto.convert(flit).build) should equal (flit)
  }

  it should "support Analog and Attach" in {
    val analog = ir.AnalogType(IntWidth(8))
    FromProto.convert(ToProto.convert(analog).build) should equal (analog)

    val attach = ir.Attach(ir.NoInfo, Seq(Reference("hi", ir.UnknownType)))
    FromProto.convert(ToProto.convert(attach).head.build) should equal (attach)
  }

  // Regression tests were generated before Chisel could emit else
  it should "support whens with elses" in {
    val expr = Reference("hi", ir.UnknownType)
    val stmt = Connect(ir.NoInfo, expr, expr)
    val when = ir.Conditionally(ir.NoInfo, expr, stmt, stmt)
    FromProto.convert(ToProto.convert(when).head.build) should equal (when)
  }

  it should "support SIntLiteral with a width" in {
    val slit = ir.SIntLiteral(-123)
    FromProto.convert(ToProto.convert(slit).build) should equal (slit)
  }

  // Backwards compatibility
  it should "support mems using old uint32 and new BigInt" in {
    val size = 128
    val mem = DefMemory(NoInfo, "m", UIntType(IntWidth(8)), size, 1, 1, List("r"), List("w"), List("rw"))
    val builder = ToProto.convert(mem).head
    val defaultProto = builder.build()
    val oldProto = Firrtl.Statement.newBuilder().setMemory(
      builder.getMemoryBuilder.clearDepth().setUintDepth(size)
    ).build()
    // These Proto messages are not the same
    defaultProto shouldNot equal (oldProto)

    val defaultMem = FromProto.convert(defaultProto)
    val oldMem = FromProto.convert(oldProto)

    // But they both deserialize to the original!
    defaultMem should equal (mem)
    oldMem should equal (mem)
  }

  // Backwards compatibility
  it should "support cmems using old VectorType and new TypeAndDepth" in {
    val size = 128
    val cmem = CDefMemory(NoInfo, "m", UIntType(IntWidth(8)), size, true)
    val vtpe = ToProto.convert(VectorType(UIntType(IntWidth(8)), size))
    val builder = ToProto.convert(cmem).head
    val defaultProto = builder.build()
    val oldProto = Firrtl.Statement.newBuilder().setCmemory(
      builder.getCmemoryBuilder.clearTypeAndDepth().setVectorType(vtpe)
    ).build()
    // These Proto messages are not the same
    defaultProto shouldNot equal (oldProto)

    val defaultCMem = FromProto.convert(defaultProto)
    val oldCMem = FromProto.convert(oldProto)

    // But they both deserialize to the original!
    defaultCMem should equal (cmem)
    oldCMem should equal (cmem)
  }

  // readunderwrite support
  it should "support readunderwrite parameters" in {
    val m1 = DefMemory(NoInfo, "m", UIntType(IntWidth(8)), 128, 1, 1, List("r"), List("w"), Nil, ir.ReadUnderWrite.Old)
    FromProto.convert(ToProto.convert(m1).head.build) should equal (m1)

    val m2 = m1.copy(readUnderWrite = ir.ReadUnderWrite.New)
    FromProto.convert(ToProto.convert(m2).head.build) should equal (m2)

    val cm1 = CDefMemory(NoInfo, "m", UIntType(IntWidth(8)), 128, true, ir.ReadUnderWrite.Old)
    FromProto.convert(ToProto.convert(cm1).head.build) should equal (cm1)

    val cm2 = cm1.copy(readUnderWrite = ir.ReadUnderWrite.New)
    FromProto.convert(ToProto.convert(cm2).head.build) should equal (cm2)
  }

  it should "support AsyncResetTypes" in {
    val port = ir.Port(ir.NoInfo, "reset", ir.Input, ir.AsyncResetType)
    FromProto.convert(ToProto.convert(port).build) should equal (port)
  }

  it should "support ResetTypes" in {
    val port = ir.Port(ir.NoInfo, "reset", ir.Input, ir.ResetType)
    FromProto.convert(ToProto.convert(port).build) should equal (port)
  }
}
