///|
/// The command line: parse the global options, parse the subcommand's options,
/// load the project, and dispatch.

///|
/// The argument list without the program name.
fn tail(args : Array[String]) -> Array[String] {
  let out : Array[String] = []
  for i in 1.. Bool {
  t == "-f" ||
  t == "--file" ||
  t == "-p" ||
  t == "--project-name" ||
  t == "--profile" ||
  t == "--env-file" ||
  t == "--podman-path" ||
  t == "--podman-args" ||
  t == "--in-pod" ||
  t == "--pod-args"
}

///|
/// Parse the global options up to the subcommand name. Returns the options, the
/// command, and the command's own arguments.
fn parse_global(
  tokens : Array[String],
) -> (GlobalOptions, String, Array[String]) raise Error {
  let g = new_global_options()
  let mut command = ""
  let rest : Array[String] = []
  let mut version_flag = false
  let mut help_flag = false
  let mut i = 0
  while i < tokens.length() {
    let t = tokens[i]
    if command.length() > 0 {
      rest.push(t)
      i += 1
      continue
    }
    if t == "--" {
      i += 1
      if i < tokens.length() {
        command = tokens[i]
        i += 1
      }
      continue
    }
    if t == "-v" || t == "--version" {
      version_flag = true
      i += 1
      continue
    }
    if t == "-h" || t == "--help" {
      help_flag = true
      i += 1
      continue
    }
    if t == "--dry-run" {
      g.dry_run = true
      i += 1
      continue
    }
    if t == "--verbose" {
      g.verbose = true
      i += 1
      continue
    }
    if t == "--no-ansi" {
      g.no_ansi = true
      i += 1
      continue
    }
    if t == "--no-cleanup" {
      g.no_cleanup = true
      i += 1
      continue
    }
    if t.has_prefix("--podman-") && t.has_suffix("-args") {
      // `--podman--args VALUE` injects arguments into `podman `.
      let sub = t[9:t.length() - 5].to_owned()
      if i + 1 >= tokens.length() {
        raise fail("\{t} requires a value")
      }
      let extra = g.podman_cmd_args.get(sub).unwrap_or([])
      extra.push(tokens[i + 1])
      g.podman_cmd_args[sub] = extra
      i += 2
      continue
    }
    if t.has_prefix("--") {
      match split_long(t) {
        Some(pair) => {
          let (name, inline_value) = pair
          let value = match inline_value {
            Some(v) => v
            None => {
              if i + 1 >= tokens.length() {
                raise fail("\{name} requires a value")
              }
              i += 1
              tokens[i]
            }
          }
          match name {
            "--file" => g.files.push(value)
            "--project-name" => g.project_name = Some(value)
            "--profile" => g.profiles.push(value)
            "--env-file" => g.env_files.push(value)
            "--podman-path" => g.podman_path = value
            "--podman-args" => g.podman_args.push(value)
            "--in-pod" => g.in_pod = Some(value)
            "--pod-args" => g.pod_args = Some(value)
            _ => raise fail("unknown option: \{name} (try --help)")
          }
          i += 1
          continue
        }
        None => raise fail("unknown option: \{t} (try --help)")
      }
    }
    if t.has_prefix("-") && t.length() > 1 {
      // Short value-taking global options.
      let name = t
      if is_global_value_flag(name) {
        if i + 1 >= tokens.length() {
          raise fail("\{name} requires a value")
        }
        let value = tokens[i + 1]
        match name {
          "-f" => g.files.push(value)
          "-p" => g.project_name = Some(value)
          _ => ()
        }
        i += 2
        continue
      }
      raise fail("unknown option: \{t} (try --help)")
    }
    command = t
    i += 1
  }
  if command.length() == 0 {
    if version_flag {
      command = "version"
    } else if help_flag {
      command = "help"
    }
  }
  if help_flag {
    command = "help"
  }
  (g, command, rest)
}

///|
/// Split `--name=value` into `("--name", Some("value"))`, or `("--name", None)`.
fn split_long(t : String) -> (String, String?)? {
  match t.split_once("=") {
    Some(pair) => Some((pair.0.to_owned(), Some(pair.1.to_owned())))
    None => Some((t, None))
  }
}

///|
/// Parse the subcommand's own options.
fn parse_command(
  command : String,
  tokens : Array[String],
) -> CommandArgs raise Error {
  let args = new_command_args()
  let mut i = 0
  // For `run` and `exec`, everything after the service name belongs to the
  // command inside the container.
  let mut in_command = false
  while i < tokens.length() {
    let t = tokens[i]
    if in_command {
      args.cnt_command.push(t)
      i += 1
      continue
    }
    if t == "--" {
      i += 1
      in_command = true
      continue
    }
    let is_positional = !t.has_prefix("-") || t == "-"
    if is_positional {
      match command {
        "run" | "exec" =>
          if args.services.length() == 0 {
            args.services.push(t)
            in_command = true
            i += 1
            continue
          }
        "port" =>
          if args.services.length() == 0 {
            args.services.push(t)
          } else {
            match try_int_str(t) {
              Some(p) => args.private_port = p
              None => raise fail("private_port must be a number")
            }
          }
        _ => args.services.push(t)
      }
      i += 1
      continue
    }
    // Long options.
    if t.has_prefix("--") {
      let (name, inline_value) = match split_long(t) {
        Some(p) => p
        None => (t, None)
      }
      let needs_value = long_needs_value(name)
      let value : String = if needs_value {
        match inline_value {
          Some(v) => v
          None => {
            if i + 1 >= tokens.length() {
              raise fail("\{name} requires a value")
            }
            i += 1
            tokens[i]
          }
        }
      } else {
        inline_value.unwrap_or("")
      }
      apply_long(command, args, name, value)
      i += 1
      continue
    }
    // Short options.
    let needs_value = short_needs_value(command, t)
    let value : String = if needs_value {
      if i + 1 >= tokens.length() {
        raise fail("\{t} requires a value")
      }
      i += 1
      tokens[i]
    } else {
      ""
    }
    apply_short(command, args, t, value)
    i += 1
  }
  args
}

///|
fn long_needs_value(name : String) -> Bool {
  [
    "--name", "--entrypoint", "--env", "--label", "--user", "--publish", "--volume",
    "--workdir", "--timeout", "--tail", "--since", "--until", "--format", "--signal",
    "--index", "--protocol", "--rmi", "--pull", "--build-arg", "--exit-code-from",
    "--no-attach", "--scale", "--wait-timeout", "--pod-args", "--in-pod",
  ].contains(name)
}

///|
fn short_needs_value(command : String, t : String) -> Bool {
  match t {
    "-e" | "-u" | "-w" | "-p" | "-v" | "-s" | "-i" => true
    "-t" =>
      command == "logs" ||
      command == "up" ||
      command == "down" ||
      command == "stop" ||
      command == "restart"
    "-l" => command == "run"
    "-f" => false
    _ => false
  }
}

///|
fn apply_long(
  command : String,
  args : CommandArgs,
  name : String,
  value : String,
) -> Unit raise Error {
  match name {
    "--detach" => args.detach = true
    "--no-deps" => args.no_deps = true
    "--no-build" => args.no_build = true
    "--build" => args.build = true
    "--force-recreate" => args.force_recreate = true
    "--no-recreate" => args.no_recreate = true
    "--no-start" => args.no_start = true
    "--volumes" => args.volumes = true
    "--remove-orphans" => args.remove_orphans = true
    "--rmi" => args.rmi = Some(if value.length() == 0 { "all" } else { value })
    "--timeout" =>
      match try_int_str(value) {
        Some(t) => args.timeout = Some(t)
        None => raise fail("--timeout must be a number")
      }
    "--follow" => args.follow = true
    "--tail" => args.tail = Some(value)
    "--since" => args.since = Some(value)
    "--until" => args.until = Some(value)
    "--timestamps" => args.timestamps = true
    "--names" => args.names = true
    "--no-color" => args.no_color = true
    "--no-log-prefix" => args.no_log_prefix = true
    "--quiet" => args.quiet = true
    "--format" => args.format = Some(value)
    "--short" => args.short = true
    "--signal" => args.signal = Some(value)
    "--all" => args.all = true
    "--index" =>
      match try_int_str(value) {
        Some(n) => args.index = n
        None => raise fail("--index must be a number")
      }
    "--protocol" => args.protocol = value
    "--no-cache" => args.no_cache = true
    "--pull" =>
      args.pull = Some(if value.length() == 0 { "newer" } else { value })
    "--pull-always" => args.pull = Some("always")
    "--build-arg" => args.build_arg.push(value)
    "--quiet-pull" => args.quiet_pull = true
    "--wait" => args.wait = true
    "--wait-timeout" =>
      match try_int_str(value) {
        Some(t) => args.wait_timeout = Some(t)
        None => raise fail("--wait-timeout must be a number")
      }
    "--name" => args.name = Some(value)
    "--entrypoint" => args.entrypoint = Some(value)
    "--env" => args.env.push(value)
    "--label" => args.build_arg.push("--label=" + value)
    "--user" => args.user = Some(value)
    "--workdir" => args.workdir = Some(value)
    "--rm" => args.rm = true
    "--publish" => args.publish.push(value)
    "--service-ports" => args.service_ports = true
    "--volume" => args.volume.push(value)
    "--privileged" => args.privileged = true
    "--force-local" => args.force_local = true
    "--services" => args.services_flag = true
    "--no-normalize" => args.no_normalize = true
    "--abort-on-container-exit" => args.abort_on_container_exit = true
    "--abort-on-container-failure" => args.abort_on_container_failure = true
    "--exit-code-from" => args.exit_code_from = Some(value)
    "--no-attach" => args.no_attach.push(value)
    "--scale" => args.scale = Some(value)
    "--renew-anon-volumes" => args.renew_anon_volumes = true
    "--no-hosts" => args.no_hosts = true
    _ => raise fail("unknown option for \{command}: \{name}")
  }
}

///|
fn apply_short(
  command : String,
  args : CommandArgs,
  t : String,
  value : String,
) -> Unit raise Error {
  match t {
    "-d" => args.detach = true
    "-f" =>
      match command {
        "version" | "ps" | "stats" | "ls" => args.format = Some(value)
        "logs" => args.follow = true
        _ => ()
      }
    "-q" => args.quiet = true
    "-t" =>
      match command {
        "logs" => args.timestamps = true
        _ =>
          match try_int_str(value) {
            Some(n) => args.timeout = Some(n)
            None => raise fail("-t must be a number")
          }
      }
    "-l" =>
      match command {
        "run" => args.build_arg.push("--label=" + value)
        _ => ()
      }
    "-n" => args.names = true
    "-i" =>
      match command {
        "stats" =>
          match try_int_str(value) {
            Some(n) => args.index = n
            None => ()
          }
        _ => ()
      }
    "-e" => args.env.push(value)
    "-u" => args.user = Some(value)
    "-w" => args.workdir = Some(value)
    "-p" => args.publish.push(value)
    "-v" =>
      match command {
        "down" => args.volumes = true
        _ => args.volume.push(value)
      }
    "-T" => args.no_tty = true
    "-V" => args.renew_anon_volumes = true
    "-a" =>
      match command {
        "kill" => args.all = true
        _ => ()
      }
    "-s" => args.signal = Some(value)
    _ => raise fail("unknown option for \{command}: \{t}")
  }
}

///|
/// The `podcomp --help` text.
pub fn print_help() -> Unit {
  println("podcomp — run Compose files with Podman")
  println("")
  println(
    "Usage: podcomp [global options] COMMAND [command options] [services...]",
  )
  println("")
  println("Global options:")
  println("  -f, --file FILE          compose file (repeatable, '-' for stdin)")
  println("  -p, --project-name NAME  project name (default: directory name)")
  println("      --profile NAME       enable a profile (repeatable)")
  println("      --env-file FILE      alternate environment file (repeatable)")
  println("      --podman-path PATH   podman binary (default: podman)")
  println("      --podman-args ARGS   extra global arguments for podman")
  println("      --dry-run            print the commands without running them")
  println("      --in-pod true|false|NAME")
  println("  -v, --version            show the version")
  println("  -h, --help               show this help")
  println("")
  println("Commands:")
  println("  up        create and start the stack")
  println("  down      stop and remove the stack")
  println("  ps        list containers")
  println("  logs      show container logs")
  println("  start     start services")
  println("  stop      stop services")
  println("  restart   restart services")
  println("  kill      kill services")
  println("  rm        remove stopped containers")
  println("  pull      pull service images")
  println("  build     build service images")
  println("  images    list service images")
  println("  exec      run a command in a running container")
  println("  run       run a one-off command in a new container")
  println("  port      print the host port for a published port")
  println("  pause     pause services")
  println("  unpause   unpause services")
  println("  config    print the merged compose file")
  println("  version   show the version")
}

///|
/// The subcommands `dispatch` knows how to run.
fn is_known_command(command : String) -> Bool {
  [
    "config", "up", "down", "ps", "logs", "start", "stop", "restart", "kill", "rm",
    "pull", "build", "images", "exec", "run", "port", "pause", "unpause",
  ].contains(command)
}

///|
/// The entry point the executable calls.
pub async fn main_cli() -> Unit raise Error {
  let argv = tail(@env.args())
  let (g, command, rest) = parse_global(argv)
  if command.length() == 0 || command == "help" {
    print_help()
    return
  }
  if command != "version" && !is_known_command(command) {
    eprintln("podcomp: unknown command: \{command}")
    print_help()
    @sys.exit(1)
  }
  let cargs = parse_command(command, rest)
  let podman = new_podman(g)
  if command == "version" {
    cmd_version(podman, cargs)
    return
  }
  // Every other command needs a project.
  let compose = load_compose(g)
  let code = dispatch(command, compose, podman, cargs)
  if code != 0 {
    @sys.exit(code)
  }
}

///|
/// Run one command against a loaded project.
async fn dispatch(
  command : String,
  compose : Compose,
  podman : Podman,
  args : CommandArgs,
) -> Int32 raise Error {
  match command {
    "config" => {
      cmd_config(compose, args)
      0
    }
    "up" => cmd_up(compose, podman, args)
    "down" => cmd_down(compose, podman, args)
    "ps" => cmd_ps(compose, podman, args)
    "logs" => cmd_logs(compose, podman, args)
    "start" => cmd_start(compose, podman, args)
    "stop" => cmd_stop(compose, podman, args)
    "restart" => cmd_restart(compose, podman, args)
    "kill" => cmd_kill(compose, podman, args)
    "rm" => cmd_rm(compose, podman, args)
    "pull" => cmd_pull(compose, podman, args)
    "build" => cmd_build(compose, podman, args)
    "images" => cmd_images(compose, podman, args)
    "exec" => cmd_exec(compose, podman, args)
    "run" => cmd_run(compose, podman, args)
    "port" => cmd_port(compose, podman, args)
    "pause" => cmd_pause_unpause(compose, podman, args, "pause")
    "unpause" => cmd_pause_unpause(compose, podman, args, "unpause")
    _ => {
      eprintln("podcomp: unknown command: \{command}")
      print_help()
      1
    }
  }
}