/*
 * 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.utils;

import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

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.configuration.X509CertificateValidationData;
import com.scytl.cryptolib.certificates.configuration.X509CertificateValidationResult;
import com.scytl.cryptolib.certificates.configuration.X509CertificateValidationType;
import com.scytl.cryptolib.certificates.factory.CryptoX509Certificate;
import com.scytl.cryptolib.commons.utils.validations.Validate;

/**
 * Class to validate the content of a
 * {@link com.scytl.cryptolib.certificates.factory.CryptoX509Certificate}.
 */
public class X509CertificateValidator {

    private final CryptoX509Certificate _cryptoX509Cert;

    private final X509CertificateValidationData _validationData;

    private final Set<X509CertificateValidationType> _validationTypes;

    private final List<X509CertificateValidationType> _failedValidationTypes;

    /**
     * Creates a certificate validator instance.
     * 
     * @param cryptoX509Cert
     *            the {@link CryptoX509Certificate} whose content is to be
     *            validated.
     * @param validationData
     *            the {@link X509CertificateValidationData} against which the
     *            content of the {@link CryptoX509Certificate} is to be
     *            validated.
     * @param validationTypes
     *            the array of {@link X509CertificateValidationType} objects
     *            specifying the types of validations to apply to the
     *            {@link CryptoX509Certificate} .
     * @throws GeneralCryptoLibException
     *             if the input validation fails.
     */
    public X509CertificateValidator(
            final CryptoX509Certificate cryptoX509Cert,
            final X509CertificateValidationData validationData,
            final X509CertificateValidationType... validationTypes)
            throws GeneralCryptoLibException {

        Validate.notNull(cryptoX509Cert, "Certificate to validate");
        Validate.notNull(validationData, "Validation data");
        Validate.notNullOrEmpty(validationTypes, "Validation type array");
        for (X509CertificateValidationType validationType : validationTypes) {
            Validate.notNull(validationType, "A validation type");
        }

        _cryptoX509Cert = cryptoX509Cert;

        _validationData = validationData;

        _validationTypes = new HashSet<>();
        for (X509CertificateValidationType validationType : validationTypes) {
            if (validationType == null) {
                throw new CryptoLibException(
                    "Null certificate validation type provided for certificate content validation.");
            }

            _validationTypes.add(validationType);
        }

        _failedValidationTypes = new ArrayList<>();
    }

    /**
     * Validates the content of the {@link CryptoX509Certificate}.
     * 
     * @return a {@link X509CertificateValidationResult} indicating any
     *         validations that failed.
     * @throws GeneralCryptoLibException
     *             if the validation process fails.
     */
    public X509CertificateValidationResult validate()
            throws GeneralCryptoLibException {

        for (X509CertificateValidationType validationType : _validationTypes) {
            switch (validationType) {
            case DATE:
                if (!validateDate()) {
                    _failedValidationTypes
                        .add(X509CertificateValidationType.DATE);
                }
                break;
            case SUBJECT:
                if (!validateSubjectDn()) {
                    _failedValidationTypes
                        .add(X509CertificateValidationType.SUBJECT);
                }
                break;
            case ISSUER:
                if (!validateIssuerDn()) {
                    _failedValidationTypes
                        .add(X509CertificateValidationType.ISSUER);
                }
                break;
            case KEY_TYPE:
                if (!validateKeyType()) {
                    _failedValidationTypes
                        .add(X509CertificateValidationType.KEY_TYPE);
                }
                break;
            default:
                if (!validateSignature()) {
                    _failedValidationTypes
                        .add(X509CertificateValidationType.SIGNATURE);
                }
                break;
            }
        }

        X509CertificateValidationType[] failedValidationTypes =
            new X509CertificateValidationType[_failedValidationTypes
                .size()];
        _failedValidationTypes.toArray(failedValidationTypes);

        return new X509CertificateValidationResult(
            _failedValidationTypes.isEmpty(), failedValidationTypes);
    }

    private boolean validateDate() throws GeneralCryptoLibException {

        Date date = _validationData.getDate();

        Validate.notNull(date,
            "Validity date of certificate validation data");

        return _cryptoX509Cert.checkValidity(date);
    }

    private boolean validateSubjectDn() throws GeneralCryptoLibException {

        X509DistinguishedName subjectDn = _validationData.getSubjectDn();

        Validate.notNull(subjectDn,
            "Subject DN of certificate validation data");

        return subjectDn.equals(_cryptoX509Cert.getSubjectDn());
    }

    private boolean validateIssuerDn() throws GeneralCryptoLibException {

        X509DistinguishedName issuerDn = _validationData.getIssuerDn();

        Validate.notNull(issuerDn,
            "Issuer DN of certificate validation data");

        return issuerDn.equals(_cryptoX509Cert.getIssuerDn());
    }

    private boolean validateKeyType() throws GeneralCryptoLibException {

        X509CertificateType certType =
            _validationData.getCertificateType();

        Validate.notNull(certType,
            "Certificate type of certificate validation data");

        return _cryptoX509Cert.isCertificateType(certType);
    }

    private boolean validateSignature() throws GeneralCryptoLibException {

        PublicKey caPublicKey = _validationData.getCaPublicKey();

        Validate.notNull(caPublicKey,
            "CA public key of certificate validation data");

        return _cryptoX509Cert.verify(caPublicKey);
    }
}
