" autoload/klammertext.vim — implementation of the Klammertext commands.
"
" All the structure-aware work (indentation, table alignment, delimiter
" matching, diagnostics) is done by the shared Python core,
" klammertext_edit.py — the single implementation used by the Sublime,
" Vim, and VS Code integrations and by the language server.  This file is
" glue: it locates the core, runs it (as a stdin/stdout filter, or
" in-process via +python3 where that is faster), and applies the results
" to the buffer.
"
" Configuration:
"   g:klammertext_edit_py   full path to klammertext_edit.py (overrides the
"                           search described below)
"   g:klammertext_python    the Python interpreter (default "python3")

" --- locating the shared core ---------------------------------------------
" Search order: an explicit g:klammertext_edit_py; a vendored copy at this
" plugin's root (the layout the editing zip installs); ../shared relative
" to the plugin root (the repository layout); $KLAMMERTEXT_HOME.

let s:plugin_root = expand('<sfile>:p:h:h')

function! s:EditScript() abort
  if exists('g:klammertext_edit_py')
    return g:klammertext_edit_py
  endif
  for candidate in [
        \ s:plugin_root . '/klammertext_edit.py',
        \ fnamemodify(s:plugin_root, ':h') . '/shared/klammertext_edit.py',
        \ (empty($KLAMMERTEXT_HOME) ? '' :
        \  $KLAMMERTEXT_HOME . '/doc/edit/shared/klammertext_edit.py')]
    if !empty(candidate) && filereadable(candidate)
      return candidate
    endif
  endfor
  return ''
endfunction

function! s:Python() abort
  return exists('g:klammertext_python') ? g:klammertext_python : 'python3'
endfunction

" Run the core CLI over the whole buffer.  ARGS is the argument string
" (e.g. "indent 3-7").  Returns a dict {ok, lines, msg}: LINES is the
" transformed buffer (for the filter modes), MSG the stderr message.
function! s:RunCore(args) abort
  let script = s:EditScript()
  if empty(script)
    return {'ok': 0, 'lines': [], 'msg':
          \ 'Klammertext: cannot locate klammertext_edit.py'
          \ . ' (set g:klammertext_edit_py)'}
  endif
  let errfile = tempname()
  let cmd = s:Python() . ' ' . shellescape(script) . ' ' . a:args
        \ . ' 2>' . shellescape(errfile)
  let out = systemlist(cmd, getline(1, '$'))
  let msg = filereadable(errfile) ? join(readfile(errfile), ' ') : ''
  call delete(errfile)
  if v:shell_error
    return {'ok': 0, 'lines': [], 'msg':
          \ empty(msg) ? 'Klammertext: the shared core failed' : msg}
  endif
  return {'ok': 1, 'lines': out, 'msg': msg}
endfunction

" Replace the buffer with LINES, touching only the lines that changed (so
" undo stays small and the cursor does not move).  The transformations
" never add or remove lines; refuse if the count disagrees.
function! s:ApplyLines(lines) abort
  if len(a:lines) != line('$')
    echohl ErrorMsg
    echo 'Klammertext: unexpected line count from the shared core'
    echohl None
    return 0
  endif
  let changed = 0
  for i in range(1, line('$'))
    if getline(i) !=# a:lines[i - 1]
      call setline(i, a:lines[i - 1])
      let changed += 1
    endif
  endfor
  return changed
endfunction

function! s:CursorArgs() abort
  let col = exists('*charcol') ? charcol('.') : col('.')
  return line('.') . ' ' . col
endfunction

" --- the commands ----------------------------------------------------------

function! klammertext#Reindent(first, last) abort
  let r = s:RunCore('indent ' . a:first . '-' . a:last)
  if !r.ok
    echohl ErrorMsg | echo r.msg | echohl None
    return
  endif
  call s:ApplyLines(r.lines)
endfunction

function! klammertext#AlignTable() abort
  let r = s:RunCore('align ' . s:CursorArgs())
  if !r.ok
    echohl ErrorMsg | echo r.msg | echohl None
    return
  endif
  call s:ApplyLines(r.lines)
  if !empty(r.msg)
    echo 'Klammertext: ' . r.msg
  endif
endfunction

function! klammertext#JumpToMatch() abort
  let script = s:EditScript()
  if empty(script)
    echohl ErrorMsg
    echo 'Klammertext: cannot locate klammertext_edit.py'
    echohl None
    return
  endif
  let out = systemlist(s:Python() . ' ' . shellescape(script)
        \ . ' match ' . s:CursorArgs(), getline(1, '$'))
  if v:shell_error || empty(out)
    echohl ErrorMsg | echo 'Klammertext: the shared core failed' | echohl None
    return
  endif
  let parts = split(out[0], ' ')
  if parts[0] ==# 'none'
    echo 'Klammertext: ' . join(parts[1:], ' ')
    return
  endif
  " "match L C" or "mismatch L C message..."
  let lnum = str2nr(parts[1])
  let ccol = str2nr(parts[2])
  if exists('*setcursorcharpos')
    call setcursorcharpos(lnum, ccol)
  else
    call cursor(lnum, ccol)
  endif
  if parts[0] ==# 'mismatch'
    echohl WarningMsg
    echo 'Klammertext: ' . join(parts[3:], ' ')
    echohl None
  endif
