///|
/// A minimal syntax-highlight tokenizer for fenced code blocks. It classifies
/// runs into four token kinds — keywords, strings, comments and numbers — and
/// maps each to a color. Coverage is intentionally small (MoonBit, JavaScript,
/// Markdown, JSON, TypeScript, Python, Rust, Go, C/C++, Bash, YAML, TOML, SQL, PHP, Ruby, Swift, Kotlin, Scala, HTML, CSS, XML, Diff, Makefile, Dockerfile, INI) so unknown languages fall back to a single plain run; the
/// goal is visual differentiation close to Typora's default, not a full grammar.
///
/// The tokenizer is line-oriented and stateless between lines except for
/// block-comment tracking, which keeps it cheap on every reparse.

///|
/// Token kinds produced by the highlighter, each with a stable color.
pub enum CodeHighlightToken {
  CodePlain
  CodeKeyword
  CodeString
  CodeComment
  CodeNumber
  CodeFunction
  CodeType
  CodeOperator
  CodeAnnotation
  CodeEscape
} derive(Eq, Debug)

///|
/// A highlighted slice: a piece of source text and the token kind it belongs to.
pub struct CodeHighlightSpan {
  text : String
  kind : CodeHighlightToken
}

///|
/// Resolve a token kind to a display color. Colors are tuned to read well on
/// both light and dark surfaces.
fn code_highlight_color(kind : CodeHighlightToken) -> @core.Color {
  match kind {
    // Plain text uses the run's inherited color (caller decides), so signal
    // that with a sentinel that callers translate to None.
    CodePlain => @core.Color::black()
    CodeKeyword =>
      // Indigo.
      @core.Color::rgba(r=0.36, g=0.32, b=0.78, a=1.0)
    CodeString =>
      // Green.
      @core.Color::rgba(r=0.20, g=0.62, b=0.36, a=1.0)
    CodeComment =>
      // Muted grey.
      @core.Color::rgba(r=0.45, g=0.48, b=0.52, a=1.0)
    CodeNumber =>
      // Amber.
      @core.Color::rgba(r=0.78, g=0.52, b=0.18, a=1.0)
    CodeFunction =>
      // Blue.
      @core.Color::rgba(r=0.27, g=0.42, b=0.70, a=1.0)
    CodeType =>
      // Teal.
      @core.Color::rgba(r=0.22, g=0.77, b=0.62, a=1.0)
    CodeOperator =>
      // Red.
      @core.Color::rgba(r=0.87, g=0.33, b=0.33, a=1.0)
    CodeAnnotation =>
      // Magenta.
      @core.Color::rgba(r=0.76, g=0.32, b=0.62, a=1.0)
    CodeEscape =>
      // Purple.
      @core.Color::rgba(r=0.67, g=0.29, b=0.91, a=1.0)
  }
}

///|
/// True if the language id has a highlighter. Used to decide whether to apply
/// token coloring or fall back to a single plain run.
pub fn code_highlight_supports_language(language : String) -> Bool {
  let lower = language.to_lower()
  lower == "moonbit" ||
  lower == "moon" ||
  lower == "python" ||
  lower == "py" ||
  lower == "rust" ||
  lower == "rs" ||
  lower == "go" ||
  lower == "golang" ||
  lower == "typescript" ||
  lower == "ts" ||
  lower == "html" ||
  lower == "css" ||
  lower == "bash" ||
  lower == "sh" ||
  lower == "shell" ||
  lower == "yaml" ||
  lower == "yml" ||
  lower == "c" ||
  lower == "cpp" ||
  lower == "java" ||
  lower == "php" ||
  lower == "ruby" ||
  lower == "rb" ||
  lower == "swift" ||
  lower == "kotlin" ||
  lower == "scala" ||
  lower == "sql" ||
  lower == "xml" ||
  lower == "toml" ||
  lower == "ini" ||
  lower == "diff" ||
  lower == "makefile" ||
  lower == "dockerfile" ||
  lower == "js" ||
  lower == "javascript" ||
  lower == "markdown" ||
  lower == "md" ||
  lower == "json"
}

