// SPDX-License-Identifier: Apache-2.0

package firrtl.graph

import scala.collection.mutable

/**
  * An exception that indicates that an edge cannot be found in a graph with edge data.
  *
  * @note the vertex type is not captured as a type parameter, as it would be erased.
  */
class EdgeNotFoundException(u: Any, v: Any) extends IllegalArgumentException(s"Edge (${u}, ${v}) does not exist!")

/**
  * Mixing this trait into a DiGraph indicates that each edge may be associated with an optional
  * data value. The EdgeData trait provides a minimal API for viewing edge data without mutation.
  *
  * @tparam V the vertex type (datatype) of the underlying DiGraph
  * @tparam E the type of each edge data value
  */
trait EdgeData[V, E] {
  this: DiGraph[V] =>
  protected val edgeDataMap: collection.Map[(V, V), E]

  protected def assertEdgeExists(u: V, v: V): Unit = {
    if (!contains(u) || !getEdges(u).contains(v)) {
      throw new EdgeNotFoundException(u, v)
    }
  }

  /**
    * @return the edge data associated with a given edge
    * @param u the source of the edge
    * @param v the destination of the edge
    * @throws EdgeNotFoundException if the edge does not exist
    * @throws scala.NoSuchElementException if the edge has no data
    */
  def edgeData(u: V, v: V): E = {
    assertEdgeExists(u, v)
    edgeDataMap((u, v))
  }

  /**
    * Optionally return the edge data associated with a given edge.
    *
    * @return an option containing the edge data, if any, or None
    * @param u the source of the edge
    * @param v the destination of the edge
    */
  def getEdgeData(u: V, v: V): Option[E] = edgeDataMap.get((u, v))
}

/**
  * Mixing this trait into a DiGraph indicates that each edge may be associated with an optional
  * data value. The MutableEdgeData trait provides an API for viewing and mutating edge data.
  *
  * @tparam V the vertex type (datatype) of the underlying DiGraph
  * @tparam E the type of each edge data value
  */
trait MutableEdgeData[V, E] extends EdgeData[V, E] {
  this: MutableDiGraph[V] =>

  protected val edgeDataMap: mutable.Map[(V, V), E] = new mutable.LinkedHashMap[(V, V), E]

  /**
    * Associate an edge data value with a graph edge.
    *
    * @param u the source of the edge
    * @param v the destination of the edge
    * @param data the edge data to associate with the edge
    * @throws EdgeNotFoundException if the edge does not exist in the graph
    */
  def setEdgeData(u: V, v: V, data: E): Unit = {
    assertEdgeExists(u, v)
    edgeDataMap((u, v)) = data
  }

  /** Add an edge (u,v) to the graph with associated edge data.
    *
    * @see [[MutableDiGraph.addEdge]]
    * @param u the source of the edge
    * @param v the destination of the edge
    * @param data the edge data to associate with the edge
    * @throws scala.IllegalArgumentException if u or v is not part of the graph
    */
  def addEdge(u: V, v: V, data: E): Unit = {
    addEdge(u, v)
    setEdgeData(u, v, data)
  }

  /** Safely add an edge (u,v) to the graph with associated edge data. If on or more of the two
    * vertices is not present in the graph, add them before creating the edge.
    *
    * @see [[MutableDiGraph.addPairWithEdge]]
    * @param u the source of the edge
    * @param v the destination of the edge
    * @param data the edge data to associate with the edge
    */
  def addPairWithEdge(u: V, v: V, data: E): Unit = {
    addPairWithEdge(u, v)
    setEdgeData(u, v, data)
  }

  /**
    * Safely add an edge (u,v) to the graph with associated edge data if and only if both vertices
    * are present in the graph. This is useful for preventing spurious edge creating when examining
    * a subset of possible nodes.
    *
    * @see [[MutableDiGraph.addEdgeIfValid]]
    * @return a Boolean indicating whether the edge was added
    * @param u the source of the edge
    * @param v the destination of the edge
    * @param data the edge data to associate with the edge
    */
  def addEdgeIfValid(u: V, v: V, data: E): Boolean = {
    if (addEdgeIfValid(u, v)) {
      setEdgeData(u, v, data)
      true
    } else {
      false
    }
  }
}