endfunction

function! klammertext#Check() abort
  let script = s:EditScript()
  if empty(script)
    echohl ErrorMsg
    echo 'Klammertext: cannot locate klammertext_edit.py'
    echohl None
    return
  endif
  let out = systemlist(s:Python() . ' ' . shellescape(script) . ' check',
        \ getline(1, '$'))
  if v:shell_error
    echohl ErrorMsg | echo 'Klammertext: the shared core failed' | echohl None
    return
  endif
  let items = []
  for line in out
    let m = matchlist(line, '^\(\d\+\):\(\d\+\): \(.*\)$')
    if !empty(m)
      call add(items, {'bufnr': bufnr('%'), 'lnum': str2nr(m[1]),
            \ 'col': str2nr(m[2]), 'text': m[3], 'type': 'E'})
    endif
  endfor
  call setloclist(0, items, ' ')
  call setloclist(0, [], 'a', {'title': 'Klammertext delimiter check'})
  if empty(items)
    lclose
    echo 'Klammertext: no delimiter problems found'
  else
    lopen
  endif
endfunction

" --- +python3: in-process indentexpr and live match highlighting ----------
" These load the shared core into Vim's embedded Python once, so the =
" operator and the per-cursor-move matcher run without spawning processes.

let s:py_ready = 0

function! s:PySetup() abort
  if s:py_ready
    return 1
  endif
  let script = s:EditScript()
  if empty(script) || !has('python3')
    return 0
  endif
  let g:klammertext_py_dir = fnamemodify(script, ':h')
  py3 << EOF
import sys
import vim
_kt_dir = vim.eval('g:klammertext_py_dir')
if _kt_dir not in sys.path:
    sys.path.insert(0, _kt_dir)
import klammertext_edit as _kt

def _kt_buffer_and_offset():
    buf = vim.current.buffer
    lines = buf[:]
    s = '\n'.join(lines)
    row, bytecol = vim.current.window.cursor      # bytecol is 0-based bytes
    line = lines[row - 1] if row <= len(lines) else ''
    charcol = len(line.encode('utf-8')[:bytecol].decode('utf-8', 'replace'))
    offset = sum(len(l) + 1 for l in lines[:row - 1]) + charcol
    return s, lines, offset

def _kt_indent(lnum):
    buf = vim.current.buffer
    lines = buf[:]
    s = '\n'.join(lines)
    bol = sum(len(l) + 1 for l in lines[:lnum - 1])
    col = _kt.target_column(s, bol)
    return -1 if col is None else col

def _kt_pos(lines, offset):
    """(1-based line, 0-based char col, the line's text) for char OFFSET."""
    line = 0
    while line < len(lines) and offset > len(lines[line]):
        offset -= len(lines[line]) + 1
        line += 1
    text = lines[line] if line < len(lines) else ''
    return (line + 1, offset, text)

def _kt_match():
    """[] when not on a delimiter; else [mismatch, message,
    l1, c1, len1, (l2, c2, len2)?] with byte columns for matchaddpos()."""
    s, lines, offset = _kt_buffer_and_offset()
    m = _kt.match_at(s, offset)
    if m is None:
        return []
    out = [1 if m['mismatch'] else 0, m['message'] or '']
    for tok in [m['token'], m['match_token']]:
        if tok is None:
            continue
        line, ccol, text = _kt_pos(lines, tok[0])
        bcol = len(text[:ccol].encode('utf-8')) + 1
        blen = len(text[ccol:ccol + (tok[1] - tok[0])].encode('utf-8'))
        out.extend([line, bcol, blen])
    return out
EOF
  let s:py_ready = 1
  return 1
endfunction

function! klammertext#IndentExpr() abort
  if !s:PySetup()
    return -1
  endif
  return py3eval('_kt_indent(' . v:lnum . ')')
endfunction

function! s:ClearMatchHighlight() abort
  if exists('w:klammertext_match_ids')
    for id in w:klammertext_match_ids
      silent! call matchdelete(id)
    endfor
  endif
  let w:klammertext_match_ids = []
endfunction

function! s:UpdateMatchHighlight() abort
  call s:ClearMatchHighlight()
  let r = py3eval('_kt_match()')
  if empty(r)
    return
  endif
  let group = r[0] ? 'KlammertextMismatch' : 'KlammertextMatch'
  let pos = []
  let i = 2
  while i + 3 <= len(r)
    call add(pos, [r[i], r[i + 1], r[i + 2]])
    let i += 3
  endwhile
  if !empty(pos)
    call add(w:klammertext_match_ids, matchaddpos(group, pos))
  endif
  if r[0] && !empty(r[1])
    echo 'Klammertext: ' . r[1]
  endif
endfunction

function! klammertext#SetupMatchHighlight() abort
  if !s:PySetup()
    return
  endif
  hi def link KlammertextMatch MatchParen
  hi def KlammertextMismatch guifg=#ff5555 gui=bold ctermfg=203 cterm=bold
  augroup klammertextMatch
    autocmd! * <buffer>
    autocmd CursorMoved,CursorMovedI <buffer> call s:UpdateMatchHighlight()
    autocmd BufLeave,WinLeave <buffer> call s:ClearMatchHighlight()
  augroup END
endfunction