///|
/// Check if a multi-character or single-char operator starts at index `pos`.
/// Returns Some(length) if found, None otherwise.
fn code_highlight_try_operator(chars : Array[Char], n : Int, pos : Int) -> Int? {
  if pos + 1 < n {
    let two = String::from_array(chars[pos:pos + 2])
    if two == "==" ||
      two == "!=" ||
      two == "<=" ||
      two == ">=" ||
      two == "&&" ||
      two == "||" ||
      two == "++" ||
      two == "--" ||
      two == "->" ||
      two == "=>" ||
      two == "::" ||
      two == ".." ||
      two == "+=" ||
      two == "-=" ||
      two == "*=" ||
      two == "/=" ||
      two == "%=" {
      return Some(2)
    }
  }
  let c = chars[pos]
  if c == '+' ||
    c == '-' ||
    c == '*' ||
    c == '/' ||
    c == '%' ||
    c == '=' ||
    c == '!' ||
    c == '<' ||
    c == '>' ||
    c == '&' ||
    c == '|' ||
    c == '^' ||
    c == '~' ||
    c == ':' ||
    c == '.' ||
    c == '?' {
    return Some(1)
  }
  None
}

///|
/// Tokenize a code source into highlight spans. Adjacent spans of the same kind
/// are merged so the run list stays compact. Returns a single plain span when
/// the language is unsupported or the source is empty.
pub fn code_highlight_tokenize(
  source : String,
  language : String,
) -> Array[CodeHighlightSpan] {
  if source == "" || !code_highlight_supports_language(language) {
    return [{ text: source, kind: CodePlain }]
  }
  let lower = language.to_lower()
  let keywords = code_highlight_keywords(lower)
  let spans : Array[CodeHighlightSpan] = []
  let chars = source.to_array()
  let n = chars.length()
  let mut i = 0
  let mut buffer : Array[Char] = []
  // Flush the accumulated plain buffer as a single plain span.
  let flush_plain = () => {
    if buffer.length() > 0 {
      spans.push({ text: String::from_array(buffer), kind: CodePlain })
      buffer = []
    }
  }
  while i < n {
    let c = chars[i]
    // Annotation: @ prefix or #[ prefix.
    if c == '@' {
      flush_plain()
      let mut j = i
      while j < n &&
            chars[j] != ' ' &&
            chars[j] != '\t' &&
            chars[j] != '\n' &&
            chars[j] != '(' &&
            chars[j] != ')' {
        j = j + 1
      }
      spans.push({ text: String::from_array(chars[i:j]), kind: CodeAnnotation })
      i = j
      continue
    }
    if c == '#' && i + 1 < n && chars[i + 1] == '[' {
      flush_plain()
      let mut j = i
      while j < n && chars[j] != '\n' {
        j = j + 1
      }
      spans.push({ text: String::from_array(chars[i:j]), kind: CodeAnnotation })
      i = j
      continue
    }
    // Line comment: // ... end of line (MoonBit/JS/JSON-free).
    if c == '/' && i + 1 < n && chars[i + 1] == '/' {
      flush_plain()
      let mut j = i
      while j < n && chars[j] != '\n' {
        j = j + 1
      }
      spans.push({ text: String::from_array(chars[i:j]), kind: CodeComment })
      i = j
      continue
    }
    // Hash comment: # ... end of line (Markdown front-matter-ish / shell / YAML / Python / Ruby / TOML / Makefile / Dockerfile).
    if c == '#' &&
      (
        lower == "markdown" ||
        lower == "md" ||
        lower == "python" ||
        lower == "py" ||
        lower == "ruby" ||
        lower == "rb" ||
        lower == "bash" ||
        lower == "sh" ||
        lower == "shell" ||
        lower == "yaml" ||
        lower == "yml" ||
        lower == "toml" ||
        lower == "makefile" ||
        lower == "dockerfile"
      ) {
      flush_plain()
      let mut j = i
      while j < n && chars[j] != '\n' {
        j = j + 1
      }
      spans.push({ text: String::from_array(chars[i:j]), kind: CodeComment })
      i = j
      continue
    }
    // String literal: " ... " or ' ... ' with escape highlighting.
    if c == '"' || c == '\'' {
      flush_plain()
      let quote = c
      let mut j = i + 1
      let mut seg_start = i
      while j < n && chars[j] != quote {
        if chars[j] == '\\' && j + 1 < n {
          // Push text before escape as string.
          if j > seg_start {
            spans.push({
              text: String::from_array(chars[seg_start:j]),
              kind: CodeString,
            })
          }
          // Push escape sequence as CodeEscape.
          let esc_end = if j + 2 < n &&
            chars[j + 1] == 'u' &&
            chars[j + 2] == '{' {
            // \u{...} multi-char escape
            let mut k = j + 3
            while k < n &&
                  chars[k] != '}' &&
                  chars[k] != '"' &&
                  chars[k] != '\'' {
              k = k + 1
            }
            if k < n && chars[k] == '}' {
              k + 1
            } else {
              j + 2
            }
          } else {
            j + 2
          }
          spans.push({
            text: String::from_array(chars[j:esc_end]),
            kind: CodeEscape,
          })
          j = esc_end
          seg_start = j
        } else {
          j = j + 1
        }
      }
      if j < n {
        j = j + 1
      }
      if j > seg_start {
        spans.push({
          text: String::from_array(chars[seg_start:j]),
          kind: CodeString,
        })
      }
      i = j
      continue
    }
    // Number: leading digit, consume digits and a single decimal point.
    if c.is_ascii_digit() {
      flush_plain()
      let mut j = i
      let mut seen_dot = false
      while j < n {
        if chars[j].is_ascii_digit() {
          j = j + 1
        } else if chars[j] == '.' &&
          !seen_dot &&
          j + 1 < n &&
          chars[j + 1].is_ascii_digit() {
          seen_dot = true
          j = j + 1
        } else {
          break
        }
      }
      spans.push({ text: String::from_array(chars[i:j]), kind: CodeNumber })
      i = j
      continue
    }
    // Identifier: letter/underscore, check against keyword/function/type.
    if c.is_ascii_alphabetic() || c == '_' {
      flush_plain()
      let mut j = i
      while j < n {
        let d = chars[j]
        if d.is_ascii_alphabetic() || d.is_ascii_digit() || d == '_' {
          j = j + 1
        } else {
          break
        }
      }
      let word = String::from_array(chars[i:j])
      let kind = if keywords.contains(word) {
        CodeKeyword
      } else {
        // Check if followed by '(' — likely a function call.
        let mut peek = j
        while peek < n && (chars[peek] == ' ' || chars[peek] == '\t') {
          peek = peek + 1
        }
        if peek < n && chars[peek] == '(' {
          CodeFunction
        } else if word.length() > 0 {
          let first = word[0]
          // PascalCase (starts with 'A'-'Z' and not a common keyword) → likely a type name.
          if first >= 65 && first <= 90 {
            CodeType
          } else {
            CodePlain
          }
        } else if code_highlight_common_type(word) {
          CodeType
        } else {
          CodePlain
        }
      }
      spans.push({ text: word, kind })
      i = j
      continue
    }
    // Operator: multi-char or single-char operator symbols.
    if code_highlight_try_operator(chars, n, i) is Some(len) {
      flush_plain()
      spans.push({
        text: String::from_array(chars[i:i + len]),
        kind: CodeOperator,
      })
      i = i + len
      continue
    }
    // Any other character accumulates into the plain buffer.
    buffer.push(c)
    i = i + 1
  }
  flush_plain()
  code_highlight_merge_adjacent(spans)
}

