// SPDX-License-Identifier: Apache-2.0

package chiselTests

import chisel3._
import chisel3.experimental._
import _root_.firrtl.{ir => firrtlir}
import chisel3.internal.firrtl.{BinaryPoint, IntervalRange, KnownBinaryPoint, UnknownBinaryPoint}
import org.scalatest.freespec.AnyFreeSpec
import org.scalatest.matchers.should.Matchers

class IntervalRangeSpec extends AnyFreeSpec with Matchers {

  "IntervalRanges" - {
    def C(b: BigDecimal): firrtlir.Bound = firrtlir.Closed(b)

    def O(b: BigDecimal): firrtlir.Bound = firrtlir.Open(b)

    def U(): firrtlir.Bound = firrtlir.UnknownBound

    def UBP(): BinaryPoint = UnknownBinaryPoint

    def checkRange(r: IntervalRange, l: firrtlir.Bound, u: firrtlir.Bound, b: BinaryPoint): Unit = {
      r.lowerBound should be(l)
      r.upperBound should be(u)
      r.binaryPoint should be(b)
    }

    def checkBinaryPoint(r: IntervalRange, b: BinaryPoint): Unit = {
      r.binaryPoint should be(b)
    }

    "IntervalRange describes the range of values of the Interval Type" - {
      "Factory methods can create IntervalRanges" - {
        "ranges can start or end open or closed, default binary point is none" in {
          checkRange(range"[0,10]", C(0), C(10), 0.BP)
          checkRange(range"[-1,10)", C(-1), O(10), 0.BP)
          checkRange(range"(11,12]", O(11), C(12), 0.BP)
          checkRange(range"(-21,-10)", O(-21), O(-10), 0.BP)
        }

        "ranges can have unknown bounds" in {
          checkRange(range"[?,10]", U(), C(10), 0.BP)
          checkRange(range"(?,10]", U(), C(10), 0.BP)
          checkRange(range"[-1,?]", C(-1), U(), 0.BP)
          checkRange(range"[-1,?)", C(-1), U(), 0.BP)
          checkRange(range"[?,?]", U(), U(), 0.BP)
          checkRange(range"[?,?].?", U(), U(), UBP())
        }

        "binary points can be specified" in {
          checkBinaryPoint(range"[?,10].0", 0.BP)
          checkBinaryPoint(range"[?,10].2", 2.BP)
          checkBinaryPoint(range"[?,10].?", UBP())
        }
        "malformed ranges will throw ChiselException or are compile time errors" in {
          // must be a cleverer way to show this
          intercept[ChiselException] {
            range"[19,5]"
          }
          assertDoesNotCompile(""" range"?,10] """)
          assertDoesNotCompile(""" range"?,? """)
        }
      }
    }

    "Ranges can be specified for UInt, SInt, and FixedPoint" - {
      "invalid range specifiers should fail at compile time" in {
        assertDoesNotCompile(""" range"" """)
        assertDoesNotCompile(""" range"[]" """)
        assertDoesNotCompile(""" range"0" """)
        assertDoesNotCompile(""" range"[0]" """)
        assertDoesNotCompile(""" range"[0, 1" """)
        assertDoesNotCompile(""" range"0, 1]" """)
        assertDoesNotCompile(""" range"[0, 1, 2]" """)
        assertDoesNotCompile(""" range"[a]" """)
        assertDoesNotCompile(""" range"[a, b]" """)
        assertCompiles(""" range"[0, 1]" """) // syntax sanity check
      }

      "range macros should allow open and closed bounds" in {
        range"[-1, 1)" should be(range"[-1,1).0")
        range"[-1, 1)" should be(IntervalRange(C(-1), O(1), 0.BP))
        range"[-1, 1]" should be(IntervalRange(C(-1), C(1), 0.BP))
        range"(-1, 1]" should be(IntervalRange(O(-1), C(1), 0.BP))
        range"(-1, 1)" should be(IntervalRange(O(-1), O(1), 0.BP))
      }

      "range specifiers should be whitespace tolerant" in {
        range"[-1,1)" should be(IntervalRange(C(-1), O(1), 0.BP))
        range" [-1,1) " should be(IntervalRange(C(-1), O(1), 0.BP))
        range" [ -1 , 1 ) " should be(IntervalRange(C(-1), O(1), 0.BP))
        range"   [   -1   ,   1   )   " should be(IntervalRange(C(-1), O(1), 0.BP))
      }

      "range macros should work with interpolated variables" in {
        val a = 10
        val b = -3

        range"[$b, $a)" should be(IntervalRange(C(b), O(a), 0.BP))
        range"[${a + b}, $a)" should be(IntervalRange(C(a + b), O(a), 0.BP))
        range"[${-3 - 7}, ${-3 + a})" should be(IntervalRange(C(-10), O(-3 + a), 0.BP))

        def number(n: Int): Int = n

        range"[${number(1)}, ${number(3)})" should be(IntervalRange(C(1), O(3), 0.BP))
      }

      "UInt should get the correct width from a range" in {
        UInt(range"[0, 8)").getWidth should be(3)
        UInt(range"[0, 8]").getWidth should be(4)
        UInt(range"[0, 0]").getWidth should be(1)
      }

      "SInt should get the correct width from a range" in {
        SInt(range"[0, 8)").getWidth should be(4)
        SInt(range"[0, 8]").getWidth should be(5)
        SInt(range"[-4, 4)").getWidth should be(3)
        SInt(range"[0, 0]").getWidth should be(1)
      }

      "UInt should check that the range is valid" in {
        an[ChiselException] should be thrownBy {
          UInt(range"[1, 0]")
        }
        an[ChiselException] should be thrownBy {
          UInt(range"[-1, 1]")
        }
        an[ChiselException] should be thrownBy {
          UInt(range"(0,0]")
        }
        an[ChiselException] should be thrownBy {
          UInt(range"[0,0)")
        }
        an[ChiselException] should be thrownBy {
          UInt(range"(0,0)")
        }
        an[ChiselException] should be thrownBy {
          UInt(range"(0,1)")
        }
      }

      "SInt should check that the range is valid" in {
        an[ChiselException] should be thrownBy {
          SInt(range"[1, 0]")
        }
        an[ChiselException] should be thrownBy {
          SInt(range"(0,0]")
        }
        an[ChiselException] should be thrownBy {
          SInt(range"[0,0)")
        }
        an[ChiselException] should be thrownBy {
          SInt(range"(0,0)")
        }
        an[ChiselException] should be thrownBy {
          SInt(range"(0,1)")
        }
      }
    }

    "shift operations should work on ranges" - {
      "<<, shiftLeft affects the bounds but not the binary point" in {
        checkRange(range"[0,7].1", C(0), C(7), 1.BP)
        checkRange(range"[0,7].1" << 1, C(0), C(14), 1.BP)

        checkRange(range"[2,7].2", C(2), C(7), 2.BP)
        checkRange(range"[2,7].2" << 1, C(4), C(14), 2.BP)
      }

      ">>, shiftRight affects the bounds but not the binary point" in {
        checkRange(range"[0,7].0", C(0), C(7), 0.BP)
        checkRange(range"[0,7].0" >> 1, C(0), C(3), 0.BP)

        checkRange(range"[0,7].1", C(0), C(7), 1.BP)
        checkRange(range"[0,7].1" >> 1, C(0), C(3.5), 1.BP)

        checkRange(range"[2,7].2", C(2), C(7), 2.BP)
        checkRange(range"[2,7].2" >> 1, C(1), C(3.5), 2.BP)

        checkRange(range"[2,7].2", C(2), C(7), 2.BP)
        checkRange(range"[2,7].2" >> 2, C(0.5), C(1.75), 2.BP)

        // the 7(b111) >> 3 => 0.875(b0.111) but since
        // binary point is two, lopping must occur so 0.875 becomes 0.75
        checkRange(range"[-8,7].2", C(-8), C(7), 2.BP)
        checkRange(range"[-8,7].2" >> 3, C(-1), C(0.75), 2.BP)

        checkRange(range"(0,7).0", O(0), O(7), 0.BP)
        checkRange(range"(0,7).0" >> 1, O(0), O(3), 0.BP)

        checkRange(range"(0,7).1", O(0), O(7), 1.BP)
        checkRange(range"(0,7).1" >> 1, O(0), O(3.5), 1.BP)

        checkRange(range"(2,7).2", O(2), O(7), 2.BP)
        checkRange(range"(2,7).2" >> 1, O(1), O(3.5), 2.BP)

        checkRange(range"(2,7).2", O(2), O(7), 2.BP)
        checkRange(range"(2,7).2" >> 2, O(0.5), O(1.75), 2.BP)

        // the 7(b111) >> 3 => 0.875(b0.111) but since
        // binary point is two, lopping must occur so 0.875 becomes 0.75
        checkRange(range"(-8,7).2", O(-8), O(7), 2.BP)
        checkRange(range"(-8,7).2" >> 3, O(-1), O(0.75), 2.BP)
      }

      "set precision can change the bounds due to precision loss, direction of change is always to lower value" in {
        intercept[ChiselException] {
          checkRange(range"[-7.875,7.875].3".setPrecision(UnknownBinaryPoint), C(-7.875), C(7.875), 5.BP)
        }

        checkRange(range"[-7.875,7.875].3", C(-7.875), C(7.875), 3.BP)
        checkRange(range"[1.25,2].2".setPrecision(1.BP), C(1.0), C(2), 1.BP)
        checkRange(range"[-7.875,7.875].3".setPrecision(5.BP), C(-7.875), C(7.875), 5.BP)
        checkRange(range"[-7.875,7.875].3".setPrecision(1.BP), C(-8.0), C(7.5), 1.BP)
      }
    }

    "get possible values should return all values from high to low" in {
      var range = range"[0,4]"
      range.getLowestPossibleValue should be(Some(0))
      range.getHighestPossibleValue should be(Some(4))
      range.getPossibleValues should be(Seq(0, 1, 2, 3, 4))

      range = range"(0,4)"
      range.getLowestPossibleValue should be(Some(1))
      range.getHighestPossibleValue should be(Some(3))
      range.getPossibleValues should be(Seq(1, 2, 3))

      range = range"(-1,4).1"
      range.getLowestPossibleValue should be(Some(-0.5))
      range.getHighestPossibleValue should be(Some(3.5))
      range.getPossibleValues should be(Seq(-0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5))
    }
  }

}
