// -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; c-brace-offset: 0; -*-
//
// fontpool.cpp
//
// SPDX-FileCopyrightText: 2001-2005 Stefan Kebekus
// SPDX-License-Identifier: GPL-2.0-or-later

#include <config.h>

#include "TeXFont.h"
#include "debug_dvi.h"
#include "fontpool.h"

#include <KLocalizedString>

#include <QApplication>
#include <QPainter>
#include <QStandardPaths>

#include <cmath>
#include <math.h>

// #define DEBUG_FONTPOOL

// List of permissible MetaFontModes which are supported by kdvi.

// const char *MFModes[]       = { "cx", "ljfour", "lexmarks" };
// const char *MFModenames[]   = { "Canon CX", "LaserJet 4", "Lexmark S" };
// const int   MFResolutions[] = { 300, 600, 1200 };

#ifdef PERFORMANCE_MEASUREMENT
QTime fontPoolTimer;
bool fontPoolTimerFlag;
#endif

fontPool::fontPool(bool useFontHinting)
{
#ifdef DEBUG_FONTPOOL
    qCDebug(OkularDviDebug) << "fontPool::fontPool() called";
#endif

    setObjectName(QStringLiteral("Font Pool"));

    displayResolution_in_dpi = 100.0; // A not-too-bad-default
    useFontHints = useFontHinting;
    CMperDVIunit = 0;
    extraSearchPath.clear();

#ifdef HAVE_FREETYPE
    // Initialize the Freetype Library
    if (FT_Init_FreeType(&FreeType_library) != 0) {
        qCCritical(OkularDviDebug) << "Cannot load the FreeType library. KDVI proceeds without FreeType support.";
        FreeType_could_be_loaded = false;
    } else {
        FreeType_could_be_loaded = true;
    }
#endif

    // Check if the QT library supports the alpha channel of
    // QImages. Experiments show that --depending of the configuration
    // of QT at compile and runtime or the availability of the XFt
    // extension, alpha channels are either supported, or silently
    // ignored.
    QImage start(1, 1, QImage::Format_ARGB32); // Generate a 1x1 image, black with alpha=0x10
    quint32 *destScanLine = reinterpret_cast<quint32 *>(start.scanLine(0));
    *destScanLine = 0x80000000;
    QPixmap intermediate = QPixmap::fromImage(start);
    QPixmap dest(1, 1);
    dest.fill(Qt::white);
    QPainter paint(&dest);
    paint.drawPixmap(0, 0, intermediate);
    paint.end();
    start = dest.toImage().convertToFormat(QImage::Format_ARGB32);
    quint8 result = *(start.scanLine(0)) & 0xff;

    if ((result == 0xff) || (result == 0x00)) {
#ifdef DEBUG_FONTPOOL
        qCDebug(OkularDviDebug) << "fontPool::fontPool(): QPixmap does not support the alpha channel";
#endif
        QPixmapSupportsAlpha = false;
    } else {
#ifdef DEBUG_FONTPOOL
        qCDebug(OkularDviDebug) << "fontPool::fontPool(): QPixmap supports the alpha channel";
#endif
        QPixmapSupportsAlpha = true;
    }
}

fontPool::~fontPool()
{
#ifdef DEBUG_FONTPOOL
    qCDebug(OkularDviDebug) << "fontPool::~fontPool() called";
#endif

    // need to manually clear the fonts _before_ freetype gets unloaded
    qDeleteAll(fontList);
    fontList.clear();

#ifdef HAVE_FREETYPE
    if (FreeType_could_be_loaded == true) {
        FT_Done_FreeType(FreeType_library);
    }
#endif
}

TeXFontDefinition *fontPool::appendx(const QString &fontname, quint32 checksum, quint32 scale, double enlargement)
{
    // Reuse font if possible: check if a font with that name and
    // natural resolution is already in the fontpool, and use that, if
    // possible.
    QList<TeXFontDefinition *>::iterator it_fontp = fontList.begin();
    for (; it_fontp != fontList.end(); ++it_fontp) {
        TeXFontDefinition *fontp = *it_fontp;
        if ((fontname == fontp->fontname) && ((int)(enlargement * 1000.0 + 0.5)) == (int)(fontp->enlargement * 1000.0 + 0.5)) {
            // if font is already in the list
            fontp->mark_as_used();
            return fontp;
        }
    }

    // If font doesn't exist yet, we have to generate a new font.

    double displayResolution = displayResolution_in_dpi;

    TeXFontDefinition *fontp = new TeXFontDefinition(fontname, displayResolution * enlargement, checksum, scale, this, enlargement);
    if (fontp == nullptr) {
        qCCritical(OkularDviDebug) << "Could not allocate memory for a font structure";
        exit(0);
    }
    fontList.append(fontp);

#ifdef PERFORMANCE_MEASUREMENT
    fontPoolTimer.start();
    fontPoolTimerFlag = false;
#endif

    // Now start kpsewhich/MetaFont, etc. if necessary
    return fontp;
}

