// See LICENSE for license details.

package firrtlTests

import java.io.{File, FileWriter}

import org.scalatest.freespec.AnyFreeSpec
import org.scalatest.matchers.should.Matchers
import firrtl.passes.InlineAnnotation
import firrtl.passes.memlib.{InferReadWriteAnnotation, ReplSeqMemAnnotation}
import firrtl.transforms.BlackBoxTargetDirAnno
import firrtl._
import firrtl.FileUtils
import firrtl.annotations._
import firrtl.util.BackendCompilationUtilities
import firrtl.testutils.FirrtlFlatSpec

import scala.util.Success

class ExceptingTransform extends Transform {
  def inputForm = HighForm
  def outputForm = HighForm
  def execute(state: CircuitState): CircuitState = {
    throw new ExceptingTransform.CustomException("I ran!")
  }
}
object ExceptingTransform {
  case class CustomException(msg: String) extends Exception
}

//noinspection ScalaStyle
class DriverSpec extends AnyFreeSpec with Matchers with BackendCompilationUtilities {
  val outputDir = createTestDirectory("DriverSpec")
  "CommonOptions are some simple options available across the chisel3 ecosystem" - {
    "CommonOption provide an scopt implementation of an OptionParser" - {
      "Options can be set from an Array[String] as is passed into a main" - {
        "With no arguments default values come out" in {
          val optionsManager = new ExecutionOptionsManager("test")
          optionsManager.parse(Array.empty[String]) should be(true)

          val commonOptions = optionsManager.commonOptions
          commonOptions.topName should be("")
          commonOptions.targetDirName should be(".")
        }
        "top name and target can be set" in {
          val optionsManager = new ExecutionOptionsManager("test")
          optionsManager.parse(Array("--top-name", "dog", "--target-dir", "a/b/c")) should be(true)
          val commonOptions = optionsManager.commonOptions

          commonOptions.topName should be("dog")
          commonOptions.targetDirName should be("a/b/c")

          optionsManager.getBuildFileName(".fir") should be("a/b/c/dog.fir")
          optionsManager.getBuildFileName("fir") should be("a/b/c/dog.fir")
        }
      }
      "CommonOptions can create a directory" in {
        val top_dir = new java.io.File(outputDir, "a")
        val dir = new java.io.File(top_dir, "b/c")
        if (dir.exists()) {
          dir.delete()
        }
        val optionsManager = new ExecutionOptionsManager("test")
        val targetPath = dir.getPath
        optionsManager.parse(Array("--top-name", "dog", "--target-dir", targetPath)) should be(true)
        val commonOptions = optionsManager.commonOptions

        commonOptions.topName should be("dog")
        commonOptions.targetDirName should be(targetPath)

        optionsManager.makeTargetDir() should be(true)
        val tdir = new java.io.File(targetPath)
        tdir.exists() should be(true)
        FileUtils.deleteDirectoryHierarchy(top_dir) should be(true)
      }
    }
    "options include by default a list of strings that are returned in commonOptions.programArgs" in {
      val optionsManager = new ExecutionOptionsManager("test")

      optionsManager.parse(Array("--top-name", "dog", "fox", "tardigrade", "stomatopod")) should be(true)
      info(s"programArgs ${optionsManager.commonOptions.programArgs}")
      optionsManager.commonOptions.programArgs.length should be(3)
      optionsManager.commonOptions.programArgs should be("fox" :: "tardigrade" :: "stomatopod" :: Nil)

      optionsManager.commonOptions = CommonOptions()
      optionsManager.parse(
        Array("dog", "stomatopod")) should be(true)
      info(s"programArgs ${optionsManager.commonOptions.programArgs}")
      optionsManager.commonOptions.programArgs.length should be(2)
      optionsManager.commonOptions.programArgs should be("dog" :: "stomatopod" :: Nil)

      optionsManager.commonOptions = CommonOptions()
      optionsManager.parse(
        Array("fox", "--top-name", "dog", "tardigrade", "stomatopod")) should be(true)
      info(s"programArgs ${optionsManager.commonOptions.programArgs}")
      optionsManager.commonOptions.programArgs.length should be(3)
      optionsManager.commonOptions.programArgs should be("fox" :: "tardigrade" :: "stomatopod" :: Nil)

    }
  }
  "FirrtlOptions holds option information for the firrtl compiler" - {
    "It includes a CommonOptions" in {
      val optionsManager = new ExecutionOptionsManager("test")
      optionsManager.commonOptions.targetDirName should be(".")
    }
    "It provides input and output file names based on target" in {
      val optionsManager = new ExecutionOptionsManager("test") with HasFirrtlOptions

      optionsManager.parse(Array("--top-name", "cat")) should be(true)

      val firrtlOptions = optionsManager.firrtlOptions
      val inputFileName = optionsManager.getBuildFileName("fir", firrtlOptions.inputFileNameOverride)
      inputFileName should be("./cat.fir")
      val outputFileName = firrtlOptions.getTargetFile(optionsManager)
      outputFileName should be("./cat.v")
    }
    "input and output file names can be overridden, overrides do not use targetDir" in {
      val optionsManager = new ExecutionOptionsManager("test") with HasFirrtlOptions

      optionsManager.parse(
        Array("--top-name", "cat", "-i", "./bob.fir", "-o", "carol.v")
      ) should be(true)

      val firrtlOptions = optionsManager.firrtlOptions
      val inputFileName = optionsManager.getBuildFileName("fir", firrtlOptions.inputFileNameOverride)
      inputFileName should be("./bob.fir")
      val outputFileName = firrtlOptions.getTargetFile(optionsManager)
      outputFileName should be("carol.v")
    }
    val input = """
      |circuit Top :
      |  module Top :
      |    input x : UInt<8>
      |    output y : UInt<8>
      |    y <= x""".stripMargin
    val circuit = Parser.parse(input.split("\n").toIterator)
    "firrtl source can be provided directly" in {
      val manager = new ExecutionOptionsManager("test") with HasFirrtlOptions {
        commonOptions = CommonOptions(topName = "Top")
        firrtlOptions = FirrtlExecutionOptions(firrtlSource = Some(input))
      }
      assert(firrtl.Driver.getCircuit(manager).isSuccess)
    }
    "firrtl Circuits can be provided directly" in {
      val manager = new ExecutionOptionsManager("test") with HasFirrtlOptions {
        commonOptions = CommonOptions(topName = "Top")
        firrtlOptions = FirrtlExecutionOptions(firrtlCircuit = Some(circuit))
      }
      firrtl.Driver.getCircuit(manager) shouldBe Success(circuit)
    }
    "Only one of inputFileNameOverride, firrtlSource, and firrtlCircuit can be used at a time" in {
      val manager1 = new ExecutionOptionsManager("test") with HasFirrtlOptions {
        commonOptions = CommonOptions(topName = "Top")
        firrtlOptions = FirrtlExecutionOptions(firrtlCircuit = Some(circuit),
                                               firrtlSource = Some(input))
      }
      val manager2 = new ExecutionOptionsManager("test") with HasFirrtlOptions {
        commonOptions = CommonOptions(topName = "Top")
        firrtlOptions = FirrtlExecutionOptions(inputFileNameOverride = "hi",
                                               firrtlSource = Some(input))
      }
      val manager3 = new ExecutionOptionsManager("test") with HasFirrtlOptions {
        commonOptions = CommonOptions(topName = "Top")
        firrtlOptions = FirrtlExecutionOptions(inputFileNameOverride = "hi",
                                               firrtlCircuit = Some(circuit))
      }
      assert(firrtl.Driver.getCircuit(manager1).isFailure)
      assert(firrtl.Driver.getCircuit(manager2).isFailure)
      assert(firrtl.Driver.getCircuit(manager3).isFailure)
    }
    "various annotations can be created from command line, currently:" - {
      "inline annotation" in {
        val optionsManager = new ExecutionOptionsManager("test") with HasFirrtlOptions

        optionsManager.parse(
          Array("--inline", "module,module.submodule,module.submodule.instance")
        ) should be(true)

        val firrtlOptions = optionsManager.firrtlOptions
        firrtlOptions.annotations.length should be(3)
        firrtlOptions.annotations.foreach(_ shouldBe an[InlineAnnotation])
      }
      "infer-rw annotation" in {
        val optionsManager = new ExecutionOptionsManager("test") with HasFirrtlOptions

        optionsManager.parse(
          Array("--infer-rw")
        ) should be(true)

        val firrtlOptions = optionsManager.firrtlOptions
        firrtlOptions.annotations.length should be(1)
        firrtlOptions.annotations.head should be(InferReadWriteAnnotation)
      }
      "repl-seq-mem annotation" in {
        val optionsManager = new ExecutionOptionsManager("test") with HasFirrtlOptions

        optionsManager.parse(
          Array("--repl-seq-mem", "-c:circuit1:-i:infile1:-o:outfile1")
        ) should be(true)

        val firrtlOptions = optionsManager.firrtlOptions
        firrtlOptions.annotations.length should be(1)
        firrtlOptions.annotations.head should matchPattern {
          case ReplSeqMemAnnotation("infile1", "outfile1") =>
        }
      }
    }
  }

