/*

Copyright 2018 S. Razi Alavizadeh
Copyright 2012-2013, 2015-2018 Adam Reichold

This file is part of qpdfview.

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

qpdfview 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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with qpdfview.  If not, see <http://www.gnu.org/licenses/>.

*/

#include "pluginhandler.h"

#include <QApplication>
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QImageReader>
#include <QMessageBox>
#include <QPluginLoader>
#include <QProcess>
#include <QTemporaryFile>

#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)

#include <QMimeDatabase>

#endif // QT_VERSION

#ifdef WITH_MAGIC

#include <magic.h>

#endif // WITH_MAGIC

#include "model.h"

namespace
{

using namespace qpdfview;

Plugin* loadStaticPlugin(const QString& objectName)
{
    foreach(QObject* object, QPluginLoader::staticInstances())
    {
        if(object->objectName() == objectName)
        {
            if(Plugin* plugin = qobject_cast< Plugin* >(object))
            {
                return plugin;
            }
        }
    }

    qCritical() << "Could not load static plug-in:" << objectName;

    return 0;
}

Plugin* loadPlugin(const QString& fileName)
{
    QPluginLoader pluginLoader;

#ifdef PLUGIN_RESOLVE_ALL

    pluginLoader.setLoadHints(QLibrary::ResolveAllSymbolsHint);

#endif // PLUGIN_RESOLVE_ALL

    const QString localFileName = QDir(QApplication::applicationDirPath()).absoluteFilePath(fileName);
    pluginLoader.setFileName(localFileName);

    if(!pluginLoader.load())
    {
        const QString localErrorString = pluginLoader.errorString();

        const QString globalFileName = QDir(PLUGIN_INSTALL_PATH).absoluteFilePath(fileName);
        pluginLoader.setFileName(globalFileName);

        if(!pluginLoader.load())
        {
            const QString globalErrorString = pluginLoader.errorString();

            qCritical() << "Could not load local plug-in:" << localFileName;
            qCritical() << localErrorString;

            qCritical() << "Could not load global plug-in:" << globalFileName;
            qCritical() << globalErrorString;

            return 0;
        }
    }

    Plugin* plugin = qobject_cast< Plugin* >(pluginLoader.instance());

    if(plugin == 0)
    {
        qCritical() << "Could not instantiate plug-in:" << pluginLoader.fileName();
        qCritical() << pluginLoader.errorString();
    }

    return plugin;
}

QStringList supportedImageFormats()
{
    QStringList formats;

    foreach(const QByteArray& format, QImageReader::supportedImageFormats())
    {
        const QString name = QString::fromLocal8Bit(format);

        formats.append(QLatin1String("*.") + name.toLower());
        formats.append(QLatin1String("*.") + name.toUpper());
    }

    return formats;
}

struct MimeTypeMapping
{
    const char* mimeType;
    PluginHandler::FileType fileType;
    const char* suffix;
    const char* alternativeSuffix;
};

const MimeTypeMapping mimeTypeMappings[] =
{
    { "application/pdf", PluginHandler::PDF, "pdf", 0 },
    { "application/postscript", PluginHandler::PS, "ps", "eps" },
    { "image/vnd.djvu", PluginHandler::DjVu, "djvu", "djv" },
    { "application/x-gzip", PluginHandler::GZip, "gz", 0 },
    { "application/x-bzip2", PluginHandler::BZip2, "bz2", 0 },
    { "application/x-xz", PluginHandler::XZ, "xz", 0 },
    { "application/epub+zip", PluginHandler::EPUB, "epub", 0 },
    { "application/x-fictionbook+xml", PluginHandler::FB2, "fb2", 0 },
    { "application/x-zip-compressed-fb2", PluginHandler::FB2, "fb2", 0 },
    { "application/zip", PluginHandler::ZIP, "zip", 0 }
};

const MimeTypeMapping* const endOfMimeTypeMappings = mimeTypeMappings + sizeof(mimeTypeMappings) / sizeof(mimeTypeMappings[0]);

void matchArchiveAndImageType(const QString& filePath, PluginHandler::FileType& fileType)
{
    if(fileType == PluginHandler::ZIP)
    {
        const QString suffix = QFileInfo(filePath).suffix().toLower();

        if (suffix == "cbz")
        {
            fileType = PluginHandler::CBZ;
        }
        else if (suffix == "xps" || suffix == "oxps")
        {
            fileType = PluginHandler::XPS;
        }
        else
        {
            fileType = PluginHandler::Unknown;
        }
    }

    if(fileType == PluginHandler::Unknown && !QImageReader::imageFormat(filePath).isEmpty())
    {
        fileType = PluginHandler::Image;
    }
}

PluginHandler::FileType matchFileType(const QString& filePath)
{
    PluginHandler::FileType fileType = PluginHandler::Unknown;

#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)

    const QMimeType mimeType = QMimeDatabase().mimeTypeForFile(filePath, QMimeDatabase::MatchContent);

    for(const MimeTypeMapping* mapping = mimeTypeMappings; mapping != endOfMimeTypeMappings; ++mapping)
    {
        if(mimeType.inherits(mapping->mimeType))
        {
            fileType = mapping->fileType;
            break;
        }
    }

    matchArchiveAndImageType(filePath, fileType);

    if(fileType == PluginHandler::Unknown)
    {
        qDebug() << "Unknown MIME type:" << mimeType.name();
    }

#else

#ifdef WITH_MAGIC

    magic_t cookie = magic_open(MAGIC_MIME_TYPE | MAGIC_SYMLINK);

    if(magic_load(cookie, 0) == 0)
    {
        const char* const mimeType = magic_file(cookie, QFile::encodeName(filePath));

        for(const MimeTypeMapping* mapping = mimeTypeMappings; mapping != endOfMimeTypeMappings; ++mapping)
        {
            if(qstrcmp(mimeType, mapping->mimeType) == 0)
            {
                fileType = mapping->fileType;
                break;
            }
        }

        matchArchiveAndImageType(filePath, fileType);

        if(fileType == PluginHandler::Unknown)
        {
            qDebug() << "Unknown MIME type:" << mimeType;
        }
    }

    magic_close(cookie);

#else

    const QString suffix = QFileInfo(filePath).suffix().toLower();

    for(const MimeTypeMapping* mapping = mimeTypeMappings; mapping != endOfMimeTypeMappings; ++mapping)
    {
        if(suffix == mapping->suffix || (mapping->alternativeSuffix != 0 && suffix == mapping->alternativeSuffix))
        {
            fileType = mapping->fileType;
            break;
        }
    }

    matchArchiveAndImageType(filePath, fileType);

    if(fileType == PluginHandler::Unknown)
    {
        qDebug() << "Unkown file suffix:" << suffix;
    }

#endif // WITH_MAGIC

#endif // QT_VERSION

    return fileType;
}

int execute(QProcess& process, const QString& program, const QStringList& arguments = QStringList())
{
    process.start(program, arguments, QIODevice::NotOpen);

    if(!process.waitForStarted())
    {
        return -1;
    }

    if(!process.waitForFinished())
    {
        return -2;
    }

    return process.exitCode();
}

QStringList supportedCompressedFormats()
{
    QStringList formats;

    QProcess process;
    process.setStandardInputFile("/dev/null");
    process.setStandardOutputFile("/dev/null");

    if(execute(process, "gzip") >= 0)
    {
        formats.append("*.gz *.GZ");
    }

    if(execute(process, "bzip2") >= 0)
    {
        formats.append("*.bz2 *.BZ2");
    }

    if(execute(process, "xz") >= 0)
    {
        formats.append("*.xz *.XZ");
    }

    return formats;
}

QString decompressToTemporaryFile(const QString& filePath, const PluginHandler::FileType fileType)
{
    const char* command;

    switch(fileType)
    {
    case PluginHandler::GZip:
        command = "gzip";
        break;
    case PluginHandler::BZip2:
        command = "bzip2";
        break;
    case PluginHandler::XZ:
        command = "xz";
        break;
    default:
        return QString();
    }

    QTemporaryFile file;
    file.setAutoRemove(false);

    if(!file.open())
    {
        return QString();
    }

    file.close();

    QProcess process;
    process.setStandardInputFile("/dev/null");
    process.setStandardOutputFile(file.fileName());

    if(execute(process, command, QStringList() << "-dck" << filePath) != 0)
    {
        return QString();
    }

    return file.fileName();
}

} // anonymous

