// See LICENSE for license details.
package firrtl.transforms
package TopWiring

import firrtl._
import firrtl.ir._
import firrtl.passes.{InferTypes, ResolveKinds, ResolveFlows, ExpandConnects}
import firrtl.annotations._
import firrtl.Mappers._
import firrtl.stage.Forms

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 = 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 = new firrtl.analyses.InstanceGraph(state.circuit)
    val cMap = instGraph.getChildrenInstances.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.InstanceGraph,
                       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
        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 = instgraph.getChildrenInstances(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 = new firrtl.analyses.InstanceGraph(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
  }
}