  // Deprecated
  "Annotations can be read implicitly from the name of the circuit" - {
    val input = """|circuit foo :
                   |  module foo :
                   |    input x : UInt<8>
                   |    output y : UInt<8>
                   |    y <= x""".stripMargin
    val top = "foo"
    val optionsManager = new ExecutionOptionsManager("test") with HasFirrtlOptions {
      commonOptions = commonOptions.copy(topName = top)
      firrtlOptions = firrtlOptions.copy(firrtlSource = Some(input))
    }
    val annoFile = new File(optionsManager.commonOptions.targetDirName, top + ".anno")
    val vFile = new File(optionsManager.commonOptions.targetDirName, top + ".v")
    "Using Driver.getAnnotations" in {
      copyResourceToFile("/annotations/SampleAnnotations.anno", annoFile)
      optionsManager.firrtlOptions.annotations.length should be(0)
      val annos = Driver.getAnnotations(optionsManager)
      annos.length should be(12) // 9 from circuit plus 3 general purpose
      annos.count(_.isInstanceOf[InlineAnnotation]) should be(9)
      annoFile.delete()
      vFile.delete()
    }
    "Using Driver.execute" in {
      copyResourceToFile("/annotations/SampleAnnotations.anno", annoFile)
      Driver.execute(optionsManager) match {
        case r: FirrtlExecutionSuccess =>
          val annos = r.circuitState.annotations
          annos.count(_.isInstanceOf[InlineAnnotation]) should be(9)
      }
      annoFile.delete()
      vFile.delete()
    }
  }

