/*                                                                           *\
**        _____ ____  _____   _____    __                                    **
**       / ___// __ \/  _/ | / /   |  / /   HDL Core                         **
**       \__ \/ /_/ // //  |/ / /| | / /    (c) Dolu, All rights reserved    **
**      ___/ / ____// // /|  / ___ |/ /___                                   **
**     /____/_/   /___/_/ |_/_/  |_/_____/                                   **
**                                                                           **
**      This library is free software; you can redistribute it and/or        **
**    modify it under the terms of the GNU Lesser General Public             **
**    License as published by the Free Software Foundation; either           **
**    version 3.0 of the License, or (at your option) any later version.     **
**                                                                           **
**      This library is distributed in the hope that it will be useful,      **
**    but WITHOUT ANY WARRANTY; without even the implied warranty of         **
**    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU      **
**    Lesser General Public License for more details.                        **
**                                                                           **
**      You should have received a copy of the GNU Lesser General Public     **
**    License along with this library.                                       **
\*                                                                           */
package spinal.core


import org.apache.commons.io.FileUtils

import java.io.{BufferedWriter, File, FileWriter}
import spinal.core.internals._

import java.text.SimpleDateFormat
import java.util.Date
import scala.collection.mutable
import scala.collection.mutable.{ArrayBuffer, ListBuffer}
import scala.io.Source
import scala.util.matching.Regex


//TODO
// undefined

trait SpinalMode
object VHDL    extends SpinalMode
object Verilog extends SpinalMode
object SystemVerilog extends SpinalMode


case class DumpWaveConfig(depth: Int = 0, vcdPath: String = "wave.vcd")

case class ObfuscateConfig (
                             keepDefinitionNames: Boolean = false,
                             keepInstanceNames: Boolean = false,
                             keepClkResetNames: Boolean = false,
                             prefix: String = "oo_",
                             hierarchyKeepLevel: Int = 0
                           )


/**
 * target device
 */
case class Device(vendor: String = "?",
                  family: String = "?",
                  name: String = "?",
                  supportBootResetKind : Boolean = true){
  def isVendorDefault = vendor == "?"
}
object Device{
  val ALTERA = Device(vendor = "altera")
  val XILINX = Device(vendor = "xilinx")
  val LATTICE = Device(vendor = "lattice")
  val ACTEL = Device(vendor = "actel")
  val EFINIX = Device(vendor = "efinix")
  val ASIC = Device(vendor = "asic", supportBootResetKind = false)
  val NONE = Device(vendor = "none")

  def get = GlobalData.get.config.device
}


/** Policy for memory blackbox replacement. 
  * @see [[https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Sequential%20logic/memory.html#blackboxing-policy RAM/ROM Blackboxing policy documentation]]
  */
trait MemBlackboxingPolicy {
  def translationInterest(topology: MemTopology): Boolean

  def onUnblackboxable(topology: MemTopology, who: Any, message: String): Unit

  def generateUnblackboxableError(topology: MemTopology, who: Any, message: String): Unit = {
    PendingError(s"${this.getClass} is not able to blackbox ${topology.mem}\n  write ports : ${topology.writes.size} \n  readAsync ports : ${topology.readsAsync.size} \n  readSync ports : ${topology.readsSync.size} \n  readWrite ports : ${topology.readWriteSync.size} \n  readAsyncWrite ports : ${topology.readAsyncWrite.size}\n  -> $message")
  }
}

/** Blackbox every memory that is replaceable.
  *
  * @see [[https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Sequential%20logic/memory.html#blackboxing-policy RAM/ROM Blackboxing policy documentation]]
  */
object blackboxAllWhatsYouCan extends MemBlackboxingPolicy {
  override def translationInterest(topology: MemTopology): Boolean = true

  override def onUnblackboxable(topology: MemTopology, who: Any, message: String): Unit = {}
}

/** Blackbox all memory.
  *
  * Throw an error on unblackboxable memory.
  * @see [[https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Sequential%20logic/memory.html#blackboxing-policy RAM/ROM Blackboxing policy documentation]]
  */
object blackboxAll extends MemBlackboxingPolicy {
  override def translationInterest(topology: MemTopology): Boolean = true

  override def onUnblackboxable(topology: MemTopology, who: Any, message: String): Unit = generateUnblackboxableError(topology, who, message)
}

/** Blackbox memory specified by the user and memory that is known to be uninferable (mixed-width, …).
  * 
  * Throw an error on unblackboxable memory.
  * @see [[https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Sequential%20logic/memory.html#blackboxing-policy RAM/ROM Blackboxing policy documentation]]
  */
