// SPDX-License-Identifier: Apache-2.0
package firrtl.transforms
package TopWiring

import firrtl._
import firrtl.ir._
import firrtl.passes.{ExpandConnects, InferTypes, LowerTypes, ResolveFlows, ResolveKinds}
import firrtl.annotations._
import firrtl.Mappers._
import firrtl.analyses.InstanceKeyGraph
import firrtl.stage.Forms
import firrtl.options.Dependency

import collection.mutable

/** Annotation for optional output files, and what directory to put those files in (absolute path) * */
case class TopWiringOutputFilesAnnotation(
  dirName: String,
  outputFunction: (String, Seq[((ComponentName, Type, Boolean, Seq[String], String), Int)],
    CircuitState) => CircuitState)
    extends NoTargetAnnotation

/** Annotation for indicating component to be wired, and what prefix to add to the ports that are generated */
case class TopWiringAnnotation(target: ComponentName, prefix: String) extends SingleTargetAnnotation[ComponentName] {
  def duplicate(n: ComponentName) = this.copy(target = n)
}

/** Punch out annotated ports out to the toplevel of the circuit.
  *    This also has an option to pass a function as a parmeter to generate
  *    custom output files as a result of the additional ports
  * @note This *does* work for deduped modules
  */
class TopWiringTransform extends Transform with DependencyAPIMigration {

  override def prerequisites = Forms.MidForm
  override def optionalPrerequisites = Seq.empty
  override def optionalPrerequisiteOf = Dependency(LowerTypes) +: Forms.MidEmitters

  override def invalidates(a: Transform): Boolean = a match {
    case InferTypes | ResolveKinds | ResolveFlows | ExpandConnects => true
    case _                                                         => false
  }

  type InstPath = Seq[String]

  /** Get the names of the targets that need to be wired */
  private def getSourceNames(state: CircuitState): Map[ComponentName, String] = {
    state.annotations.collect {
      case TopWiringAnnotation(srcname, prefix) =>
        (srcname -> prefix)
    }.toMap.withDefaultValue("")
  }

  /** Get the names of the modules which include the  targets that need to be wired */
  private def getSourceModNames(state: CircuitState): Seq[String] = {
    state.annotations.collect { case TopWiringAnnotation(ComponentName(_, ModuleName(srcmodname, _)), _) => srcmodname }
  }

  /** Get the Type of each wire to be connected
    *
    * Find the definition of each wire in sourceList, and get the type and whether or not it's a port
    * Update the results in sourceMap
    */
  private def getSourceTypes(
    sourceList:    Map[ComponentName, String],
    sourceMap:     mutable.Map[String, Seq[(ComponentName, Type, Boolean, InstPath, String)]],
    currentmodule: ModuleName,
    state:         CircuitState
  )(s:             Statement
  ): Statement = s match {
    // If target wire, add name and size to to sourceMap
    case w: IsDeclaration =>
      if (sourceList.keys.toSeq.contains(ComponentName(w.name, currentmodule))) {
        val (isport, tpe, prefix) = w match {
          case d: DefWire     => (false, d.tpe, sourceList(ComponentName(w.name, currentmodule)))
          case d: DefNode     => (false, d.value.tpe, sourceList(ComponentName(w.name, currentmodule)))
          case d: DefRegister => (false, d.tpe, sourceList(ComponentName(w.name, currentmodule)))
          case d: Port        => (true, d.tpe, sourceList(ComponentName(w.name, currentmodule)))
          case _ => throw new Exception(s"Cannot wire this type of declaration! ${w.serialize}")
        }
        sourceMap.get(currentmodule.name) match {
          case Some(xs: Seq[(ComponentName, Type, Boolean, InstPath, String)]) =>
            sourceMap.update(
              currentmodule.name,
              xs :+ ((ComponentName(w.name, currentmodule), tpe, isport, Seq[String](w.name), prefix))
            )
          case None =>
            sourceMap(currentmodule.name) = Seq(
              (ComponentName(w.name, currentmodule), tpe, isport, Seq[String](w.name), prefix)
            )
        }
      }
      w // Return argument unchanged (ok because DefWire has no Statement children)
    // If not, apply to all children Statement
    case _ => s.map(getSourceTypes(sourceList, sourceMap, currentmodule, state))
  }

