///|
pub fn parse(text : String) -> Robots {
  let groups : Array[Group] = []
  let sitemaps : Array[String] = []
  let hosts : Array[String] = []
  let mut agents : Array[String] = []
  let mut rules : Array[Rule] = []
  let mut delay : Int? = None
  let mut saw_rule = false

  fn flush() {
    if !agents.is_empty() || !rules.is_empty() || delay is Some(_) {
      groups.push({ agents, rules, crawl_delay: delay })
    }
    agents = []
    rules = []
    delay = None
    saw_rule = false
  }

  for raw in split_lines(text) {
    let line = trim_ascii(strip_inline_comment(raw))
    if line == "" {
      flush()
      continue
    }
    let parts = split_once(line, ":")
    if parts.length() < 2 {
      continue
    }
    let key = lower_ascii(trim_ascii(parts[0]))
    let value = trim_ascii(parts[1])
    if key == "user-agent" {
      if saw_rule {
        flush()
      }
      agents.push(lower_ascii(value))
    } else if key == "allow" {
      saw_rule = true
      rules.push(allow_rule(normalize_path(value)))
    } else if key == "disallow" {
      saw_rule = true
      if value != "" {
        rules.push(disallow_rule(normalize_path(value)))
      }
    } else if key == "crawl-delay" {
      saw_rule = true
      delay = parse_non_negative_int(value)
    } else if key == "sitemap" {
      sitemaps.push(value)
    } else if key == "host" {
      hosts.push(value)
    }
  }
  flush()
  { groups, sitemaps, hosts }
}

///|
pub fn parse_report(text : String) -> String {
  let robots = parse(text)
  "groups=" +
  robots.groups.length().to_string() +
  "\nsitemaps=" +
  robots.sitemaps.length().to_string() +
  "\nhosts=" +
  robots.hosts.length().to_string()
}

///|
pub fn parse_errors(text : String) -> Array[String] {
  let errors : Array[String] = []
  let mut line_no = 0
  for raw in split_lines(text) {
    line_no += 1
    let line = trim_ascii(strip_inline_comment(raw))
    if line == "" {
      continue
    }
    let parts = split_once(line, ":")
    if parts.length() < 2 {
      errors.push("line " + line_no.to_string() + ": missing ':'")
      continue
    }
    let key = lower_ascii(trim_ascii(parts[0]))
    let value = trim_ascii(parts[1])
    if key == "crawl-delay" && parse_non_negative_int(value) is None {
      errors.push("line " + line_no.to_string() + ": invalid crawl-delay")
    } else if key == "user-agent" && value == "" {
      errors.push("line " + line_no.to_string() + ": empty user-agent")
    }
  }
  errors
}

///|
pub fn validation_report(text : String) -> String {
  let errors = parse_errors(text)
  if errors.is_empty() {
    "ok"
  } else {
    join_lines(errors)
  }
}