"""
Sophi Lexer
Tokenises .sph source files into a flat list of tokens.
"""

import re
from dataclasses import dataclass
from typing import List

# ── Token types ────────────────────────────────────────────────────────────────

KEYWORDS = {
    'swap', 'wants', 'gives', 'blind', 'partial', 'expires',
    'block', 'timestamp', 'btc', 'usdt', 'eth', 'usd',
}

@dataclass
class Token:
    type: str   # KEYWORD | IDENT | NUMBER | HASH | LPAREN | RPAREN | LBRACE | RBRACE | NEWLINE | EOF
    value: str
    line: int

# ── Lexer ──────────────────────────────────────────────────────────────────────

TOKEN_PATTERNS = [
    ('COMMENT',  r'//[^\n]*'),
    ('NUMBER',   r'\d+(\.\d+)?'),
    ('HASH',     r'0x[0-9a-fA-F]{64}|[0-9a-fA-F]{64}'),
    ('IDENT',    r'[a-zA-Z_][a-zA-Z0-9_]*'),
    ('LPAREN',   r'\('),
    ('RPAREN',   r'\)'),
    ('LBRACE',   r'\{'),
    ('RBRACE',   r'\}'),
    ('NEWLINE',  r'\n'),
    ('SKIP',     r'[ \t\r]+'),
    ('MISMATCH', r'.'),
]

MASTER_PATTERN = re.compile(
    '|'.join(f'(?P<{name}>{pat})' for name, pat in TOKEN_PATTERNS)
)

class LexError(Exception):
    pass

def tokenise(source: str) -> List[Token]:
    tokens = []
    line = 1
    for mo in MASTER_PATTERN.finditer(source):
        kind = mo.lastgroup
        value = mo.group()

        if kind == 'COMMENT' or kind == 'SKIP':
            pass
        elif kind == 'NEWLINE':
            line += 1
        elif kind == 'MISMATCH':
            raise LexError(f"Unexpected character {value!r} on line {line}")
        else:
            # Normalise keywords to lowercase
            if kind == 'IDENT' and value.lower() in KEYWORDS:
                kind = 'KEYWORD'
                value = value.lower()
            tokens.append(Token(kind, value, line))

    tokens.append(Token('EOF', '', line))
    return tokens
