/*
    SPDX-FileCopyrightText: 2006 Pino Toscano <toscano.pino@tiscali.it>

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

#include "generator_tiff.h"

#include <QBuffer>
#include <QDateTime>
#include <QFile>
#include <QFileInfo>
#include <QImage>
#include <QList>
#include <QPainter>
#include <QPrinter>

#include <KAboutData>
#include <KLocalizedString>
#include <QDebug>

#include <core/document.h>
#include <core/fileprinter.h>
#include <core/page.h>
#include <core/utils.h>

#include <tiff.h>
#include <tiffio.h>

#define TiffDebug 4714

tsize_t okular_tiffReadProc(thandle_t handle, tdata_t buf, tsize_t size)
{
    QIODevice *device = static_cast<QIODevice *>(handle);
    return device->isReadable() ? device->read(static_cast<char *>(buf), size) : -1;
}

tsize_t okular_tiffWriteProc(thandle_t handle, tdata_t buf, tsize_t size)
{
    QIODevice *device = static_cast<QIODevice *>(handle);
    return device->write(static_cast<char *>(buf), size);
}

toff_t okular_tiffSeekProc(thandle_t handle, toff_t offset, int whence)
{
    QIODevice *device = static_cast<QIODevice *>(handle);
    switch (whence) {
    case SEEK_SET:
        device->seek(offset);
        break;
    case SEEK_CUR:
        device->seek(device->pos() + offset);
        break;
    case SEEK_END:
        device->seek(device->size() + offset);
        break;
    }

    return device->pos();
}

int okular_tiffCloseProc(thandle_t handle)
{
    Q_UNUSED(handle)
    return 0;
}

toff_t okular_tiffSizeProc(thandle_t handle)
{
    const QIODevice *device = static_cast<QIODevice *>(handle);
    return device->size();
}

int okular_tiffMapProc(thandle_t, tdata_t *, toff_t *)
{
    return 0;
}

void okular_tiffUnmapProc(thandle_t, tdata_t, toff_t)
{
}

class TIFFGenerator::Private
{
public:
    Private()
        : tiff(nullptr)
    {
    }

    TIFF *tiff;
    QByteArray data;
    std::unique_ptr<QIODevice> dev;
};

static QDateTime convertTIFFDateTime(const char *tiffdate)
{
    if (!tiffdate) {
        return QDateTime();
    }

    return QDateTime::fromString(QString::fromLatin1(tiffdate), QStringLiteral("yyyy:MM:dd HH:mm:ss"));
}

static void adaptSizeToResolution(TIFF *tiff, ttag_t whichres, double dpi, uint32_t *size)
{
    float resvalue = 1.0;
    uint16_t resunit = 0;
    if (!TIFFGetField(tiff, whichres, &resvalue) || !TIFFGetFieldDefaulted(tiff, TIFFTAG_RESOLUTIONUNIT, &resunit)) {
        return;
    }

    float newsize = *size / resvalue;
    switch (resunit) {
    case RESUNIT_INCH:
        *size = (uint32_t)(newsize * dpi);
        break;
    case RESUNIT_CENTIMETER:
        *size = (uint32_t)(newsize * 10.0 / 25.4 * dpi);
        break;
    case RESUNIT_NONE:
        break;
    }
}

static Okular::Rotation readTiffRotation(TIFF *tiff)
{
    uint32_t tiffOrientation = 0;

    if (!TIFFGetField(tiff, TIFFTAG_ORIENTATION, &tiffOrientation)) {
        return Okular::Rotation0;
    }

    Okular::Rotation ret = Okular::Rotation0;
    switch (tiffOrientation) {
    case ORIENTATION_TOPLEFT:
    case ORIENTATION_TOPRIGHT:
        ret = Okular::Rotation0;
        break;
    case ORIENTATION_BOTRIGHT:
    case ORIENTATION_BOTLEFT:
        ret = Okular::Rotation180;
        break;
    case ORIENTATION_LEFTTOP:
    case ORIENTATION_LEFTBOT:
        ret = Okular::Rotation270;
        break;
    case ORIENTATION_RIGHTTOP:
    case ORIENTATION_RIGHTBOT:
        ret = Okular::Rotation90;
        break;
    }

    return ret;
}

K_PLUGIN_CLASS_WITH_JSON(TIFFGenerator, "libokularGenerator_tiff.json")

TIFFGenerator::TIFFGenerator(QObject *parent, const QVariantList &args)
    : Okular::Generator(parent, args)
    , d(new Private)
{
    setFeature(Threaded);
    setFeature(PrintNative);
    setFeature(PrintToFile);
    setFeature(ReadRawData);
}

TIFFGenerator::~TIFFGenerator()
{
    if (d->tiff) {
        TIFFClose(d->tiff);
        d->tiff = nullptr;
    }

    delete d;
}

bool TIFFGenerator::loadDocument(const QString &fileName, QList<Okular::Page *> &pagesVector)
{
    auto qfile = std::make_unique<QFile>(fileName);
    if (!qfile->open(QIODevice::ReadOnly)) {
        return false;
    }
    d->data = QFile::encodeName(QFileInfo(*qfile).fileName());
    d->dev = std::move(qfile);
    return loadTiff(pagesVector, d->data.constData());
}

bool TIFFGenerator::loadDocumentFromData(const QByteArray &fileData, QList<Okular::Page *> &pagesVector)
{
    d->data = fileData;
    auto qbuffer = std::make_unique<QBuffer>(&d->data);
    qbuffer->open(QIODevice::ReadOnly);
    d->dev = std::move(qbuffer);
    return loadTiff(pagesVector, "<stdin>");
}

bool TIFFGenerator::loadTiff(QList<Okular::Page *> &pagesVector, const char *name)
{
    d->tiff = TIFFClientOpen(name, "r", d->dev.get(), okular_tiffReadProc, okular_tiffWriteProc, okular_tiffSeekProc, okular_tiffCloseProc, okular_tiffSizeProc, okular_tiffMapProc, okular_tiffUnmapProc);
    if (!d->tiff) {
        d->dev.reset();
        d->data.clear();
        return false;
    }

    loadPages(pagesVector);

    return true;
}

bool TIFFGenerator::doCloseDocument()
{
    // closing the old document
    if (d->tiff) {
        TIFFClose(d->tiff);
        d->tiff = nullptr;
        d->dev.reset();
        d->data.clear();
        m_pageMapping.clear();
    }

    return true;
}

QImage TIFFGenerator::image(Okular::PixmapRequest *request)
{
    if (TIFFSetDirectory(d->tiff, mapPage(request->page()->number()))) {
        int rotation = request->page()->rotation();
        uint32_t width = 1;
        uint32_t height = 1;
        uint32_t orientation = 0;
        TIFFGetField(d->tiff, TIFFTAG_IMAGEWIDTH, &width);
        TIFFGetField(d->tiff, TIFFTAG_IMAGELENGTH, &height);

        if (!TIFFGetField(d->tiff, TIFFTAG_ORIENTATION, &orientation)) {
            orientation = ORIENTATION_TOPLEFT;
        }

        QImage img(width, height, QImage::Format_RGB32);
        uint32_t *data = reinterpret_cast<uint32_t *>(img.bits());

        // read data
        if (TIFFReadRGBAImageOriented(d->tiff, width, height, data, orientation) != 0) {
            // an image read by ReadRGBAImage is ABGR, we need ARGB, so swap red and blue
            uint32_t size = width * height;
            for (uint32_t i = 0; i < size; ++i) {
                uint32_t red = (data[i] & 0x00FF0000) >> 16;
                uint32_t blue = (data[i] & 0x000000FF) << 16;
                data[i] = (data[i] & 0xFF00FF00) + red + blue;
            }

            int reqwidth = request->width();
            int reqheight = request->height();
            if (rotation % 2 == 1) {
                std::swap(reqwidth, reqheight);
            }
            return img.scaled(reqwidth, reqheight, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
        }
    }

    QImage img(request->width(), request->height(), QImage::Format_RGB32);
    img.fill(qRgb(255, 255, 255));
    return img;
}

Okular::DocumentInfo TIFFGenerator::generateDocumentInfo(const QSet<Okular::DocumentInfo::Key> &keys) const
{
    Okular::DocumentInfo docInfo;
    if (d->tiff) {
        if (keys.contains(Okular::DocumentInfo::MimeType)) {
            docInfo.set(Okular::DocumentInfo::MimeType, QStringLiteral("image/tiff"));
        }

        if (keys.contains(Okular::DocumentInfo::Description)) {
            char *buffer = nullptr;
            TIFFGetField(d->tiff, TIFFTAG_IMAGEDESCRIPTION, &buffer);
            docInfo.set(Okular::DocumentInfo::Description, buffer ? QString::fromLatin1(buffer) : QString());
        }

        if (keys.contains(Okular::DocumentInfo::Producer)) {
            char *buffer = nullptr;
            TIFFGetField(d->tiff, TIFFTAG_SOFTWARE, &buffer);
            docInfo.set(Okular::DocumentInfo::Producer, buffer ? QString::fromLatin1(buffer) : QString());
        }

        if (keys.contains(Okular::DocumentInfo::Copyright)) {
            char *buffer = nullptr;
            TIFFGetField(d->tiff, TIFFTAG_COPYRIGHT, &buffer);
            docInfo.set(Okular::DocumentInfo::Copyright, buffer ? QString::fromLatin1(buffer) : QString());
        }

        if (keys.contains(Okular::DocumentInfo::Author)) {
            char *buffer = nullptr;
            TIFFGetField(d->tiff, TIFFTAG_ARTIST, &buffer);
            docInfo.set(Okular::DocumentInfo::Author, buffer ? QString::fromLatin1(buffer) : QString());
        }

        if (keys.contains(Okular::DocumentInfo::CreationDate)) {
            char *buffer = nullptr;
            TIFFGetField(d->tiff, TIFFTAG_DATETIME, &buffer);
            QDateTime date = convertTIFFDateTime(buffer);
            docInfo.set(Okular::DocumentInfo::CreationDate, date.isValid() ? QLocale().toString(date, QLocale::LongFormat) : QString());
        }
    }

    return docInfo;
}

void TIFFGenerator::loadPages(QList<Okular::Page *> &pagesVector)
{
    if (!d->tiff) {
        return;
    }

    tdir_t dirs = TIFFNumberOfDirectories(d->tiff);
    pagesVector.resize(dirs);
    tdir_t realdirs = 0;

    uint32_t width = 0;
    uint32_t height = 0;

    const QSizeF dpi = Okular::Utils::realDpi(nullptr);
    for (tdir_t i = 0; i < dirs; ++i) {
        if (!TIFFSetDirectory(d->tiff, i)) {
            continue;
        }

        if (TIFFGetField(d->tiff, TIFFTAG_IMAGEWIDTH, &width) != 1 || TIFFGetField(d->tiff, TIFFTAG_IMAGELENGTH, &height) != 1) {
            continue;
        }

        adaptSizeToResolution(d->tiff, TIFFTAG_XRESOLUTION, dpi.width(), &width);
        adaptSizeToResolution(d->tiff, TIFFTAG_YRESOLUTION, dpi.height(), &height);

        Okular::Page *page = new Okular::Page(realdirs, width, height, readTiffRotation(d->tiff));
        pagesVector[realdirs] = page;

        m_pageMapping[realdirs] = i;

        ++realdirs;
    }

    pagesVector.resize(realdirs);
}

Okular::Document::PrintError TIFFGenerator::print(QPrinter &printer)
{
    uint32_t width = 0;
    uint32_t height = 0;

    QPainter p(&printer);

    QList<int> pageList = Okular::FilePrinter::pageList(printer, document()->pages(), document()->currentPage() + 1, document()->bookmarkedPageList());

    for (int i = 0; i < pageList.count(); ++i) {
        if (!TIFFSetDirectory(d->tiff, mapPage(pageList[i] - 1))) {
            continue;
        }

        if (TIFFGetField(d->tiff, TIFFTAG_IMAGEWIDTH, &width) != 1 || TIFFGetField(d->tiff, TIFFTAG_IMAGELENGTH, &height) != 1) {
            continue;
        }

        QImage printImage(width, height, QImage::Format_RGB32);
        uint32_t *data = reinterpret_cast<uint32_t *>(printImage.bits());

        // read data
        if (TIFFReadRGBAImageOriented(d->tiff, width, height, data, ORIENTATION_TOPLEFT) != 0) {
            // an image read by ReadRGBAImage is ABGR, we need ARGB, so swap red and blue
            uint32_t size = width * height;
            for (uint32_t j = 0; j < size; ++j) {
                uint32_t red = (data[j] & 0x00FF0000) >> 16;
                uint32_t blue = (data[j] & 0x000000FF) << 16;
                data[j] = (data[j] & 0xFF00FF00) + red + blue;
            }
        }

        if (i != 0) {
            printer.newPage();
        }

        QSize targetSize = printer.pageRect(QPrinter::Unit::DevicePixel).size().toSize();

        if ((printImage.width() < targetSize.width()) && (printImage.height() < targetSize.height())) {
            // draw small images at 100% (don't scale up)
            p.drawImage(0, 0, printImage);
        } else {
            // fit to page
            p.drawImage(0, 0, printImage.scaled(targetSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
        }
    }

    return Okular::Document::NoPrintError;
}

int TIFFGenerator::mapPage(int page) const
{
    QHash<int, int>::const_iterator it = m_pageMapping.find(page);
    if (it == m_pageMapping.end()) {
        qCWarning(OkularTiffDebug) << "Requesting unmapped page" << page << ":" << m_pageMapping;
        return -1;
    }
    return it.value();
}

Q_LOGGING_CATEGORY(OkularTiffDebug, "org.kde.okular.generators.tiff", QtWarningMsg)

#include "generator_tiff.moc"
