// See LICENSE.SiFive for license details.

package freechips.rocketchip.diplomacy

import Chisel._
import chisel3.experimental.IO
import chisel3.internal.sourceinfo.SourceInfo
import freechips.rocketchip.config.{Field, Parameters}
import freechips.rocketchip.util.HeterogeneousBag

import scala.collection.immutable
import scala.collection.mutable.ListBuffer

/** A field available in [[Parameters]] used to determine whether [[InwardNodeImp.monitor]] will be called. */
case object MonitorsEnabled extends Field[Boolean](true)

/** When rendering the edge in a graphical format, flip the order in which the edges' source and sink are presented.
  *
  * For example, when rendering graphML, yEd by default tries to put the source node vertically above the sink node, but
  * [[RenderFlipped]] inverts this relationship. When a particular [[LazyModule]] contains both source nodes and sink nodes,
  * flipping the rendering of one node's edge will usual produce a more concise visual layout for the [[LazyModule]].
  */
case object RenderFlipped extends Field[Boolean](false)

/** [[RenderedEdge]] can set the color and label of the visualization of the DAG. */
case class RenderedEdge(
  colour:  String,
  label:   String  = "",
  flipped: Boolean = false)

/** [[InwardNodeImp]] defines the types that describe the inward side of the [[BaseNode]].
  *
  * @tparam DI The type of the downward-flowing parameters received on the inner side of the node.
  * @tparam UI The type of the upward-flowing parameters generated by the inner side of the node.
  * @tparam EI The type of the diplomatically-resolved parameters for an Edge connected to the inner side of the node.
  * @tparam BI The type of the [[chisel3.Data]] (usually a [[chisel3.Bundle]]) used when connecting to the inner side of the node,
  *            corresponding to the real hardware interface that is emitted along the graph edge,
  *            generally parameterized by the [[EI]] type.
  */
trait InwardNodeImp[DI, UI, EI, BI <: Data]
{
  /** Creates the inward edge parameters by combining the downward-flowing and upward-flowing parameters for edges
    * that connect to the inward side of this [[BaseNode]].
    *
    * It is left up to a user defining a particular protocol implementation to decide how the parameters flowing through
    * the graph in both directions on this Edge are combined into a single representation.
    *
    * @param pd         The downward-flowing parameters into the node along the edge.
    * @param pu         The upward-flowing parameters going out of the node along the edge.
    * @param p          A view of [[Parameters]] at the point at which the returned edge is being bound.
    * @param sourceInfo [[SourceInfo]] of this edge.
    * @return An inward edge of this node.
    */
  def edgeI(pd: DI, pu: UI, p: Parameters, sourceInfo: SourceInfo): EI

  /** Create an inward bundle parameterized by the inward edge.
    *
    * @param ei Inward edge of this node.
    * @return An outward Bundle of this node parameterized by the negotiated Edge parameters.
    */
  def bundleI(ei: EI): BI

  /** Defines how input parameters can be "mixed" or negotiated together.
    *
    * The default behavior is to just return `pu`.
    *
    * @param pu   The upward-flowing parameters going out of the node along the edge.
    * @param node An inward node to "mix" the upward-flowing parameters into.
    * @return Altered version of the upward-flowing parameters.
    */
  def mixI(pu: UI, node: InwardNode[DI, UI, BI]): UI = pu

  /** Function to generate and attach a monitor for this node input.
    *
    * @param bundle Inward bundle of this node to attach the monitor to.
    * @param edge   Edge of this node used to parameterize the bundle.
    */
  def monitor(bundle: BI, edge: EI): Unit = {}

  /** Define how the edge should be rendered (e.g. in GraphML).
    *
    * @param e Edge to render.
    * @return [[RenderedEdge]] description of how the edge should be generated.
    */
  def render(e: EI): RenderedEdge
}

/** [[OutwardNodeImp]] defines the types that describe the outwards side of the [[BaseNode]].
  *
  * @tparam DO The type of the downward-flowing parameters generated by the outer side of the node
  * @tparam UO Tye type of the upward-flowing parameters received by the outer side of the node
  * @tparam EO The type of the diplomatically-resolved parameters for an Edge connected to the outer side of the node.
  * @tparam BO The type of the [[chisel3.Data]] (usually a [[chisel3.Bundle]]) used when connecting to the outer side of the node,
  *            corresponding to the real hardware interface that is emitted along the graph edge,
  *            generally parameterized by the [[EO]] type.
  */
trait OutwardNodeImp[DO, UO, EO, BO <: Data]
{
  /** Creates the outward edge parameters by combining the downward-flowing and upward-flowing parameters for edges
    * that connect to the outward side of this [[BaseNode]].
    *
    * It is left up to a user defining a particular protocol implementation to decide how the parameters flowing through
    * the graph in both directions on this Edge are combined into a single representation.
    *
    * @param pd         The downward-flowing parameters going out of the node along the edge.
    * @param pu         The upward-flowing parameters into the node along the edge.
    * @param p          A view of [[Parameters]] at the point at which the returned edge is being bound.
    * @param sourceInfo [[SourceInfo]] of this edge.
    * @return An outward edge of this node.
    */
  def edgeO(pd: DO, pu: UO, p: Parameters, sourceInfo: SourceInfo): EO

  /** Create an outward Bundle parameterized by the outward edge.
    *
    * @param eo Outward Edge of this node.
    * @return An outward Bundle of this node parameterized by the negotiated Edge parameters.
    */
  def bundleO(eo: EO): BO

  /** Defines how outward parameters can be "mixed" or negotiated together.
    *
    * The default behavior is to just return `pd`.
    *
    * @param pd   The downward-flowing parameters into the node along the edge.
    * @param node An outward node to "mix" the downward-flowing parameters into.
    * @return Altered version of the downward-flowing parameters.
    */
  def mixO(pd: DO, node: OutwardNode[DO, UO, BO]): DO = pd
}

/** The [[NodeImp]] combines an [[InwardNodeImp]] and an [[OutwardNodeImp]].
  *
  * This allows it to define whether it is a protocol-modifying (bridging) sort of node,
  * or whether it is an adapter type node that just modifies the parameters within a protocol.
  *
  * This class has no members and is solely used for holding type information.
  * Applications of diplomacy should extend [[NodeImp]] with a case object that sets concrete type arguments.
  *
  * @tparam D  Type of the downward-flowing parameters of the node.
  * @tparam U  Type of upward-flowing parameters of the node.
  * @tparam EO Type of the parameters describing an edge on the outer side of the node.
  * @tparam EI Type of the parameters describing an edge on the inner side of the node.
  * @tparam B  Bundle type generated on edges connecting to this node.
  */
abstract class NodeImp[D, U, EO, EI, B <: Data] extends Object
  with InwardNodeImp[D, U, EI, B]
  with OutwardNodeImp[D, U, EO, B]

/** A [[NodeImp]] where the inward and outward edge parameters are of the same type.
  *
  * If, in a given protocol implementation, the parameters visible to the node on the inward side of an edge are
  * the same as the parameters visible to the node on the outward side of an edge,
  * [[SimpleNodeImp]] can be used instead of [[NodeImp]].
  *
  * @tparam D Type of the downward-flowing parameters of the node.
  * @tparam U Type of the upward-flowing parameters of the node.
  * @tparam E Edge Parameters describing the connections on either side of the node.
  * @tparam B Bundle type generated on edges connecting to this node.
  */
abstract class SimpleNodeImp[D, U, E, B <: Data]
  extends NodeImp[D, U, E, E, B] {
  /** Creates the edge parameters by combining the downward-flowing and upward-flowing parameters for edges that connect to this node.
    *
    * It is left up to a user defining a particular protocol implementation to decide how the parameters flowing through the graph in
    * both directions are combined into a single representation on an Edge.
    *
    * @param pd         The downward-flowing parameters into the node along the edge.
    * @param pu         The upward-flowing parameters going out of the node along the edge.
    * @param p          [[Parameters]]s which can be used during negotiation.
    * @param sourceInfo [[SourceInfo]] of this edge.
    * @return Negotiated edge parameters.
    */
  def edge(pd: D, pu: U, p: Parameters, sourceInfo: SourceInfo): E

  def edgeO(pd: D, pu: U, p: Parameters, sourceInfo: SourceInfo): E = edge(pd, pu, p, sourceInfo)

  def edgeI(pd: D, pu: U, p: Parameters, sourceInfo: SourceInfo): E = edge(pd, pu, p, sourceInfo)

  /** Generate the Bundle from the negotiated Edge parameters.
    *
    * @param e the negotiated Edge parameters
    * @return the corresponding Bundle of this node
    */
  def bundle(e: E): B

  def bundleO(e: E): B = bundle(e)

  def bundleI(e: E): B = bundle(e)
}

/** [[BaseNode]] is the abstract base class of the type hierarchy of diplomacy node classes.
  *
  * @param valName [[ValName]] of this node, used by naming inference.
  */
abstract class BaseNode(implicit val valName: ValName) {
  /** All subclasses of [[BaseNode]]s are expected to be instantiated only within [[LazyModule]]s.
    *
    * Sometimes one wants to view the entire diplomacy graph in a way
    * where you do not care about the specific types of the edges.
    * [[BaseNode]]s are type-erased and provide this view.
    *
    * @return The [[LazyModule]] which contains this Node.
    */
  val scope: Option[LazyModule] = LazyModule.scope

  /** @return The index for this node in the containing [[LazyModule]]/[[LazyScope]]'s list of [[BaseNode]]s */
  val index: Int = scope.map(_.nodes.size).getOrElse(0)

  /** @return The [[LazyModule]] which contains this [[BaseNode]] */
  def lazyModule: LazyModule = scope.get

  // Prepend this node to the current [[LazyModule]]'s list of nodes
  scope.foreach { lm => lm.nodes = this :: lm.nodes }

  /** @return The serial number for this node in the global list of [[BaseNode]]s. */
  val serial: Int = BaseNode.serial

  BaseNode.serial = BaseNode.serial + 1

  /** Instantiate this node.
    *
    * This happens after all nodes connections have been made and we are ready to perform parameter negotiation.
    * This also determines which connections need to leave this node's LazyScope and cross hierarchical boundaries.
    * That information is captured in [[Dangle]]s which are returned from this function.
    *
    * @return A sequence of [[Dangle]]s from this node that leave this [[BaseNode]]'s [[LazyScope]].
    */
  protected[diplomacy] def instantiate(): Seq[Dangle]

  /** @return name of this node. */
  def name: String = scope.map(_.name).getOrElse("TOP") + "." + valName.name

  /** Determines whether or not this node will be excluded from the graph visualization.
    *
    * By default, if this node has neither inputs nor outputs it will be excluded.
    */
  def omitGraphML: Boolean = outputs.isEmpty && inputs.isEmpty

  /** Debug string of this node, used in [[LazyModule.graphML]]. */
  lazy val nodedebugstring: String = ""

  /** Mark whether this node represents a circuit "identity" that outputs its inputs unchanged.
    *
    * This information may be used to elide monitors or inline the parent module.
    */
  def circuitIdentity: Boolean = false

