/**
 * CodeMirror 6 language extension for CalcPad syntax highlighting.
 * Adapted from epic/9-2-codemirror-6-editor.
 *
 * Uses StreamLanguage to tokenize CalcPad input and map token types
 * to CodeMirror highlight tags.
 */

import { StreamLanguage, type StringStream, LanguageSupport } from '@codemirror/language'
import { tags, type Tag } from '@lezer/highlight'

const KEYWORDS = new Set([
  'if', 'then', 'else',
  'in', 'to', 'as', 'of', 'per',
  'sum', 'total', 'average', 'avg', 'min', 'max', 'count',
])

const BUILTIN_FUNCTIONS = new Set([
  'sin', 'cos', 'tan', 'asin', 'acos', 'atan',
  'log', 'ln', 'exp', 'sqrt', 'abs',
  'floor', 'ceil', 'round',
])

const CONSTANTS = new Set(['pi', 'e'])

export interface CalcPadState {
  inComment: boolean
}

export const calcpadStreamParser = {
  startState(): CalcPadState {
    return { inComment: false }
  },

  token(stream: StringStream, _state: CalcPadState): string | null {
    // Start of line: check for heading
    if (stream.sol() && stream.match(/^#{1,6}\s/)) {
      stream.skipToEnd()
      return 'heading'
    }

    // Comment
    if (stream.match('//')) {
      stream.skipToEnd()
      return 'lineComment'
    }

    // Skip whitespace
    if (stream.eatSpace()) return null

    // Line reference: #N
    if (stream.match(/^#\d+/)) {
      return 'special'
    }

    // Line reference: lineN
    if (stream.match(/^line\d+/i)) {
      return 'special'
    }

    // Number (integer or decimal, with optional scientific notation)
    if (stream.match(/^\d+(\.\d+)?([eE][+-]?\d+)?/)) {
      return 'number'
    }

    // Assignment operator
    if (stream.eat('=')) {
      return 'definitionOperator'
    }

    // Operators
    if (stream.match(/^[+\-*\/^%]/)) {
      return 'operator'
    }

    // Comparison operators
    if (stream.match(/^[<>]=?/) || stream.match(/^[!=]=/)) {
      return 'operator'
    }

    // Parentheses
    if (stream.eat('(') || stream.eat(')')) {
      return 'paren'
    }

    // Comma
    if (stream.eat(',')) {
      return 'punctuation'
    }

    // Currency symbols
    if (stream.eat('$') || stream.eat('\u20AC') || stream.eat('\u00A3') || stream.eat('\u00A5')) {
      return 'keyword'
    }

    // Identifier, keyword, or function
    const identMatch = stream.match(/^[a-zA-Z_][a-zA-Z0-9_]*/)
    if (identMatch) {
      const word = typeof identMatch === 'string' ? identMatch : (identMatch as RegExpMatchArray)[0]
      const lower = word.toLowerCase()

      if (BUILTIN_FUNCTIONS.has(lower)) {
        return 'function'
      }
      if (CONSTANTS.has(lower)) {
        return 'constant'
      }
      if (KEYWORDS.has(lower)) {
        return 'keyword'
      }
      return 'variableName'
    }

    // Skip unknown character
    stream.next()
    return null
  },
}

export const calcpadStreamLanguage = StreamLanguage.define<CalcPadState>(calcpadStreamParser)

/**
 * Tag mapping for CalcPad token types to CodeMirror highlight tags.
 */
export const calcpadHighlightTags: Record<string, Tag> = {
  number: tags.number,
  operator: tags.operator,
  variableName: tags.variableName,
  function: tags.function(tags.variableName),
  keyword: tags.keyword,
  lineComment: tags.lineComment,
  heading: tags.heading,
  definitionOperator: tags.definitionOperator,
  special: tags.special(tags.variableName),
  constant: tags.constant(tags.variableName),
  paren: tags.paren,
  punctuation: tags.punctuation,
}

/**
 * Creates a CalcPad language extension for CodeMirror 6.
 */
export function calcpadLanguage(): LanguageSupport {
  return new LanguageSupport(calcpadStreamLanguage)
}
