# ================================
#   BLang - Linguagem do Bernardo
#   100% original, sem copyright
# ================================

TOKEN_NUMERO   = "NUMERO"
TOKEN_TEXTO    = "TEXTO"
TOKEN_BOOL     = "BOOL"
TOKEN_NOME     = "NOME"
TOKEN_OP       = "OP"
TOKEN_COMP     = "COMP"
TOKEN_IGUAL    = "IGUAL"
TOKEN_ABRE     = "ABRE"
TOKEN_FECHA    = "FECHA"
TOKEN_VIRGULA  = "VIRGULA"
TOKEN_FIM      = "FIM"

PALAVRAS_CHAVE = {"fala", "var", "se", "entao", "senao", "fim", "repete", "vezes", "funcao", "retorna", "e", "ou", "nao"}

def tokenizar(codigo: str) -> list:
    tokens = []
    i = 0
    while i < len(codigo):
        c = codigo[i]
        if c in " \t\r":
            i += 1
            continue
        if c == "#":
            while i < len(codigo) and codigo[i] != "\n":
                i += 1
            continue
        if c == "\n":
            if tokens and tokens[-1][0] != TOKEN_FIM:
                tokens.append((TOKEN_FIM, "\n"))
            i += 1
            continue
        if c.isdigit() or (c == "-" and i + 1 < len(codigo) and codigo[i+1].isdigit()):
            j = i + 1
            tem_ponto = False
            while j < len(codigo) and (codigo[j].isdigit() or (codigo[j] == "." and not tem_ponto)):
                if codigo[j] == ".":
                    tem_ponto = True
                j += 1
            val = float(codigo[i:j]) if tem_ponto else int(codigo[i:j])
            tokens.append((TOKEN_NUMERO, val))
            i = j
            continue
        if c == '"':
            j = i + 1
            while j < len(codigo) and codigo[j] != '"':
                j += 1
            tokens.append((TOKEN_TEXTO, codigo[i+1:j]))
            i = j + 1
            continue
        if c in "!<>=" and i + 1 < len(codigo) and codigo[i+1] == "=":
            tokens.append((TOKEN_COMP, c + "="))
            i += 2
            continue
        if c in "<>":
            tokens.append((TOKEN_COMP, c))
            i += 1
            continue
        if c == "=":
            tokens.append((TOKEN_IGUAL, "="))
            i += 1
            continue
        if c in "+-*/%":
            tokens.append((TOKEN_OP, c))
            i += 1
            continue
        if c == "(":
            tokens.append((TOKEN_ABRE, "("))
            i += 1
            continue
        if c == ")":
            tokens.append((TOKEN_FECHA, ")"))
            i += 1
            continue
        if c == ",":
            tokens.append((TOKEN_VIRGULA, ","))
            i += 1
            continue
        if c.isalpha() or c == "_":
            j = i
            while j < len(codigo) and (codigo[j].isalnum() or codigo[j] == "_"):
                j += 1
            palavra = codigo[i:j]
            if palavra in ("verdade", "mentira"):
                tokens.append((TOKEN_BOOL, palavra == "verdade"))
            elif palavra in PALAVRAS_CHAVE:
                tokens.append((palavra, palavra))
            else:
                tokens.append((TOKEN_NOME, palavra))
            i = j
            continue
        raise SyntaxError(f"[BLang] Caractere desconhecido: '{c}'")
    if tokens and tokens[-1][0] != TOKEN_FIM:
        tokens.append((TOKEN_FIM, "\n"))
    return tokens


class _Retorno(Exception):
    def __init__(self, valor):
        self.valor = valor


