///|
/// Resolve the escape sequences of a `moon.mod` string literal.
///
/// `moon_config` hands back the literal's raw source text (`"a \"b\""` becomes
/// `a \"b\"`), so a value that is about to be re-emitted as MoonBit source has
/// to be decoded first; otherwise every round trip doubles the backslashes.
///
/// Every sequence the MoonBit lexer recognizes is decoded, which makes `quote`
/// a true inverse and regenerating a file a no-op. Text that is not valid
/// source (a bad `\u` sequence, a trailing backslash) is kept verbatim instead
/// of being swallowed.
///
/// Quotes are not part of the value: `moon_config` hands back what is inside
/// them, and `quote` is what puts them back.
pub fn unescape(raw : String) -> String {
  let buf = StringBuilder()
  let chars = raw.to_array()
  let mut i = 0
  let end = chars.length()
  while i < end {
    let ch = chars[i]
    if ch != '\\' || i + 1 >= end {
      buf.write_char(ch)
      i = i + 1
      continue
    }
    let next = chars[i + 1]
    match next {
      'n' => {
        buf.write_char('\n')
        i = i + 2
      }
      'r' => {
        buf.write_char('\r')
        i = i + 2
      }
      't' => {
        buf.write_char('\t')
        i = i + 2
      }
      'b' => {
        buf.write_char('\u{8}')
        i = i + 2
      }
      'f' => {
        buf.write_char('\u{C}')
        i = i + 2
      }
      'v' => {
        buf.write_char('\u{B}')
        i = i + 2
      }
      '0' => {
        buf.write_char('\u{0}')
        i = i + 2
      }
      '\\' | '"' | '\'' | '`' | '/' | ' ' => {
        // The lexer accepts these and means the character itself.
        buf.write_char(next)
        i = i + 2
      }
      'u' =>
        match decode_unicode(chars, i + 2) {
          Some((decoded, width)) => {
            buf.write_char(decoded)
            i = i + 2 + width
          }
          None => {
            buf.write_char('\\')
            buf.write_char(next)
            i = i + 2
          }
        }
      'x' =>
        match digits_value(chars, i + 2, 2, 16) {
          Some(code) => {
            buf.write_char(code.unsafe_to_char())
            i = i + 4
          }
          None => {
            buf.write_char('\\')
            buf.write_char(next)
            i = i + 2
          }
        }
      'o' =>
        match digits_value(chars, i + 2, 3, 8) {
          Some(code) => {
            buf.write_char(code.unsafe_to_char())
            i = i + 5
          }
          None => {
            buf.write_char('\\')
            buf.write_char(next)
            i = i + 2
          }
        }
      // `\` swallows the newline and the following indentation: it is
      // a line continuation, the usual way to keep a long `description` tidy.
      '\n' => {
        let mut j = i + 2
        while j < end && is_blank(chars[j]) {
          j = j + 1
        }
        i = j
      }
      _ => {
        // Unknown escape: keep the pair, so nothing is silently dropped.
        buf.write_char('\\')
        buf.write_char(next)
        i = i + 2
      }
    }
  }
  buf.to_string()
}

///|
fn is_blank(ch : Char) -> Bool {
  ch == ' ' || ch == '\t'
}

///|
/// The value of `width` digits in `base` starting at `start`, or `None` when
/// they are missing or not digits in that base.
fn digits_value(
  chars : Array[Char],
  start : Int32,
  width : Int32,
  base : Int32,
) -> Int32? {
  if start + width > chars.length() {
    return None
  }
  let mut code = 0
  for offset = 0; offset < width; offset = offset + 1 {
    let digit = match chars[start + offset] {
      '0'..<='9' => chars[start + offset].to_int() - '0'.to_int()
      'a'..<='f' => chars[start + offset].to_int() - 'a'.to_int() + 10
      'A'..<='F' => chars[start + offset].to_int() - 'A'.to_int() + 10
      _ => return None
    }
    if digit >= base {
      return None
    }
    code = code * base + digit
  }
  if code > 0x10FFFF {
    return None
  }
  Some(code)
}

///|
/// `\u{1F600}` and `\u0041`; returns the decoded char and how many chars past
/// the `u` it consumed.
fn decode_unicode(chars : Array[Char], start : Int32) -> (Char, Int32)? {
  if start < chars.length() && chars[start] == '{' {
    let mut end = start + 1
    while end < chars.length() && chars[end] != '}' {
      end = end + 1
    }
    if end >= chars.length() || end == start + 1 {
      return None
    }
    match digits_value(chars, start + 1, end - start - 1, 16) {
      Some(code) => Some((code.unsafe_to_char(), end - start + 1))
      None => None
    }
  } else {
    match digits_value(chars, start, 4, 16) {
      Some(code) => Some((code.unsafe_to_char(), 4))
      None => None
    }
  }
}