///|
/// The ends of a graph edge (RFC 0025): an ordered pair for a directed edge,
/// an unordered pair for an undirected edge. Ends name node ids.
pub(all) enum Ends {
  Directed(from~ : String, to~ : String)
  Undirected(String, String)
} derive(Debug)

///|
/// A graph node: its id from the source data, its labels (a set, kept in
/// first-seen order) and an optional payload. An absent payload is not NULL:
/// it acts as MISSING in queries.
pub struct GraphNode {
  id : String
  priv labels : Array[String]
  payload : Value?
}

///|
fn dedup_labels(labels : Array[String]) -> Array[String] {
  let out : Array[String] = []
  for label in labels {
    if !out.contains(label) {
      out.push(label)
    }
  }
  out
}

///|
/// Build a node and remove duplicate labels in first-seen order.
/// Id uniqueness and MISSING payloads are checked when building `GraphValue`.
#alias(new, deprecated="Use GraphNode(...) instead")
pub fn GraphNode::GraphNode(
  id~ : String,
  labels~ : Array[String] = [],
  payload~ : Value? = None,
) -> GraphNode {
  { id, labels: dedup_labels(labels), payload, }
}

///|
/// A graph edge: id, labels, ends and an optional payload.
pub struct GraphEdge {
  id : String
  priv labels : Array[String]
  ends : Ends
  payload : Value?
}

///|
/// Build an edge and remove duplicate labels in first-seen order.
/// Endpoint existence, id uniqueness and MISSING payloads are checked by `GraphValue`.
#alias(new, deprecated="Use GraphEdge(...) instead")
pub fn GraphEdge::GraphEdge(
  id~ : String,
  ends~ : Ends,
  labels~ : Array[String] = [],
  payload~ : Value? = None,
) -> GraphEdge {
  { id, labels: dedup_labels(labels), ends, payload, }
}

///|
/// The labels, without duplicates, in first-seen order.
pub fn GraphNode::labels(self : GraphNode) -> ArrayView[String] {
  self.labels[:]
}

///|
/// The labels, without duplicates, in first-seen order.
pub fn GraphEdge::labels(self : GraphEdge) -> ArrayView[String] {
  self.labels[:]
}

///|
/// Why a graph could not be built.
pub(all) suberror GraphError {
  DuplicateNodeId(String)
  DuplicateEdgeId(String)
  /// An edge end that names no node.
  UnknownNode(String)
  /// A payload that is MISSING (use no payload instead).
  MissingPayload(String)
} derive(Debug, Eq)

///|
/// A PartiQL graph (RFC 0025). Node ids are unique among nodes, edge ids
/// among edges (two namespaces), and every edge end names a node.
pub struct GraphValue {
  priv nodes : Array[GraphNode]
  priv edges : Array[GraphEdge]
}

///|
/// Build a graph from copies of the node and edge arrays.
/// Raise `GraphError` for duplicate ids within either namespace, unknown
/// endpoints, or MISSING payloads. Node and edge ids may share a name.
#alias(new, deprecated="Use GraphValue(...) instead")
pub fn GraphValue::GraphValue(
  nodes : Array[GraphNode],
  edges : Array[GraphEdge],
) -> GraphValue raise GraphError {
  let node_ids : Array[String] = []
  for n in nodes {
    if node_ids.contains(n.id) {
      raise DuplicateNodeId(n.id)
    }
    if n.payload is Some(Missing) {
      raise MissingPayload(n.id)
    }
    node_ids.push(n.id)
  }
  let edge_ids : Array[String] = []
  for e in edges {
    if edge_ids.contains(e.id) {
      raise DuplicateEdgeId(e.id)
    }
    if e.payload is Some(Missing) {
      raise MissingPayload(e.id)
    }
    let (a, b) = match e.ends {
      Directed(from~, to~) => (from, to)
      Undirected(x, y) => (x, y)
    }
    for end in [a, b] {
      if !node_ids.contains(end) {
        raise UnknownNode(end)
      }
    }
    edge_ids.push(e.id)
  }
  // Copies: the caller's arrays must not change the graph.
  { nodes: nodes.copy(), edges: edges.copy(), }
}

///|
/// A read-only view of nodes in their input order.
pub fn GraphValue::nodes(self : GraphValue) -> ArrayView[GraphNode] {
  self.nodes[:]
}

///|
/// A read-only view of edges in their input order.
pub fn GraphValue::edges(self : GraphValue) -> ArrayView[GraphEdge] {
  self.edges[:]
}

///|
fn same_label_set(a : Array[String], b : Array[String]) -> Bool {
  // Labels are de-duplicated on construction, so equal sizes and inclusion
  // mean equal sets.
  a.length() == b.length() && a.iter().all(label => b.contains(label))
}