#!/usr/bin/env python3
"""Generate editor syntax-highlighting files for the Otty config format.

Otty's user config (`~/.config/otty/config.toml`) is a flat, Ghostty-style
`key = value` format (it is a valid TOML *subset*, but editors treat it as
plain TOML and don't know the option names / enum values). This script emits:

  - vim/syntax/otty.vim      Vim / Neovim syntax
  - vim/ftdetect/otty.vim    Vim / Neovim filetype detection
  - bat/otty.sublime-syntax  Sublime-syntax for `bat`

The authoritative list of config keys is parsed straight from
`packages/config/src/lib.rs` (`Config::known_keys()`), so the keyword list
never drifts from the source of truth. Re-run after adding/removing keys:

    python3 resources/editors/generate.py

Enum literal values are curated below (they change rarely); a value that
isn't listed simply renders with the generic value color — no harm.
"""

import os
import re

HERE = os.path.dirname(os.path.abspath(__file__))
LIB_RS = os.path.normpath(os.path.join(HERE, "..", "..", "packages", "config", "src", "lib.rs"))


def parse_known_keys() -> list[str]:
    """Extract the string literals inside `Config::known_keys() { &[ ... ] }`."""
    with open(LIB_RS, encoding="utf-8") as fh:
        src = fh.read()
    m = re.search(r"fn known_keys\(\)\s*->\s*&'static \[&'static str\]\s*\{\s*&\[(.*?)\]\s*\}", src, re.S)
    if not m:
        raise SystemExit(f"could not locate known_keys() in {LIB_RS}")
    keys = re.findall(r'"([a-z0-9][a-z0-9-]*)"', m.group(1))
    if len(keys) < 100:
        raise SystemExit(f"parsed only {len(keys)} keys — pattern likely broke")
    # Dedup, preserve no particular order; we sort for matching below.
    return sorted(set(keys))


# Curated enum-literal values that appear on the value side of `key = value`.
# Single-token identifiers only (dashed/underscored is fine); multi-token
# values like `tab+right-arrow` or `ctrl+space` are skipped — they highlight
# fine as plain text. Common-word values (copy, none, all, …) are intentionally
# included; they're only matched as bare value tokens, never inside strings or
# comments.
ENUM_VALUES = sorted(set([
    # cursor / font
    "block", "bar", "underline", "block_hollow",
    "off", "smooth",
    "srgb-over", "macos-like", "linear", "perceptual",
    "auto", "primary-only", "synthetic",
    "calt", "dlig",
    # widen-ambiguous
    "enclosed-alphanumerics", "number-forms", "math-operators",
    "misc-technical", "misc-symbols", "dingbats", "arrows", "geometric-shapes",
    # session
    "plain", "redacted", "none", "whitelist", "all",
    # option-as-alt
    "left", "right",
    # terminal-scroll-past-*
    "disabled", "last-line-with-content", "last-line-in-middle", "cursor-line",
    "same-as-last-line", "first-line-with-content", "first-line-in-middle",
    # right-click / open-with / clipboard / link
    "context-menu", "copy", "paste", "copy-or-paste", "ignore",
    "browser", "custom", "default-app", "ask", "allow", "deny",
    # osc-color-report-format
    "sixteen-bit", "eight-bit",
    # window
    "frame", "grid", "fullscreen",
    "sidebar-left", "tabs-top", "tabs-bottom",
    "always", "never", "focused",
    # language / launch / close / tab-position
    "system", "chinese", "english",
    "new_window", "restore_session",
    "process", "multiple-tabs",
    "end", "after-current",
    # autocomplete
    "disable", "escape", "option-escape",
    "tab-unfocused",
    # quick-terminal-cwd
    "current", "home", "last_used",
    # recipe-replay-saved
    "ask_once", "manually",
]))


def vim_alt(words: list[str]) -> str:
    """Vim alternation, longest-first so dashed words match fully before a
    shorter prefix (e.g. `font-family-fallback` before `font-family`)."""
    ordered = sorted(words, key=lambda w: (-len(w), w))
    return r"\|".join(ordered)


def re_alt(words: list[str]) -> str:
    """Regex alternation, longest-first (for Sublime / Oniguruma)."""
    ordered = sorted(words, key=lambda w: (-len(w), w))
    return "|".join(ordered)


def write(path: str, content: str) -> None:
    full = os.path.join(HERE, path)
    os.makedirs(os.path.dirname(full), exist_ok=True)
    with open(full, "w", encoding="utf-8") as fh:
        fh.write(content)
    print(f"wrote {os.path.relpath(full, HERE)} ({len(content)} bytes)")


