///|
fn p(x : Int, y : Int) -> @core.Point {
  @core.point(x, y)
}

///|
#cfg(target="native")
#external
type FileHandle

///|
#cfg(target="native")
#borrow(path, mode)
extern "C" fn file_open(path : Bytes, mode : Bytes) -> FileHandle = "moonbit_fopen_ffi"

///|
#cfg(target="native")
extern "C" fn is_null_file(file : FileHandle) -> Bool = "moonbit_is_null"

///|
#cfg(target="native")
extern "C" fn file_seek(file : FileHandle, offset : Int, whence : Int) -> Int = "moonbit_fseek_ffi"

///|
#cfg(target="native")
extern "C" fn file_tell(file : FileHandle) -> Int = "moonbit_ftell_ffi"

///|
#cfg(target="native")
#borrow(buffer, file)
extern "C" fn file_read(
  buffer : Bytes,
  size : Int,
  nitems : Int,
  file : FileHandle,
) -> Int = "moonbit_fread_ffi"

///|
#cfg(target="native")
extern "C" fn file_close(file : FileHandle) -> Int = "moonbit_fclose_ffi"

///|
#cfg(target="native")
fn c_string(text : String) -> Bytes {
  let bytes = @utf8.encode(text).to_array()
  bytes.push(b'\x00')
  Bytes::from_array(bytes)
}

///|
#cfg(target="native")
fn read_text_file(path : String) -> String? {
  let file = file_open(c_string(path), c_string("rb"))
  if is_null_file(file) {
    return None
  }
  if file_seek(file, 0, 2) != 0 {
    ignore(file_close(file))
    return None
  }
  let size = file_tell(file)
  if size < 0 || file_seek(file, 0, 0) != 0 {
    ignore(file_close(file))
    return None
  }
  let buffer = Bytes::new(size)
  let read = file_read(buffer, 1, size, file)
  ignore(file_close(file))
  if read == size {
    Some(@utf8.decode_lossy(buffer, ignore_bom=true))
  } else {
    None
  }
}

///|
#cfg(not(target="native"))
fn read_text_file(path : String) -> String? {
  let _ = path
  None
}

///|
fn simple_map() -> @grid.GridMap {
  @grid.new_grid(20, 20, p(0, 0), p(19, 19)).with_obstacles([
    p(4, 2),
    p(4, 3),
    p(4, 4),
    p(4, 5),
    p(4, 6),
    p(4, 7),
    p(8, 5),
    p(9, 5),
    p(10, 5),
    p(11, 5),
    p(12, 5),
    p(13, 5),
    p(15, 3),
    p(15, 4),
    p(15, 5),
    p(12, 9),
    p(12, 10),
    p(12, 11),
    p(12, 12),
    p(12, 13),
    p(12, 14),
    p(16, 10),
    p(17, 10),
    p(18, 10),
    p(3, 13),
    p(4, 13),
    p(5, 13),
    p(6, 13),
    p(7, 13),
    p(9, 16),
    p(10, 16),
    p(11, 16),
    p(12, 16),
    p(13, 16),
  ])
}

