#include <cmath>
#include <memory>
#include <sstream>

#include <QtTest/QTest>
#include <QTemporaryFile>

#include <poppler-qt6.h>

#include "poppler/Annot.h"
#include "goo/GooString.h"
#include "goo/gstrtod.h"

class TestAnnotations : public QObject
{
    Q_OBJECT
public:
    explicit TestAnnotations(QObject *parent = nullptr) : QObject(parent) { }

    void saveAndCheck(const std::unique_ptr<Poppler::Document> &doc, const std::function<void(Poppler::Annotation *a)> &checkFunction);

private slots:
    void checkQColorPrecision();
    void checkFontSizeAndColor();
    void checkHighlightFromAndToQuads();
    void checkUTF16LEAnnot();
    void checkModificationCreationDate();
    void checkNonMarkupAnnotations();
    void checkDefaultAppearance();
};

/* Is .5f sufficient for 16 bit color channel roundtrip trough save and load on all architectures? */
void TestAnnotations::checkQColorPrecision()
{
    bool precisionOk = true;
    for (int i = std::numeric_limits<uint16_t>::min(); i <= std::numeric_limits<uint16_t>::max(); i++) {
        double normalized = static_cast<uint16_t>(i) / static_cast<double>(std::numeric_limits<uint16_t>::max());
        const std::unique_ptr<GooString> serialized = GooString::format("{0:.5f}", normalized);
        double deserialized = gatof(serialized->c_str());
        uint16_t denormalized = std::round(deserialized * std::numeric_limits<uint16_t>::max());
        if (static_cast<uint16_t>(i) != denormalized) {
            precisionOk = false;
            break;
        }
    }
    QVERIFY(precisionOk);
}

void TestAnnotations::checkFontSizeAndColor()
{
    const QString contents = QStringLiteral("foobar");
    const std::vector<QColor> testColors { QColor::fromRgb(0xAB, 0xCD, 0xEF), QColor::fromCmyk(0xAB, 0xBC, 0xCD, 0xDE) };
    const QFont testFont(QStringLiteral("Helvetica"), 20);

    QTemporaryFile tempFile;
    QVERIFY(tempFile.open());
    tempFile.close();

    {
        std::unique_ptr<Poppler::Document> doc { Poppler::Document::load(TESTDATADIR "/unittestcases/UseNone.pdf") };
        QVERIFY(doc.get());

        std::unique_ptr<Poppler::Page> page { doc->page(0) };
        QVERIFY(page.get());

        for (const auto &color : testColors) {
            auto annot = std::make_unique<Poppler::TextAnnotation>(Poppler::TextAnnotation::InPlace);
            annot->setBoundary(QRectF(0.0, 0.0, 1.0, 1.0));
            annot->setContents(contents);
            annot->setTextFont(testFont);
            annot->setTextColor(color);
            page->addAnnotation(annot.get());
        }

        std::unique_ptr<Poppler::PDFConverter> conv(doc->pdfConverter());
        QVERIFY(conv.get());
        conv->setOutputFileName(tempFile.fileName());
        conv->setPDFOptions(Poppler::PDFConverter::WithChanges);
        QVERIFY(conv->convert());
    }

    {
        std::unique_ptr<Poppler::Document> doc { Poppler::Document::load(tempFile.fileName()) };
        QVERIFY(doc.get());

        std::unique_ptr<Poppler::Page> page { doc->page(0) };
        QVERIFY(page.get());

        auto annots = page->annotations();
        QCOMPARE(annots.size(), static_cast<int>(testColors.size()));

        auto &&annot = annots.cbegin();
        for (const auto &color : testColors) {
            QCOMPARE((*annot)->subType(), Poppler::Annotation::AText);
            auto textAnnot = static_cast<Poppler::TextAnnotation *>(annot->get());
            QCOMPARE(textAnnot->contents(), contents);
            QCOMPARE(textAnnot->textFont().pointSize(), testFont.pointSize());
            QCOMPARE(static_cast<int>(textAnnot->textColor().spec()), static_cast<int>(color.spec()));
            QCOMPARE(textAnnot->textColor(), color);
            if (annot != annots.cend()) {
                ++annot;
            }
        }
    }
}

namespace Poppler {
static bool operator==(const Poppler::HighlightAnnotation::Quad &a, const Poppler::HighlightAnnotation::Quad &b)
{
    // FIXME We do not compare capStart, capEnd and feather since AnnotQuadrilaterals doesn't contain that info and thus
    //       HighlightAnnotationPrivate::fromQuadrilaterals uses default values
    return a.points[0] == b.points[0] && a.points[1] == b.points[1] && a.points[2] == b.points[2] && a.points[3] == b.points[3];
}
}

void TestAnnotations::checkHighlightFromAndToQuads()
{
    std::unique_ptr<Poppler::Document> doc { Poppler::Document::load(TESTDATADIR "/unittestcases/UseNone.pdf") };

    std::unique_ptr<Poppler::Page> page { doc->page(0) };

    auto ha = std::make_unique<Poppler::HighlightAnnotation>();
    page->addAnnotation(ha.get());

    const QList<Poppler::HighlightAnnotation::Quad> quads = { { { { 0, 0.1 }, { 0.2, 0.3 }, { 0.4, 0.5 }, { 0.6, 0.7 } }, false, false, 0 }, { { { 0.8, 0.9 }, { 0.1, 0.2 }, { 0.3, 0.4 }, { 0.5, 0.6 } }, true, false, 0.4 } };
    ha->setHighlightQuads(quads);
    QCOMPARE(ha->highlightQuads(), quads);
}

