//    Copyright (C) 2020-2021 Jakub Melka
//
//    This file is part of PDF4QT.
//
//    PDF4QT is free software: you can redistribute it and/or modify
//    it under the terms of the GNU Lesser General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    with the written consent of the copyright owner, any later version.
//
//    PDF4QT 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 Lesser General Public License for more details.
//
//    You should have received a copy of the GNU Lesser General Public License
//    along with PDF4QT.  If not, see <https://www.gnu.org/licenses/>.

#include "pdftoolinfo.h"
#include "pdfform.h"
#include "pdfjavascriptscanner.h"

#include <QPageSize>
#include <QCryptographicHash>

namespace pdftool
{

static PDFToolInfoApplication s_infoApplication;

QString PDFToolInfoApplication::getStandardString(StandardString standardString) const
{
    switch (standardString)
    {
        case Command:
            return "info";

        case Name:
            return PDFToolTranslationContext::tr("Info");

        case Description:
            return PDFToolTranslationContext::tr("Retrieve basic informations about a document.");

        default:
            Q_ASSERT(false);
            break;
    }

    return QString();
}

int PDFToolInfoApplication::execute(const PDFToolOptions& options)
{
    pdf::PDFDocument document;
    QByteArray sourceData;
    if (!readDocument(options, document, &sourceData, false))
    {
        return ErrorDocumentReading;
    }

    QLocale locale;

    const pdf::PDFDocumentInfo* info = document.getInfo();
    const pdf::PDFCatalog* catalog = document.getCatalog();

    PDFOutputFormatter formatter(options.outputStyle);
    formatter.beginDocument("info", PDFToolTranslationContext::tr("Information about document %1").arg(options.document));
    formatter.endl();

    formatter.beginTable("properties", PDFToolTranslationContext::tr("Properties:"));

    formatter.beginTableHeaderRow("header");
    formatter.writeTableHeaderColumn("property", PDFToolTranslationContext::tr("Property"), Qt::AlignLeft);
    formatter.writeTableHeaderColumn("value", PDFToolTranslationContext::tr("Value"), Qt::AlignLeft);
    formatter.endTableHeaderRow();

    auto writeProperty = [&formatter](const QString& propertyName, const QString& property, const QString& value)
    {
        formatter.beginTableRow(propertyName);
        formatter.writeTableColumn("property", property);
        formatter.writeTableColumn("value", value);
        formatter.endTableRow();
    };

    writeProperty("title", PDFToolTranslationContext::tr("Title"), info->title);
    writeProperty("subject", PDFToolTranslationContext::tr("Subject"), info->subject);
    writeProperty("keywords", PDFToolTranslationContext::tr("Keywords"), info->keywords);
    writeProperty("author", PDFToolTranslationContext::tr("Author"), info->author);
    writeProperty("creator", PDFToolTranslationContext::tr("Creator"), info->creator);
    writeProperty("producer", PDFToolTranslationContext::tr("Producer"), info->producer);
    writeProperty("creation-date", PDFToolTranslationContext::tr("Creation date"), convertDateTimeToString(info->creationDate.toLocalTime(), options.outputDateFormat));
    writeProperty("modified-date", PDFToolTranslationContext::tr("Modified date"), convertDateTimeToString(info->modifiedDate.toLocalTime(), options.outputDateFormat));
    writeProperty("version", PDFToolTranslationContext::tr("Version"), QString::fromLatin1(document.getVersion()));

    QString trapped;
    switch (info->trapped)
    {
        case pdf::PDFDocumentInfo::Trapped::True:
            trapped = PDFToolTranslationContext::tr("Yes");
            break;

        case pdf::PDFDocumentInfo::Trapped::False:
            trapped = PDFToolTranslationContext::tr("No");
            break;

        case pdf::PDFDocumentInfo::Trapped::Unknown:
            trapped = PDFToolTranslationContext::tr("Unknown");
            break;

        default:
            Q_ASSERT(false);
            break;
    }

    writeProperty("tagged", PDFToolTranslationContext::tr("Tagged"), trapped);

    QString formType;
    pdf::PDFForm form = pdf::PDFForm::parse(&document, catalog->getFormObject());
    switch (form.getFormType())
    {
        case pdf::PDFForm::FormType::None:
            formType = PDFToolTranslationContext::tr("None");
            break;

        case pdf::PDFForm::FormType::AcroForm:
            formType = PDFToolTranslationContext::tr("AcroForm");
            break;

        case pdf::PDFForm::FormType::XFAForm:
            formType = PDFToolTranslationContext::tr("XFA");
            break;

        default:
            Q_ASSERT(false);
            break;
    }

    writeProperty("form-type", PDFToolTranslationContext::tr("Form type"), formType);

    const pdf::PDFInteger pageCount = catalog->getPageCount();
    writeProperty("page-count", PDFToolTranslationContext::tr("Page count"), locale.toString(pageCount));
    if (pageCount > 0)
    {
        const pdf::PDFPage* firstPage = catalog->getPage(0);
        QSizeF pageSizeMM = firstPage->getRectMM(firstPage->getRotatedMediaBox()).size();
        QPageSize pageSize(pageSizeMM, QPageSize::Millimeter, QString(), QPageSize::FuzzyOrientationMatch);
        QString paperSizeString = QString("%1 x %2 mm").arg(locale.toString(pageSizeMM.width()), locale.toString(pageSizeMM.height()));

        writeProperty("paper-format", PDFToolTranslationContext::tr("Paper format"), pageSize.name());
        writeProperty("paper-size", PDFToolTranslationContext::tr("Paper size"), paperSizeString);
    }

    if (!info->extra.empty())
    {
        for (const auto& item : info->extra)
        {
            QString key = QString::fromLatin1(item.first);
            QVariant valueVariant = item.second;
            QString value = (valueVariant.typeId() == QMetaType::QDateTime) ? convertDateTimeToString(valueVariant.toDateTime().toLocalTime(), options.outputDateFormat) : valueVariant.toString();
            writeProperty("custom-property", key, value);
        }
    }

    writeProperty("file-name", PDFToolTranslationContext::tr("File name"), options.document);
    writeProperty("file-size", PDFToolTranslationContext::tr("File size"), locale.toString(sourceData.size()));

    pdf::PDFJavaScriptScanner scanner(&document);
    writeProperty("javascript", PDFToolTranslationContext::tr("JavaScript"), scanner.hasJavaScript() ? PDFToolTranslationContext::tr("Yes") : PDFToolTranslationContext::tr("No"));

    const pdf::PDFSecurityHandler* securityHandler = document.getStorage().getSecurityHandler();
    const pdf::EncryptionMode mode = securityHandler->getMode();
    QString modeString;
    switch (mode)
    {
        case pdf::EncryptionMode::None:
            modeString = PDFToolTranslationContext::tr("None");
            break;

        case pdf::EncryptionMode::Standard:
            modeString = PDFToolTranslationContext::tr("Standard");
            break;

        case pdf::EncryptionMode::PublicKey:
            modeString = PDFToolTranslationContext::tr("Public Key");
            break;

        case pdf::EncryptionMode::Custom:
            modeString = PDFToolTranslationContext::tr("Custom");
            break;

        default:
            Q_ASSERT(false);
            break;
    }

    QString authorizationMode;
    switch (securityHandler->getAuthorizationResult())
    {
        case pdf::PDFSecurityHandler::AuthorizationResult::NoAuthorizationRequired:
            authorizationMode = PDFToolTranslationContext::tr("No authorization required");
            break;

        case pdf::PDFSecurityHandler::AuthorizationResult::OwnerAuthorized:
            authorizationMode = PDFToolTranslationContext::tr("Authorized as owner");
            break;

        case pdf::PDFSecurityHandler::AuthorizationResult::UserAuthorized:
            authorizationMode = PDFToolTranslationContext::tr("Authorized as user");
            break;

        default:
            Q_ASSERT(false);
            break;
    }

    writeProperty("encryption", PDFToolTranslationContext::tr("Encryption"), modeString);
    writeProperty("authorized-as", PDFToolTranslationContext::tr("Authorization"), authorizationMode);

    QStringList permissions;
    if (securityHandler->getAuthorizationResult() != pdf::PDFSecurityHandler::AuthorizationResult::NoAuthorizationRequired)
    {
        writeProperty("metadata-encrypted", PDFToolTranslationContext::tr("Metadata encrypted"), securityHandler->isMetadataEncrypted() ? PDFToolTranslationContext::tr("Yes") : PDFToolTranslationContext::tr("No"));
        writeProperty("version", PDFToolTranslationContext::tr("Version"), locale.toString(securityHandler->getVersion()));

        if (securityHandler->isAllowed(pdf::PDFSecurityHandler::Permission::PrintLowResolution))
        {
            permissions << PDFToolTranslationContext::tr("Print (low resolution)");
        }
        if (securityHandler->isAllowed(pdf::PDFSecurityHandler::Permission::PrintHighResolution))
        {
            permissions << PDFToolTranslationContext::tr("Print");
        }
        if (securityHandler->isAllowed(pdf::PDFSecurityHandler::Permission::CopyContent))
        {
            permissions << PDFToolTranslationContext::tr("Copy content");
        }
        if (securityHandler->isAllowed(pdf::PDFSecurityHandler::Permission::Accessibility))
        {
            permissions << PDFToolTranslationContext::tr("Accessibility");
        }
        if (securityHandler->isAllowed(pdf::PDFSecurityHandler::Permission::Assemble))
        {
            permissions << PDFToolTranslationContext::tr("Page assembling");
        }
        if (securityHandler->isAllowed(pdf::PDFSecurityHandler::Permission::Modify))
        {
            permissions << PDFToolTranslationContext::tr("Modify content");
        }
        if (securityHandler->isAllowed(pdf::PDFSecurityHandler::Permission::ModifyInteractiveItems))
        {
            permissions << PDFToolTranslationContext::tr("Modify interactive items");
        }
        if (securityHandler->isAllowed(pdf::PDFSecurityHandler::Permission::ModifyFormFields))
        {
            permissions << PDFToolTranslationContext::tr("Form filling");
        }
    }
    else
    {
        permissions << PDFToolTranslationContext::tr("All");
    }

    writeProperty("permissions", PDFToolTranslationContext::tr("Permissions"), permissions.join(", "));

    formatter.endTable();

    if (options.computeHashes)
    {
        formatter.endl();

        formatter.beginTable("hashes", PDFToolTranslationContext::tr("File hashes:"));

        formatter.beginTableHeaderRow("header");
        formatter.writeTableHeaderColumn("algorithm", PDFToolTranslationContext::tr("Algorithm"), Qt::AlignLeft);
        formatter.writeTableHeaderColumn("hash", PDFToolTranslationContext::tr("Hash"), Qt::AlignLeft);
        formatter.endTableHeaderRow();

        auto writeHash = [&formatter, &sourceData](QCryptographicHash::Algorithm algorithm, const QString& algorithmName, const QString& algorithmDescription)
        {
            formatter.beginTableRow(algorithmName);
            formatter.writeTableColumn("algorithm", algorithmDescription);
            formatter.writeTableColumn("hash", QString::fromLatin1(QCryptographicHash::hash(sourceData, algorithm).toHex()).toUpper());
            formatter.endTableRow();
        };

        writeHash(QCryptographicHash::Md5, "MD5", PDFToolTranslationContext::tr("MD5"));
        writeHash(QCryptographicHash::Sha1, "SHA1", PDFToolTranslationContext::tr("SHA1"));
        writeHash(QCryptographicHash::Sha256, "SHA256", PDFToolTranslationContext::tr("SHA256"));
        writeHash(QCryptographicHash::Sha384, "SHA384", PDFToolTranslationContext::tr("SHA384"));
        writeHash(QCryptographicHash::Sha512, "SHA512", PDFToolTranslationContext::tr("SHA512"));

        formatter.endTable();
    }

    formatter.endDocument();
    PDFConsole::writeText(formatter.getString(), options.outputCodec);

    return ExitSuccess;
}

PDFToolAbstractApplication::Options PDFToolInfoApplication::getOptionsFlags() const
{
    return ConsoleFormat | OpenDocument | DateFormat | ComputeHashes;
}

}   // namespace pdftool
