///|
pub(open) trait LeafCommand: ToJson + @json.FromJson + Default {}

///|
priv suberror ParseError {
  NoSubcommandFoundError
  NoSubcommandMatchError
  MissingArgumentError(String)
  UnknownArgumentError(String)
  DecodeError(@json.JsonDecodeError)
  InvalidCommandError
  NoPositionalFieldError
  PositionalIsNotArrayError
} derive(Debug)

///|
struct CommandParser[T] {
  run : (ArrayView[String]) -> T raise ParseError
}

///|
pub fn[T] CommandParser::parse(
  self : CommandParser[T],
  args : Array[String],
) -> T {
  (self.run)(args[:]) catch {
    e => {
      println(Repr(e))
      panic()
    }
  }
}

///|
pub fn[T] CommandParser::parse_terminated(self : CommandParser[T]) -> T {
  (self.run)(get_cli_args()[2:]) catch {
    e => {
      println(Repr(e))
      panic()
    }
  }
}

///|
pub fn[T : LeafCommand] leaf() -> CommandParser[T] {
  { run: args => parse_leaf(args, Default::default()), }
}

///|
pub fn[T] father(subs : Array[(String, CommandParser[T])]) -> CommandParser[T] {
  {
    run: args => {
      if args.length() == 0 {
        raise NoSubcommandFoundError
      }
      let head = args[0]
      for pair in subs {
        let sub_name = pair.0
        let sub_parser = pair.1
        if head == sub_name {
          return (sub_parser.run)(args[1:])
        }
      } nobreak {
        raise NoSubcommandMatchError
      }
    },
  }
}

///|
pub fn unit() -> CommandParser[Unit] {
  { run: fn(_args) { return () }, }
}

///|
pub fn[Sub, T] CommandParser::map(
  self : CommandParser[Sub],
  builder : (Sub) -> T,
) -> CommandParser[T] {
  { run: args => builder((self.run)(args)), }
}

///|
pub fn[T] unit_map(builder : T) -> CommandParser[T] {
  { run: fn(_args) { builder }, }
}

///|
fn[T : LeafCommand] parse_leaf(
  argv : ArrayView[String],
  proto : T,
) -> T raise ParseError {
  guard ToJson::to_json(proto) is Object(proto) else {
    raise InvalidCommandError
  }
  for args = argv {
    match args {
      [x, .. xs] =>
        match proto.get(x) {
          Some(field) =>
            if field is (True | False) {
              proto[x] = Json::boolean(true)
              continue xs
            } else if field is String(_) {
              match xs {
                [y, .. ys] => {
                  proto[x] = Json::string(y)
                  continue ys
                }
                _ => raise MissingArgumentError(x)
              }
            } else {
              raise UnknownArgumentError(x)
            }
          None => {
            let positionals = proto
              .get("positionals")
              .unwrap_or_else(() => raise NoPositionalFieldError)
            guard positionals is Array(values) else {
              raise PositionalIsNotArrayError
            }
            values.push(Json::string(x))
            continue xs
          }
        }
      [] => break
    }
  }
  @json.from_json(ToJson::to_json(proto)) catch {
    e => raise DecodeError(e)
  }
}

///|
#cfg(not(target="js"))
pub fn get_cli_args() -> Array[String] {
  @env.args()
}

///|
#cfg(target="js")
pub fn get_cli_args() -> Array[String] {
  @env.args()[1:].to_array()
}