///|
/// Merge consecutive spans of the same kind into one, keeping the run list
/// compact for the rich-text renderer.
fn code_highlight_merge_adjacent(
  spans : Array[CodeHighlightSpan],
) -> Array[CodeHighlightSpan] {
  let merged : Array[CodeHighlightSpan] = []
  for span in spans {
    match merged.length() {
      0 => merged.push(span)
      _ => {
        let last = merged[merged.length() - 1]
        if last.kind == span.kind &&
          !code_highlight_span_is_identifier(last) &&
          !code_highlight_span_is_identifier(span) {
          merged[merged.length() - 1] = {
            text: last.text + span.text,
            kind: last.kind,
          }
        } else {
          merged.push(span)
        }
      }
    }
  }
  merged
}

///|
fn code_highlight_span_is_identifier(span : CodeHighlightSpan) -> Bool {
  if span.kind != CodePlain {
    return false
  }
  let chars = span.text.to_array()
  let mut i = 0
  while i < chars.length() {
    let c = chars[i]
    if !(c.is_ascii_alphabetic() || c == '_') {
      return false
    }
    i = i + 1
  }
  true
}

///|
/// Common type names recognized across languages. Matched when a PascalCase
/// check is not sufficient (e.g. all-caps acronyms or single-word types).
fn code_highlight_common_type(word : String) -> Bool {
  word == "String" ||
  word == "Int" ||
  word == "Float" ||
  word == "Double" ||
  word == "Bool" ||
  word == "Boolean" ||
  word == "Char" ||
  word == "Byte" ||
  word == "Short" ||
  word == "Long" ||
  word == "Void" ||
  word == "Unit" ||
  word == "Object" ||
  word == "Array" ||
  word == "List" ||
  word == "Map" ||
  word == "Set" ||
  word == "Vector" ||
  word == "Option" ||
  word == "Result" ||
  word == "Future" ||
  word == "Promise" ||
  word == "Error" ||
  word == "Nullable" ||
  word == "Any" ||
  word == "Never" ||
  word == "number" ||
  word == "string" ||
  word == "boolean" ||
  word == "undefined"
}