object blackboxRequestedAndUninferable extends MemBlackboxingPolicy {

  override def translationInterest(topology: MemTopology): Boolean = {
    if(blackboxOnlyIfRequested.translationInterest(topology))                      return true
    if(topology.readsAsync.exists(_.readUnderWrite != writeFirst))                 return true
    if(topology.readsSync.exists(_.readUnderWrite != readFirst))                   return true
    if(topology.writeReadSameAddressSync.exists(_._2.readUnderWrite != readFirst)) return true
//    if(topology.readWriteSync.exists(_._2.readUnderWrite != readFirst)) return true
    return false
  }

  override def onUnblackboxable(topology: MemTopology, who: Any, message: String): Unit = generateUnblackboxableError(topology, who, message)
}

/** Blackbox memory specified by the user.
  * 
  * Throw an error on unblackboxable memory.
  * @see [[https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Sequential%20logic/memory.html#blackboxing-policy RAM/ROM Blackboxing policy documentation]]
  */
object blackboxOnlyIfRequested extends MemBlackboxingPolicy {
  override def translationInterest(topology: MemTopology): Boolean = {
    topology.mem.forceMemToBlackboxTranslation
  }

  override def onUnblackboxable(topology: MemTopology, who: Any, message: String): Unit = generateUnblackboxableError(topology, who, message)
}

/** Blackbox every memory which use write port with byte mask.
  * 
  * Useful because synthesis tool don't support an unified way to infer byte mask in verilog/VHDL.
  * Throw an error on unblackboxable memory.
  * @see [[https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Sequential%20logic/memory.html#blackboxing-policy RAM/ROM Blackboxing policy documentation]]
  */
object blackboxByteEnables extends MemBlackboxingPolicy {
  override def translationInterest(topology: MemTopology): Boolean = {
    if(topology.writes.exists(_.mask != null) && topology.mem.initialContent == null) return true
    if(topology.readWriteSync.exists(_.mask != null) && topology.mem.initialContent == null) return true
    false
  }

  override def onUnblackboxable(topology: MemTopology, who: Any, message: String): Unit = generateUnblackboxableError(topology, who, message)
}


/**
  * Spinal configuration for the generation of the RTL
  *
  * @see [[https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Other%20language%20features/vhdl_generation.html#vhdl-and-verilog-generation VHDL and Verilog generation doc]]
  */