///|
fn complex_map() -> @grid.GridMap {
  @grid.new_grid(20, 20, p(0, 0), p(19, 19))
  .with_obstacles([
    p(3, 1),
    p(3, 2),
    p(3, 3),
    p(3, 5),
    p(3, 6),
    p(3, 7),
    p(3, 8),
    p(3, 9),
    p(3, 11),
    p(3, 12),
    p(3, 13),
    p(3, 14),
    p(3, 15),
    p(7, 4),
    p(7, 5),
    p(7, 6),
    p(7, 7),
    p(7, 9),
    p(7, 10),
    p(7, 11),
    p(7, 12),
    p(7, 13),
    p(7, 14),
    p(7, 16),
    p(7, 17),
    p(7, 18),
    p(7, 19),
    p(11, 0),
    p(11, 1),
    p(11, 2),
    p(11, 3),
    p(11, 4),
    p(11, 5),
    p(11, 7),
    p(11, 8),
    p(11, 9),
    p(11, 10),
    p(11, 11),
    p(11, 13),
    p(11, 14),
    p(15, 5),
    p(15, 6),
    p(15, 7),
    p(15, 8),
    p(15, 10),
    p(15, 11),
    p(15, 12),
    p(15, 13),
    p(15, 14),
    p(15, 15),
    p(15, 17),
    p(15, 18),
    p(15, 19),
    p(5, 3),
    p(6, 3),
    p(7, 3),
    p(8, 3),
    p(10, 3),
    p(12, 3),
    p(13, 3),
    p(15, 3),
    p(16, 3),
    p(17, 3),
    p(0, 8),
    p(1, 8),
    p(4, 8),
    p(5, 8),
    p(7, 8),
    p(8, 8),
    p(9, 8),
    p(12, 8),
    p(13, 8),
    p(6, 13),
    p(8, 13),
    p(9, 13),
    p(10, 13),
    p(12, 13),
    p(13, 13),
    p(14, 13),
    p(16, 13),
    p(18, 13),
    p(19, 13),
  ])
  .with_terrain(p(5, 5), 3.0)
  .with_terrain(p(6, 6), 3.0)
  .with_terrain(p(10, 10), 5.0)
  .with_terrain(p(14, 14), 4.0)
  .with_terrain(p(17, 16), 2.5)
}

///|
fn dynamic_map() -> @grid.GridMap {
  @grid.new_grid(5, 1, p(0, 0), p(4, 0))
}

///|
fn dynamic_crossing_map() -> @grid.GridMap {
  @grid.new_grid(10, 10, p(0, 0), p(9, 9))
}

///|
fn dynamic_mixed_map() -> @grid.GridMap {
  @grid.new_grid(12, 12, p(0, 0), p(11, 11)).with_obstacles([
    p(2, 4),
    p(3, 4),
    p(4, 2),
    p(4, 3),
    p(5, 9),
    p(7, 8),
    p(8, 7),
    p(9, 5),
  ])
}

///|
fn status_name(result : @core.PathResult) -> String {
  if result.status is @core.Found {
    "found"
  } else if result.status is @core.NoPath {
    "no_path"
  } else {
    "invalid_input"
  }
}

///|
fn default_repeats() -> Int {
  5
}

///|
fn heuristic_for(movement : @grid.MovementMode) -> @heuristics.HeuristicKind {
  match movement {
    @grid.FourWay => @heuristics.Manhattan
    @grid.EightWay => @heuristics.Octile
  }
}

///|
struct SafetyMetrics {
  evaluated : Int
  safe : Int
  min_clearance : Double
} derive(Debug)

///|
fn no_safety_metrics() -> SafetyMetrics {
  { evaluated: 0, safe: 0, min_clearance: 0.0 }
}

///|
fn bool_int(value : Bool) -> Int {
  if value {
    1
  } else {
    0
  }
}

///|
fn continuous_safety_metrics(
  path : Array[@core.Point],
  obstacles : Array[@dynamic.ContinuousMovingObstacle],
  options : @dynamic.CollisionOptions,
  step_time : Double,
  samples_per_segment : Int,
) -> SafetyMetrics {
  if path.length() < 2 || obstacles.is_empty() {
    return no_safety_metrics()
  }
  let segments = @dynamic.continuous_segments_from_grid_path(path, step_time)
  let safe = @dynamic.continuous_path_is_safe(
    segments, obstacles, options, samples_per_segment,
  )
  let min_clearance = match
    @dynamic.continuous_path_min_clearance(
      segments, obstacles, options, samples_per_segment,
    ) {
    Some(clearance) => clearance.clearance
    None => 0.0
  }
  { evaluated: 1, safe: bool_int(safe), min_clearance }
}

