/***************************************************************************
 *   Copyright (C) 2018 by Chinmoy Ranjan Pradhan <chinmoyrp65@gmail.com>  *
 *                                                                         *
 *   This program 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.                                   *
 ***************************************************************************/

#include "certificateviewer.h"

#include <KMessageBox>
#include <KLocalizedString>
#include <KColumnResizer>

#include <QDebug>
#include <QLabel>
#include <QTextEdit>
#include <QTreeView>
#include <QGroupBox>
#include <QFormLayout>
#include <QPushButton>
#include <QVBoxLayout>
#include <QFileDialog>
#include <QDialogButtonBox>
#include <QCryptographicHash>

#include "signatureguiutils.h"

// DN (DistinguishedName) attributes can be
//     C Country
//     CN Common name
//     DC Domain component
//     E E-mail address
//     EMAIL E-mail address (preferred)
//     EMAILADDRESS E-mail address
//     L Locality
//     O Organization name
//     OU Organizational unit name
//     PC Postal code
//     S State or province
//     SN Family name
//     SP State or province
//     ST State or province (preferred)
//     STREET Street
//     T Title


// CN=James Hacker,
//    L=Basingstoke,
//    O=Widget Inc,
//    C=GB
// CN=L. Eagle, O="Sue, Grabbit and Runn", C=GB
// CN=L. Eagle, O=Sue\, Grabbit and Runn, C=GB


// This is a poor man's attempt at parsing DN, if it fails it is not a problem since it's only for display in a list
static QString splitDNAttributes(const QStringList &text)
{
    const QStringList attributes = { "C", "CN", "DC", "E", "EMAIL", "EMAILADDRESS", "L", "O", "OU", "PC", "S", "SN", "SP", "ST", "STREET", "T" };

    for ( const QString &t : text )
    {
        for ( const QString &attribute : attributes )
        {
            const QRegularExpression re( QStringLiteral( "(.*),\\s*(%1=.*)" ).arg( attribute ), QRegularExpression::DotMatchesEverythingOption );
            const QRegularExpressionMatch match = re.match( t );
            if ( match.hasMatch() )
            {
                QStringList results = text;
                const int index = results.indexOf( t );
                results.removeAt( index );
                results.insert( index, match.captured( 2 ) );
                results.insert( index, match.captured( 1 ) );
                return splitDNAttributes( results );
            }
        }
    }

    // Clean escaped commas
    QStringList result = text;
    for ( QString &t : result )
    {
        t.replace( QLatin1String("\\,"), QLatin1String(",") );
    }

    // Clean up quoted attributes
    for ( QString &t : result )
    {
        for ( const QString &attribute : attributes )
        {
            const QRegularExpression re( QStringLiteral( "%1=\"(.*)\"" ).arg( attribute ) );
            const QRegularExpressionMatch match = re.match( t );
            if ( match.hasMatch() )
            {
                t = attribute + "=" + match.captured( 1 );
            }
        }
    }

    return result.join(QStringLiteral("\n"));
}

static QString splitDNAttributes(const QString &text)
{
    return splitDNAttributes( QStringList{ text } );
}


CertificateModel::CertificateModel( const Okular::CertificateInfo &certInfo, QObject * parent )
  : QAbstractTableModel( parent ), m_certificateInfo( certInfo )
{
    m_certificateProperties = { Version, SerialNumber, Issuer, IssuedOn, ExpiresOn, Subject, PublicKey, KeyUsage };
}


int CertificateModel::columnCount( const QModelIndex & ) const
{
    return 2;
}

int CertificateModel::rowCount( const QModelIndex & ) const
{
    return m_certificateProperties.size();
}

static QString propertyVisibleName( CertificateModel::Property p )
{
    switch ( p )
    {
        case CertificateModel::Version:
            return i18n("Version");
        case CertificateModel::SerialNumber:
            return i18n("Serial Number");
        case CertificateModel::Issuer:
            return i18n("Issuer");
        case CertificateModel::IssuedOn:
            return i18n("Issued On");
        case CertificateModel::ExpiresOn:
            return i18n("Expires On");
        case CertificateModel::Subject:
            return i18nc("The person/company that made the signature", "Subject");
        case CertificateModel::PublicKey:
            return i18n("Public Key");
        case CertificateModel::KeyUsage:
            return i18n("Key Usage");
    }
    return QString();
}

