/*
    SPDX-FileCopyrightText: 2002 Frerich Raabe <raabe@kde.org>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "glossary.h"
#include "khc_debug.h"
#include "view.h"

#include <KLocalizedString>
#include <KProcess>
#include <KXmlGuiWindow>

#include <QApplication>
#include <QDir>
#include <QFileInfo>
#include <QIcon>
#include <QStandardPaths>
#include <QStatusBar>
#include <QTreeWidgetItem>

#include <sys/stat.h>

#include <prefs.h>

using namespace KHC;

class SectionItem : public QTreeWidgetItem
{
public:
    SectionItem(QTreeWidgetItem *parent, const QString &text)
        : QTreeWidgetItem(parent)
    {
        setText(0, text);
        setIcon(0, QIcon::fromTheme(QStringLiteral("help-contents")));
    }
};

class EntryItem : public QTreeWidgetItem
{
public:
    EntryItem(SectionItem *parent, const QString &term, const QString &id)
        : QTreeWidgetItem(parent)
        , m_id(id)
    {
        setText(0, term);
    }

    QString id() const
    {
        return m_id;
    }

private:
    QString m_id;
};

bool Glossary::m_alreadyWarned = false;

Glossary::Glossary(QWidget *parent)
    : QTreeWidget(parent)
{
    m_initialized = false;
    setFrameStyle(QFrame::NoFrame);

    connect(this, &QTreeWidget::itemClicked, this, &Glossary::treeItemSelected);

    setHeaderHidden(true);
    setAllColumnsShowFocus(true);
    setRootIsDecorated(true);

    m_byTopicItem = new QTreeWidgetItem(this);
    m_byTopicItem->setText(0, i18n("By Topic"));
    m_byTopicItem->setIcon(0, QIcon::fromTheme(QStringLiteral("help-contents")));

    m_alphabItem = new QTreeWidgetItem(this);
    m_alphabItem->setText(0, i18n("Alphabetically"));
    m_alphabItem->setIcon(0, QIcon::fromTheme(QStringLiteral("character-set")));

    m_cacheFile = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1Char('/') + QStringLiteral("help/glossary.xml");
    QDir().mkpath(QFileInfo(m_cacheFile).absolutePath());

    m_sourceFile = View::langLookup(QStringLiteral("khelpcenter/glossary/index.docbook"));
}

void Glossary::showEvent(QShowEvent *event)
{
    if (!m_initialized) {
        if (cacheStatus() == NeedRebuild) {
            rebuildGlossaryCache();
        } else {
            buildGlossaryTree();
        }
        m_initialized = true;
    }
    QTreeWidget::showEvent(event);
}

Glossary::~Glossary()
{
    qDeleteAll(m_glossEntries);
}

const GlossaryEntry &Glossary::entry(const QString &id) const
{
    return *m_glossEntries[id];
}

Glossary::CacheStatus Glossary::cacheStatus() const
{
    if (!QFile::exists(m_cacheFile) || Prefs::cachedGlossary() != m_sourceFile || Prefs::cachedGlossaryTimestamp() != glossaryCTime())
        return NeedRebuild;

    return CacheOk;
}

int Glossary::glossaryCTime() const
{
    struct stat stat_buf;
    stat(QFile::encodeName(m_sourceFile).data(), &stat_buf);

    return stat_buf.st_ctime;
}

void Glossary::rebuildGlossaryCache()
{
    KXmlGuiWindow *mainWindow = dynamic_cast<KXmlGuiWindow *>(qobject_cast<QApplication *>(qApp)->activeWindow());
    if (mainWindow)
        mainWindow->statusBar()->showMessage(i18n("Rebuilding glossary cache..."));

    KProcess *meinproc = new KProcess;
    connect(meinproc, QOverload<int, QProcess::ExitStatus>::of(&KProcess::finished), this, &Glossary::meinprocFinished);
    *meinproc << QStandardPaths::findExecutable(QStringLiteral("meinproc6"));
    *meinproc << QStringLiteral("--output") << m_cacheFile;
    *meinproc << QStringLiteral("--stylesheet") << QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("khelpcenter/glossary.xslt"));
    *meinproc << m_sourceFile;

    meinproc->setOutputChannelMode(KProcess::OnlyStderrChannel);
    meinproc->start();
    if (!meinproc->waitForStarted()) {
        qCWarning(KHC_LOG) << "could not start process" << meinproc->program();
        if (mainWindow && !m_alreadyWarned) {
            ; // add warning message box with don't display again option
            m_alreadyWarned = true;
        }
        delete meinproc;
    }
}

void Glossary::meinprocFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
    KProcess *meinproc = static_cast<KProcess *>(sender());
    KXmlGuiWindow *mainWindow = dynamic_cast<KXmlGuiWindow *>(qobject_cast<QApplication *>(qApp)->activeWindow());

    if (exitStatus != QProcess::NormalExit || exitCode != 0) {
        qCWarning(KHC_LOG) << "running" << meinproc->program() << "failed with exitCode" << exitCode;
        qCWarning(KHC_LOG) << "stderr output:" << meinproc->readAllStandardError();
        if (mainWindow && !m_alreadyWarned) {
            ; // add warning message box with don't display again option
            m_alreadyWarned = true;
        }
        delete meinproc;
        return;
    }
    delete meinproc;

    if (!QFile::exists(m_cacheFile))
        return;

    Prefs::setCachedGlossary(m_sourceFile);
    Prefs::setCachedGlossaryTimestamp(glossaryCTime());
    Prefs::self()->save();

    m_status = CacheOk;

    if (mainWindow)
        mainWindow->statusBar()->showMessage(i18n("Rebuilding cache... done."), 2000);

    buildGlossaryTree();
}

void Glossary::buildGlossaryTree()
{
    QFile cacheFile(m_cacheFile);
    if (!cacheFile.open(QIODevice::ReadOnly))
        return;

    QDomDocument doc;
    if (!doc.setContent(&cacheFile))
        return;

    QHash<QChar, SectionItem *> alphabSections;

    QDomNodeList sectionNodes = doc.documentElement().elementsByTagName(QStringLiteral("section"));
    for (int i = 0; i < sectionNodes.count(); i++) {
        QDomElement sectionElement = sectionNodes.item(i).toElement();
        QString title = sectionElement.attribute(QStringLiteral("title"));
        SectionItem *topicSection = new SectionItem(m_byTopicItem, title);

        QDomNodeList entryNodes = sectionElement.elementsByTagName(QStringLiteral("entry"));
        for (int j = 0; j < entryNodes.count(); j++) {
            QDomElement entryElement = entryNodes.item(j).toElement();

            QString entryId = entryElement.attribute(QStringLiteral("id"));
            if (entryId.isNull())
                continue;

            QDomElement termElement = childElement(entryElement, QStringLiteral("term"));
            QString term = termElement.text().simplified();

            EntryItem *entry = new EntryItem(topicSection, term, entryId);
            m_idDict.insert(entryId, entry);

            const QChar first = term.at(0).toUpper();
            SectionItem *alphabSection = alphabSections.value(first);

            if (!alphabSection) {
                alphabSection = new SectionItem(m_alphabItem, QString(first));
                alphabSections.insert(first, alphabSection);
            }

            new EntryItem(alphabSection, term, entryId);

            QDomElement definitionElement = childElement(entryElement, QStringLiteral("definition"));
            QString definition = definitionElement.text().simplified();

            GlossaryEntryXRef::List seeAlso;

            QDomElement referencesElement = childElement(entryElement, QStringLiteral("references"));
            QDomNodeList referenceNodes = referencesElement.elementsByTagName(QStringLiteral("reference"));
            if (referenceNodes.count() > 0)
                for (int k = 0; k < referenceNodes.count(); k++) {
                    QDomElement referenceElement = referenceNodes.item(k).toElement();

                    QString term = referenceElement.attribute(QStringLiteral("term"));
                    QString id = referenceElement.attribute(QStringLiteral("id"));

                    seeAlso += GlossaryEntryXRef(term, id);
                }

            m_glossEntries.insert(entryId, new GlossaryEntry(entryId, term, definition, seeAlso));
        }
    }
    sortItems(0, Qt::AscendingOrder);
}

void Glossary::treeItemSelected(QTreeWidgetItem *item)
{
    if (!item)
        return;

    if (EntryItem *i = dynamic_cast<EntryItem *>(item))
        Q_EMIT entrySelected(entry(i->id()));

    item->setExpanded(!item->isExpanded());
}

QDomElement Glossary::childElement(const QDomElement &element, const QString &name)
{
    QDomElement e;
    for (e = element.firstChild().toElement(); !e.isNull(); e = e.nextSibling().toElement())
        if (e.tagName() == name)
            break;
    return e;
}

void Glossary::slotSelectGlossEntry(const QString &id)
{
    if (!m_idDict.contains(id))
        return;

    EntryItem *newItem = m_idDict.value(id);
    EntryItem *curItem = dynamic_cast<EntryItem *>(currentItem());
    if (curItem != nullptr) {
        if (curItem->id() == id) {
            return;
        }
        curItem->parent()->setExpanded(false);
    }

    setCurrentItem(newItem);
}

#include "moc_glossary.cpp"

// vim:ts=4:sw=4:et
