// SPDX-License-Identifier: Apache-2.0

package firrtl.passes
package wiring

import firrtl._
import firrtl.ir._

import scala.collection.mutable
import firrtl.annotations._
import firrtl.annotations.AnnotationUtils._
import firrtl.analyses.InstanceKeyGraph
import WiringUtils._
import firrtl.analyses.InstanceKeyGraph.InstanceKey
import firrtl.graph.EulerTour

/** A data store of one sink--source wiring relationship */
case class WiringInfo(source: ComponentName, sinks: Seq[Named], pin: String)

/** A data store of wiring names */
case class WiringNames(compName: String, source: String, sinks: Seq[Named], pin: String)

/** Pass that computes and applies a sequence of wiring modifications
  *
  * @constructor construct a new Wiring pass
  * @param wiSeq the [[WiringInfo]] to apply
  */
class Wiring(wiSeq: Seq[WiringInfo]) extends Pass {
  def run(c: Circuit): Circuit = analyze(c)
    .foldLeft(c) {
      case (cx, (tpe, modsMap)) => cx.copy(modules = cx.modules.map(onModule(tpe, modsMap)))
    }

  /** Converts multiple units of wiring information to module modifications */
  private def analyze(c: Circuit): Seq[(Type, Map[String, Modifications])] = {

    val names = wiSeq
      .map(wi =>
        (wi.source, wi.sinks, wi.pin) match {
          case (ComponentName(comp, ModuleName(source, _)), sinks, pin) =>
            WiringNames(comp, source, sinks, pin)
        }
      )

    val portNames = mutable.Seq.fill(names.size)(Map[String, String]())
    c.modules.foreach { m =>
      val ns = Namespace(m)
      names.zipWithIndex.foreach {
        case (WiringNames(c, so, si, p), i) =>
          portNames(i) = portNames(i) +
            (m.name -> {
              if (si.exists(getModuleName(_) == m.name)) ns.newName(p)
              else ns.newName(tokenize(c).filterNot("[]." contains _).mkString("_"))
            })
      }
    }

    val iGraph = InstanceKeyGraph(c)
    names.zip(portNames).map {
      case (WiringNames(comp, so, si, _), pn) =>
        computeModifications(c, iGraph, comp, so, si, pn)
    }
  }