  // Deprecated
  "Annotations can be read using annotationFileNameOverride" in {
    val optionsManager = new ExecutionOptionsManager("test") with HasFirrtlOptions {
      commonOptions = commonOptions.copy(topName = "a.fir")
      firrtlOptions = firrtlOptions.copy(
        annotationFileNameOverride = "SampleAnnotations"
      )
    }
    val annotationsTestFile = new File(optionsManager.commonOptions.targetDirName, optionsManager.firrtlOptions.annotationFileNameOverride + ".anno")
    copyResourceToFile("/annotations/SampleAnnotations.anno", annotationsTestFile)
    optionsManager.firrtlOptions.annotations.length should be(0)
    val annos = Driver.getAnnotations(optionsManager)
    annos.length should be(12) // 9 from circuit plus 3 general purpose
    annos.count(_.isInstanceOf[InlineAnnotation]) should be(9)
    annotationsTestFile.delete()
  }

  // Deprecated
  "Supported LegacyAnnotations will be converted automagically" in {
    val testDir = createTestDirectory("test")
    val annoFilename = "LegacyAnnotations.anno"
    val annotationsTestFile = new File(testDir, annoFilename)
    val optionsManager = new ExecutionOptionsManager("test") with HasFirrtlOptions {
      commonOptions = commonOptions.copy(topName = "test", targetDirName = testDir.toString)
      firrtlOptions = firrtlOptions.copy(
        annotationFileNames = List(annotationsTestFile.toString)
      )
    }
    copyResourceToFile(s"/annotations/$annoFilename", annotationsTestFile)
    val annos = Driver.getAnnotations(optionsManager)

    val cname = CircuitName("foo")
    val mname = ModuleName("bar", cname)
    val compname = ComponentName("x", mname)
    import firrtl.passes.clocklist._
    import firrtl.passes.memlib._
    import firrtl.passes.wiring._
    import firrtl.transforms._
    val expected = List(
      InlineAnnotation(cname),
      ClockListAnnotation(mname, "output"),
      InferReadWriteAnnotation,
      ReplSeqMemAnnotation("input", "output"),
      NoDedupMemAnnotation(compname),
      NoDedupAnnotation(mname),
      SourceAnnotation(compname, "pin"),
      SinkAnnotation(compname, "pin"),
      BlackBoxResourceAnno(mname, "resource"),
      BlackBoxInlineAnno(mname, "name", "text"),
      BlackBoxTargetDirAnno("targetdir"),
      NoDCEAnnotation,
      DontTouchAnnotation(compname),
      OptimizableExtModuleAnnotation(mname)
    )
    for (e <- expected) {
      annos should contain(e)
    }
  }

