/*  This file is part of the KDE libraries
    Copyright (C) 2006 Chusslove Illich <caslav.ilic@gmx.net>

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Library General Public
    License as published by the Free Software Foundation; either
    version 2 of the License, or (at your option) any later version.

    This library 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
    Library General Public License for more details.

    You should have received a copy of the GNU Library General Public License
    along with this library; see the file COPYING.LIB.  If not, write to
    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
    Boston, MA 02110-1301, USA.
*/

#include <klocalizedstring.h>

#include <config.h>

#include <kglobal.h>
#include <kdebug.h>
#include <klocale.h>
#include <klocale_p.h>
#include <kstandarddirs.h>
#include <kuitsemantics_p.h>
#include "kcatalogname_p.h"

#include <QtCore/qmutex.h>
#include <QStringList>
#include <QByteArray>
#include <QChar>
#include <QHash>
#include <QList>
#include <QVector>

// Truncates string, for output of long messages.
static QString shortenMessage (const QString &str)
{
    const int maxlen = 20;
    if (str.length() <= maxlen)
        return str;
    else
        return str.left(maxlen).append(QLatin1String("..."));
}

typedef qulonglong pluraln;
typedef qlonglong intn;
typedef qulonglong uintn;
typedef double realn;

class KLocalizedStringPrivateStatics;

class KLocalizedStringPrivate
{
    friend class KLocalizedString;

    QStringList args;
    QList<QVariant> vals;
    bool numberSet;
    pluraln number;
    int numberOrd;
    QByteArray ctxt;
    QHash<QString, QString> dynctxt;
    QByteArray msg;
    QByteArray plural;

    QString toString (const KLocale *locale) const;
    QString selectForEnglish () const;
    QString substituteSimple (const QString &trans,
                              const QChar &plchar = QLatin1Char('%'),
                              bool partial = false) const;
    QString postFormat (const QString &text,
                        const QString &lang,
                        const QString &ctxt) const;
};

class KLocalizedStringPrivateStatics
{
public:
    QHash<QString, KuitSemantics*> formatters;

    ~KLocalizedStringPrivateStatics ()
    {
        qDeleteAll(formatters);
    }
};
K_GLOBAL_STATIC(KLocalizedStringPrivateStatics, staticsKLSP)
Q_GLOBAL_STATIC(QMutex, staticsKLSPMutex)

KLocalizedString::KLocalizedString ()
: d(new KLocalizedStringPrivate)
{
    d->numberSet = false;
    d->number = 0;
    d->numberOrd = 0;
}

KLocalizedString::KLocalizedString (const char *ctxt,
                                    const char *msg, const char *plural)
: d(new KLocalizedStringPrivate)
{
    d->ctxt = ctxt;
    d->msg = msg;
    d->plural = plural;
    d->numberSet = false;
    d->number = 0;
    d->numberOrd = 0;
}

KLocalizedString::KLocalizedString(const KLocalizedString &rhs)
: d(new KLocalizedStringPrivate(*rhs.d))
{
}

KLocalizedString& KLocalizedString::operator= (const KLocalizedString &rhs)
{
    if (&rhs != this)
    {
        *d = *rhs.d;
    }
    return *this;
}

KLocalizedString::~KLocalizedString ()
{
    delete d;
}

bool KLocalizedString::isEmpty () const
{
    return d->msg.isEmpty();
}

QString KLocalizedString::toString () const
{
    return d->toString(KGlobal::locale());
}

QString KLocalizedString::toString (const KLocale *locale) const
{
    return d->toString(locale);
}

