///|
pub fn valid_ipv4(text : String) -> Bool {
  let parts = split_once_slash(text)
  if !valid_dotted_ipv4(parts.0) {
    return false
  }
  match parts.1 {
    None => true
    Some(prefix) => valid_int_range(prefix, 0, 32)
  }
}

///|
pub fn valid_ipv6(text : String) -> Bool {
  let parts = split_once_slash(text)
  if !valid_raw_ipv6(parts.0) {
    return false
  }
  match parts.1 {
    None => true
    Some(prefix) => valid_int_range(prefix, 0, 128)
  }
}

///|
pub fn valid_mac(text : String) -> Bool {
  if text.length() != 17 {
    return false
  }
  for i = 0; i < 17; i = i + 1 {
    match char_at(text, i) {
      None => return false
      Some(ch) =>
        if i % 3 == 2 {
          if ch != ':' && ch != '-' {
            return false
          }
        } else if !ch.is_ascii_hexdigit() {
          return false
        }
    }
  }
  true
}

///|
pub fn valid_email(text : String) -> Bool {
  match text.find("@") {
    None => false
    Some(idx) =>
      if idx == 0 || idx == text.length() - 1 {
        false
      } else {
        match find_from(text, ".", idx + 1) {
          None => false
          Some(dot) => dot > idx + 1 && dot < text.length() - 1
        }
      }
  }
}

///|
pub fn valid_url(text : String) -> Bool {
  match text.find("://") {
    None => false
    Some(idx) => idx >= 2 && idx + 3 < text.length()
  }
}

///|
pub fn valid_hex_binary(text : String) -> Bool {
  if text.length() == 0 || text.length() % 2 != 0 {
    return false
  }
  for i = 0; i < text.length(); i = i + 1 {
    match char_at(text, i) {
      Some(ch) => if !ch.is_ascii_hexdigit() { return false }
      None => return false
    }
  }
  true
}

///|
fn split_once_slash(text : String) -> (String, String?) {
  match text.find("/") {
    None => (text, None)
    Some(idx) => (slice_to(text, 0, idx), Some(slice_from(text, idx + 1)))
  }
}

///|
fn valid_dotted_ipv4(text : String) -> Bool {
  let mut start = 0
  let mut seen = 0
  for i = 0; i <= text.length(); i = i + 1 {
    if i == text.length() || char_eq(text, i, '.') {
      if i == start {
        return false
      }
      let part = slice_to(text, start, i)
      if !valid_int_range(part, 0, 255) {
        return false
      }
      seen += 1
      start = i + 1
    }
  }
  seen == 4
}

///|
fn valid_raw_ipv6(text : String) -> Bool {
  if text.length() == 0 {
    return false
  }
  match text.find("::") {
    None =>
      match ipv6_group_count(text) {
        Some(count) => count == 8
        None => false
      }
    Some(idx) =>
      match find_from(text, "::", idx + 2) {
        Some(_) => false
        None => {
          let left = slice_to(text, 0, idx)
          let right = slice_from(text, idx + 2)
          match (ipv6_group_count(left), ipv6_group_count(right)) {
            (Some(left_count), Some(right_count)) =>
              left_count + right_count <= 7
            _ => false
          }
        }
      }
  }
}

///|
fn ipv6_group_count(text : String) -> Int? {
  if text.length() == 0 {
    return Some(0)
  }
  let mut start = 0
  let mut count = 0
  for i = 0; i <= text.length(); i = i + 1 {
    if i == text.length() || char_eq(text, i, ':') {
      if i == start {
        return None
      }
      let part = slice_to(text, start, i)
      if part.length() == 0 || part.length() > 4 {
        return None
      }
      for p = 0; p < part.length(); p = p + 1 {
        match char_at(part, p) {
          Some(ch) => if !ch.is_ascii_hexdigit() { return None }
          None => return None
        }
      }
      count += 1
      start = i + 1
    }
  }
  Some(count)
}

///|
fn valid_int_range(text : String, min : Int, max : Int) -> Bool {
  if text.length() == 0 {
    return false
  }
  let mut value = 0
  for i = 0; i < text.length(); i = i + 1 {
    match char_at(text, i) {
      Some(ch) => {
        if !ch.is_ascii_digit() {
          return false
        }
        value = value * 10 + (ch.to_int() - 48)
        if value > max {
          return false
        }
      }
      None => return false
    }
  }
  value >= min && value <= max
}