///|
pub(all) enum TopologyIssueKind {
  OrphanNode
  MissingTrackEndpoint
  InvalidTrackGeometry
  MissingTrackEdge
  InvalidTurnoutReference
  InvalidSignalBinding
} derive(Eq, Debug)

///|
pub fn TopologyIssueKind::to_string(self : TopologyIssueKind) -> String {
  match self {
    OrphanNode => "ORPHAN_NODE"
    MissingTrackEndpoint => "MISSING_TRACK_ENDPOINT"
    InvalidTrackGeometry => "INVALID_TRACK_GEOMETRY"
    MissingTrackEdge => "MISSING_TRACK_EDGE"
    InvalidTurnoutReference => "INVALID_TURNOUT_REFERENCE"
    InvalidSignalBinding => "INVALID_SIGNAL_BINDING"
  }
}

///|
pub(all) struct TopologyIssue {
  kind : TopologyIssueKind
  resource : String
  detail : String
} derive(Debug)

///|
pub fn TopologyIssue::new(
  kind : TopologyIssueKind,
  resource : String,
  detail : String,
) -> TopologyIssue {
  { kind, resource, detail }
}

///|
pub fn TopologyIssue::to_string(self : TopologyIssue) -> String {
  self.kind.to_string() + ": " + self.resource + " - " + self.detail
}

///|
pub(all) struct TopologyValidationReport {
  issues : Array[TopologyIssue]
  mut error_count_value : Int
} derive(Debug)

///|
pub fn TopologyValidationReport::new() -> TopologyValidationReport {
  { issues: [], error_count_value: 0 }
}

///|
fn TopologyValidationReport::add_issue(
  self : TopologyValidationReport,
  issue : TopologyIssue,
) -> Unit {
  self.issues.push(issue)
  self.error_count_value = self.error_count_value + 1
}

///|
pub fn TopologyValidationReport::is_valid(
  self : TopologyValidationReport,
) -> Bool {
  self.error_count_value == 0
}

///|
pub fn TopologyValidationReport::error_count(
  self : TopologyValidationReport,
) -> Int {
  self.error_count_value
}

///|
pub fn TopologyValidationReport::issues(
  self : TopologyValidationReport,
) -> Array[TopologyIssue] {
  self.issues
}

///|
pub fn Station::validate_operational_topology(
  self : Station,
) -> TopologyValidationReport {
  let report = TopologyValidationReport::new()

  for _, node in self.graph.nodes {
    let has_incoming = match self.graph.reverse_adjacency.get(node.id.id) {
      Some(edges) => edges.length() > 0
      None => false
    }
    let has_outgoing = match self.graph.adjacency.get(node.id.id) {
      Some(edges) => edges.length() > 0
      None => false
    }
    if !has_incoming && !has_outgoing && node.node_type != SignalNode {
      report.add_issue(
        TopologyIssue::new(
          OrphanNode,
          node.name,
          "node has no incoming or outgoing track edge",
        ),
      )
    }
  }

  for _, track in self.graph.tracks {
    if track.length_meters <= 0.0 || track.speed_limit_kmh <= 0.0 {
      report.add_issue(
        TopologyIssue::new(
          InvalidTrackGeometry,
          track.name,
          "length and speed limit must both be positive",
        ),
      )
    }
    if !self.graph.nodes.contains(track.start_node.id) ||
      !self.graph.nodes.contains(track.end_node.id) {
      report.add_issue(
        TopologyIssue::new(
          MissingTrackEndpoint,
          track.name,
          "track references a node that is not in the station graph",
        ),
      )
    }

    let mut edge_count = 0
    for _, edge in self.graph.edges {
      if edge.track_id.id == track.id.id {
        edge_count = edge_count + 1
      }
    }
    if edge_count == 0 {
      report.add_issue(
        TopologyIssue::new(
          MissingTrackEdge,
          track.name,
          "track has no graph edge",
        ),
      )
    }
  }

  for _, turnout in self.graph.turnouts {
    let refs_exist = self.graph.nodes.contains(turnout.base_node.id) &&
      self.graph.nodes.contains(turnout.normal_next_node.id) &&
      self.graph.nodes.contains(turnout.reverse_next_node.id)
    if !refs_exist {
      report.add_issue(
        TopologyIssue::new(
          InvalidTurnoutReference,
          turnout.name,
          "turnout references a node that is not in the station graph",
        ),
      )
    }
  }

  for _, signal in self.graph.signals {
    if !self.graph.nodes.contains(signal.bound_node.id) {
      report.add_issue(
        TopologyIssue::new(
          InvalidSignalBinding,
          signal.name,
          "signal references a node that is not in the station graph",
        ),
      )
    }
  }

  report
}