class BLang:
    def __init__(self):
        self.variaveis: dict = {}
        self.funcoes: dict = {}
        self.tokens: list = []
        self.pos: int = 0

    def atual(self):
        if self.pos < len(self.tokens):
            return self.tokens[self.pos]
        return (TOKEN_FIM, None)

    def consumir(self, tipo=None):
        tok = self.atual()
        if tipo and tok[0] != tipo:
            raise SyntaxError(f"[BLang] Esperava '{tipo}', recebi '{tok[0]}' ({tok[1]})")
        self.pos += 1
        return tok

    def pular_fins(self):
        while self.atual()[0] == TOKEN_FIM:
            self.pos += 1

    def acabou(self):
        return self.pos >= len(self.tokens)

    def executar(self, codigo: str):
        self.tokens = tokenizar(codigo)
        self.pos = 0
        resultado = None
        while not self.acabou():
            self.pular_fins()
            if self.acabou():
                break
            resultado = self.instrucao()
        return resultado

    def instrucao(self):
        tok = self.atual()
        if tok[0] == "fala":
            return self.cmd_fala()
        if tok[0] == "var":
            return self.cmd_var()
        if tok[0] == "se":
            return self.cmd_se()
        if tok[0] == "repete":
            return self.cmd_repete()
        if tok[0] == "funcao":
            return self.cmd_def_funcao()
        if tok[0] == "retorna":
            return self.cmd_retorna()
        if tok[0] == TOKEN_NOME:
            return self.cmd_nome()
        if tok[0] == TOKEN_FIM:
            self.consumir()
            return None
        raise SyntaxError(f"[BLang] Instrução desconhecida: '{tok[1]}'")

    def cmd_fala(self):
        self.consumir("fala")
        valor = self.expressao()
        self.consumir_fim()
        print(valor)
        return valor

    def cmd_var(self):
        self.consumir("var")
        nome = self.consumir(TOKEN_NOME)[1]
        self.consumir(TOKEN_IGUAL)
        valor = self.expressao()
        self.consumir_fim()
        self.variaveis[nome] = valor
        return valor

    def cmd_nome(self):
        nome = self.consumir(TOKEN_NOME)[1]
        if self.atual()[0] == TOKEN_IGUAL:
            self.consumir(TOKEN_IGUAL)
            valor = self.expressao()
            self.consumir_fim()
            if nome not in self.variaveis:
                raise NameError(f"[BLang] Variável '{nome}' não existe. Use 'var' primeiro.")
            self.variaveis[nome] = valor
            return valor
        if self.atual()[0] == TOKEN_ABRE:
            resultado = self.chamar_funcao(nome)
            self.consumir_fim()
            return resultado
        raise SyntaxError(f"[BLang] Uso inválido de '{nome}'")

    def cmd_se(self):
        self.consumir("se")
        condicao = self.expressao_logica()
        self.consumir("entao")
        self.consumir_fim_opcional()
        bloco_se = []
        bloco_senao = []
        em_senao = False
        while True:
            self.pular_fins()
            if self.atual()[0] == "senao":
                self.consumir("senao")
                self.consumir_fim_opcional()
                em_senao = True
                continue
            if self.atual()[0] == "fim":
                break
            linha = self.coletar_linha()
            if em_senao:
                bloco_senao.append(linha)
            else:
                bloco_se.append(linha)
        self.consumir("fim")
        self.consumir_fim()
        bloco = bloco_se if condicao else bloco_senao
        resultado = None
        for linha in bloco:
            resultado = self.executar_bloco(linha)
        return resultado

    def cmd_repete(self):
        self.consumir("repete")
        vezes = self.expressao()
        self.consumir("vezes")
        self.consumir_fim_opcional()
        linhas = []
        while True:
            self.pular_fins()
            if self.atual()[0] == "fim":
                break
            linhas.append(self.coletar_linha())
        self.consumir("fim")
        self.consumir_fim()
        resultado = None
        for _ in range(int(vezes)):
            for linha in linhas:
                resultado = self.executar_bloco(linha)
        return resultado

    def cmd_def_funcao(self):
        self.consumir("funcao")
        nome = self.consumir(TOKEN_NOME)[1]
        self.consumir(TOKEN_ABRE)
        params = []
        while self.atual()[0] != TOKEN_FECHA:
            params.append(self.consumir(TOKEN_NOME)[1])
            if self.atual()[0] == TOKEN_VIRGULA:
                self.consumir(TOKEN_VIRGULA)
        self.consumir(TOKEN_FECHA)
        self.consumir_fim_opcional()
        linhas = []
        while True:
            self.pular_fins()
            if self.atual()[0] == "fim":
                break
            linhas.append(self.coletar_linha())
        self.consumir("fim")
        self.consumir_fim()
        self.funcoes[nome] = (params, linhas)
        return None

    def cmd_retorna(self):
        self.consumir("retorna")
        valor = self.expressao()
        self.consumir_fim()
        raise _Retorno(valor)

    def expressao_logica(self):
        esq = self.expressao_comparacao()
        while self.atual()[0] in ("e", "ou"):
            op = self.consumir()[1]
            dir = self.expressao_comparacao()
            esq = esq and dir if op == "e" else esq or dir
        return esq

    def expressao_comparacao(self):
        if self.atual()[0] == "nao":
            self.consumir("nao")
            return not self.expressao_comparacao()
        esq = self.expressao()
        if self.atual()[0] == TOKEN_COMP:
            op = self.consumir(TOKEN_COMP)[1]
            dir = self.expressao()
            return self.comparar(esq, op, dir)
        return bool(esq)

    def comparar(self, a, op, b):
        if op == "==": return a == b
        if op == "!=": return a != b
        if op == "<":  return a < b
        if op == ">":  return a > b
        if op == "<=": return a <= b
        if op == ">=": return a >= b

    def expressao(self):
        esq = self.termo()
        while self.atual()[0] == TOKEN_OP and self.atual()[1] in "+-":
            op = self.consumir(TOKEN_OP)[1]
            dir = self.termo()
            if op == "+":
                esq = (str(esq) + str(dir)) if isinstance(esq, str) or isinstance(dir, str) else esq + dir
            else:
                esq = esq - dir
        return esq

    def termo(self):
        esq = self.fator()
        while self.atual()[0] == TOKEN_OP and self.atual()[1] in "*/%":
            op = self.consumir(TOKEN_OP)[1]
            dir = self.fator()
            if op == "*": esq = esq * dir
            elif op == "/":
                if dir == 0:
                    raise ZeroDivisionError("[BLang] Divisão por zero!")
                esq = esq / dir
            elif op == "%": esq = esq % dir
        return esq

    def fator(self):
        tok = self.atual()
        if tok[0] == TOKEN_NUMERO:
            self.consumir()
            return tok[1]
        if tok[0] == TOKEN_TEXTO:
            self.consumir()
            return tok[1]
        if tok[0] == TOKEN_BOOL:
            self.consumir()
            return tok[1]
        if tok[0] == TOKEN_ABRE:
            self.consumir(TOKEN_ABRE)
            val = self.expressao_logica()
            self.consumir(TOKEN_FECHA)
            return val
        if tok[0] == TOKEN_NOME:
            nome = self.consumir(TOKEN_NOME)[1]
            if self.atual()[0] == TOKEN_ABRE:
                return self.chamar_funcao(nome)
            if nome not in self.variaveis:
                raise NameError(f"[BLang] Variável '{nome}' não existe.")
            return self.variaveis[nome]
        if tok[0] == TOKEN_OP and tok[1] == "-":
            self.consumir()
            return -self.fator()
        raise SyntaxError(f"[BLang] Valor inválido: '{tok[1]}'")

    def chamar_funcao(self, nome):
        if nome == "tipo":
            self.consumir(TOKEN_ABRE)
            val = self.expressao()
            self.consumir(TOKEN_FECHA)
            return type(val).__name__
        if nome == "num":
            self.consumir(TOKEN_ABRE)
            val = self.expressao()
            self.consumir(TOKEN_FECHA)
            try: return int(val)
            except: return float(val)
        if nome == "texto":
            self.consumir(TOKEN_ABRE)
            val = self.expressao()
            self.consumir(TOKEN_FECHA)
            return str(val)
        if nome == "tamanho":
            self.consumir(TOKEN_ABRE)
            val = self.expressao()
            self.consumir(TOKEN_FECHA)
            return len(str(val))
        if nome not in self.funcoes:
            raise NameError(f"[BLang] Função '{nome}' não existe.")
        params, linhas = self.funcoes[nome]
        self.consumir(TOKEN_ABRE)
        args = []
        while self.atual()[0] != TOKEN_FECHA:
            args.append(self.expressao())
            if self.atual()[0] == TOKEN_VIRGULA:
                self.consumir(TOKEN_VIRGULA)
        self.consumir(TOKEN_FECHA)
        if len(args) != len(params):
            raise TypeError(f"[BLang] '{nome}' espera {len(params)} argumento(s), recebeu {len(args)}.")
        vars_antes = self.variaveis.copy()
        for p, a in zip(params, args):
            self.variaveis[p] = a
        resultado = None
        try:
            for linha in linhas:
                self.executar_bloco(linha)
        except _Retorno as r:
            resultado = r.valor
        self.variaveis = vars_antes
        return resultado

    def coletar_linha(self) -> list:
        inicio = self.pos
        while self.pos < len(self.tokens) and self.tokens[self.pos][0] != TOKEN_FIM:
            self.pos += 1
        if self.pos < len(self.tokens):
            self.pos += 1
        return self.tokens[inicio:self.pos]

    def executar_bloco(self, tokens_linha: list):
        salvo_tokens = self.tokens
        salvo_pos = self.pos
        self.tokens = tokens_linha
        self.pos = 0
        resultado = None
        try:
            while not self.acabou():
                self.pular_fins()
                if self.acabou(): break
                resultado = self.instrucao()
        finally:
            self.tokens = salvo_tokens
            self.pos = salvo_pos
        return resultado

    def consumir_fim(self):
        if self.atual()[0] == TOKEN_FIM:
            self.consumir()

    def consumir_fim_opcional(self):
        if self.atual()[0] == TOKEN_FIM:
            self.consumir()
