/*                                                                           *\
**        _____ ____  _____   _____    __                                    **
**       / ___// __ \/  _/ | / /   |  / /   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.internals

import spinal.core._

import scala.collection.mutable
import scala.collection.mutable.{ArrayBuffer, ListBuffer}


class PhaseVerilog(pc: PhaseContext, report: SpinalReport[_]) extends PhaseMisc with VerilogBase {
  import pc._
  globalPrefix = pc.config.globalPrefix

  var outFile: java.io.FileWriter = null
  def rtlName = (if(pc.config.netlistFileName == null)(topLevel.definitionName + (if(pc.config.isSystemVerilog) ".sv" else ".v")) else pc.config.netlistFileName)
  def targetPath = pc.config.targetDirectory + "/" + rtlName

  override def impl(pc: PhaseContext): Unit = {

    report.toplevelName = pc.topLevel.definitionName
    if (!pc.config.oneFilePerComponent) {
      report.generatedSourcesPaths += targetPath
      outFile = new java.io.FileWriter(targetPath)
      outFile.write(VhdlVerilogBase.getHeader("//", pc.config.rtlHeader, topLevel, config.headerWithDate, config.headerWithRepoHash))

      if(pc.config.withTimescale) outFile.write("`timescale 1ns/1ps")

      emitEnumPackage(outFile)

      if(!svInterface.isEmpty) {
        outFile.write("\n")
        for ((name, interface) <- svInterface) {
          outFile.write(interface.result())
        }
      }

      val componentsText = ArrayBuffer[() => String]()
      for (c <- sortedComponents) {
        if (!c.isInBlackBoxTree) {
          componentsText += compile(c)
        }
      }

      for(e <- componentsText.reverse){
        outFile.write(e())
      }

      val bbImplStrings = mutable.HashSet[String]()
      sortedComponents.foreach{
        case bb : BlackBox if bb.impl != null => {
          val str = bb.impl.getVerilog()
          if(!bbImplStrings.contains(str)) {
            bbImplStrings += str
            outFile.write("\n")
            outFile.write(str)
            outFile.write("\n")
          }
        }
        case _ =>
      }

      outFile.flush()
      outFile.close()
    } else {
      val fileList: mutable.LinkedHashSet[String] = new mutable.LinkedHashSet()
      // dump Enum define to define.v instead attach that on every .v file
      if(enums.nonEmpty){
        val defineFileName = pc.config.targetDirectory + "/enumdefine" + (if(pc.config.isSystemVerilog) ".sv" else ".v")
        val defineFile = new java.io.FileWriter(defineFileName)
        emitEnumPackage(defineFile)
        defineFile.flush()
        defineFile.close()
        fileList += defineFileName
      }

      // dump Interface define to (interface.definitionName).v
      if(!svInterface.isEmpty) {
        for ((name, interface) <- svInterface) {
          val ifFileName = pc.config.targetDirectory + "/" + name + (if(pc.config.isSystemVerilog) ".sv" else ".v")
          val ifFile = new java.io.FileWriter(ifFileName)
          ifFile.write("\n")
          ifFile.write(interface.result())
          ifFile.flush()
          ifFile.close()
          fileList += ifFileName
        }
      }

      val bbImplStrings = mutable.HashSet[String]()
      for (c <- sortedComponents) {
        val moduleContent = compile(c)()
        val targetFilePath = pc.config.targetDirectory + "/" +  (if(pc.config.netlistFileName == null)(c.definitionName + (if(pc.config.isSystemVerilog) ".sv" else ".v")) else pc.config.netlistFileName)
        report.generatedSourcesPaths += targetFilePath

        if (!moduleContent.contains("replaced by")) {
          if (!c.isInBlackBoxTree) {
            outFile = new java.io.FileWriter(targetFilePath)
            outFile.write(VhdlVerilogBase.getHeader("//", pc.config.rtlHeader, c, config.headerWithDate, config.headerWithRepoHash))
            if(pc.config.withTimescale) outFile.write("`timescale 1ns/1ps ")
//            emitEnumPackage(outFile)
            outFile.write(moduleContent)
            outFile.flush()
            outFile.close()
            fileList += targetFilePath
          }
          c match {
            case bb: BlackBox =>
              fileList ++= bb.listRTLPath
              if (bb.impl != null) {
                val str = bb.impl.getVerilog()
                if(!bbImplStrings.contains(str)) {
                  outFile = new java.io.FileWriter(targetFilePath)
                  outFile.write(VhdlVerilogBase.getHeader("//", pc.config.rtlHeader, c, config.headerWithDate, config.headerWithRepoHash))
                  if(pc.config.withTimescale) outFile.write("`timescale 1ns/1ps ")
                  outFile.write(str)
                  outFile.flush()
                  outFile.close()
                  fileList += targetFilePath
                  bbImplStrings += str
                }
              }
            case _ =>
          }
        }
      }

      if(pc.config.printFilelist){
        val fileListFile = new java.io.FileWriter(pc.config.targetDirectory + "/" + topLevel.definitionName + ".lst")
        fileList.foreach(file => fileListFile.write(file.replace("//", "/") + "\n"))
        fileListFile.flush()
        fileListFile.close()
      }
    }
  }

  val allocateAlgoIncrementalBase = globalData.allocateAlgoIncremental()
  val usedDefinitionNames = mutable.HashSet[String]()


  val romCache = mutable.HashMap[String, String]()
  def compile(component: Component): () => String = {
    val componentBuilderVerilog = new ComponentEmitterVerilog(
      c                           = component,
      systemVerilog               = pc.config.isSystemVerilog,
      verilogBase                 = this,
      algoIdIncrementalBase       = allocateAlgoIncrementalBase,
      mergeAsyncProcess           = config.mergeAsyncProcess,
      asyncResetCombSensitivity   = config.asyncResetCombSensitivity,
      anonymSignalPrefix          = if(pc.config.anonymSignalUniqueness) globalData.anonymSignalPrefix + "_" + component.definitionName else globalData.anonymSignalPrefix,
      nativeRom                   = config.inlineRom,
      nativeRomFilePrefix         = rtlName,
      caseRom                     = config.caseRom,
      emitedComponentRef          = emitedComponentRef,
      emitedRtlSourcesPath        = report.generatedSourcesPaths,
      spinalConfig                = pc.config,
      pc                          = pc,
      romCache                    = romCache
    )

    if(component.parentScope == null && pc.config.dumpWave != null) {
      componentBuilderVerilog.logics ++=
        s"""
  initial begin
    $$dumpfile("${pc.config.dumpWave.vcdPath}");
    $$dumpvars(${pc.config.dumpWave.depth}, ${component.definitionName});
  end
"""
    }

    val trace = componentBuilderVerilog.getTrace()
    val oldComponent = emitedComponent.getOrElse(trace, null)

    if (oldComponent == null || component.definitionNameNoMerge && component.definitionName != oldComponent.definitionName) {
      assert(!usedDefinitionNames.contains(component.definitionName) || component.isInBlackBoxTree, s"Component '${component}' with definition name '${component.definitionName}' was already used once for a different layout\n${component.getScalaLocationLong}")
      usedDefinitionNames += component.definitionName
      emitedComponent += (trace -> component)
      () => componentBuilderVerilog.result
    } else {
      emitedComponentRef.put(component, oldComponent)
      val originalName = component.definitionName
      component match{
        case x: BlackBox =>
        case _ => component.definitionName = oldComponent.definitionName
      }
      () => s"\n//${originalName} replaced by ${oldComponent.definitionName}\n"
    }
  }

  val emitedComponent    = mutable.Map[ComponentEmitterTrace, Component]()
  val emitedComponentRef = new java.util.concurrent.ConcurrentHashMap[Component,Component]()

  def emitEnumPackage(out: java.io.FileWriter): Unit = {
    val ret = new StringBuilder()

    ret ++= "\n"
    for ((enumDef, encodings) <- enums) {
      val enumName = enumDef.getName()
      for (encoding <- encodings) {
        val encodingName = encoding.getName()
        val bitCount     = encoding.getWidth(enumDef)
        val vhdlEnumType = emitEnumType(enumDef, encoding, "")

//        ret ++= s"`define $vhdlEnumType [${bitCount - 1}:0]\n"
        if(enumDef.isGlobalEnable) {
          for (element <- enumDef.elements) {
            ret ++= s"`define ${emitEnumLiteral(element, encoding, "")} ${idToBits(element, encoding)}\n"
          }

          ret ++= "\n"
        }
      }
    }

    def idToBits[T <: SpinalEnum](senum: SpinalEnumElement[T], encoding: SpinalEnumEncoding): String = {
      val str    = encoding.getValue(senum).toString(2)
      val length = encoding.getWidth(senum.spinalEnum)
      length.toString + "'b" + ("0" * (length - str.length)) + str
    }

    out.write(ret.result())
  }


  def emitFunctions(component: Component, ret: StringBuilder): Unit = {
    val alreadyEmitted = mutable.Set[String]()
    component.dslBody.walkStatements(s => s.walkExpression {
      case node: CastEnumToEnum =>
        val encodingSrc = node.input.getEncoding
        val enumDef     = node.getDefinition
        val encodingDst = node.getEncoding
        val fName       = getReEncodingFuntion(enumDef, encodingSrc, encodingDst)

        if (!alreadyEmitted.contains(fName)) {
          alreadyEmitted += fName
          ret ++= s"  function $fName(${emitEnumType(enumDef, encodingSrc)} that);\n"
          ret ++= "  begin\n"
          ret ++= "    case(that) \n"
          for (e <- enumDef.elements) {
            ret ++= s"      ${emitEnumLiteral(e, encodingSrc)} : $fName =  ${emitEnumLiteral(e, encodingDst)};\n"
          }
          ret ++= s"      default : $fName =  ${emitEnumLiteral(enumDef.elements.head, encodingDst)};\n"
          ret ++= "    endcase\n"
          ret ++= "  end\n"
          ret ++= "  endfunction\n\n"
        }
      case _ =>
    })
  }

  
}

class PhaseInterface(pc: PhaseContext) extends PhaseNetlist{
  def emitInterface(interface: Interface): StringBuilder = {
    import pc._
    var ret = new StringBuilder()
    val theme = new Tab2 //TODO add into SpinalConfig
    val generic = if(interface.genericElements.isEmpty) ""
      else
        "#(\n" + interface.genericElements.map{case (name, useless, default) =>
          if(default == null)
            s"${theme.porttab}parameter ${name},\n"
          else
            s"${theme.porttab}parameter ${name} = ${default},\n"
        }.reduce(_ + _).stripSuffix(",\n") + "\n) "
    ret ++= s"interface ${interface.definitionName} ${generic}() ;\n\n"
    val sizeZero = mutable.HashSet[String]()
    def genBase[T <: Data](ret: StringBuilder, name: String, name1: String, elem: T): Unit = {
      elem match {
        case _: Bool => {
          val size = ""
          ret ++= f"${theme.porttab}logic  ${size}%-8s ${name}_${name1} ;\n"
        }
        case node: BitVector => {
          val size = interface.widthGeneric.get(node) match {
            case Some(x) => s"[${x}-1:0]"
            case None => if (node.getWidth > 0)
              s"[${node.getWidth - 1}:0]"
            else {
              globalData.nodeAreInferringWidth = false
              val width = node.getWidth
              globalData.nodeAreInferringWidth = true
              s"[${width - 1}:0]"
            }
          }
          if (size != "[-1:0]")
            ret ++= f"${theme.porttab}logic  ${size}%-8s ${name}_${name1} ;\n"
          else
            sizeZero.add(name)
        }
        case x => genSig(ret, s"${name}_${name1}", x)
      }
    }
    def genSig[T <: Data](ret: StringBuilder, name: String, elem: T): Unit = {
      elem match {
        case node: Interface if !node.thisIsNotSVIF => {

          val genericFlat = node.genericElements

          val t = if (genericFlat.nonEmpty) {
            val ret = genericFlat.map{ e =>
              interface.IFGeneric.get((node, e._1)) match {
                case Some(value) => e._1 -> value
                case None => {
                  e match {
                    //TODO:case (name: String, bt: BaseType, _)      => name -> s"${emitExpression(bt.getTag(classOf[GenericValue]).get.e)}"
                    case (name: String, rs: VerilogValues, _) => name -> s"${rs.v}"
                    case (name: String, s: String, _)         => name -> s"""\"$s\""""
                    case (name: String, i: Int, _)            => name -> s"$i"
                    case (name: String, d: Double, _)         => name -> s"$d"
                    case (name: String, b: Boolean, _)        => name -> s"${if(b) "1'b1" else "1'b0"}"
                    case (name: String, b: BigInt, _)         => name -> s"${b.toString(16).size*4}'h${b.toString(16)}"
                    case _                                 => SpinalError(s"The generic type ${"\""}${e._1} - ${e._2}${"\""} of the interface ${"\""}${node.definitionName}${"\""} is not supported in Verilog")
                  }
                }
              }
            }
            val namelens = ret.map(_._1.size).max
            val exprlens = ret.map(_._2.size).max
            val params   = ret.map(t =>  s"    .%-${namelens}s (%-${exprlens}s )".format(t._1, t._2))
            s"""${node.definitionName} #(
               |${params.mkString(",\n")}
               |  )""".stripMargin
          } else f"${node.definitionName}%-15s"
          val  cl = if(genericFlat.nonEmpty) "\n" else ""
          ret ++= f"${theme.porttab}${t} ${name}();\n${cl}"//TODO:parameter
        }
        case nodes: Bundle => {
          for ((name1, node) <- nodes.elementsCache) {
            genBase(ret, name, name1, node)
          }
        }
        case nodes: Vec[_] => {
          for ((node, idx) <- nodes.zipWithIndex) {
            genBase(ret, name, idx.toString, node)
          }
        }
        case _ => {
          val size = elem match {
            case _: Bool => ""
            case node: BitVector => interface.widthGeneric.get(node) match {
              case Some(x) => s"[${x}-1:0]"
              case None => if(node.getWidth > 0)
                s"[${node.getWidth - 1}:0]"
              else {
                globalData.nodeAreInferringWidth = false
                val width = node.getWidth
                globalData.nodeAreInferringWidth = true
                s"[${width - 1}:0]"
              }
            }
            case x => LocatedPendingError(s"The SystemVerilog interface feature is still an experimental feature. In interface, ${x} is not supported yet")
          }
          if(size != "[-1:0]")
            ret ++= f"${theme.porttab}logic  ${size}%-8s ${name} ;\n"
          else
            sizeZero.add(name)
        }
      }
    }
    for ((name, elem) <- interface.elementsCache) {
      genSig(ret, name, elem)
    }
    ret ++= "\n"
    interface.allModPort
      .foreach{case x =>
        var modportString = new StringBuilder()
        modportString ++= s"${theme.porttab}modport ${x} (\n"

        val toplevel = globalData.toplevel
        val phase = globalData.phaseContext.topLevel
        globalData.toplevel = null
        globalData.phaseContext.topLevel = null
        val c = new Component {
          val y = interface.clone().asInstanceOf[interface.type]
          y.callModPort(x)
        }
        globalData.toplevel = toplevel
        globalData.phaseContext.topLevel = phase

        def genModportSig[T <: Data](modportString: StringBuilder, name: String, elem: T): Unit = {
          elem match {
            case elem: Interface if !elem.thisIsNotSVIF => {
              //TODO:check more than one modport has same `in` `out` direction
              val modport = if(elem.checkModport().isEmpty) {
                LocatedPendingError(s"no suitable modport found for ${elem}")
                ""
              } else {
                elem.checkModport().head
              }
              modportString ++= f"${theme.porttab}${theme.porttab}.${name}(${name}.${modport}),\n"
            }
            case elem: Bundle => {
              for((name1, node) <- elem.elementsCache) {
                genModportSig(modportString, s"${name}_${name1}", node)
              }
            }
            case elem: Vec[_] => {
              for((node, idx) <- elem.zipWithIndex) {
                genModportSig(modportString, s"${name}_${idx}", node)
              }
            }
            case elem => {
              val dir = elem.dir match {
                case `in`    => "input "
                case `out`   => "output"
                case `inout` => "inout "
                case _       => throw new Exception(s"Unknown direction in interface ${interface}: ${elem}"); ""
              }
              if(!sizeZero.contains(name))
                modportString ++= f"${theme.porttab}${theme.porttab}${dir}%-15s ${name},\n"
            }
          }
        }
        for ((name, elem) <- c.y.elementsCache) {
          genModportSig(modportString, name, elem)
        }
        ret ++= modportString.toString().stripSuffix(",\n") + "\n" + s"${theme.porttab});\n\n"
      }
    ret ++= "endinterface\n\n"
    ret
  }

  override def impl(pc: PhaseContext): Unit = {
    import pc._

    //locate root interface name
    val allocated = mutable.HashSet[Data]()
    //rename myif_a to myif.a
    walkDeclarations {
      case node: BaseType if(node.hasTag(IsInterface)) => {
        def insertIFmap(): Unit = {
          node.rootIFList().foreach{interface =>
            val interfaceString = emitInterface(interface)
            svInterface.get(interface.definitionName) match {
              case Some(s) => if(s != interfaceString) {
                globalScope.lock = false
                interface.setDefinitionName(globalScope.allocateName(interface.definitionName))
                globalScope.lock = true
                insertIFmap()
              }
              case None => svInterface += interface.definitionName -> interfaceString
            }
          }
        }
        insertIFmap()
        node.rootIFList().foreach{intf =>
          if(intf.getName() == null || intf.getName() == "")
            PendingError(s"INTERFACE SHOULD HAVE NAME: ${node.toStringMultiLine} at \n${node.getScalaLocationLong}")
        }
        val rootIF = node.rootIF()
        if(!allocated.contains(rootIF)) {
          rootIF.setName(node.component.localNamingScope.allocateName(rootIF.getName()))
          allocated += rootIF
        }
        val IFlist = node.rootIFList()
        def getElemName(cache: ArrayBuffer[(String, Data)], name: String): Option[(String, Data)] = {
          cache.flatMap{
            case (a, x: Bundle) => getElemName(x.elementsCache, s"${name}_${a}").map(x => (x._1.stripPrefix("_"), x._2))
            case (a, x: Vec[_]) => getElemName(x.elements, s"${name}_${a}").map(x => (x._1.stripPrefix("_"), x._2))
            case (a, x) => if(x == node) Some((s"${name}_${a}".stripPrefix("_"), x)) else None
          }.headOption
        }
        val newName = IFlist match {
          case head :: tail => tail.foldLeft((head, List(head.getName()))){case ((nowIf, nameList), node) =>
            (node, nowIf.elementsCache.find(_._2 == node).get._1 :: nameList)//TODO:error handle on find.get
          }._2.reverse.reduce(_ + "." + _) + "." +
            getElemName(IFlist.last.elementsCache, "").getOrElse("no_name", null)._1//TODO:error handle on find.get
        }
        node.name = newName
      }
      case _ =>
    }
  }
}
