///|
/// Parsed view of a CSI payload. Missing parameters are represented as `None`;
/// this preserves the semantic difference between an omitted value and zero.
pub(all) struct CsiCommand {
  private_prefix : String
  params : Array[Int?]
  intermediates : String
  final_byte : Char
} derive(Debug, Eq)

///|
/// Parsed view of an OSC event.
pub(all) struct OscCommand {
  command : String
  data : String
} derive(Debug, Eq)

///|
fn is_ascii_digit(c : Char) -> Bool {
  c >= '0' && c <= '9'
}

///|
fn parse_decimal(chars : Array[Char]) -> Int? {
  if chars.length() == 0 {
    return None
  }
  let mut value = 0
  for c in chars {
    if !is_ascii_digit(c) {
      return None
    }
    value = value * 10 + c.to_int() - '0'.to_int()
  }
  Some(value)
}

///|
pub fn parse_csi(event : Event) -> CsiCommand? {
  guard event.kind == Csi else { return None }
  guard event.final_byte is Some(final_byte) else { return None }
  let private_chars : Array[Char] = []
  let intermediate_chars : Array[Char] = []
  let params : Array[Int?] = []
  let current : Array[Char] = []
  let mut phase = 0 // 0 private, 1 parameters, 2 intermediates
  let mut saw_parameter_syntax = false
  for c in event.payload {
    let n = c.to_int()
    if phase == 0 && n >= 0x3c && n <= 0x3f {
      private_chars.push(c)
    } else if n >= 0x30 && n <= 0x3b && phase < 2 {
      phase = 1
      saw_parameter_syntax = true
      if c == ';' || c == ':' {
        params.push(parse_decimal(current))
        current.clear()
      } else {
        current.push(c)
      }
    } else if n >= 0x20 && n <= 0x2f {
      if phase == 1 {
        params.push(parse_decimal(current))
        current.clear()
      }
      phase = 2
      intermediate_chars.push(c)
    } else {
      return None
    }
  }
  if phase == 1 || saw_parameter_syntax {
    params.push(parse_decimal(current))
  }
  Some({
    private_prefix: String::from_array(private_chars),
    params,
    intermediates: String::from_array(intermediate_chars),
    final_byte,
  })
}

///|
pub fn parse_osc(event : Event) -> OscCommand? {
  guard event.kind == Osc else { return None }
  let command = StringBuilder::new()
  let data = StringBuilder::new()
  let mut in_data = false
  for c in event.payload {
    if !in_data && c == ';' {
      in_data = true
    } else if in_data {
      data.write_char(c)
    } else {
      command.write_char(c)
    }
  }
  Some({ command: command.to_string(), data: data.to_string() })
}

///|
pub fn csi_param(command : CsiCommand, index : Int, default : Int) -> Int {
  if index < 0 || index >= command.params.length() {
    return default
  }
  match command.params[index] {
    Some(value) => value
    None => default
  }
}

///|
fn ascii_lower(c : Char) -> Char {
  if c >= 'A' && c <= 'Z' {
    Int::unsafe_to_char(c.to_int() + 32)
  } else {
    c
  }
}

///|
pub fn lower_ascii(input : String) -> String {
  let out = StringBuilder::new(size_hint=input.length())
  for c in input {
    out.write_char(ascii_lower(c))
  }
  out.to_string()
}

///|
pub fn starts_with_ascii_case_insensitive(
  input : String,
  prefix : String,
) -> Bool {
  if prefix.length() > input.length() {
    return false
  }
  let mut i = 0
  while i < prefix.length() {
    let left = match input.unsafe_get(i).to_char() {
      Some(c) => c
      None => return false
    }
    let right = match prefix.unsafe_get(i).to_char() {
      Some(c) => c
      None => return false
    }
    if ascii_lower(left) != ascii_lower(right) {
      return false
    }
    i += 1
  }
  true
}

///|
pub fn contains_char(input : String, needle : Char) -> Bool {
  for c in input {
    if c == needle {
      return true
    }
  }
  false
}

///|
pub fn is_suspicious_uri(uri : String) -> Bool {
  let value = lower_ascii(uri)
  starts_with_ascii_case_insensitive(value, "javascript:") ||
  starts_with_ascii_case_insensitive(value, "data:") ||
  starts_with_ascii_case_insensitive(value, "file:") ||
  starts_with_ascii_case_insensitive(value, "vbscript:") ||
  starts_with_ascii_case_insensitive(value, "ssh:")
}

///|
pub fn is_bidi_control(c : Char) -> Bool {
  let n = c.to_int()
  n == 0x061c ||
  n == 0x200e ||
  n == 0x200f ||
  n == 0x202a ||
  n == 0x202b ||
  n == 0x202c ||
  n == 0x202d ||
  n == 0x202e ||
  n == 0x2066 ||
  n == 0x2067 ||
  n == 0x2068 ||
  n == 0x2069
}