///|
/// Result of one CLI invocation. `Ok` is printed to stdout. `Err` is printed
/// to stderr and the process exits with status 1.
enum Run {
  Ok(String)
  Err(String)
}

///|
fn usage() -> String {
  let text =
    #|moongeodesy
    #|
    #|Usage:
    #|  geod inverse    
    #|  geod direct    
    #|  rhumb inverse    
    #|  rhumb direct    
    #|  intersect closest      
    #|  enu forward     [h]
    #|  enu reverse     
    #|  coord wgs2gcj  
    #|  coord gcj2wgs  
    #|  coord gcj2bd  
    #|  coord bd2gcj  
    #|  coord wgs2bd  
    #|  coord bd2wgs  
    #|
    #|geod / rhumb / intersect / enu use the WGS-84 ellipsoid.
    #|Distances and ENU offsets are metres. Angles are degrees. Heights are
    #|ellipsoidal metres (default 0 for enu forward).
    #|coord converts one point. gcj2wgs, bd2gcj, and bd2wgs are exact inverses.
    #|
    #|  moon run src/examples/cli -- geod inverse 39.9087 116.3975 31.2304 121.4737
    #|  moon run src/examples/cli -- rhumb inverse 51.4778 -0.0015 35.6762 139.6503
    #|  moon run src/examples/cli -- intersect closest 0 0 45 45 10 135
    #|  moon run src/examples/cli -- enu forward 39.9087 116.3975 39.9097 116.3985 50
    #|  moon run src/examples/cli -- coord wgs2gcj 39.9087 116.3975
  text
}

///|
fn die(message : String) -> Run {
  Err("error: \{message}\n\n\{usage()}")
}

///|
fn is_runtime_prefix(token : String) -> Bool {
  token == "node" || token.contains("/") || token.contains("\\")
}

