
// REFACT0R: Remote open/save file logic is duplicated in kpDocument.
// HITODO: Test when remote file support in KDE 4 stabilizes

/* This file is part of the KDE libraries
    SPDX-FileCopyrightText: 1999 Waldo Bastian (bastian@kde.org)
    SPDX-FileCopyrightText: 2007 Clarence Dang (dang@kde.org)

    SPDX-License-Identifier: LGPL-2.0-only
*/
//-----------------------------------------------------------------------------
// KDE color collection

#define DEBUG_KP_COLOR_COLLECTION 0

#include "kpColorCollection.h"

#include "kpUrlFormatter.h"

#include "kpLogCategories.h"
#include <KIO/FileCopyJob>
#include <KIO/StoredTransferJob>
#include <KJobWidgets>
#include <KLocalizedString>
#include <KMessageBox>

#include <QDir>
#include <QFile>
#include <QSaveFile>
#include <QStandardPaths>
#include <QTemporaryFile>
#include <QTextStream>
#include <QUrl>

struct ColorNode {
    ColorNode(const QColor &c, const QString &n)
        : color(c)
        , name(n)
    {
    }

    QColor color;
    QString name;
};

//---------------------------------------------------------------------

Q_LOGGING_CATEGORY(kpLogColorCollection, "kp.colorCollection")

// BEGIN kpColorCollectionPrivate
class kpColorCollectionPrivate
{
public:
    kpColorCollectionPrivate();
    kpColorCollectionPrivate(const kpColorCollectionPrivate &);

    QList<ColorNode> colorList;
    QString name;
    QString desc;
    kpColorCollection::Editable editable;
};

kpColorCollectionPrivate::kpColorCollectionPrivate()
    : editable(kpColorCollection::Yes)
{
}

kpColorCollectionPrivate::kpColorCollectionPrivate(const kpColorCollectionPrivate &p)
    : colorList(p.colorList)
    , name(p.name)
    , desc(p.desc)
    , editable(p.editable)
{
}
// END kpColorCollectionPrivate

//---------------------------------------------------------------------

QStringList kpColorCollection::installedCollections()
{
    QStringList paletteList;

    QStringList paths = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, QStringLiteral("colors"), QStandardPaths::LocateDirectory);
    for (const auto &path : paths) {
        paletteList.append(QDir(path).entryList(QStringList(), QDir::Files));
    }

    return paletteList;
}

kpColorCollection::kpColorCollection()
{
    d = new kpColorCollectionPrivate();
}

kpColorCollection::kpColorCollection(const kpColorCollection &p)
{
    d = new kpColorCollectionPrivate(*p.d);
}

kpColorCollection::~kpColorCollection()
{
    // Need auto-save?
    delete d;
}

static void CouldNotOpenDialog(const QUrl &url, QWidget *parent)
{
    KMessageBox::error(parent, i18n("Could not open color palette \"%1\".", kpUrlFormatter::PrettyFilename(url)));
}

// TODO: Set d->editable?
bool kpColorCollection::open(const QUrl &url, QWidget *parent)
{
    if (url.isEmpty()) {
        return false;
    }

    KIO::StoredTransferJob *job = KIO::storedGet(url);
    KJobWidgets::setWindow(job, parent);

    if (!job->exec()) {
#if DEBUG_KP_COLOR_COLLECTION
        qCDebug(kpLogColorCollection) << "\tcould not download";
#endif
        ::CouldNotOpenDialog(url, parent);
        return false;
    }

    const QByteArray &data = job->data();
    QTextStream stream(data);

    // Read first line
    // Expected "GIMP Palette" or "KDE RGB Palette" or "KDE RGBA Palette"
    QString line = stream.readLine();
    if (line.indexOf(QLatin1String(" Palette")) == -1) {
        KMessageBox::error(parent,
                           i18n("Could not open color palette \"%1\" - unsupported format.\n"
                                "The file may be corrupt.",
                                kpUrlFormatter::PrettyFilename(url)));
        return false;
    }

    bool hasAlpha = line == QLatin1String("KDE RGBA Palette"); // new format includes alpha

    QList<ColorNode> newColorList;
    QString newDesc;

    while (!stream.atEnd()) {
        line = stream.readLine();
        if (!line.isEmpty() && (line[0] == QLatin1Char('#'))) {
            // This is a comment line
            line = line.mid(1); // Strip '#'
            line = line.trimmed(); // Strip remaining white space..
            if (!line.isEmpty()) {
                newDesc += line + QLatin1Char('\n'); // Add comment to description
            }
        } else {
            // This is a color line, hopefully
            line = line.trimmed();
            if (line.isEmpty())
                continue;
            int r, g, b, a = 255;
            int pos = 0;
            bool ok = false;

            if (hasAlpha)
                ok = (sscanf(line.toLatin1().constData(), "%d %d %d %d%n", &r, &g, &b, &a, &pos) >= 4);
            else
                ok = (sscanf(line.toLatin1().constData(), "%d %d %d%n", &r, &g, &b, &pos) >= 3);

            if (ok) {
                r = qBound(0, r, 255);
                g = qBound(0, g, 255);
                b = qBound(0, b, 255);
                a = qBound(0, a, 255);
                QString name = line.mid(pos).trimmed();
                newColorList.append(ColorNode(QColor(r, g, b, a), name));
            }
        }
    }

    d->colorList = newColorList;
    d->name.clear();
    d->desc = newDesc;

    return true;
}