  // Deprecated
  "UNsupported LegacyAnnotations should throw errors" in {
    val testDir = createTestDirectory("test")
    val annoFilename = "InvalidLegacyAnnotations.anno"
    val annotationsTestFile = new File(testDir, annoFilename)
    copyResourceToFile(s"/annotations/$annoFilename", annotationsTestFile)

    import net.jcazevedo.moultingyaml._
    val text = FileUtils.getText(annotationsTestFile)
    val yamlAnnos = text.parseYaml match {
      case YamlArray(xs) => xs
    }

    // Since each one should error, emit each one to an anno file and try to read it
    for ((anno, i) <- yamlAnnos.zipWithIndex) {
      val annoFile = new File(testDir, s"anno_$i.anno")
      val fw = new FileWriter(annoFile)
      fw.write(YamlArray(anno).prettyPrint)
      fw.close()
      val optionsManager = new ExecutionOptionsManager("test") with HasFirrtlOptions {
        commonOptions = commonOptions.copy(topName = "test", targetDirName = testDir.toString)
        firrtlOptions = firrtlOptions.copy(
          annotationFileNames = List(annoFile.toString)
        )
      }
      (the[Exception] thrownBy {
        Driver.getAnnotations(optionsManager)
      }).getMessage should include("Old-style annotations")
    }
  }

  "Annotations can be read from multiple files" in {
    val filename = "SampleAnnotations.anno.json"
    val optionsManager = new ExecutionOptionsManager("test") with HasFirrtlOptions {
      commonOptions = commonOptions.copy(topName = "a.fir")
      firrtlOptions = firrtlOptions.copy(
        annotationFileNames = List.fill(2)(filename) // just read the safe file twice
      )
    }
    val annotationsTestFile = new File(optionsManager.commonOptions.targetDirName, filename)
    copyResourceToFile(s"/annotations/$filename", annotationsTestFile)
    optionsManager.firrtlOptions.annotations.length should be(0)
    val annos = Driver.getAnnotations(optionsManager)
    annos.length should be(21) // 18 from files plus 3 general purpose
    annos.count(_.isInstanceOf[InlineAnnotation]) should be(18)
    annotationsTestFile.delete()
  }

  "Annotations can be created from the command line and read from a file at the same time" in {
    val optionsManager = new ExecutionOptionsManager("test") with HasFirrtlOptions
    val targetDir = new File(optionsManager.commonOptions.targetDirName)
    val annoFile = new File(targetDir, "annotations.anno")

    optionsManager.parse(
      Array("--infer-rw", "-faf", annoFile.toString)
    ) should be(true)

    copyResourceToFile("/annotations/SampleAnnotations.anno.json", annoFile)

    val firrtlOptions = optionsManager.firrtlOptions
    firrtlOptions.annotations.length should be(1) // infer-rw

    val anns = Driver.getAnnotations(optionsManager)
    anns should contain(BlackBoxTargetDirAnno(".")) // built in to getAnnotations
    anns should contain(InferReadWriteAnnotation) // --infer-rw
    anns.collect { case a: InlineAnnotation => a }.length should be(9) // annotations file

    annoFile.delete()
  }

