# -*- coding: utf-8 -*-
# Engine Shassid v2 — segmentos clicaveis, delete por marcacao, enter fecha frase,
# barra de espaco = zerador da funcao ativa. Para testes internos.
import json
from openpyxl import load_workbook

LETRAS=['A','O','I','D','C','B','H','N','X','T']
NUM={'A':'1','O':'2','I':'3','D':'4','C':'5','B':'6','H':'7','N':'8','X':'9','T':'0'}
ABRE={'[':']','{':'}','<':'>'}
CORTE_EXC={'CT':'Ve'}  # excecoes confirmadas
# Simbolos matematicos: rotulo -> caractere na fonte CID CXI
SIMBOLOS=[('+','W'),('-','Q'),('x','w'),(':','q'),('=','Z')]
SIM_CHAR={lab:ch for lab,ch in SIMBOLOS}

def forma_corte(par,duas):
    """Forma transliterada de um corte '/' sobre um par de 2 letras XY."""
    if par in CORTE_EXC: return CORTE_EXC[par]
    if len(par)!=2: return None
    a,b=par[0],par[1]; base=duas.get(par)
    if base is None: return None
    pref=base[:-1]
    if b in ('A','B','X'): return pref+b+'J'
    if b in ('D','H','N','O','I'): return pref+b+'K'
    if b=='C': return pref+'V'
    if b=='T': return pref+('e' if a in ('O','D','H','N','I','B') else 'E')
    return pref+b

def carregar(caminho):
    wb=load_workbook(caminho,data_only=True); duas={}; tres={}
    for nome in wb.sheetnames:
        for code,trans in wb[nome].iter_rows(min_row=2,max_col=2,values_only=True):
            if trans:(duas if len(code)==2 else tres)[code]=trans
    return duas,tres

