///|
fn print_usage() -> Unit {
  println("MoonBit GaitLab - Biomechanical Gait Analysis CLI Utility")
  println("Usage:")
  println("  moon run main -- [options]")
  println("")
  println("Options:")
  println(
    "  -d, --demo               Run analysis on a high-fidelity simulated clinical gait dataset",
  )
  println(
    "  -c, --csv-data   Analyze raw CSV joint trajectory dataset passed as string",
  )
  println(
    "  -j, --json-data  Analyze raw JSON joint trajectory dataset passed as string",
  )
  println(
    "  -f, --format  Output format (default: markdown)",
  )
  println("      --strict            Treat trajectory quality errors as fatal")
  println(
    "      --sampling-rate  Override the input sampling-rate metadata",
  )
  println("      --benchmark         Print benchmark entry-point guidance")
  println("  -h, --help               Display this help information")
  println("")
  println("Examples:")
  println("  moon run main -- --demo")
  println("  moon run main -- --demo > report.md")
}

///|
fn option_value(args : Array[String], name : String) -> String? {
  for index = 0; index + 1 < args.length(); index = index + 1 {
    if args[index] == name {
      return Some(args[index + 1])
    }
  }
  None
}

///|
fn analysis_config(args : Array[String]) -> @pipeline.AnalysisConfig {
  let defaults = @pipeline.default_config()
  let strict = args.contains("--strict")
  let mut sampling_rate = defaults.sampling_rate
  match option_value(args, "--sampling-rate") {
    None => ()
    Some(value) =>
      match @math.parse_double(value) {
        Ok(rate) => if rate > 0.0 { sampling_rate = rate }
        Err(_) => ()
      }
  }
  @pipeline.AnalysisConfig::{
    strict,
    sampling_rate,
    filter: defaults.filter,
    events: defaults.events,
  }
}

///|
fn output_format(args : Array[String]) -> String {
  match option_value(args, "--format") {
    Some(value) => value
    None => "markdown"
  }
}

///|
fn parse_cell(row : Array[String], index : Int) -> Result[Double, String] {
  if index < 0 || index >= row.length() {
    return Err("CSV row is missing column " + index.to_string())
  }
  match @math.parse_double(row[index]) {
    Ok(value) => Ok(value)
    Err(_) => Err("CSV column " + index.to_string() + " is not numeric")
  }
}

///|
fn parse_csv_frames(input : String) -> Result[Array[@gait.JointFrame], String] {
  match @csv.parse_csv(input) {
    Err(error) => Err(error)
    Ok(rows) => {
      if rows.length() < 2 {
        return Err("CSV requires a header and at least one row")
      }
      if rows[0].length() < 13 {
        return Err("CSV header must contain time plus 12 coordinate columns")
      }
      let frames = Array::new()
      for row_index = 1; row_index < rows.length(); row_index = row_index + 1 {
        let row = rows[row_index]
        let values = Array::new()
        for column = 0; column < 13; column = column + 1 {
          match parse_cell(row, column) {
            Err(error) => return Err(error + " at row " + row_index.to_string())
            Ok(value) => values.push(value)
          }
        }
        frames.push(@gait.JointFrame::{
          time: values[0],
          hip: @math.Point3D::{ x: values[1], y: values[2], z: values[3] },
          knee: @math.Point3D::{ x: values[4], y: values[5], z: values[6] },
          ankle: @math.Point3D::{ x: values[7], y: values[8], z: values[9] },
          foot: @math.Point3D::{ x: values[10], y: values[11], z: values[12] },
        })
      }
      Ok(frames)
    }
  }
}

///|
fn json_number(
  value : @json.JsonValue?,
  label : String,
) -> Result[Double, String] {
  match value {
    Some(@json.JsonValue::Number(number)) => Ok(number)
    _ => Err("JSON field " + label + " must be numeric")
  }
}

///|
fn json_point(
  value : @json.JsonValue?,
  label : String,
) -> Result[@math.Point3D, String] {
  match value {
    Some(object) =>
      match object {
        @json.JsonValue::Object(values) =>
          match json_number(values.get("x"), label + ".x") {
            Err(error) => Err(error)
            Ok(x) =>
              match json_number(values.get("y"), label + ".y") {
                Err(error) => Err(error)
                Ok(y) =>
                  match json_number(values.get("z"), label + ".z") {
                    Err(error) => Err(error)
                    Ok(z_value) => Ok(@math.Point3D::{ x, y, z: z_value })
                  }
              }
          }
        _ => Err("JSON field " + label + " must be an object")
      }
    None => Err("JSON field " + label + " is missing")
  }
}

///|
fn parse_json_frames(input : String) -> Result[Array[@gait.JointFrame], String] {
  match @json.parse_json(input) {
    Err(error) => Err(error)
    Ok(root) =>
      match root.get("frames") {
        Some(@json.JsonValue::Array(items)) => {
          let frames = Array::new()
          for item in items {
            match item {
              @json.JsonValue::Object(values) =>
                match json_number(values.get("time"), "time") {
                  Err(error) => return Err(error)
                  Ok(time) =>
                    match json_point(values.get("left_foot"), "left_foot") {
                      Err(error) => return Err(error)
                      Ok(foot) =>
                        frames.push(@gait.JointFrame::{
                          time,
                          hip: foot,
                          knee: foot,
                          ankle: foot,
                          foot,
                        })
                    }
                }
              _ => return Err("JSON frames must contain objects")
            }
          }
          Ok(frames)
        }
        _ => Err("JSON must contain a frames array")
      }
  }
}

