///|
/// Current version marker for the portable dictionary text format.
pub const DICTIONARY_FORMAT_VERSION : String = "MLX1"

///|
/// Statistics about a serialized dictionary payload.
pub(all) struct SerializationStats {
  version : String
  entry_count : Int
  byte_length : Int
  checksum : Int
  valid_header : Bool
} derive(Debug, Eq, ToJson)

///|
/// Escape a dictionary field for the MLX1 line format.
pub fn escape_dictionary_field(value : String) -> String {
  let output = StringBuilder()
  for ch in value {
    match ch {
      '\\' => output.write_string("\\\\")
      '\t' => output.write_string("\\t")
      '\n' => output.write_string("\\n")
      '\r' => output.write_string("\\r")
      _ => output.write_char(ch)
    }
  }
  output.to_string()
}

///|
/// Decode one escaped MLX1 field. Unknown escapes retain their character.
pub fn unescape_dictionary_field(value : String) -> String {
  let output = StringBuilder()
  let mut escaping = false
  for ch in value {
    if escaping {
      match ch {
        '\\' => output.write_char('\\')
        't' => output.write_char('\t')
        'n' => output.write_char('\n')
        'r' => output.write_char('\r')
        _ => {
          output.write_char('\\')
          output.write_char(ch)
        }
      }
      escaping = false
    } else if ch == '\\' {
      escaping = true
    } else {
      output.write_char(ch)
    }
  }
  if escaping {
    output.write_char('\\')
  }
  output.to_string()
}

///|
/// Return a deterministic, line-oriented representation of dictionary entries.
pub fn encode_entries(entries : ArrayView[Entry]) -> String {
  let output = StringBuilder()
  output.write_string("\{DICTIONARY_FORMAT_VERSION}\n")
  for item in entries {
    output.write_string(escape_dictionary_field(item.keyword))
    output.write_char('\t')
    output.write_string(escape_dictionary_field(item.label))
    output.write_char('\t')
    output.write_object(item.weight)
    output.write_char('\n')
  }
  output.to_string()
}

///|
/// Serialize all entries currently stored in a lexicon.
pub fn serialize_lexicon(lexicon : Lexicon) -> String {
  encode_entries(lexicon.entries())
}

///|
/// Decode a MLX1 payload. Malformed rows are skipped and never become empty
/// keywords, which makes it safe to inspect partially written cache files.
pub fn decode_entries(payload : String) -> Array[Entry] {
  let lines = [ for line in payload.split("\n") => line.to_owned() ]
  if lines.length() == 0 || lines[0] != DICTIONARY_FORMAT_VERSION {
    return []
  }
  let entries : Array[Entry] = []
  for index, line in lines {
    if index > 0 && line.length() > 0 {
      let fields = [ for field in line.split("\t") => field.to_owned() ]
      if fields.length() == 3 {
        let keyword = unescape_dictionary_field(fields[0])
        let label = unescape_dictionary_field(fields[1])
        match parse_serialized_int(fields[2]) {
          Some(weight) =>
            if keyword.length() > 0 {
              entries.push({ keyword, label, weight })
            }
          None => ()
        }
      }
    }
  }
  entries
}

///|
/// True when a payload has the MLX1 header.
pub fn is_dictionary_payload(payload : String) -> Bool {
  let lines = [ for line in payload.split("\n") => line.to_owned() ]
  match lines.get(0) {
    Some(header) => header == DICTIONARY_FORMAT_VERSION
    None => false
  }
}

///|
/// Compute a stable non-cryptographic checksum for dictionary entries.
pub fn dictionary_checksum(entries : ArrayView[Entry]) -> Int {
  let mut checksum = 216613626
  for item in entries {
    for ch in item.keyword {
      checksum = checksum * 16777619 + ch.to_int()
    }
    checksum = checksum * 16777619 + 31
    for ch in item.label {
      checksum = checksum * 16777619 + ch.to_int()
    }
    checksum = checksum * 16777619 + item.weight
  }
  if checksum == 0 {
    1
  } else {
    checksum
  }
}