  /** @return A sequence of [[LazyModule]] up to and including Top. */
  def parents: Seq[LazyModule] = scope.map(lm => lm +: lm.parents).getOrElse(Nil)

  /** @return The context string for debug. */
  def context: String = {
    s"""$description $name node:
       |parents: ${parents.map(_.name).mkString("/")}
       |locator: ${scope.map(_.line).getOrElse("<undef>")}
       |""".stripMargin
  }

  /** Determines the name to be used in elements of auto-punched bundles.
    *
    * It takes the name of the node as determined from [[valName]],
    * converts camel case into snake case, and strips "Node" or "NodeOpt" suffixes.
    */
  def wirePrefix: String = {
    val camelCase = "([a-z])([A-Z])".r
    val decamel = camelCase.replaceAllIn(valName.name, _ match { case camelCase(l, h) => l + "_" + h })
    val name = decamel.toLowerCase.stripSuffix("_opt").stripSuffix("node").stripSuffix("_")
    if (name.isEmpty) "" else name + "_"
  }

  /** @return [[BaseNode]] description, which should be defined by subclasses and is generally expected to be a constant. */
  def description: String

  /** @return [[BaseNode]] instance description, which can be overridden with more detailed information about each node. */
  def formatNode: String = ""

  /** @return Metadata to visualize inward edges into this node. */
  def inputs:  Seq[(BaseNode, RenderedEdge)]

  /** @return Metadata to visualize outward edges from this node. */
  def outputs: Seq[(BaseNode, RenderedEdge)]

  /** @return Whether this node can handle [[BIND_FLEX]] type connections on either side.
    *
    *  For example, a node `b` will have [[flexibleArityDirection]] be `true` if both are legal:
    *    `a :*=* b :*= c`, which resolves to `a :*= b :*= c`
    *    or
    *    `a :=* b :*=* c`, which resolves to `a :=* b :=* c`
    *
    *  If this is `false`, the node can only support `:*=*` if it connects to a node with `flexibleArityDirection = true`
    */
  protected[diplomacy] def flexibleArityDirection: Boolean = false

  /** @return The sink cardinality.
    *
    * How many times is this node used as a sink.
    */
  protected[diplomacy] val sinkCard: Int

  /** @return The source cardinality.
    *
    * How many times is this node used as a source.
    */
  protected[diplomacy] val sourceCard: Int

  /** @return The "flex" cardinality.
    *
    * How many times is this node used in a way that could be either source or sink, depending on final
    * directional determination.
    */
  protected[diplomacy] val flexes: Seq[BaseNode]

  /** Resolves the flex to be either source or sink.
    *
    * @return A value >= 0 if it is sink cardinality, a negative value for source cardinality. The magnitude of the value does not matter.
    */
  protected[diplomacy] val flexOffset: Int
}

/** Companion object for [[BaseNode]], which is only used to hold the the global serial number of all [[BaseNode]]s. */
object BaseNode {
  protected[diplomacy] var serial = 0
}

/** Trait that enables a string representation of an edge. */
trait FormatEdge {
  def formatEdge: String
}

/** Trait that enables iterating over a [[BaseNode]]'s edges to produce a formatted string representation.
  *
  * In practice this is generally GraphML metadata.
  */
trait FormatNode[I <: FormatEdge, O <: FormatEdge] extends BaseNode {
  def edges: Edges[I,O]

  /** Format the edges of the [[BaseNode]] for emission (generally in GraphML). */
  override def formatNode = if (circuitIdentity) "" else {
    edges.out.map(currEdge =>
      "On Output Edge:\n\n" + currEdge.formatEdge).mkString +
    "\n---------------------------------------------\n\n" +
    edges.in.map(currEdge =>
      "On Input Edge:\n\n" + currEdge.formatEdge).mkString
  }
}

/** A Handle with no explicitly defined binding functionality.
  *
  * A [[NoHandle]] is at the top of the Handle type hierarchy, but it does not define any binding operators,
  * so by itself a [[NoHandle]] cannot be used on either side of a bind operator.
  *
  * For example, a source node connected directly to a sink node produces a [[NoHandle]],
  * because there are no further bindings that could be applied to either side of the pair of nodes.
  *
  * The other Handle types extend this type and bestow actual binding semantics.
  * They can always be used wherever a [[NoHandle]] is expected because a [[NoHandle]]
  * doesn't provide any guaranteed behavior.
  *
  * Handle algebra:
  *
  * "x---x" [[NoHandle]]
  * "x---<" [[InwardNodeHandle]]
  * "<---x" [[OutwardNodeHandle]]
  * "<---<" (Full) [[NodeHandle]]
  *
  * "<" can be bound to (arrow points in the direction of binding).
  * "x" cannot be bound to.
  *
  * The left side is outer, the right side is inner.
  *
  * Two Handles can be bound if their adjacent ends are both "<".
  */
trait NoHandle
case object NoHandleObject extends NoHandle

/** A Handle that can be used on either side of a bind operator. */
trait NodeHandle[DI, UI, EI, BI <: Data, DO, UO, EO, BO <: Data]
  extends InwardNodeHandle[DI, UI, EI, BI] with OutwardNodeHandle[DO, UO, EO, BO] {
  /** Connects two full nodes handles => full node handle.
    *
    * <---< := <---<  == <---<
    * This and that node are both [[BIND_ONCE]].
    *
    * @param h A source node also with sink handle.
    * @return A [[NodeHandle]] with that node as `inwardNode`, this node as `outwardNode`.
    */
  override def :=  [DX, UX, EX, BX <: Data, EY](h: NodeHandle[DX, UX, EX, BX, DI, UI, EY, BI])(implicit p: Parameters, sourceInfo: SourceInfo): NodeHandle[DX, UX, EX, BX, DO, UO, EO, BO] = { bind(h, BIND_ONCE);  NodeHandle(h, this) }

  /** Connects two full nodes handles => full node handle.
    *
    * <---< :*= <---<  == <---<
    * [[BIND_STAR]] this node as sink, [[BIND_QUERY]] that node as source.
    *
    * @param h A source node also with sink handle.
    * @return A [[NodeHandle]] with that node as `InwardNode`, this node as `OutwardNode`.
    */
  override def :*= [DX, UX, EX, BX <: Data, EY](h: NodeHandle[DX, UX, EX, BX, DI, UI, EY, BI])(implicit p: Parameters, sourceInfo: SourceInfo): NodeHandle[DX, UX, EX, BX, DO, UO, EO, BO] = { bind(h, BIND_STAR);  NodeHandle(h, this) }

  /** Connects two full nodes handles => full node handle.
    *
    * <---< :=* <---<  == <---<
    * [[BIND_QUERY]] this node as sink, [[BIND_STAR]] that node as source.
    *
    * @param h A source node also with sink handle.
    * @return A [[NodeHandle]] with that node as `InwardNode`, this node as `OutwardNode`.
    */
  override def :=* [DX, UX, EX, BX <: Data, EY](h: NodeHandle[DX, UX, EX, BX, DI, UI, EY, BI])(implicit p: Parameters, sourceInfo: SourceInfo): NodeHandle[DX, UX, EX, BX, DO, UO, EO, BO] = { bind(h, BIND_QUERY); NodeHandle(h, this) }

  /** Connects two full nodes handles => full node handle.
    *
    * <---< :*=* <---<  == <---<
    * [[BIND_FLEX]] this node as sink, [[BIND_FLEX]] that node as source.
    *
    * @param h A source node also with sink handle.
    * @return A [[NodeHandle]] with that node as `inwardNode`, this node as `outwardNode`.
    */
  override def :*=*[DX, UX, EX, BX <: Data, EY](h: NodeHandle[DX, UX, EX, BX, DI, UI, EY, BI])(implicit p: Parameters, sourceInfo: SourceInfo): NodeHandle[DX, UX, EX, BX, DO, UO, EO, BO] = { bind(h, BIND_FLEX);  NodeHandle(h, this) }

  /** Connects a full node with an output node => an output handle.
    *
    * <---< := <---x  ==  <---x
    * [[BIND_ONCE]] this node as sink, [[BIND_ONCE]] that node as source.
    *
    * @param h A source node also without sink handle.
    * @return A [[OutwardNodeHandle]] with this node as `outwardNode`.
    */
  override def :=  [EY](h: OutwardNodeHandle[DI, UI, EY, BI])(implicit p: Parameters, sourceInfo: SourceInfo): OutwardNodeHandle[DO, UO, EO, BO] = { bind(h, BIND_ONCE);  this }

  /** Connects a full node with an output node => an output handle.
    *
    * <---< :*= <---x  ==  <---x
    * [[BIND_STAR]] this node as sink, [[BIND_QUERY]] that node as source.
    *
    * @param h A source node also without sink handle.
    * @return A [[OutwardNodeHandle]] with this node as `outwardNode`.
    */
  override def :*= [EY](h: OutwardNodeHandle[DI, UI, EY, BI])(implicit p: Parameters, sourceInfo: SourceInfo): OutwardNodeHandle[DO, UO, EO, BO] = { bind(h, BIND_STAR);  this }

  /** Connects a full node with an output => an output.
    *
    * <---< :=* <---x  ==  <---x
    * [[BIND_QUERY]] this node as sink, [[BIND_STAR]] that node as source.
    *
    * @param h A source node also without sink handle.
    * @return A [[OutwardNodeHandle]] with this node as `outwardNode`.
    */
  override def :=* [EY](h: OutwardNodeHandle[DI, UI, EY, BI])(implicit p: Parameters, sourceInfo: SourceInfo): OutwardNodeHandle[DO, UO, EO, BO] = { bind(h, BIND_QUERY); this }

  /** Connects a full node with an output => an output.
    *
    * <---< :*=* <---x  ==  <---x
    * [[BIND_FLEX]] this node as sink, [[BIND_FLEX]] that node as source.
    *
    * @param h A source node also without sink handle.
    * @return A [[OutwardNodeHandle]] with this node as `outwardNode`.
    */
  override def :*=*[EY](h: OutwardNodeHandle[DI, UI, EY, BI])(implicit p: Parameters, sourceInfo: SourceInfo): OutwardNodeHandle[DO, UO, EO, BO] = { bind(h, BIND_FLEX);  this }
}

object NodeHandle {
  /** generate a [[NodeHandle]] by combining an [[InwardNodeHandle]] and an [[OutwardNodeHandle]].
    *
    * @param i Inward node handle.
    * @param o Outward node handle.
    * @return [[NodeHandlePair]] with `inwardNode` of `i`, `outwardNode` of `o`.
    */
  def apply[DI, UI, EI, BI <: Data, DO, UO, EO, BO <: Data](i: InwardNodeHandle[DI, UI, EI, BI], o: OutwardNodeHandle[DO, UO, EO, BO]) = new NodeHandlePair(i, o)
}