bool fontPool::areFontsLocated()
{
#ifdef DEBUG_FONTPOOL
    qCDebug(OkularDviDebug) << "fontPool::areFontsLocated() called";
#endif

    // Is there a font whose name we did not try to find out yet?
    QList<TeXFontDefinition *>::const_iterator cit_fontp = fontList.constBegin();
    for (; cit_fontp != fontList.constEnd(); ++cit_fontp) {
        const TeXFontDefinition *fontp = *cit_fontp;
        if (!fontp->isLocated()) {
            return false;
        }
    }

#ifdef DEBUG_FONTPOOL
    qCDebug(OkularDviDebug) << "... yes, all fonts are located (but not necessarily loaded).";
#endif
    return true; // That says that all fonts are located.
}

void fontPool::locateFonts()
{
    kpsewhichOutput.clear();

    // First, we try and find those fonts which exist on disk
    // already. If virtual fonts are found, they will add new fonts to
    // the list of fonts whose font files need to be located, so that we
    // repeat the lookup.
    bool vffound;
    do {
        vffound = false;
        locateFonts(false, false, &vffound);
    } while (vffound);

    // If still not all fonts are found, look again, this time with
    // on-demand generation of PK fonts enabled.
    if (!areFontsLocated()) {
        locateFonts(true, false);
    }

    // If still not all fonts are found, we look for TFM files as a last
    // resort, so that we can at least draw filled rectangles for
    // characters.
    if (!areFontsLocated()) {
        locateFonts(false, true);
    }

    // If still not all fonts are found, we give up. We mark all fonts
    // as 'located', so that we won't look for them any more, and
    // present an error message to the user.
    if (!areFontsLocated()) {
        markFontsAsLocated();
        Q_EMIT error(i18n("<qt><p>Okular was not able to locate all the font files "
                          "which are necessary to display the current DVI file. "
                          "Your document might be unreadable.</p>"
                          "<p><small><b>PATH:</b> %1</small></p>"
                          "<p><small>%2</small></p></qt>",
                          QString::fromLocal8Bit(qgetenv("PATH")),
                          kpsewhichOutput.replace(QLatin1String("\n"), QLatin1String("<br/>"))),
                     -1);
    }
}