///|
/// The keyword set for a given language id (already lower-cased).
fn code_highlight_keywords(language : String) -> Array[String] {
  match language {
    "moonbit" | "moon" =>
      [
        "fn", "let", "mut", "pub", "priv", "struct", "enum", "type", "match", "if",
        "else", "for", "while", "return", "break", "continue", "true", "false", "Unit",
        "Bool", "Int", "Double", "String", "Char", "Array", "ignore", "trait", "impl",
        "extern", "derive", "test", "in", "is", "as", "where", "try", "catch", "raise",
      ]
    "js" | "javascript" =>
      [
        "function", "const", "let", "var", "return", "if", "else", "for", "while",
        "do", "break", "continue", "switch", "case", "default", "class", "extends",
        "new", "this", "super", "import", "export", "from", "async", "await", "yield",
        "try", "catch", "finally", "throw", "typeof", "instanceof", "in", "of", "true",
        "false", "null", "undefined", "void",
      ]
    "typescript" | "ts" =>
      [
        "function", "const", "let", "var", "return", "if", "else", "for", "while",
        "do", "break", "continue", "switch", "case", "default", "class", "extends",
        "new", "this", "super", "import", "export", "from", "async", "await", "yield",
        "try", "catch", "finally", "throw", "typeof", "instanceof", "in", "of", "true",
        "false", "null", "undefined", "void", "interface", "type", "as", "readonly",
      ]
    "python" | "py" =>
      [
        "def", "class", "return", "if", "elif", "else", "for", "while", "break",
        "continue", "import", "from", "as", "try", "except", "finally", "raise",
        "with", "yield", "lambda", "pass", "and", "or", "not", "is", "in", "True",
        "False", "None", "self", "async", "await",
      ]
    "rust" | "rs" =>
      [
        "fn", "let", "mut", "pub", "priv", "struct", "enum", "impl", "trait", "match",
        "if", "else", "for", "while", "loop", "return", "break", "continue", "true",
        "false", "mod", "use", "super", "self", "Self", "const", "static", "unsafe",
        "async", "await", "move", "ref", "type", "where", "dyn", "crate",
      ]
    "go" | "golang" =>
      [
        "func", "return", "if", "else", "for", "range", "switch", "case", "default",
        "struct", "type", "var", "const", "import", "package", "defer", "go", "select",
        "chan", "map", "make", "new", "nil", "true", "false", "fallthrough",
      ]
    "c" | "cpp" =>
      [
        "int", "long", "short", "char", "float", "double", "void", "return", "if",
        "else", "for", "while", "do", "switch", "case", "default", "break", "continue",
        "struct", "class", "public", "private", "protected", "new", "delete", "true",
        "false", "nullptr", "const", "static", "typedef", "enum", "namespace", "using",
        "template", "typename", "virtual", "override", "final",
      ]
    "java" =>
      [
        "public", "private", "protected", "static", "final", "class", "interface",
        "extends", "implements", "new", "return", "if", "else", "for", "while", "do",
        "switch", "case", "default", "break", "continue", "throw", "throws", "try",
        "catch", "finally", "import", "package", "true", "false", "null", "this",
        "super", "instanceof", "void", "int", "long", "double", "boolean", "String",
      ]
    "php" =>
      [
        "function", "class", "public", "private", "protected", "static", "return",
        "if", "else", "elseif", "foreach", "for", "while", "do", "switch", "case",
        "default", "break", "continue", "try", "catch", "finally", "throw", "new",
        "echo", "print", "true", "false", "null", "array", "echo", "isset", "unset",
        "namespace", "use", "as", "implements", "extends", "interface", "trait",
      ]
    "ruby" | "rb" =>
      [
        "def", "end", "class", "module", "return", "if", "elsif", "else", "unless",
        "case", "when", "while", "until", "for", "do", "break", "next", "redo", "retry",
        "raise", "rescue", "ensure", "begin", "yield", "self", "true", "false", "nil",
        "require", "include", "extend", "attr_reader", "attr_writer", "attr_accessor",
      ]
    "swift" =>
      [
        "func", "return", "if", "else", "guard", "switch", "case", "default", "for",
        "while", "repeat", "break", "continue", "class", "struct", "enum", "protocol",
        "extension", "import", "true", "false", "nil", "self", "super", "init", "deinit",
        "as", "try", "catch", "throw", "throws", "rethrows", "let", "var",
      ]
    "kotlin" =>
      [
        "fun", "return", "if", "else", "when", "for", "while", "do", "break", "continue",
        "class", "interface", "object", "sealed", "data", "enum", "val", "var", "true",
        "false", "null", "this", "super", "as", "is", "in", "throw", "try", "catch",
        "finally", "import", "package", "companion", "init", "suspend", "lateinit",
      ]
    "scala" =>
      [
        "def", "class", "trait", "object", "extends", "with", "new", "return", "if",
        "else", "for", "while", "do", "break", "continue", "match", "case", "try",
        "catch", "finally", "throw", "import", "package", "true", "false", "null",
        "val", "var", "lazy", "implicit", "sealed", "final", "override", "type",
      ]
    "sql" =>
      [
        "select", "from", "where", "join", "left", "right", "inner", "outer", "on",
        "group", "by", "order", "having", "limit", "offset", "insert", "into", "values",
        "update", "set", "delete", "create", "table", "alter", "drop", "index", "view",
        "as", "case", "when", "then", "else", "end", "and", "or", "not", "null",
        "true", "false", "count", "sum", "avg", "min", "max", "distinct",
      ]
    "html" | "css" | "xml" => []
    "bash" | "sh" | "shell" | "makefile" | "dockerfile" => []
    "yaml" | "yml" | "toml" | "ini" | "diff" => []
    "json" => ["true", "false", "null"]
    "markdown" | "md" => []
    _ => []
  }
}