case class SpinalConfig(mode                           : SpinalMode = null,
                        flags                          : mutable.HashSet[Any] = mutable.HashSet[Any](),
                        debugComponents                : mutable.HashSet[Class[_]] = mutable.HashSet[Class[_]](),
                        keepAll                        : Boolean = false,
                        defaultConfigForClockDomains   : ClockDomainConfig = ClockDomainConfig(),
                        onlyStdLogicVectorAtTopLevelIo : Boolean = false,
                        defaultClockDomainFrequency    : IClockDomainFrequency = UnknownFrequency(),
                        var targetDirectory            : String = SpinalConfig.defaultTargetDirectory,
                        oneFilePerComponent            : Boolean = false,
                        var netlistFileName            : String = null,
                        dumpWave                       : DumpWaveConfig = null,
                        globalPrefix                   : String = "",
                        var privateNamespace           : Boolean = false,
                        var formalAsserts              : Boolean = false,
                        anonymSignalPrefix             : String = null,
                        var device                     : Device = Device(),
                        inlineRom                      : Boolean = false,
                        caseRom                        : Boolean = false,
                        romReuse                       : Boolean = false,
                        genVhdlPkg                     : Boolean = true,
                        verbose                        : Boolean = false,
                        mergeAsyncProcess              : Boolean = false,
                        mergeSyncProcess               : Boolean = true,
                        asyncResetCombSensitivity      : Boolean = false,
                        anonymSignalUniqueness         : Boolean = false,
                        inlineConditionalExpression    : Boolean = false,
                        nameWhenByFile                 : Boolean = true,
                        var genLineComments            : Boolean = false,
                        noRandBoot                     : Boolean = false,
                        randBootFixValue               : Boolean = true,
                        noAssert                       : Boolean = false,
                        fixToWithWrap                  : Boolean = true,
                        headerWithDate                 : Boolean = false,
                        headerWithRepoHash             : Boolean = true,
                        removePruned                   : Boolean = false,
                        allowOutOfRangeLiterals        : Boolean = false,
                        var dontCareGenAsZero          : Boolean = false,
                        var obfuscateNames             : Boolean = false,
                        obfuscate                      : ObfuscateConfig = ObfuscateConfig(),
                        var normalizeComponentClockDomainName : Boolean = false,
                        var devicePhaseHandler         : PhaseDeviceHandler = PhaseDeviceDefault,
                        phasesInserters                : ArrayBuffer[(ArrayBuffer[Phase]) => Unit] = ArrayBuffer[(ArrayBuffer[Phase]) => Unit](),
                        transformationPhases           : ArrayBuffer[Phase] = ArrayBuffer[Phase](),
                        memBlackBoxers                 : ArrayBuffer[Phase] = ArrayBuffer[Phase] (/*new PhaseMemBlackBoxerDefault(blackboxNothing)*/),
                        rtlHeader                      : String = null,
                        scopeProperties                : mutable.LinkedHashMap[ScopeProperty[_], Any] = mutable.LinkedHashMap[ScopeProperty[_], Any](),
                        private [core] var _withEnumString : Boolean = true,
                        var enumPrefixEnable           : Boolean = true,
                        var enumGlobalEnable           : Boolean = false,
                        bitVectorWidthMax              : Int = 4096,
                        var singleTopLevel             : Boolean = true,
                        var noAssertAtTimeZero         : Boolean = false,
                        var cutLongExpressions         : Boolean = true,
                        var withTimescale              : Boolean = true,
                        var printFilelist              : Boolean = true,
                        var emitFullComponentBindings  : Boolean = true,
                        var reportIncludeSourceLocation: Boolean = false,
                        var reportSourceLocationFormat : String = SpinalConfig.defaultReportSourceLocationFormat,
                        var svInterface                : Boolean = false
){
  def generate       [T <: Component](gen: => T): SpinalReport[T] = Spinal(this)(gen)
  def generateVhdl   [T <: Component](gen: => T): SpinalReport[T] = Spinal(this.copy(mode = VHDL))(gen)
  def generateVerilog[T <: Component](gen: => T): SpinalReport[T] = Spinal(this.copy(mode = Verilog))(gen)
  def generateSystemVerilog[T <: Component](gen: => T): SpinalReport[T] = Spinal(this.copy(mode = SystemVerilog))(gen)

  def apply[T <: Component](gen : => T): SpinalReport[T] = {
    Spinal(this)(gen)
  }

  def applyToGlobalData(globalData: GlobalData): Unit = {
    globalData.scalaLocatedEnable = debugComponents.nonEmpty
    globalData.scalaLocatedComponents ++= debugComponents
    globalData.commonClockConfig  = defaultConfigForClockDomains
    for((p, v) <- scopeProperties){
      p.asInstanceOf[ScopeProperty[Any]].set(v)
    }
  }

  def dumpWave(depth: Int = 0, vcdPath: String = "wave.vcd"): SpinalConfig = this.copy(dumpWave=DumpWaveConfig(depth,vcdPath))

  def addTransformationPhase(phase: Phase): SpinalConfig = {
    transformationPhases += phase
    this
  }

  def isSystemVerilog = mode == SystemVerilog

  def withPrivateNamespace : this.type = { privateNamespace = true; this }

  def addStandardMemBlackboxing(policy: MemBlackboxingPolicy): this.type = {
    memBlackBoxers += new PhaseMemBlackBoxingDefault(policy)
    this
  }

  def withoutAssert : SpinalConfig = this.copy(noAssert = true)
  def withoutEnumString() : this.type = {
    _withEnumString = false
    this
  }
  def includeSynthesis : this.type = {flags += GenerationFlags.synthesis; this}
  def includeFormal : this.type = {flags += GenerationFlags.formal; formalAsserts = true; this}
  def includeSimulation : this.type = {flags += GenerationFlags.simulation; this}


  def setScopeProperty[T](scopeProperty: ScopeProperty[T], value : T): this.type ={
    scopeProperties(scopeProperty) = value
    this
  }

  def setScopeProperty[T](value: ScopePropertyValue): this.type ={
    scopeProperties(value.dady) = value
    this
  }

  def withGlobalEnum: this.type ={
    enumGlobalEnable = true
    this
  }

  def withoutLineComment: this.type = {
    genLineComments = false
    this
  }
  def withLineComment: this.type = {
    genLineComments = true
    this
  }
  def addOptions(parser: scopt.OptionParser[Unit]): Unit = {
    import parser._
    opt[String]("target-directory") action { (v, c) => targetDirectory = v }
  }
}
class GenerationFlags {
  def isEnabled = GlobalData.get.config.flags.contains(this)
  def apply[T](block : => T) : T = if(isEnabled) block else null.asInstanceOf[T]
}

