"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.InterpretadorPortugolIpt = void 0;
const espaco_memoria_1 = require("../../espaco-memoria");
const estruturas_1 = require("../../estruturas");
const excecoes_1 = require("../../../excecoes");
const quebras_1 = require("../../../quebras");
const pilha_escopos_execucao_1 = require("../../pilha-escopos-execucao");
const inferenciador_1 = require("../../../inferenciador");
const portugol_ipt_1 = __importDefault(require("../../../tipos-de-simbolos/portugol-ipt"));
class InterpretadorPortugolIpt {
    constructor(diretorioBase, funcaoDeRetorno = null, funcaoDeRetornoMesmaLinha = null) {
        this.funcaoDeRetorno = null;
        this.funcaoDeRetornoMesmaLinha = null;
        this.resultadoInterpretador = [];
        this.diretorioBase = diretorioBase;
        this.funcaoDeRetorno = funcaoDeRetorno || console.log;
        this.funcaoDeRetornoMesmaLinha =
            funcaoDeRetornoMesmaLinha || process.stdout.write.bind(process.stdout);
        this.erros = [];
        this.declaracoes = [];
        this.pilhaEscoposExecucao = new pilha_escopos_execucao_1.PilhaEscoposExecucao();
        const escopoExecucao = {
            declaracoes: [],
            declaracaoAtual: 0,
            espacoMemoria: new espaco_memoria_1.EspacoMemoria(),
            finalizado: false,
            tipo: 'outro',
            emLacoRepeticao: false,
        };
        this.pilhaEscoposExecucao.empilhar(escopoExecucao);
    }
    /* istanbul ignore next */
    visitarExpressaoTuplaN(expressao) {
        throw new Error('Método não implementado.');
    }
    /* istanbul ignore next */
    visitarExpressaoAcessoIntervaloVariavel(expressao) {
        throw new Error('Método não implementado.');
    }
    /* istanbul ignore next */
    visitarDeclaracaoTextoDocumentacao(declaracao) {
        throw new Error('Método não implementado.');
    }
    /* istanbul ignore next */
    visitarExpressaoSeparador(expressao) {
        throw new Error('Método não implementado.');
    }
    /* istanbul ignore next */
    visitarExpressaoComentario(expressao) {
        throw new Error('Método não implementado.');
    }
    resolverValor(objeto) {
        if (objeto === null || objeto === undefined) {
            return objeto;
        }
        if (objeto.hasOwnProperty('valor')) {
            return objeto.valor;
        }
        return objeto;
    }
    /* istanbul ignore next */
    visitarExpressaoArgumentoReferenciaFuncao(expressao) {
        throw new Error('Método não implementado.');
    }
    /* istanbul ignore next */
    visitarExpressaoReferenciaFuncao(expressao) {
        throw new Error('Método não implementado.');
    }
    /* istanbul ignore next */
    visitarExpressaoAcessoMetodo(expressao) {
        throw new Error('Método não implementado.');
    }
    /* istanbul ignore next */
    visitarExpressaoAcessoPropriedade(expressao) {
        throw new Error('Método não implementado.');
    }
    visitarDeclaracaoComentario(declaracao) {
        return Promise.resolve();
    }
    /* istanbul ignore next */
    visitarDeclaracaoTendoComo(declaracao) {
        throw new Error('Método não implementado.');
    }
    /* istanbul ignore next */
    visitarDeclaracaoInicioAlgoritmo(declaracao) {
        throw new Error('Método não implementado.');
    }
    /* istanbul ignore next */
    visitarDeclaracaoCabecalhoPrograma(declaracao) {
        throw new Error('Método não implementado.');
    }
    /* istanbul ignore next */
    visitarExpressaoTupla(expressao) {
        throw new Error('Método não implementado.');
    }
    /* istanbul ignore next */
    visitarExpressaoAcessoElementoMatriz(expressao) {
        throw new Error('Método não implementado.');
    }
    /* istanbul ignore next */
    visitarExpressaoAtribuicaoPorIndicesMatriz(expressao) {
        throw new Error('Método não implementado.');
    }
    /* istanbul ignore next */
    visitarExpressaoExpressaoRegular(expressao) {
        throw new Error('Método não implementado.');
    }
    /* istanbul ignore next */
    visitarExpressaoTipoDe(expressao) {
        throw new Error('Método não implementado.');
    }
    /* istanbul ignore next */
    visitarExpressaoFalhar(expressao) {
        throw new Error('Método não implementado.');
    }
    /* istanbul ignore next */
    visitarDeclaracaoParaCada(declaracao) {
        throw new Error('Método não implementado.');
    }
    visitarExpressaoLiteral(expressao) {
        return Promise.resolve(expressao.valor);
    }
    async avaliar(expressao) {
        // Descomente o código abaixo quando precisar detectar expressões undefined ou nulas.
        // Por algum motivo o depurador do VSCode não funciona direito aqui
        // com breakpoint condicional.
        /* if (expressao === null || expressao === undefined) {
            console.log('Aqui');
        } */
        return await expressao.aceitar(this);
    }
    async visitarExpressaoAgrupamento(expressao) {
        return await this.avaliar(expressao.expressao);
    }
    /* istanbul ignore next */
    visitarExpressaoUnaria(expressao) {
        throw new Error('Método não implementado');
    }
    /**
     * Verifica se operandos são números, que podem ser tanto variáveis puras do JavaScript
     * (neste caso, `number`), ou podem ser variáveis de Delégua com inferência (`VariavelInterface`).
     * @param operador O símbolo do operador.
     * @param direita O operando direito.
     * @param esquerda O operando esquerdo.
     * @returns Se ambos os operandos são números ou não.
     */
    verificarOperandosNumeros(operador, direita, esquerda) {
        const tipoDireita = direita.tipo
            ? direita.tipo
            : typeof direita === 'number'
                ? 'número'
                : String(NaN);
        const tipoEsquerda = esquerda.tipo
            ? esquerda.tipo
            : typeof esquerda === 'number'
                ? 'número'
                : String(NaN);
        const tiposNumericos = ['inteiro', 'numero', 'número', 'real'];
        if (tiposNumericos.includes(tipoDireita.toLowerCase()) &&
            tiposNumericos.includes(tipoEsquerda.toLowerCase()))
            return;
        throw new excecoes_1.ErroEmTempoDeExecucao(operador, 'Operadores precisam ser números.', operador.linha);
    }
    eIgual(esquerda, direita) {
        if (esquerda === null && direita === null)
            return true;
        if (esquerda === null)
            return false;
        return esquerda === direita;
    }
    async visitarExpressaoBinaria(expressao) {
        try {
            const esquerda = await this.avaliar(expressao.esquerda);
            const direita = await this.avaliar(expressao.direita);
            const valorEsquerdo = this.resolverValor(esquerda);
            const valorDireito = this.resolverValor(direita);
            const tipoEsquerdo = (esquerda === null || esquerda === void 0 ? void 0 : esquerda.hasOwnProperty('tipo'))
                ? esquerda.tipo
                : (0, inferenciador_1.inferirTipoVariavel)(esquerda);
            const tipoDireito = (direita === null || direita === void 0 ? void 0 : direita.hasOwnProperty('tipo'))
                ? direita.tipo
                : (0, inferenciador_1.inferirTipoVariavel)(direita);
            switch (expressao.operador.tipo) {
                case portugol_ipt_1.default.EXPONENCIACAO:
                    this.verificarOperandosNumeros(expressao.operador, esquerda, direita);
                    return Math.pow(valorEsquerdo, valorDireito);
                case portugol_ipt_1.default.MAIOR:
                    this.verificarOperandosNumeros(expressao.operador, esquerda, direita);
                    return Number(valorEsquerdo) > Number(valorDireito);
                case portugol_ipt_1.default.MAIOR_IGUAL:
                    this.verificarOperandosNumeros(expressao.operador, esquerda, direita);
                    return Number(valorEsquerdo) >= Number(valorDireito);
                case portugol_ipt_1.default.MENOR:
                    this.verificarOperandosNumeros(expressao.operador, esquerda, direita);
                    return Number(valorEsquerdo) < Number(valorDireito);
                case portugol_ipt_1.default.MENOR_IGUAL:
                    this.verificarOperandosNumeros(expressao.operador, esquerda, direita);
                    return Number(valorEsquerdo) <= Number(valorDireito);
                case portugol_ipt_1.default.SUBTRACAO:
                    this.verificarOperandosNumeros(expressao.operador, esquerda, direita);
                    return Number(valorEsquerdo) - Number(valorDireito);
                case portugol_ipt_1.default.ADICAO:
                    if (tipoEsquerdo === 'número' && tipoDireito === 'número') {
                        return Number(valorEsquerdo) + Number(valorDireito);
                    }
                    else {
                        return String(valorEsquerdo) + String(valorDireito);
                    }
                case portugol_ipt_1.default.DIVISAO:
                    this.verificarOperandosNumeros(expressao.operador, esquerda, direita);
                    return Number(valorEsquerdo) / Number(valorDireito);
                case portugol_ipt_1.default.DIVISAO_INTEIRA:
                    this.verificarOperandosNumeros(expressao.operador, esquerda, direita);
                    return Math.floor(Number(valorEsquerdo) / Number(valorDireito));
                case portugol_ipt_1.default.MULTIPLICACAO:
                    if (tipoEsquerdo === 'texto' || tipoDireito === 'texto') {
                        // Sem ambos os valores resolvem como texto, multiplica normal.
                        // Se apenas um resolve como texto, o outro repete o
                        // texto n vezes, sendo n o valor do outro.
                        if (tipoEsquerdo === 'texto' && tipoDireito === 'texto') {
                            return Number(valorEsquerdo) * Number(valorDireito);
                        }
                        if (tipoEsquerdo === 'texto') {
                            return valorEsquerdo.repeat(Number(valorDireito));
                        }
                        return valorDireito.repeat(Number(valorEsquerdo));
                    }
                    return Number(valorEsquerdo) * Number(valorDireito);
                case portugol_ipt_1.default.MODULO:
                    this.verificarOperandosNumeros(expressao.operador, esquerda, direita);
                    return Number(valorEsquerdo) % Number(valorDireito);
                case portugol_ipt_1.default.DIFERENTE:
                    return !this.eIgual(valorEsquerdo, valorDireito);
                case portugol_ipt_1.default.IGUAL:
                    return this.eIgual(valorEsquerdo, valorDireito);
            }
        }
        catch (erro) {
            return Promise.reject(erro);
        }
    }
    /* istanbul ignore next */
    visitarExpressaoDeChamada(expressao) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarExpressaoDeAtribuicao(expressao) {
        throw new Error('Método não implementado');
    }
    procurarVariavel(simbolo) {
        return this.pilhaEscoposExecucao.obterValorVariavel(simbolo);
    }
    visitarExpressaoDeVariavel(expressao) {
        return this.procurarVariavel(expressao.simbolo);
    }
    async visitarDeclaracaoDeExpressao(declaracao) {
        return declaracao.expressao.aceitar(this);
    }
    /**
     * Execução da leitura de valores da entrada configurada no
     * início da aplicação.
     * @param expressao Expressão do tipo Leia
     * @returns Promise com o resultado da leitura.
     */
    async visitarExpressaoLeia(expressao) {
        const mensagem = expressao.argumentos && expressao.argumentos[0] ? expressao.argumentos[0].valor : '> ';
        return new Promise((resolucao) => this.interfaceEntradaSaida.question(mensagem, (resposta) => {
            resolucao(resposta);
        }));
    }
    /* istanbul ignore next */
    visitarExpressaoLogica(expressao) {
        throw new Error('Método não implementado');
    }
    eVerdadeiro(objeto) {
        if (objeto === null)
            return false;
        if (typeof objeto === 'boolean')
            return Boolean(objeto);
        if (objeto.hasOwnProperty('valor')) {
            return Boolean(objeto.valor);
        }
        return true;
    }
    /**
     * Executa uma expressão Se, que tem uma condição, pode ter um bloco
     * Senão, e múltiplos blocos Senão-se.
     * @param declaracao A declaração Se.
     * @returns O resultado da avaliação do bloco cuja condição é verdadeira.
     */
    async visitarDeclaracaoSe(declaracao) {
        if (this.eVerdadeiro(await this.avaliar(declaracao.condicao))) {
            return await this.executar(declaracao.caminhoEntao);
        }
        if (declaracao.caminhoSenao !== null) {
            return await this.executar(declaracao.caminhoSenao);
        }
        return null;
    }
    /* istanbul ignore next */
    visitarDeclaracaoPara(declaracao) {
        return Promise.reject('Método não implementado');
    }
    /* istanbul ignore next */
    visitarExpressaoFimPara(declaracao) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarDeclaracaoFazer(declaracao) {
        throw new Error('Método não implementado');
    }
    async visitarExpressaoFormatacaoEscrita(declaracao) {
        let resultado = '';
        const conteudo = await this.avaliar(declaracao.expressao);
        const valorConteudo = this.resolverValor(conteudo);
        const tipoConteudo = conteudo.hasOwnProperty('tipo')
            ? conteudo.tipo
            : typeof conteudo;
        resultado = valorConteudo;
        if (['número', 'number'].includes(tipoConteudo) && declaracao.casasDecimais > 0) {
            resultado = valorConteudo.toLocaleString('pt', {
                maximumFractionDigits: declaracao.casasDecimais,
            });
        }
        if (declaracao.espacos > 0) {
            resultado += ' '.repeat(declaracao.espacos);
        }
        return resultado;
    }
    /* istanbul ignore next */
    visitarDeclaracaoEscolha(declaracao) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarDeclaracaoTente(declaracao) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarDeclaracaoEnquanto(declaracao) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarDeclaracaoImportar(declaracao) {
        throw new Error('Método não implementado');
    }
    async avaliarArgumentosEscreva(argumentos) {
        let formatoTexto = '';
        for (const argumento of argumentos) {
            const resultadoAvaliacao = await this.avaliar(argumento);
            let valor = this.resolverValor(resultadoAvaliacao);
            formatoTexto += `${this.paraTexto(valor)} `;
        }
        return formatoTexto.trimEnd();
    }
    /**
     * Execução de uma escrita na saída configurada, que pode ser `console` (padrão) ou
     * alguma função para escrever numa página Web.
     * @param declaracao A declaração.
     * @returns Sempre nulo, por convenção de visita.
     */
    async visitarDeclaracaoEscreva(declaracao) {
        try {
            const formatoTexto = await this.avaliarArgumentosEscreva(declaracao.argumentos);
            this.funcaoDeRetorno(formatoTexto);
            return null;
        }
        catch (erro) {
            this.erros.push({
                erroInterno: erro,
                linha: declaracao.linha,
                hashArquivo: declaracao.hashArquivo,
            });
        }
    }
    async visitarDeclaracaoEscrevaMesmaLinha(declaracao) {
        try {
            const formatoTexto = await this.avaliarArgumentosEscreva(declaracao.argumentos);
            this.funcaoDeRetornoMesmaLinha(formatoTexto);
            return null;
        }
        catch (erro) {
            this.erros.push({
                erroInterno: erro,
                linha: declaracao.linha,
                hashArquivo: declaracao.hashArquivo,
            });
        }
    }
    /* istanbul ignore next */
    executarBloco(declaracoes, ambiente) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarExpressaoBloco(declaracao) {
        throw new Error('Método não implementado');
    }
    async avaliacaoDeclaracaoVar(declaracao) {
        let valorOuOutraVariavel = null;
        if (declaracao.inicializador !== null) {
            valorOuOutraVariavel = await this.avaliar(declaracao.inicializador);
        }
        let valorFinal = null;
        if (valorOuOutraVariavel !== null && valorOuOutraVariavel !== undefined) {
            valorFinal = this.resolverValor(valorOuOutraVariavel);
        }
        return valorFinal;
    }
    /**
     * Executa expressão de definição de variável.
     * @param declaracao A declaração Var
     * @returns Sempre retorna nulo.
     */
    async visitarDeclaracaoVar(declaracao) {
        const valorFinal = await this.avaliacaoDeclaracaoVar(declaracao);
        this.pilhaEscoposExecucao.definirVariavel(declaracao.simbolo.lexema, valorFinal, declaracao.tipo);
        return null;
    }
    /* istanbul ignore next */
    visitarDeclaracaoVarMultiplo(declaracao) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarDeclaracaoConst(declaracao) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarDeclaracaoConstMultiplo(declaracao) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarExpressaoContinua(declaracao) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarExpressaoSustar(declaracao) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarExpressaoRetornar(declaracao) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarExpressaoFuncaoConstruto(expressao) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarExpressaoAtribuicaoPorIndice(expressao) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarExpressaoAcessoIndiceVariavel(expressao) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarExpressaoDefinirValor(expressao) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarDeclaracaoDefinicaoFuncao(declaracao) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarDeclaracaoClasse(declaracao) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarExpressaoAcessoMetodoOuPropriedade(expressao) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarExpressaoIsto(expressao) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarExpressaoDicionario(expressao) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarExpressaoVetor(expressao) {
        throw new Error('Método não implementado');
    }
    /* istanbul ignore next */
    visitarExpressaoSuper(expressao) {
        throw new Error('Método não implementado');
    }
    paraTexto(objeto) {
        if (objeto === null || objeto === undefined)
            return 'nulo';
        if (typeof objeto === 'boolean') {
            return objeto ? 'verdadeiro' : 'falso';
        }
        if (objeto instanceof Date) {
            const formato = Intl.DateTimeFormat('pt', {
                dateStyle: 'full',
                timeStyle: 'full',
            });
            return formato.format(objeto);
        }
        if (Array.isArray(objeto))
            return objeto;
        if (objeto.valor instanceof estruturas_1.ObjetoPadrao)
            return objeto.valor.paraTexto();
        if (typeof objeto === 'object')
            return JSON.stringify(objeto);
        return objeto.toString();
    }
    /**
     * Efetivamente executa uma declaração.
     * @param declaracao A declaração a ser executada.
     * @param mostrarResultado Se resultado deve ser mostrado ou não. Normalmente usado
     *                         pelo modo LAIR.
     */
    async executar(declaracao, mostrarResultado = false) {
        const resultado = await declaracao.aceitar(this);
        if (mostrarResultado) {
            this.funcaoDeRetorno(this.paraTexto(resultado));
        }
        if (resultado || typeof resultado === 'boolean') {
            this.resultadoInterpretador.push(this.paraTexto(resultado));
        }
        return resultado;
    }
    /**
     * Executa o último escopo empilhado no topo na pilha de escopos do interpretador.
     * Esse método pega exceções, mas apenas as devolve.
     *
     * O tratamento das exceções é feito de acordo com o bloco chamador.
     * Por exemplo, em `tente ... pegue ... finalmente`, a exceção é capturada e tratada.
     * Em outros blocos, pode ser desejável ter o erro em tela.
     * @param manterAmbiente Se verdadeiro, ambiente do topo da pilha de escopo é copiado para o ambiente imediatamente abaixo.
     * @returns O resultado da execução do escopo, se houver.
     */
    async executarUltimoEscopo(manterAmbiente = false) {
        const ultimoEscopo = this.pilhaEscoposExecucao.topoDaPilha();
        try {
            let retornoExecucao;
            for (; !(retornoExecucao instanceof quebras_1.Quebra) &&
                ultimoEscopo.declaracaoAtual < ultimoEscopo.declaracoes.length; ultimoEscopo.declaracaoAtual++) {
                retornoExecucao = await this.executar(ultimoEscopo.declaracoes[ultimoEscopo.declaracaoAtual]);
            }
            return retornoExecucao;
        }
        catch (erro) {
            return Promise.reject(erro);
        }
        finally {
            this.pilhaEscoposExecucao.removerUltimo();
            if (manterAmbiente) {
                const escopoAnterior = this.pilhaEscoposExecucao.topoDaPilha();
                escopoAnterior.espacoMemoria.valores = Object.assign(escopoAnterior.espacoMemoria.valores, ultimoEscopo.espacoMemoria.valores);
            }
        }
    }
    async interpretar(declaracoes, manterAmbiente) {
        this.erros = [];
        const escopoExecucao = {
            declaracoes: declaracoes,
            declaracaoAtual: 0,
            espacoMemoria: new espaco_memoria_1.EspacoMemoria(),
            finalizado: false,
            tipo: 'outro',
            emLacoRepeticao: false,
        };
        this.pilhaEscoposExecucao.empilhar(escopoExecucao);
        try {
            const retornoOuErro = await this.executarUltimoEscopo(manterAmbiente);
            if (retornoOuErro instanceof excecoes_1.ErroEmTempoDeExecucao) {
                this.erros.push(retornoOuErro);
            }
        }
        catch (erro) {
            this.erros.push(erro);
        }
        finally {
            const retorno = {
                erros: this.erros,
                resultado: this.resultadoInterpretador,
            };
            this.resultadoInterpretador = [];
            return retorno;
        }
    }
}
exports.InterpretadorPortugolIpt = InterpretadorPortugolIpt;
//# sourceMappingURL=interpretador-portugol-ipt.js.map