///|
fn upper(text : String) -> String {
  text[:].to_upper().to_owned()
}

///|
fn lower(text : String) -> String {
  text[:].to_lower().to_owned()
}

///|
fn is_digit(c : Char) -> Bool {
  c >= '0' && c <= '9'
}

///|
fn ends_in_dot(text : String) -> Bool {
  let chars = text.to_array()
  chars.length() > 0 && chars[chars.length() - 1] == '.'
}

///|
/// Convert a master-file owner or domain-valued RDATA to an absolute DNS name.
pub fn absolute_name(name : String, origin : String) -> String {
  let canonical_origin = if ends_in_dot(origin) {
    lower(origin)
  } else {
    lower(origin) + "."
  }
  if name == "@" {
    canonical_origin
  } else if name == "." {
    "."
  } else if ends_in_dot(name) {
    lower(name)
  } else {
    lower(name) + "." + canonical_origin
  }
}

///|
fn ttl_factor(c : Char) -> Int? {
  match c {
    's' | 'S' => Some(1)
    'm' | 'M' => Some(60)
    'h' | 'H' => Some(3600)
    'd' | 'D' => Some(86400)
    'w' | 'W' => Some(604800)
    _ => None
  }
}

///|
/// Parse a TTL written as seconds or a sequence such as 1w2d3h.
pub fn parse_ttl(input : String) -> Int? {
  let chars = input.to_array()
  if chars.length() == 0 {
    return None
  }
  let mut total = 0
  let mut group = 0
  let mut digits = false
  for c in chars {
    if is_digit(c) {
      let digit = c.to_int() - '0'.to_int()
      if group > (2147483647 - digit) / 10 {
        return None
      }
      group = group * 10 + digit
      digits = true
    } else {
      if !digits {
        return None
      }
      let factor = match ttl_factor(c) {
        Some(value) => value
        None => return None
      }
      if group > (2147483647 - total) / factor {
        return None
      }
      total += group * factor
      group = 0
      digits = false
    }
  }
  if digits {
    if group > 2147483647 - total {
      return None
    }
    total += group
  }
  Some(total)
}

///|
fn is_class(input : String) -> Bool {
  upper(input) == "IN" || upper(input) == "CH" || upper(input) == "HS"
}

///|
fn is_record_type(input : String) -> Bool {
  match upper(input) {
    "A"
    | "AAAA"
    | "NS"
    | "SOA"
    | "CNAME"
    | "MX"
    | "TXT"
    | "PTR"
    | "SRV"
    | "CAA"
    | "DS"
    | "DNSKEY"
    | "RRSIG"
    | "NSEC"
    | "NSEC3"
    | "TLSA"
    | "SSHFP"
    | "NAPTR"
    | "SPF"
    | "LOC" => true
    _ => false
  }
}

///|
fn add_parse_error(
  diagnostics : Array[Diagnostic],
  code : String,
  message : String,
  stmt : Statement,
) -> Unit {
  let column = if stmt.tokens.length() > 0 { stmt.tokens[0].column } else { 1 }
  diagnostics.push(diagnostic(code, "error", message, stmt.line, column))
}

///|
fn parse_directive(
  stmt : Statement,
  current_origin : String,
  default_ttl : Int,
  diagnostics : Array[Diagnostic],
) -> (String, Int) {
  let head = upper(stmt.tokens[0].text)
  match head {
    "$ORIGIN" =>
      if stmt.tokens.length() != 2 {
        add_parse_error(diagnostics, "Z010", "$ORIGIN requires one name", stmt)
        (current_origin, default_ttl)
      } else {
        (absolute_name(stmt.tokens[1].text, current_origin), default_ttl)
      }
    "$TTL" =>
      if stmt.tokens.length() != 2 {
        add_parse_error(diagnostics, "Z011", "$TTL requires one duration", stmt)
        (current_origin, default_ttl)
      } else {
        match parse_ttl(stmt.tokens[1].text) {
          Some(value) => (current_origin, value)
          None => {
            add_parse_error(diagnostics, "Z012", "invalid $TTL duration", stmt)
            (current_origin, default_ttl)
          }
        }
      }
    "$INCLUDE" | "$GENERATE" => {
      add_parse_error(
        diagnostics, "Z013", "directive is outside the supported offline subset",
        stmt,
      )
      (current_origin, default_ttl)
    }
    _ => {
      add_parse_error(
        diagnostics, "Z014", "unknown master-file directive", stmt,
      )
      (current_origin, default_ttl)
    }
  }
}