object GenerationFlags{
  object synthesis extends GenerationFlags
  object formal extends GenerationFlags
  object simulation extends GenerationFlags

  implicit def generationFlagsToBoolean(flag : GenerationFlags) : Boolean = flag.isEnabled
}

object SpinalConfig{
  def shell[T <: Component](args: Seq[String]): SpinalConfig = {
    val parser = new scopt.OptionParser[SpinalConfig]("SpinalCore") {
      opt[Unit]("vhdl")                   action { (_, c) => c.copy(mode = VHDL)         } text("Select the VHDL mode")
      opt[Unit]("verilog")                action { (_, c) => c.copy(mode = Verilog)      } text("Select the Verilog mode")
      opt[String]('o', "targetDirectory") action { (v, c) => c.copy(targetDirectory = v) } text("Set the target directory")
    }

    parser.parse(args, SpinalConfig()) match {
      case Some(config) => config
      case None         => ???
    }
  }

  def addOptions(): Unit = {

  }

  var defaultTargetDirectory: String = System.getenv().getOrDefault("SPINAL_TARGET_DIR", ".")
  var defaultReportSourceLocationFormat: String =
    System.getenv().getOrDefault("SPINAL_REPORT_SOURCE_LOCATION_FORMAT", "$SEVERITY($FILE:$LINE) ")
}


/**
 * Spinal report give after the generation of the RTL
 */
class SpinalReport[T <: Component]() {
  var toplevel: T     = null.asInstanceOf[T]
  val prunedSignals   = mutable.Set[BaseType]()
  val unusedSignals   = mutable.Set[BaseType]()
  var counterRegister = 0
  var toplevelName: String = null
  var globalData : GlobalData = null


  val generatedSourcesPaths  = mutable.LinkedHashSet[String]()
  val blackboxesSourcesPaths = mutable.LinkedHashSet[String]()
  val blackboxesIncludeDir = mutable.LinkedHashSet[String]()
  def rtlSourcesPaths        = generatedSourcesPaths ++ blackboxesSourcesPaths
  def rtlIncludeDirs        = blackboxesIncludeDir


  def printUnused() : this.type = {
    unusedSignals.foreach(bt => SpinalWarning(s"Unused signal detected : $bt"))
    this
  }

  def printPruned() : this.type = {
    prunedSignals.foreach(bt => SpinalWarning(s"Pruned signal detected : $bt"))
    this
  }

  def printPrunedIo() : this.type = {
    prunedSignals.filter(_.dir != null).foreach(bt => SpinalWarning(s"Pruned signal detected : $bt"))
    this
  }

  def printRtl() : this.type = {
    for(f <- generatedSourcesPaths){
      println(scala.io.Source.fromFile(f).mkString)
    }
    this
  }

  def printZeroWidth() : this.type = {
    if(globalData.zeroWidths.isEmpty) return this
    globalData.zeroWidths.foreach{case (c, n) =>
      SpinalWarning(s"${c}/${n.toString}")
    }
    this
  }


  def mergeRTLSource(fileName: String = null): Unit = {

    val bb_vhdl    = new mutable.LinkedHashSet[String]()
    val bb_verilog = new mutable.LinkedHashSet[String]()

    /** Split verilog/vhdl path */
    blackboxesSourcesPaths.foreach{ path =>
      val vhdl_regex    = """.*\.(vhdl|vhd)""".r
      val verilog_regex = """.*\.(v)""".r
      val systemVerilog_regex = """.*\.(sv)""".r

      path.toLowerCase match {
        case vhdl_regex(f)    => bb_vhdl    += path
        case verilog_regex(f) => bb_verilog += path
        case systemVerilog_regex(f) => bb_verilog += path
        case _                => SpinalWarning(s"Merging blackbox sources : Extension file not supported (${path})")
      }
    }


    /** Merge a list of path into one file */
    def mergeFile(listPath: mutable.LinkedHashSet[String], fileName: String) {
      val str = new StringBuilder() //We use a temporary string to build the result, allowing overriding on input file as output

      listPath.foreach{ path =>
        if( new File(path).exists ) {
          val buffer = Source.fromFile(path)
          buffer.getLines.foreach{ line => str. ++= (line + "\n") }
          buffer.close()
        }else{
          SpinalWarning(s"Merging blackbox sources : Path (${new File(path).getAbsolutePath}) not found ")
        }
      }
      val fw = new FileWriter(new File(s"${globalData.config.targetDirectory}/$fileName"))
      val bw = new BufferedWriter(fw)
      bw.write(str.toString())
      bw.close()
      fw.close()
    }

    // Merge vhdl/verilog file
    val nameFile = if(fileName == null) s"${toplevel.definitionName}_bb" else fileName
    if(bb_vhdl.size > 0)   { mergeFile(bb_vhdl,    s"${nameFile}.vhd") }
    if (globalData.config.mode == Verilog) {
      if(bb_verilog.size > 0){ mergeFile(bb_verilog, s"${nameFile}.v") }
    } else if (globalData.config.mode == SystemVerilog) {
      if(bb_verilog.size > 0){ mergeFile(bb_verilog, s"${nameFile}.sv") }
    }

  }