  "Circuits are emitted on properly" - {
    val input =
      """|circuit Top :
         |  module Top :
         |    output foo : UInt<32>
         |    inst c of Child
         |    inst e of External
         |    foo <= tail(add(c.foo, e.foo), 1)
         |  module Child :
         |    output foo : UInt<32>
         |    inst e of External
         |    foo <= e.foo
         |  extmodule External :
         |    output foo : UInt<32>
      """.stripMargin

    "To a single file with file extension depending on the compiler by default" in {
      Seq(
        "none" -> "./Foo.fir",
        "low" -> "./Foo.lo.fir",
        "high" -> "./Foo.hi.fir",
        "middle" -> "./Foo.mid.fir",
        "verilog" -> "./Foo.v",
        "mverilog" -> "./Foo.v",
        "sverilog" -> "./Foo.sv"
      ).foreach { case (compilerName, expectedOutputFileName) =>
        info(s"$compilerName -> $expectedOutputFileName")
        val manager = new ExecutionOptionsManager("test") with HasFirrtlOptions {
          commonOptions = CommonOptions(topName = "Foo")
          firrtlOptions = FirrtlExecutionOptions(firrtlSource = Some(input), compilerName = compilerName)
        }

        firrtl.Driver.execute(manager) match {
          case success: FirrtlExecutionSuccess =>
            success.emitted.size should not be (0)
            success.circuitState.annotations.length should be > (0)
          case a: FirrtlExecutionFailure =>
            fail(s"Got a FirrtlExecutionFailure! Expected FirrtlExecutionSuccess. Full message:\n${a.message}")
        }



        val file = new File(expectedOutputFileName)
        file.exists() should be(true)
        file.delete()
      }
    }
    "To a single file per module if OneFilePerModule is specified" in {
      Seq(
        "none" -> Seq("./Top.fir", "./Child.fir"),
        "low" -> Seq("./Top.lo.fir", "./Child.lo.fir"),
        "high" -> Seq("./Top.hi.fir", "./Child.hi.fir"),
        "middle" -> Seq("./Top.mid.fir", "./Child.mid.fir"),
        "verilog" -> Seq("./Top.v", "./Child.v"),
        "mverilog" -> Seq("./Top.v", "./Child.v"),
        "sverilog" -> Seq("./Top.sv", "./Child.sv")
      ).foreach { case (compilerName, expectedOutputFileNames) =>
        info(s"$compilerName -> $expectedOutputFileNames")
        val manager = new ExecutionOptionsManager("test") with HasFirrtlOptions {
          firrtlOptions = FirrtlExecutionOptions(firrtlSource = Some(input),
            compilerName = compilerName,
            emitOneFilePerModule = true)
        }

        firrtl.Driver.execute(manager) match {
          case success: FirrtlExecutionSuccess =>
            success.emitted.size should not be (0)
            success.circuitState.annotations.length should be > (0)
          case failure: FirrtlExecutionFailure =>
            fail(s"Got a FirrtlExecutionFailure! Expected FirrtlExecutionSuccess. Full message:\n${failure.message}")
        }

        for (name <- expectedOutputFileNames) {
          val file = new File(name)
          file.exists() should be(true)
          file.delete()
        }
      }
    }
  }

  "The Driver is sensitive to the file extension of input files" - {
    val design = "GCDTester"
    val outputDir = createTestDirectory("DriverFileExtensionSensitivity")
    val verilogFromFir = new File(outputDir, s"$design.fromfir.v")
    val verilogFromPb = new File(outputDir, s"$design.frompb.v")
    val commonArgs = Array("-X", "verilog", "--info-mode", "use")
    ".fir means FIRRTL file" in {
      val inFile = new File(getClass.getResource(s"/integration/$design.fir").getFile)
      val args = Array("-i", inFile.getAbsolutePath, "-o", verilogFromFir.getAbsolutePath) ++ commonArgs
      Driver.execute(args)
    }
    ".pb means ProtoBuf file" in {
      val inFile = new File(getClass.getResource(s"/integration/$design.pb").getFile)
      val args = Array("-i", inFile.getAbsolutePath, "-o", verilogFromPb.getAbsolutePath) ++ commonArgs
      Driver.execute(args)
    }
    "Both paths do the same thing" in {
      val s1 = FileUtils.getText(verilogFromFir)
      val s2 = FileUtils.getText(verilogFromPb)
      s1 should equal (s2)
    }
  }

  "Directory deleter is handy for cleaning up after tests" - {
    "for example making a directory tree, and deleting it looks like" in {
      FileUtils.makeDirectory("dog/fox/wolf")
      val dir = new File("dog/fox/wolf")
      dir.exists() should be(true)
      dir.isDirectory should be(true)

      FileUtils.deleteDirectoryHierarchy("wolf") should be(false)
      FileUtils.deleteDirectoryHierarchy("dog") should be(true)
      dir.exists() should be(false)
    }
  }
}

class VcdSuppressionSpec extends FirrtlFlatSpec {
  "Default option" should "generate a vcd" in {
    val prefix = "ZeroPortMem"

    def testIfVcdCreated(suppress: Boolean): Unit = {
      val optionsManager = new ExecutionOptionsManager("test") with HasFirrtlOptions

      val testDir = compileFirrtlTest(prefix, "/features", Seq.empty, Seq.empty)
      val harness = new File(testDir, s"top.cpp")
      copyResourceToFile(cppHarnessResourceName, harness)

      verilogToCpp(prefix, testDir, Seq.empty, harness, suppress) #&&
      cppToExe(prefix, testDir) ! loggingProcessLogger

      assert(executeExpectingSuccess(prefix, testDir))

      val vcdFile = new File(s"$testDir/dump.vcd")
      vcdFile.exists() should be(! suppress)
    }

    testIfVcdCreated(suppress = false)
    testIfVcdCreated(suppress = true)
  }
}