void fontPool::locateFonts(bool makePK, bool locateTFMonly, bool *virtualFontsFound)
{
    // Make sure kpsewhich is in PATH and not just in the CWD
    static const QString kpsewhichFullPath = QStandardPaths::findExecutable(QStringLiteral("kpsewhich"));
    if (kpsewhichFullPath.isEmpty()) {
        return;
    }

    // Set up the kpsewhich process. If pass == 0, look for vf-fonts and
    // disable automatic font generation as vf-fonts can't be
    // generated. If pass == 0, enable font generation, if it was
    // enabled globally.

    // Now generate the command line for the kpsewhich
    // program. Unfortunately, this can be rather long and involved...
    QStringList kpsewhich_args;
    kpsewhich_args << QStringLiteral("--dpi") << QStringLiteral("1200") << QStringLiteral("--mode") << QStringLiteral("lexmarks");

    // Disable automatic pk-font generation.
    kpsewhich_args << QString::fromLocal8Bit(makePK ? "--mktex" : "--no-mktex") << QStringLiteral("pk");

    // Names of fonts that shall be located
    quint16 numFontsInJob = 0;
    QList<TeXFontDefinition *>::const_iterator cit_fontp = fontList.constBegin();
    for (; cit_fontp != fontList.constEnd(); ++cit_fontp) {
        TeXFontDefinition *fontp = *cit_fontp;
        if (!fontp->isLocated()) {
            numFontsInJob++;

            if (locateTFMonly == true) {
                kpsewhich_args << QStringLiteral("%1.tfm").arg(fontp->fontname);
            } else {
#ifdef HAVE_FREETYPE
                if (FreeType_could_be_loaded == true) {
                    const QString &filename = fontsByTeXName.findFileName(fontp->fontname);
                    if (!filename.isEmpty()) {
                        kpsewhich_args << QStringLiteral("%1").arg(filename);
                    }
                }
#endif
                kpsewhich_args << QStringLiteral("%1.vf").arg(fontp->fontname) << QStringLiteral("%1.1200pk").arg(fontp->fontname);
            }
        }
    }

    if (numFontsInJob == 0) {
        return;
    }

    // If PK fonts are generated, the kpsewhich command will re-route
    // the output of MetaFont into its stderr. Here we make sure this
    // output is intercepted and parsed.
    kpsewhich_ = std::make_unique<QProcess>();
    connect(kpsewhich_.get(), &QProcess::readyReadStandardError, this, &fontPool::mf_output_receiver);

    // Now run... kpsewhich. In case of error, kick up a fuss.
    // This string is not going to be quoted, as it might be were it
    // a real command line, but who cares?
    const QString kpsewhich_exe = QStringLiteral("kpsewhich");
    kpsewhichOutput += QStringLiteral("<b>") + kpsewhich_exe + QLatin1Char(' ') + kpsewhich_args.join(QStringLiteral(" ")) + QStringLiteral("</b>");

    kpsewhich_->start(kpsewhichFullPath, kpsewhich_args, QIODevice::ReadOnly | QIODevice::Text);
    if (!kpsewhich_->waitForStarted()) {
        QApplication::restoreOverrideCursor();
        Q_EMIT error(i18n("<qt><p>There were problems running <em>kpsewhich</em>. As a result, "
                          "some font files could not be located, and your document might be unreadable.<br/>"
                          "Possible reason: the <em>kpsewhich</em> program is perhaps not installed on your system, "
                          "or it cannot be found in the current search path.</p>"
                          "<p><small><b>PATH:</b> %1</small></p>"
                          "<p><small>%2</small></p></qt>",
                          QString::fromLocal8Bit(qgetenv("PATH")),
                          kpsewhichOutput.replace(QLatin1String("\n"), QLatin1String("<br/>"))),
                     -1);

        // This makes sure the we don't try to run kpsewhich again
        markFontsAsLocated();
        kpsewhich_.reset();
        return;
    }
    // We wait here while the external program runs concurrently.
    kpsewhich_->waitForFinished();

    // Handle fatal errors.
    int const kpsewhich_exit_code = kpsewhich_->exitCode();
    if (kpsewhich_exit_code < 0) {
        Q_EMIT warning(i18n("<qt>The font generation by <em>kpsewhich</em> was aborted (exit code %1, error %2). As a "
                            "result, some font files could not be located, and your document might be unreadable.</qt>",
                            kpsewhich_exit_code,
                            kpsewhich_->errorString()),
                       -1);

        // This makes sure the we don't try to run kpsewhich again
        if (makePK == false) {
            markFontsAsLocated();
        }
    }

    // Create a list with all filenames found by the kpsewhich program.
    const QStringList fileNameList = QString::fromLocal8Bit(kpsewhich_->readAll()).split(QLatin1Char('\n'), Qt::SkipEmptyParts);

    // Now associate the file names found with the fonts
    QList<TeXFontDefinition *>::iterator it_fontp = fontList.begin();
    for (; it_fontp != fontList.end(); ++it_fontp) {
        TeXFontDefinition *fontp = *it_fontp;

        if (fontp->filename.isEmpty() == true) {
            QStringList matchingFiles;
#ifdef HAVE_FREETYPE
            const QString &fn = fontsByTeXName.findFileName(fontp->fontname);
            if (!fn.isEmpty()) {
                matchingFiles = fileNameList.filter(fn);
            }
#endif
            if (matchingFiles.isEmpty() == true) { /* cppcheck-suppress knownConditionTrueFalse */
                matchingFiles += fileNameList.filter(QLatin1Char('/') + fontp->fontname + QLatin1Char('.'));
            }

            if (matchingFiles.isEmpty() != true) {
#ifdef DEBUG_FONTPOOL
                qCDebug(OkularDviDebug) << "Associated " << fontp->fontname << " to " << matchingFiles.first();
#endif
                QString fname = matchingFiles.first();
                fontp->fontNameReceiver(fname);
                fontp->flags |= TeXFontDefinition::FONT_KPSE_NAME;
                if (fname.endsWith(QLatin1String(".vf"))) {
                    if (virtualFontsFound != nullptr) {
                        *virtualFontsFound = true;
                    }
                    // Constructing a virtual font will most likely insert other
                    // fonts into the fontList. After that, fontList.next() will
                    // no longer work. It is therefore safer to start over.
                    it_fontp = fontList.begin();
                    continue;
                }
            }
        } // of if (fontp->filename.isEmpty() == true)
    }
    kpsewhich_.reset();
}

void fontPool::setCMperDVIunit(double _CMperDVI)
{
#ifdef DEBUG_FONTPOOL
    qCDebug(OkularDviDebug) << "fontPool::setCMperDVIunit( " << _CMperDVI << " )";
#endif

    if (CMperDVIunit == _CMperDVI) {
        return;
    }

    CMperDVIunit = _CMperDVI;

    QList<TeXFontDefinition *>::iterator it_fontp = fontList.begin();
    for (; it_fontp != fontList.end(); ++it_fontp) {
        TeXFontDefinition *fontp = *it_fontp;
        fontp->setDisplayResolution(displayResolution_in_dpi * fontp->enlargement);
    }
}

