///|
pub struct CliConfig {
  start : @lib.Date
  rule : @lib.DateRule
  exclude_start : Bool
  calendar : String
  extension : @lib.ExtensionPolicy
  explain : Bool
  format : OutputFormat
}

///|
pub enum OutputFormat {
  Text
  Table
} derive(Debug, Eq, Compare)

///|
pub enum CliError {
  MissingOption(String)
  InvalidOption(String)
  InvalidRule(String)
  InvalidNumber(String)
  InvalidDate(@lib.DateError)
} derive(Debug)

///|
fn parse_nonnegative_int(text : String) -> Result[Int, CliError] {
  if text.length() == 0 {
    return Err(InvalidNumber(text))
  }
  let mut value = 0
  let mut index = 0
  while index < text.length() {
    let code = text.at(index).to_int()
    if code < 48 || code > 57 {
      return Err(InvalidNumber(text))
    }
    value = value * 10 + code - 48
    index = index + 1
  }
  Ok(value)
}

///|
fn parse_one_rule(text : String) -> Result[@lib.DateRule, CliError] {
  let parts = [ for part in text.split(":") => part.to_owned() ]
  if parts.length() != 2 {
    return Err(InvalidRule(text))
  }
  let count = match parse_nonnegative_int(parts[1]) {
    Ok(value) => value
    Err(_) => return Err(InvalidRule(text))
  }
  match parts[0] {
    "calendar-days" => Ok(@lib.DateRule::calendar_days(count))
    "business-days" => Ok(@lib.DateRule::business_days(count))
    "weeks" => Ok(@lib.DateRule::weeks(count))
    "months" => Ok(@lib.DateRule::months(count))
    "years" => Ok(@lib.DateRule::years(count))
    _ => Err(InvalidRule(text))
  }
}

///|
pub fn parse_rule_spec(text : String) -> Result[@lib.DateRule, CliError] {
  let parts = [ for part in text.split(",") => part.to_owned() ]
  if parts.length() == 0 {
    return Err(InvalidRule(text))
  }
  if parts.length() == 1 {
    return parse_one_rule(parts[0])
  }
  let rules = Array::new()
  for part in parts {
    match parse_one_rule(part) {
      Ok(rule) => rules.push(rule)
      Err(_) => return Err(InvalidRule(part))
    }
  }
  Ok(@lib.DateRule::sequence(rules))
}

///|
fn parse_extension(text : String) -> Result[@lib.ExtensionPolicy, CliError] {
  match text {
    "none" => Ok(@lib.ExtensionPolicy::no_extension())
    "following" => Ok(@lib.ExtensionPolicy::following())
    "modified-following" => Ok(@lib.ExtensionPolicy::modified_following())
    "preceding" => Ok(@lib.ExtensionPolicy::preceding())
    _ => Err(InvalidOption("--extension=" + text))
  }
}

///|
pub fn parse_cli_arguments(args : Array[String]) -> Result[CliConfig, CliError] {
  let mut index = 0
  if args.length() > 0 && !args[0].has_prefix("--") {
    index = 1
  }
  let mut start_text = ""
  let mut rule_text = ""
  let mut calendar = "standard"
  let mut extension_text = "none"
  let mut exclude_start = false
  let mut explain = false
  let mut format = Text
  while index < args.length() {
    match args[index] {
      "--start" => {
        index = index + 1
        if index >= args.length() {
          return Err(MissingOption("--start"))
        }
        start_text = args[index]
      }
      "--rule" => {
        index = index + 1
        if index >= args.length() {
          return Err(MissingOption("--rule"))
        }
        rule_text = args[index]
      }
      "--calendar" => {
        index = index + 1
        if index >= args.length() {
          return Err(MissingOption("--calendar"))
        }
        calendar = args[index]
      }
      "--extension" => {
        index = index + 1
        if index >= args.length() {
          return Err(MissingOption("--extension"))
        }
        extension_text = args[index]
      }
      "--exclude-start" => exclude_start = true
      "--explain" => explain = true
      "--format" => {
        index = index + 1
        if index >= args.length() {
          return Err(MissingOption("--format"))
        }
        format = match args[index] {
          "text" => Text
          "table" => Table
          value => return Err(InvalidOption("--format=" + value))
        }
      }
      "--help" => return Err(InvalidOption("help"))
      option => return Err(InvalidOption(option))
    }
    index = index + 1
  }
  if start_text == "" {
    return Err(MissingOption("--start"))
  }
  if rule_text == "" {
    return Err(MissingOption("--rule"))
  }
  let start = match @lib.Date::parse(start_text) {
    Ok(date) => date
    Err(error) => return Err(InvalidDate(error))
  }
  let rule = match parse_rule_spec(rule_text) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let extension = match parse_extension(extension_text) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  Ok({ start, rule, exclude_start, calendar, extension, explain, format })
}

///|
fn cli_error_message(error : CliError) -> String {
  match error {
    MissingOption(name) => "missing required option: " + name
    InvalidOption(name) => "invalid option or value: " + name
    InvalidRule(rule) => "invalid rule: " + rule
    InvalidNumber(value) => "invalid non-negative number: " + value
    InvalidDate(_) => "invalid date; expected YYYY-MM-DD"
  }
}

///|
fn calendar_for(name : String) -> Result[@lib.Calendar, CliError] {
  match name {
    "standard" => Ok(@lib.Calendar::standard())
    "china-2026" => Ok(@lib.Calendar::china_2026())
    _ => Err(InvalidOption("--calendar=" + name))
  }
}

///|
pub fn execute(args : Array[String]) -> Result[String, CliError] {
  let config = match parse_cli_arguments(args) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let calendar = match calendar_for(config.calendar) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let start_rule = if config.exclude_start {
    @lib.StartDateRule::exclude()
  } else {
    @lib.StartDateRule::including_start()
  }
  let deadline = @lib.Deadline::new(
    start_date=config.start,
    rule=config.rule,
    start_rule~,
    calendar~,
    extension=config.extension,
  )
  match deadline.calculate() {
    Ok(result) =>
      match config.format {
        Table => Ok(@lib.AuditReport::from_result(result, calendar).to_table())
        Text =>
          if config.explain {
            Ok(@lib.AuditReport::from_result(result, calendar).to_text())
          } else {
            Ok(
              result.original_date().to_string() +
              " -> " +
              result.adjusted_date().to_string(),
            )
          }
      }
    Err(_) => Err(InvalidOption("deadline calculation failed"))
  }
}