  def getRtlString(): String = {
    assert(generatedSourcesPaths.size == 1)
    scala.io.Source.fromFile(generatedSourcesPaths.head).mkString
  }
}


object Spinal {
  val version = (if(Character.isDigit(spinal.core.Info.version(0))) "v" else "") + spinal.core.Info.version

  def apply[T <: Component](config: SpinalConfig)(gen: => T): SpinalReport[T] = {

    if(config.memBlackBoxers.isEmpty)
      config.addStandardMemBlackboxing(blackboxOnlyIfRequested)
    val configPatched = config.copy(targetDirectory = if(config.targetDirectory.startsWith("~")) System.getProperty( "user.home" ) + config.targetDirectory.drop(1) else config.targetDirectory)

    println({
      SpinalLog.tag("Runtime", Console.YELLOW)
    } + s" SpinalHDL $version    git head : ${spinal.core.Info.gitHash}")


    val runtime = Runtime.getRuntime
    println({
      SpinalLog.tag("Runtime", Console.YELLOW)
    } + s" JVM max memory : ${f"${(runtime.maxMemory()).toFloat / 1048576f}%1.1f"}MiB")

    val curDate: Date = new Date()
    val dateFmt: SimpleDateFormat = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss")
    println({
      SpinalLog.tag("Runtime", Console.YELLOW)
    } + s" Current date : ${dateFmt.format(curDate)}")

    FileUtils.forceMkdir(new File(config.targetDirectory))

    val report = configPatched.mode match {
      case `VHDL`    => SpinalVhdlBoot(configPatched)(gen)
      case `Verilog` => SpinalVerilogBoot(configPatched)(gen)
      case `SystemVerilog` => SpinalVerilogBoot(configPatched)(gen)
      case null => throw new Exception("Please specify mode in SpinalConfig (mode=[Verilog, SystemVerilog, VHDL])")
    }

    println({SpinalLog.tag("Done", Console.GREEN)} + s" at ${f"${Driver.executionTime}%1.3f"}")
    report
  }
}


object SpinalVhdl {
  /** Generate VHDL from a ``SpinalConfig``
  * 
  * @see [[https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Other%20language%20features/vhdl_generation.html#vhdl-and-verilog-generation VHDL and Verilog generation doc]]
  */
  def apply[T <: Component](config: SpinalConfig)(gen: => T): SpinalReport[T] = Spinal(config.copy(mode = VHDL))(gen)

  /** Generate VHDL from a function that return a component
  * 
  * Multiple instances of the component class may be needed, therefore the first argument is not
  * a Component reference, but a function that returns a new component.
  * 
  * @see [[https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Other%20language%20features/vhdl_generation.html#vhdl-and-verilog-generation VHDL and Verilog generation doc]]
  */
  def apply[T <: Component](gen: => T): SpinalReport[T] = SpinalConfig(mode = VHDL).generate(gen)
}


object SpinalVerilog {
/** Generate Verilog from a ``SpinalConfig``
  * 
  * @see [[https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Other%20language%20features/vhdl_generation.html#vhdl-and-verilog-generation VHDL and Verilog generation doc]]
  */
  def apply[T <: Component](config: SpinalConfig)(gen: => T): SpinalReport[T] = Spinal(config.copy(mode = Verilog))(gen)

/** Generate Verilog from a function that return a component
  * 
  * Multiple instances of the component class may be needed, therefore the first argument is not
  * a Component reference, but a function that returns a new component.
  * 
  * @see [[https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Other%20language%20features/vhdl_generation.html#vhdl-and-verilog-generation VHDL and Verilog generation doc]]
  */
  def apply[T <: Component](gen: => T): SpinalReport[T] = SpinalConfig(mode = Verilog).generate(gen)
}

object SpinalSystemVerilog {
  def apply[T <: Component](config: SpinalConfig)(gen: => T): SpinalReport[T] = Spinal(config.copy(mode = SystemVerilog))(gen)
  def apply[T <: Component](gen: => T): SpinalReport[T] = SpinalConfig(mode = SystemVerilog).generate(gen)
}