/** A data structure that preserves information about the innermost and outermost Nodes in a [[NodeHandle]]. */
class NodeHandlePair[DI, UI, EI, BI <: Data, DO, UO, EO, BO <: Data]
  (inwardHandle: InwardNodeHandle[DI, UI, EI, BI], outwardHandle: OutwardNodeHandle[DO, UO, EO, BO])
  extends NodeHandle[DI, UI, EI, BI, DO, UO, EO, BO] {
  /** @return [[InwardNode]] of [[inwardHandle]]. */
  val inward: InwardNode[DI, UI, BI] = inwardHandle.inward

  /** @return [[OutwardNode]] of [[outwardHandle]]. */
  val outward: OutwardNode[DO, UO, BO] = outwardHandle.outward

  /** @return The innermost [[InwardNodeImp]] of this [[NodeHandlePair]].  */
  def inner: InwardNodeImp[DI, UI, EI, BI] = inwardHandle.inner

  /** @return The outermost [[OutwardNodeImp]] of [[NodeHandlePair]]. */
  def outer: OutwardNodeImp[DO, UO, EO, BO] = outwardHandle.outer
}

/** A handle for an [[InwardNode]], which may appear on the left side of a bind operator. */
trait InwardNodeHandle[DI, UI, EI, BI <: Data] extends NoHandle
{
  /** @return [[InwardNode]] of `inwardHandle`. */
  def inward: InwardNode[DI, UI, BI]

  /** @return [[InwardNodeImp]] of `inwardHandle`. */
  def inner: InwardNodeImp[DI, UI, EI, BI]

  /** Bind this node to an [[OutwardNodeHandle]]. */
  protected def bind[EY](h: OutwardNodeHandle[DI, UI, EY, BI], binding: NodeBinding)(implicit p: Parameters, sourceInfo: SourceInfo): Unit = inward.bind(h.outward, binding)

  /** Connect an input node with a full node => inward node handle.
    *
    * x---< := <---<  == x---<
    * [[BIND_ONCE]] this node as sink, [[BIND_ONCE]] that node as source.
    *
    * @param h A source node also with sink handle.
    * @return A [[NodeHandle]] with that node as `inwardNode`, this node as `outwardNode`.
    */
  def :=  [DX, UX, EX, BX <: Data, EY](h: NodeHandle[DX, UX, EX, BX, DI, UI, EY, BI])(implicit p: Parameters, sourceInfo: SourceInfo): InwardNodeHandle[DX, UX, EX, BX] = { bind(h, BIND_ONCE);  h }

  /** Connect an input node with a full node => an input node.
    *
    * x---< :*= <---<  == x---<
    * [[BIND_STAR]] this node as sink, [[BIND_QUERY]] that node as source.
    *
    * @param h A Source node also with sink handle.
    * @return A [[NodeHandle]] with that node as `inwardNode`, this node as `outwardNode`.
    */
  def :*= [DX, UX, EX, BX <: Data, EY](h: NodeHandle[DX, UX, EX, BX, DI, UI, EY, BI])(implicit p: Parameters, sourceInfo: SourceInfo): InwardNodeHandle[DX, UX, EX, BX] = { bind(h, BIND_STAR);  h }

  /** Connect an input node with a full node => an inward node handle.
    *
    * x---< :=* <---<  == x---<
    * [[BIND_QUERY]] this node as sink, [[BIND_STAR]] that node as source.
    *
    * @param h A source node also with sink handle.
    * @return A [[NodeHandle]] with that node as `inwardNode`, this node as `outwardNode`.
    */
  def :=* [DX, UX, EX, BX <: Data, EY](h: NodeHandle[DX, UX, EX, BX, DI, UI, EY, BI])(implicit p: Parameters, sourceInfo: SourceInfo): InwardNodeHandle[DX, UX, EX, BX] = { bind(h, BIND_QUERY); h }

  /** Connect an input node with a full node => an input node.
    *
    * x---< :*=* <---<  == x---<
    * [[BIND_FLEX]] this node as sink, [[BIND_FLEX]] that node as source.
    *
    * @param h A source node also with sink handle.
    * @return A [[NodeHandle]] with that node as `inwardNode`, this node as `outwardNode`.
    */
  def :*=*[DX, UX, EX, BX <: Data, EY](h: NodeHandle[DX, UX, EX, BX, DI, UI, EY, BI])(implicit p: Parameters, sourceInfo: SourceInfo): InwardNodeHandle[DX, UX, EX, BX] = { bind(h, BIND_FLEX);  h }

  /** Connect an input node with output node => no node.
    *
    * x---< := <---x  == x---x
    * [[BIND_ONCE]] this node as sink, [[BIND_ONCE]] that node as source.
    *
    * @param h A source node also without sink handle.
    * @return A [[NoHandle]] since neither side can bind to a node.
    */
  def :=  [EY](h: OutwardNodeHandle[DI, UI, EY, BI])(implicit p: Parameters, sourceInfo: SourceInfo): NoHandle = { bind(h, BIND_ONCE);  NoHandleObject }

  /** Connect an input node with output node => no node.
    *
    * x---< :*= <---x  == x---x
    * [[BIND_STAR]] this node as sink, [[BIND_QUERY]] that node as source.
    *
    * @param h A source node also without sink handle.
    * @return A [[NoHandle]] since neither side can bind to a node.
    */
  def :*= [EY](h: OutwardNodeHandle[DI, UI, EY, BI])(implicit p: Parameters, sourceInfo: SourceInfo): NoHandle = { bind(h, BIND_STAR);  NoHandleObject }

  /** Connect an input node with output node => no node.
    *
    * x---< :=* <---x  == x---x
    * [[BIND_QUERY]] this node as sink, [[BIND_STAR]] that node as source.
    *
    * @param h A source node also without sink handle.
    * @return A [[NoHandle]] since neither side can bind to another node.
    */
  def :=* [EY](h: OutwardNodeHandle[DI, UI, EY, BI])(implicit p: Parameters, sourceInfo: SourceInfo): NoHandle = { bind(h, BIND_QUERY); NoHandleObject }

  /** Connect an input node with output node => no node.
    *
    * x---< :*=* <---x  == x---x
    * [[BIND_FLEX]] this node as sink, [[BIND_FLEX]] that node as source.
    *
    * @param h A source node also without sink handle.
    * @return A [[NoHandle]] since neither side can bind to another node.
    */
  def :*=*[EY](h: OutwardNodeHandle[DI, UI, EY, BI])(implicit p: Parameters, sourceInfo: SourceInfo): NoHandle = { bind(h, BIND_FLEX);  NoHandleObject }
}

/** Enumeration of types of binding operations. */
sealed trait NodeBinding

/** Only connects a single edge. */
case object BIND_ONCE  extends NodeBinding {
  override def toString: String = "once"
}

/** Connects N (N >= 0) edges.
  *
  * The other side of the edge determines cardinality.
  */
case object BIND_QUERY extends NodeBinding {
  override def toString: String = "query"
}

/** Connect N (N >= 0) edges.
  *
  * Our side of the edge determines cardinality.
  */
case object BIND_STAR  extends NodeBinding {
  override def toString: String = "star"
}

/** Connect N (N >= 0) connections.
  *
  * The number of edges N will be determined by either the right or left side,
  * once the direction ([[BIND_STAR]] or [[BIND_QUERY]]) is determined by the other connections as well.
  */
case object BIND_FLEX  extends NodeBinding {
  override def toString: String = "flex"
}

/** A Node that defines inward behavior, meaning that it can have edges coming into it and be used on the left side of binding expressions. */
trait InwardNode[DI, UI, BI <: Data] extends BaseNode {
  /** accumulates input connections. */
  private val accPI = ListBuffer[(Int, OutwardNode[DI, UI, BI], NodeBinding, Parameters, SourceInfo)]()

  /** Initially `false`, set to `true` once [[iBindings]] has been evaluated. */
  private var iRealized = false

  /** @return debug information of [[iBindings]]. */
  def iBindingInfo: String = s"""${iBindings.size} inward nodes bound: [${iBindings.map(n => s"${n._3}-${n._2.name}").mkString(",")}]"""


  /** The accumulated number of input connections. */
  protected[diplomacy] def iPushed: Int = accPI.size

  /** Accumulate an input connection. 
    *
    * Can only be called before [[iBindings]] is accessed.
    *
    * @param index   index of this [[InwardNode]] in that [[OutwardNode]].
    * @param node    the [[OutwardNode]] to bind to this [[InwardNode]].
    * @param binding [[NodeBinding]] type.
    */
  protected[diplomacy] def iPush(index: Int, node: OutwardNode[DI, UI, BI], binding: NodeBinding)(implicit p: Parameters, sourceInfo: SourceInfo): Unit = {
    val info = sourceLine(sourceInfo, " at ", "")
    require (!iRealized,
      s"""Diplomacy has detected a problem in your code:
         |The following node was incorrectly connected as a sink to ${node.name} after its .module was evaluated at $info.
         |$context
         |$iBindingInfo
         |""".stripMargin)
    accPI += ((index, node, binding, p, sourceInfo))
  }

  /** Ends the binding accumulation stage and returns all the input bindings to this node.
    *
    * Evaluating this lazy val will mark the inwards bindings as frozen,
    * preventing subsequent bindings from being created via [[iPush]].
    *
    * The bindings are each a tuple of:
    * - numeric index of this binding in the other end of [[OutwardNode]].
    * - [[OutwardNode]] on the other end of this binding.
    * - [[NodeBinding]] describing the type of binding.
    * - A view of [[Parameters]] where the binding occurred.
    * - [[SourceInfo]] for source-level error reporting.
    */
  protected[diplomacy] lazy val iBindings: immutable.Seq[(Int, OutwardNode[DI, UI, BI], NodeBinding, Parameters, SourceInfo)] = { iRealized = true; accPI.result() }

  /** resolved [[BIND_STAR]] binding of inward nodes: how many connections the star represents. */
  protected[diplomacy] val iStar: Int

  /** A mapping to convert Node binding index to port range.
    *
    * @return a sequence of tuple of mapping, the item in each a tuple of:
    *         - index: the index of connected [[OutwardNode]]
    *         - element: port range of connected [[OutwardNode]]
    */
  protected[diplomacy] val iPortMapping: Seq[(Int, Int)]

  /** "Forward" an input connection through this node so that the node can be removed from the graph.
    *
    * @return None if no forwarding is needing.
    */
  protected[diplomacy] def iForward(x: Int): Option[(Int, InwardNode[DI, UI, BI])] = None

  /** Downward-flowing inward parameters.
    *
    * Generated from the nodes connected to the inward side of this node and sent downstream to this node.
    */
  protected[diplomacy] val diParams: Seq[DI]

  /** Upward-flowing inward parameters.
    *
    * Generated by this node and sent upstream to the nodes connected to the inward side of this node.
    */
  protected[diplomacy] val uiParams: Seq[UI]

  /** Create a binding from this node to an [[OutwardNode]].
    *
    * @param h       The [[OutwardNode]] to bind to.
    * @param binding [[NodeBinding]] the type of binding.
    */
  protected[diplomacy] def bind(h: OutwardNode[DI, UI, BI], binding: NodeBinding)(implicit p: Parameters, sourceInfo: SourceInfo): Unit
}

/** A Handle for OutwardNodes, which may appear on the right side of a bind operator. */
trait OutwardNodeHandle[DO, UO, EO, BO <: Data] extends NoHandle {
  /** @return [[OutwardNode]] of `outwardHandle`. */
  def outward: OutwardNode[DO, UO, BO]

