/*
ordenacaoVersoes.js

Ordenacao das tags de versao apresentadas pelo atualizador.

Antes cada listarImagensETagsExistentes.js tinha (ou nao tinha) a sua propria
comparacao. Duas consequencias:

- alguns scripts nao ordenavam nada e mostravam a lista na ordem em que o GitLab
  devolve, que e alfabetica: 1.0.0, 1.1.0, 1.10.0, 1.2.0 ... 1.9.0;
- os que ordenavam usavam `semver.compare` sobre `\d+\.\d+\.\d+`, entao versao
  com dois componentes (1.80) caia na comparacao alfabetica de branch.

As regras seguem scriptsPython/lib/versoes_docker.py, para o atualizador e a
implantacao concordarem sobre qual e a versao mais recente:

- numero de versao vem sempre acima de nome de branch, antes de qualquer
  comparacao de numero;
- dentro da mesma faixa, a parte numerica manda componente por componente
  (1.10.0 > 1.9.0) e componente ausente vale zero (1.80 é tratada como 1.80.0);
- sufixo de plataforma nao altera precedencia (5.4.9-linux == 5.4.9), e o mesmo
  vale para pre-lancamento: 0.4.0-beta e versao, ordenada pelo numero, porque a
  Central de Relatorios publicou as betas como release oficial antes da 1.0.0. A
  marca de beta/rc so desempata o mesmo numero (1.6.0-rc1 < 1.6.0);
- entre branches, prefixo numerico ainda ordena, para a ficha mais nova aparecer
  primeiro; sem prefixo, a ordem e alfabetica so para o resultado ser estavel.

A faixa vem antes do numero porque branch de ficha comeca com o numero da ficha
(493081_junio_SacolaAdicionarProduto_Predial). Comparando numero direto, 493081
ficaria acima da 5.111.2 e as versoes de teste subiriam para o topo da lista.
*/

// Sufixos que apenas identificam a plataforma da imagem, nao a precedencia
const SUFIXOS_PLATAFORMA = [
    'linux',
    'windows',
    'win',
    'alpine',
    'amd64',
    'arm64',
    'x64',
    'x86',
    '2016',
    '2019',
    '2022',
    '2025'
]

// Sufixos que identificam versao ainda nao liberada oficialmente
const SUFIXOS_PRE_LANCAMENTO = [
    'alpha',
    'beta',
    'rc',
    'pre',
    'preview',
    'snapshot',
    'dev',
    'teste',
    'test',
    'homolog',
    'homologacao'
]

const INICIO_NUMERICO = /^(\d+(?:\.\d+)*)/

function nuloOuVazio(valor) {
    return valor === undefined || valor === null || String(valor).trim() === ''
}

function separarTokensSufixo(sufixo) {
    return !sufixo ? [] : sufixo.toLowerCase().split(/[-_.+]/).filter(token => token !== '')
}

function classificarSufixo(sufixo) {
    let ehPreLancamento = false
    const tokensDesconhecidos = []

    for (const token of separarTokensSufixo(sufixo)) {
        // rc1, beta2, alpha3 tambem contam como pre-lancamento
        const tokenSemNumero = token.replace(/\d+$/, '')

        if (SUFIXOS_PRE_LANCAMENTO.includes(token) || SUFIXOS_PRE_LANCAMENTO.includes(tokenSemNumero)) {
            ehPreLancamento = true
        } else if (SUFIXOS_PLATAFORMA.includes(token)) {
            continue
        } else if (/^\d+$/.test(token)) {
            // numero solto depois do sufixo de plataforma (ex.: windows-2016)
            continue
        } else {
            tokensDesconhecidos.push(token)
        }
    }

    return { ehPreLancamento, tokensDesconhecidos }
}