static void CouldNotOpenKDEDialog(const QString &name, QWidget *parent)
{
    KMessageBox::error(parent, i18n("Could not open KDE color palette \"%1\".", name));
}

bool kpColorCollection::openKDE(const QString &name, QWidget *parent)
{
#if DEBUG_KP_COLOR_COLLECTION
    qCDebug(kpLogColorCollection) << "name=" << name;
#endif

    if (name.isEmpty()) {
#if DEBUG_KP_COLOR_COLLECTION
        qCDebug(kpLogColorCollection) << "name.isEmpty";
#endif
        ::CouldNotOpenKDEDialog(name, parent);
        return false;
    }

    QString filename = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("colors/") + name);
    if (filename.isEmpty()) {
#if DEBUG_KP_COLOR_COLLECTION
        qCDebug(kpLogColorCollection) << "could not find file";
#endif
        ::CouldNotOpenKDEDialog(name, parent);
        return false;
    }

    // (this will pop up an error dialog on failure)
    if (!open(QUrl::fromLocalFile(filename), parent)) {
#if DEBUG_KP_COLOR_COLLECTION
        qCDebug(kpLogColorCollection) << "could not open";
#endif
        return false;
    }

    d->name = name;
#if DEBUG_KP_COLOR_COLLECTION
    qCDebug(kpLogColorCollection) << "opened";
#endif
    return true;
}

static void CouldNotSaveDialog(const QUrl &url, QWidget *parent)
{
    // TODO: use file.errorString()
    KMessageBox::error(parent, i18n("Could not save color palette as \"%1\".", kpUrlFormatter::PrettyFilename(url)));
}

static void SaveToFile(kpColorCollectionPrivate *d, QIODevice *device)
{
    // HITODO: QTextStream can fail but does not report errors.
    //         Bug in KColorCollection too.
    QTextStream str(device);

    QString description = d->desc.trimmed();
    description = QLatin1Char('#') + description.split(QLatin1Char('\n'), Qt::KeepEmptyParts).join(QLatin1String("\n#"));

    str << "KDE RGBA Palette\n";
    str << description << "\n";
    for (const auto &node : d->colorList) {
        // Added for KolourPaint.
        if (!node.color.isValid())
            continue;

        int r, g, b, a;
        node.color.getRgb(&r, &g, &b, &a);
        str << r << " " << g << " " << b << " " << a << " " << node.name << "\n";
    }

    str.flush();
}

