///|
/// Punycode encoding/decoding (RFC 3492)
/// Used for Internationalized Domain Names in Applications (IDNA)

///|
/// Error type for Punycode operations
pub(all) suberror PunycodeError {
  Overflow
  InvalidInput
  BadInput
}

///|
pub impl Show for PunycodeError with fn output(self, logger) {
  match self {
    Overflow => logger.write_string("Overflow")
    InvalidInput => logger.write_string("InvalidInput")
    BadInput => logger.write_string("BadInput")
  }
}

// RFC 3492 constants (Section 4)

///|
/// Base for variable-length integer encoding. Punycode uses base-36
/// (letters a-z represent 0-25, digits 0-9 represent 26-35).
const Base : Int = 36

///|
/// Minimum threshold for variable-length integer encoding.
/// Ensures at least one digit is always output.
const Tmin : Int = 1

///|
/// Maximum threshold for variable-length integer encoding.
/// Equals the number of lowercase letters (a-z).
const Tmax : Int = 26

///|
/// Skew parameter for bias adaptation function.
/// Controls how quickly the bias adjusts to character frequencies.
const Skew : Int = 38

///|
/// Damping factor applied only to the first delta value.
/// Prevents the bias from changing too rapidly at the start.
const Damp : Int = 700

///|
/// Initial bias value for the adaptation algorithm.
/// Chosen to work well for typical internationalized domain names.
const InitialBias : Int = 72

///|
/// Initial value of n, the threshold between basic ASCII (0-127)
/// and non-basic code points that require encoding.
const InitialN : Int = 0x80

///|
/// Delimiter character separating the basic ASCII portion
/// from the Punycode-encoded portion in the output.
const Delimiter : Char = '-'

///|
/// Largest positive value representable by MoonBit's signed Int type.
const MaxInt : Int = 0x7FFFFFFF

///|
/// Bias adaptation function (RFC 3492 Section 3.4)
fn adapt(delta : Int, numpoints : Int, firsttime : Bool) -> Int {
  let mut delta = delta
  if firsttime {
    delta = delta / Damp
  } else {
    delta = delta / 2
  }
  delta = delta + delta / numpoints
  let mut k = 0
  while delta > (Base - Tmin) * Tmax / 2 {
    delta = delta / (Base - Tmin)
    k = k + Base
  }
  k + (Base - Tmin + 1) * delta / (delta + Skew)
}

///|
/// Encode a digit to a basic code point (a-z for 0-25, 0-9 for 26-35)
fn encode_digit(d : Int) -> Char {
  if d < 26 {
    (d + 'a'.to_int()).unsafe_to_char()
  } else {
    (d - 26 + '0'.to_int()).unsafe_to_char()
  }
}

///|
/// Decode a basic code point to a digit
fn decode_digit(cp : Int) -> Int {
  if cp >= 'a'.to_int() && cp <= 'z'.to_int() {
    cp - 'a'.to_int()
  } else if cp >= 'A'.to_int() && cp <= 'Z'.to_int() {
    cp - 'A'.to_int()
  } else if cp >= '0'.to_int() && cp <= '9'.to_int() {
    cp - '0'.to_int() + 26
  } else {
    Base // Invalid
  }
}

///|
/// Check if a character is a basic ASCII character (0-127)
fn is_basic(c : Char) -> Bool {
  c.to_int() < 0x80
}