  /** Converts a single unit of wiring information to module modifications
    *
    * @param c the circuit that will be modified
    * @param iGraph an InstanceGraph representation of the circuit
    * @param compName the name of a component
    * @param source the name of the source component
    * @param sinks a list of sink components/modules that the source
    * should be connected to
    * @param portNames a mapping of module names to new ports/wires
    * that should be generated if needed
    *
    * @return a tuple of the component type and a map of module names
    * to pending modifications
    */
  private def computeModifications(
    c:         Circuit,
    iGraph:    InstanceKeyGraph,
    compName:  String,
    source:    String,
    sinks:     Seq[Named],
    portNames: Map[String, String]
  ): (Type, Map[String, Modifications]) = {

    val sourceComponentType = getType(c, source, compName)
    val sinkComponents: Map[String, Seq[String]] = sinks.collect { case ComponentName(c, ModuleName(m, _)) => (c, m) }
      .foldLeft(new scala.collection.immutable.HashMap[String, Seq[String]]) {
        case (a, (c, m)) => a ++ Map(m -> (Seq(c) ++ a.getOrElse(m, Nil)))
      }

    // Determine "ownership" of sources to sinks via minimum distance
    val owners = sinksToSourcesSeq(sinks, source, iGraph)

    // Determine port and pending modifications for all sink--source
    // ownership pairs
    val meta = new mutable.HashMap[String, Modifications]
      .withDefaultValue(Modifications())

    // only make something a wire if it isn't an output or input already
    def makeWire(m: Modifications, portName: String): Modifications =
      m.copy(addPortOrWire = Some(m.addPortOrWire.getOrElse((portName, DecWire))))
    def makeWireC(m: Modifications, portName: String, c: (String, String)): Modifications =
      m.copy(addPortOrWire = Some(m.addPortOrWire.getOrElse((portName, DecWire))), cons = (m.cons :+ c).distinct)

    val tour = EulerTour(iGraph.graph, iGraph.top)
    // Finds the lowest common ancestor instances for two module names in a design
    def lowestCommonAncestor(moduleA: Seq[InstanceKey], moduleB: Seq[InstanceKey]): Seq[InstanceKey] =
      tour.rmq(moduleA, moduleB)

    owners.foreach {
      case (sink, source) =>
        val lca = lowestCommonAncestor(sink, source)

        // Compute metadata along Sink to LCA paths.
        sink.drop(lca.size - 1).sliding(2).toList.reverse.foreach {
          case Seq(InstanceKey(_, pm), InstanceKey(ci, cm)) =>
            val to = s"$ci.${portNames(cm)}"
            val from = s"${portNames(pm)}"
            meta(pm) = makeWireC(meta(pm), portNames(pm), (to, from))
            meta(cm) = meta(cm).copy(
              addPortOrWire = Some((portNames(cm), DecInput))
            )
          // Case where the sink is the LCA
          case Seq(InstanceKey(_, pm)) =>
            // Case where the source is also the LCA
            if (source.drop(lca.size).isEmpty) {
              meta(pm) = makeWire(meta(pm), portNames(pm))
            } else {
              val InstanceKey(ci, cm) = source.drop(lca.size).head
              val to = s"${portNames(pm)}"
              val from = s"$ci.${portNames(cm)}"
              meta(pm) = makeWireC(meta(pm), portNames(pm), (to, from))
            }
        }

        // Compute metadata for the Sink
        sink.last match {
          case InstanceKey(_, m) =>
            if (sinkComponents.contains(m)) {
              val from = s"${portNames(m)}"
              sinkComponents(m).foreach(to =>
                meta(m) = meta(m).copy(
                  cons = (meta(m).cons :+ ((to, from))).distinct
                )
              )
            }
        }

        // Compute metadata for the Source
        source.last match {
          case InstanceKey(_, m) =>
            val to = s"${portNames(m)}"
            val from = compName
            meta(m) = meta(m).copy(
              cons = (meta(m).cons :+ ((to, from))).distinct
            )
        }

        // Compute metadata along Source to LCA path
        source.drop(lca.size - 1).sliding(2).toList.reverse.map {
          case Seq(InstanceKey(_, pm), InstanceKey(ci, cm)) => {
            val to = s"${portNames(pm)}"
            val from = s"$ci.${portNames(cm)}"
            meta(pm) = meta(pm).copy(
              cons = (meta(pm).cons :+ ((to, from))).distinct
            )
            meta(cm) = meta(cm).copy(
              addPortOrWire = Some((portNames(cm), DecOutput))
            )
          }
          // Case where the source is the LCA
          case Seq(InstanceKey(_, pm)) => {
            // Case where the sink is also the LCA. We do nothing here,
            // as we've created the connecting wire above
            if (sink.drop(lca.size).isEmpty) {} else {
              val InstanceKey(ci, cm) = sink.drop(lca.size).head
              val to = s"$ci.${portNames(cm)}"
              val from = s"${portNames(pm)}"
              meta(pm) = meta(pm).copy(
                cons = (meta(pm).cons :+ ((to, from))).distinct
              )
            }
          }
        }
    }
    (sourceComponentType, meta.toMap)
  }

  /** Apply modifications to a module */
  private def onModule(t: Type, map: Map[String, Modifications])(m: DefModule) = {
    map.get(m.name) match {
      case None => m
      case Some(l) =>
        val defines = mutable.ArrayBuffer[Statement]()
        val connects = mutable.ArrayBuffer[Statement]()
        val ports = mutable.ArrayBuffer[Port]()
        l.addPortOrWire match {
          case None =>
          case Some((s, dt)) =>
            dt match {
              case DecInput  => ports += Port(NoInfo, s, Input, t)
              case DecOutput => ports += Port(NoInfo, s, Output, t)
              case DecWire   => defines += DefWire(NoInfo, s, t)
            }
        }
        connects ++= (l.cons.map {
          case ((l, r)) =>
            Connect(NoInfo, toExp(l), toExp(r))
        })
        m match {
          case Module(i, n, ps, body) =>
            val stmts = body match {
              case Block(sx) => sx
              case s         => Seq(s)
            }
            Module(i, n, ps ++ ports, Block(List() ++ defines ++ stmts ++ connects))
          case ExtModule(i, n, ps, dn, p) => ExtModule(i, n, ps ++ ports, dn, p)
        }
    }
  }
}
