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

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

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

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

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

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

///|
pub fn father[T](subs : Array[(String, CommandParser[T])]) -> CommandParser[T] {
  {
    run: fn(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:])
        }
      } else {
        raise NoSubcommandMatchError
      }
    },
  }
}

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

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

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

///|
fn parse_leaf[T : LeafCommand](
  argv : ArrayView[String],
  proto : T
) -> T!ParseError {
  let proto = proto.to_json().as_object().or_error!(InvalidCommandError)
  loop argv[:] {
    [x, .. xs] =>
      match proto[x] {
        Some(field) =>
          if field.as_bool() is Some(_) {
            proto[x] = Json::boolean(true)
            continue xs
          } else if field.as_string() is Some(_) {
            match xs {
              [y, .. ys] => {
                proto[x] = Json::string(y)
                continue ys
              }
              _ => raise MissingArgumentError(x)
            }
          } else {
            raise UnknownArgumentError(x)
          }
        None => {
          let positionals = proto["positionals"].or_error!(
            NoPositionalFieldError,
          )
          positionals
          .as_array()
          .or_error!(PositionalIsNotArrayError)
          .push(Json::string(x))
          continue xs
        }
      }
    [] => break
  }
  try {
    @json.from_json!(proto.to_json())
  } catch {
    e => raise DecodeError(e)
  }
}

///|
pub fn get_cli_args() -> Array[String] {
  @sys.get_cli_args()
}