///|
fn parse_record(
  stmt : Statement,
  current_origin : String,
  previous_owner : String,
  default_ttl : Int,
  diagnostics : Array[Diagnostic],
) -> ResourceRecord? {
  let tokens = stmt.tokens
  let mut index = 0
  let owner = if stmt.indented {
    if previous_owner == "" {
      add_parse_error(
        diagnostics, "Z020", "inherited owner has no preceding record", stmt,
      )
      return None
    }
    previous_owner
  } else {
    index = 1
    absolute_name(tokens[0].text, current_origin)
  }
  let mut ttl = default_ttl
  let mut class_name = "IN"
  let mut seen_ttl = false
  let mut seen_class = false
  while index < tokens.length() && !is_record_type(tokens[index].text) {
    let token = tokens[index]
    if is_class(token.text) {
      if seen_class {
        diagnostics.push(
          diagnostic(
            "Z021",
            "error",
            "duplicate DNS class",
            token.line,
            token.column,
            owner~,
          ),
        )
      }
      class_name = upper(token.text)
      seen_class = true
    } else {
      match parse_ttl(token.text) {
        Some(value) => {
          if seen_ttl {
            diagnostics.push(
              diagnostic(
                "Z022",
                "error",
                "duplicate TTL",
                token.line,
                token.column,
                owner~,
              ),
            )
          }
          ttl = value
          seen_ttl = true
        }
        None => {
          diagnostics.push(
            diagnostic(
              "Z023",
              "error",
              "unsupported or missing record type",
              token.line,
              token.column,
              owner~,
            ),
          )
          return None
        }
      }
    }
    index += 1
  }
  if index >= tokens.length() {
    add_parse_error(diagnostics, "Z024", "resource record has no type", stmt)
    return None
  }
  let record_type = upper(tokens[index].text)
  index += 1
  let rdata : Array[String] = []
  while index < tokens.length() {
    rdata.push(tokens[index].text)
    index += 1
  }
  if rdata.length() == 0 {
    add_parse_error(diagnostics, "Z025", "resource record has no data", stmt)
  }
  Some({
    owner,
    origin: current_origin,
    ttl,
    class_name,
    record_type,
    rdata,
    line: stmt.line,
    column: tokens[0].column,
  })
}

///|
/// Parse the supported RFC 1035 zone-file subset without filesystem or network access.
pub fn parse_zone(input : String, origin : String) -> Zone {
  let lexical = lex_zone(input)
  let diagnostics = lexical.diagnostics
  let records : Array[ResourceRecord] = []
  let initial_origin = absolute_name("@", origin)
  let mut current_origin = initial_origin
  let mut previous_owner = ""
  let mut default_ttl = 3600
  for stmt in lexical.statements {
    if stmt.tokens.length() == 0 {
      continue
    }
    if stmt.tokens[0].text.to_array()[0] == '$' {
      let (next_origin, next_ttl) = parse_directive(
        stmt, current_origin, default_ttl, diagnostics,
      )
      current_origin = next_origin
      default_ttl = next_ttl
    } else {
      match
        parse_record(
          stmt, current_origin, previous_owner, default_ttl, diagnostics,
        ) {
        Some(record) => {
          previous_owner = record.owner
          records.push(record)
        }
        None => ()
      }
    }
  }
  { origin: initial_origin, records, diagnostics, }
}