static QString propertyVisibleValue( CertificateModel::Property p, const Okular::CertificateInfo &certInfo )
{
    switch ( p )
    {
        case CertificateModel::Version:
            return i18n("V%1", QString::number( certInfo.version() ));
        case CertificateModel::SerialNumber:
            return certInfo.serialNumber().toHex(' ');
        case CertificateModel::Issuer:
            return certInfo.issuerInfo(Okular::CertificateInfo::DistinguishedName);
        case CertificateModel::IssuedOn:
            return certInfo.validityStart().toString( Qt::DefaultLocaleLongDate );
        case CertificateModel::ExpiresOn:
            return certInfo.validityEnd().toString( Qt::DefaultLocaleLongDate );
        case CertificateModel::Subject:
            return certInfo.subjectInfo(Okular::CertificateInfo::DistinguishedName);
        case CertificateModel::PublicKey:
            return i18n("%1 (%2 bits)", SignatureGuiUtils::getReadablePublicKeyType( certInfo.publicKeyType() ),
                                                                        certInfo.publicKeyStrength());
        case CertificateModel::KeyUsage:
            return SignatureGuiUtils::getReadableKeyUsageCommaSeparated( certInfo.keyUsageExtensions() );
    }
    return QString();
}

QVariant CertificateModel::data( const QModelIndex &index, int role ) const
{
    const int row = index.row();
    if ( !index.isValid() || row < 0 || row >= m_certificateProperties.count() )
        return QVariant();

    switch ( role )
    {
        case Qt::DisplayRole:
        case Qt::ToolTipRole:
        switch ( index.column() )
        {
            case 0:
                return propertyVisibleName( m_certificateProperties[row] );
            case 1:
                return propertyVisibleValue( m_certificateProperties[row], m_certificateInfo );
            default:
                return QString();
        }
        case PropertyKeyRole:
            return m_certificateProperties[row];
        case PropertyVisibleValueRole:
            return propertyVisibleValue( m_certificateProperties[row], m_certificateInfo );
    }

    return QVariant();
}

QVariant CertificateModel::headerData( int section, Qt::Orientation orientation, int role ) const
{
    if ( role == Qt::TextAlignmentRole )
        return QVariant( Qt::AlignLeft );

    if ( orientation != Qt::Horizontal || role != Qt::DisplayRole)
        return QVariant();

    switch ( section )
    {
        case 0:
            return i18n("Property");
        case 1:
            return i18n("Value");
        default:
            return QVariant();
    }
}

