// SPDX-License-Identifier: Apache-2.0

package firrtl.passes
package wiring

import firrtl._
import firrtl.ir._
import firrtl.Utils._
import firrtl.Mappers._
import firrtl.analyses.InstanceKeyGraph.InstanceKey
import firrtl.traversals.Foreachers._

import scala.collection.mutable
import firrtl.annotations._
import firrtl.annotations.AnnotationUtils._
import firrtl.analyses.{InstanceGraph, InstanceKeyGraph}

/** Declaration kind in lineage (e.g. input port, output port, wire)
  */
sealed trait DecKind
case object DecInput extends DecKind
case object DecOutput extends DecKind
case object DecWire extends DecKind

/** Store of pending wiring information for a Module */
case class Modifications(
  addPortOrWire: Option[(String, DecKind)] = None,
  cons:          Seq[(String, String)] = Seq.empty) {

  override def toString: String = serialize("")

  def serialize(tab: String): String = s"""
                                          |$tab addPortOrWire: $addPortOrWire
                                          |$tab cons: $cons
                                          |""".stripMargin
}

/** A lineage tree representing the instance hierarchy in a design
  */
@deprecated("Use DiGraph/InstanceGraph", "FIRRTL 1.1.1")
case class Lineage(
  name:         String,
  children:     Seq[(String, Lineage)] = Seq.empty,
  source:       Boolean = false,
  sink:         Boolean = false,
  sourceParent: Boolean = false,
  sinkParent:   Boolean = false,
  sharedParent: Boolean = false,
  addPort:      Option[(String, DecKind)] = None,
  cons:         Seq[(String, String)] = Seq.empty) {

  def map(f: Lineage => Lineage): Lineage =
    this.copy(children = children.map { case (i, m) => (i, f(m)) })

  override def toString: String = shortSerialize("")

  def shortSerialize(tab: String): String = s"""
                                               |$tab name: $name,
                                               |$tab children: ${children.map(c =>
    tab + "   " + c._2.shortSerialize(tab + "    ")
  )}
                                               |""".stripMargin

  def foldLeft[B](z: B)(op: (B, (String, Lineage)) => B): B =
    this.children.foldLeft(z)(op)

  def serialize(tab: String): String = s"""
                                          |$tab name: $name,
                                          |$tab source: $source,
                                          |$tab sink: $sink,
                                          |$tab sourceParent: $sourceParent,
                                          |$tab sinkParent: $sinkParent,
                                          |$tab sharedParent: $sharedParent,
                                          |$tab addPort: $addPort
                                          |$tab cons: $cons
                                          |$tab children: ${children.map(c =>
    tab + "   " + c._2.serialize(tab + "    ")
  )}
                                          |""".stripMargin
}

object WiringUtils {
  @deprecated("Use DiGraph/InstanceGraph", "FIRRTL 1.1.1")
  type ChildrenMap = mutable.HashMap[String, Seq[(String, String)]]

  /** Given a circuit, returns a map from module name to children
    * instance/module names
    */
  @deprecated("Use DiGraph/InstanceGraph", "FIRRTL 1.1.1")
  def getChildrenMap(c: Circuit): ChildrenMap = {
    val childrenMap = new ChildrenMap()
    def getChildren(mname: String)(s: Statement): Unit = s match {
      case s: DefInstance =>
        childrenMap(mname) = childrenMap(mname) :+ ((s.name, s.module))
      case s => s.foreach(getChildren(mname))
    }
    c.modules.foreach { m =>
      childrenMap(m.name) = Nil
      m.foreach(getChildren(m.name))
    }
    childrenMap
  }

  /** Returns a module's lineage, containing all children lineages as well
    */
  @deprecated("Use DiGraph/InstanceGraph", "FIRRTL 1.1.1")
  def getLineage(childrenMap: ChildrenMap, module: String): Lineage =
    Lineage(module, childrenMap(module).map { case (i, m) => (i, getLineage(childrenMap, m)) })