class Teclado:
    def __init__(self,duas,tres): self.duas,self.tres=duas,tres; self.reset()
    def reset(self):
        self.segs=[]      # cada seg: {'t':tipo,'v':valor,'code':codigo?}  tipos: open close word ono num paren nl
        self.pend=''; self.num=False; self.frase=None; self.paren=False
        self.word_in=False; self.marked=None; self.hist=[]; self.log=[]
        self.cleared=None  # snapshot do ultimo apagar-tudo, para restaurar via barra longa
        self.sp_armado=False  # espaco sem frase aguardando proximo toque (Forma B)
        self.sem_chave=False  # modo de frase sem chave A1
    # --- historico p/ desfazer ---
    def _snap(self): return json.dumps({'s':self.segs,'p':self.pend,'n':self.num,'f':self.frase,'pr':self.paren,'w':self.word_in,'sc':self.sem_chave})
    def _push(self): self.hist.append(self._snap())
    def _undo(self):
        if not self.hist: return False
        d=json.loads(self.hist.pop())
        self.segs=d['s'];self.pend=d['p'];self.num=d['n'];self.frase=d['f'];self.paren=d['pr'];self.word_in=d['w'];self.sem_chave=d.get('sc',False);self.marked=None
        return True
    # --- acoes ---
    def _aberta(self):  # ha frase 'ativa' (com chave A1 ou modo sem-chave)
        return self.frase is not None or self.sem_chave
    def letra(self,L):
        if not self._aberta() and not self.num: self.log.append("ignorado: letra sem frase aberta"); return
        if self.num:
            if self.segs and self.segs[-1]['t']=='num': self.segs[-1]['v']+=NUM[L]
            else: self.segs.append({'t':'num','v':NUM[L]})
            return
        self.pend+=L
        if len(self.pend)==3:
            self.segs.append({'t':'word','v':self.tres.get(self.pend,'?'+self.pend+'?'),'code':self.pend})
            self.word_in=True; self.pend=''
    def chave(self,k):
        if self.sem_chave and self.frase is None:
            # dentro do modo sem-chave A1: abrir uma chave A2/A3 e' uma sub-frase normal (com espaco antes se ja ha palavra)
            self.segs.append({'t':'open','v':k}); self.frase=k; self.paren=False; self.word_in=False; return
        if self.frase is None:
            self.segs.append({'t':'open','v':k}); self.frase=k; self.paren=False; self.word_in=False
        elif k==self.frase:
            if self.paren: self.segs.append({'t':'paren','v':')'}); self.paren=False
            self.segs.append({'t':'close','v':ABRE[self.frase]}); self.segs.append({'t':'open','v':k}); self.word_in=False
        else:
            if self.paren: self.segs.append({'t':'paren','v':')'}); self.paren=False
            self.segs.append({'t':'close','v':ABRE[k]}); self.frase=None
    def parentese(self):
        if not self._aberta(): self.log.append("ignorado: ( sem frase"); return
        if not self.word_in and not self.paren: self.log.append("ignorado: nunca [("); return
        if not self.paren: self.segs.append({'t':'paren','v':'('}); self.paren=True
        else: self.segs.append({'t':'paren','v':')('})
    def espaco(self):
        if self.marked is not None: self.marked=None; return          # zera marcacao -> cursor ao fim
        if self.num: self.num=False; return                           # zera modo numero
        if len(self.pend)==2:                                         # 2 letras = onomatopeia
            self.segs.append({'t':'ono','v':self.duas.get(self.pend,'?'+self.pend+'?'),'code':self.pend}); self.word_in=True; self.pend=''; return
        if len(self.pend)==1:                                        # 1 letra + barra = letra latina solta
            self.segs.append({'t':'latin','v':self.pend,'code':self.pend}); self.word_in=True; self.pend=''; return
        # pend == 0
        if self.frase is not None:                                    # frase com chave aberta -> fecha
            if self.paren: self.segs.append({'t':'paren','v':')'}); self.paren=False
            self.segs.append({'t':'close','v':ABRE[self.frase]}); self.frase=None; return
        if self.sem_chave:                                           # modo sem-chave -> encerra a frase sem chave
            if self.paren: self.segs.append({'t':'paren','v':')'}); self.paren=False
            self.sem_chave=False; return
        # sem frase nenhuma: ARMA o espaco (Forma B), resolve no proximo toque
        self.sp_armado=True
    def _resolver_armado(self):
        # chamado antes de processar um toque que NAO seja enter, quando havia espaco armado
        self.sp_armado=False
        if any(s['t']=='close' for s in self.segs):
            self.segs.append({'t':'lspace','v':' '})   # espaco literal so se ja houve frase fechada
    def enter(self):
        if self.sp_armado:                                          # gatilho: espaco armado + enter = modo sem-chave
            self.sp_armado=False; self.sem_chave=True; return
        if self.frase is not None:
            if self.paren: self.segs.append({'t':'paren','v':')'}); self.paren=False
            self.segs.append({'t':'close','v':ABRE[self.frase]}); self.frase=None
        elif self.sem_chave:                                        # enter encerra modo sem-chave e pula linha
            if self.paren: self.segs.append({'t':'paren','v':')'}); self.paren=False
            self.sem_chave=False
        self.segs.append({'t':'nl','v':'\n'})
    def corta(self):
        # '/' encerra a frase substituindo qualquer chave/parentese final por '/'.
        if not self._aberta(): self.log.append("ignorado: / sem frase"); return
        if len(self.pend)==2:
            fc=forma_corte(self.pend,self.duas)
            self.segs.append({'t':'cut','v':(fc if fc else '?'+self.pend+'/?'),'code':self.pend})
            self.word_in=True
        self.paren=False; self.pend=''
        self.segs.append({'t':'close','v':'\\'}); self.frase=None; self.sem_chave=False  # '\' fecha (com ou sem chave A1)
    def abrir_simbolos(self):
        if not self._aberta(): self.log.append("ignorado: simbolos sem frase"); return
        self.num=True
    def simbolo(self,lab):
        if not self._aberta(): self.log.append("ignorado: simbolo sem frase"); return
        ch=SIM_CHAR.get(lab)
        if ch is None: self.log.append("simbolo desconhecido: "+str(lab)); return
        self.segs.append({'t':'sym','v':ch,'lab':lab}); self.num=True; self.need_space=False
    def delete(self):
        if self.marked is not None:            self._push(); del self.segs[self.marked]; self.marked=None; return
        self._undo()
    def limpar_tudo(self):
        # toque-longo no delete: apaga tudo da tela e zera o estado; guarda para restaurar
        if not self.segs and self.frase is None and not self.pend:
            self.log.append("ignorado: nada para limpar"); return
        self.cleared=self._snap()
        self.segs=[]; self.pend=''; self.num=False; self.frase=None; self.paren=False; self.word_in=False; self.marked=None
    def restaurar(self):
        # toque-longo na barra: restaura o ultimo apagar-tudo
        if not self.cleared: self.log.append("ignorado: nada para restaurar"); return
        d=json.loads(self.cleared)
        self.segs=d['s']; self.pend=d['p']; self.num=d['n']; self.frase=d['f']; self.paren=d['pr']; self.word_in=d['w']; self.marked=None
        self.cleared=None
    def numero(self):
        if not self._aberta(): self.log.append("ignorado: numero sem frase"); return
        self.num=not self.num
    def click(self,i):
        if 0<=i<len(self.segs) and self.segs[i]['t'] in ('word','ono','num','latin'): self.marked=i
    # --- render ---
    def texto(self):
        WORDLIKE=('word','ono','num','latin','cut')
        out=[]; prev=None
        for s in self.segs:
            sp=False
            if prev is not None:
                if prev['t']=='close' and prev['v']=='\\':
                    sp=True                                 # '\' (fechamento de corte) sempre deixa espaco antes do proximo
                elif s['t'] in WORDLIKE:
                    if prev['t']=='open': sp=False
                    elif prev['t']=='paren' and prev['v'].endswith('('): sp=False
                    elif prev['t']=='nl': sp=False
                    elif prev['t']=='sym': sp=False        # numero colado ao simbolo (2Z4)
                    elif s['t']=='num' and prev['t']=='num': sp=False  # digitos seguidos
                    else: sp=True
                elif s['t']=='paren' and s['v']=='(':
                    sp = prev['t'] in WORDLIKE
                elif s['t']=='open':
                    sp = prev['t'] in WORDLIKE   # chave A2/A3 em modo sem-chave: espaco antes se ja ha palavra
            out.append((' ' if sp else '')+s['v']); prev=s
        return ''.join(out)
    def digitar(self,seq):
        for t in seq:
            # resolve espaco armado (Forma B)
            if self.sp_armado and t=='SP':
                self._resolver_armado(); self.sp_armado=True; continue   # varios espacos literais
            if self.sp_armado and t!='ENT':
                self._resolver_armado()
            if t in LETRAS: self._push(); self.letra(t)
            elif t in ABRE: self._push(); self.chave(t)
            elif t=='(': self._push(); self.parentese()
            elif t=='SP': self._push(); self.espaco()
            elif t=='NUM': self._push(); self.numero()
            elif t=='ENT': self._push(); self.enter()
            elif t=='CUT': self._push(); self.corta()
            elif t=='SIMOPEN': self._push(); self.abrir_simbolos()
            elif isinstance(t,tuple) and t[0]=='SYM': self._push(); self.simbolo(t[1])
            elif isinstance(t,tuple) and t[0]=='CLICK': self.click(t[1])
            elif t=='DEL': self.delete()
            elif t=='CLEARALL': self.limpar_tudo()
            elif t=='RESTORE': self.restaurar()
        return self.texto()