namespace qpdfview
{

PluginHandler* PluginHandler::s_instance = 0;

PluginHandler* PluginHandler::instance()
{
    if(s_instance == 0)
    {
        s_instance = new PluginHandler(qApp);
    }

    return s_instance;
}

PluginHandler::~PluginHandler()
{
    s_instance = 0;
}

QLatin1String PluginHandler::fileTypeName(PluginHandler::FileType fileType)
{
    switch(fileType)
    {
    default:
    case PluginHandler::Unknown:
        return QLatin1String("Unknown");
    case PluginHandler::PDF:
        return QLatin1String("PDF");
    case PluginHandler::PS:
        return QLatin1String("PS");
    case PluginHandler::DjVu:
        return QLatin1String("DjVu");
    case PluginHandler::Image:
        return QLatin1String("Image");
    case PluginHandler::EPUB:
        return QLatin1String("EPUB");
    case PluginHandler::XPS:
        return QLatin1String("XPS");
    case PluginHandler::FB2:
        return QLatin1String("FictionBook2");
    case PluginHandler::CBZ:
        return QLatin1String("CBZ");
    case PluginHandler::GZip:
    case PluginHandler::BZip2:
    case PluginHandler::XZ:
    case PluginHandler::ZIP:
        return QLatin1String("Compressed");
    }
}

QStringList PluginHandler::openFilter()
{
    QStringList openFilter;
    QStringList supportedFormats;

#if defined(WITH_PDF) || defined(WITH_FITZ)

    openFilter.append(QLatin1String("Portable document format (*.pdf *.PDF)"));
    supportedFormats.append(QLatin1String("*.pdf *.PDF"));

#endif // WITH_PDF // WITH_FITZ

#if defined(WITH_FITZ)

    openFilter.append(QLatin1String("EPUB (*.epub *.EPUB)"));
    supportedFormats.append(QLatin1String("*.epub *.EPUB"));

    openFilter.append(QLatin1String("XPS (*.xps *.XPS *.oxps *.OXPS)"));
    supportedFormats.append(QLatin1String("*.xps *.XPS *.oxps *.OXPS"));

    openFilter.append(QLatin1String("FictionBook 2 (*.fb2 *.FB2)"));
    supportedFormats.append(QLatin1String("*.fb2 *.FB2"));

    openFilter.append(QLatin1String("CBZ (*.cbz *.CBZ)"));
    supportedFormats.append(QLatin1String("*.cbz *.CBZ"));

#endif // WITH_FITZ

#ifdef WITH_PS

    openFilter.append(QLatin1String("PostScript (*.ps *.PS)"));
    openFilter.append(QLatin1String("Encapsulated PostScript (*.eps *.EPS)"));
    supportedFormats.append(QLatin1String("*.ps *.PS *.eps *.EPS"));

#endif // WITH_PS

#ifdef WITH_DJVU

    openFilter.append(QLatin1String("DjVu (*.djvu *.DJVU *.djv *.DJV)"));
    supportedFormats.append(QLatin1String("*.djvu *.DJVU *.djv *.DJV"));

#endif // WITH_DJVU

#ifdef WITH_IMAGE

    static QStringList imageFormats;

    if(imageFormats.isEmpty())
    {
        imageFormats = supportedImageFormats();
    }

    if(!imageFormats.isEmpty())
    {
        openFilter.append(tr("Image (%1)").arg(imageFormats.join(QLatin1String(" "))));
        supportedFormats.append(imageFormats);
    }

#endif // WITH_IMAGE

    static QStringList compressedFormats;

    if(compressedFormats.isEmpty())
    {
        compressedFormats = supportedCompressedFormats();
    }

    if(!compressedFormats.isEmpty())
    {
        openFilter.append(tr("Compressed (%1)").arg(compressedFormats.join(QLatin1String(" "))));
        supportedFormats.append(compressedFormats);
    }

    openFilter.prepend(tr("Supported formats (%1)").arg(supportedFormats.join(QLatin1String(" "))));

    return openFilter;
}

Model::Document* PluginHandler::loadDocument(const QString& filePath)
{
    FileType fileType = matchFileType(filePath);
    QString adjustedFilePath = filePath;

    if(fileType == GZip || fileType == BZip2 || fileType == XZ)
    {
        adjustedFilePath = decompressToTemporaryFile(filePath, fileType);

        if(adjustedFilePath.isEmpty())
        {
            qWarning() << tr("Could not decompress '%1'!").arg(filePath);

            return 0;
        }

        fileType = matchFileType(adjustedFilePath);
    }

    if(fileType == Unknown)
    {
        qWarning() << tr("Could not match file type of '%1'!").arg(filePath);

        return 0;
    }

    if(!loadPlugin(fileType))
    {
        QMessageBox::critical(0, tr("Critical"), tr("Could not load plug-in for file type '%1'!").arg(fileTypeName(fileType)));

        return 0;
    }

    return m_plugins.value(fileType)->loadDocument(adjustedFilePath);
}

SettingsWidget* PluginHandler::createSettingsWidget(FileType fileType, QWidget* parent)
{
    return loadPlugin(fileType) ? m_plugins.value(fileType)->createSettingsWidget(parent) : 0;
}

PluginHandler::PluginHandler(QObject* parent) : QObject(parent),
    m_plugins()
{
#ifdef WITH_IMAGE
#ifdef STATIC_IMAGE_PLUGIN
    m_objectNames.insertMulti(Image, QLatin1String("ImagePlugin"));
#else
    m_fileNames.insertMulti(Image, QLatin1String(IMAGE_PLUGIN_NAME));
#endif // STATIC_IMAGE_PLUGIN
#endif // WITH_IMAGE

#ifdef WITH_FITZ
#ifdef STATIC_FITZ_PLUGIN
    m_objectNames.insertMulti(PDF, QLatin1String("FitzPlugin"));
    m_objectNames.insertMulti(EPUB, QLatin1String("FitzPlugin"));
    m_objectNames.insertMulti(XPS, QLatin1String("FitzPlugin"));
    m_objectNames.insertMulti(FB2, QLatin1String("FitzPlugin"));
    m_objectNames.insertMulti(CBZ, QLatin1String("FitzPlugin"));
#else
    m_fileNames.insertMulti(PDF, QLatin1String(FITZ_PLUGIN_NAME));
    m_fileNames.insertMulti(EPUB, QLatin1String(FITZ_PLUGIN_NAME));
    m_fileNames.insertMulti(XPS, QLatin1String(FITZ_PLUGIN_NAME));
    m_fileNames.insertMulti(FB2, QLatin1String(FITZ_PLUGIN_NAME));
    m_fileNames.insertMulti(CBZ, QLatin1String(FITZ_PLUGIN_NAME));
#endif // STATIC_FITZ_PLUGIN
#endif // WITH_FITZ

#ifdef WITH_PDF
#ifdef STATIC_PDF_PLUGIN
    m_objectNames.insertMulti(PDF, QLatin1String("PdfPlugin"));
#else
    m_fileNames.insertMulti(PDF, QLatin1String(PDF_PLUGIN_NAME));
#endif // STATIC_PDF_PLUGIN
#endif // WITH_PDF

#ifdef WITH_PS
#ifdef STATIC_PS_PLUGIN
    m_objectNames.insertMulti(PS, QLatin1String("PsPlugin"));
#else
    m_fileNames.insertMulti(PS, QLatin1String(PS_PLUGIN_NAME));
#endif // STATIC_PS_PLUGIN
#endif // WITH_PS

#ifdef WITH_DJVU
#ifdef STATIC_DJVU_PLUGIN
    m_objectNames.insertMulti(DjVu, QLatin1String("DjVuPlugin"));
#else
    m_fileNames.insertMulti(DjVu, QLatin1String(DJVU_PLUGIN_NAME));
#endif // STATIC_DJVU_PLUGIN
#endif // WITH_DJVU
}

bool PluginHandler::loadPlugin(FileType fileType)
{
    if(m_plugins.contains(fileType))
    {
        return true;
    }

    foreach(const QString& objectName, m_objectNames.values(fileType))
    {
        if(Plugin* plugin = ::loadStaticPlugin(objectName))
        {
            m_plugins.insert(fileType, plugin);

            return true;
        }
    }

    foreach(const QString& fileName, m_fileNames.values(fileType))
    {
        if(Plugin* plugin = ::loadPlugin(fileName))
        {
            m_plugins.insert(fileType, plugin);

            return true;
        }
    }

    return false;
}

} // qpdfview