  /** Return a map of sink instances to source instances that minimizes
    * distance
    *
    * @param sinks a sequence of sink modules
    * @param source the source module
    * @param i a graph representing a circuit
    * @return a map of sink instance names to source instance names
    * @throws WiringException if a sink is equidistant to two sources
    */
  @deprecated(
    "This method can lead to non-determinism in your compiler pass and exposes internal details." +
      " Please file an issue with firrtl if you have a use case!",
    "Firrtl 1.4"
  )
  def sinksToSources(sinks: Seq[Named], source: String, i: InstanceGraph): Map[Seq[WDefInstance], Seq[WDefInstance]] = {
    // The order of owners influences the order of the results, it thus needs to be deterministic with a  LinkedHashMap.
    val owners = new mutable.LinkedHashMap[Seq[WDefInstance], Vector[Seq[WDefInstance]]]
    val queue = new mutable.Queue[Seq[WDefInstance]]
    val visited = new mutable.HashMap[Seq[WDefInstance], Boolean].withDefaultValue(false)

    val sourcePaths = i.fullHierarchy.collect { case (k, v) if k.module == source => v }
    sourcePaths.flatten.foreach { l =>
      queue.enqueue(l)
      owners(l) = Vector(l)
    }

    val sinkModuleNames = sinks.map(getModuleName).toSet
    val sinkPaths = i.fullHierarchy.collect { case (k, v) if sinkModuleNames.contains(k.module) => v }
    // sinkInsts needs to have unique entries but is also iterated over which is why we use a LinkedHashSet
    val sinkInsts = mutable.LinkedHashSet() ++ sinkPaths.flatten

    /** If we're lucky and there is only one source, then that source owns
      * all sinks. If we're unlucky, we need to do a full (slow) BFS
      * to figure out who owns what. Currently, the BFS is not
      * performant.
      *
      * [todo] The performance of this will need to be improved.
      * Possible directions are that if we're purely source-under-sink
      * or sink-under-source, then ownership is trivially a mapping
      * down/up. Ownership seems to require a BFS if we have
      * sources/sinks not under sinks/sources.
      */
    if (queue.size == 1) {
      val u = queue.dequeue()
      sinkInsts.foreach { v => owners(v) = Vector(u) }
    } else {
      while (queue.nonEmpty) {
        val u = queue.dequeue()
        visited(u) = true

        val edges = (i.graph.getEdges(u.last).map(u :+ _).toVector :+ u.dropRight(1))

        // [todo] This is the critical section
        edges
          .filter(e => !visited(e) && e.nonEmpty)
          .foreach { v =>
            owners(v) = owners.getOrElse(v, Vector()) ++ owners(u)
            queue.enqueue(v)
          }
      }

      // Check that every sink has one unique owner. The only time that
      // this should fail is if a sink is equidistant to two sources.
      sinkInsts.foreach { s =>
        if (!owners.contains(s) || owners(s).size > 1) {
          throw new WiringException(s"Unable to determine source mapping for sink '${s.map(_.name)}'")
        }
      }
    }

    owners.collect { case (k, v) if sinkInsts.contains(k) => (k, v.flatten) }.toMap
  }