///|
fn print_result_with_safety(
  scenario : String,
  algorithm : String,
  result : @core.PathResult,
  iterations : Int,
  candidates : Int,
  best_fitness : Double,
  seed : Int,
  population_size : Int,
  max_iterations : Int,
  repeats : Int,
  elapsed_us_total : Double,
  safety : SafetyMetrics,
) -> Unit {
  let elapsed_us_avg = elapsed_us_total / repeats.to_double()
  println(
    "\{scenario},\{algorithm},\{status_name(result)},\{result.path.length()}," +
    "\{result.total_cost},\{@swarm.path_length(result.path)}," +
    "\{@swarm.path_smoothness(result.path)},\{result.visited_nodes}," +
    "\{result.expanded_nodes},\{iterations},\{candidates},\{best_fitness}," +
    "\{seed},\{population_size},\{max_iterations},\{repeats}," +
    "\{elapsed_us_total},\{elapsed_us_avg},\{safety.evaluated}," +
    "\{safety.safe},\{safety.min_clearance}",
  )
}

///|
fn print_result(
  scenario : String,
  algorithm : String,
  result : @core.PathResult,
  iterations : Int,
  candidates : Int,
  best_fitness : Double,
  seed : Int,
  population_size : Int,
  max_iterations : Int,
  repeats : Int,
  elapsed_us_total : Double,
) -> Unit {
  print_result_with_safety(
    scenario,
    algorithm,
    result,
    iterations,
    candidates,
    best_fitness,
    seed,
    population_size,
    max_iterations,
    repeats,
    elapsed_us_total,
    no_safety_metrics(),
  )
}

///|
fn adjusted_result(
  base : @core.PathResult,
  adjusted : Array[@core.Point],
) -> @core.PathResult {
  if base.status is @core.Found {
    @core.found_result(
      adjusted,
      @swarm.path_length(adjusted),
      base.visited_nodes,
      base.expanded_nodes,
    )
  } else {
    base
  }
}

