///|
/// One part of a character class: a single code unit or an inclusive range.
pub(all) enum ClassPart {
  Char(String)
  Range(String, String)
} derive(Eq, Debug)

///|
pub impl ToJson for ClassPart with fn to_json(self) {
  match self {
    Char(c) => c.to_json()
    Range(a, b) => [a.to_json(), b.to_json()]
  }
}

///|
/// Something the parser expected at the failure position.
pub(all) enum Expectation {
  Literal(text~ : String, ignore_case~ : Bool)
  Class(parts~ : Array[ClassPart], inverted~ : Bool, ignore_case~ : Bool)
  Any
  End
  Other(description~ : String)
  Not(Expectation)
} derive(Eq, Debug)

///|
pub impl ToJson for Expectation with fn to_json(self) {
  match self {
    Literal(text~, ignore_case~) =>
      {
        "type": "literal",
        "text": text.to_json(),
        "ignoreCase": ignore_case.to_json(),
      }
    Class(parts~, inverted~, ignore_case~) =>
      {
        "type": "class",
        "parts": parts.to_json(),
        "inverted": inverted.to_json(),
        "ignoreCase": ignore_case.to_json(),
      }
    Any => { "type": "any" }
    End => { "type": "end" }
    Other(description~) =>
      { "type": "other", "description": description.to_json() }
    Not(e) => { "type": "not", "expected": e.to_json() }
  }
}

///|
fn hex_upper(c : Int) -> String {
  let digits = "0123456789ABCDEF"
  if c < 16 {
    digits[c:c + 1].to_owned()
  } else {
    let sb = StringBuilder()
    let mut started = false
    for shift = 12; shift >= 0; shift = shift - 4 {
      let d = (c >> shift) & 0xF
      if d != 0 || started {
        started = true
        sb.write_view(digits[d:d + 1])
      }
    }
    sb.to_string()
  }
}

///|
/// Shared tail of `literalEscape` / `classEscape` in the generated parser.
fn escape_controls(sb : StringBuilder, c : Int) -> Unit {
  match c {
    0 => sb.write_string("\\0")
    9 => sb.write_string("\\t")
    10 => sb.write_string("\\n")
    13 => sb.write_string("\\r")
    _ =>
      if c <= 0x0F {
        sb.write_string("\\x0")
        sb.write_string(hex_upper(c))
      } else if c <= 0x1F || (c >= 0x7F && c <= 0x9F) {
        sb.write_string("\\x")
        sb.write_string(hex_upper(c))
      } else {
        sb.write_char(c.unsafe_to_char())
      }
  }
}

///|
fn literal_escape(s : String) -> String {
  let sb = StringBuilder()
  for i in 0.. sb.write_string("\\\\")
      '"' => sb.write_string("\\\"")
      _ => escape_controls(sb, c)
    }
  }
  sb.to_string()
}

///|
fn class_escape(s : String) -> String {
  let sb = StringBuilder()
  for i in 0.. sb.write_string("\\\\")
      ']' => sb.write_string("\\]")
      '^' => sb.write_string("\\^")
      '-' => sb.write_string("\\-")
      _ => escape_controls(sb, c)
    }
  }
  sb.to_string()
}

///|
/// Human-readable description of an expectation, as in `describeExpectation`.
pub fn Expectation::describe(self : Expectation) -> String {
  match self {
    Literal(text~, ..) => "\"" + literal_escape(text) + "\""
    Class(parts~, inverted~, ..) => {
      // JS coerces the escaped-parts array to a string, joining with ",".
      let escaped = parts.map(part => {
        match part {
          Char(c) => class_escape(c)
          Range(a, b) => class_escape(a) + "-" + class_escape(b)
        }
      })
      "[" + (if inverted { "^" } else { "" }) + escaped.join(",") + "]"
    }
    Any => "any character"
    End => "end of input"
    Other(description~) => description
    Not(e) => "not " + e.describe()
  }
}

///|
fn describe_expected(expected : Array[Expectation]) -> String {
  let descriptions = expected.map(Expectation::describe)
  descriptions.sort_by(js_compare)
  let unique : Array[String] = []
  for d in descriptions {
    if unique.last() is Some(prev) && prev == d {
      continue
    }
    unique.push(d)
  }
  match unique.length() {
    1 => unique[0]
    2 => unique[0] + " or " + unique[1]
    // With no descriptions JS yields `", or undefined"`; keep that quirk.
    0 => ", or undefined"
    n => unique[:n - 1].join(", ") + ", or " + unique[n - 1]
  }
}

///|
/// `peg$SyntaxError.buildMessage(expected, found)`.
pub fn build_message(expected : Array[Expectation], found : String?) -> String {
  let found_desc = match found {
    Some(f) if f != "" => "\"" + literal_escape(f) + "\""
    _ => "end of input"
  }
  "Expected " + describe_expected(expected) + " but " + found_desc + " found."
}

///|
pub extend ClassPart with ToJson::{to_json}

///|
pub extend Expectation with ToJson::{to_json}