// Copyright (c) 2026 colmugx
// SPDX-License-Identifier: Apache-2.0

///|
/// Derived property of one code point (RFC 5892 3) as far as an already mapped,
/// NFC-normalised label needs it. `Unassigned` is folded into `Disallowed`
/// because neither value may appear in a label.
priv enum FormatIdnaProperty {
  Pvalid
  ContextJ
  ContextO
  Disallowed
}

///|
/// IDNA2008 validity of one label, applied on top of UTS #46 processing. The
/// UTS #46 mapping table behind `@idna` marks punctuation, symbols and the
/// RFC 5892 2.6 exceptions valid, so a ToASCII-only pipeline accepts labels
/// that IDNA2008 forbids; only the derived property and the Appendix A
/// contextual rules can reject them.
fn format_idna2008_label(label : StringView) -> Bool {
  if label.is_empty() {
    return false
  }
  let chars = label.to_array()
  let mut arabic_indic = false
  let mut extended_arabic_indic = false
  let mut kana_or_han = false
  for index in 0.. return false
      ContextJ | ContextO | Pvalid => ()
    }
    if ch is ('\u{0660}'..<='\u{0669}') {
      arabic_indic = true
    }
    if ch is ('\u{06F0}'..<='\u{06F9}') {
      extended_arabic_indic = true
    }
    if format_idna_kana_or_han(ch) {
      kana_or_han = true
    }
  }
  // RFC 5892 Appendix A.8 / A.9: the two digit blocks must not be mixed.
  if arabic_indic && extended_arabic_indic {
    return false
  }
  for index in 0.. FormatIdnaProperty {
  if format_idna_exception(ch) is Some(property) {
    return property
  }
  // 2.5 LDH.
  if ch is ('-' | '0'..<='9' | 'a'..<='z') {
    return Pvalid
  }
  // 2.8 JoinControl. `@idna` has already applied the Appendix A.1/A.2 rules.
  if ch is ('\u{200C}' | '\u{200D}') {
    return ContextJ
  }
  // 2.3 IgnorableProperties.
  if @ucd.is_default_ignorable_code_point(ch) ||
    @ucd.is_white_space(ch) ||
    format_is_noncharacter(ch) {
    return Disallowed
  }
  // 2.4 IgnorableBlocks: Combining Diacritical Marks for Symbols, Musical
  // Symbols, Ancient Greek Musical Notation.
  if ch
    is ('\u{20D0}'..<='\u{20FF}'
    | '\u{1D100}'..<='\u{1D1FF}'
    | '\u{1D200}'..<='\u{1D24F}') {
    return Disallowed
  }
  // 2.9 OldHangulJamo.
  if @ucd.hangul_syllable_type(ch) is (LeadingJamo | VowelJamo | TrailingJamo) {
    return Disallowed
  }
  // 2.1 LetterDigits.
  if @ucd.general_category(ch) is (Ll | Lu | Lo | Nd | Lm | Mn | Mc) {
    return Pvalid
  }
  Disallowed
}

///|
/// RFC 5892 2.6 Exceptions: the code points whose derived property cannot be
/// computed from the core property values, so the table decides.
fn format_idna_exception(ch : Char) -> FormatIdnaProperty? {
  match ch.to_int() {
    0x00DF | 0x03C2 | 0x06FD | 0x06FE | 0x0F0B | 0x3007 => Some(Pvalid)
    0x00B7
    | 0x0375
    | 0x05F3
    | 0x05F4
    | 0x0660..<=0x0669
    | 0x06F0..<=0x06F9
    | 0x30FB => Some(ContextO)
    0x0640 | 0x07FA | 0x302E | 0x302F | 0x3031..<=0x3035 | 0x303B =>
      Some(Disallowed)
    _ => None
  }
}

///|
/// RFC 5892 Appendix A.3-A.7 contextual rules. The Arabic-Indic digit rules
/// (A.8/A.9) are decided once per label, not per position.
fn format_idna_context(
  chars : Array[Char],
  index : Int32,
  kana_or_han~ : Bool,
) -> Bool {
  let ch = chars[index]
  if ch is '\u{00B7}' {
    // A.3: MIDDLE DOT only between two 'l'.
    return index > 0 &&
      index + 1 < chars.length() &&
      chars[index - 1] == 'l' &&
      chars[index + 1] == 'l'
  }
  if ch is '\u{0375}' {
    // A.4: GREEK KERAIA must be followed by Greek.
    return index + 1 < chars.length() && format_idna_greek(chars[index + 1])
  }
  if ch is ('\u{05F3}' | '\u{05F4}') {
    // A.5/A.6: GERESH and GERSHAYIM must be preceded by Hebrew.
    return index > 0 && format_idna_hebrew(chars[index - 1])
  }
  if ch is '\u{30FB}' {
    // A.7: KATAKANA MIDDLE DOT requires Hiragana, Katakana or Han in the label.
    return kana_or_han
  }
  // A.8/A.9: nothing positional left, the digit-block mixing rule decides.
  true
}

///|
/// `Script(cp).eq.Greek` approximated by the Greek and Coptic plus Greek
/// Extended blocks, which is what the rule needs to recognise.
fn format_idna_greek(ch : Char) -> Bool {
  ch is ('\u{0370}'..<='\u{03FF}' | '\u{1F00}'..<='\u{1FFF}')
}

///|
/// `Script(cp).eq.Hebrew` approximated by the Hebrew block plus the Hebrew
/// presentation forms.
fn format_idna_hebrew(ch : Char) -> Bool {
  ch is ('\u{0590}'..<='\u{05FF}' | '\u{FB1D}'..<='\u{FB4F}')
}

///|
/// `Script(cp).in.{Hiragana, Katakana, Han}` approximated by the Hiragana,
/// Katakana, Katakana Phonetic Extensions, halfwidth Katakana, CJK Unified
/// Ideographs (all three blocks) and CJK Compatibility Ideographs blocks.
/// U+30FB KATAKANA MIDDLE DOT is excluded because its own Script is Common,
/// which is exactly what the A.7 rule relies on.
fn format_idna_kana_or_han(ch : Char) -> Bool {
  ch
  is ('\u{3040}'..<='\u{309F}'
  | '\u{30A0}'..<='\u{30FA}'
  | '\u{30FC}'..<='\u{30FF}'
  | '\u{31F0}'..<='\u{31FF}'
  | '\u{3400}'..<='\u{4DBF}'
  | '\u{4E00}'..<='\u{9FFF}'
  | '\u{F900}'..<='\u{FAFF}'
  | '\u{FF66}'..<='\u{FF9D}'
  | '\u{20000}'..<='\u{2FA1F}')
}

///|
/// `Noncharacter_Code_Point`, which RFC 5892 2.3 adds to IgnorableProperties.
fn format_is_noncharacter(ch : Char) -> Bool {
  let code = ch.to_int()
  (code >= 0xFDD0 && code <= 0xFDEF) || (code & 0xFFFE) == 0xFFFE
}