///|
/// One auditable graph mutation. Values are stored as JSON text so history is
/// compact and exchangeable without weakening the typed graph representation.
pub(all) struct GraphChange {
  version : Int
  kind : String
  edge_id : Int
  previous : String
  current : String
  reason : String
} derive(Eq, Debug, FromJson, ToJson)

///|
/// Versioned graph state used by editors, maintenance tools and simulations.
pub(all) struct GraphState {
  version : Int
  graph : Graph
  changes : Array[GraphChange]
} derive(Eq, Debug, FromJson, ToJson)

///|
pub fn new_graph_state(graph : Graph) -> GraphState? {
  if valid_graph(graph) {
    Some({ version: 1, graph, changes: [] })
  } else {
    None
  }
}

///|
pub fn graph_state_to_json(state : GraphState) -> String {
  ToJson::to_json(state).stringify(indent=2)
}

///|
pub fn graph_state_from_json(source : String) -> GraphState raise {
  @json.from_json(@json.parse(source))
}

///|
fn edge_index_by_id(graph : Graph, edge_id : Int) -> Int {
  let mut index = 0
  while index < graph.edges.length() {
    if graph.edges[index].id == edge_id {
      return index
    }
    index = index + 1
  }
  -1
}

///|
fn append_change(
  state : GraphState,
  graph : Graph,
  kind : String,
  edge_id : Int,
  previous : String,
  current : String,
  reason : String,
) -> GraphState {
  let next_version = state.version + 1
  let changes = state.changes.copy()
  changes.push({
    version: next_version,
    kind,
    edge_id,
    previous,
    current,
    reason,
  })
  { version: next_version, graph, changes }
}

///|
/// Replace an edge while preserving its identifier and recording the change.
pub fn update_edge(
  state : GraphState,
  replacement : Edge,
  reason : String,
) -> GraphState? {
  let index = edge_index_by_id(state.graph, replacement.id)
  if index < 0 {
    return None
  }
  let edges = state.graph.edges.copy()
  let previous = edges[index]
  edges[index] = replacement
  let graph = { nodes: state.graph.nodes.copy(), edges }
  if !valid_graph(graph) {
    return None
  }
  Some(
    append_change(
      state,
      graph,
      "update-edge",
      replacement.id,
      ToJson::to_json(previous).stringify(),
      ToJson::to_json(replacement).stringify(),
      reason,
    ),
  )
}

///|
/// Mark or clear a construction closure without mutating an earlier version.
pub fn set_edge_construction(
  state : GraphState,
  edge_id : Int,
  construction : Bool,
  reason : String,
) -> GraphState? {
  let index = edge_index_by_id(state.graph, edge_id)
  if index < 0 {
    return None
  }
  let edge = state.graph.edges[index]
  update_edge(
    state,
    {
      id: edge.id,
      from: edge.from,
      to: edge.to,
      meters: edge.meters,
      slope_percent: edge.slope_percent,
      steps: edge.steps,
      lighting: edge.lighting,
      crossing: edge.crossing,
      construction,
      surface: edge.surface,
      width_cm: edge.width_cm,
      covered: edge.covered,
      note: edge.note,
    },
    reason,
  )
}

///|
/// Add a newly surveyed route segment and record its introduction.
pub fn add_edge(
  state : GraphState,
  edge : Edge,
  reason : String,
) -> GraphState? {
  if edge_index_by_id(state.graph, edge.id) >= 0 {
    return None
  }
  let edges = state.graph.edges.copy()
  edges.push(edge)
  let graph = { nodes: state.graph.nodes.copy(), edges }
  if !valid_graph(graph) {
    return None
  }
  Some(
    append_change(
      state,
      graph,
      "add-edge",
      edge.id,
      "null",
      ToJson::to_json(edge).stringify(),
      reason,
    ),
  )
}

///|
/// Remove a segment by identifier while keeping an audit record.
pub fn remove_edge(
  state : GraphState,
  edge_id : Int,
  reason : String,
) -> GraphState? {
  let index = edge_index_by_id(state.graph, edge_id)
  if index < 0 {
    return None
  }
  let removed = state.graph.edges[index]
  let edges : Array[Edge] = []
  let mut current = 0
  while current < state.graph.edges.length() {
    if current != index {
      edges.push(state.graph.edges[current])
    }
    current = current + 1
  }
  let graph = { nodes: state.graph.nodes.copy(), edges }
  Some(
    append_change(
      state,
      graph,
      "remove-edge",
      edge_id,
      ToJson::to_json(removed).stringify(),
      "null",
      reason,
    ),
  )
}

///|
pub fn graph_change_at(state : GraphState, version : Int) -> GraphChange? {
  let mut index = 0
  while index < state.changes.length() {
    if state.changes[index].version == version {
      return Some(state.changes[index])
    }
    index = index + 1
  }
  None
}