def gen_vim_syntax(keys: list[str]) -> str:
    key_alt = vim_alt(keys)
    enum_alt = vim_alt(ENUM_VALUES)
    return f"""\
" otty.vim — Vim / Neovim syntax for the Otty terminal config.
" Target file: ~/.config/otty/config.toml (Ghostty-style `key = value`).
"
" GENERATED by resources/editors/generate.py from packages/config/src/lib.rs
" (Config::known_keys()). Do not edit by hand — re-run the generator instead.

if exists('b:current_syntax')
  finish
endif

syn case match

" Comments ------------------------------------------------------------------
syn keyword ottyTodo    contained TODO FIXME XXX NOTE HACK
syn match   ottyComment /#.*$/ contains=ottyTodo

" Values --------------------------------------------------------------------
syn keyword ottyBoolean true false
syn match   ottyNumber  /\\<\\d\\+\\%(\\.\\d\\+\\)\\?\\>/
syn match   ottyEscape  /\\\\./ contained
syn region  ottyString  start=/"/ skip=/\\\\"/ end=/"/ contains=ottyColor,ottyEscape
" Hex colors: #rgb / #rrggbb / #rrggbbaa. `containedin` forces them to light
" up inside the quoted color values (the common case); the bare match also
" covers the rare unquoted color.
syn match   ottyColor   /#\\x\\{{8}}\\|#\\x\\{{6}}\\|#\\x\\{{3}}/ containedin=ottyString
" Enum-like literal values on the value side.
syn match   ottyEnum    /\\<\\%({enum_alt}\\)\\>/

" The assignment separator.
syn match   ottyAssign  /=/

" Keys ----------------------------------------------------------------------
" Any `key =` at line start (covers palette-N, keybind, future keys).
syn match   ottyKey      /^\\s*\\zs[a-z][a-z0-9-]*\\ze\\s*=/
" Known options override the generic key color (last definition wins), so a
" typo'd key stays dim while real options stand out.
syn match   ottyKnownKey /^\\s*\\zs\\%({key_alt}\\)\\ze\\s*=/

hi def link ottyComment  Comment
hi def link ottyTodo     Todo
hi def link ottyBoolean  Boolean
hi def link ottyNumber   Number
hi def link ottyColor    Constant
hi def link ottyString   String
hi def link ottyEscape   SpecialChar
hi def link ottyEnum     Constant
hi def link ottyAssign   Operator
hi def link ottyKey      Identifier
hi def link ottyKnownKey Keyword

let b:current_syntax = 'otty'
"""


def gen_vim_ftdetect() -> str:
    return """\
" GENERATED by resources/editors/generate.py — Otty config filetype detection.
" The file is named config.toml but is Ghostty-style key=value, not real TOML,
" so we override TOML detection for the otty config path specifically.
augroup ottyConfig
  autocmd!
  autocmd BufRead,BufNewFile */otty/config,*/otty/config.toml,*/otty-debug/config,*/otty-debug/config.toml
        \\ setlocal filetype=otty
augroup END
"""


def gen_sublime(keys: list[str]) -> str:
    key_alt = re_alt(keys)
    enum_alt = re_alt(ENUM_VALUES)
    # Sublime-syntax YAML. `file_extensions: []` keeps bat from hijacking all
    # .toml files; activate via `--map-syntax` (see README).
    return f"""\
%YAML 1.2
---
# GENERATED by resources/editors/generate.py from packages/config/src/lib.rs.
# Sublime-syntax for `bat` — the Otty terminal config (Ghostty-style key=value).
# Do not edit by hand; re-run the generator instead.
name: Otty
scope: source.otty
version: 2
file_extensions: []

contexts:
  main:
    - match: '#.*$'
      scope: comment.line.number-sign.otty
    # Known options (highlighted before the generic key fallback).
    - match: '^\\s*({key_alt})\\s*(=)'
      captures:
        1: keyword.declaration.otty
        2: keyword.operator.assignment.otty
    # Any other `key =` (palette-N, keybind, future keys).
    - match: '^\\s*([a-z][a-z0-9-]*)\\s*(=)'
      captures:
        1: variable.other.otty
        2: keyword.operator.assignment.otty
    - match: '"'
      scope: punctuation.definition.string.begin.otty
      push: string
    - match: '#[0-9A-Fa-f]{{8}}\\b|#[0-9A-Fa-f]{{6}}\\b|#[0-9A-Fa-f]{{3}}\\b'
      scope: constant.other.color.rgb-value.otty
    - match: '\\b(true|false)\\b'
      scope: constant.language.boolean.otty
    - match: '\\b\\d+(\\.\\d+)?\\b'
      scope: constant.numeric.otty
    - match: '\\b({enum_alt})\\b'
      scope: constant.other.enum.otty

  string:
    - meta_scope: string.quoted.double.otty
    - match: '#[0-9A-Fa-f]{{3,8}}'
      scope: constant.other.color.rgb-value.otty
    - match: '\\\\.'
      scope: constant.character.escape.otty
    - match: '"'
      scope: punctuation.definition.string.end.otty
      pop: true
"""


def main() -> None:
    keys = parse_known_keys()
    print(f"parsed {len(keys)} config keys from {os.path.relpath(LIB_RS, HERE)}")
    write("vim/syntax/otty.vim", gen_vim_syntax(keys))
    write("vim/ftdetect/otty.vim", gen_vim_ftdetect())
    write("bat/otty.sublime-syntax", gen_sublime(keys))


if __name__ == "__main__":
    main()