///|
/// Encode a Unicode string to Punycode
pub fn encode(input : String) -> String raise PunycodeError {
  let output = StringBuilder::new()

  // Convert input to array of characters
  let chars : Array[Char] = []
  for c in input {
    chars.push(c)
  }

  // Copy all basic code points to output
  let mut basic_count = 0
  for c in chars {
    if is_basic(c) {
      output.write_char(c)
      basic_count = basic_count + 1
    }
  }

  // Add delimiter if there were basic characters
  let handled = basic_count
  if basic_count > 0 {
    output.write_char(Delimiter)
  }
  let mut n = InitialN
  let mut delta = 0
  let mut bias = InitialBias
  let mut handled = handled
  while handled < chars.length() {
    // Find the minimum code point >= n
    let mut m = 0x10FFFF + 1
    for c in chars {
      let cp = c.to_int()
      if cp >= n && cp < m {
        m = cp
      }
    }

    // Increase delta enough to advance the decoder's  state to 
    let code_point_step = m - n
    let point_count = handled + 1
    if code_point_step > (MaxInt - delta) / point_count {
      raise Overflow
    }
    delta = delta + code_point_step * point_count
    n = m
    for c in chars {
      let cp = c.to_int()
      if cp < n {
        if delta == MaxInt {
          raise Overflow
        }
        delta = delta + 1
      } else if cp == n {
        // Represent delta as a generalized variable-length integer
        let mut q = delta
        let mut k = Base
        while true {
          let t = if k <= bias {
            Tmin
          } else if k >= bias + Tmax {
            Tmax
          } else {
            k - bias
          }
          if q < t {
            break
          }
          output.write_char(encode_digit(t + (q - t) % (Base - t)))
          q = (q - t) / (Base - t)
          k = k + Base
        }
        output.write_char(encode_digit(q))
        bias = adapt(delta, handled + 1, handled == basic_count)
        delta = 0
        handled = handled + 1
      }
    }
    if delta == MaxInt {
      raise Overflow
    }
    delta = delta + 1
    n = n + 1
  }
  output.to_string()
}

///|
/// Decode a Punycode string to Unicode
pub fn decode(input : String) -> String raise PunycodeError {
  let output : Array[Char] = []

  // Find the last delimiter
  let mut basic_end = -1
  let mut i = 0
  for c in input {
    if c == Delimiter {
      basic_end = i
    }
    i = i + 1
  }

  // Copy basic code points before the last delimiter
  if basic_end > 0 {
    let mut j = 0
    for c in input {
      if j >= basic_end {
        break
      }
      if !is_basic(c) {
        raise BadInput
      }
      output.push(c)
      j = j + 1
    }
  }

  // Main decoding loop
  let mut n = InitialN
  let mut i_val = 0
  let mut bias = InitialBias
  let mut in_pos = if basic_end >= 0 { basic_end + 1 } else { 0 }

  // Convert input to array for easier indexing
  let input_chars : Array[Char] = []
  for c in input {
    input_chars.push(c)
  }
  while in_pos < input_chars.length() {
    let oldi = i_val
    let mut w = 1
    let mut k = Base
    while true {
      if in_pos >= input_chars.length() {
        raise BadInput
      }
      let digit = decode_digit(input_chars[in_pos].to_int())
      in_pos = in_pos + 1
      if digit >= Base {
        raise BadInput
      }
      if digit > (MaxInt - i_val) / w {
        raise Overflow
      }
      i_val = i_val + digit * w
      let t = if k <= bias {
        Tmin
      } else if k >= bias + Tmax {
        Tmax
      } else {
        k - bias
      }
      if digit < t {
        break
      }
      if w > MaxInt / (Base - t) {
        raise Overflow
      }
      w = w * (Base - t)
      k = k + Base
    }
    let out_len = output.length() + 1
    bias = adapt(i_val - oldi, out_len, oldi == 0)
    if i_val / out_len > MaxInt - n {
      raise Overflow
    }
    n = n + i_val / out_len
    i_val = i_val % out_len

    // Reject values that are not valid Unicode scalar values before building
    // the output string. Int::to_char also rejects surrogate code points.
    guard n.to_char() is Some(decoded_char) else { raise BadInput }
    output.insert(i_val, decoded_char)
    i_val = i_val + 1
  }

  // Convert characters to string
  chars_to_string(output)
}

///|
/// Convert array of characters to string
fn chars_to_string(chars : Array[Char]) -> String {
  let sb = StringBuilder::new()
  for c in chars {
    sb.write_char(c)
  }
  sb.to_string()
}