function analisarVersao(tag) {
    const texto = nuloOuVazio(tag) ? '' : String(tag).trim()

    const correspondencia = INICIO_NUMERICO.exec(texto)

    const parteNumerica = correspondencia == null ? '' : correspondencia[1]
    const sufixo = correspondencia == null ? texto : texto.slice(parteNumerica.length).replace(/^[-_.+]+/, '')

    const numeros = parteNumerica === '' ? [] : parteNumerica.split('.').map(Number)

    const { ehPreLancamento, tokensDesconhecidos } = classificarSufixo(sufixo)

    return {
        tag: texto,
        parteNumerica,
        sufixo,
        numeros,
        ehNumerica: numeros.length > 0,
        ehPreLancamento,
        tokensDesconhecidos,
        plataformas: separarTokensSufixo(sufixo).filter(token => SUFIXOS_PLATAFORMA.includes(token))
    }
}

// Faixas de precedencia. A faixa manda antes de qualquer numero: numero de
// versao sempre acima de nome de branch.
const FAIXA_BRANCH = 0
const FAIXA_VERSAO = 1

// Numero de versao tem ponto (5.111.2, 1.80, 5.4.9-linux, 0.4.0-beta). Numero
// solto e numero de ficha, nao versao.
const COMPONENTES_MINIMOS_DE_VERSAO = 2

/**
 * Faixa de precedencia da tag.
 *
 * O criterio e o padrao das versoes de producao, e nao o padrao dos nomes de
 * teste: nao ha padrao de nome de teste em que dar para confiar (493081_junio_x,
 * Matheus_PA_492055_x, TESTE2, fix_pendencia_desconto, master). Entao e versao o
 * que se parece com numero de versao - numero com ponto e sufixo so de plataforma
 * ou de pre-lancamento - e todo o resto e branch.
 *
 * O prefixo numerico sozinho nao basta: branch de ficha comeca com o numero da
 * ficha, e 493081 apareceria acima da 5.111.2 se o numero fosse comparado direto.
 *
 * Pre-lancamento entra como versao, e nao numa faixa abaixo: a Central de
 * Relatorios publicou 0.1.0-beta ... 0.4.0-beta como release oficial antes da
 * 1.0.0, e essas tags precisam aparecer no bloco das versoes, ordenadas pelo
 * numero. A marca de beta/rc so desempata o mesmo numero (0.9-beta < 0.9).
 */
function faixaDeVersao(versao) {
    const pareceVersao = versao.numeros.length >= COMPONENTES_MINIMOS_DE_VERSAO
        && versao.tokensDesconhecidos.length === 0

    return pareceVersao ? FAIXA_VERSAO : FAIXA_BRANCH
}

/**
 * Versao liberada como release: numero de versao sem marca de pre-lancamento.
 *
 * Usada para escolher "a ultima versao" de um sistema, onde beta/rc nao deve ser
 * escolhida. Para ordenar a lista apresentada, o que vale e faixaDeVersao.
 */
function ehVersaoDeProducao(tag) {
    const versao = analisarVersao(tag)

    return faixaDeVersao(versao) === FAIXA_VERSAO && !versao.ehPreLancamento
}

function temPlataforma(tag, plataforma) {
    return !plataforma ? true : analisarVersao(tag).plataformas.includes(String(plataforma).trim().toLowerCase())
}

function componente(numeros, indice) {
    return indice < numeros.length ? numeros[indice] : 0
}

function compararTexto(t1, t2) {
    return t1 > t2 ? 1 : t1 < t2 ? -1 : 0
}

/** Compara a parte numerica componente por componente, ausente valendo zero. */
function compararNumeros(versao1, versao2) {
    const quantidadeComponentes = Math.max(versao1.numeros.length, versao2.numeros.length)

    for (let indice = 0; indice < quantidadeComponentes; indice++) {
        const numero1 = componente(versao1.numeros, indice)
        const numero2 = componente(versao2.numeros, indice)

        if (numero1 !== numero2) {
            return numero1 > numero2 ? 1 : -1
        }
    }

    return 0
}

/**
 * Compara duas tags. Devolve -1, 0 ou 1 (tag1 menor, igual ou maior).
 *
 * Serve direto como comparador de Array.prototype.sort, que fica em ordem
 * crescente.
 */