///|
fn should_print_demo_banner(format : String) -> Bool {
  format != "json" && format != "JSON" && format != "csv" && format != "CSV"
}

///|
fn run_demo(format : String, config : @pipeline.AnalysisConfig) -> Unit {
  let verbose = should_print_demo_banner(format)
  if verbose {
    println("Initializing MoonBit GaitLab Demo...")
    println("Simulating 3.5 seconds of post-stroke hemiplegic walk at 100Hz...")
  }

  let left_frames = Array::new()
  let right_frames = Array::new()
  let fs = 100.0
  let dt = 1.0 / fs
  let num_frames = 350

  for i = 0; i < num_frames; i = i + 1 {
    let t = i.to_double() * dt
    let speed = 0.7
    let x_disp = speed * t

    // Left foot (Healthy)
    let left_stride_progress = t - (t / 1.0).to_int().to_double() * 1.0
    let left_height = if left_stride_progress > 0.60 {
      let swing = (left_stride_progress - 0.60) / 0.40
      0.12 * @math.sin(swing * @math.PI)
    } else {
      0.0
    }
    let left_foot = @math.Point3D::{ x: x_disp, y: left_height, z: 0.0 }
    left_frames.push(@gait.JointFrame::{
      time: t,
      hip: @math.Point3D::{ x: x_disp, y: 0.9, z: 0.0 },
      knee: @math.Point3D::{ x: x_disp, y: 0.5, z: 0.0 },
      ankle: @math.Point3D::{ x: x_disp, y: 0.1, z: 0.0 },
      foot: left_foot,
    })

    // Right foot (Hemiplegic affected)
    let right_t_shifted = t + 0.5
    let right_stride_progress = right_t_shifted -
      (right_t_shifted / 1.0).to_int().to_double() * 1.0
    let right_height = if right_stride_progress > 0.68 {
      let swing = (right_stride_progress - 0.68) / 0.32
      0.05 * @math.sin(swing * @math.PI)
    } else {
      0.0
    }
    let right_x = x_disp - 0.12
    let right_foot = @math.Point3D::{ x: right_x, y: right_height, z: 0.14 }
    right_frames.push(@gait.JointFrame::{
      time: t,
      hip: @math.Point3D::{ x: x_disp, y: 0.9, z: 0.1 },
      knee: @math.Point3D::{ x: x_disp, y: 0.5, z: 0.1 },
      ankle: @math.Point3D::{ x: x_disp, y: 0.1, z: 0.1 },
      foot: right_foot,
    })
  }

  let result = @pipeline.analyze_frames(left_frames, right_frames, config)
  if verbose {
    println(
      "\n=================================================================",
    )
    println("                       GAIT ANALYSIS RESULTS                     ")
    println("=================================================================")
  }
  println(@report.render_report(result.report, format))
  if verbose && @pipeline.has_fatal_diagnostics(result) {
    println("Diagnostics: " + @pipeline.diagnostics_to_text(result.diagnostics))
  }
}

///|
fn main {
  let args = @env.args()

  if args.length() <= 1 {
    print_usage()
    return
  }

  let arg1 = args[1]
  if arg1 == "-h" || arg1 == "--help" {
    print_usage()
  } else if arg1 == "-d" || arg1 == "--demo" {
    run_demo(output_format(args), analysis_config(args))
  } else if arg1 == "-c" || arg1 == "--csv-data" {
    if args.length() < 3 {
      println("Error: Missing CSV string argument.")
      return
    }
    let csv_str = args[2]
    match parse_csv_frames(csv_str) {
      Err(e) => println("CSV Parse Error: " + e)
      Ok(frames) => {
        let result = @pipeline.analyze_frames(
          frames,
          frames,
          analysis_config(args),
        )
        println(@report.render_report(result.report, output_format(args)))
      }
    }
  } else if arg1 == "-j" || arg1 == "--json-data" {
    if args.length() < 3 {
      println("Error: Missing JSON string argument.")
      return
    }
    let json_str = args[2]
    match @json.parse_json(json_str) {
      Err(e) => println("JSON Parse Error: " + e)
      Ok(_) =>
        match parse_json_frames(json_str) {
          Err(e) => println("JSON Frame Error: " + e)
          Ok(frames) => {
            let result = @pipeline.analyze_frames(
              frames,
              frames,
              analysis_config(args),
            )
            println(@report.render_report(result.report, output_format(args)))
          }
        }
    }
  } else if arg1 == "--benchmark" {
    println(
      "Run the native benchmark with: pwsh -File scripts/run_benchmark.ps1",
    )
  } else {
    println("Unknown option: " + arg1)
    print_usage()
  }
}