///|
pub struct RouteIssue {
  index : Int
  code : String
  severity : Int
  detail : String
}

///|
pub struct RouteQualityReport {
  point_count : Int
  segment_count : Int
  duplicate_points : Int
  stationary_segments : Int
  long_segments : Int
  sharp_turns : Int
  self_intersections : Int
  score : Int
  issues : Array[RouteIssue]
}

///|
pub fn RouteIssue::RouteIssue(
  index : Int,
  code : String,
  severity : Int,
  detail : String,
) -> RouteIssue {
  { index, code, severity, detail }
}

///|
pub fn RouteIssue::to_string(self : RouteIssue) -> String {
  "\{self.code}@\{self.index}: \{self.detail}"
}

///|
pub fn RouteQualityReport::report(self : RouteQualityReport) -> String {
  [
    "point_count: \{self.point_count}",
    "segment_count: \{self.segment_count}",
    "duplicate_points: \{self.duplicate_points}",
    "stationary_segments: \{self.stationary_segments}",
    "long_segments: \{self.long_segments}",
    "sharp_turns: \{self.sharp_turns}",
    "self_intersections: \{self.self_intersections}",
    "score: \{self.score}",
    "issue_count: \{self.issues.length()}",
  ].join("\n")
}

///|
pub fn duplicate_point_indices(
  points : ArrayView[Point],
) -> Result[Array[Int], RouteError] {
  match require_points(points) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let out = Array::new()
  if points.length() < 2 {
    return Ok(out)
  }
  for i in 1.. Result[Array[Int], RouteError] {
  match validate_distance(threshold_m) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  match require_points(points) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let out = Array::new()
  if points.length() < 2 {
    return Ok(out)
  }
  for i in 1.. value
      Err(err) => return Err(err)
    }
    if distance <= threshold_m {
      out.push(i - 1)
    }
  }
  Ok(out)
}

///|
pub fn long_segment_indices(
  points : ArrayView[Point],
  max_segment_m : Double,
) -> Result[Array[Int], RouteError] {
  match validate_positive_distance(max_segment_m) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  match require_points(points) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let out = Array::new()
  if points.length() < 2 {
    return Ok(out)
  }
  for i in 1.. value
      Err(err) => return Err(err)
    }
    if distance > max_segment_m {
      out.push(i - 1)
    }
  }
  Ok(out)
}

///|
pub fn sharp_turn_indices(
  points : ArrayView[Point],
  threshold_deg? : Double = 100.0,
) -> Result[Array[Int], RouteError] {
  if threshold_deg.is_nan() || threshold_deg.is_inf() || threshold_deg < 0.0 {
    return Err(InvalidFraction(fraction=threshold_deg))
  }
  let turns = match route_turn_angles(points) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let out = Array::new()
  for i, turn in turns {
    if abs_double(turn) >= threshold_deg {
      out.push(i + 1)
    }
  }
  Ok(out)
}