function compararVersoes(tag1, tag2) {
    if (nuloOuVazio(tag1) || nuloOuVazio(tag2)) {
        return 0
    }

    const versao1 = analisarVersao(tag1)
    const versao2 = analisarVersao(tag2)

    const faixa1 = faixaDeVersao(versao1)
    const faixa2 = faixaDeVersao(versao2)

    if (faixa1 !== faixa2) {
        return faixa1 > faixa2 ? 1 : -1
    }

    if (faixa1 === FAIXA_BRANCH) {
        // Entre branches, o prefixo numerico ainda ordena, para a ficha mais nova
        // aparecer primeiro (493081... acima de 492914...).
        if (versao1.ehNumerica !== versao2.ehNumerica) {
            return versao1.ehNumerica ? 1 : -1
        }

        if (!versao1.ehNumerica) {
            return compararTexto(versao1.tag.toLowerCase(), versao2.tag.toLowerCase())
        }

        const diferencaNumerica = compararNumeros(versao1, versao2)

        return diferencaNumerica !== 0
            ? diferencaNumerica
            : compararTexto(versao1.tag.toLowerCase(), versao2.tag.toLowerCase())
    }

    const diferencaNumerica = compararNumeros(versao1, versao2)

    if (diferencaNumerica !== 0) {
        return diferencaNumerica
    }

    // Mesmo numero: a marca de pre-lancamento desempata (1.6.0-rc1 < 1.6.0)
    if (versao1.ehPreLancamento !== versao2.ehPreLancamento) {
        return versao1.ehPreLancamento ? -1 : 1
    }

    if (versao1.ehPreLancamento) {
        return compararTexto(versao1.sufixo.toLowerCase(), versao2.sufixo.toLowerCase())
    }

    return 0
}

/** Tags ordenadas da mais recente para a mais antiga. */
function ordenarTagsDaMaisRecenteParaMaisAntiga(tags) {
    return (tags || [])
        .filter(tag => !nuloOuVazio(tag))
        .sort((tag1, tag2) => compararVersoes(tag2, tag1))
}

/**
 * Ordena a lista de imagens/tags da API do GitLab da mais recente para a mais
 * antiga, pelo campo `name` de cada item.
 */
function ordenarListaImagensETags(listaImagensETags) {
    return (listaImagensETags || []).slice().sort(
        (elemento1, elemento2) => compararVersoes(
            elemento2 == null ? null : elemento2.name,
            elemento1 == null ? null : elemento1.name
        )
    )
}

/**
 * Ultima versao de uma lista de tags.
 *
 * Prioriza versao de producao (numerica, sem pre-lancamento e sem sufixo de
 * branch). Se nao houver nenhuma - caso dos projetos publicados por branch, como
 * o Whatsapp Bot -, cai para a tag mais recente pela ordenacao geral, porque
 * devolver nada seria pior que devolver o nome da branch.
 *
 * Mesma regra de selecionar_ultima_versao em scriptsPython/lib/catalogo_versoes.py.
 */
function selecionarUltimaVersao(tags, plataforma) {
    const candidatas = (tags || []).filter(tag =>
        ehVersaoDeProducao(tag) && temPlataforma(tag, plataforma))

    if (candidatas.length > 0) {
        return ordenarTagsDaMaisRecenteParaMaisAntiga(candidatas)[0]
    }

    if (plataforma) {
        return null
    }

    const ordenadas = ordenarTagsDaMaisRecenteParaMaisAntiga(tags)

    return ordenadas.length > 0 ? ordenadas[0] : null
}

/** Nomes das tags, da mais recente para a mais antiga, um por linha. */
function obterListaDeVersoesParaApresentacao(listaImagensETags) {
    return ordenarListaImagensETags(listaImagensETags).map(elemento => elemento.name).join("\n")
}

module.exports = {
    analisarVersao,
    compararVersoes,
    ehVersaoDeProducao,
    nuloOuVazio,
    obterListaDeVersoesParaApresentacao,
    ordenarListaImagensETags,
    ordenarTagsDaMaisRecenteParaMaisAntiga,
    selecionarUltimaVersao,
    temPlataforma
}