void TestAnnotations::checkUTF16LEAnnot()
{
    std::unique_ptr<Poppler::Document> doc { Poppler::Document::load(TESTDATADIR "/unittestcases/utf16le-annot.pdf") };
    QVERIFY(doc.get());

    std::unique_ptr<Poppler::Page> page { doc->page(0) };
    QVERIFY(page.get());

    auto annots = page->annotations();
    QCOMPARE(annots.size(), 2);

    const auto &annot = annots[1];
    QCOMPARE(annot->contents(), QString::fromUtf8("Únîcödé豰")); // clazy:exclude=qstring-allocations
}

void TestAnnotations::saveAndCheck(const std::unique_ptr<Poppler::Document> &doc, const std::function<void(Poppler::Annotation *a)> &checkFunction)
{
    // also check that saving yields the same output
    QTemporaryFile tempFile;
    QVERIFY(tempFile.open());
    tempFile.close();

    std::unique_ptr<Poppler::PDFConverter> conv(doc->pdfConverter());
    conv->setOutputFileName(tempFile.fileName());
    conv->setPDFOptions(Poppler::PDFConverter::WithChanges);
    conv->convert();

    std::unique_ptr<Poppler::Document> savedDoc { Poppler::Document::load(tempFile.fileName()) };
    std::unique_ptr<Poppler::Page> page { doc->page(0) };
    auto annots = page->annotations();
    checkFunction(annots.at(1).get());
}

void TestAnnotations::checkModificationCreationDate()
{
    std::unique_ptr<Poppler::Document> doc { Poppler::Document::load(TESTDATADIR "/unittestcases/utf16le-annot.pdf") };
    QVERIFY(doc.get());

    std::unique_ptr<Poppler::Page> page { doc->page(0) };

    auto annots = page->annotations();
    const auto &annot = annots.at(1);
    QCOMPARE(annot->creationDate(), QDateTime());
    QCOMPARE(annot->modificationDate(), QDateTime());

    const QDateTime dt1(QDate(2020, 8, 7), QTime(18, 34, 56));
    annot->setCreationDate(dt1);
    auto checkFunction1 = [dt1](Poppler::Annotation *a) {
        QCOMPARE(a->creationDate(), dt1);
        // setting the creation date updates the modification date
        QVERIFY(std::abs(a->modificationDate().secsTo(QDateTime::currentDateTime())) < 2);
    };
    checkFunction1(annot.get());
    saveAndCheck(doc, checkFunction1);

    const QDateTime dt2(QDate(2020, 8, 30), QTime(8, 14, 52));
    annot->setModificationDate(dt2);
    auto checkFunction2 = [dt2](Poppler::Annotation *a) { QCOMPARE(a->modificationDate(), dt2); };
    checkFunction2(annot.get());
    saveAndCheck(doc, checkFunction2);

    // setting the creation date to empty means "use the modification date" and also updates the modification date
    // so both creation date and modification date are the same and are now
    annot->setCreationDate(QDateTime());
    auto checkFunction3 = [](Poppler::Annotation *a) {
        QVERIFY(std::abs(a->creationDate().secsTo(QDateTime::currentDateTime())) < 2);
        QCOMPARE(a->creationDate(), a->modificationDate());
    };
    checkFunction3(annot.get());
    saveAndCheck(doc, checkFunction3);

    annot->setModificationDate(QDateTime());
    auto checkFunction4 = [](Poppler::Annotation *a) {
        QCOMPARE(a->creationDate(), QDateTime());
        QCOMPARE(a->modificationDate(), QDateTime());
    };
    checkFunction4(annot.get());
    saveAndCheck(doc, checkFunction4);
}

void TestAnnotations::checkNonMarkupAnnotations()
{
    std::unique_ptr<Poppler::Document> doc { Poppler::Document::load(TESTDATADIR "/unittestcases/checkbox_issue_159.pdf") };
    QVERIFY(doc.get());

    std::unique_ptr<Poppler::Page> page { doc->page(0) };
    QVERIFY(page.get());

    auto annots = page->annotations();
    QCOMPARE(annots.size(), 17);
}

void TestAnnotations::checkDefaultAppearance()
{
    std::unique_ptr<GooString> roundtripString;
    {
        GooString daString { "/Helv 10 Tf 0.1 0.2 0.3 rg" };
        const DefaultAppearance da { &daString };
        QCOMPARE(da.getFontPtSize(), 10.);
        QVERIFY(da.getFontName().isName());
        QCOMPARE(da.getFontName().getName(), "Helv");
        const AnnotColor *color = da.getFontColor();
        QVERIFY(color);
        QCOMPARE(color->getSpace(), AnnotColor::colorRGB);
        QCOMPARE(color->getValues()[0], 0.1);
        QCOMPARE(color->getValues()[1], 0.2);
        QCOMPARE(color->getValues()[2], 0.3);
        roundtripString = std::make_unique<GooString>(da.toAppearanceString());
    }
    {
        /* roundtrip through parse/generate/parse shall preserve values */
        const DefaultAppearance da { roundtripString.get() };
        QCOMPARE(da.getFontPtSize(), 10.);
        QVERIFY(da.getFontName().isName());
        QCOMPARE(da.getFontName().getName(), "Helv");
        const AnnotColor *color = da.getFontColor();
        QVERIFY(color);
        QCOMPARE(color->getSpace(), AnnotColor::colorRGB);
        QCOMPARE(color->getValues()[0], 0.1);
        QCOMPARE(color->getValues()[1], 0.2);
        QCOMPARE(color->getValues()[2], 0.3);
    }
    {
        /* parsing bad DA strings must not cause crash */
        GooString daString { "/ % Tf 1 2 rg" };
        const DefaultAppearance da { &daString };
        QVERIFY(!da.getFontName().isName());
    }
}

QTEST_GUILESS_MAIN(TestAnnotations)

#include "check_annotations.moc"