  /** @return [[OutwardNodeImp]] of `inwardHandle`. */
  def outer: OutwardNodeImp[DO, UO, EO, BO]
}

/** A Node that defines outward behavior, meaning that it can have edges coming out of it. */
trait OutwardNode[DO, UO, BO <: Data] extends BaseNode {
  /** Accumulates output connections. */
  private val accPO = ListBuffer[(Int, InwardNode [DO, UO, BO], NodeBinding, Parameters, SourceInfo)]()

  /** Initially set to `true`, this is set to false once [[oBindings]] is referenced. */
  private var oRealized = false

  /** @return debug information of [[oBindings]]. */
  def oBindingInfo: String = s"""${oBindings.size} outward nodes bound: [${oBindings.map(n => s"${n._3}-${n._2.name}").mkString(",")}]"""

  /** The accumulated number of output connections of this node. */
  protected[diplomacy] def oPushed: Int = accPO.size

  /** Accumulate an output connection. 
    *
    * Can only be called before [[oBindings]] is accessed.
    *
    * @param index   Index of this [[OutwardNode]] in that [[InwardNode]].
    * @param node    [[InwardNode]] to bind to.
    * @param binding Binding type.
    */
  protected[diplomacy] def oPush(index: Int, node: InwardNode [DO, UO, BO], binding: NodeBinding)(implicit p: Parameters, sourceInfo: SourceInfo): Unit = {
    val info = sourceLine(sourceInfo, " at ", "")
    require (!oRealized,
      s"""Diplomacy has detected a problem in your code:
         |The following node was incorrectly connected as a source to ${node.name} after its .module was evaluated at $info.
         |$context
         |$oBindingInfo
         |""".stripMargin)
    accPO += ((index, node, binding, p, sourceInfo))
  }

  /** Ends the binding accumulation stage and returns all the output bindings to this node.
    *
    * Evaluating this lazy val will mark the outward bindings as frozen,
    * preventing subsequent bindings from being created via [[oPush]].
    *
    * The bindings are each a tuple of:
    * - numeric index of this binding in the other end of [[InwardNode]].
    * - [[InwardNode]] on the other end of this binding
    * - [[NodeBinding]] describing the type of binding
    * - A view of [[Parameters]] where the binding occurred.
    * - [[SourceInfo]] for source-level error reporting
    */
  protected[diplomacy] lazy val oBindings: Seq[(Int, InwardNode[DO, UO, BO], NodeBinding, Parameters, SourceInfo)] = { oRealized = true; accPO.result() }

  /** resolved [[BIND_STAR]] binding of outward nodes: how many connections the star represents. */
  protected[diplomacy] val oStar: Int

  /** A mapping to convert Node binding index to port range.
    *
    * @return a sequence of tuple of mapping, the item in each a tuple of:
    *         - index: the index of connected [[InwardNode]]
    *         - element: port range of connected [[InwardNode]]
    */
  protected[diplomacy] val oPortMapping: Seq[(Int, Int)]

  /** "Forward" an output connection through this node so that the node can be removed from the graph.
    *
    * @return None if no forwarding is needed.
    */
  protected[diplomacy] def oForward(x: Int): Option[(Int, OutwardNode[DO, UO, BO])] = None

  /** Upward-flowing outward parameters.
    *
    * Generated from the nodes connected to the outward side of this node and sent upstream to this node.
    */
  protected[diplomacy] val uoParams: Seq[UO]

  /** Downward-flowing outward parameters.
    *
    * Generated by this node and sent downstream to the nodes connected to the outward side of this node.
    */
  protected[diplomacy] val doParams: Seq[DO]
}

abstract class CycleException(kind: String, loop: Seq[String]) extends Exception(s"Diplomatic $kind cycle detected involving $loop")
case class StarCycleException(loop: Seq[String] = Nil) extends CycleException("star", loop)
case class DownwardCycleException(loop: Seq[String] = Nil) extends CycleException("downward", loop)
case class UpwardCycleException(loop: Seq[String] = Nil) extends CycleException("upward", loop)

/** [[Edges]] is a collection of parameters describing the functionality and connection for an interface,
  * which is often derived from the interconnection protocol and can inform the parameterization
  * of the hardware bundles that actually implement the protocol.
  */
case class Edges[EI, EO](in: Seq[EI], out: Seq[EO])

/** The sealed node class in the package, all node are derived from it.
  *
  * @param inner   Sink interface implementation.
  * @param outer   Source interface implementation.
  * @param valName val name of this node.
  * @tparam DI Downward-flowing parameters received on the inner side of the node.
  *            It is usually a brunch of parameters describing the protocol parameters from a source.
  *            For an [[InwardNode]], it is determined by the connected [[OutwardNode]].
  *            Since it can be connected to multiple sources, this parameter is always a Seq of source port parameters.
  * @tparam UI Upward-flowing parameters generated by the inner side of the node.
  *            It is usually a brunch of parameters describing the protocol parameters of a sink.
  *            For an [[InwardNode]], it is determined itself.
  * @tparam EI Edge Parameters describing a connection on the inner side of the node.
  *            It is usually a brunch of transfers specified for a sink according to protocol.
  * @tparam BI Bundle type used when connecting to the inner side of the node.
  *            It is a hardware interface of this sink interface.
  *            It should extends from [[chisel3.Data]], which represents the real hardware.
  * @tparam DO Downward-flowing parameters generated on the outer side of the node.
  *            It is usually a brunch of parameters describing the protocol parameters of a source.
  *            For an [[OutwardNode]], it is determined itself.
  * @tparam UO Upward-flowing parameters received by the outer side of the node.
  *            It is usually a brunch of parameters describing the protocol parameters from a sink.
  *            For an [[OutwardNode]], it is determined by the connected [[InwardNode]].
  *            Since it can be connected to multiple sinks, this parameter is always a Seq of sink port parameters.
  * @tparam EO Edge Parameters describing a connection on the outer side of the node.
  *            It is usually a brunch of transfers specified for a source according to protocol.
  * @tparam BO Bundle type used when connecting to the outer side of the node.
  *            It is a hardware interface of this source interface.
  *            It should extends from [[chisel3.Data]], which represents the real hardware.
  *
  * @note Call Graph of [[MixedNode]]
  *       - line `─`: source is process by a function and generate pass to others
  *       - Arrow `→`: target of arrow is generated by source
  *
  * {{{
  *                                                                                                             (from the other node)
  *                                                  ┌─────────────────────────────────────────────────────────[[InwardNode.uiParams]]─────────────┐
  *                                                  ↓                                                                                             │
  *   (binding node when elaboration)   [[OutwardNode.uoParams]]────────────────────────[[MixedNode.mapParamsU]]→──────────┐                       │
  *      [[InwardNode.accPI]]                                                                      │                       │                       │
  *                  │                                                                             │            (based on protocol)                │
  *                  │                                                                             │            [[MixedNode.inner.edgeI]]          │
  *                  │                                                                             │                       ↓                       │
  *                  ↓                                                                             │                       │                       │
  * (immobilize after elaboration)   (inward port from [[OutwardNode]])                            │                       ↓                       │
  *      [[InwardNode.iBindings]]──┐     [[MixedNode.iDirectPorts]]────────────────────→[[MixedNode.iPorts]]   [[InwardNode.uiParams]]             │
  *                  │             │                ↑                                              │                       │                       │
  *                  │             │                │                                 [[OutwardNode.doParams]]             │                       │
  *                  │             │                │                                   (from the other node)              │                       │
  *                  │             │                │                                              │                       │                       │
  *                  │             │                │                                              │                       │                       │
  *                  │             │                │                                              └────────┬──────────────┤                       │
  *                  │             │                │                                                       │              │                       │
  *                  │             │                │                                                       │   (based on protocol)                │
  *                  │             │                │                                                       │   [[MixedNode.inner.edgeI]]          │
  *                  │             │                │                                                       │              │                       │
  *                  │             │     (from the other node)                                              │              ↓                       │
  *                  │             └───[[OutwardNode.oPortMapping]]  [[OutwardNode.oStar]]                  │   [[MixedNode.edgesIn]]───┐          │
  *                  │                              ↑                             ↑                         │              │            ↓          │
  *                  │                              │                             │                         │              │ [[MixedNode.in]]      │
  *                  │                              │                             │                         │              ↓            ↑          │
  *                  │   (solve star connection)    │                             │                         │   [[MixedNode.bundleIn]]──┘          │
  *                  ├───[[MixedNode.resolveStar]]→─┼─────────────────────────────┤                         └────────────────────────────────────┐ │
  *                  │                              │                             │                             [[MixedNode.bundleOut]]─┐        │ │
  *                  │                              │                             │                                        ↑            ↓        │ │
  *                  │                              │                             │                                        │ [[MixedNode.out]]   │ │
  *                  │                              ↓                             ↓                                        │            ↑        │ │
  *                  │            ┌─────[[InwardNode.iPortMapping]]    [[InwardNode.iStar]]                     [[MixedNode.edgesOut]]──┘        │ │
  *                  │            │       (from the other node)                                                            ↑                     │ │
  *                  │            │                 │                                                                      │                     │ │
  *                  │            │                 │                                                           [[MixedNode.outer.edgeO]]        │ │
  *                  │            │                 │                                                           (based on protocol)              │ │
  *                  │            │                 │                                                                      │                     │ │
  *                  │            │                 │                             ┌────────────────────────────────────────┤                     │ │
  *                  │            │                 │                             │                                        │                     │ │
  *                  │            │                 │                             │                                        │                     │ │
  *                  │            │                 │                             │                                        │                     │ │
  * (immobilize after elaboration)│                 ↓                             │                                        │                     │ │
  *     [[OutwardNode.oBindings]]─┘      [[MixedNode.oDirectPorts]]───→[[MixedNode.oPorts]]                   [[OutwardNode.doParams]]           │ │
  *                  ↑               (inward port from [[OutwardNode]])           │                                        │                     │ │
  *                  │                  ┌─────────────────────────────────────────┤                                        │                     │ │
  *                  │                  │                                         │                                        │                     │ │
  *                  │                  │                                         │                                        │                     │ │
  *     [[OutwardNode.accPO]]           │                                         ↓                                        │                     │ │
  *   (binding node when elaboration)   │ [[InwardNode.diParams]]─────→[[MixedNode.mapParamsD]]────────────────────────────┘                     │ │
  *                                     │             ↑                                                                                          │ │
  *                                     │             └──────────────────────────────────────────────────────────────────────────────────────────┘ │
  *                                     └──────────────────────────────────────────────────────────────────────────────────────────────────────────┘
  * }}}
  */