void fontPool::setDisplayResolution(double _displayResolution_in_dpi)
{
#ifdef DEBUG_FONTPOOL
    qCDebug(OkularDviDebug) << "fontPool::setDisplayResolution( displayResolution_in_dpi=" << _displayResolution_in_dpi << " ) called";
#endif

    // Ignore minute changes by less than 2 DPI. The difference would
    // hardly be visible anyway. That saves a lot of re-painting,
    // e.g. when the user resizes the window, and a flickery mouse
    // changes the window size by 1 pixel all the time.
    if (fabs(displayResolution_in_dpi - _displayResolution_in_dpi) <= 2.0) {
#ifdef DEBUG_FONTPOOL
        qCDebug(OkularDviDebug) << "fontPool::setDisplayResolution(...): resolution wasn't changed. Aborting.";
#endif
        return;
    }

    displayResolution_in_dpi = _displayResolution_in_dpi;
    double displayResolution = displayResolution_in_dpi;

    QList<TeXFontDefinition *>::iterator it_fontp = fontList.begin();
    for (; it_fontp != fontList.end(); ++it_fontp) {
        TeXFontDefinition *fontp = *it_fontp;
        fontp->setDisplayResolution(displayResolution * fontp->enlargement);
    }

    // Do something that causes re-rendering of the dvi-window
    /*@@@@
    Q_EMIT fonts_have_been_loaded(this);
    */
}

void fontPool::markFontsAsLocated()
{
    QList<TeXFontDefinition *>::iterator it_fontp = fontList.begin();
    for (; it_fontp != fontList.end(); ++it_fontp) {
        TeXFontDefinition *fontp = *it_fontp;
        fontp->markAsLocated();
    }
}

void fontPool::mark_fonts_as_unused()
{
#ifdef DEBUG_FONTPOOL
    qCDebug(OkularDviDebug) << "fontPool::mark_fonts_as_unused() called";
#endif

    QList<TeXFontDefinition *>::iterator it_fontp = fontList.begin();
    for (; it_fontp != fontList.end(); ++it_fontp) {
        TeXFontDefinition *fontp = *it_fontp;
        fontp->flags &= ~TeXFontDefinition::FONT_IN_USE;
    }
}

void fontPool::release_fonts()
{
#ifdef DEBUG_FONTPOOL
    qCDebug(OkularDviDebug) << "Release_fonts";
#endif

    QMutableListIterator<TeXFontDefinition *> it_fontp(fontList);
    while (it_fontp.hasNext()) {
        TeXFontDefinition *fontp = it_fontp.next();
        if ((fontp->flags & TeXFontDefinition::FONT_IN_USE) != TeXFontDefinition::FONT_IN_USE) {
            delete fontp;
            it_fontp.remove();
        }
    }
}

void fontPool::mf_output_receiver()
{
    if (kpsewhich_) {
        const QString output_data = QString::fromLocal8Bit(kpsewhich_->readAllStandardError());
        kpsewhichOutput.append(output_data);
        MetafontOutput.append(output_data);
    }

    // We'd like to print only full lines of text.
    int numleft;
    while ((numleft = MetafontOutput.indexOf(QLatin1Char('\n'))) != -1) {
        QString line = MetafontOutput.left(numleft + 1);
#ifdef DEBUG_FONTPOOL
        qCDebug(OkularDviDebug) << "MF OUTPUT RECEIVED: " << line;
#endif
        // If the Output of the kpsewhich program contains a line starting
        // with "kpathsea:", this means that a new MetaFont-run has been
        // started. We filter these lines out and update the display
        // accordingly.
        int startlineindex = line.indexOf(QStringLiteral("kpathsea:"));
        if (startlineindex != -1) {
            int endstartline = line.indexOf(QStringLiteral("\n"), startlineindex);
            QString startLine = line.mid(startlineindex, endstartline - startlineindex);

            // The last word in the startline is the name of the font which we
            // are generating. The second-to-last word is the resolution in
            // dots per inch. Display this info in the text label below the
            // progress bar.
            int lastblank = startLine.lastIndexOf(QLatin1Char(' '));
            QString fontName = startLine.mid(lastblank + 1);
            int secondblank = startLine.lastIndexOf(QLatin1Char(' '), lastblank - 1);
            QString dpi = startLine.mid(secondblank + 1, lastblank - secondblank - 1);

            Q_EMIT warning(i18n("Currently generating %1 at %2 dpi…", fontName, dpi), -1);
        }
        MetafontOutput = MetafontOutput.remove(0, numleft + 1);
    }
}