DUAS,TRES=carregar("/mnt/user-data/uploads/Shassid_Palavras_1000.xlsx")
def t(label,seq,esp=None):
    kb=Teclado(DUAS,TRES); out=kb.digitar(seq)
    ok="" if esp is None else (" \u2705" if out==esp else f" \u274c (esp: {esp!r})")
    extra=("  | "+"; ".join(kb.log)) if kb.log else ""
    seqs=' '.join(str(x) for x in seq)
    print(f"  {label}\n     {seqs}\n     -> {out!r}{ok}{extra}")

print(f"Dados: {len(DUAS)} onomatopeias, {len(TRES)} palavras\n=== TESTES v2 ===\n")
t("1) palavra + fecha", ['[','A','A','A','SP'], "[AJAJA]")
t("2) duas palavras + fecha", ['[','A','A','A','A','O','A','SP'], "[AJAJA AjOkA]")
t("3) onomatopeia + fecha", ['<','A','A','SP','SP'], "<AJA>")
t("4) mesma chave fecha-e-reabre", ['[','A','A','A','['], "[AJAJA][")
t("5) chave diferente so fecha", ['[','A','A','A','<'], "[AJAJA>")
t("6) parenteses", ['[','A','A','A','(','A','O','A','SP'], "[AJAJA (AjOkA)]")
t("7) )( duplo", ['[','A','A','A','(','A','O','A','(','A','O','O','SP'], "[AJAJA (AjOkA)(AjOKO)]")
t("8) modo numero", ['[','NUM','A','O','I','SP','SP'], "[123]")
print("  --- NOVO v2 ---")
t("9) ENTER fecha frase + quebra linha", ['[','A','A','A','ENT'], "[AJAJA]\n")
t("10) ENTER com frase ja fechada", ['[','A','A','A','SP','ENT'], "[AJAJA]\n")
t("11) DELETE = desfaz ultimo toque (3a letra)", ['[','A','A','A','DEL'], "[")
t("12) clicar palavra do meio + DELETE remove ela", ['[','A','A','A','A','O','A','A','O','O',('CLICK',2),'DEL','SP'], "[AJAJA AjOKO]")
t("13) barra zera marcacao (cursor ao fim), nao fecha", ['[','A','A','A',('CLICK',1),'SP'], "[AJAJA")
print("  --- NOVO v3 ---")
t("14) letra sem frase = ignorada", ['A','A','A'], "")
t("15) parentese/espaco/num sem frase = ignorados", ['(','SP','NUM','A'], "")
t("16) 1 letra + barra = letra latina solta + espaco posterior", ['[','A','SP','A','A','A','SP'], "[A AJAJA]")
t("17) 1 letra + barra, depois fecha", ['[','A','SP','SP'], "[A]")
print("  --- NOVO v4: tecla / (corte, fecha com \\) ---")
t("18) / corta no meio: AA -> AJAJ\\", ['[','A','A','CUT'], "[AJAJ\\")
t("19) / corta no meio: AO -> AjOK\\", ['[','A','O','CUT'], "[AjOK\\")
t("20) / corta no meio: CT -> Ve\\ (excecao)", ['[','C','T','CUT'], "[Ve\\")
t("21) / fecha frase substituindo )]", ['[','A','A','A','(','A','O','A','CUT'], "[AJAJA (AjOkA\\")
t("22) / sem nada pendente fecha com \\", ['[','A','A','A','CUT'], "[AJAJA\\")
t("23) chave depois de / inicia nova frase (com espaco apos \\)", ['[','A','A','CUT','[','A','A','A','SP'], "[AJAJ\\ [AJAJA]")
print("  --- NOVO v5: simbolos matematicos (E1) ---")
t("24) abrir simbolos liga modo numero: 2=4", ['[','SIMOPEN','O',('SYM','='),'D','SP','SP'], "[2Z4]")
t("25) 2+2=4", ['[','SIMOPEN','O',('SYM','+'),'O',('SYM','='),'D','SP','SP'], "[2W2Z4]")
t("26) simbolo sem frase = ignorado", [('SYM','='),'SIMOPEN'], "")
print("  --- NOVO v6: espaco literal entre frases ---")
t("27) espaco literal apos frase fechada", ['[','A','A','A','SP','SP','[','A','A','A','SP'], "[AJAJA] [AJAJA]")
t("28) espacos literais: SP arma, proximos materializam", ['[','A','A','A','SP','SP','SP','SP','[','A','A','A','SP'], "[AJAJA]   [AJAJA]")
t("29) barra na tela vazia = ignorada", ['SP','SP'], "")
print("  --- NOVO v7: delete longo (limpar tudo) + barra longa (restaurar) ---")
t("30) delete longo limpa tudo", ['[','A','A','A','SP','CLEARALL'], "")
t("31) barra longa restaura o limpo", ['[','A','A','A','SP','CLEARALL','RESTORE'], "[AJAJA]")
t("32) restaurar sem ter limpado = ignorado", ['[','A','A','A','RESTORE'], "[AJAJA")
print("  --- NOVO v8: frase sem chave A1 (gatilho SP+ENT) ---")
t("33) SP+ENT abre modo sem-chave; palavras soltas", ['SP','ENT','A','O','A','A','A','A','SP'], "AjOkA AJAJA")
t("34) sem-chave + chave A2 (com espaco antes)", ['SP','ENT','A','O','B','C','B','A','{','N','I','D','C','C','B','SP'], "AjOKB vBLA {NKIf vvB}")
t("35) SP+ENT so vale sem frase aberta (no meio nao)", ['[','A','A','SP','ENT'], "[AJA]\n")
t("36) sem-chave: palavras com auto-espaco, SP fecha", ['SP','ENT','A','A','A','A','O','A','SP'], "AJAJA AjOkA")
t("37) sem-chave + ENT no fim pula linha e volta ao normal", ['SP','ENT','A','A','A','ENT','A','A','A'], None)