///|
/// Drop the executable path (and `node` / the script path on JavaScript).
fn user_args(argv : Array[String]) -> Array[String] {
  let mut start = 0
  while start < argv.length() && is_runtime_prefix(argv[start]) {
    start = start + 1
  }
  let out : Array[String] = []
  for i in start.. Double? {
  Some(@string.parse_double(token)) catch {
    _ => None
  }
}

///|
fn bad_token(tokens : Array[String]) -> String {
  for t in tokens {
    if parse_num(t) is None {
      return t
    }
  }
  ""
}

///|
fn rest_from(args : Array[String], start : Int32) -> Array[String] {
  let out : Array[String] = []
  for i in start.. Run {
  let args = user_args(argv)
  if args.length() == 0 ||
    args[0] == "help" ||
    args[0] == "-h" ||
    args[0] == "--help" {
    return Ok(usage())
  }
  if args[0] == "geod" {
    return execute_geod(args)
  }
  if args[0] == "rhumb" {
    return execute_rhumb(args)
  }
  if args[0] == "intersect" {
    return execute_intersect(args)
  }
  if args[0] == "enu" {
    return execute_enu(args)
  }
  if args[0] == "coord" {
    return execute_coord(args)
  }
  die("unknown command `\{args[0]}`")
}

///|
fn execute_geod(args : Array[String]) -> Run {
  if args.length() < 2 {
    return die("geod needs a subcommand: inverse or direct")
  }
  let rest = rest_from(args, 2)
  match args[1] {
    "inverse" => geod_inverse(rest)
    "direct" => geod_direct(rest)
    other => die("unknown geod command `\{other}`")
  }
}

///|
fn geod_inverse(tokens : Array[String]) -> Run {
  if tokens.length() != 4 {
    return die("geod inverse needs 4 numbers:    ")
  }
  match
    (
      parse_num(tokens[0]),
      parse_num(tokens[1]),
      parse_num(tokens[2]),
      parse_num(tokens[3]),
    ) {
    (Some(lat1), Some(lon1), Some(lat2), Some(lon2)) => {
      let r = @geodesic.Geodesic::wgs84().inverse(lat1, lon1, lat2, lon2)
      Ok(
        "distance_m: \{r.s12}\nazimuth1_deg: \{r.azi1}\nazimuth2_deg: \{r.azi2}",
      )
    }
    _ => die("not a number: `\{bad_token(tokens)}`")
  }
}

///|
fn geod_direct(tokens : Array[String]) -> Run {
  if tokens.length() != 4 {
    return die("geod direct needs 4 numbers:    ")
  }
  match
    (
      parse_num(tokens[0]),
      parse_num(tokens[1]),
      parse_num(tokens[2]),
      parse_num(tokens[3]),
    ) {
    (Some(lat1), Some(lon1), Some(azi1), Some(s12)) => {
      let d = @geodesic.Geodesic::wgs84().direct(lat1, lon1, azi1, s12)
      Ok("lat2: \{d.lat2}\nlon2: \{d.lon2}\nazimuth2_deg: \{d.azi2}")
    }
    _ => die("not a number: `\{bad_token(tokens)}`")
  }
}

///|
fn execute_rhumb(args : Array[String]) -> Run {
  if args.length() < 2 {
    return die("rhumb needs a subcommand: inverse or direct")
  }
  let rest = rest_from(args, 2)
  match args[1] {
    "inverse" => rhumb_inverse(rest)
    "direct" => rhumb_direct(rest)
    other => die("unknown rhumb command `\{other}`")
  }
}

///|
fn rhumb_inverse(tokens : Array[String]) -> Run {
  if tokens.length() != 4 {
    return die("rhumb inverse needs 4 numbers:    ")
  }
  match
    (
      parse_num(tokens[0]),
      parse_num(tokens[1]),
      parse_num(tokens[2]),
      parse_num(tokens[3]),
    ) {
    (Some(lat1), Some(lon1), Some(lat2), Some(lon2)) => {
      let r = @geodesic.Rhumb::wgs84().inverse(lat1, lon1, lat2, lon2)
      Ok("distance_m: \{r.s12}\nazimuth_deg: \{r.azi12}\narea_m2: \{r.area12}")
    }
    _ => die("not a number: `\{bad_token(tokens)}`")
  }
}

///|
fn rhumb_direct(tokens : Array[String]) -> Run {
  if tokens.length() != 4 {
    return die("rhumb direct needs 4 numbers:    ")
  }
  match
    (
      parse_num(tokens[0]),
      parse_num(tokens[1]),
      parse_num(tokens[2]),
      parse_num(tokens[3]),
    ) {
    (Some(lat1), Some(lon1), Some(azi12), Some(s12)) => {
      let d = @geodesic.Rhumb::wgs84().direct(lat1, lon1, azi12, s12)
      Ok("lat2: \{d.lat2}\nlon2: \{d.lon2}\narea_m2: \{d.area12}")
    }
    _ => die("not a number: `\{bad_token(tokens)}`")
  }
}

///|
fn execute_intersect(args : Array[String]) -> Run {
  if args.length() < 2 {
    return die("intersect needs a subcommand: closest")
  }
  let rest = rest_from(args, 2)
  match args[1] {
    "closest" => intersect_closest(rest)
    other => die("unknown intersect command `\{other}`")
  }
}

///|
fn intersect_closest(tokens : Array[String]) -> Run {
  if tokens.length() != 6 {
    return die(
      "intersect closest needs 6 numbers:      ",
    )
  }
  match
    (
      parse_num(tokens[0]),
      parse_num(tokens[1]),
      parse_num(tokens[2]),
      parse_num(tokens[3]),
      parse_num(tokens[4]),
      parse_num(tokens[5]),
    ) {
    (
      Some(lat_x),
      Some(lon_x),
      Some(azi_x),
      Some(lat_y),
      Some(lon_y),
      Some(azi_y),
    ) => {
      let cross = @geodesic.Intersect::wgs84().closest(
        lat_x, lon_x, azi_x, lat_y, lon_y, azi_y,
      ) catch {
        e => return die("intersect closest failed: \{e.to_repr()}")
      }
      Ok("x_m: \{cross.x}\ny_m: \{cross.y}\nc: \{cross.c}")
    }
    _ => die("not a number: `\{bad_token(tokens)}`")
  }
}

///|
fn execute_enu(args : Array[String]) -> Run {
  if args.length() < 2 {
    return die("enu needs a subcommand: forward or reverse")
  }
  let rest = rest_from(args, 2)
  match args[1] {
    "forward" => enu_forward(rest)
    "reverse" => enu_reverse(rest)
    other => die("unknown enu command `\{other}`")
  }
}

///|
fn enu_forward(tokens : Array[String]) -> Run {
  if tokens.length() != 4 && tokens.length() != 5 {
    return die(
      "enu forward needs 4 or 5 numbers:     [h]",
    )
  }
  let h_tok = if tokens.length() == 5 { tokens[4] } else { "0" }
  match
    (
      parse_num(tokens[0]),
      parse_num(tokens[1]),
      parse_num(tokens[2]),
      parse_num(tokens[3]),
      parse_num(h_tok),
    ) {
    (Some(lat0), Some(lon0), Some(lat), Some(lon), Some(h)) => {
      let lc = @geodesic.LocalCartesian::wgs84(lat0, lon0)
      let enu = lc.forward(lat, lon, h)
      Ok("east_m: \{enu.x}\nnorth_m: \{enu.y}\nup_m: \{enu.z}")
    }
    _ => die("not a number: `\{bad_token(tokens)}`")
  }
}

///|
fn enu_reverse(tokens : Array[String]) -> Run {
  if tokens.length() != 5 {
    return die("enu reverse needs 5 numbers:     ")
  }
  match
    (
      parse_num(tokens[0]),
      parse_num(tokens[1]),
      parse_num(tokens[2]),
      parse_num(tokens[3]),
      parse_num(tokens[4]),
    ) {
    (Some(lat0), Some(lon0), Some(x), Some(y), Some(z)) => {
      let lc = @geodesic.LocalCartesian::wgs84(lat0, lon0)
      let back = lc.reverse(x, y, z)
      Ok("lat: \{back.lat}\nlon: \{back.lon}\nh_m: \{back.h}")
    }
    _ => die("not a number: `\{bad_token(tokens)}`")
  }
}

///|
fn execute_coord(args : Array[String]) -> Run {
  if args.length() != 4 {
    return die("coord needs   ")
  }
  let op = args[1]
  match (parse_num(args[2]), parse_num(args[3])) {
    (None, _) => die("not a number: `\{args[2]}`")
    (_, None) => die("not a number: `\{args[3]}`")
    (Some(lat), Some(lon)) =>
      match convert(op, lat, lon) {
        Some(text) => Ok(text)
        None => die("unknown coord command `\{op}`")
      }
  }
}

///|
fn coord_line(lat : Double, lon : Double) -> String {
  "lat: \{lat}\nlon: \{lon}"
}

///|
fn convert(op : String, lat : Double, lon : Double) -> String? {
  let p = @cn.LatLon::new(lat, lon)
  match op {
    "wgs2gcj" => {
      let q = @cn.wgs84_to_gcj02(p)
      Some(coord_line(q.lat, q.lon))
    }
    "gcj2wgs" => {
      let q = @cn.gcj02_to_wgs84(p)
      Some(coord_line(q.lat, q.lon))
    }
    "gcj2bd" => {
      let q = @cn.gcj02_to_bd09(p)
      Some(coord_line(q.lat, q.lon))
    }
    "bd2gcj" => {
      let q = @cn.bd09_to_gcj02(p)
      Some(coord_line(q.lat, q.lon))
    }
    "wgs2bd" => {
      let q = @cn.wgs84_to_bd09(p)
      Some(coord_line(q.lat, q.lon))
    }
    "bd2wgs" => {
      let q = @cn.bd09_to_wgs84(p)
      Some(coord_line(q.lat, q.lon))
    }
    _ => None
  }
}