///|
/// Compute payload metadata without decoding the dictionary twice.
pub fn serialization_stats(payload : String) -> SerializationStats {
  let entries = decode_entries(payload)
  let lines = [ for line in payload.split("\n") => line.to_owned() ]
  let version = match lines.get(0) {
    Some(header) => header
    None => ""
  }
  {
    version,
    entry_count: entries.length(),
    byte_length: payload.length(),
    checksum: dictionary_checksum(entries),
    valid_header: version == DICTIONARY_FORMAT_VERSION,
  }
}

///|
/// Serialize entries and append a checksum trailer for cache validation.
pub fn encode_entries_checked(entries : ArrayView[Entry]) -> String {
  let output = StringBuilder()
  output.write_string(encode_entries(entries))
  output.write_string("# checksum\t")
  output.write_object(dictionary_checksum(entries))
  output.write_char('\n')
  output.to_string()
}

///|
/// Decode a checked payload only when its checksum agrees with the rows.
pub fn decode_entries_checked(payload : String) -> Array[Entry] {
  let lines = [ for line in payload.split("\n") => line.to_owned() ]
  if lines.length() < 2 || lines[0] != DICTIONARY_FORMAT_VERSION {
    return []
  }
  let trailer = lines[lines.length() - 2]
  if !trailer.has_prefix("# checksum\t") {
    return []
  }
  let trailer_fields = [ for field in trailer.split("\t") => field.to_owned() ]
  if trailer_fields.length() != 2 {
    return []
  }
  let expected_text = trailer_fields[1]
  match parse_serialized_int(expected_text) {
    Some(expected) => {
      let body = StringBuilder()
      for index, line in lines {
        if index < lines.length() - 2 {
          body.write_string(line)
          body.write_char('\n')
        }
      }
      let entries = decode_entries(body.to_string())
      if dictionary_checksum(entries) == expected {
        entries
      } else {
        []
      }
    }
    None => []
  }
}

///|
/// Return the number of escaped control characters in a field.
pub fn escaped_character_count(value : String) -> Int {
  let mut count = 0
  for ch in value {
    if ch == '\\' || ch == '\t' || ch == '\n' || ch == '\r' {
      count += 1
    }
  }
  count
}

///|
/// Estimate the encoded size of entries without allocating the payload.
pub fn encoded_size(entries : ArrayView[Entry]) -> Int {
  let mut size = DICTIONARY_FORMAT_VERSION.length() + 1
  for item in entries {
    size += item.keyword.length() + escaped_character_count(item.keyword)
    size += item.label.length() + escaped_character_count(item.label)
    size += 3
    size += integer_width(item.weight)
  }
  size
}

///|
/// Return the integer width used by the text serializer.
pub fn integer_width(value : Int) -> Int {
  let mut number = if value < 0 { -value } else { value }
  let mut width = if value < 0 { 1 } else { 0 }
  if number == 0 {
    return width + 1
  }
  while number > 0 {
    width += 1
    number /= 10
  }
  width
}

///|
/// Compare the encoded size estimate with the actual payload length.
pub fn encoded_size_matches(entries : ArrayView[Entry]) -> Bool {
  encoded_size(entries) == encode_entries(entries).length()
}

///|
/// Return a copy of entries after a serialize/decode cycle.
pub fn normalize_serialized_entries(entries : ArrayView[Entry]) -> Array[Entry] {
  decode_entries(encode_entries(entries))
}

///|
/// Return a short format description for CLI help and diagnostics.
pub fn dictionary_format_description() -> String {
  "MLX1: escaped UTF-8 keyword, label, and signed weight rows"
}

///|
fn parse_serialized_int(value : String) -> Int? {
  if value.length() == 0 {
    return None
  }
  let chars = value.to_array()
  let mut sign = 1
  let mut index = 0
  if chars[0] == '-' {
    sign = -1
    index = 1
  }
  if index >= chars.length() {
    return None
  }
  let mut number = 0
  while index < chars.length() {
    let ch = chars[index]
    if ch < '0' || ch > '9' {
      return None
    }
    number = number * 10 + ch.to_int() - '0'.to_int()
    index += 1
  }
  Some(sign * number)
}