///|
/// Represents an IPv4 address as a 32-bit unsigned integer.
/// See: https://url.spec.whatwg.org/#concept-ipv4
struct IPv4(UInt) derive(Debug)

///|
/// Serialize IPv4 address to dotted-decimal notation (e.g., "192.168.1.1").
/// See: https://url.spec.whatwg.org/#concept-ipv4-serializer
pub fn IPv4::to_string(self : IPv4) -> String {
  let n = self.0
  let b0 = (n >> 24) & 0xFF
  let b1 = (n >> 16) & 0xFF
  let b2 = (n >> 8) & 0xFF
  let b3 = n & 0xFF
  "\{b0}.\{b1}.\{b2}.\{b3}"
}

///|
/// Implement Show trait for IPv4, outputting dotted-decimal notation
pub impl Show for IPv4 with fn output(self : IPv4, logger : &Logger) -> Unit {
  logger.write_string(self.to_string())
}

///|
/// Implement ToJson trait for IPv4, outputting as array of 4 bytes
pub impl ToJson for IPv4 with fn to_json(self : IPv4) -> Json {
  let b0 = ((self.0 >> 24) & 0xFF).to_byte()
  let b1 = ((self.0 >> 16) & 0xFF).to_byte()
  let b2 = ((self.0 >> 8) & 0xFF).to_byte()
  let b3 = (self.0 & 0xFF).to_byte()
  [b0, b1, b2, b3].to_json()
}

///|
/// Parse an IPv4 address per WHATWG URL spec.
/// See: https://url.spec.whatwg.org/#concept-ipv4-parser
/// Supports decimal, octal (0-prefix), and hexadecimal (0x-prefix) notation.
///
/// The IPv4 parser takes an ASCII string input and then runs these steps:
/// 1. Let parts be the result of strictly splitting input on U+002E (.).
/// 2. If the last item in parts is the empty string, then:
///    2.1. IPv4-empty-part validation error.
///    2.2. If parts's size is greater than 1, then remove the last item from parts.
/// 3. If parts's size is greater than 4, IPv4-too-many-parts validation error,
///    return failure.
/// 4. Let numbers be an empty list.
/// 5. For each part of parts:
///    5.1. Let result be the result of parsing part.
///    5.2. If result is failure, IPv4-non-numeric-part validation error, return failure.
///    5.3. Append result to numbers.
/// 6. If any item in numbers is greater than 255, IPv4-out-of-range-part validation error.
/// 7. If the last item in numbers is greater than or equal to 256^(5 - numbers's size),
///    IPv4-out-of-range-part validation error, return failure.
/// 8. If any but the last item in numbers is greater than 255, then return failure.
/// 9. Let ipv4 be the last item in numbers.
/// 10. Remove the last item from numbers.
/// 11. Let counter be 0.
/// 12. For each n of numbers:
///     12.1. Increment ipv4 by n * 256^(3 - counter).
///     12.2. Increment counter by 1.
/// 13. Return ipv4.
pub fn IPv4::parse(input : StringView) -> IPv4 raise ValidationError {
  // 1. Let parts be the result of strictly splitting input on U+002E (.).
  let parts = input.split(".").collect()
  // 2. If the last item in parts is the empty string, remove it (if size > 1)
  let parts = if parts is [.. parts, ""] { parts } else { parts }
  // 3. If parts's size is greater than 4, return failure
  if parts.length() > 4 {
    raise IPv4TooManyParts
  }
  // 4-5. Parse each part as a number
  let numbers = []
  for part in parts {
    let number = IPv4::parse_number(part)
    numbers.push(number)
  }
  guard numbers is [.. numbers, last_number] else { abort("unreachable") }
  // 6, 8. If any but the last item in numbers is greater than 255, return failure
  for number in numbers {
    if number > 255 {
      raise IPv4OutOfRangePart
    }
  }
  // 7. If the last item in numbers is >= 256^(5 - numbers's size), return failure
  if last_number >= 1UL << (8 * (4 - numbers.length())) {
    raise IPv4OutOfRangePart
  }
  // 9-12. Calculate final IPv4 value
  let mut ipv4 = last_number.to_uint()
  let mut counter = 0
  for n in numbers {
    ipv4 = ipv4 + (n.to_uint() << (8 * (3 - counter)))
    counter = counter + 1
  }
  // 13. Return ipv4
  IPv4(ipv4)
}