sealed abstract class MixedNode[DI, UI, EI, BI <: Data, DO, UO, EO, BO <: Data](
  val inner: InwardNodeImp [DI, UI, EI, BI],
  val outer: OutwardNodeImp[DO, UO, EO, BO])(
  implicit valName: ValName)
  extends BaseNode with NodeHandle[DI, UI, EI, BI, DO, UO, EO, BO] with InwardNode[DI, UI, BI] with OutwardNode[DO, UO, BO] {
  // Generate a [[NodeHandle]] with inward and outward node are both this node.
  val inward = this
  val outward = this

  /** Debug info of nodes binding. */
  def bindingInfo: String =
    s"""$iBindingInfo
       |$oBindingInfo
       |""".stripMargin

  /** Debug info of ports connecting. */
  def connectedPortsInfo: String =
    s"""${oPorts.size} outward ports connected: [${oPorts.map(_._2.name).mkString(",")}]
       |${iPorts.size} inward ports connected: [${iPorts.map(_._2.name).mkString(",")}]
       |""".stripMargin

  /** Debug info of parameters propagations. */
  def parametersInfo: String =
    s"""${doParams.size} downstream outward parameters: [${doParams.mkString(",")}]
       |${uoParams.size} upstream outward parameters: [${uoParams.mkString(",")}]
       |${diParams.size} downstream inward parameters: [${diParams.mkString(",")}]
       |${uiParams.size} upstream inward parameters: [${uiParams.mkString(",")}]
       |""".stripMargin

  /** For a given node, converts [[OutwardNode.accPO]] and [[InwardNode.accPI]] to [[MixedNode.oPortMapping]] and [[MixedNode.iPortMapping]].
    *
    * Given counts of known inward and outward binding and inward and outward star bindings, return the resolved inward stars and outward stars.
    *
    * This method will also validate the arguments and throw a runtime error if the values are unsuitable for this type of node.
    *
    * @param iKnown Number of known-size ([[BIND_ONCE]]) input bindings.
    * @param oKnown Number of known-size ([[BIND_ONCE]]) output bindings.
    * @param iStar  Number of unknown size ([[BIND_STAR]]) input bindings.
    * @param oStar  Number of unknown size ([[BIND_STAR]]) output bindings.
    * @return A Tuple of the resolved number of input and output connections.
    */
  protected[diplomacy] def resolveStar(iKnown: Int, oKnown: Int, iStar: Int, oStar: Int): (Int, Int)

  /** Function to generate downward-flowing outward params from the downward-flowing input params and the current output ports.
    *
    * @param n The size of the output sequence to generate.
    * @param p Sequence of downward-flowing input parameters of this node.
    * @return A `n`-sized sequence of downward-flowing output edge parameters.
    */
  protected[diplomacy] def mapParamsD(n: Int, p: Seq[DI]): Seq[DO]

  /** Function to generate upward-flowing input parameters from the upward-flowing output parameters [[uiParams]].
    *
    * @param n Size of the output sequence.
    * @param p Upward-flowing output edge parameters.
    * @return A n-sized sequence of upward-flowing input edge parameters.
    */
  protected[diplomacy] def mapParamsU(n: Int, p: Seq[UO]): Seq[UI]

  /** @return The sink cardinality of the node, the number of outputs bound with [[BIND_QUERY]] summed with inputs bound with [[BIND_STAR]]. */
  protected[diplomacy] lazy val sinkCard: Int = oBindings.count(_._3 == BIND_QUERY) + iBindings.count(_._3 == BIND_STAR)

  /** @return The source cardinality of this node, the number of inputs bound with [[BIND_QUERY]] summed with the number of output bindings bound with [[BIND_STAR]]. */
  protected[diplomacy] lazy val sourceCard: Int = iBindings.count(_._3 == BIND_QUERY) + oBindings.count(_._3 == BIND_STAR)

  /** @return list of nodes involved in flex bindings with this node. */
  protected[diplomacy] lazy val flexes: Seq[BaseNode] = oBindings.filter(_._3 == BIND_FLEX).map(_._2) ++ iBindings.filter(_._3 == BIND_FLEX).map(_._2)

  /** Resolves the flex to be either source or sink and returns the offset where the [[BIND_STAR]] operators begin greedily taking up the remaining connections.
    *
    * @return A value >= 0 if it is sink cardinality, a negative value for source cardinality. The magnitude of the return value is not relevant.
    */
  protected[diplomacy] lazy val flexOffset: Int = {
    /** Recursively performs a depth-first search of the [[flexes]],
      * [[BaseNode]]s connected to this node with flex operators.
      * The algorithm bottoms out when we either get to a node we have already visited
      * or when we get to a connection that is not a flex and can set the direction for us.
      * Otherwise, recurse by visiting the `flexes` of each node in the current set
      * and decide whether they should be added to the set or not.
      *
      * @return the mapping of [[BaseNode]] indexed by their serial numbers.
      */
    def DFS(v: BaseNode, visited: Map[Int, BaseNode]): Map[Int, BaseNode] = {
      if (visited.contains(v.serial) || !v.flexibleArityDirection) {
        visited
      } else {
        v.flexes.foldLeft(visited + (v.serial -> v))((sum, n) => DFS(n, sum))
      }
    }

    /** Determine which [[BaseNode]] are involved in resolving the flex connections to/from this node.
      *
      * @example
      * {{{
      *        a :*=* b :*=* c
      *        d :*=* b
      *        e :*=* f
      * }}}
      *
      * `flexSet` for `a`, `b`, `c`, or `d` will be `Set(a, b, c, d)`
      * `flexSet` for `e` or `f` will be `Set(e,f)`
      */
    val flexSet = DFS(this, Map()).values

    /** The total number of :*= operators where we're on the left. */
    val allSink   = flexSet.map(_.sinkCard).sum

    /** The total number of :=* operators used when we're on the right. */
    val allSource = flexSet.map(_.sourceCard).sum

    require (allSink == 0 || allSource == 0,
      s"The nodes ${flexSet.map(_.name)} which are inter-connected by :*=* have ${allSink} :*= operators and ${allSource} :=* operators connected to them, making it impossible to determine cardinality inference direction.")
    allSink - allSource
  }

  /** @return A value >= 0 if it is sink cardinality, a negative value for source cardinality. */
  protected[diplomacy] def edgeArityDirection(n: BaseNode): Int = {
    if (  flexibleArityDirection)   flexOffset else
    if (n.flexibleArityDirection) n.flexOffset else
    0
  }

  /** For a node which is connected between two nodes, select the one that will influence the direction of the flex resolution. */
  protected[diplomacy] def edgeAritySelect(n: BaseNode, l: => Int, r: => Int): Int = {
    val dir = edgeArityDirection(n)
    if (dir < 0) l else if (dir > 0) r else 1
  }

  /** Ensure that the same node is not visited twice in resolving `:*=`, etc operators. */
  private var starCycleGuard = false

  /** Resolve all the star operators into concrete indicies.
    * As connections are being made, some may be "star" connections which need to be resolved.
    * In some way to determine how many actual edges they correspond to.
    * We also need to build up the ranges of edges which correspond to each binding operator, so that
    * We can apply the correct edge parameters and later build up correct bundle connections.
    *
    * [[oPortMapping]]: `Seq[(Int, Int)]` where each item is the range of edges corresponding to that oPort (binding operator).
    * [[iPortMapping]]: `Seq[(Int, Int)]` where each item is the range of edges corresponding to that iPort (binding operator).
    * [[oStar]]: `Int` the value to return for this node `N` for any `N :*= foo` or `N :*=* foo :*= bar`
    * [[iStar]]: `Int` the value to return for this node `N` for any `foo :=* N` or `bar :=* foo :*=* N`
    */
  protected[diplomacy] lazy val (oPortMapping: Seq[(Int, Int)], iPortMapping: Seq[(Int, Int)], oStar: Int, iStar: Int) = {
    try {
      if (starCycleGuard) throw StarCycleException()
      starCycleGuard = true
      // For a given node N...
      //   Number of foo :=* N
      // + Number of bar :=* foo :*=* N
      val oStars = oBindings.count { case (_,n,b,_,_) => b == BIND_STAR || (b == BIND_FLEX && edgeArityDirection(n) < 0) }
      //   Number of N :*= foo
      // + Number of N :*=* foo :*= bar
      val iStars = iBindings.count { case (_,n,b,_,_) => b == BIND_STAR || (b == BIND_FLEX && edgeArityDirection(n) > 0) }
      //   1         for foo := N
      // + bar.iStar for bar :*= foo :*=* N
      // + foo.iStar for foo :*= N
      // + 0         for foo :=* N 
      val oKnown = oBindings.map { case (_, n, b, _, _) => b match {
        case BIND_ONCE  => 1
        case BIND_FLEX  => edgeAritySelect(n, 0, n.iStar)
        case BIND_QUERY => n.iStar
        case BIND_STAR  => 0 }}.sum
      //   1         for N := foo
      // + bar.oStar for N :*=* foo :=* bar
      // + foo.oStar for N :=* foo
      // + 0         for N :*= foo
      val iKnown = iBindings.map { case (_, n, b, _, _) => b match {
        case BIND_ONCE  => 1
        case BIND_FLEX  => edgeAritySelect(n, n.oStar, 0)
        case BIND_QUERY => n.oStar
        case BIND_STAR  => 0 }}.sum
      // Resolve star depends on the node subclass to implement the algorithm for this.
      val (iStar, oStar) = resolveStar(iKnown, oKnown, iStars, oStars)
      // Cumulative list of resolved outward binding range starting points
      val oSum = oBindings.map { case (_, n, b, _, _) => b match {
        case BIND_ONCE  => 1
        case BIND_FLEX  => edgeAritySelect(n, oStar, n.iStar)
        case BIND_QUERY => n.iStar
        case BIND_STAR  => oStar }}.scanLeft(0)(_+_)
      // Cumulative list of resolved inward binding range starting points
      val iSum = iBindings.map { case (_, n, b, _, _) => b match {
        case BIND_ONCE  => 1
        case BIND_FLEX  => edgeAritySelect(n, n.oStar, iStar)
        case BIND_QUERY => n.oStar
        case BIND_STAR  => iStar }}.scanLeft(0)(_+_)
      // Create ranges for each binding based on the running sums and return
      // those along with resolved values for the star operations.
      (oSum.init zip oSum.tail, iSum.init zip iSum.tail, oStar, iStar)
    } catch {
      case c: StarCycleException => throw c.copy(loop = context +: c.loop)
    }
  }

  /** Sequence of inward ports.
    *
    * This should be called after all star bindings are resolved.
    *
    * Each element is:
    * `j` Port index of this binding in the Node's [[oPortMapping]] on the other side of the binding.
    * `n` Instance of inward node.
    * `p` View of [[Parameters]] where this connection was made.
    * `s` Source info where this connection was made in the source code.
    */
  protected[diplomacy] lazy val oDirectPorts: Seq[(Int, InwardNode[DO, UO, BO], Parameters, SourceInfo)] = oBindings.flatMap { case (i, n, _, p, s) =>
    // for each binding operator in this node, look at what it connects to
    val (start, end) = n.iPortMapping(i)
    (start until end) map { j => (j, n, p, s) }
  }

  /** Sequence of outward ports.
    *
    * This should be called after all star bindings are resolved.
    *
    * `j` Port index of this binding in the Node's [[oPortMapping]] on the other side of the binding.
    * `n` Instance of outward node.
    * `p` View of [[Parameters]] where this connection was made.
    * `s` [[SourceInfo]] where this connection was made in the source code.
    */
  protected[diplomacy] lazy val iDirectPorts: Seq[(Int, OutwardNode[DI, UI, BI], Parameters, SourceInfo)] = iBindings.flatMap { case (i, n, _, p, s) =>
    // query this port index range of this node in the other side of node.
    val (start, end) = n.oPortMapping(i)
    (start until end) map { j => (j, n, p, s) }
  }

  // Ephemeral nodes ( which have non-None iForward/oForward) have in_degree = out_degree
  // Thus, there must exist an Eulerian path and the below algorithms terminate
  @scala.annotation.tailrec
  private def oTrace(tuple: (Int, InwardNode[DO, UO, BO], Parameters, SourceInfo)): (Int, InwardNode[DO, UO, BO], Parameters, SourceInfo) =
    tuple match { case (i, n, p, s) => n.iForward(i) match {
      case None => (i, n, p, s)
      case Some ((j, m)) => oTrace((j, m, p, s))
    } }

  @scala.annotation.tailrec
  private def iTrace(tuple: (Int, OutwardNode[DI, UI, BI], Parameters, SourceInfo)): (Int, OutwardNode[DI, UI, BI], Parameters, SourceInfo) =
    tuple match { case (i, n, p, s) => n.oForward(i) match {
      case None => (i, n, p, s)
      case Some ((j, m)) => iTrace((j, m, p, s))
    } }

  /** Final output ports after all stars and port forwarding (e.g. [[EphemeralNode]]s) have been resolved.
    *
    * Each Port is a tuple of:
    * - Numeric index of this binding in the [[InwardNode]] on the other end.
    * - [[InwardNode]] on the other end of this binding.
    * - A view of [[Parameters]] where the binding occurred.
    * - [[SourceInfo]] for source-level error reporting.
    */
  lazy val oPorts: Seq[(Int, InwardNode[DO, UO, BO], Parameters, SourceInfo)] = oDirectPorts.map(oTrace)

  /** Final input ports after all stars and port forwarding (e.g. [[EphemeralNode]]s) have been resolved.
    *
    *  Each Port is a tuple of:
    * - numeric index of this binding in [[OutwardNode]] on the other end.
    * - [[OutwardNode]] on the other end of this binding.
    * - a view of [[Parameters]] where the binding occurred.
    * - [[SourceInfo]] for source-level error reporting.
    */
  lazy val iPorts: Seq[(Int, OutwardNode[DI, UI, BI], Parameters, SourceInfo)] = iDirectPorts.map(iTrace)

  private var oParamsCycleGuard = false
  protected[diplomacy] lazy val diParams: Seq[DI] = iPorts.map { case (i, n, _, _) => n.doParams(i) }
  protected[diplomacy] lazy val doParams: Seq[DO] = {
    try {
      if (oParamsCycleGuard) throw DownwardCycleException()
      oParamsCycleGuard = true
      val o = mapParamsD(oPorts.size, diParams)
      require (o.size == oPorts.size,
        s"""Diplomacy has detected a problem with your graph:
           |At the following node, the number of outward ports should equal the number of produced outward parameters.
           |$context
           |$connectedPortsInfo
           |Downstreamed inward parameters: [${diParams.mkString(",")}]
           |Produced outward parameters: [${o.mkString(",")}]
           |""".stripMargin)
      o.map(outer.mixO(_, this))
    } catch {
      case c: DownwardCycleException => throw c.copy(loop = context +: c.loop)
    }
  }

  private var iParamsCycleGuard = false
  protected[diplomacy] lazy val uoParams: Seq[UO] = oPorts.map { case (o, n, _, _) => n.uiParams(o) }
  protected[diplomacy] lazy val uiParams: Seq[UI] = {
    try {
      if (iParamsCycleGuard) throw UpwardCycleException()
      iParamsCycleGuard = true
      val i = mapParamsU(iPorts.size, uoParams)
      require (i.size == iPorts.size,
        s"""Diplomacy has detected a problem with your graph:
           |At the following node, the number of inward ports should equal the number of produced inward parameters.
           |$context
           |$connectedPortsInfo
           |Upstreamed outward parameters: [${uoParams.mkString(",")}]
           |Produced inward parameters: [${i.mkString(",")}]
           |""".stripMargin)
      i.map(inner.mixI(_, this))
    } catch {
      case c: UpwardCycleException => throw c.copy(loop = context +: c.loop)
    }
  }

  /** Outward edge parameters. */
  protected[diplomacy] lazy val edgesOut: Seq[EO] = (oPorts zip doParams).map { case ((i, n, p, s), o) => outer.edgeO(o, n.uiParams(i), p, s) }

  /** Inward edge parameters. */
  protected[diplomacy] lazy val edgesIn: Seq[EI] = (iPorts zip uiParams).map { case ((o, n, p, s), i) => inner.edgeI(n.doParams(o), i, p, s) }

  /** A tuple of the input edge parameters and output edge parameters for the edges bound to this node.
    *
    * If you need to access to the edges of a foreign Node, use this method (in/out create bundles).
    */
  lazy val edges: Edges[EI, EO] = Edges(edgesIn, edgesOut)

  // These need to be chisel3.Wire because Chisel.Wire assigns Reset to a default value of Bool,
  // and FIRRTL will not allow a Reset assigned to Bool to later be assigned to AsyncReset.
  // If the diplomatic Bundle contains Resets this will hamstring them into synchronous resets.
  // The jury is still out on whether the lack of ability to override the reset type
  // is a Chisel/firrtl bug or whether this should be supported,
  // but as of today it does not work to do so.

  /** Create actual Wires corresponding to the Bundles parameterized by the outward edges of this node. */
  protected[diplomacy] lazy val bundleOut: Seq[BO] = edgesOut.map(e => chisel3.Wire(outer.bundleO(e)))

  /** Create actual Wires corresponding to the Bundles parameterized by the inward edges of this node. */
  protected[diplomacy] lazy val bundleIn:  Seq[BI] = edgesIn .map(e => chisel3.Wire(inner.bundleI(e)))

  /** Create the [[Dangle]]s which describe the connections from this node output to other nodes inputs. */
  protected[diplomacy] def danglesOut: Seq[Dangle] = oPorts.zipWithIndex.map { case ((j, n, _, _), i) =>
    Dangle(
      source = HalfEdge(serial, i),
      sink   = HalfEdge(n.serial, j),
      flipped= false,
      name   = wirePrefix + "out",
      data   = bundleOut(i))
  }

  /** Create the [[Dangle]]s which describe the connections from this node input from other nodes outputs. */
  protected[diplomacy] def danglesIn: Seq[Dangle] = iPorts.zipWithIndex.map { case ((j, n, _, _), i) =>
    Dangle(
      source = HalfEdge(n.serial, j),
      sink   = HalfEdge(serial, i),
      flipped= true,
      name   = wirePrefix + "in",
      data   = bundleIn(i))
  }

  private[diplomacy] var instantiated = false

  /** Gather Bundle and edge parameters of outward ports.
    *
    * Accessors to the result of negotiation to be used within
    * [[LazyModuleImp]] Code. Should only be used within [[LazyModuleImp]] code
    * or after its instantiation has completed.
    */
  def out: Seq[(BO, EO)] = {
    require(instantiated, s"$name.out should not be called until after instantiation of its parent LazyModule.module has begun")
    bundleOut zip edgesOut
  }

  /** Gather Bundle and edge parameters of inward ports.
    *
    * Accessors to the result of negotiation to be used within
    * [[LazyModuleImp]] Code. Should only be used within [[LazyModuleImp]] code
    * or after its instantiation has completed.
    */
  def in: Seq[(BI, EI)] = {
    require(instantiated, s"$name.in should not be called until after instantiation of its parent LazyModule.module has begun")
    bundleIn zip edgesIn
  }

  /** Actually instantiate this node during [[LazyModuleImp]] evaluation.
    * Mark that it's safe to use the Bundle wires,
    * instantiate monitors on all input ports if appropriate,
    * and return all the dangles of this node.
    */
  protected[diplomacy] def instantiate(): Seq[Dangle] = {
    instantiated = true
    if (!circuitIdentity) {
      (iPorts zip in) foreach {
        case ((_, _, p, _), (b, e)) => if (p(MonitorsEnabled)) inner.monitor(b, e)
    } }
    danglesOut ++ danglesIn
  }

  /** Connects the outward part of a node with the inward part of this node. */
  protected[diplomacy] def bind(h: OutwardNode[DI, UI, BI], binding: NodeBinding)(implicit p: Parameters, sourceInfo: SourceInfo): Unit = {
    val x = this // x := y
    val y = h
    val info = sourceLine(sourceInfo, " at ", "")
    val i = x.iPushed
    val o = y.oPushed
    y.oPush(i, x, binding match {
      case BIND_ONCE  => BIND_ONCE
      case BIND_FLEX  => BIND_FLEX
      case BIND_STAR  => BIND_QUERY
      case BIND_QUERY => BIND_STAR
    })
    x.iPush(o, y, binding)
  }

  /* Metadata for printing the node graph. */
  def inputs: Seq[(OutwardNode[DI, UI, BI], RenderedEdge)] = (iPorts zip edgesIn) map { case ((_, n, p, _), e) =>
    val re = inner.render(e)
    (n, re.copy(flipped = re.flipped != p(RenderFlipped)))
  }
  /** Metadata for printing the node graph */
  def outputs: Seq[(InwardNode[DO, UO, BO], RenderedEdge)] = oPorts map { case (i, n, _, _) => (n, n.inputs(i)._2) }
}