QString KLocalizedStringPrivate::toString (const KLocale *locale) const
{
    std::lock_guard<std::recursive_mutex> lock(kLocaleMutex());

    // Assure the message has been supplied.
    if (msg.isEmpty())
    {
        kDebug(173) << "Trying to convert empty KLocalizedString to QString.";
        #ifndef NDEBUG
        return QString::fromLatin1("(I18N_EMPTY_MESSAGE)");
        #else
        return QString();
        #endif
    }

    // Check whether plural argument has been supplied, if message has plural.
    if (!plural.isEmpty() && !numberSet)
        kDebug(173) << QString::fromLatin1("Plural argument to message {%1} not supplied before conversion.")
                              .arg(shortenMessage(QString::fromUtf8(msg)));

    // Get raw translation.
    QString rawtrans, lang, ctry;
    if (locale != NULL) {
        if (!ctxt.isEmpty() && !plural.isEmpty()) {
            locale->translateRaw(ctxt, msg, plural, number, &lang, &rawtrans);
        } else if (!plural.isEmpty()) {
            locale->translateRaw(msg, plural, number, &lang, &rawtrans);
        } else if (!ctxt.isEmpty()) {
            locale->translateRaw(ctxt, msg, &lang, &rawtrans);
        } else {
            locale->translateRaw(msg, &lang, &rawtrans);
        }
        ctry = locale->country();
    } else {
        lang = KLocale::defaultLanguage();
        ctry = QLatin1Char('C');
        rawtrans = selectForEnglish();
    }

    // Substitute placeholders in ordinary translation.
    QString finalstr = substituteSimple(rawtrans);
    // Post-format ordinary translation.
    return postFormat(finalstr, lang, QString::fromLatin1(ctxt));
}

QString KLocalizedStringPrivate::selectForEnglish () const
{
    if (!plural.isEmpty()) {
        if (number == 1) {
            return QString::fromUtf8(msg);
        }
        return QString::fromUtf8(plural);
    }
    return QString::fromUtf8(msg);
}

QString KLocalizedStringPrivate::substituteSimple (const QString &trans,
                                                   const QChar &plchar,
                                                   bool partial) const
{
    #ifdef NDEBUG
    Q_UNUSED(partial);
    #endif

    QStringList tsegs; // text segments per placeholder occurrence
    QList<int> plords; // ordinal numbers per placeholder occurrence
    #ifndef NDEBUG
    QVector<int> ords; // indicates which placeholders are present
    #endif
    int slen = trans.length();
    int spos = 0;
    int tpos = trans.indexOf(plchar);
    while (tpos >= 0)
    {
        int ctpos = tpos;

        tpos++;
        if (tpos == slen)
            break;

        if (trans[tpos].digitValue() > 0) // %0 not considered a placeholder
        {
            // Get the placeholder ordinal.
            int plord = 0;
            while (tpos < slen && trans[tpos].digitValue() >= 0)
            {
                plord = 10 * plord + trans[tpos].digitValue();
                tpos++;
            }
            plord--; // ordinals are zero based

            #ifndef NDEBUG
            // Perhaps enlarge storage for indicators.
            // Note that QVector<int> will initialize new elements to 0,
            // as they are supposed to be.
            if (plord >= ords.size())
                ords.resize(plord + 1);

            // Indicate that placeholder with computed ordinal is present.
            ords[plord] = 1;
            #endif

            // Store text segment prior to placeholder and placeholder number.
            tsegs.append(trans.mid(spos, ctpos - spos));
            plords.append(plord);

            // Position of next text segment.
            spos = tpos;
        }

        tpos = trans.indexOf(plchar, tpos);
    }
    // Store last text segment.
    tsegs.append(trans.mid(spos));

    #ifndef NDEBUG
    // Perhaps enlarge storage for plural-number ordinal.
    if (!plural.isEmpty() && numberOrd >= ords.size())
        ords.resize(numberOrd + 1);

    // Message might have plural but without plural placeholder, which is an
    // allowed state. To ease further logic, indicate that plural placeholder
    // is present anyway if message has plural.
    if (!plural.isEmpty())
        ords[numberOrd] = 1;
    #endif

    // Assemble the final string from text segments and arguments.
    QString finalstr;
    for (int i = 0; i < plords.size(); i++)
    {
        finalstr.append(tsegs.at(i));
        if (plords.at(i) >= args.size())
        // too little arguments
        {
            // put back the placeholder
            finalstr.append(QLatin1Char('%') + QString::number(plords.at(i) + 1));
            #ifndef NDEBUG
            if (!partial)
                // spoof the message
                finalstr.append(QLatin1String("(I18N_ARGUMENT_MISSING)"));
            #endif
        }
        else
        // just fine
            finalstr.append(args.at(plords.at(i)));
    }
    finalstr.append(tsegs.last());

    #ifndef NDEBUG
    if (!partial)
    {
        // Check that there are no gaps in numbering sequence of placeholders.
        bool gaps = false;
        for (int i = 0; i < ords.size(); i++)
            if (!ords.at(i))
            {
                gaps = true;
                kDebug(173) << QString::fromLatin1("Placeholder %%1 skipped in message {%2}.")
                                      .arg(QString::number(i + 1), shortenMessage(trans));
            }
        // If no gaps, check for mismatch between number of unique placeholders and
        // actually supplied arguments.
        if (!gaps && ords.size() != args.size())
            kDebug(173) << QString::fromLatin1("%1 instead of %2 arguments to message {%3} supplied before conversion.")
                                  .arg(args.size()).arg(ords.size()).arg(shortenMessage(trans));

        // Some spoofs.
        if (gaps)
            finalstr.append(QLatin1String("(I18N_GAPS_IN_PLACEHOLDER_SEQUENCE)"));
        if (ords.size() < args.size())
            finalstr.append(QLatin1String("(I18N_EXCESS_ARGUMENTS_SUPPLIED)"));
        if (!plural.isEmpty() && !numberSet)
            finalstr.append(QLatin1String("(I18N_PLURAL_ARGUMENT_MISSING)"));
    }
    #endif

    return finalstr;
}

