// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

// Host parsing, as the URL Standard spells it (§3.5): a domain name run
// through IDNA, an IPv4 address in any of the shapes a number may take, an
// IPv6 address in brackets, or — for a scheme that is not special — an opaque
// string.

///|
/// Parse a host (§3.5), returning it the way [`Url::encode`] writes it: an
/// IPv6 address without its brackets, everything else as it stands.
///
/// `special` says whether the scheme is one of the six the standard gives its
/// own rules: a host under any other scheme is any string without a forbidden
/// code point, not a registered name.
fn host(
  text : StringView,
  special~ : Bool,
  at~ : Int32,
) -> String raise Refused {
  let n = text.length()
  if n > 0 && text[0].to_int() == 0x5B {
    guard n > 1 && text[n - 1].to_int() == 0x5D else { raise Host(at~) }
    return ipv6(text[1:n - 1], at~)
  }
  if !special {
    return opaque_host(text, at~)
  }
  // A host may be written in escapes; the name is what they stand for.
  let name = domain_parser(unescape_text(text)[:], strict=false, at~)
  if ends_in_number(name[:]) {
    return ipv4(name[:], at~)
  }
  name
}

///|
/// The domain parser of §3.3: the host name a browser would make of `text`.
///
/// Strict is for a name that has to be one: `CheckHyphens`, the STD 3 rules
/// and the DNS lengths are all on, and invalid Punycode is refused.
///
/// Lenient is the browser: an ASCII string is taken as it stands, lowercased,
/// whatever IDNA would say about the label — a name that reads as Punycode but
/// is not still addresses something. Everything else goes through UTS 46.
///
/// ```mbt check
/// test {
///   assert_eq(@moonurl.domain("MÜNCHEN.example"), "xn--mnchen-3ya.example")
///   assert_eq(@moonurl.domain("xn--a"), "xn--a")
/// }
/// ```
pub fn domain(
  text : StringView,
  strict~ : Bool = false,
) -> String raise Refused {
  domain_parser(text, strict~, at=0)
}

///|
fn domain_parser(
  text : StringView,
  strict~ : Bool,
  at~ : Int32,
) -> String raise Refused {
  if strict {
    let outcome = @idna.to_ascii(text, options=@idna.strict)
    guard outcome.is_ok() else { raise Name(at~) }
    return outcome.name
  }
  let name = if ascii(text) {
    downcase(text)
  } else {
    // A browser keeps a label it cannot read; the standard's own profile
    // refuses one it cannot decode, which is what this is.
    let options = { ..@idna.options, ignore_invalid_punycode: false, }
    let outcome = @idna.to_ascii(text, options~)
    guard outcome.is_ok() else { raise Name(at~) }
    outcome.name
  }
  guard name.length() > 0 else { raise Name(at~) }
  for character in name {
    guard !forbidden_domain(character) else { raise Name(at~) }
  }
  name
}

///|
/// `text` with its ASCII letters lowered, which is all a domain name needs:
/// everything else is IDNA's business.
fn downcase(text : StringView) -> String {
  let out = StringBuilder()
  for character in text {
    let point = character.to_int()
    if point >= 0x41 && point <= 0x5A {
      out.write_char((point + 32).unsafe_to_char())
    } else {
      out.write_char(character)
    }
  }
  out.to_string()
}

///|
/// Whether `text` is ASCII, which decides whether a name takes the browser's
/// short way through the domain parser.
fn ascii(text : StringView) -> Bool {
  for character in text {
    if character.to_int() >= 0x80 {
      return false
    }
  }
  true
}

// ---------------------------------------------------------------------- IPv4

///|
/// Parse an IPv4 address (§3.5): one to four parts, each in decimal, octal or
/// hexadecimal, the last one filling whatever the others left over.
///
/// The arithmetic is 64-bit wide: an address is a number that does not fit in
/// 32 bits once it is signed, and `0xFFFFFFFF` is a perfectly good host.
fn ipv4(text : StringView, at~ : Int32) -> String raise Refused {
  let parts = split_dots(text)
  if parts.length() > 0 && parts[parts.length() - 1].length() == 0 {
    if parts.length() > 1 {
      let _ = parts.pop()
    }
  }
  guard parts.length() <= 4 else { raise Address(at~) }
  let numbers : Array[Int64] = []
  for part in parts {
    let (number, _) = ipv4_number(part)
    numbers.push(number)
  }
  let last = numbers.length() - 1
  let octet : Int64 = 255
  for i, number in numbers {
    guard i == last || number <= octet else { raise Address(at~) }
  }
  // The last part fills what the others leave: 127.1 is 127.0.0.1.
  let mut address = numbers[last]
  let one : Int64 = 1
  guard address < one << (8 * (5 - numbers.length())) else {
    raise Address(at~)
  }
  let mut shift = 3
  for i = 0; i < last; i = i + 1 {
    address += numbers[i] << (8 * shift)
    shift -= 1
  }
  let out = StringBuilder()
  let byte : Int64 = 0xFF
  for i = 3; i >= 0; i = i - 1 {
    if i < 3 {
      out.write_char('.')
    }
    out.write_string(((address >> (8 * i)) & byte).to_string())
  }
  out.to_string()
}

