// SPDX-License-Identifier: Apache-2.0

package firrtlTests.fixed

import firrtl.{ChirrtlForm, CircuitState, LowFirrtlCompiler}
import firrtl.testutils.FirrtlFlatSpec

class FixedPointMathSpec extends FirrtlFlatSpec {

  val SumPattern = """.*output sum.*<(\d+)>.*.*""".r
  val ProductPattern = """.*output product.*<(\d+)>.*""".r
  val DifferencePattern = """.*output difference.*<(\d+)>.*""".r

  val AssignPattern = """\s*(\w+) <= (\w+)\((.*)\)\s*""".r

  for {
    bits1 <- 1 to 4
    binaryPoint1 <- 1 to 4
    bits2 <- 1 to 4
    binaryPoint2 <- 1 to 4
  } {
    def config = s"($bits1,$binaryPoint1)($bits2,$binaryPoint2)"

    s"Configuration $config" should "pass" in {

      val input =
        s"""circuit Unit :
           |  module Unit :
           |    input  a : Fixed<$bits1><<$binaryPoint1>>
           |    input  b : Fixed<$bits2><<$binaryPoint2>>
           |    output sum        : Fixed
           |    output product    : Fixed
           |    output difference : Fixed
           |    sum        <= add(a, b)
           |    product    <= mul(a, b)
           |    difference <= sub(a, b)
           |    """.stripMargin

      val lowerer = new LowFirrtlCompiler

      val res = lowerer.compileAndEmit(CircuitState(parse(input), ChirrtlForm))

      val output = res.getEmittedCircuit.value.split("\n")

      def inferredAddWidth: Int = {
        val binaryDifference = binaryPoint1 - binaryPoint2
        val (newW1, newW2) = if (binaryDifference > 0) {
          (bits1, bits2 + binaryDifference)
        } else {
          (bits1 + binaryDifference.abs, bits2)
        }
        newW1.max(newW2) + 1
      }

      for (line <- output) {
        line match {
          case SumPattern(varWidth) =>
            assert(varWidth.toInt === inferredAddWidth, s"$config sum sint bits wrong for $line")
          case ProductPattern(varWidth) =>
            assert(varWidth.toInt === bits1 + bits2, s"$config product bits wrong for $line")
          case DifferencePattern(varWidth) =>
            assert(varWidth.toInt === inferredAddWidth, s"$config difference bits wrong for $line")
          case AssignPattern(varName, operation, args) =>
            varName match {
              case "sum" =>
                assert(operation === "add", s"var sum should be result of an add in $line")
                if (binaryPoint1 > binaryPoint2) {
                  assert(!args.contains("shl(a"), s"$config first arg should be just a in $line")
                  assert(
                    args.contains(s"shl(b, ${binaryPoint1 - binaryPoint2})"),
                    s"$config second arg incorrect in $line"
                  )
                } else if (binaryPoint1 < binaryPoint2) {
                  assert(
                    args.contains(s"shl(a, ${(binaryPoint1 - binaryPoint2).abs})"),
                    s"$config second arg incorrect in $line"
                  )
                  assert(!args.contains("shl(b"), s"$config second arg should be just b in $line")
                } else {
                  assert(!args.contains("shl(a"), s"$config first arg should be just a in $line")
                  assert(!args.contains("shl(b"), s"$config second arg should be just b in $line")
                }
              case "product" =>
                assert(operation === "mul", s"var sum should be result of an add in $line")
                assert(!args.contains("shl(a"), s"$config first arg should be just a in $line")
                assert(!args.contains("shl(b"), s"$config second arg should be just b in $line")
              case "difference" =>
                assert(operation === "sub", s"var difference should be result of an sub in $line")
                if (binaryPoint1 > binaryPoint2) {
                  assert(!args.contains("shl(a"), s"$config first arg should be just a in $line")
                  assert(
                    args.contains(s"shl(b, ${binaryPoint1 - binaryPoint2})"),
                    s"$config second arg incorrect in $line"
                  )
                } else if (binaryPoint1 < binaryPoint2) {
                  assert(
                    args.contains(s"shl(a, ${(binaryPoint1 - binaryPoint2).abs})"),
                    s"$config second arg incorrect in $line"
                  )
                  assert(!args.contains("shl(b"), s"$config second arg should be just b in $line")
                } else {
                  assert(!args.contains("shl(a"), s"$config first arg should be just a in $line")
                  assert(!args.contains("shl(b"), s"$config second arg should be just b in $line")
                }
              case _ =>
            }
          case _ =>
        }
      }
    }
  }
}