#ifdef STATIC_IMAGE_PLUGIN
    #if QT_VERSION < QT_VERSION_CHECK(5,0,0)
        Q_IMPORT_PLUGIN(qpdfview_image)
    #else
        Q_IMPORT_PLUGIN(ImagePlugin)
    #endif // QT_VERSION
#endif // STATIC_IMAGE_PLUGIN

#ifdef STATIC_FITZ_PLUGIN
    #if QT_VERSION < QT_VERSION_CHECK(5,0,0)
        Q_IMPORT_PLUGIN(qpdfview_fitz)
    #else
        Q_IMPORT_PLUGIN(FitzPlugin)
    #endif // QT_VERSION
#endif // STATIC_FITZ_PLUGIN

#ifdef STATIC_PDF_PLUGIN
    #if QT_VERSION < QT_VERSION_CHECK(5,0,0)
        Q_IMPORT_PLUGIN(qpdfview_pdf)
    #else
        Q_IMPORT_PLUGIN(PdfPlugin)
    #endif // QT_VERSION
#endif // STATIC_PDF_PLUGIN

#ifdef STATIC_PS_PLUGIN
    #if QT_VERSION < QT_VERSION_CHECK(5,0,0)
        Q_IMPORT_PLUGIN(qpdfview_ps)
    #else
        Q_IMPORT_PLUGIN(PsPlugin)
    #endif // QT_VERSION
#endif // STATIC_PS_PLUGIN

#ifdef STATIC_DJVU_PLUGIN
    #if QT_VERSION < QT_VERSION_CHECK(5,0,0)
        Q_IMPORT_PLUGIN(qpdfview_djvu)
    #else
        Q_IMPORT_PLUGIN(DjVuPlugin)
    #endif // QT_VERSION
#endif // STATIC_DJVU_PLUGIN
