/* DigiDoc4J library
*
* This software is released under either the GNU Library General Public
* License (see LICENSE.LGPL).
*
* Note that the only valid version of the LGPL license as far as this
* project is concerned is the original GNU Library General Public License
* Version 2.1, February 1999
*/

package org.digidoc4j;

import org.apache.commons.lang.StringUtils;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DLSequence;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Serializable;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.*;


/**
 * Wrapper for java.security.cert.X509Certificate object.
 */
public class X509Cert implements Serializable {
  private static final Logger logger = LoggerFactory.getLogger(X509Cert.class);
  private X509Certificate originalCert;
  private Map<String, String> issuerPartMap;
  private Map<String, String> subjectNamePartMap;

  /**
   * Key usage.
   */
  public enum KeyUsage {
    DIGITAL_SIGNATURE,
    /**
     * Used for signing certificate selection in the current library
     */
    NON_REPUDIATION,
    KEY_ENCIPHERMENT,
    DATA_ENCIPHERMENT,
    KEY_AGREEMENT,
    KEY_CERTIFICATESIGN,
    CRL_SIGN,
    ENCIPHER_ONLY,
    DECIPHER_ONLY
  }

  /**
   * Issuer parts.
   */
  public enum Issuer {
    EMAILADDRESS,
    C,
    O,
    CN
  }

  /**
   * Subject Name parts.
   */
  public enum SubjectName {
    SERIALNUMBER,
    GIVENNAME,
    SURNAME,
    CN,
    OU,
    O,
    C
  }


  /**
   * Creates a copy of the X509Certificate.
   *
   * @param cert X509 certificate to be wrapped
   */
  public X509Cert(X509Certificate cert) {
    logger.debug("");
    originalCert = cert;
  }

  /**
   * Creates an X509 certificate from a path.
   *
   * @param path X509 certificate path
   * @throws Exception throws an exception if the X509 certificate parsing fails
   */
  X509Cert(String path) throws CertificateException, IOException {
    logger.debug("");
    CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
    try (FileInputStream inStream = new FileInputStream(new File(path))) {
      originalCert = (X509Certificate) certificateFactory.generateCertificate(inStream);
    }
    logger.debug("Certificate created from path: " + path);
  }

  /**
   * Returns current certificate policies or null if no policies was found.
   *
   * @return list of policies
   * @throws IOException when policy parsing fails
   */
  public List<String> getCertificatePolicies() throws IOException {
    logger.debug("");
    byte[] extensionValue = originalCert.getExtensionValue("2.5.29.32");
    List<String> policies = new ArrayList<>();

    byte[] octets = ((DEROctetString) DEROctetString.fromByteArray(extensionValue)).getOctets();
    ASN1Sequence sequence = (ASN1Sequence) ASN1Sequence.fromByteArray(octets);

    Enumeration sequenceObjects = sequence.getObjects();
    while (sequenceObjects.hasMoreElements()) {
      DLSequence next = (DLSequence) sequenceObjects.nextElement();
      Object objectAt = next.getObjectAt(0);
      if (objectAt != null) {
        policies.add(objectAt.toString());
      }
    }
    return policies;
  }


  /**
   * Returns the internal X509 Certificate of the certificate.
   *
   * @return X509Certificate
   */
  public X509Certificate getX509Certificate() {
    logger.debug("");
    return originalCert;
  }


  /**
   * Retrieves part of the issuer name (for example if set to CN it returns the Common Name part).
   *
   * @param part sets part of issuer name to return
   * @return part of issuer name
   */
  public String issuerName(Issuer part) {
    logger.debug("Part: " + part);
    if (issuerPartMap == null) {
      loadIssuerParts();
    }
    String issuerName = issuerPartMap.get(part.name());
    logger.debug("Issuer name: " + issuerName);
    return issuerName;
  }

  private void loadIssuerParts() {
    logger.debug("");
    String[] parts = StringUtils.split(issuerName(), ',');
    issuerPartMap = new HashMap<>();
    for (String part : parts) {
      String[] strings = StringUtils.split(part, "=");
      String key = strings[0].trim();
      String value = strings[1].trim();
      issuerPartMap.put(key, value);
      logger.debug("Subject name part key: " + key + " value: " + value);
    }
  }

  /**
   * Reads the the whole issuer name from the X.509 certificate.
   *
   * @return issuer name
   */
  public String issuerName() {
    logger.debug("");
    String name = originalCert.getIssuerDN().getName();
    logger.debug("Issuer name: " + name);
    return name;
  }

  /**
   * Validates if the certificate is in a valid time slot.
   *
   * @param date sets date to compare
   * @return boolean indicating if the certificate is in a valid time slot
   */
  public boolean isValid(Date date) {
    logger.debug("Date: " + date);
    try {
      originalCert.checkValidity(date);
    } catch (CertificateExpiredException e) {
      logger.debug("Date " + date + " is not valid");
      return false;
    } catch (CertificateNotYetValidException e) {
      logger.debug("Date " + date + " is not valid");
      return false;
    }
    logger.debug("Date " + date + " is valid");
    return true;
  }

  /**
   * Validates if the current time is between the certificate's validity start date and expiration date.
   *
   * @return boolean indicating if the current time is between the certificate's validity start and expiration date
   */
  public boolean isValid() {
    logger.debug("");
    return (isValid(new Date()));
  }

  /**
   * Returns the current certificate key usage.
   *
   * @return list of key usages
   */
  public List<KeyUsage> getKeyUsages() {
    logger.debug("");
    List<KeyUsage> keyUsages = new ArrayList<>();
    boolean[] keyUsagesBits = originalCert.getKeyUsage();
    for (int i = 0; i < keyUsagesBits.length; i++) {
      if (keyUsagesBits[i]) {
        keyUsages.add(KeyUsage.values()[i]);
      }
    }

    logger.debug("Returning " + keyUsages.size() + "key usages:");
    for (KeyUsage keyUsage : keyUsages) {
      logger.debug("\t" + keyUsage.toString());
    }

    return keyUsages;
  }

  /**
   * Reads serial number from X.509 certificate.
   *
   * @return serial number of the X.509 certificate
   */
  public String getSerial() {
    logger.debug("");
    String serial = Hex.toHexString(originalCert.getSerialNumber().toByteArray());
    logger.debug("Serial number: " + serial);
    return serial;
  }

  /**
   * Returns part of the subject name (for example if set to CN it returns the Common Name part).
   *
   * @param part sets part of subject name to return
   * @return subject name
   */
  public String getSubjectName(SubjectName part) {
    logger.debug("Part: " + part);
    if (subjectNamePartMap == null) {
      loadSubjectNameParts();
    }
    String subjectName = subjectNamePartMap.get(part.name());
    logger.debug("Subject name: " + subjectName);
    return subjectName;
  }

  private void loadSubjectNameParts() {
    logger.debug("");
    String[] parts = getSubjectName().split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)");
    subjectNamePartMap = new HashMap<>();
    for (String part : parts) {
      String[] strings = part.split("=(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)");
      String key = strings[0].trim();
      String value = strings[1].trim();
      subjectNamePartMap.put(key, value);
      logger.debug("Subject name part key: " + key + " value: " + value);
    }
  }

  /**
   * Returns the whole subject name.
   *
   * @return subject name
   */
  public String getSubjectName() {
    logger.debug("");
    String subjectName = originalCert.getSubjectDN().toString();
    logger.debug("Subject name: " + subjectName);
    return subjectName;
  }
}