  /** Return a map of sink instances to source instances that minimizes
    * distance
    *
    * @param sinks a sequence of sink modules
    * @param source the source module
    * @param i a graph representing a circuit
    * @return a map of sink instance names to source instance names
    * @throws WiringException if a sink is equidistant to two sources
    */
  private[firrtl] def sinksToSourcesSeq(
    sinks:  Seq[Named],
    source: String,
    i:      InstanceKeyGraph
  ): Seq[(Seq[InstanceKey], Seq[InstanceKey])] = {
    // The order of owners influences the order of the results, it thus needs to be deterministic with a  LinkedHashMap.
    val owners = new mutable.LinkedHashMap[Seq[InstanceKey], Vector[Seq[InstanceKey]]]
    val queue = new mutable.Queue[Seq[InstanceKey]]
    val visited = new mutable.HashMap[Seq[InstanceKey], Boolean].withDefaultValue(false)

    val sourcePaths = i.fullHierarchy.collect { case (k, v) if k.module == source => v }
    sourcePaths.flatten.foreach { l =>
      queue.enqueue(l)
      owners(l) = Vector(l)
    }

    val sinkModuleNames = sinks.map(getModuleName).toSet
    val sinkPaths = i.fullHierarchy.collect { case (k, v) if sinkModuleNames.contains(k.module) => v }
    // sinkInsts needs to have unique entries but is also iterated over which is why we use a LinkedHashSet
    val sinkInsts = mutable.LinkedHashSet() ++ sinkPaths.flatten

    /** If we're lucky and there is only one source, then that source owns
      * all sinks. If we're unlucky, we need to do a full (slow) BFS
      * to figure out who owns what. Currently, the BFS is not
      * performant.
      *
      * [todo] The performance of this will need to be improved.
      * Possible directions are that if we're purely source-under-sink
      * or sink-under-source, then ownership is trivially a mapping
      * down/up. Ownership seems to require a BFS if we have
      * sources/sinks not under sinks/sources.
      */
    if (queue.size == 1) {
      val u = queue.dequeue()
      sinkInsts.foreach { v => owners(v) = Vector(u) }
    } else {
      while (queue.nonEmpty) {
        val u = queue.dequeue()
        visited(u) = true

        val edges = i.graph.getEdges(u.last).map(u :+ _).toVector :+ u.dropRight(1)

        // [todo] This is the critical section
        edges
          .filter(e => !visited(e) && e.nonEmpty)
          .foreach { v =>
            owners(v) = owners.getOrElse(v, Vector()) ++ owners(u)
            queue.enqueue(v)
          }
      }

      // Check that every sink has one unique owner. The only time that
      // this should fail is if a sink is equidistant to two sources.
      sinkInsts.foreach { s =>
        if (!owners.contains(s) || owners(s).size > 1) {
          throw new WiringException(s"Unable to determine source mapping for sink '${s.map(_.name)}'")
        }
      }
    }

    owners.collect { case (k, v) if sinkInsts.contains(k) => (k, v.flatten) }.toSeq
  }

  /** Helper script to extract a module name from a named Module or Target */
  def getModuleName(n: Named): String = {
    n match {
      case ModuleName(m, _)                   => m
      case ComponentName(_, ModuleName(m, _)) => m
      case _                                  => throw new WiringException("Only Components or Modules have an associated Module name")
    }
  }

  /** Determine the Type of a specific component
    *
    * @param c the circuit containing the target module
    * @param module the module containing the target component
    * @param comp the target component
    * @return the component's type
    * @throws WiringException if the module is not contained in the
    * circuit or if the component is not contained in the module
    */
  def getType(c: Circuit, module: String, comp: String): Type = {
    def getRoot(e: Expression): String = e match {
      case r: Reference => r.name
      case i: SubIndex  => getRoot(i.expr)
      case a: SubAccess => getRoot(a.expr)
      case f: SubField  => getRoot(f.expr)
    }
    val eComp = toExp(comp)
    val root = getRoot(eComp)
    var tpe: Option[Type] = None
    def getType(s: Statement): Statement = s match {
      case DefRegister(_, n, t, _, _, _) if n == root =>
        tpe = Some(t)
        s
      case DefWire(_, n, t) if n == root =>
        tpe = Some(t)
        s
      case WDefInstance(_, n, _, t) if n == root =>
        tpe = Some(t)
        s
      case DefNode(_, n, e) if n == root =>
        tpe = Some(e.tpe)
        s
      case sx: DefMemory if sx.name == root =>
        tpe = Some(MemPortUtils.memType(sx))
        sx
      case sx => sx.map(getType)
    }
    val m = c.modules.find(_.name == module).getOrElse {
      throw new WiringException(s"Must have a module named $module")
    }
    tpe = m.ports.find(_.name == root).map(_.tpe)
    m match {
      case Module(i, n, ps, b) => getType(b)
      case e: ExtModule =>
    }
    tpe match {
      case None => throw new WiringException(s"Didn't find $comp in $module!")
      case Some(t) =>
        def setType(e: Expression): Expression = e.map(setType) match {
          case ex: Reference => ex.copy(tpe = t)
          case ex: SubField  => ex.copy(tpe = field_type(ex.expr.tpe, ex.name))
          case ex: SubIndex  => ex.copy(tpe = sub_type(ex.expr.tpe))
          case ex: SubAccess => ex.copy(tpe = sub_type(ex.expr.tpe))
        }
        setType(eComp).tpe
    }
  }
}