/** A [[MixedNode]] that may be extended with custom behavior. */
abstract class MixedCustomNode[DI, UI, EI, BI <: Data, DO, UO, EO, BO <: Data](
  inner: InwardNodeImp [DI, UI, EI, BI],
  outer: OutwardNodeImp[DO, UO, EO, BO])(
  implicit valName: ValName)
  extends MixedNode(inner, outer) {
  override def description = "custom"
  def resolveStar(iKnown: Int, oKnown: Int, iStars: Int, oStars: Int): (Int, Int)
  def mapParamsD(n: Int, p: Seq[DI]): Seq[DO]
  def mapParamsU(n: Int, p: Seq[UO]): Seq[UI]
}

/** A [[NodeImp]] that may be extended with custom behavior.
  *
  * Different from a [[MixedNode]] in that the inner and outer [[NodeImp]]s are the same.
  */
abstract class CustomNode[D, U, EO, EI, B <: Data](imp: NodeImp[D, U, EO, EI, B])(
  implicit valName: ValName)
  extends MixedCustomNode(imp, imp)

/** A JunctionNode creates multiple parallel arbiters.
  *
  * @example
  * {{{
  *   val jbar = LazyModule(new JBar)
  *   slave1.node := jbar.node
  *   slave2.node := jbar.node
  *   extras.node :=* jbar.node
  *   jbar.node :*= masters1.node
  *   jbar.node :*= masters2.node
  * }}}
  *
  * In the above example, only the first two connections have their multiplicity specified.
  * All the other connections include a '*' on the JBar's side, so the JBar decides the multiplicity.
  * Thus, in this example, we get 2x crossbars with 2 masters like this:
  * {slave1, extras.1} <= jbar.1 <= {masters1.1, masters2.1}
  * {slave2, extras.2} <= jbar.2 <= {masters1.2, masters2,2}
  *
  * @example
  * {{{
  *   val jbar = LazyModule(new JBar)
  *   jbar.node :=* masters.node
  *   slaves1.node :=* jbar.node
  *   slaves2.node :=* jbar.node
  * }}}
  * In the above example, the first connection takes multiplicity (*) from the right (masters).
  * Supposing masters.node had 3 edges, this would result in these three arbiters:
  * {slaves1.1, slaves2.1} <= jbar.1 <= { masters.1 }
  * {slaves1.2, slaves2.2} <= jbar.2 <= { masters.2 }
  * {slaves1.3, slaves2.3} <= jbar.3 <= { masters.3 }
  */
