///|
/// Records the notation recognized by the parser.
pub(all) enum Orthography {
  PlainInput
  ToneNumberInput
  ToneMarkInput
} derive(Eq, Debug)

///|
/// A validated Pinyin syllable in canonical lowercase form.
pub struct Syllable {
  base_value : String
  initial_value : String
  final_value : String
  tone_value : Int
  input_form : Orthography
} derive(Eq, Debug)

///|
fn syllable_ascii_slice(input : String, start : Int, end : Int) -> String {
  let mut output = ""
  let from = if start < 0 { 0 } else { start }
  let until = if end > input.length() { input.length() } else { end }
  for index = from; index < until; index = index + 1 {
    output = output + input[index].unsafe_to_char().to_string()
  }
  output
}

///|
fn syllable_starts_with(input : String, prefix : String) -> Bool {
  if prefix.length() > input.length() {
    return false
  }
  for index = 0; index < prefix.length(); index = index + 1 {
    if input[index] != prefix[index] {
      return false
    }
  }
  true
}

///|
fn syllable_ends_with(input : String, suffix : String) -> Bool {
  if suffix.length() > input.length() {
    return false
  }
  let start = input.length() - suffix.length()
  for index = 0; index < suffix.length(); index = index + 1 {
    if input[start + index] != suffix[index] {
      return false
    }
  }
  true
}

///|
fn marked_vowel(code : UInt16) -> (String, Int)? {
  match code.to_int() {
    0x0101 => Some(("a", 1))
    0x00E1 => Some(("a", 2))
    0x01CE => Some(("a", 3))
    0x00E0 => Some(("a", 4))
    0x0113 => Some(("e", 1))
    0x00E9 => Some(("e", 2))
    0x011B => Some(("e", 3))
    0x00E8 => Some(("e", 4))
    0x012B => Some(("i", 1))
    0x00ED => Some(("i", 2))
    0x01D0 => Some(("i", 3))
    0x00EC => Some(("i", 4))
    0x014D => Some(("o", 1))
    0x00F3 => Some(("o", 2))
    0x01D2 => Some(("o", 3))
    0x00F2 => Some(("o", 4))
    0x016B => Some(("u", 1))
    0x00FA => Some(("u", 2))
    0x01D4 => Some(("u", 3))
    0x00F9 => Some(("u", 4))
    0x01D6 => Some(("ü", 1))
    0x01D8 => Some(("ü", 2))
    0x01DA => Some(("ü", 3))
    0x01DC => Some(("ü", 4))
    0x0144 => Some(("n", 2))
    0x0148 => Some(("n", 3))
    0x01F9 => Some(("n", 4))
    0x1E3F => Some(("m", 2))
    0x1EBF => Some(("ê", 2))
    0x1EC1 => Some(("ê", 4))
    _ => None
  }
}

///|
fn combining_tone(code : Int) -> Int {
  match code {
    0x0304 => 1
    0x0301 => 2
    0x030C => 3
    0x0300 => 4
    _ => 0
  }
}

///|
fn accepts_combining_tone(code : Int) -> Bool {
  code == 97 ||
  code == 101 ||
  code == 105 ||
  code == 111 ||
  code == 117 ||
  code == 0x00FC ||
  code == 0x00EA ||
  code == 109 ||
  code == 110
}

///|
fn canonical_initial(base : String) -> String {
  for
    candidate in [
      "zh", "ch", "sh", "b", "p", "m", "f", "d", "t", "n", "l", "g", "k", "h", "j",
      "q", "x", "r", "z", "c", "s", "y", "w",
    ] {
    if syllable_starts_with(base, candidate) {
      return candidate
    }
  }
  ""
}

///|
fn has_syllable_nucleus(base : String) -> Bool {
  for index = 0; index < base.length(); index = index + 1 {
    let code = base[index].to_int()
    if code == 97 ||
      code == 101 ||
      code == 105 ||
      code == 111 ||
      code == 117 ||
      code == 0x00FC ||
      code == 0x00EA {
      return true
    }
  }
  base == "m" ||
  base == "n" ||
  base == "ng" ||
  syllable_ends_with(base, "m") ||
  syllable_ends_with(base, "n") ||
  syllable_ends_with(base, "ng")
}

///|
/// Parses plain, numbered, or precomposed tone-mark Pinyin.
pub fn parse_syllable(input : String) -> Result[Syllable, PinyinError] {
  if input.length() == 0 {
    return Err(InvalidSyllable(input, "empty"))
  }
  let mut end = input.length()
  let mut numbered_tone = -1
  let last = input[input.length() - 1].to_int()
  if last >= 48 && last <= 53 {
    numbered_tone = if last == 53 { 0 } else { last - 48 }
    end = end - 1
  }
  let mut base = ""
  let mut marked_tone = 0
  let mut saw_mark = false
  let mut index = 0
  while index < end {
    let original = input[index]
    let mut code = original.to_int()
    if code >= 65 && code <= 90 {
      code = code + 32
    }
    if code == 117 && index + 1 < end && input[index + 1].to_int() == 58 {
      base = base + "ü"
      index = index + 2
      continue
    }
    if code == 118 {
      base = base + "ü"
      index = index + 1
      continue
    }
    let combining = combining_tone(code)
    if combining > 0 {
      if saw_mark {
        return Err(InvalidSyllable(input, "multiple_tone_marks"))
      }
      if base.length() == 0 ||
        !accepts_combining_tone(base[base.length() - 1].to_int()) {
        return Err(InvalidSyllable(input, "invalid_combining_tone"))
      }
      marked_tone = combining
      saw_mark = true
      index = index + 1
      continue
    }
    let normalized_code = code.to_uint16()
    match marked_vowel(normalized_code) {
      Some((plain, tone)) => {
        if saw_mark {
          return Err(InvalidSyllable(input, "multiple_tone_marks"))
        }
        base = base + plain
        marked_tone = tone
        saw_mark = true
      }
      None =>
        if (code >= 97 && code <= 122) || code == 0x00FC || code == 0x00EA {
          base = base + code.to_uint16().unsafe_to_char().to_string()
        } else {
          return Err(InvalidSyllable(input, "invalid_character"))
        }
    }
    index = index + 1
  }
  if base.length() == 0 || !has_syllable_nucleus(base) {
    return Err(InvalidSyllable(input, "missing_nucleus"))
  }
  if saw_mark && numbered_tone >= 0 {
    return Err(InvalidSyllable(input, "conflicting_tones"))
  }
  let tone = if saw_mark {
    marked_tone
  } else if numbered_tone >= 0 {
    numbered_tone
  } else {
    0
  }
  let form = if saw_mark {
    ToneMarkInput
  } else if numbered_tone >= 0 {
    ToneNumberInput
  } else {
    PlainInput
  }
  let initial = canonical_initial(base)
  let final_part = syllable_ascii_slice(base, initial.length(), base.length())
  Ok({
    base_value: base,
    initial_value: initial,
    final_value: final_part,
    tone_value: tone,
    input_form: form,
  })
}

///|
pub fn Syllable::plain(self : Syllable) -> String {
  self.base_value
}

///|
pub fn Syllable::initial(self : Syllable) -> String {
  self.initial_value
}

///|
pub fn Syllable::final_part(self : Syllable) -> String {
  self.final_value
}

///|
pub fn Syllable::tone_number(self : Syllable) -> Int {
  self.tone_value
}

///|
pub fn Syllable::orthography(self : Syllable) -> Orthography {
  self.input_form
}