/*
    Copyright (C) 2008  Tim Fechtner < urwald at users dot sourceforge dot net >
    Modfied by Adrian @MXLinux

    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) version 3 or any later version
    accepted by the membership of KDE e.V. (or its successor approved
    by the membership of KDE e.V.), which shall act as a proxy
    defined in Section 14 of version 3 of the license.

    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, see <http://www.gnu.org/licenses/>.
*/

#include "versionnumber.h"

VersionNumber::VersionNumber(const QString &value)
{
    setStrings(value);
}

QString VersionNumber::toString() const
{
    return str;
}

void VersionNumber::setStrings(const QString &value)
{
    // Initialize epoch, upstream, and debian strings
    str = value;
    QString upstreamStr;
    QString debianStr;

    // Parse epoch and upstream_version
    int colonIndex = value.indexOf(':');
    if (colonIndex != -1) {
        epoch = value.left(colonIndex).toInt();
        upstreamStr = value.mid(colonIndex + 1);
    } else {
        epoch = 0;
        upstreamStr = value;
    }

    // Parse debian_revision
    int dashIndex = upstreamStr.lastIndexOf('-');
    if (dashIndex != -1) {
        debianStr = upstreamStr.mid(dashIndex + 1);
        upstreamStr = upstreamStr.left(dashIndex);
    }

    upstreamVersion = groupDigits(upstreamStr);
    debianRevision = groupDigits(debianStr);
}

VersionNumber &VersionNumber::operator=(const QString &value)
{
    setStrings(value);
    return *this;
}

std::strong_ordering VersionNumber::operator<=>(const VersionNumber &value) const
{
    int result = compare(*this, value);
    if (result > 0) {
        return std::strong_ordering::less;
    }
    if (result < 0) {
        return std::strong_ordering::greater;
    }
    return std::strong_ordering::equal;
}

bool VersionNumber::operator==(const VersionNumber &value) const
{
    return compare(*this, value) == 0;
}

// Transform QString into QStringList with digits grouped together
QStringList VersionNumber::groupDigits(const QString &value)
{
    QStringList result;
    int length = value.length();
    result.reserve(length);
    QString cache;

    for (int i = 0; i < length; ++i) {
        QChar currentChar = value.at(i);
        if (currentChar.isDigit()) {
            cache.append(currentChar);
            if (i == length - 1) {
                result.append(cache);
            }
        } else {
            if (!cache.isEmpty()) { // Add accumulated digits
                result.append(cache);
                cache.clear();
            }
            result.append(currentChar);
        }
    }

    return result;
}

// Return 1 if second > first, -1 if second < first, 0 if equal
int VersionNumber::compare(const VersionNumber &first, const VersionNumber &second) const
{
    if (second.epoch > first.epoch) {
        return 1;
    } else if (second.epoch < first.epoch) {
        return -1;
    }
    int upstreamComparison = compare(first.upstreamVersion, second.upstreamVersion);
    if (upstreamComparison != 0) {
        return upstreamComparison;
    }
    if (!first.debianRevision.isEmpty() || !second.debianRevision.isEmpty()) {
        return compare(first.debianRevision, second.debianRevision);
    }
    return 0;
}

// Return 1 if second > first, -1 if second < first, 0 if equal
int VersionNumber::compare(const QStringList &first, const QStringList &second)
{
    // Compare QStringList versions
    int minLength = qMin(first.length(), second.length());
    for (int i = 0; i < minLength; ++i) {
        if (first.at(i) == second.at(i)) {
            continue;
        }

        if (first.at(i).startsWith('~') && !second.at(i).startsWith('~')) {
            return 1;
        } else if (second.at(i).startsWith('~') && !first.at(i).startsWith('~')) {
            return -1;
        }

        // if one char length check which one is larger
        if (first.at(i).length() == 1 && second.at(i).length() == 1) {
            int res = compare(first.at(i).at(0), second.at(i).at(0));
            if (res == 0) {
                continue;
            } else {
                return res;
            }
            // one char (not-number) vs. multiple (digits)
        } else if (first.at(i).length() > 1 && second.at(i).length() == 1 && !second.at(i).at(0).isDigit()) {
            return 1;
        } else if (first.at(i).length() == 1 && !first.at(i).at(0).isDigit() && second.at(i).length() > 1) {
            return -1;
        }

        // Compare remaining digits - follow dpkg semantics. Both entries are
        // all-digit groups here, so compare them as arbitrary-precision integers
        // rather than via toInt(), which returns 0 (comparing equal) for
        // components wider than INT_MAX, e.g. snapshot timestamps.
        int numericComparison = compareNumeric(first.at(i), second.at(i));
        if (numericComparison != 0) {
            return numericComparison;
        }
        // Numerically equal - dpkg treats versions like "8" and "08" as equal.
        // Continue to next component (don't return here)
        continue;
    }

    // If equal till the end of one of the lists, compare list size
    // if the larger list doesn't have "~" it's the bigger version
    if (second.length() > first.length()) {
        return second.at(first.length()).startsWith('~') ? -1 : 1;
    } else if (second.length() < first.length()) {
        return first.at(second.length()).startsWith('~') ? 1 : -1;
    }

    return 0;
}

// Return 1 if second > first, -1 if second < first, 0 if equal.
// Both arguments are all-digit strings; compares them as integers of
// arbitrary width by ignoring leading zeros, then length, then digits.
int VersionNumber::compareNumeric(const QString &first, const QString &second)
{
    auto significant = [](const QString &digits) {
        qsizetype start = 0;
        while (start < digits.size() - 1 && digits.at(start) == QLatin1Char('0')) {
            ++start;
        }
        return QStringView(digits).mid(start);
    };

    const QStringView a = significant(first);
    const QStringView b = significant(second);

    if (a.size() != b.size()) {
        return a.size() < b.size() ? 1 : -1;
    }
    const int lexical = a.compare(b);
    if (lexical == 0) {
        return 0;
    }
    return lexical < 0 ? 1 : -1;
}

// Return 1 if second > first, -1 if second < first, 0 if equal
// letters and number sort before special chars
int VersionNumber::compare(QChar first, QChar second)
{
    // Compare QChars
    if (first == second) {
        return 0;
    }

    // Sort letters and numbers before special char
    if (first.isLetterOrNumber() && !second.isLetterOrNumber()) {
        return 1;
    } else if (!first.isLetterOrNumber() && second.isLetterOrNumber()) {
        return -1;
    }

    return first < second ? 1 : -1;
}