///|
pub fn self_intersection_count(
  points : ArrayView[Point],
) -> Result[Int, RouteError] {
  match require_points(points) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  if points.length() < 4 {
    return Ok(0)
  }
  let mut count = 0
  for a in 1.. Result[Array[RouteIssue], RouteError] {
  let issues = Array::new()
  let duplicates = match duplicate_point_indices(points) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  for index in duplicates {
    issues.push(
      RouteIssue(
        index, "duplicate_point", 2, "same coordinate as previous point",
      ),
    )
  }
  let stationary = match
    stationary_segment_indices(points, threshold_m=stationary_threshold_m) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  for index in stationary {
    issues.push(
      RouteIssue(
        index, "stationary_segment", 1, "segment is shorter than threshold",
      ),
    )
  }
  let long_segments = match long_segment_indices(points, max_segment_m) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  for index in long_segments {
    issues.push(
      RouteIssue(index, "long_segment", 3, "segment is longer than expected"),
    )
  }
  let sharp_turns = match
    sharp_turn_indices(points, threshold_deg=sharp_turn_deg) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  for index in sharp_turns {
    issues.push(
      RouteIssue(index, "sharp_turn", 1, "bearing change exceeds threshold"),
    )
  }
  let intersections = match self_intersection_count(points) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  if intersections > 0 {
    issues.push(
      RouteIssue(
        0,
        "self_intersection",
        2,
        "\{intersections} crossing segments detected",
      ),
    )
  }
  Ok(issues)
}

///|
pub fn route_quality_report(
  points : ArrayView[Point],
  stationary_threshold_m? : Double = 1.0,
  max_segment_m? : Double = 2000.0,
  sharp_turn_deg? : Double = 100.0,
) -> Result[RouteQualityReport, RouteError] {
  match require_points(points) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let duplicates = match duplicate_point_indices(points) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let stationary = match
    stationary_segment_indices(points, threshold_m=stationary_threshold_m) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let long_segments = match long_segment_indices(points, max_segment_m) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let sharp_turns = match
    sharp_turn_indices(points, threshold_deg=sharp_turn_deg) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let intersections = match self_intersection_count(points) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let issues = match
    route_quality_issues(
      points,
      stationary_threshold_m~,
      max_segment_m~,
      sharp_turn_deg~,
    ) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let score = route_quality_score(issues)
  Ok({
    point_count: points.length(),
    segment_count: max_int(points.length() - 1, 0),
    duplicate_points: duplicates.length(),
    stationary_segments: stationary.length(),
    long_segments: long_segments.length(),
    sharp_turns: sharp_turns.length(),
    self_intersections: intersections,
    score,
    issues,
  })
}

///|
pub fn remove_consecutive_duplicates(
  points : ArrayView[Point],
) -> Result[Array[Point], RouteError] {
  match require_points(points) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let out = Array::new()
  out.push(points[0])
  for i in 1.. Result[Array[Point], RouteError] {
  match validate_distance(threshold_m) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  match require_points(points) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let out = Array::new()
  out.push(points[0])
  for i in 1.. value
      Err(err) => return Err(err)
    }
    if distance > threshold_m || i == points.length() - 1 {
      out.push(points[i])
    }
  }
  Ok(out)
}

///|
pub fn smooth_route_window(
  points : ArrayView[Point],
  radius : Int,
) -> Result[Array[Point], RouteError] {
  match require_points(points) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  if radius < 0 {
    return Err(InvalidSampleCount(count=radius))
  }
  if radius == 0 || points.length() <= 2 {
    return Ok(points.to_owned())
  }
  let out = Array::new(capacity=points.length())
  for i in 0.. Int {
  let mut penalty = 0
  for issue in issues {
    penalty = penalty + issue.severity * 8
  }
  let score = 100 - penalty
  if score < 0 {
    0
  } else {
    score
  }
}

///|
fn segments_intersect(a1 : Point, a2 : Point, b1 : Point, b2 : Point) -> Bool {
  let d1 = direction(a1, a2, b1)
  let d2 = direction(a1, a2, b2)
  let d3 = direction(b1, b2, a1)
  let d4 = direction(b1, b2, a2)
  if ((d1 > 0.0 && d2 < 0.0) || (d1 < 0.0 && d2 > 0.0)) &&
    ((d3 > 0.0 && d4 < 0.0) || (d3 < 0.0 && d4 > 0.0)) {
    true
  } else if d1 == 0.0 && on_segment(a1, a2, b1) {
    true
  } else if d2 == 0.0 && on_segment(a1, a2, b2) {
    true
  } else if d3 == 0.0 && on_segment(b1, b2, a1) {
    true
  } else if d4 == 0.0 && on_segment(b1, b2, a2) {
    true
  } else {
    false
  }
}

///|
fn direction(a : Point, b : Point, c : Point) -> Double {
  (c.lon - a.lon) * (b.lat - a.lat) - (b.lon - a.lon) * (c.lat - a.lat)
}

///|
fn on_segment(a : Point, b : Point, c : Point) -> Bool {
  c.lon >= min_double(a.lon, b.lon) &&
  c.lon <= max_double(a.lon, b.lon) &&
  c.lat >= min_double(a.lat, b.lat) &&
  c.lat <= max_double(a.lat, b.lat)
}