//    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 "pdftoolverifysignatures.h"

#include "pdfdocumentreader.h"
#include "pdfsignaturehandler.h"
#include "pdfform.h"

namespace pdftool
{

static PDFToolVerifySignaturesApplication s_verifySignaturesApplication;

QString PDFToolVerifySignaturesApplication::getStandardString(StandardString standardString) const
{
    switch (standardString)
    {
        case Command:
            return "verify-signatures";

        case Name:
            return PDFToolTranslationContext::tr("Signature verification");

        case Description:
            return PDFToolTranslationContext::tr("Verify signatures and timestamps in pdf document.");

        default:
            Q_ASSERT(false);
            break;
    }

    return QString();
}

int PDFToolVerifySignaturesApplication::execute(const PDFToolOptions& options)
{
    // No document specified?
    if (options.document.isEmpty())
    {
        PDFConsole::writeError(PDFToolTranslationContext::tr("No document specified."), options.outputCodec);
        return ErrorNoDocumentSpecified;
    }

    bool isFirstPasswordAttempt = true;
    auto passwordCallback = [&options, &isFirstPasswordAttempt](bool* ok) -> QString
    {
        *ok = isFirstPasswordAttempt;
        isFirstPasswordAttempt = false;
        return options.password;
    };
    pdf::PDFDocumentReader reader(nullptr, passwordCallback, options.permissiveReading, false);
    pdf::PDFDocument document = reader.readFromFile(options.document);

    switch (reader.getReadingResult())
    {
        case pdf::PDFDocumentReader::Result::OK:
            break;

        case pdf::PDFDocumentReader::Result::Cancelled:
            return ExitSuccess;

        case pdf::PDFDocumentReader::Result::Failed:
        {
            PDFConsole::writeError(PDFToolTranslationContext::tr("Error occured during document reading. %1").arg(reader.getErrorMessage()), options.outputCodec);
            return ErrorDocumentReading;
        }

        default:
            Q_ASSERT(false);
            return ErrorDocumentReading;
    }

    for (const QString& warning : reader.getWarnings())
    {
        PDFConsole::writeError(PDFToolTranslationContext::tr("Warning: %1").arg(warning), options.outputCodec);
    }

    // Verify signatures
    pdf::PDFCertificateStore certificateStore;
    if (options.verificationUseUserCertificates)
    {
        certificateStore.loadDefaultUserCertificates();
    }

    pdf::PDFSignatureHandler::Parameters parameters;
    parameters.store = &certificateStore;
    parameters.dss = &document.getCatalog()->getDocumentSecurityStore();
    parameters.enableVerification = true;
    parameters.ignoreExpirationDate = options.verificationIgnoreExpirationDate;
    parameters.useSystemCertificateStore = options.verificationUseSystemCertificates;

    pdf::PDFForm form = pdf::PDFForm::parse(&document, document.getCatalog()->getFormObject());
    std::vector<pdf::PDFSignatureVerificationResult> signatures = pdf::PDFSignatureHandler::verifySignatures(form, reader.getSource(), parameters);

    PDFOutputFormatter formatter(options.outputStyle);
    formatter.beginDocument("signatures", PDFToolTranslationContext::tr("Digital signatures/timestamps verification of %1").arg(options.document));
    formatter.endl();

    auto getTypeName = [](const pdf::PDFSignatureVerificationResult& signature)
    {
        switch (signature.getType())
        {
            case pdf::PDFSignature::Type::Invalid:
                return PDFToolTranslationContext::tr("Invalid");

            case pdf::PDFSignature::Type::Sig:
                return PDFToolTranslationContext::tr("Signature");

            case pdf::PDFSignature::Type::DocTimeStamp:
                return PDFToolTranslationContext::tr("Timestamp");
                break;

            default:
                Q_ASSERT(false);
                break;
        }

        return QString();
    };

    if (!signatures.empty())
    {
        formatter.beginTable("overview", PDFToolTranslationContext::tr("Overview"));

        formatter.beginTableHeaderRow("header");
        formatter.writeTableHeaderColumn("no", PDFToolTranslationContext::tr("No."), Qt::AlignLeft);
        formatter.writeTableHeaderColumn("type", PDFToolTranslationContext::tr("Type"), Qt::AlignLeft);
        formatter.writeTableHeaderColumn("common-name", PDFToolTranslationContext::tr("Signed by"), Qt::AlignLeft);
        formatter.writeTableHeaderColumn("cert-status", PDFToolTranslationContext::tr("Certificate"), Qt::AlignLeft);
        formatter.writeTableHeaderColumn("signature-status", PDFToolTranslationContext::tr("Signature"), Qt::AlignLeft);
        formatter.writeTableHeaderColumn("signing-date", PDFToolTranslationContext::tr("Signing date"), Qt::AlignLeft);
        formatter.writeTableHeaderColumn("timestamp-date", PDFToolTranslationContext::tr("Timestamp date"), Qt::AlignLeft);
        formatter.writeTableHeaderColumn("hash-algorithm", PDFToolTranslationContext::tr("Hash alg."), Qt::AlignLeft);
        formatter.writeTableHeaderColumn("handler", PDFToolTranslationContext::tr("Handler"), Qt::AlignLeft);
        formatter.writeTableHeaderColumn("whole-signed", PDFToolTranslationContext::tr("Signed whole"), Qt::AlignLeft);
        formatter.endTableHeaderRow();

        int i = 1;
        for (const pdf::PDFSignatureVerificationResult& signature : signatures)
        {
            const pdf::PDFCertificateInfos& certificateInfos = signature.getCertificateInfos();
            const pdf::PDFCertificateInfo* certificateInfo = !certificateInfos.empty() ? &certificateInfos.front() : nullptr;

            formatter.beginTableRow("signature", i);

            formatter.writeTableColumn("no", QString::number(i), Qt::AlignRight);
            formatter.writeTableColumn("type", getTypeName(signature));

            QString commonName = certificateInfo ? certificateInfo->getName(pdf::PDFCertificateInfo::CommonName) : PDFToolTranslationContext::tr("Unknown");
            formatter.writeTableColumn("common-name", commonName);
            formatter.writeTableColumn("cert-status", options.verificationOmitCertificateCheck ? PDFToolTranslationContext::tr("Skipped") : signature.getCertificateStatusText());
            formatter.writeTableColumn("signature-status", signature.getSignatureStatusText());
            formatter.writeTableColumn("signing-date", signature.getSignatureDate().isValid() ? convertDateTimeToString(signature.getSignatureDate().toLocalTime(), options.outputDateFormat) : QString());
            formatter.writeTableColumn("timestamp-date", signature.getTimestampDate().isValid() ? convertDateTimeToString(signature.getTimestampDate().toLocalTime(), options.outputDateFormat) : QString());
            formatter.writeTableColumn("hash-algorithm", signature.getHashAlgorithms().join(", ").toUpper());
            formatter.writeTableColumn("handler", QString::fromLatin1(signature.getSignatureHandler()));
            formatter.writeTableColumn("whole-signed", signature.hasFlag(pdf::PDFSignatureVerificationResult::Warning_Signature_NotCoveredBytes) ? PDFToolTranslationContext::tr("No") : PDFToolTranslationContext::tr("Yes"));

            formatter.endTableRow();
            ++i;
        }

        formatter.endTable();

        if (options.verificationPrintCertificateDetails)
        {
            formatter.endl();
            formatter.beginHeader("details", PDFToolTranslationContext::tr("Details"));

            int ii = 1;
            for (const pdf::PDFSignatureVerificationResult& signature : signatures)
            {
                formatter.endl();
                formatter.beginHeader("signature", PDFToolTranslationContext::tr("%1 #%2").arg(getTypeName(signature)).arg(ii), ii);

                const pdf::PDFCertificateInfos& certificateInfos = signature.getCertificateInfos();
                const pdf::PDFCertificateInfo* certificateInfo = !certificateInfos.empty() ? &certificateInfos.front() : nullptr;
                QString commonName = certificateInfo ? certificateInfo->getName(pdf::PDFCertificateInfo::CommonName) : PDFToolTranslationContext::tr("Unknown");
                formatter.writeText("common-name", PDFToolTranslationContext::tr("Signed by: %1").arg(commonName));
                formatter.writeText("certificate-status", PDFToolTranslationContext::tr("Certificate status: %1").arg(options.verificationOmitCertificateCheck ? PDFToolTranslationContext::tr("Skipped") : signature.getCertificateStatusText()));
                formatter.writeText("signature-status", PDFToolTranslationContext::tr("Signature status: %1").arg(signature.getSignatureStatusText()));
                formatter.writeText("signing-date", PDFToolTranslationContext::tr("Signing date: %1").arg(signature.getSignatureDate().isValid() ? convertDateTimeToString(signature.getSignatureDate().toLocalTime(), options.outputDateFormat) : QString()));
                formatter.writeText("timestamp-date", PDFToolTranslationContext::tr("Timestamp date: %1").arg(signature.getTimestampDate().isValid() ? convertDateTimeToString(signature.getTimestampDate().toLocalTime(), options.outputDateFormat) : QString()));
                formatter.writeText("hash-algorithm", PDFToolTranslationContext::tr("Hash algorithm: %1").arg(signature.getHashAlgorithms().join(", ").toUpper()));
                formatter.writeText("handler", PDFToolTranslationContext::tr("Handler: %1").arg(QString::fromLatin1(signature.getSignatureHandler())));
                formatter.writeText("whole-signed", PDFToolTranslationContext::tr("Is whole document signed: %1").arg(signature.hasFlag(pdf::PDFSignatureVerificationResult::Warning_Signature_NotCoveredBytes) ? PDFToolTranslationContext::tr("No") : PDFToolTranslationContext::tr("Yes")));

                // Signature range
                const pdf::PDFClosedIntervalSet& bytesCoveredBySignature = signature.getBytesCoveredBySignature();
                formatter.writeText("byte-range", PDFToolTranslationContext::tr("Byte range covered by signature: %1").arg(bytesCoveredBySignature.toText(false)));

                if (signature.hasError())
                {
                    formatter.endl();
                    formatter.beginHeader("errors", PDFToolTranslationContext::tr("Errors:"));
                    for (const QString& error : signature.getErrors())
                    {
                        formatter.writeText("error", error);
                    }
                    formatter.endHeader();
                }

                if (signature.hasWarning())
                {
                    formatter.endl();
                    formatter.beginHeader("warnings", PDFToolTranslationContext::tr("Warnings:"));
                    for (const QString& warning : signature.getWarnings())
                    {
                        formatter.writeText("warning", warning);
                    }
                    formatter.endHeader();
                }

                formatter.endl();

                if (!options.verificationOmitCertificateCheck)
                {
                    int certificateIndex = 1;
                    for (const pdf::PDFCertificateInfo& currentInfo : certificateInfos)
                    {
                        formatter.beginTable("certificate", PDFToolTranslationContext::tr("Certificate No. #%1").arg(certificateIndex++));

                        formatter.beginTableHeaderRow("header");
                        formatter.writeTableHeaderColumn("description", PDFToolTranslationContext::tr("Description"));
                        formatter.writeTableHeaderColumn("value", PDFToolTranslationContext::tr("Value"));
                        formatter.endTableHeaderRow();

                        auto addName = [&formatter, &currentInfo](pdf::PDFCertificateInfo::NameEntry nameEntry, QString caption)
                        {
                            QString text = currentInfo.getName(nameEntry);
                            if (!text.isEmpty())
                            {
                                formatter.beginTableRow("info", int(nameEntry));
                                formatter.writeTableColumn("description", caption);
                                formatter.writeTableColumn("value", text);
                                formatter.endTableRow();
                            }
                        };

                        addName(pdf::PDFCertificateInfo::CountryName, PDFToolTranslationContext::tr("Country"));
                        addName(pdf::PDFCertificateInfo::OrganizationName, PDFToolTranslationContext::tr("Organization"));
                        addName(pdf::PDFCertificateInfo::OrganizationalUnitName, PDFToolTranslationContext::tr("Org. unit"));
                        addName(pdf::PDFCertificateInfo::DistinguishedName, PDFToolTranslationContext::tr("Name"));
                        addName(pdf::PDFCertificateInfo::StateOrProvinceName, PDFToolTranslationContext::tr("State"));
                        addName(pdf::PDFCertificateInfo::SerialNumber, PDFToolTranslationContext::tr("Serial number"));
                        addName(pdf::PDFCertificateInfo::LocalityName, PDFToolTranslationContext::tr("Locality"));
                        addName(pdf::PDFCertificateInfo::Title, PDFToolTranslationContext::tr("Title"));
                        addName(pdf::PDFCertificateInfo::Surname, PDFToolTranslationContext::tr("Surname"));
                        addName(pdf::PDFCertificateInfo::GivenName, PDFToolTranslationContext::tr("Forename"));
                        addName(pdf::PDFCertificateInfo::Initials, PDFToolTranslationContext::tr("Initials"));
                        addName(pdf::PDFCertificateInfo::Pseudonym, PDFToolTranslationContext::tr("Pseudonym"));
                        addName(pdf::PDFCertificateInfo::GenerationalQualifier, PDFToolTranslationContext::tr("Qualifier"));
                        addName(pdf::PDFCertificateInfo::Email, PDFToolTranslationContext::tr("Email"));

                        QString publicKeyMethod;
                        switch (currentInfo.getPublicKey())
                        {
                            case pdf::PDFCertificateInfo::KeyRSA:
                                publicKeyMethod = PDFToolTranslationContext::tr("RSA method, %1-bit key").arg(currentInfo.getKeySize());
                                break;

                            case pdf::PDFCertificateInfo::KeyDSA:
                                publicKeyMethod = PDFToolTranslationContext::tr("DSA method, %1-bit key").arg(currentInfo.getKeySize());
                                break;

                            case pdf::PDFCertificateInfo::KeyEC:
                                publicKeyMethod = PDFToolTranslationContext::tr("EC method, %1-bit key").arg(currentInfo.getKeySize());
                                break;

                            case pdf::PDFCertificateInfo::KeyDH:
                                publicKeyMethod = PDFToolTranslationContext::tr("DH method, %1-bit key").arg(currentInfo.getKeySize());
                                break;

                            case pdf::PDFCertificateInfo::KeyUnknown:
                                publicKeyMethod = PDFToolTranslationContext::tr("Unknown method, %1-bit key").arg(currentInfo.getKeySize());
                                break;

                            default:
                                Q_ASSERT(false);
                                break;
                        }

                        formatter.beginTableRow("protection-method");
                        formatter.writeTableColumn("description", PDFToolTranslationContext::tr("Encryption method"));
                        formatter.writeTableColumn("value", publicKeyMethod);
                        formatter.endTableRow();

                        QDateTime notValidBefore = currentInfo.getNotValidBefore().toLocalTime();
                        QDateTime notValidAfter = currentInfo.getNotValidAfter().toLocalTime();

                        if (notValidBefore.isValid())
                        {
                            formatter.beginTableRow("valid-from");
                            formatter.writeTableColumn("description", PDFToolTranslationContext::tr("Valid from"));
                            formatter.writeTableColumn("value", convertDateTimeToString(notValidBefore, options.outputDateFormat));
                            formatter.endTableRow();
                        }

                        if (notValidAfter.isValid())
                        {
                            formatter.beginTableRow("valid-to");
                            formatter.writeTableColumn("description", PDFToolTranslationContext::tr("Valid to"));
                            formatter.writeTableColumn("value", convertDateTimeToString(notValidAfter, options.outputDateFormat));
                            formatter.endTableRow();
                        }

                        pdf::PDFCertificateInfo::KeyUsageFlags keyUsageFlags = currentInfo.getKeyUsage();
                        QStringList keyUsages;
                        if (keyUsageFlags.testFlag(pdf::PDFCertificateInfo::KeyUsageDigitalSignature))
                        {
                            keyUsages << PDFToolTranslationContext::tr("Digital signatures");
                        }
                        if (keyUsageFlags.testFlag(pdf::PDFCertificateInfo::KeyUsageNonRepudiation))
                        {
                            keyUsages << PDFToolTranslationContext::tr("Non-repudiation");
                        }
                        if (keyUsageFlags.testFlag(pdf::PDFCertificateInfo::KeyUsageKeyEncipherment))
                        {
                            keyUsages << PDFToolTranslationContext::tr("Key encipherement");
                        }
                        if (keyUsageFlags.testFlag(pdf::PDFCertificateInfo::KeyUsageDataEncipherment))
                        {
                            keyUsages << PDFToolTranslationContext::tr("Application data encipherement");
                        }
                        if (keyUsageFlags.testFlag(pdf::PDFCertificateInfo::KeyUsageAgreement))
                        {
                            keyUsages << PDFToolTranslationContext::tr("Key agreement");
                        }
                        if (keyUsageFlags.testFlag(pdf::PDFCertificateInfo::KeyUsageCertSign))
                        {
                            keyUsages << PDFToolTranslationContext::tr("Verify signatures on certificates");
                        }
                        if (keyUsageFlags.testFlag(pdf::PDFCertificateInfo::KeyUsageCrlSign))
                        {
                            keyUsages << PDFToolTranslationContext::tr("Verify signatures on revocation information");
                        }
                        if (keyUsageFlags.testFlag(pdf::PDFCertificateInfo::KeyUsageEncipherOnly))
                        {
                            keyUsages << PDFToolTranslationContext::tr("Encipher data during key agreement");
                        }
                        if (keyUsageFlags.testFlag(pdf::PDFCertificateInfo::KeyUsageDecipherOnly))
                        {
                            keyUsages << PDFToolTranslationContext::tr("Decipher data during key agreement");
                        }
                        if (keyUsageFlags.testFlag(pdf::PDFCertificateInfo::KeyUsageExtended_TIMESTAMP))
                        {
                            keyUsages << PDFToolTranslationContext::tr("Trusted timestamping");
                        }

                        formatter.beginTableRow("key-usages");
                        formatter.writeTableColumn("description", PDFToolTranslationContext::tr("Key usage"));
                        formatter.writeTableColumn("value", keyUsages.join(", "));
                        formatter.endTableRow();

                        formatter.endTable();
                        formatter.endl();
                    }
                }

                formatter.endHeader();
                ++ii;
            }

            formatter.endHeader();
        }
    }
    else
    {
        formatter.writeText("no-signatures", PDFToolTranslationContext::tr("No digital signatures or timestamps found in the document."));
    }

    formatter.endDocument();

    PDFConsole::writeText(formatter.getString(), options.outputCodec);
    return ExitSuccess;
}

PDFToolAbstractApplication::Options PDFToolVerifySignaturesApplication::getOptionsFlags() const
{
    return PDFToolAbstractApplication::ConsoleFormat | PDFToolAbstractApplication::OpenDocument | PDFToolAbstractApplication::SignatureVerification | PDFToolAbstractApplication::DateFormat;
}

}   // namespace pdftool
