/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
package com.scytl.cryptolib.certificates.factory;

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.EnumSet;

import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.certificates.bean.X509CertificateType;
import com.scytl.cryptolib.certificates.bean.X509DistinguishedName;
import com.scytl.cryptolib.certificates.bean.extensions.AbstractCertificateExtension;
import com.scytl.cryptolib.certificates.bean.extensions.CertificateKeyUsage;
import com.scytl.cryptolib.certificates.bean.extensions.CertificateKeyUsageExtension;
import com.scytl.cryptolib.certificates.bean.extensions.ExtensionType;
import com.scytl.cryptolib.certificates.constants.X509CertificateConstants;
import com.scytl.cryptolib.certificates.cryptoapi.CryptoAPIX509Certificate;
import com.scytl.cryptolib.certificates.utils.LdapHelper;
import com.scytl.cryptolib.commons.utils.validations.Validate;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;

/**
 * Class which implements {@link CryptoAPIX509Certificate}.
 * <p>
 * Instances of this class are immutable.
 */
public final class CryptoX509Certificate implements
        CryptoAPIX509Certificate {

    private static final String PARSE_ISSUER_DN_ERROR_MESSAGE =
        "Could not parse issuer distinguished name.";

    private static final String PARSE_SUBJECT_DN_ERROR_MESSAGE =
        "Could not parse subject distinguished name.";

    private static final String GET_ENCODED_ERROR_MESSAGE =
        "Could not retrieve certificate as array of bytes.";

    private static final String UNRECOGNIZED_BASIC_CONSTRAINTS_INTEGER_VALUE =
        "Unrecognized basic constraints integer value ";

    private final X509Certificate _certificate;

    private X509DistinguishedName _subjectDn;

    private X509DistinguishedName _issuerDn;

    private final boolean[] _keyUsageFlags;

    /**
     * Returns the JCE X509 certificate.
     *
     * @return the JCE X509 certificate.
     */
    @Override
    public X509Certificate getCertificate() {

        return _certificate;
    }

    /**
     * Creates an object with X509 certificate that is going to be stored.
     *
     * @param certificate
     *            The X509 certificate that is going to be stored.
     * @throws GeneralCryptoLibException
     *             if the subject distinguished name could not be parsed.
     */
    public CryptoX509Certificate(final X509Certificate certificate)
            throws GeneralCryptoLibException {

        Validate.notNull(certificate, "X509 certificate");
        try {
            Validate.notNullOrEmpty(certificate.getEncoded(),
                "X509 certificate content");
        } catch (CertificateEncodingException e) {
            throw new GeneralCryptoLibException(
                "Could not validate content of X509 certificate.", e);
        }

        _certificate = certificate;

        final LdapHelper ldapHelper = new LdapHelper();

        String subjectDn =
            _certificate.getSubjectX500Principal().getName();
        try {
            String subjectCn =
                ldapHelper.getAttributeFromDistinguishedName(subjectDn,
                    X509CertificateConstants.COMMON_NAME_ATTRIBUTE_NAME);
            String subjectOrgUnit =
                ldapHelper
                    .getAttributeFromDistinguishedName(
                        subjectDn,
                        X509CertificateConstants.ORGANIZATIONAL_UNIT_ATTRIBUTE_NAME);
            String subjectOrg =
                ldapHelper.getAttributeFromDistinguishedName(subjectDn,
                    X509CertificateConstants.ORGANIZATION_ATTRIBUTE_NAME);
            String subjectLocality =
                ldapHelper.getAttributeFromDistinguishedName(subjectDn,
                    X509CertificateConstants.LOCALITY_ATTRIBUTE_NAME);
            String subjectCountry =
                ldapHelper.getAttributeFromDistinguishedName(subjectDn,
                    X509CertificateConstants.COUNTRY_ATTRIBUTE_NAME);
            _subjectDn =
                new X509DistinguishedName.Builder(subjectCn,
                    subjectCountry).addOrganizationalUnit(subjectOrgUnit)
                    .addOrganization(subjectOrg)
                    .addLocality(subjectLocality).build();

        } catch (GeneralCryptoLibException e) {
            throw new GeneralCryptoLibException(
                PARSE_SUBJECT_DN_ERROR_MESSAGE, e);
        }

        String issuerDn = _certificate.getIssuerX500Principal().getName();
        try {
            String issuerCn =
                ldapHelper.getAttributeFromDistinguishedName(issuerDn,
                    X509CertificateConstants.COMMON_NAME_ATTRIBUTE_NAME);
            String issuerOrgUnit =
                ldapHelper
                    .getAttributeFromDistinguishedName(
                        issuerDn,
                        X509CertificateConstants.ORGANIZATIONAL_UNIT_ATTRIBUTE_NAME);
            String issuerOrg =
                ldapHelper.getAttributeFromDistinguishedName(issuerDn,
                    X509CertificateConstants.ORGANIZATION_ATTRIBUTE_NAME);
            String issuerLocality =
                ldapHelper.getAttributeFromDistinguishedName(issuerDn,
                    X509CertificateConstants.LOCALITY_ATTRIBUTE_NAME);
            String issuerCountry =
                ldapHelper.getAttributeFromDistinguishedName(issuerDn,
                    X509CertificateConstants.COUNTRY_ATTRIBUTE_NAME);
            _issuerDn =
                new X509DistinguishedName.Builder(issuerCn, issuerCountry)
                    .addOrganizationalUnit(issuerOrgUnit)
                    .addOrganization(issuerOrg)
                    .addLocality(issuerLocality).build();
        } catch (GeneralCryptoLibException e) {
            throw new GeneralCryptoLibException(
                PARSE_ISSUER_DN_ERROR_MESSAGE, e);
        }

        _keyUsageFlags = _certificate.getKeyUsage();
    }

    @Override
    public BigInteger getSerialNumber() {

        return _certificate.getSerialNumber();
    }

    @Override
    public Date getNotBefore() {

        return _certificate.getNotBefore();
    }

    @Override
    public Date getNotAfter() {

        return _certificate.getNotAfter();
    }

    @Override
    public boolean checkValidity() {

        // If checkValidity method does not throw exception, then certificate is
        // valid.
        try {
            _certificate.checkValidity();
            return true;
        } catch (CertificateExpiredException e) {
            return false;
        } catch (CertificateNotYetValidException e) {
            return false;
        }
    }

    @Override
    public boolean checkValidity(final Date date)
            throws GeneralCryptoLibException {

        Validate.notNull(date, "Date");

        // If checkValidity method does not throw exception, then date is within
        // the certificate's period of validity.
        try {
            _certificate.checkValidity(date);
            return true;
        } catch (CertificateExpiredException e) {
            return false;
        } catch (CertificateNotYetValidException e) {
            return false;
        }
    }

    @Override
    public X509DistinguishedName getSubjectDn() {

        return _subjectDn;
    }

    @Override
    public X509DistinguishedName getIssuerDn() {

        return _issuerDn;
    }

    @Override
    public PublicKey getPublicKey() {

        return _certificate.getPublicKey();
    }

    @Override
    public boolean verify(final PublicKey issuerPublicKey)
            throws GeneralCryptoLibException {

        Validate.notNull(issuerPublicKey, "Issuer public key");
        Validate.notNullOrEmpty(issuerPublicKey.getEncoded(),
            "Issuer public key content");

        try {
            _certificate.verify(issuerPublicKey);
        } catch (InvalidKeyException | CertificateException
                | NoSuchProviderException | NoSuchAlgorithmException
                | SignatureException e) {
            return false;
        }
        return true;
    }

    /**
     * Checks if a specified X509 certificate has authority key usage.
     *
     * @return true if certificate is a certificate authority and false
     *         otherwise.
     * @deprecated Should use new method {@code isCertificateType} instead.
     */
    @Override
    @Deprecated
    public boolean isCertificateAuthority() {

        int basicConstraintsIntVal = _certificate.getBasicConstraints();
        switch (basicConstraintsIntVal) {
        case X509CertificateConstants.IS_CERTIFICATE_AUTHORITY_BASIC_CONSTRAINTS_INTEGER_VALUE:
            return true;
        case X509CertificateConstants.IS_NOT_CERTIFICATE_AUTHORITY_BASIC_CONSTRAINTS_INTEGER_VALUE:
            return false;
        default:
            throw new CryptoLibException(
                UNRECOGNIZED_BASIC_CONSTRAINTS_INTEGER_VALUE + "'"
                    + basicConstraintsIntVal + "'");
        }
    }

    @Override
	public boolean isCertificateType(final X509CertificateType certificateType) {

        if (certificateType == X509CertificateType.CERTIFICATE_AUTHORITY &&
			_certificate
						.getBasicConstraints() != 
							X509CertificateConstants.IS_CERTIFICATE_AUTHORITY_BASIC_CONSTRAINTS_INTEGER_VALUE) {
			return false;
        }

        return hasKeyUsageSet(getKeyUsageSetForCertificateType(certificateType));
    }

    @Override
    public byte[] getEncoded() {

        try {
            return _certificate.getEncoded();
        } catch (CertificateEncodingException e) {
            throw new CryptoLibException(GET_ENCODED_ERROR_MESSAGE, e);
        }
    }

    @Override
    public byte[] getPemEncoded() {

        try {
            return PemUtils.certificateToPem(_certificate).getBytes(
                StandardCharsets.UTF_8);
        } catch (GeneralCryptoLibException e) {
            throw new CryptoLibException(
                "Could not convert certificate to PEM format.", e);
        }
    }

    private EnumSet<CertificateKeyUsage> getKeyUsageSetForCertificateType(
            final X509CertificateType certificateType) {

        AbstractCertificateExtension[] certificateExtensions =
            certificateType.getExtensions();

        EnumSet<CertificateKeyUsage> certificateKeyUsages =
            EnumSet.noneOf(CertificateKeyUsage.class);
        for (AbstractCertificateExtension extension : certificateExtensions) {
            if (extension.getExtensionType() == ExtensionType.KEY_USAGE) {
                certificateKeyUsages =
                    ((CertificateKeyUsageExtension) extension)
                        .getKeyUsages();
            }
        }

        return certificateKeyUsages;
    }

    private boolean hasKeyUsageSet(
            final EnumSet<CertificateKeyUsage> certificateKeyUsages) {

        if (_keyUsageFlags == null) {
            return false;
        }

        int keyUsageCounter = 0;
        for (CertificateKeyUsage keyUsage : certificateKeyUsages) {
            if ((keyUsage == CertificateKeyUsage.DIGITAL_SIGNATURE)
                && _keyUsageFlags[0]) {
                keyUsageCounter++;
            } else if ((keyUsage == CertificateKeyUsage.NON_REPUDIATION)
                && _keyUsageFlags[1]) {
                keyUsageCounter++;
            } else if ((keyUsage == CertificateKeyUsage.KEY_ENCIPHERMENT)
                && _keyUsageFlags[2]) {
                keyUsageCounter++;
            } else if ((keyUsage == CertificateKeyUsage.DATA_ENCIPHERMENT)
                && _keyUsageFlags[3]) {
                keyUsageCounter++;
            } else if ((keyUsage == CertificateKeyUsage.KEY_AGREEMENT)
                && _keyUsageFlags[4]) {
                keyUsageCounter++;
            } else if ((keyUsage == CertificateKeyUsage.KEY_CERT_SIGN)
                && _keyUsageFlags[5]) {
                keyUsageCounter++;
            } else if ((keyUsage == CertificateKeyUsage.CRL_SIGN)
                && _keyUsageFlags[6]) {
                keyUsageCounter++;
            } else if ((keyUsage == CertificateKeyUsage.ENCIPHER_ONLY)
                && _keyUsageFlags[7]) {
                keyUsageCounter++;
            } else if ((keyUsage == CertificateKeyUsage.DECIPHER_ONLY)
                && _keyUsageFlags[8]) {
                keyUsageCounter++;
            }
        }

        if (keyUsageCounter == certificateKeyUsages.size()) {
            return true;
        } else {
            return false;
        }
    }
}