///|
/// One part of an IPv4 address as a number, in the radix its prefix names:
/// `0x` hexadecimal, a leading `0` octal, decimal otherwise.
fn ipv4_number(text : StringView) -> (Int64, Bool) raise Refused {
  guard text.length() > 0 else { raise Address(at=0) }
  let mut body = text
  let mut radix : Int64 = 10
  let mut octal = false
  if text.length() >= 2 &&
    text[0].to_int() == 0x30 &&
    (text[1].to_int() == 0x78 || text[1].to_int() == 0x58) {
    body = text[2:]
    radix = 16
    octal = true
  } else if text.length() >= 2 && text[0].to_int() == 0x30 {
    body = text[1:]
    radix = 8
    octal = true
  }
  if body.length() == 0 {
    return (0, true)
  }
  // A part may be a number no address can hold. That is not a parse failure:
  // it is the range check below that refuses it.
  let ceiling : Int64 = 0x20000000000
  let mut value : Int64 = 0
  for character in body {
    let written = match character {
      '0'..<='9' => character.to_int() - 0x30
      'a'..<='f' => character.to_int() - 0x57
      'A'..<='F' => character.to_int() - 0x37
      _ => -1
    }
    guard written >= 0 else { raise Address(at=0) }
    let digit = written.to_int64()
    guard digit < radix else { raise Address(at=0) }
    // Past the ceiling the value only has to stay too big to be an address;
    // stopping it there keeps it from overflowing on a long run of digits.
    value = if value > ceiling / radix {
      ceiling
    } else {
      value * radix + digit
    }
  }
  (value, octal)
}

// ---------------------------------------------------------------------- IPv6

///|
/// Parse an IPv6 address (§3.5), the brackets already taken off, and write it
/// back in the compressed form of RFC 5952.
fn ipv6(text : StringView, at~ : Int32) -> String raise Refused {
  let input : Array[Char] = []
  for character in text {
    input.push(character)
  }
  let n = input.length()
  let pieces : Array[Int32] = Array::make(8, 0)
  let mut pointer = 0
  let mut piece = 0
  let mut compress = -1
  if code(input, 0) == Some(':') {
    guard code(input, 1) == Some(':') else { raise Address(at~) }
    pointer = 2
    piece = 1
    compress = 1
  }
  while pointer < n {
    guard piece < 8 else { raise Address(at~) }
    if code(input, pointer) == Some(':') {
      guard compress < 0 else { raise Address(at~) }
      pointer += 1
      piece += 1
      compress = piece
      continue
    }
    let mut value = 0
    let mut length = 0
    while length < 4 {
      match code(input, pointer) {
        Some(character) if hex(character) => {
          value = value * 0x10 + hex_value(character)
          pointer += 1
          length += 1
        }
        _ => break
      }
    }
    if code(input, pointer) == Some('.') {
      // The tail is an IPv4 address, written two bytes at a time.
      guard length > 0 && piece <= 6 else { raise Address(at~) }
      pointer -= length
      let mut seen = 0
      while pointer < n {
        if seen > 0 {
          guard code(input, pointer) == Some('.') && seen < 4 else {
            raise Address(at~)
          }
          pointer += 1
        }
        let mut octet = -1
        let mut digits = 0
        while pointer < n {
          match code(input, pointer) {
            Some(character) if character >= '0' && character <= '9' => {
              let number = character.to_int() - 0x30
              guard !(digits == 1 && octet == 0) else { raise Address(at~) }
              octet = if octet < 0 { number } else { octet * 10 + number }
              guard octet <= 255 else { raise Address(at~) }
              pointer += 1
              digits += 1
            }
            _ => break
          }
        }
        guard digits > 0 else { raise Address(at~) }
        pieces[piece] = pieces[piece] * 0x100 + octet
        seen += 1
        if seen == 2 || seen == 4 {
          piece += 1
        }
      }
      guard seen == 4 else { raise Address(at~) }
      break
    } else if code(input, pointer) == Some(':') {
      pointer += 1
      guard pointer < n else { raise Address(at~) }
    } else if pointer < n {
      raise Address(at~)
    }
    pieces[piece] = value
    piece += 1
  }
  if compress >= 0 {
    // Move the pieces after the gap to the end of the address.
    let mut swaps = piece - compress
    let mut target = 7
    while target > 0 && swaps > 0 {
      let temporary = pieces[target]
      pieces[target] = pieces[compress + swaps - 1]
      pieces[compress + swaps - 1] = temporary
      target -= 1
      swaps -= 1
    }
  } else {
    guard piece == 8 else { raise Address(at~) }
  }
  ipv6_text(pieces)
}

