/// ASCII comparison demo for MoonPath.
///
/// The sample deliberately places expensive terrain on the shortest route so
/// BFS, Dijkstra, and A* visibly make different decisions.

///|
/// Parses a rectangular ASCII map.
///
/// - `.` is normal terrain
/// - `#` is a wall
/// - `1` through `9` are weighted terrain
/// - `S` and `G` are the start and goal
fn parse_map(
  map : String,
) -> (@moonpath.Grid, @moonpath.Point, @moonpath.Point) {
  let lines = split_lines(map)
  let height = lines.length()
  let mut width = 0
  for line in lines {
    if line.length() > width {
      width = line.length()
    }
  }

  let updates : Array[(@moonpath.Point, @moonpath.Cell)] = []
  let mut start = @moonpath.Point::new(0, 0)
  let mut goal = @moonpath.Point::new(0, 0)

  for y = 0; y < height; y = y + 1 {
    let line = lines[y]
    for x = 0; x < line.length(); x = x + 1 {
      let character = line[x]
      if character == '#' {
        updates.push((@moonpath.Point::new(x, y), @moonpath.Cell::Blocked))
      } else if character == 'S' {
        start = @moonpath.Point::new(x, y)
      } else if character == 'G' {
        goal = @moonpath.Point::new(x, y)
      } else if character >= '1' && character <= '9' {
        updates.push(
          (
            @moonpath.Point::new(x, y),
            @moonpath.Cell::Weighted(character.to_int() - '0'.to_int()),
          ),
        )
      }
    }
  }

  let grid = @moonpath.Grid::new(width, height).set_cells(updates)
  (grid, start, goal)
}

///|
fn split_lines(text : String) -> Array[String] {
  let result : Array[String] = []
  let mut current = ""
  for character in text {
    if character == '\n' || character == '\r' {
      if current.length() > 0 {
        result.push(current)
        current = ""
      }
    } else {
      current = current + character.to_string()
    }
  }
  if current.length() > 0 {
    result.push(current)
  }
  result
}

///|
fn print_grid(
  grid : @moonpath.Grid,
  start : @moonpath.Point,
  goal : @moonpath.Point,
  path : Array[@moonpath.Point],
) -> Unit {
  let path_cells = Array::make(grid.size(), false)
  for point in path {
    path_cells[point.y * grid.width() + point.x] = true
  }

  for y = 0; y < grid.height(); y = y + 1 {
    let mut row = ""
    for x = 0; x < grid.width(); x = x + 1 {
      let point = @moonpath.Point::new(x, y)
      let index = y * grid.width() + x
      let symbol = if point == start {
        "S"
      } else if point == goal {
        "G"
      } else if path_cells[index] {
        "*"
      } else if !grid.is_passable(point) {
        "#"
      } else if grid.cost_at(point) > 1.0 {
        grid.cost_at(point).to_int().to_string()
      } else {
        "."
      }
      row = row + symbol
      if x < grid.width() - 1 {
        row = row + " "
      }
    }
    println(row)
  }
}

///|
fn terrain_cost(grid : @moonpath.Grid, path : Array[@moonpath.Point]) -> Double {
  let mut cost = 0.0
  for index = 1; index < path.length(); index = index + 1 {
    cost = cost + grid.cost_at(path[index])
  }
  cost
}

///|
fn print_result(
  name : String,
  grid : @moonpath.Grid,
  start : @moonpath.Point,
  goal : @moonpath.Point,
  result : @moonpath.SearchResult,
) -> Unit {
  println("")
  println("--- " + name + " ---")
  match result.path {
    None => println("No path found")
    Some(path) => {
      print_grid(grid, start, goal, path)
      println("Steps: " + result.steps().to_string())
      println("Search cost: " + result.total_cost.to_string())
      println("Terrain cost: " + terrain_cost(grid, path).to_string())
      println(
        "Visited / expanded: " +
        result.nodes_visited.to_string() +
        " / " +
        result.nodes_expanded.to_string(),
      )
    }
  }
}

///|
fn main {
  let map = "S99999G\n.#####.\n......."
  let (grid, start, goal) = parse_map(map)

  println("=== MoonPath ยท Pathfinding Comparison ===")
  println("Legend: # wall, 1-9 terrain cost, * selected path")
  println("")
  println("Map:")
  print_grid(grid, start, goal, [])

  print_result(
    "BFS (fewest steps; ignores weights)",
    grid,
    start,
    goal,
    @moonpath.bfs(grid, start, goal),
  )
  print_result(
    "Dijkstra (lowest terrain cost)",
    grid,
    start,
    goal,
    @moonpath.dijkstra(grid, start, goal),
  )

  let options = @moonpath.SearchOptions::four_way()
  print_result(
    "A* (lowest cost, heuristic-guided)",
    grid,
    start,
    goal,
    @moonpath.astar(grid, start, goal, options),
  )

  println("")
  println(
    "BFS takes the short expensive route; Dijkstra and A* detour around it.",
  )
}