CertificateViewer::CertificateViewer( const Okular::CertificateInfo &certInfo, QWidget *parent )
    : KPageDialog( parent ), m_certificateInfo( certInfo )
{
    setModal( true );
    setMinimumSize( QSize( 500, 500 ));
    setFaceType( Tabbed );
    setWindowTitle( i18n("Certificate Viewer") );
    setStandardButtons( QDialogButtonBox::Close );

    auto exportBtn = new QPushButton( i18n("Export...") );
    connect( exportBtn, &QPushButton::clicked, this, &CertificateViewer::exportCertificate );
    addActionButton( exportBtn );

    // General tab
    auto generalPage = new QFrame( this );
    addPage( generalPage, i18n("General") );

    auto issuerBox = new QGroupBox( i18n("Issued By"), generalPage );
    auto issuerFormLayout = new QFormLayout( issuerBox );
    issuerFormLayout->setLabelAlignment( Qt::AlignLeft );
    issuerFormLayout->addRow( i18n("Common Name(CN)"), new QLabel( m_certificateInfo.issuerInfo( Okular::CertificateInfo::CommonName ) ) );
    issuerFormLayout->addRow( i18n("EMail"), new QLabel( m_certificateInfo.issuerInfo( Okular::CertificateInfo::EmailAddress ) ) );
    issuerFormLayout->addRow( i18n("Organization(O)"), new QLabel( m_certificateInfo.issuerInfo( Okular::CertificateInfo::Organization ) ) );

    auto subjectBox = new QGroupBox( i18n("Issued To"), generalPage );
    auto subjectFormLayout = new QFormLayout( subjectBox );
    subjectFormLayout->setLabelAlignment( Qt::AlignLeft );
    subjectFormLayout->addRow( i18n("Common Name(CN)"), new QLabel( m_certificateInfo.subjectInfo( Okular::CertificateInfo::CommonName ) ) );
    subjectFormLayout->addRow( i18n("EMail"), new QLabel( m_certificateInfo.subjectInfo( Okular::CertificateInfo::EmailAddress ) ) );
    subjectFormLayout->addRow( i18n("Organization(O)"), new QLabel( m_certificateInfo.subjectInfo( Okular::CertificateInfo::Organization ) ) );

    auto validityBox = new QGroupBox( i18n("Validity"), generalPage );
    auto validityFormLayout = new QFormLayout( validityBox );
    validityFormLayout->setLabelAlignment( Qt::AlignLeft );
    validityFormLayout->addRow( i18n("Issued On"), new QLabel( m_certificateInfo.validityStart().toString( Qt::DefaultLocaleLongDate ) ) );
    validityFormLayout->addRow( i18n("Expires On"), new QLabel( m_certificateInfo.validityEnd().toString( Qt::DefaultLocaleLongDate ) ) );

    auto fingerprintBox = new QGroupBox( i18n("Fingerprints"), generalPage );
    auto fingerprintFormLayout = new QFormLayout( fingerprintBox );
    fingerprintFormLayout->setLabelAlignment( Qt::AlignLeft );
    QByteArray certData = m_certificateInfo.certificateData();
    auto sha1Label = new QLabel( QString( QCryptographicHash::hash( certData, QCryptographicHash::Sha1 ).toHex(' ') ) );
    sha1Label->setWordWrap( true );
    auto sha256Label = new QLabel( QString( QCryptographicHash::hash( certData, QCryptographicHash::Sha256 ).toHex(' ') ) );
    sha256Label->setWordWrap( true );
    fingerprintFormLayout->addRow( i18n("SHA-1 Fingerprint"), sha1Label );
    fingerprintFormLayout->addRow( i18n("SHA-256 Fingerprint"), sha256Label );

    auto generalPageLayout = new QVBoxLayout( generalPage );
    generalPageLayout->addWidget( issuerBox );
    generalPageLayout->addWidget( subjectBox );
    generalPageLayout->addWidget( validityBox );
    generalPageLayout->addWidget( fingerprintBox );
    generalPageLayout->addStretch();

    //force column 1 to have same width
    auto resizer = new KColumnResizer( this );
    resizer->addWidgetsFromLayout( issuerBox->layout(), 0 );
    resizer->addWidgetsFromLayout( subjectBox->layout(), 0 );
    resizer->addWidgetsFromLayout( validityBox->layout(), 0 );
    resizer->addWidgetsFromLayout( fingerprintBox->layout(), 0 );

    // Details tab
    auto detailsFrame = new QFrame( this );
    addPage( detailsFrame, i18n("Details") );
    auto certDataLabel = new QLabel( i18n("Certificate Data:") );
    auto certTree = new QTreeView( this );
    certTree->setIndentation( 0 );
    m_certificateModel = new CertificateModel( m_certificateInfo, this );
    certTree->setModel( m_certificateModel );
    connect( certTree->selectionModel(), &QItemSelectionModel::currentChanged, this, &CertificateViewer::updateText );
    m_propertyText = new QTextEdit( this );
    m_propertyText->setReadOnly( true );

    auto detailsPageLayout = new QVBoxLayout( detailsFrame );
    detailsPageLayout->addWidget( certDataLabel );
    detailsPageLayout->addWidget( certTree );
    detailsPageLayout->addWidget( m_propertyText );
}

void CertificateViewer::updateText( const QModelIndex &index )
{
    QString text;
    const CertificateModel::Property key = m_certificateModel->data( index, CertificateModel::PropertyKeyRole ).value<CertificateModel::Property>();
    switch ( key )
    {
        case CertificateModel::SerialNumber:
        case CertificateModel::Version:
        case CertificateModel::IssuedOn:
        case CertificateModel::ExpiresOn:
            text = m_certificateModel->data( index, CertificateModel::PropertyVisibleValueRole ).toString();
            break;
        case CertificateModel::Issuer:
        case CertificateModel::Subject:
            text = splitDNAttributes( m_certificateModel->data( index, CertificateModel::PropertyVisibleValueRole ).toString() );
            break;
        case CertificateModel::PublicKey:
            text = m_certificateInfo.publicKey().toHex(' ');
            break;
        case CertificateModel::KeyUsage:
            text = SignatureGuiUtils::getReadableKeyUsageNewLineSeparated( m_certificateInfo.keyUsageExtensions() );
            break;
    }
    m_propertyText->setText( text );
}

void CertificateViewer::exportCertificate()
{
    const QString caption = i18n("Where do you want to save this certificate?");
    const QString path = QFileDialog::getSaveFileName( this, caption, QStringLiteral("Certificate.cer"), i18n("Certificate File (*.cer)") );
    if ( !path.isEmpty() )
    {
        QFile targetFile( path );
        targetFile.open( QIODevice::WriteOnly );
        if ( targetFile.write( m_certificateInfo.certificateData() ) == -1 )
        {
            KMessageBox::error( this, i18n("Unable to export certificate!") );
        }
        targetFile.close();
    }
}