QString KLocalizedStringPrivate::postFormat (const QString &text,
                                             const QString &lang,
                                             const QString &ctxt) const
{
    QMutexLocker lock(staticsKLSPMutex());
    const KLocalizedStringPrivateStatics *s = staticsKLSP;
    // Transform any semantic markup into visual formatting.
    if (s->formatters.contains(lang)) {
        return s->formatters[lang]->format(text, ctxt);
    }
    return text;
}

static QString wrapNum (const QString &tag, const QString &numstr,
                        int fieldWidth, const QChar &fillChar)
{
    QString optag;
    if (fieldWidth != 0) {
        QString fillString = Qt::escape(fillChar);
        optag = QString::fromLatin1("<%1 width='%2' fill='%3'>")
                       .arg(tag, QString::number(fieldWidth), fillString);
    } else {
        optag = QString::fromLatin1("<%1>").arg(tag);
    }
    QString cltag = QString::fromLatin1("</%1>").arg(tag);
    return optag + numstr + cltag;
}

KLocalizedString KLocalizedString::subs (int a, int fieldWidth, int base,
                                         const QChar &fillChar) const
{
    KLocalizedString kls(*this);
    if (!kls.d->plural.isEmpty() && !kls.d->numberSet) {
        kls.d->number = static_cast<pluraln>(std::abs(a));
        kls.d->numberSet = true;
        kls.d->numberOrd = d->args.size();
    }
    kls.d->args.append(wrapNum(QString::fromLatin1(KUIT_NUMINTG), QString::number(a, base),
                               fieldWidth, fillChar));
    kls.d->vals.append(static_cast<intn>(a));
    return kls;
}

KLocalizedString KLocalizedString::subs (uint a, int fieldWidth, int base,
                                         const QChar &fillChar) const
{
    KLocalizedString kls(*this);
    if (!kls.d->plural.isEmpty() && !kls.d->numberSet) {
        kls.d->number = static_cast<pluraln>(a);
        kls.d->numberSet = true;
        kls.d->numberOrd = d->args.size();
    }
    kls.d->args.append(wrapNum(QString::fromLatin1(KUIT_NUMINTG), QString::number(a, base),
                               fieldWidth, fillChar));
    kls.d->vals.append(static_cast<uintn>(a));
    return kls;
}

KLocalizedString KLocalizedString::subs (long a, int fieldWidth, int base,
                                         const QChar &fillChar) const
{
    KLocalizedString kls(*this);
    if (!kls.d->plural.isEmpty() && !kls.d->numberSet) {
        kls.d->number = static_cast<pluraln>(std::abs(a));
        kls.d->numberSet = true;
        kls.d->numberOrd = d->args.size();
    }
    kls.d->args.append(wrapNum(QString::fromLatin1(KUIT_NUMINTG), QString::number(a, base),
                               fieldWidth, fillChar));
    kls.d->vals.append(static_cast<intn>(a));
    return kls;
}