///|
fn run_rs_apso_variant(
  name : String,
  map : @grid.GridMap,
  region : @region.SearchRegion,
  movement : @grid.MovementMode,
  population_size : Int,
  max_iterations : Int,
  stagnation_limit : Int,
  repeats : Int,
) -> Unit {
  let options = @swarm.options(
    population_size,
    max_iterations,
    42,
    stagnation_limit,
    movement,
    @swarm.default_fitness_weights(),
  )
  let start = @clock.monotonic_clock_start()
  let mut rs_apso = @swarm.rs_apso_plan(map, region, options)
  for _ in 1.. Unit {
  let repeats = default_repeats()
  let swarm_options = @swarm.options(
    60,
    100,
    42,
    3,
    movement,
    @swarm.default_fitness_weights(),
  )
  let rrt_options = @continuous.rrt_options(600, 42, 0.2)
  let rrt_star_options = @continuous.rrt_star_options(600, 42, 0.2, 3)
  let region = @region.build_search_region(map, @region.default_options())
  let heuristic = heuristic_for(movement)
  let astar_start = @clock.monotonic_clock_start()
  let mut astar = @algorithms.astar(map, movement, heuristic)
  for _ in 1.. Unit {
  let repeats = default_repeats()
  let map = dynamic_map()
  let movement = @grid.FourWay
  let options = @dynamic.collision_options(2.0, 0.5, 0.1)
  let base = @algorithms.astar(map, movement, @heuristics.Manhattan)
  let baseline_start = @clock.monotonic_clock_start()
  let mut baseline = base
  for _ in 1.. Unit {
  let repeats = default_repeats()
  let map = dynamic_crossing_map()
  let movement = @grid.EightWay
  let options = @dynamic.collision_options(2.0, 0.5, 0.1)
  let base = @algorithms.astar(map, movement, @heuristics.Octile)
  let baseline_start = @clock.monotonic_clock_start()
  let mut baseline = base
  for _ in 1.. Unit {
  let repeats = default_repeats()
  let map = dynamic_mixed_map()
  let movement = @grid.EightWay
  let options = @dynamic.collision_options(2.0, 0.5, 0.1)
  let base = @algorithms.astar(map, movement, @heuristics.Octile)
  let baseline_start = @clock.monotonic_clock_start()
  let mut baseline = base
  for _ in 1.. Unit {
  run_scenario("rs_apso_20x20_simple", simple_map(), @grid.EightWay)
  run_scenario("rs_apso_20x20_complex", complex_map(), @grid.EightWay)
  run_dynamic_scenarios()
}

///|
fn run_json_text(source : String, text : String) -> Bool {
  match @json_map.grid_from_json(text) {
    Some(data) => {
      run_scenario(source, data.map, data.movement)
      true
    }
    None => {
      println("Invalid benchmark grid JSON: \{source}")
      false
    }
  }
}

///|
fn run_json_file(path : String) -> Bool {
  match read_text_file(path) {
    Some(text) => run_json_text(path, text)
    None => {
      println("Could not read benchmark JSON file: \{path}")
      false
    }
  }
}

///|
fn run_example(name : String) -> Bool {
  match @json_map.example_json(name) {
    Some(text) => run_json_text("example:\{name}", text)
    None => {
      println("Unknown benchmark example map: \{name}")
      false
    }
  }
}

///|
fn usage() -> Unit {
  println("Usage:")
  println("  moon run ./bench")
  println("  moon run ./bench -- --json ")
  println("  moon run ./bench -- --example  [--example ]")
  println("  moon run ./bench --target native --  [more.json]")
  println("  moon run ./bench --target native -- --map ")
}

///|
fn json_args(args : Array[String]) -> Array[String]? {
  let texts = []
  let mut index = 1
  while index < args.length() {
    match args[index] {
      "--help" | "-h" => return Some([])
      "--json" | "-j" =>
        if index + 1 < args.length() {
          texts.push(args[index + 1])
          index = index + 1
        } else {
          return Some([])
        }
      _ => ()
    }
    index = index + 1
  }
  if texts.length() == 0 {
    None
  } else {
    Some(texts)
  }
}

///|
fn example_args(args : Array[String]) -> Array[String]? {
  let names = []
  let mut index = 1
  while index < args.length() {
    match args[index] {
      "--help" | "-h" => return Some([])
      "--example" | "-e" =>
        if index + 1 < args.length() {
          names.push(args[index + 1])
          index = index + 1
        } else {
          return Some([])
        }
      _ => ()
    }
    index = index + 1
  }
  if names.length() == 0 {
    None
  } else {
    Some(names)
  }
}

///|
fn map_args(args : Array[String]) -> Array[String]? {
  let paths = []
  let mut index = 1
  while index < args.length() {
    match args[index] {
      "--help" | "-h" => return Some([])
      "--map" | "-m" =>
        if index + 1 < args.length() {
          paths.push(args[index + 1])
          index = index + 1
        } else {
          return Some([])
        }
      value => paths.push(value)
    }
    index = index + 1
  }
  if paths.length() == 0 {
    None
  } else {
    Some(paths)
  }
}

///|
fn run_json_files(paths : Array[String]) -> Bool {
  let mut ok = true
  for path in paths {
    if !run_json_file(path) {
      ok = false
    }
  }
  ok
}

///|
fn run_examples(names : Array[String]) -> Bool {
  let mut ok = true
  for name in names {
    if !run_example(name) {
      ok = false
    }
  }
  ok
}

///|
fn run_json_texts(texts : Array[String]) -> Bool {
  let mut ok = true
  for index, text in texts {
    if !run_json_text("inline-json-\{index + 1}", text) {
      ok = false
    }
  }
  ok
}

///|
fn main {
  println(
    "scenario,algorithm,status,path_nodes,total_cost,path_length,smoothness," +
    "visited_nodes,expanded_nodes,iterations,candidates,best_fitness,seed," +
    "population_size,max_iterations,repeats,elapsed_us_total,elapsed_us_avg," +
    "safety_evaluated,continuous_safe,min_clearance",
  )
  let args = @env.args()
  match json_args(args) {
    Some(texts) =>
      if texts.length() == 0 {
        usage()
      } else {
        ignore(run_json_texts(texts))
      }
    None =>
      match example_args(args) {
        Some(names) =>
          if names.length() == 0 {
            usage()
          } else {
            ignore(run_examples(names))
          }
        None =>
          match map_args(args) {
            Some(paths) =>
              if paths.length() == 0 {
                usage()
              } else {
                ignore(run_json_files(paths))
              }
            None => run_default_scenarios()
          }
      }
  }
}