/**
 * UGENE - Integrated Bioinformatics Tools.
 * Copyright (C) 2008-2024 UniPro <ugene@unipro.ru>
 * http://ugene.net
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 */

#include "MsaConsensusUtils.h"

#include <QPair>
#include <QVector>

#include <U2Core/U2SafePoints.h>

#include "MsaConsensusAlgorithm.h"

namespace U2 {

void MsaConsensusUtils::updateConsensus(const Msa& ma, QByteArray& cons, MsaConsensusAlgorithm* algo) {
    U2Region r(0, ma->getLength());
    updateConsensus(ma, r, cons, algo);
}

void MsaConsensusUtils::updateConsensus(const Msa& ma, const U2Region& region, QByteArray& cons, MsaConsensusAlgorithm* algo) {
    QVector<U2Region> l;
    l.append(region);
    updateConsensus(ma, l, cons, algo);
}

void MsaConsensusUtils::updateConsensus(const Msa& ma, const QVector<U2Region>& regions, QByteArray& cons, MsaConsensusAlgorithm* algo) {
    if (ma->isEmpty()) {
        return;
    }
    int aliLen = ma->getLength();
    if (cons.length() != aliLen) {
        cons.resize(aliLen);
    }

    foreach (const U2Region& r, regions) {
        for (int i = (int)r.startPos, n = (int)r.endPos(); i < n; i++) {
            cons[i] = algo->getConsensusChar(ma, i);
        }
    }
}

QString MsaConsensusUtils::getConsensusPercentTip(const Msa& ma, int pos, int minReportPercent, int maxReportChars, bool ignoreLeadingTrailingGaps /* = false */) {
    if (ma->getLength() == 0) {
        return "";
    }
    QVector<QPair<int, char>> freqs(32);  // TODO: try QVarLengthArray?
    assert(pos >= 0 && pos < ma->getLength());
    int nSeq = ma->getRowCount();
    assert(nSeq > 0);
    if (nSeq == 0) {
        return "";
    }
    int gaps = 0;
    for (int seq = 0; seq < ma->getRowCount(); seq++) {
        char c = ma->charAt(seq, pos);
        if (c >= 'A' && c <= 'Z') {
            int idx = c - 'A';
            freqs[idx].first++;
            freqs[idx].second = c;
        } else {
            // count gaps
            if (ignoreLeadingTrailingGaps && ma->isLeadingOrTrailingGap(seq, pos)) {
                nSeq--;
                continue;
            }
            ++gaps;
        }
    }
    CHECK(nSeq != 0, QString());
    std::sort(freqs.begin(), freqs.end());
    double percentK = 100.0 / nSeq;

    QString res = "<table cellspacing=7>";
    int i = 0;
    for (; i < 32; i++) {
        int p = freqs[freqs.size() - i - 1].first;
        double percent = p * percentK;
        if (percent < minReportPercent || percent < 1.0 / nSeq) {
            break;
        }
        if (i == maxReportChars) {
            break;
        }
        char c = freqs[freqs.size() - i - 1].second;
        res = res + "<tr><td><b>" + QChar(c) + "</b></td>";
        res = res + "<td align=right>" + QString::number(percent, 'f', 1) + "%</td>";
        res = res + "<td align=right>" + QString::number(p) + "</td>";
        res = res + "</tr>";
    }
    if (i == 0) {
        return "";
    }
    if (i == maxReportChars) {
        res += "<tr><td colspan=3>...</td></tr>";
    }

    // adding gaps statstics
    if (gaps != 0) {
        double gapsPercent = gaps * percentK;
        res += "<tr><td><b>" + QObject::tr("Gaps:") + "</b></td>";
        res = res + "<td align=right>" + QString::number(gapsPercent, 'f', 1) + "%</td>";
        res = res + "<td align=right>" + QString::number(gaps) + "</td>";
        res = res + "</tr>";
    }

    res += "</table>";
    return res;
}

void MsaConsensusUtils::unpackConsensusCharsFromInt(quint32 val, char* charVal, int* maskPos) {
    for (int i = 0; i < 4; i++) {
        quint32 byteVal = (val >> (i * 8)) & 0xFF;
        maskPos[i] = byteVal >> 5;
        charVal[i] = (maskPos[i] < 4) ? (byteVal & 0x1F) + 'A' : 0;
    }
}

uchar MsaConsensusUtils::getColumnFreqs(const Msa& ma, int pos, QVector<int>& freqsByChar, int& nonGapChars, const QVector<int>& seqIdx) {
    assert(freqsByChar.size() == 256);
    freqsByChar.fill(0);
    nonGapChars = 0;
    uchar maxC = 0;
    int maxCFreq = 0;
    int* freqs = freqsByChar.data();
    int nSeq = seqIdx.isEmpty() ? ma->getRowCount() : seqIdx.size();
    for (int seq = 0; seq < nSeq; seq++) {
        auto c = (uchar)ma->charAt(seqIdx.isEmpty() ? seq : seqIdx[seq], pos);
        freqs[c]++;
        if (c != U2Msa::GAP_CHAR && freqs[c] > maxCFreq) {
            maxCFreq = freqs[c];
            maxC = c;
        }
        if (c != U2Msa::GAP_CHAR) {
            nonGapChars++;
        }
    }
    return maxC;
}

quint32 MsaConsensusUtils::packConsensusCharsToInt(const Msa& ma, int pos, const int* mask4, bool gapsAffectPercents) {
    QVector<QPair<int, char>> freqs(32);
    int numNoGaps = 0;
    int nSeq = ma->getRowCount();
    for (int seq = 0; seq < nSeq; seq++) {
        char c = ma->charAt(seq, pos);
        if (c >= 'A' && c <= 'Z') {
            int idx = c - 'A';
            freqs[idx].first++;
            freqs[idx].second = c;
            numNoGaps++;
        }
    }
    std::sort(freqs.begin(), freqs.end());
    if (!gapsAffectPercents && numNoGaps == 0) {
        return 0xE0E0E0E0;  //'4' in masks, '0' in values
    }
    quint32 res = 0;
    double percentK = 100.0 / (gapsAffectPercents ? nSeq : numNoGaps);
    for (int i = 0; i < 4; i++) {
        int p = int(freqs[freqs.size() - i - 1].first * percentK);
        quint32 rangeBits = (p >= mask4[0]) ? 0 : (p >= mask4[1]) ? 1
                                              : (p >= mask4[2])   ? 2
                                              : (p >= mask4[3])   ? 3
                                                                  : 4;
        quint32 charVal = rangeBits == 4 ? 'A' : quint32(freqs[freqs.size() - i - 1].second);
        quint32 maskedVal = (rangeBits << 5) | (charVal - 'A');  // 3 bits for range, 5 for symbol
        assert(maskedVal <= 255);
        res = res | (maskedVal << (8 * i));
    }
    return res;
}

}  // namespace U2