///|
/// Parse an IPv4 number component per WHATWG URL spec.
/// See: https://url.spec.whatwg.org/#ipv4-number-parser
/// The IPv4 number parser takes an ASCII string input and then runs these steps:
/// 1. If input is the empty string, then return failure.
/// 2. Let validationError be false.
/// 3. Let R be 10.
/// 4. If input contains at least 2 code points and the first two code points
///    are either "0X" or "0x", then:
///    4.1. Set validationError to true.
///    4.2. Remove the first two code points from input.
///    4.3. Set R to 16.
/// 5. Otherwise, if input contains at least 2 code points and the first code
///    point is U+0030 (0), then:
///    5.1. Set validationError to true.
///    5.2. Remove the first code point from input.
///    5.3. Set R to 8.
/// 6. If input is the empty string, then return (0, true).
/// 7. If input contains a code point that is not a radix-R digit, then return failure.
/// 8. Let output be the mathematical integer value represented by input in radix-R.
/// 9. Return (output, validationError).
fn IPv4::parse_number(input : StringView) -> UInt64 raise ValidationError {
  // 1. If input is the empty string, return failure
  if input is "" {
    raise IPv4EmptyPart
  }
  match input {
    // 4. If input starts with "0x" or "0X", use radix 16 (hexadecimal)
    ['0', 'x' | 'X', .. rest] => {
      let mut value : UInt64 = 0
      for c in rest {
        let digit = match c {
          '0'..='9' => c.to_int() - '0'
          'a'..='f' => c.to_int() - 'a' + 10
          'A'..='F' => c.to_int() - 'A' + 10
          _ => raise IPv4NonNumericPart
        }
        guard digit < 16 else { raise IPv4NonNumericPart }
        value = value * 16 + digit.to_uint64()
      }
      value
    }
    // 5. If input starts with "0" followed by digits, use radix 8 (octal)
    ['0', '0'..='9' as c, .. rest] => {
      guard c >= '0' && c <= '7' else { raise IPv4NonNumericPart }
      let mut value : UInt64 = (c.to_int() - '0').to_uint64()
      for c in rest {
        guard c >= '0' && c <= '7' else { raise IPv4NonNumericPart }
        value = value * 8 + (c.to_int() - '0').to_uint64()
      }
      value
    }
    // 6. Single "0" is decimal 0
    ['0'] => 0UL
    // Default: radix 10 (decimal)
    ['1'..='9' as c, .. rest] => {
      let mut value : UInt64 = (c.to_int() - '0').to_uint64()
      for c in rest {
        guard c >= '0' && c <= '9' else { raise IPv4NonNumericPart }
        value = value * 10 + (c.to_int() - '0').to_uint64()
      }
      value
    }
    // 7. If input contains non-radix-R digit, return failure
    _ => raise IPv4NonNumericPart
  }
}

///|
test "IPv4::parse" {
  let inputs = ["127.0.0.1", "0xffffffff"]
  debug_inspect(
    inputs.map(input => {
      try IPv4::parse(input) catch {
        err => Err(err)
      } noraise {
        ipv4 => Ok(ipv4)
      }
    }),
    content=(
      #|[Ok(IPv4(2130706433)), Ok(IPv4(4294967295))]
    ),
  )
}

///|
test "IPv4::parse_number" {
  json_inspect(IPv4::parse_number("255"), content="255")
  json_inspect(IPv4::parse_number("0xff"), content="255")
  json_inspect(IPv4::parse_number("0377"), content="255")
  debug_inspect(
    try IPv4::parse_number("09") catch {
      err => Err(err)
    } noraise {
      number => Ok(number)
    },
    content=(
      #|Err(IPv4NonNumericPart)
    ),
  )
}