bool kpColorCollection::saveAs(const QUrl &url, QWidget *parent) const
{
    if (url.isLocalFile()) {
        const QString filename = url.toLocalFile();

        // sync: All failure exit paths _must_ call QSaveFile::cancelWriting() or
        //       else, the QSaveFile destructor will overwrite the file,
        //       <filename>, despite the failure.
        QSaveFile atomicFileWriter(filename);
        {
            if (!atomicFileWriter.open(QIODevice::WriteOnly)) {
                // We probably don't need this as <filename> has not been
                // opened.
                atomicFileWriter.cancelWriting();

#if DEBUG_KP_COLOR_COLLECTION
                qCDebug(kpLogColorCollection) << "\treturning false because could not open QSaveFile"
                                              << " error=" << atomicFileWriter.error();
#endif
                ::CouldNotSaveDialog(url, parent);
                return false;
            }

            // Write to local temporary file.
            ::SaveToFile(d, &atomicFileWriter);

            // Atomically overwrite local file with the temporary file
            // we saved to.
            if (!atomicFileWriter.commit()) {
                atomicFileWriter.cancelWriting();

#if DEBUG_KP_COLOR_COLLECTION
                qCDebug(kpLogColorCollection) << "\tcould not close QSaveFile";
#endif
                ::CouldNotSaveDialog(url, parent);
                return false;
            }
        } // sync QSaveFile.cancelWriting()
    }
    // Remote file?
    else {
        // Create temporary file that is deleted when the variable goes
        // out of scope.
        QTemporaryFile tempFile;
        if (!tempFile.open()) {
#if DEBUG_KP_COLOR_COLLECTION
            qCDebug(kpLogColorCollection) << "\treturning false because could not open tempFile";
#endif
            ::CouldNotSaveDialog(url, parent);
            return false;
        }

        // Write to local temporary file.
        ::SaveToFile(d, &tempFile);

        // Collect name of temporary file now, as QTemporaryFile::fileName()
        // stops working after close() is called.
        const QString tempFileName = tempFile.fileName();
#if DEBUG_KP_COLOR_COLLECTION
        qCDebug(kpLogColorCollection) << "\ttempFileName='" << tempFileName << "'";
#endif
        Q_ASSERT(!tempFileName.isEmpty());

        tempFile.close();
        if (tempFile.error() != QFile::NoError) {
#if DEBUG_KP_COLOR_COLLECTION
            qCDebug(kpLogColorCollection) << "\treturning false because could not close";
#endif
            ::CouldNotSaveDialog(url, parent);
            return false;
        }

        // Copy local temporary file to overwrite remote.
        KIO::FileCopyJob *job = KIO::file_copy(QUrl::fromLocalFile(tempFileName), url, -1, KIO::Overwrite);
        KJobWidgets::setWindow(job, parent);
        if (!job->exec()) {
#if DEBUG_KP_COLOR_COLLECTION
            qCDebug(kpLogColorCollection) << "\treturning false because could not upload";
#endif
            ::CouldNotSaveDialog(url, parent);
            return false;
        }
    }

    d->name.clear();
    return true;
}

QString kpColorCollection::description() const
{
    return d->desc;
}

void kpColorCollection::setDescription(const QString &desc)
{
    d->desc = desc;
}

QString kpColorCollection::name() const
{
    return d->name;
}

void kpColorCollection::setName(const QString &name)
{
    d->name = name;
}

kpColorCollection::Editable kpColorCollection::editable() const
{
    return d->editable;
}

void kpColorCollection::setEditable(Editable editable)
{
    d->editable = editable;
}

int kpColorCollection::count() const
{
    return (int)d->colorList.count();
}

void kpColorCollection::resize(int newCount)
{
    if (newCount == count())
        return;
    else if (newCount < count()) {
        d->colorList.erase(d->colorList.begin() + newCount, d->colorList.end());
    } else if (newCount > count()) {
        while (newCount > count()) {
            const int ret = addColor(QColor(), QString() /*color name*/);
            Q_ASSERT(ret == count() - 1);
        }
    }
}

kpColorCollection &kpColorCollection::operator=(const kpColorCollection &p)
{
    if (&p == this)
        return *this;
    d->colorList = p.d->colorList;
    d->name = p.d->name;
    d->desc = p.d->desc;
    d->editable = p.d->editable;
    return *this;
}

QColor kpColorCollection::color(int index) const
{
    if ((index < 0) || (index >= count()))
        return {};

    return d->colorList[index].color;
}

int kpColorCollection::findColor(const QColor &color) const
{
    for (int i = 0; i < d->colorList.size(); ++i) {
        if (d->colorList[i].color == color)
            return i;
    }
    return -1;
}

QString kpColorCollection::name(int index) const
{
    if ((index < 0) || (index >= count()))
        return {};

    return d->colorList[index].name;
}

QString kpColorCollection::name(const QColor &color) const
{
    return name(findColor(color));
}

int kpColorCollection::addColor(const QColor &newColor, const QString &newColorName)
{
    d->colorList.append(ColorNode(newColor, newColorName));
    return count() - 1;
}

int kpColorCollection::changeColor(int index, const QColor &newColor, const QString &newColorName)
{
    if ((index < 0) || (index >= count()))
        return -1;

    ColorNode &node = d->colorList[index];
    node.color = newColor;
    node.name = newColorName;

    return index;
}

int kpColorCollection::changeColor(const QColor &oldColor, const QColor &newColor, const QString &newColorName)
{
    return changeColor(findColor(oldColor), newColor, newColorName);
}
