/*
    This file is part of the KDE libraries
    SPDX-FileCopyrightText: 1999 David Faure <faure@kde.org>

    SPDX-License-Identifier: LGPL-2.0-only
*/

#include "ksycoca.h"
#include "ksycocadict_p.h"
#include "ksycocaentry.h"
#include "ksycocaentry_p.h"
#include "ksycocafactory_p.h"
#include "ksycocatype.h"
#include "sycocadebug.h"

#include <QDebug>
#include <QHash>
#include <QIODevice>
#include <QThread>

class KSycocaFactoryPrivate
{
public:
    KSycocaFactoryPrivate()
    {
    }
    ~KSycocaFactoryPrivate()
    {
        delete m_sycocaDict;
    }

    int mOffset = 0;
    int m_sycocaDictOffset = 0;
    int m_beginEntryOffset = 0;
    int m_endEntryOffset = 0;
    KSycocaDict *m_sycocaDict = nullptr;
    // Used to avoid crashes when the factory failed to locate an actual data stream.
    // Mind that we need a backing buffer since callers also tap into the stream's QIODevice.
    QByteArray m_fallbackBuffer;
    QDataStream m_fallbackStream{m_fallbackBuffer};
};

KSycocaFactory::KSycocaFactory(KSycocaFactoryId factory_id, KSycoca *sycoca)
    : m_sycoca(sycoca)
    , d(new KSycocaFactoryPrivate)
{
    if (!m_sycoca->isBuilding()) {
        m_str = m_sycoca->findFactory(factory_id);
        if (m_str) {
            // Read position of index tables....
            qint32 i;
            (*m_str) >> i;
            d->m_sycocaDictOffset = i;
            (*m_str) >> i;
            d->m_beginEntryOffset = i;
            (*m_str) >> i;
            d->m_endEntryOffset = i;

            QDataStream *str = stream();
            qint64 saveOffset = str->device()->pos();
            // Init index tables
            d->m_sycocaDict = new KSycocaDict(str, d->m_sycocaDictOffset);
            saveOffset = str->device()->seek(saveOffset);
        } else {
            qWarning() << "Could not find factory with id" << int(factory_id)
                       << "in sycoca database, you must run kbuildsycoca first! Creating a fake stream to not crash.";
            m_str = &d->m_fallbackStream;
            m_entryDict = new KSycocaEntryDict;
            d->m_sycocaDict = new KSycocaDict;
            d->m_beginEntryOffset = 0;
            d->m_endEntryOffset = 0;
        }
    } else {
        // We are in kbuildsycoca -- build new database!
        m_entryDict = new KSycocaEntryDict;
        d->m_sycocaDict = new KSycocaDict;
        d->m_beginEntryOffset = 0;
        d->m_endEntryOffset = 0;

        // m_resourceList will be filled in by inherited constructors
    }
    m_sycoca->addFactory(this);
}

KSycocaFactory::~KSycocaFactory()
{
    delete m_entryDict;
}

void KSycocaFactory::saveHeader(QDataStream &str)
{
    // Write header
    str.device()->seek(d->mOffset);
    str << qint32(d->m_sycocaDictOffset);
    str << qint32(d->m_beginEntryOffset);
    str << qint32(d->m_endEntryOffset);
}

void KSycocaFactory::save(QDataStream &str)
{
    if (!m_entryDict) {
        return; // Error! Function should only be called when building database
    }
    if (!d->m_sycocaDict) {
        return; // Error!
    }

    d->mOffset = str.device()->pos(); // store position in member variable
    d->m_sycocaDictOffset = 0;

    // Write header (pass #1)
    saveHeader(str);

    d->m_beginEntryOffset = str.device()->pos();

    // Write all entries.
    int entryCount = 0;
    for (KSycocaEntry::Ptr entry : std::as_const(*m_entryDict)) {
        entry->d_ptr->save(str);
        entryCount++;
    }

    d->m_endEntryOffset = str.device()->pos();

    // Write indices...
    // Linear index
    str << qint32(entryCount);
    for (const KSycocaEntry::Ptr &entry : std::as_const(*m_entryDict)) {
        str << qint32(entry.data()->offset());
    }

    // Dictionary index
    d->m_sycocaDictOffset = str.device()->pos();
    d->m_sycocaDict->save(str);

    qint64 endOfFactoryData = str.device()->pos();

    // Update header (pass #2)
    saveHeader(str);

    // Seek to end.
    str.device()->seek(endOfFactoryData);
}

