///|
/// A lossless view of a single non-hierarchical binary graph.
pub struct FlatGraph {
  priv keys : Array[Key]
  priv graph : Graph
}

///|
fn require_flat(graph : Graph) -> Unit raise GraphError {
  require(
    graph.locator is None,
    "flat.graph",
    "external graph locator is not materialized",
  )
  require(
    graph.hyperedges.is_empty(),
    "flat.graph",
    "hyperedges need an explicit application projection",
  )
  for node in graph.nodes {
    require(
      node.graph is None,
      "flat.node",
      "nested graph cannot be silently flattened",
    )
    require(
      node.locator is None,
      "flat.node",
      "external node locator is not materialized",
    )
    require(
      node.ports.is_empty(),
      "flat.node",
      "ports cannot be silently discarded",
    )
  }
  for edge in graph.edges {
    require(
      edge.graph is None,
      "flat.edge",
      "nested edge graph cannot be discarded",
    )
    require(
      edge.source_port is None && edge.target_port is None,
      "flat.edge",
      "port references cannot be discarded",
    )
  }
}

///|
/// Reject advanced structures rather than silently translating them into a different graph.
pub fn Document::flat_graph(
  self : Document,
  address : GraphAddress,
) -> FlatGraph raise GraphError {
  let graph = graph_at(self, address)
  require_flat(graph)
  { keys: self.keys, graph, }
}

///|
pub fn FlatGraph::graph(self : FlatGraph) -> Graph {
  clone_graph(self.graph)
}

///|
/// Produce a one-graph document. Document-level metadata is outside this selected-graph view.
pub fn FlatGraph::to_document(
  self : FlatGraph,
  limits~ : Limits = Limits::default(),
) -> Document raise GraphError {
  Document::new(self.keys, [self.graph], limits~)
}

///|
pub(all) struct NodeRow {
  id : String
  attributes : Map[String, Scalar]
}

///|
pub(all) struct EdgeRow {
  id : String?
  source : String
  target : String
  direction : Direction
  attributes : Map[String, Scalar]
}

///|
pub(all) struct IndexedEdge {
  id : String?
  source : Int32
  target : Int32
  direction : Direction
}

///|
fn row_values(
  data : Array[Datum],
  defaults : Array[Key],
) -> Map[String, Scalar] raise GraphError {
  let values : Map[String, Scalar] = Map([])
  for datum in data {
    match datum.value {
      ScalarValue(value) => values[datum.key] = value
      XmlPayload(_) =>
        raise Unsupported(
          path="flat.row",
          feature="XML attribute payload cannot be a scalar table cell",
        )
    }
  }
  for key in defaults {
    if !values.contains(key.id) {
      match key.default_value {
        Some(ScalarValue(value)) => values[key.id] = value
        Some(XmlPayload(_)) =>
          raise Unsupported(
            path="flat.row",
            feature="XML default cannot be a scalar table cell",
          )
        None => ()
      }
    }
  }
  values
}

///|
fn table_defaults(
  keys : Array[Key],
  scope : Scope,
  use_defaults : Bool,
) -> Array[Key] {
  if use_defaults {
    keys.filter(fn(key) {
      key.scope.applies_to(scope) && key.default_value is Some(_)
    })
  } else {
    []
  }
}

///|
/// Tables are an explicit projection: descriptions and XML metadata remain available in graph().
pub fn FlatGraph::node_rows(
  self : FlatGraph,
  use_defaults~ : Bool = true,
) -> Array[NodeRow] raise GraphError {
  let defaults = table_defaults(self.keys, NodeScope, use_defaults)
  let result = []
  for node in self.graph.nodes {
    result.push({ id: node.id, attributes: row_values(node.data, defaults), })
  }
  result
}

///|
pub fn FlatGraph::edge_rows(
  self : FlatGraph,
  use_defaults~ : Bool = true,
) -> Array[EdgeRow] raise GraphError {
  let defaults = table_defaults(self.keys, EdgeScope, use_defaults)
  let result = []
  for edge in self.graph.edges {
    result.push({
      id: edge.id,
      source: edge.source,
      target: edge.target,
      direction: edge.effective_direction(self.graph),
      attributes: row_values(edge.data, defaults),
    })
  }
  result
}

///|
fn ordered_row_data(
  values : Map[String, Scalar],
  ordinals : Map[String, Int32],
  path : String,
) -> Array[Datum] raise GraphError {
  let ranked : Array[(Int32, Datum)] = []
  for (id, value) in values {
    let ordinal = match ordinals.get(id) {
      Some(index) => index
      None => raise InvalidGraph(path~, reason="undefined attribute key \{id}")
    }
    ranked.push((ordinal, Datum::new(id, value)))
  }
  ranked.sort_by_key(fn(item) { item.0 })
  ranked.map(fn(item) { item.1 })
}

///|
/// Stable positional endpoints allow direct use by existing graph algorithm libraries.
pub fn FlatGraph::indexed_edges(self : FlatGraph) -> Array[IndexedEdge] {
  let positions : Map[String, Int32] = Map([])
  for i = 0; i < self.graph.nodes.length(); i = i + 1 {
    positions[self.graph.nodes[i].id] = i
  }
  self.graph.edges.map(fn(edge) {
    {
      id: edge.id,
      source: positions.get(edge.source).unwrap(),
      target: positions.get(edge.target).unwrap(),
      direction: edge.effective_direction(self.graph),
    }
  })
}

///|
/// Build a flat graph from typed table rows with explicit declarations, not inferred coercions.
pub fn from_rows(
  keys : Array[Key],
  nodes : Array[NodeRow],
  edges : Array[EdgeRow],
  edge_default~ : Direction = Directed,
  graph_id? : String,
  limits~ : Limits = Limits::default(),
) -> Document raise GraphError {
  limits.validate()
  check_bound("keys", keys.length(), limits.keys)
  check_bound("graphs", 1, limits.graphs)
  check_bound("nodes", nodes.length(), limits.nodes)
  check_bound("edges", edges.length(), limits.edges)
  let graph = Graph::new(edge_default, id?=graph_id)
  // Rank declarations once, then order only authored attributes in each sparse row.
  let ordinals : Map[String, Int32] = Map([])
  for index = 0; index < keys.length(); index = index + 1 {
    ordinals[keys[index].id] = index
  }
  for row in nodes {
    let node = {
      ..Node::new(row.id),
      data: ordered_row_data(row.attributes, ordinals, "rows.node"),
    }
    graph.nodes.push(node)
  }
  for row in edges {
    let edge = Edge::new(
      row.source,
      row.target,
      id?=row.id,
      directed=row.direction == Directed,
    )
    graph.edges.push({
      ..edge,
      data: ordered_row_data(row.attributes, ordinals, "rows.edge"),
    })
  }
  Document::new(keys, [graph], limits~)
}