KLocalizedString KLocalizedString::subs (ulong a, int fieldWidth, int base,
                                         const QChar &fillChar) const
{
    KLocalizedString kls(*this);
    if (!kls.d->plural.isEmpty() && !kls.d->numberSet) {
        kls.d->number = static_cast<pluraln>(a);
        kls.d->numberSet = true;
        kls.d->numberOrd = d->args.size();
    }
    kls.d->args.append(wrapNum(QString::fromLatin1(KUIT_NUMINTG), QString::number(a, base),
                               fieldWidth, fillChar));
    kls.d->vals.append(static_cast<uintn>(a));
    return kls;
}

KLocalizedString KLocalizedString::subs (qlonglong a, int fieldWidth, int base,
                                         const QChar &fillChar) const
{
    KLocalizedString kls(*this);
    if (!kls.d->plural.isEmpty() && !kls.d->numberSet) {
        kls.d->number = static_cast<pluraln>(qAbs(a));
        kls.d->numberSet = true;
        kls.d->numberOrd = d->args.size();
    }
    kls.d->args.append(wrapNum(QString::fromLatin1(KUIT_NUMINTG), QString::number(a, base),
                               fieldWidth, fillChar));
    kls.d->vals.append(static_cast<intn>(a));
    return kls;
}

KLocalizedString KLocalizedString::subs (qulonglong a, int fieldWidth, int base,
                                         const QChar &fillChar) const
{
    KLocalizedString kls(*this);
    if (!kls.d->plural.isEmpty() && !kls.d->numberSet) {
        kls.d->number = static_cast<pluraln>(a);
        kls.d->numberSet = true;
        kls.d->numberOrd = d->args.size();
    }
    kls.d->args.append(wrapNum(QString::fromLatin1(KUIT_NUMINTG), QString::number(a, base),
                               fieldWidth, fillChar));
    kls.d->vals.append(static_cast<uintn>(a));
    return kls;
}

KLocalizedString KLocalizedString::subs (double a, int fieldWidth,
                                         char format, int precision,
                                         const QChar &fillChar) const
{
    KLocalizedString kls(*this);
    kls.d->args.append(wrapNum(QString::fromLatin1(KUIT_NUMREAL),
                               QString::number(a, format, precision),
                               fieldWidth, fillChar));
    kls.d->vals.append(static_cast<realn>(a));
    return kls;
}

KLocalizedString KLocalizedString::subs (QChar a, int fieldWidth,
                                         const QChar &fillChar) const
{
    KLocalizedString kls(*this);
    kls.d->args.append(QString::fromLatin1("%1").arg(a, fieldWidth, fillChar));
    kls.d->vals.append(QString(a));
    return kls;
}

KLocalizedString KLocalizedString::subs (const QString &a, int fieldWidth,
                                         const QChar &fillChar) const
{
    KLocalizedString kls(*this);
    // if (!Qt::mightBeRichText(a)) { ...
    // Do not try to auto-escape non-rich-text alike arguments;
    // breaks compatibility with 4.0. Perhaps for KDE 5?
    // Perhaps bad idea alltogether (too much surprise)?
    kls.d->args.append(QString::fromLatin1("%1").arg(a, fieldWidth, fillChar));
    kls.d->vals.append(a);
    return kls;
}

KLocalizedString KLocalizedString::inContext (const QString &key,
                                              const QString &text) const
{
    KLocalizedString kls(*this);
    kls.d->dynctxt[key] = text;
    return kls;
}

KLocalizedString ki18n (const char* msg)
{
    return KLocalizedString(NULL, msg, NULL);
}

KLocalizedString ki18nc (const char* ctxt, const char *msg)
{
    return KLocalizedString(ctxt, msg, NULL);
}

KLocalizedString ki18np (const char* singular, const char* plural)
{
    return KLocalizedString(NULL, singular, plural);
}

KLocalizedString ki18ncp (const char* ctxt,
                          const char* singular, const char* plural)
{
    return KLocalizedString(ctxt, singular, plural);
}

void KLocalizedString::notifyCatalogsUpdated (const QStringList &languages,
                                              const QList<KCatalogName> &catalogs)
{
    QMutexLocker lock(staticsKLSPMutex());
    if (staticsKLSP.isDestroyed()) {
        return;
    }
    KLocalizedStringPrivateStatics *s = staticsKLSP;
    // Create visual formatters for each new language.
    foreach (const QString &lang, languages) {
        if (!s->formatters.contains(lang)) {
            s->formatters.insert(lang, new KuitSemantics(lang));
        }
    }
}