void KSycocaFactory::addEntry(const KSycocaEntry::Ptr &newEntry)
{
    if (!m_entryDict) {
        return; // Error! Function should only be called when
    }
    // building database

    if (!d->m_sycocaDict) {
        return; // Error!
    }

    KSycocaEntry::Ptr oldEntry = m_entryDict->value(newEntry->storageId());
    if (oldEntry) {
        // Already exists -> replace
        // We found a more-local override, e.g. ~/.local/share/applications/kde5/foo.desktop
        // So forget about the more global file.
        //
        // This can also happen with two .protocol files using the same protocol= entry.
        // If we didn't remove one here, we would end up asserting because save()
        // wasn't called on one of the entries.
        // qDebug() << "removing" << oldEntry.data() << oldEntry->entryPath() << "because of" << newEntry->entryPath() << "they have the same storageId" <<
        // newEntry->storageId();
        removeEntry(newEntry->storageId());
    }

    const QString name = newEntry->storageId();
    m_entryDict->insert(name, newEntry);
    d->m_sycocaDict->add(name, newEntry);
}

void KSycocaFactory::removeEntry(const QString &entryName)
{
    if (!m_entryDict) {
        return; // Error! Function should only be called when
    }
    // building database

    if (!d->m_sycocaDict) {
        return; // Error!
    }

    m_entryDict->remove(entryName);
    d->m_sycocaDict->remove(entryName); // O(N)
}

KSycocaEntry::List KSycocaFactory::allEntries() const
{
    KSycocaEntry::List list;

    // Assume we're NOT building a database

    QDataStream *str = stream();
    if (!str) {
        return list;
    }
    str->device()->seek(d->m_endEntryOffset);
    qint32 entryCount;
    (*str) >> entryCount;

    if (entryCount < 0 || entryCount > 8192) { // mind that new accepts a size_t (unsigned) but we are dealing with an int here
        qCWarning(SYCOCA) << QThread::currentThread() << "error detected in factory" << this << entryCount;
        KSycoca::flagError();
        return list;
    }

    // offsetList is needed because createEntry() modifies the stream position
    qint32 *offsetList = new qint32[entryCount];
    for (int i = 0; i < entryCount; i++) {
        (*str) >> offsetList[i];
    }

    for (int i = 0; i < entryCount; i++) {
        KSycocaEntry *newEntry = createEntry(offsetList[i]);
        if (newEntry) {
            list.append(KSycocaEntry::Ptr(newEntry));
        }
    }
    delete[] offsetList;
    return list;
}

int KSycocaFactory::offset() const
{
    return d->mOffset;
}

const KSycocaResourceList &KSycocaFactory::resourceList() const
{
    return m_resourceList;
}

const KSycocaDict *KSycocaFactory::sycocaDict() const
{
    return d->m_sycocaDict;
}

bool KSycocaFactory::isEmpty() const
{
    return d->m_beginEntryOffset == d->m_endEntryOffset;
}

QDataStream *KSycocaFactory::stream() const
{
    return m_str;
}

QStringList KSycocaFactory::allDirectories(const QString &subdir)
{
    // We don't use QStandardPaths::locateAll() because we want all paths, even those that don't exist yet
    QStringList topDirs = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
    for (auto &dir : topDirs) {
        dir += QLatin1Char('/') + subdir;
    }
    return topDirs;
}

void KSycocaFactory::virtual_hook(int /*id*/, void * /*data*/)
{
    /*BASE::virtual_hook( id, data );*/
}
