/*
 *
 * Copyright (c) 2013 - 2020 Lijun Liao
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.xipki.security;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Provider;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.Date;

import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.RFC4519Style;
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.Certificate;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.xipki.security.util.AlgorithmUtil;
import org.xipki.security.util.KeyUtil;
import org.xipki.security.util.X509Util;
import org.xipki.util.Args;
import org.xipki.util.Hex;

/**
 * Wrapper to an {@link X509Certificate}.
 *
 * @author Lijun Liao
 * @since 5.3.8
 */

public class X509Cert {

  private final Object sync = new Object();

  private X509CertificateHolder bcInstance;

  private X509Certificate jceInstance;

  private final boolean selfSigned;

  private final X500Name issuer;

  private final BigInteger serialNumber;

  private final X500Name subject;

  private final Date notBefore;

  private final Date notAfter;

  private String issuerRfc4519Text;

  private String subjectRfc4519Text;

  private byte[] subjectKeyId;

  private byte[] authorityKeyId;

  private int basicConstrains = -2;

  private boolean keyUsageProcessed;

  private boolean[] keyUsage;

  private SubjectPublicKeyInfo subjectPublicKeyInfo;

  private PublicKey publicKey;

  private byte[] encoded;

  public X509Cert(Certificate cert) {
    this(new X509CertificateHolder(cert), null);
  }

  public X509Cert(Certificate cert, byte[] encoded) {
    this(new X509CertificateHolder(cert), encoded);
  }

  public X509Cert(X509Certificate cert) {
    this(cert, null);
  }

  public X509Cert(X509Certificate cert, byte[] encoded) {
    this.bcInstance = null;
    this.jceInstance = Args.notNull(cert, "cert");
    this.encoded = encoded;

    this.notBefore = cert.getNotBefore();
    this.notAfter = cert.getNotAfter();
    this.serialNumber = cert.getSerialNumber();

    this.issuer = X500Name.getInstance(cert.getIssuerX500Principal().getEncoded());
    this.subject = X500Name.getInstance(cert.getSubjectX500Principal().getEncoded());

    this.selfSigned = subject.equals(issuer);
  }

  public X509Cert(X509CertificateHolder cert) {
    this(cert, null);
  }

  public X509Cert(X509CertificateHolder cert, byte[] encoded) {
    this.bcInstance = Args.notNull(cert, "cert");
    this.jceInstance = null;
    this.encoded = encoded;

    this.notBefore = cert.getNotBefore();
    this.notAfter = cert.getNotAfter();
    this.serialNumber = cert.getSerialNumber();

    this.issuer = cert.getIssuer();
    this.subject = cert.getSubject();
    this.selfSigned = subject.equals(issuer);
  }

  /**
   * Gets the certificate constraints path length from the
   * critical {@code BasicConstraints} extension, (OID = 2.5.29.19).
   * <p/>
   * The basic constraints extension identifies whether the subject
   * of the certificate is a Certificate Authority (CA) and
   * how deep a certification path may exist through that CA. The
   * {@code pathLenConstraint} field (see below) is meaningful
   * only if {@code cA} is set to TRUE. In this case, it gives the
   * maximum number of CA certificates that may follow this certificate in a
   * certification path. A value of zero indicates that only an end-entity
   * certificate may follow in the path.
   * <p/>
   * The ASN.1 definition for this is:
   * <pre>
   * BasicConstraints ::= SEQUENCE {
   *     cA                  BOOLEAN DEFAULT FALSE,
   *     pathLenConstraint   INTEGER (0..MAX) OPTIONAL }
   * </pre>
   *
   * @return the value of {@code pathLenConstraint} if the
   *     BasicConstraints extension is present in the certificate and the
   *     subject of the certificate is a CA, otherwise -1.
   *     If the subject of the certificate is a CA and
   *     {@code pathLenConstraint} does not appear,
   *     {@code Integer.MAX_VALUE} is returned to indicate that there is no
   *     limit to the allowed length of the certification path.
   */
  public int getBasicConstraints() {
    if (basicConstrains == -2) {
      synchronized (sync) {
        if (bcInstance != null) {
          byte[] extnValue = getCoreExtValue(Extension.basicConstraints);
          if (extnValue == null) {
            basicConstrains = -1;
          } else {
            BasicConstraints bc = BasicConstraints.getInstance(extnValue);
            if (bc.isCA()) {
              BigInteger bn = bc.getPathLenConstraint();
              basicConstrains = bn == null ? Integer.MAX_VALUE : bn.intValueExact();
            } else {
              basicConstrains = -1;
            }
          }
        } else {
          basicConstrains = jceInstance.getBasicConstraints();
        }
      }
    }

    return basicConstrains;
  }