  /** Get the Type of each port to be connected
    *
    * Similar to getSourceTypes, but specifically for ports since they are not found in statements.
    * Find the definition of each port in sourceList, and get the type and whether or not it's a port
    * Update the results in sourceMap
    */
  private def getSourceTypesPorts(
    sourceList:    Map[ComponentName, String],
    sourceMap:     mutable.Map[String, Seq[(ComponentName, Type, Boolean, InstPath, String)]],
    currentmodule: ModuleName,
    state:         CircuitState
  )(s:             Port
  ): CircuitState = s match {
    // If target port, add name and size to to sourceMap
    case w: IsDeclaration =>
      if (sourceList.keys.toSeq.contains(ComponentName(w.name, currentmodule))) {
        val (isport, tpe, prefix) = w match {
          case d: Port => (true, d.tpe, sourceList(ComponentName(w.name, currentmodule)))
          case _ => throw new Exception(s"Cannot wire this type of declaration! ${w.serialize}")
        }
        sourceMap.get(currentmodule.name) match {
          case Some(xs: Seq[(ComponentName, Type, Boolean, InstPath, String)]) =>
            sourceMap.update(
              currentmodule.name,
              xs :+ ((ComponentName(w.name, currentmodule), tpe, isport, Seq[String](w.name), prefix))
            )
          case None =>
            sourceMap(currentmodule.name) = Seq(
              (ComponentName(w.name, currentmodule), tpe, isport, Seq[String](w.name), prefix)
            )
        }
      }
      state // Return argument unchanged (ok because DefWire has no Statement children)
    // If not, apply to all children Statement
    case _ => state
  }

  /** Create a map of Module name to target wires under this module
    *
    * These paths are relative but cross module (they refer down through instance hierarchy)
    */
  private def getSourcesMap(state: CircuitState): Map[String, Seq[(ComponentName, Type, Boolean, InstPath, String)]] = {
    val sSourcesModNames = getSourceModNames(state)
    val sSourcesNames = getSourceNames(state)
    val instGraph = firrtl.analyses.InstanceKeyGraph(state.circuit)
    val cMap = instGraph.getChildInstances.map {
      case (m, wdis) =>
        (m -> wdis.map { case wdi => (wdi.name, wdi.module) }.toSeq)
    }.toMap
    val topSort = instGraph.moduleOrder.reverse

    // Map of component name to relative instance paths that result in a debug wire
    val sourcemods: mutable.Map[String, Seq[(ComponentName, Type, Boolean, InstPath, String)]] =
      mutable.Map(sSourcesModNames.map(_ -> Seq()): _*)

    state.circuit.modules.foreach { m =>
      m.map(getSourceTypes(sSourcesNames, sourcemods, ModuleName(m.name, CircuitName(state.circuit.main)), state))
    }
    state.circuit.modules.foreach { m =>
      m.ports.foreach { p =>
        Seq(p).map(
          getSourceTypesPorts(sSourcesNames, sourcemods, ModuleName(m.name, CircuitName(state.circuit.main)), state)
        )
      }
    }

    for (mod <- topSort) {
      val seqChildren: Seq[(ComponentName, Type, Boolean, InstPath, String)] = cMap(mod.name).flatMap {
        case (inst, module) =>
          sourcemods.get(module).map(_.map { case (a, b, c, path, p) => (a, b, c, inst +: path, p) })
      }.flatten
      if (seqChildren.nonEmpty) {
        sourcemods(mod.name) = sourcemods.getOrElse(mod.name, Seq()) ++ seqChildren
      }
    }

    sourcemods.toMap
  }

