"""
Neuro Language — Lexer v0.1

Responsável por transformar código-fonte `.nl` em uma sequência de tokens,
conforme especificado em:

- SPEC_SYNTAX.md
- frontend/tokens.md
- frontend/grammar.md

Características principais:

- Sensível à indentação (INDENT / DEDENT) no estilo Python.
- Ignora linhas em branco e comentários iniciados por `#`.
- Suporta:
  - palavras-chave: def, se, entao, senao
  - inteiros (decimais)
  - identificadores (nomes de funções / variáveis)
  - operadores: + - * / < <= > >= == !=
  - parênteses e vírgula
  - NEWLINE e EOF
"""

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from typing import Iterator, List, Optional

from neurotron.lang.frontend.tokens import TokenType, Token, KEYWORDS


class LexerError(Exception):
    """Erro de análise léxica (caractere inesperado, indentação inconsistente, etc.)."""


class Lexer:
    """
    Lexer para a Neuro Langage v0.1.

    Uso típico:

        lexer = Lexer(source_code)
        tokens = list(lexer.tokenize())
    """

    def __init__(self, source: str) -> None:
        # Normalizamos quebras de linha para '\n'
        self._source = source.replace("\r\n", "\n").replace("\r", "\n")
        self._lines = self._source.split("\n")

        # Pilha de níveis de indentação (em colunas)
        # Começa em 0 (sem indentação)
        self._indent_stack: List[int] = [0]

    # -------------------------------------------------------------------------
    # Interface principal
    # -------------------------------------------------------------------------

    def tokenize(self) -> Iterator[Token]:
        """
        Gera tokens a partir do código-fonte.

        Implementa um modelo parecido com Python:
        - INDENT / DEDENT calculados por linha
        - NEWLINE emitido ao final de linhas não vazias
        - Em EOF, fecha todos os DEDENT pendentes e emite EOF
        """
        line_no = 0
        for line_no, raw_line in enumerate(self._lines, start=1):
            # Remove apenas quebras de linha à direita, preservando indentação à esquerda
            line = raw_line.rstrip("\n")

            # Se a linha é vazia ou só tem comentário, ignoramos completamente:
            stripped = line.lstrip()
            if stripped == "" or stripped.startswith("#"):
                # Não gera NEWLINE nem mexe em indentação
                continue

            # 1) Calcula indentação
            indent_col, content_start = self._compute_indent(line)
            yield from self._emit_indent_dedent(indent_col, line_no)

            # 2) Tokeniza o conteúdo da linha
            pos = content_start
            length = len(line)
            while pos < length:
                ch = line[pos]
                col = pos + 1

                # Espaços internos (não iniciais) são ignorados
                if ch in " \t":
                    pos += 1
                    continue

                # Comentário inline: ignora o resto da linha
                if ch == "#":
                    break

                # Números inteiros
                if ch.isdigit():
                    start = pos
                    while pos < length and line[pos].isdigit():
                        pos += 1
                    value_text = line[start:pos]
                    value = int(value_text, 10)
                    yield Token(TokenType.INT, value, line_no, start + 1)
                    continue

                # Identificadores / palavras-chave
                if ch.isalpha() or ch == "_":
                    start = pos
                    while pos < length and (line[pos].isalnum() or line[pos] == "_"):
                        pos += 1
                    ident = line[start:pos]
                    ttype = KEYWORDS.get(ident, TokenType.IDENT)
                    # Para keywords, podemos guardar o lexema ou None; aqui guardamos o texto
                    yield Token(ttype, ident, line_no, start + 1)
                    continue

                # Operadores de dois caracteres
                two = line[pos : pos + 2]
                if two in ("<=", ">=", "==", "!="):
                    op_type = {
                        "<=": TokenType.LE,
                        ">=": TokenType.GE,
                        "==": TokenType.EQ,
                        "!=": TokenType.NE,
                    }[two]
                    yield Token(op_type, two, line_no, col)
                    pos += 2
                    continue

                # Símbolos de um caractere
                single = {
                    "+": TokenType.PLUS,
                    "-": TokenType.MINUS,
                    "*": TokenType.STAR,
                    "/": TokenType.SLASH,
                    "<": TokenType.LT,
                    ">": TokenType.GT,
                    "(": TokenType.LPAREN,
                    ")": TokenType.RPAREN,
                    ",": TokenType.COMMA,
                }

                if ch in single:
                    yield Token(single[ch], ch, line_no, col)
                    pos += 1
                    continue

                # Qualquer outro caractere é considerado erro
                raise LexerError(
                    f"Caractere inesperado {ch!r} na linha {line_no}, coluna {col}"
                )

            # 3) Ao fim de uma linha "lógica", emitimos NEWLINE
            yield Token(TokenType.NEWLINE, None, line_no, len(line) + 1)

        # Após todas as linhas, fechar indentação aberta
        final_line = max(line_no, 1)
        while len(self._indent_stack) > 1:
            self._indent_stack.pop()
            yield Token(TokenType.DEDENT, None, final_line + 1, 1)

        yield Token(TokenType.EOF, None, final_line + 1, 1)

    # -------------------------------------------------------------------------
    # Helpers de indentação
    # -------------------------------------------------------------------------

    @staticmethod
    def _count_indent_cols(prefix: str) -> int:
        """
        Converte o prefixo de indentação (espaços/tabs) em contagem de colunas.

        Por simplicidade:
        - ' ' conta como 1 coluna
        - '\t' conta como 4 colunas

        Futuro: poderemos tornar isto configurável.
        """
        cols = 0
        for ch in prefix:
            if ch == " ":
                cols += 1
            elif ch == "\t":
                cols += 4
            else:
                # Não deveria acontecer; só chamamos isto com prefixo de whitespace
                break
        return cols

    def _compute_indent(self, line: str) -> tuple[int, int]:
        """
        Devolve (indent_cols, content_start_index).

        - indent_cols: número de colunas de indentação (após converter espaços/tabs)
        - content_start_index: índice em 'line' do primeiro caractere não espaço/tab
        """
        idx = 0
        length = len(line)
        while idx < length and line[idx] in (" ", "\t"):
            idx += 1
        indent_cols = self._count_indent_cols(line[:idx])
        return indent_cols, idx

    def _emit_indent_dedent(self, indent_cols: int, line_no: int) -> Iterator[Token]:
        """
        Compara indent_cols com o topo da pilha de indentação e gera
        INDENT/DEDENT conforme necessário.
        """
        current = self._indent_stack[-1]

        if indent_cols > current:
            self._indent_stack.append(indent_cols)
            yield Token(TokenType.INDENT, None, line_no, 1)
        elif indent_cols < current:
            # DEDENT até atingir o nível correspondente
            while len(self._indent_stack) > 1 and indent_cols < self._indent_stack[-1]:
                self._indent_stack.pop()
                yield Token(TokenType.DEDENT, None, line_no, 1)

            if indent_cols != self._indent_stack[-1]:
                # Indentação incompatível com níveis anteriores
                raise LexerError(
                    f"Indentação inconsistente na linha {line_no}: "
                    f"colunas={indent_cols}, esperado um de {self._indent_stack}"
                )


# -------------------------------------------------------------------------
# Função utilitária de alto nível
# -------------------------------------------------------------------------


def tokenize(source: str) -> List[Token]:
    """
    Atalho conveniente para tokenizar uma string.

    Exemplo:

        from neurotron.lang.frontend.lexer.lexer import tokenize

        code = \"\"\"
        def fib(x)
          se x < 3 entao
            1
          senao
            fib(x - 1) + fib(x - 2)

        fib(40)
        \"\"\"

        tokens = tokenize(code)
    """
    return list(Lexer(source).tokenize())


__all__ = ["TokenType", "Token", "Lexer", "LexerError", "tokenize"]