  public BigInteger getSerialNumber() {
    return serialNumber;
  }

  public String getSerialNumberHex() {
    return "0x" + Hex.encode(serialNumber.toByteArray());
  }

  public PublicKey getPublicKey() {
    if (publicKey == null) {
      synchronized (sync) {
        if (bcInstance != null) {
          try {
            this.publicKey = KeyUtil.generatePublicKey(bcInstance.getSubjectPublicKeyInfo());
          } catch (InvalidKeySpecException ex) {
            throw new IllegalStateException(ex.getMessage(), ex);
          }
        } else {
          publicKey = jceInstance.getPublicKey();
        }
      }
    }

    return publicKey;
  }

  public boolean[] getKeyUsage() {
    if (!keyUsageProcessed) {
      synchronized (sync) {
        if (bcInstance != null) {
          byte[] extnValue = getCoreExtValue(Extension.keyUsage);
          if (extnValue == null) {
            keyUsage = null;
          } else {
            org.bouncycastle.asn1.x509.KeyUsage bc =
                org.bouncycastle.asn1.x509.KeyUsage.getInstance(extnValue);
            keyUsage = new boolean[9];
            for (KeyUsage ku : KeyUsage.values()) {
              keyUsage[ku.getBit()] = bc.hasUsages(ku.getBcUsage());
            }
          }
        } else {
          keyUsage = jceInstance.getKeyUsage();
        }
      }

      keyUsageProcessed = true;
    }

    return keyUsage;
  }

  public X500Name getIssuer() {
    return issuer;
  }

  public X500Name getSubject() {
    return subject;
  }

  public byte[] getSubjectKeyId() {
    if (subjectKeyId == null) {
      synchronized (sync) {
        byte[] extnValue = getCoreExtValue(Extension.subjectKeyIdentifier);
        if (extnValue != null) {
          subjectKeyId = ASN1OctetString.getInstance(extnValue).getOctets();
        }
      }
    }

    return subjectKeyId;
  }

  public byte[] getAuthorityKeyId() {
    if (authorityKeyId == null) {
      synchronized (sync) {
        byte[] extnValue = getCoreExtValue(Extension.authorityKeyIdentifier);
        if (extnValue != null) {
          authorityKeyId = AuthorityKeyIdentifier.getInstance(extnValue).getKeyIdentifier();
        }
      }
    }

    return authorityKeyId;
  }

  public String getSubjectRfc4519Text() {
    if (subjectRfc4519Text == null) {
      synchronized (sync) {
        subjectRfc4519Text = RFC4519Style.INSTANCE.toString(subject);
      }
    }

    return subjectRfc4519Text;
  }

  public String getIssuerRfc4519Text() {
    if (issuerRfc4519Text == null) {
      synchronized (sync) {
        issuerRfc4519Text = RFC4519Style.INSTANCE.toString(subject);
      }
    }

    return issuerRfc4519Text;
  }

  public SubjectPublicKeyInfo getSubjectPublicKeyInfo() {
    if (subjectPublicKeyInfo == null) {
      synchronized (sync) {
        if (bcInstance != null) {
          subjectPublicKeyInfo = bcInstance.getSubjectPublicKeyInfo();
        } else {
          try {
            subjectPublicKeyInfo = KeyUtil.createSubjectPublicKeyInfo(jceInstance.getPublicKey());
          } catch (InvalidKeyException ex) {
            throw new IllegalStateException("error creating SubjectPublicKeyInfo from PublicKey",
                ex);
          }
        }
      }
    }

    return subjectPublicKeyInfo;
  }

  public X509Certificate toJceCert() {
    if (jceInstance == null) {
      synchronized (sync) {
        encoded = getEncoded();
        try {
          jceInstance = X509Util.parseX509Certificate(new ByteArrayInputStream(encoded));
        } catch (CertificateException ex) {
          throw new IllegalStateException("error converting to X509Certificate", ex);
        }
      }
    }

    return jceInstance;
  }

  public X509CertificateHolder toBcCert() {
    if (bcInstance == null) {
      synchronized (sync) {
        try {
          encoded = jceInstance.getEncoded();
          bcInstance = new X509CertificateHolder(encoded);
        } catch (CertificateEncodingException | IOException ex) {
          throw new IllegalStateException("error encoding certificate", ex);
        }
      }
    }

    return bcInstance;
  }