class MixedJunctionNode[DI, UI, EI, BI <: Data, DO, UO, EO, BO <: Data](
  inner: InwardNodeImp [DI, UI, EI, BI],
  outer: OutwardNodeImp[DO, UO, EO, BO])(
  dFn: Seq[DI] => Seq[DO],
  uFn: Seq[UO] => Seq[UI])(
  implicit valName: ValName)
  extends MixedNode(inner, outer) {
  protected[diplomacy] var multiplicity = 0

  def uRatio: Int = iPorts.size / multiplicity
  def dRatio: Int = oPorts.size / multiplicity

  override def description = "junction"
  protected[diplomacy] def resolveStar(iKnown: Int, oKnown: Int, iStars: Int, oStars: Int): (Int, Int) = {
    require (iKnown == 0 || oKnown == 0,
      s"""Diplomacy has detected a problem with your graph:
         |The following node appears left of a :=* or a := and right of a :*= or :=. Only one side may drive multiplicity.
         |$context
         |$bindingInfo
         |""".stripMargin)
    multiplicity = iKnown max oKnown
   (multiplicity, multiplicity)
  }
  protected[diplomacy] def mapParamsD(n: Int, p: Seq[DI]): Seq[DO] =
    p.grouped(multiplicity).toList.transpose.map(dFn).transpose.flatten
  protected[diplomacy] def mapParamsU(n: Int, p: Seq[UO]): Seq[UI] =
    p.grouped(multiplicity).toList.transpose.map(uFn).transpose.flatten

  def inoutGrouped: Seq[(Seq[(BI, EI)], Seq[(BO, EO)])] = {
    val iGroups = in .grouped(multiplicity).toList.transpose
    val oGroups = out.grouped(multiplicity).toList.transpose
    iGroups zip oGroups
  }
}

/** A node type which has a fixed ratio between the number of input edges and output edges.
  *
  * The [[NodeImp]] on either side is the same.
  *
  * One example usage would be for putting down a series of  2:1 arbiters.
  *
  * Suppose you had N banks of L2 and wanted to connect those to two different driver crossbars.
  * In that case you can do this:
  * {{{
  *   l2banks.node :*= jbar.node
  *   jbar.node :*= xbar1.node
  *   jbar.node :*= xbar2.node
  * }}}
  * If the L2 has 4 banks, now there are 4 egress ports on both xbar1 and xbar2 and they are arbitrated by the jbar.
  */
class JunctionNode[D, U, EO, EI, B <: Data](imp: NodeImp[D, U, EO, EI, B])(
  dFn: Seq[D] => Seq[D],
  uFn: Seq[U] => Seq[U])(
  implicit valName: ValName)
    extends MixedJunctionNode[D, U, EI, B, D, U, EO, B](imp, imp)(dFn, uFn)

/** [[MixedAdapterNode]] is used to transform between different diplomacy protocols ([[NodeImp]]), without changing the number of edges passing through it.
  *
  * For example, a [[MixedAdapterNode]] is needed for a TL to AXI bridge (interface).
  * {{{
  *   case class TLToAXI4Node(stripBits: Int = 0)(implicit valName: ValName) extends MixedAdapterNode(TLImp, AXI4Imp)
  * }}}
  *
  * @param dFn convert downward parameter from input to output.
  * @param uFn convert upward parameter from output to input.
  */
class MixedAdapterNode[DI, UI, EI, BI <: Data, DO, UO, EO, BO <: Data](
  inner: InwardNodeImp [DI, UI, EI, BI],
  outer: OutwardNodeImp[DO, UO, EO, BO])(
  dFn: DI => DO,
  uFn: UO => UI)(
  implicit valName: ValName)
  extends MixedNode(inner, outer) {
  override def description = "adapter"
  protected[diplomacy] override def flexibleArityDirection = true
  protected[diplomacy] def resolveStar(iKnown: Int, oKnown: Int, iStars: Int, oStars: Int): (Int, Int) = {
    require (oStars + iStars <= 1,
      s"""Diplomacy has detected a problem with your graph:
         |The following node appears left of a :*= $iStars times and right of a :=* $oStars times, at most once is allowed.
         |$context
         |$bindingInfo
         |""".stripMargin)
    if (oStars > 0) {
      require (iKnown >= oKnown,
        s"""Diplomacy has detected a problem with your graph:
           |After being connected right of :=*, the following node appears left of a := $iKnown times and right of a := $oKnown times.
           |${iKnown - oKnown} additional right of := bindings are required to resolve :=* successfully.
           |$context
           |$bindingInfo
           |""".stripMargin)
      (0, iKnown - oKnown)
    } else if (iStars > 0) {
      require (oKnown >= iKnown,
        s"""Diplomacy has detected a problem with your graph:
           |After being connected left of :*=, the following node appears left of a := $iKnown times and right of a := $oKnown times.
           |${oKnown - iKnown} additional left := bindings are required to resolve :*= successfully.
           |$context
           |$bindingInfo
           |""".stripMargin)
      (oKnown - iKnown, 0)
    } else {
      require (oKnown == iKnown,
        s"""Diplomacy has detected a problem with your graph:
           |The following node appears left of a := $iKnown times and right of a := $oKnown times.
           |Either the number of bindings on both sides of the node match, or connect this node by left-hand side of :*= or right-hand side of :=*
           |$context
           |$bindingInfo
           |""".stripMargin)
      (0, 0)
    }
  }
  protected[diplomacy] def mapParamsD(n: Int, p: Seq[DI]): Seq[DO] = {
    require(n == p.size,
      s"""Diplomacy has detected a problem with your graph:
         |The following node has ${p.size} inputs and $n outputs, they must match.
         |$context
         |$bindingInfo
         |""".stripMargin)
    p.map(dFn)
  }
  protected[diplomacy] def mapParamsU(n: Int, p: Seq[UO]): Seq[UI] = {
    require(n == p.size,
      s"""Diplomacy has detected a problem with your graph:
         |The following node has $n inputs and ${p.size} outputs, they must match
         |$context
         |$bindingInfo
         |""".stripMargin)
    p.map(uFn)
  }
}

/** A node which modifies the parameters flowing through it, but without changing the number of edges or the diplomatic protocol implementation. */
class AdapterNode[D, U, EO, EI, B <: Data](imp: NodeImp[D, U, EO, EI, B])(
  dFn: D => D,
  uFn: U => U)(
  implicit valName: ValName)
    extends MixedAdapterNode[D, U, EI, B, D, U, EO, B](imp, imp)(dFn, uFn)

/** A node which does not modify the parameters nor the protocol for edges that pass through it.
  *
  * During hardware generation, [[IdentityNode]]s automatically connect their inputs to outputs.
  */
class IdentityNode[D, U, EO, EI, B <: Data](imp: NodeImp[D, U, EO, EI, B])()(implicit valName: ValName)
  extends AdapterNode(imp)({ s => s }, { s => s }) {
  override def description = "identity"
  override final def circuitIdentity = true
  override protected[diplomacy] def instantiate(): Seq[Dangle] = {
    val dangles = super.instantiate()
    (out zip in) foreach { case ((o, _), (i, _)) => o <> i }
    dangles
  }
}

/** [[EphemeralNode]]s are used as temporary connectivity placeholders, but disappear from the final node graph.
  * An ephemeral node provides a mechanism to directly connect two nodes to each other where neither node knows about the other,
  * but both know about an ephemeral node they can use to facilitate the connection.
  */
class EphemeralNode[D, U, EO, EI, B <: Data](imp: NodeImp[D, U, EO, EI, B])()(implicit valName: ValName)
  extends AdapterNode(imp)({ s => s }, { s => s }) {
  override def description = "ephemeral"
  override final def circuitIdentity = true
  override def omitGraphML = true
  override def oForward(x: Int): Option[(Int, OutwardNode[D, U, B])] = Some(iDirectPorts(x) match { case (i, n, _, _) => (i, n) })
  override def iForward(x: Int): Option[(Int, InwardNode[D, U, B])] = Some(oDirectPorts(x) match { case (i, n, _, _) => (i, n) })
  override protected[diplomacy] def instantiate(): Seq[Dangle] = {
    instantiated = true
    Nil
  }
}

/** [[MixedNexusNode]] is used when the number of nodes connecting from either side is unknown (e.g. a Crossbar which also is a protocol adapter).
  *
  * The [[NodeImp]] is different between [[inner]] and [[outer]],
  *
  * @param dFn                 Function for mapping the parameters flowing downward into new outward flowing down parameters.
  * @param uFn                 Function for mapping the parameters flowing upward into new inward flowing up parameters.
  * @param inputRequiresOutput True if it is required that if there are input connections, there are output connections (this node can't just be a sink).
  * @param outputRequiresInput True if it is required that if there are output connections, there are input connections (this node can't just be a source).
  */
class MixedNexusNode[DI, UI, EI, BI <: Data, DO, UO, EO, BO <: Data](
  inner: InwardNodeImp [DI, UI, EI, BI],
  outer: OutwardNodeImp[DO, UO, EO, BO])(
  dFn: Seq[DI] => DO,
  uFn: Seq[UO] => UI,
  // no inputs and no outputs is always allowed
  inputRequiresOutput: Boolean = true,
  outputRequiresInput: Boolean = true)(
  implicit valName: ValName)
  extends MixedNode(inner, outer)
{
  override def description = "nexus"
  protected[diplomacy] def resolveStar(iKnown: Int, oKnown: Int, iStars: Int, oStars: Int): (Int, Int) = {
    // a nexus treats :=* as a weak pointer
    def resolveStarInfo: String =
      s"""$context
         |$bindingInfo
         |number of known := bindings to inward nodes: $iKnown
         |number of known := bindings to outward nodes: $oKnown
         |number of binding queries from inward nodes: $iStars
         |number of binding queries from outward nodes: $oStars
         |""".stripMargin
    require(!outputRequiresInput || oKnown == 0 || iStars + iKnown != 0,
      s"""Diplomacy has detected a problem with your graph:
         |The following node has $oKnown outward connections and no inward connections. At least one inward connection was required.
         |$resolveStarInfo
         |""".stripMargin)
    require(!inputRequiresOutput || iKnown == 0 || oStars + oKnown != 0,
      s"""Diplomacy has detected a problem with your graph:
         |The following node node has $iKnown inward connections and no outward connections. At least one outward connection was required.
         |$resolveStarInfo
         |""".stripMargin)
    if (iKnown == 0 && oKnown == 0) (0, 0) else (1, 1)
  }
  protected[diplomacy] def mapParamsD(n: Int, p: Seq[DI]): Seq[DO] = { if (n > 0) { val a = dFn(p); Seq.fill(n)(a) } else Nil }
  protected[diplomacy] def mapParamsU(n: Int, p: Seq[UO]): Seq[UI] = { if (n > 0) { val a = uFn(p); Seq.fill(n)(a) } else Nil }
}