///|
/// An IPv6 address written the way RFC 5952 asks: the longest run of zero
/// pieces, of two or more, written `::`.
fn ipv6_text(pieces : Array[Int32]) -> String {
  let mut best = -1
  let mut best_length = 1
  let mut run = -1
  let mut length = 0
  for i, piece in pieces {
    if piece == 0 {
      if run < 0 {
        run = i
        length = 0
      }
      length += 1
      if length > best_length {
        best = run
        best_length = length
      }
    } else {
      run = -1
      length = 0
    }
  }
  let out = StringBuilder()
  let mut i = 0
  while i < 8 {
    if i == best {
      out.write_string("::")
      i += best_length
      continue
    }
    if i > 0 && i != best + best_length {
      out.write_char(':')
    }
    out.write_string(pieces[i].to_string(radix=16))
    i += 1
  }
  out.to_string()
}

///|
fn hex(character : Char) -> Bool {
  hex_value(character) >= 0
}

///|
fn hex_value(character : Char) -> Int32 {
  match character {
    '0'..<='9' => character.to_int() - 0x30
    'a'..<='f' => character.to_int() - 0x57
    'A'..<='F' => character.to_int() - 0x37
    _ => -1
  }
}

// -------------------------------------------------------------- opaque hosts

///|
/// An opaque host (§3.5): any string without a forbidden host code point,
/// percent-encoded for what no URI may carry.
fn opaque_host(text : StringView, at~ : Int32) -> String raise Refused {
  for character in text {
    guard !forbidden_host(character) else { raise Name(at~) }
  }
  let out = StringBuilder()
  escape_each(out, text, C0Control)
  out.to_string()
}

// ------------------------------------------------------------------ predicates

///|
/// The code points no host may contain (§3.2).
fn forbidden_host(character : Char) -> Bool {
  match character {
    '\u{0}'
    | '\t'
    | '\n'
    | '\r'
    | ' '
    | '#'
    | '/'
    | ':'
    | '<'
    | '>'
    | '?'
    | '@'
    | '['
    | '\\'
    | ']'
    | '^'
    | '|' => true
    _ => false
  }
}

///|
/// What a *domain* may not contain: the above, and the C0 controls, `%` and
/// DEL, which a registered name has no business carrying either.
fn forbidden_domain(character : Char) -> Bool {
  forbidden_host(character) ||
  character.to_int() < 0x20 ||
  character == '%' ||
  character == '\u{7F}'
}

// ------------------------------------------------------------------ numbers

///|
/// Whether a domain name ends in something that reads as an IPv4 address
/// number, which is what sends it to the IPv4 parser (§3.5).
fn ends_in_number(text : StringView) -> Bool {
  let parts = split_dots(text)
  if parts.length() > 0 && parts[parts.length() - 1].length() == 0 {
    let _ = parts.pop()
  }
  guard parts.length() > 0 else { return false }
  let last = parts[parts.length() - 1]
  guard last.length() > 0 else { return false }
  if digits_only(last) {
    return true
  }
  // `0x`, or `0x` and hexadecimal digits, is a number with nothing in it yet.
  try {
    ipv4_number(last) |> ignore
    true
  } catch {
    _ => false
  }
}

///|
fn digits_only(text : StringView) -> Bool {
  guard text.length() > 0 else { return false }
  for character in text {
    guard character >= '0' && character <= '9' else { return false }
  }
  true
}

///|
fn split_dots(text : StringView) -> Array[StringView] {
  let parts : Array[StringView] = []
  let mut start = 0
  for i = 0; i <= text.length(); i = i + 1 {
    if i == text.length() || text[i].to_int() == 0x2E {
      parts.push(text[start:i])
      start = i + 1
    }
  }
  parts
}