  public boolean isSelfSigned() {
    return selfSigned;
  }

  public Date getNotBefore() {
    return notBefore;
  }

  public Date getNotAfter() {
    return notAfter;
  }

  public byte[] getEncoded() {
    if (encoded == null) {
      synchronized (sync) {
        try {
          encoded = (bcInstance != null) ? bcInstance.getEncoded() : jceInstance.getEncoded();
        } catch (CertificateEncodingException | IOException ex) {
          throw new IllegalStateException("error encoding certificate", ex);
        }
      }
    }

    return encoded;
  }

  public String getCommonName() {
    return X509Util.getCommonName(subject);
  }

  public void verify(PublicKey key)
      throws SignatureException, InvalidKeyException, CertificateException,
      NoSuchAlgorithmException, NoSuchProviderException {
    if (jceInstance != null) {
      jceInstance.verify(key);
    } else {
      String sigName = AlgorithmUtil.getSignatureAlgoName(bcInstance.getSignatureAlgorithm());
      Signature signature = Signature.getInstance(sigName);
      checkBcSignature(key, signature);
    }
  }

  public void verify(PublicKey key, Provider sigProvider)
      throws CertificateException, NoSuchAlgorithmException,
      InvalidKeyException, SignatureException, NoSuchProviderException {
    if (sigProvider == null) {
      verify(key);
    } else {
      if (jceInstance != null) {
        jceInstance.verify(key, sigProvider);
      } else {
        String sigName = AlgorithmUtil.getSignatureAlgoName(bcInstance.getSignatureAlgorithm());
        Signature signature = Signature.getInstance(sigName, sigProvider);
        checkBcSignature(key, signature);
      }
    }
  }

  public void verify(PublicKey key, String sigProvider)
      throws CertificateException, NoSuchAlgorithmException,
      InvalidKeyException, SignatureException, NoSuchProviderException {
    if (sigProvider == null) {
      verify(key);
    } else {
      if (jceInstance != null) {
        jceInstance.verify(key, sigProvider);
      } else {
        String sigName = AlgorithmUtil.getSignatureAlgoName(bcInstance.getSignatureAlgorithm());
        Signature signature = Signature.getInstance(sigName, sigProvider);
        checkBcSignature(key, signature);
      }
    }
  }

  private void checkBcSignature(PublicKey key, Signature signature)
      throws CertificateException, NoSuchAlgorithmException,
          SignatureException, InvalidKeyException {
    Certificate c = bcInstance.toASN1Structure();
    if (!c.getSignatureAlgorithm().equals(c.getTBSCertificate().getSignature())) {
      throw new CertificateException("signature algorithm in TBS cert not same as outer cert");
    }

    signature.initVerify(key);
    try {
      signature.update(c.getTBSCertificate().getEncoded());
    } catch (IOException ex) {
      throw new CertificateException("error encoding TBSCertificate");
    }

    if (!signature.verify(c.getSignature().getBytes())) {
      throw new SignatureException("certificate does not verify with supplied key");
    }
  }

  public byte[] getExtensionCoreValue(ASN1ObjectIdentifier extnType) {
    if (bcInstance != null) {
      Extension extn = bcInstance.getExtensions().getExtension(extnType);
      return extn == null ? null : extn.getExtnValue().getOctets();
    } else {
      byte[] rawValue = jceInstance.getExtensionValue(extnType.getId());
      return rawValue == null ? null : ASN1OctetString.getInstance(rawValue).getOctets();
    }
  }

  public boolean hasKeyusage(KeyUsage usage) {
    boolean[] usages = getKeyUsage();
    return usages == null ? true : usages[usage.getBit()];
  }

  @Override
  public int hashCode() {
    return Arrays.hashCode(getEncoded());
  }

  @Override
  public boolean equals(Object obj) {
    if (obj == this) {
      return true;
    } else if (!(obj instanceof X509Cert)) {
      return false;
    }

    return Arrays.equals(getEncoded(), ((X509Cert) obj).getEncoded());
  }

  private byte[] getCoreExtValue(ASN1ObjectIdentifier extnType) {
    if (bcInstance != null) {
      Extensions extns = bcInstance.getExtensions();
      if (extns == null) {
        return null;
      }
      Extension extn = extns.getExtension(extnType);
      return extn == null ? null : extn.getExtnValue().getOctets();
    } else {
      byte[] rawValue = jceInstance.getExtensionValue(extnType.getId());
      return rawValue == null ? null : ASN1OctetString.getInstance(rawValue).getOctets();
    }
  }

}