/** [[NexusNode]] is a [[MixedNexusNode]], in which the inward and outward side of the node have the same [[NodeImp]] implementation. */
class NexusNode[D, U, EO, EI, B <: Data](imp: NodeImp[D, U, EO, EI, B])(
  dFn: Seq[D] => D,
  uFn: Seq[U] => U,
  inputRequiresOutput: Boolean = true,
  outputRequiresInput: Boolean = true)(
  implicit valName: ValName)
    extends MixedNexusNode[D, U, EI, B, D, U, EO, B](imp, imp)(dFn, uFn, inputRequiresOutput, outputRequiresInput)

/** A node which represents a node in the graph which only has outward edges and no inward edges.
  *
  * A [[SourceNode]] cannot appear left of a `:=`, `:*=`, `:=*, or `:*=*`
  * There are no Mixed [[SourceNode]]s, There are no "Mixed" [[SourceNode]]s because each one only has an outward side.
  */
class SourceNode[D, U, EO, EI, B <: Data](imp: NodeImp[D, U, EO, EI, B])(po: Seq[D])(implicit valName: ValName)
  extends MixedNode(imp, imp)
{
  override def description = "source"
  protected[diplomacy] def resolveStar(iKnown: Int, oKnown: Int, iStars: Int, oStars: Int): (Int, Int) = {
    def resolveStarInfo: String =
      s"""$context
         |$bindingInfo
         |number of known := bindings to inward nodes: $iKnown
         |number of known := bindings to outward nodes: $oKnown
         |number of binding queries from inward nodes: $iStars
         |number of binding queries from outward nodes: $oStars
         |${po.size} outward parameters: [${po.map(_.toString).mkString(",")}]
         |""".stripMargin
    require(oStars <= 1,
      s"""Diplomacy has detected a problem with your graph:
         |The following node appears right of a :=* $oStars times; at most once is allowed.
         |$resolveStarInfo
         |""".stripMargin)
    require(iStars == 0,
      s"""Diplomacy has detected a problem with your graph:
         |The following node cannot appear left of a :*=
         |$resolveStarInfo
         |""".stripMargin)
    require(iKnown == 0,
      s"""Diplomacy has detected a problem with your graph:
         |The following node cannot appear left of a :=
         |$resolveStarInfo
         |""".stripMargin)
    if (oStars == 0)
      require(po.size == oKnown,
        s"""Diplomacy has detected a problem with your graph:
           |The following node has $oKnown outward bindings connected to it, but ${po.size} sources were specified to the node constructor.
           |Either the number of outward := bindings should be exactly equal to the number of sources, or connect this node on the right-hand side of a :=*
           |$resolveStarInfo
           |""".stripMargin)
    else
      require(po.size >= oKnown,
        s"""Diplomacy has detected a problem with your graph:
           |The following node has $oKnown outward bindings connected to it, but ${po.size} sources were specified to the node constructor.
           |To resolve :=*, size of outward parameters can not be less than bindings.
           |$resolveStarInfo
           |""".stripMargin
      )
    (0, po.size - oKnown)
  }
  protected[diplomacy] def mapParamsD(n: Int, p: Seq[D]): Seq[D] = po
  protected[diplomacy] def mapParamsU(n: Int, p: Seq[U]): Seq[U] = Seq()

  def makeIOs()(implicit valName: ValName): HeterogeneousBag[B] = {
    val bundles = this.out.map(_._1)
    val ios = IO(Flipped(new HeterogeneousBag(bundles.map(_.cloneType))))
    ios.suggestName(valName.name)
    bundles.zip(ios).foreach { case (bundle, io) => bundle <> io }
    ios
  }
}

/** A node which represents a node in the graph which has only inward edges, no outward edges.
  *
  * A [[SinkNode]] cannot appear cannot appear right of a `:=`, `:*=`, `:=*`, or `:*=*`
  *
  * There are no "Mixed" [[SinkNode]]s because each one only has an inward side.
  */
class SinkNode[D, U, EO, EI, B <: Data](imp: NodeImp[D, U, EO, EI, B])(pi: Seq[U])(implicit valName: ValName)
  extends MixedNode(imp, imp)
{
  override def description = "sink"
  protected[diplomacy] def resolveStar(iKnown: Int, oKnown: Int, iStars: Int, oStars: Int): (Int, Int) = {
    def resolveStarInfo: String =
      s"""$context
         |$bindingInfo
         |number of known := bindings to inward nodes: $iKnown
         |number of known := bindings to outward nodes: $oKnown
         |number of binding queries from inward nodes: $iStars
         |number of binding queries from outward nodes: $oStars
         |${pi.size} inward parameters: [${pi.map(_.toString).mkString(",")}]
         |""".stripMargin
    require (iStars <= 1,
      s"""Diplomacy has detected a problem with your graph:
         |The following node appears left of a :*= $iStars times; at most once is allowed.
         |$resolveStarInfo
         |""".stripMargin)
    require (oStars == 0,
      s"""Diplomacy has detected a problem with your graph:
         |The following node cannot appear right of a :=*
         |$resolveStarInfo
         |""".stripMargin)
    require (oKnown == 0,
      s"""Diplomacy has detected a problem with your graph:
         |The following node cannot appear right of a :=
         |$resolveStarInfo
         |""".stripMargin)
    if (iStars == 0)
      require(pi.size == iKnown,
        s"""Diplomacy has detected a problem with your graph:
           |The following node has $iKnown inward bindings connected to it, but ${pi.size} sinks were specified to the node constructor.
           |Either the number of inward := bindings should be exactly equal to the number of sink, or connect this node on the left-hand side of a :*=
           |$resolveStarInfo
           |""".stripMargin)
    else
      require(pi.size >= iKnown,
        s"""Diplomacy has detected a problem with your graph:
           |The following node has $iKnown inward bindings connected to it, but ${pi.size} sinks were specified to the node constructor.
           |To resolve :*=, size of inward parameters can not be less than bindings.
           |$resolveStarInfo
           |""".stripMargin
      )
    (pi.size - iKnown, 0)
  }
  protected[diplomacy] def mapParamsD(n: Int, p: Seq[D]): Seq[D] = Seq()
  protected[diplomacy] def mapParamsU(n: Int, p: Seq[U]): Seq[U] = pi

  def makeIOs()(implicit valName: ValName): HeterogeneousBag[B] = {
    val bundles = this.in.map(_._1)
    val ios = IO(new HeterogeneousBag(bundles.map(_.cloneType)))
    ios.suggestName(valName.name)
    bundles.zip(ios).foreach { case (bundle, io) => io <> bundle }
    ios
  }
}

/** A node intended to replace a portion of the diplomatic graph in order to test functionality of a copy (cloned) [[LazyModule]]
  *
  * @param node  the node to copy
  * @param clone the copy of the LazyModule containing [[node]]
  */
class MixedTestNode[DI, UI, EI, BI <: Data, DO, UO, EO, BO <: Data] protected[diplomacy](
  node: NodeHandle [DI, UI, EI, BI, DO, UO, EO, BO], clone: CloneLazyModule)(
  implicit valName: ValName)
  extends MixedNode(node.inner, node.outer)
{
  // The devices connected to this test node must recreate these parameters:
  def iParams: Seq[DI] = node.inward .diParams
  def oParams: Seq[UO] = node.outward.uoParams

  override def description = "test"
  protected[diplomacy] def resolveStar(iKnown: Int, oKnown: Int, iStars: Int, oStars: Int): (Int, Int) = {
    def resolveStarInfo: String =
      s"""$context
         |$bindingInfo
         |number of known := bindings to inward nodes: $iKnown
         |number of known := bindings to outward nodes: $oKnown
         |number of binding queries from inward nodes: $iStars
         |number of binding queries from outward nodes: $oStars
         |downstream inward parameters: ${node.inward.diParams}
         |upstream inward parameters: ${node.inward.uiParams}
         |upstream outward parameters: ${node.outward.uoParams}
         |downstream outward parameters: ${node.outward.doParams}
         |node.inward.uiParams.size
         |""".stripMargin

    require(oStars <= 1,
      s"""Diplomacy has detected a problem with your graph:
         |The following node appears right of a :=* $oStars times; at most once is allowed.
         |$resolveStarInfo
         |""".stripMargin)
    require(iStars <= 1,
      s"""Diplomacy has detected a problem with your graph:
         |The following node appears left of a :*= $iStars times; at most once is allowed.
         |$resolveStarInfo
         |""".stripMargin)
    require(node.inward .uiParams.size == iKnown || iStars == 1,
      s"""Diplomacy has detected a problem with your graph:
         |The following node has only $iKnown inputs, which should be ${node.inward.uiParams.size}, or connect this node on the left-hand side of :*=
         |$resolveStarInfo
         |""".stripMargin)
    require(node.outward.doParams.size == oKnown || oStars == 1,
      s"""Diplomacy has detected a problem with your graph:
         |The following node has only $oKnown outputs, which should be ${node.outward.doParams.size}, or connect this node on the right-hand side of :=*
         |$resolveStarInfo
         |""".stripMargin)
    (node.inward.uiParams.size - iKnown, node.outward.doParams.size - oKnown)
  }

  protected[diplomacy] def mapParamsU(n: Int, p: Seq[UO]): Seq[UI] = node.inward .uiParams
  protected[diplomacy] def mapParamsD(n: Int, p: Seq[DI]): Seq[DO] = node.outward.doParams

  override protected[diplomacy] def instantiate(): Seq[Dangle] = {
    val dangles = super.instantiate()
    val orig_module = clone.base.module
    val clone_auto = clone.io("auto").asInstanceOf[AutoBundle]

    danglesOut.zipWithIndex.foreach { case (d, i) =>
      val orig = orig_module.dangles.find(_.source == HalfEdge(node.outward.serial, i))
      require (orig.isDefined, s"Cloned node ${node.outward.name} must be connected externally out ${orig_module.name}")
      val io_name = orig_module.auto.elements.find(_._2 eq orig.get.data).get._1
      d.data <> clone_auto.elements(io_name)
    }
    danglesIn.zipWithIndex.foreach { case (d, i) =>
      val orig = orig_module.dangles.find(_.sink == HalfEdge(node.inward.serial, i))
      require (orig.isDefined, s"Cloned node ${node.inward.name} must be connected externally in ${orig_module.name}")
      val io_name = orig_module.auto.elements.find(_._2 eq orig.get.data).get._1
      clone_auto.elements(io_name) <> d.data
    }

    dangles
  }
}