  /** Process a given DefModule
    *
    * For Modules that contain or are in the parent hierarchy to modules containing target wires
    * 1. Add ports for each target wire this module is parent to
    * 2. Connect these ports to ports of instances that are parents to some number of target wires
    */
  private def onModule(
    sources:      Map[String, Seq[(ComponentName, Type, Boolean, InstPath, String)]],
    portnamesmap: mutable.Map[String, String],
    instgraph:    firrtl.analyses.InstanceKeyGraph,
    namespacemap: Map[String, Namespace]
  )(module:       DefModule
  ): DefModule = {
    val namespace = namespacemap(module.name)
    sources.get(module.name) match {
      case Some(p) =>
        val newPorts = p.map {
          case (ComponentName(cname, _), tpe, _, path, prefix) => {
            val newportname = portnamesmap.get(prefix + path.mkString("_")) match {
              case Some(pn) => pn
              case None => {
                val npn = namespace.newName(prefix + path.mkString("_"))
                portnamesmap(prefix + path.mkString("_")) = npn
                npn
              }
            }
            Port(NoInfo, newportname, Output, tpe)
          }
        }

        // Add connections to Module
        val childInstances = instgraph.getChildInstances.toMap
        module match {
          case m: Module =>
            val connections: Seq[Connect] = p.map {
              case (ComponentName(cname, _), _, _, path, prefix) =>
                val modRef = portnamesmap.get(prefix + path.mkString("_")) match {
                  case Some(pn) => WRef(pn)
                  case None => {
                    portnamesmap(prefix + path.mkString("_")) = namespace.newName(prefix + path.mkString("_"))
                    WRef(portnamesmap(prefix + path.mkString("_")))
                  }
                }
                path.size match {
                  case 1 => {
                    val leafRef = WRef(path.head.mkString(""))
                    Connect(NoInfo, modRef, leafRef)
                  }
                  case _ => {
                    val instportname = portnamesmap.get(prefix + path.tail.mkString("_")) match {
                      case Some(ipn) => ipn
                      case None => {
                        val instmod = childInstances(module.name).collectFirst {
                          case wdi if wdi.name == path.head => wdi.module
                        }.get
                        val instnamespace = namespacemap(instmod)
                        portnamesmap(prefix + path.tail.mkString("_")) =
                          instnamespace.newName(prefix + path.tail.mkString("_"))
                        portnamesmap(prefix + path.tail.mkString("_"))
                      }
                    }
                    val instRef = WSubField(WRef(path.head), instportname)
                    Connect(NoInfo, modRef, instRef)
                  }
                }
            }
            m.copy(ports = m.ports ++ newPorts, body = Block(Seq(m.body) ++ connections))
          case e: ExtModule =>
            e.copy(ports = e.ports ++ newPorts)
        }
      case None => module // unchanged if no paths
    }
  }

  /** Dummy function that is currently unused. Can be used to fill an outputFunction requirment in the future */
  def topWiringDummyOutputFilesFunction(
    dir:     String,
    mapping: Seq[((ComponentName, Type, Boolean, InstPath, String), Int)],
    state:   CircuitState
  ): CircuitState = {
    state
  }

  def execute(state: CircuitState): CircuitState = {

    val outputTuples: Seq[
      (String, (String, Seq[((ComponentName, Type, Boolean, InstPath, String), Int)], CircuitState) => CircuitState)
    ] = state.annotations.collect {
      case TopWiringOutputFilesAnnotation(td, of) => (td, of)
    }
    // Do actual work of this transform
    val sources = getSourcesMap(state)
    val (nstate, nmappings) = if (sources.nonEmpty) {
      val portnamesmap: mutable.Map[String, String] = mutable.Map()
      val instgraph = InstanceKeyGraph(state.circuit)
      val namespacemap = state.circuit.modules.map { case m => (m.name -> Namespace(m)) }.toMap
      val modulesx = state.circuit.modules.map(onModule(sources, portnamesmap, instgraph, namespacemap))
      val newCircuit = state.circuit.copy(modules = modulesx)
      val mappings = sources(state.circuit.main).zipWithIndex

      val annosx = state.annotations.filter {
        case _: TopWiringAnnotation => false
        case _ => true
      }

      (state.copy(circuit = newCircuit, annotations = annosx), mappings)
    } else { (state, List.empty) }
    //Generate output files based on the mapping.
    outputTuples.map { case (dir, outputfunction) => outputfunction(dir, nmappings, nstate) }
    nstate
  }
}
