/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2009-2010 Sun Microsystems, Inc.
 *      Portions Copyright 2013-2015 ForgeRock AS.
 */

package org.opends.server.util;



import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.List;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryPoolMXBean;
import java.lang.management.MemoryUsage;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.util.Reject;

import static org.opends.messages.UtilityMessages.*;
import static org.opends.server.util.ServerConstants.CERTANDKEYGEN_PROVIDER;



/**
 * Provides a wrapper class that collects all of the JVM vendor and JDK version
 * specific code in a single place.
 */
public final class Platform
{

  /** Prefix that determines which security package to use. */
  private static final String pkgPrefix;

  /** The two security package prefixes (IBM and SUN). */
  private static final String IBM_SEC = "com.ibm.security";
  private static final String SUN_SEC = "sun.security";

  /** The CertAndKeyGen class is located in different packages depending on JVM environment. */
  private static final String[] CERTANDKEYGEN_PATHS = new String[] {
      "sun.security.x509.CertAndKeyGen",          // Oracle/Sun/OpenJDK 6,7
      "sun.security.tools.keytool.CertAndKeyGen", // Oracle/Sun/OpenJDK 8
      "com.ibm.security.x509.CertAndKeyGen",      // IBM SDK 7
      "com.ibm.security.tools.CertAndKeyGen"      // IBM SDK 8
    };

  private static final PlatformIMPL IMPL;

  /** The minimum java supported version. */
  public static final String JAVA_MINIMUM_VERSION_NUMBER = "7.0";

  static
  {
    String vendor = System.getProperty("java.vendor");

    if (vendor.startsWith("IBM"))
    {
      pkgPrefix = IBM_SEC;
    }
    else
    {
      pkgPrefix = SUN_SEC;
    }
    IMPL = new DefaultPlatformIMPL();
  }

  /** Key size, key algorithm and signature algorithms used. */
  public static enum KeyType
  {
    /** RSA key algorithm with 2048 bits size and SHA1withRSA signing algorithm. */
    RSA("rsa", 2048, "SHA1WithRSA"),

    /** Elliptic Curve key algorithm with 233 bits size and SHA1withECDSA signing algorithm. */
    EC("ec", 256, "SHA1withECDSA");

    /** Default key type used when none can be determined. */
    public final static KeyType DEFAULT = RSA;

    final String keyAlgorithm;
    final int keySize;
    final String signatureAlgorithm;

    private KeyType(String keyAlgorithm, int keySize, String signatureAlgorithm)
    {
      this.keySize = keySize;
      this.keyAlgorithm = keyAlgorithm;
      this.signatureAlgorithm = signatureAlgorithm;
    }

    /**
     * Check whether or not, this key type is supported by the current JVM.
     * @return true if this key type is supported, false otherwise.
     */
    public boolean isSupported()
    {
      try
      {
        return KeyPairGenerator.getInstance(keyAlgorithm.toUpperCase()) != null;
      }
      catch (NoSuchAlgorithmException e)
      {
        return false;
      }
    }

    /**
     * Get a KeyType based on the alias name.
     *
     * @param alias
     *          certificate alias
     * @return KeyTpe deduced from the alias.
     */
    public static KeyType getTypeOrDefault(String alias)
    {
      try
      {
        return KeyType.valueOf(alias.substring(alias.lastIndexOf('-') + 1).toUpperCase());
      }
      catch (Exception e)
      {
        return KeyType.DEFAULT;
      }
    }
  }

  /**
   * Platform base class. Performs all of the certificate management functions.
   */
  private static abstract class PlatformIMPL
  {
    /** Time values used in validity calculations. */
    private static final int SEC_IN_DAY = 24 * 60 * 60;

    /** Methods pulled from the classes. */
    private static final String GENERATE_METHOD = "generate";
    private static final String GET_PRIVATE_KEY_METHOD = "getPrivateKey";
    private static final String GET_SELFSIGNED_CERT_METHOD =
      "getSelfCertificate";

    /** Classes needed to manage certificates. */
    private static final Class<?> certKeyGenClass, X500NameClass;

    /** Constructors for each of the above classes. */
    private static Constructor<?> certKeyGenCons, X500NameCons;

    /** Filesystem APIs */

    static
    {

      String certAndKeyGen = getCertAndKeyGenClassName();
      if(certAndKeyGen == null)
      {
        LocalizableMessage msg = ERR_CERTMGR_CERTGEN_NOT_FOUND.get(CERTANDKEYGEN_PROVIDER);
        throw new ExceptionInInitializerError(msg.toString());
      }

      String X500Name = pkgPrefix + ".x509.X500Name";
      try
      {
        certKeyGenClass = Class.forName(certAndKeyGen);
        X500NameClass = Class.forName(X500Name);
        certKeyGenCons = certKeyGenClass.getConstructor(String.class,
            String.class);
        X500NameCons = X500NameClass.getConstructor(String.class);
      }
      catch (ClassNotFoundException e)
      {
        LocalizableMessage msg = ERR_CERTMGR_CLASS_NOT_FOUND.get(e.getMessage());
        throw new ExceptionInInitializerError(msg.toString());
      }
      catch (SecurityException e)
      {
        LocalizableMessage msg = ERR_CERTMGR_SECURITY.get(e.getMessage());
        throw new ExceptionInInitializerError(msg.toString());
      }
      catch (NoSuchMethodException e)
      {
        LocalizableMessage msg = ERR_CERTMGR_NO_METHOD.get(e.getMessage());
        throw new ExceptionInInitializerError(msg.toString());
      }
    }

    /**
     * Try to decide which CertAndKeyGen class to use.
     *
     * @return a fully qualified class name or null
     */
    private static String getCertAndKeyGenClassName() {
      String certAndKeyGen = System.getProperty(CERTANDKEYGEN_PROVIDER);
      if (certAndKeyGen != null)
      {
        return certAndKeyGen;
      }

      for (String className : CERTANDKEYGEN_PATHS)
      {
        if (classExists(className))
        {
          return className;
        }
      }
      return null;
    }

    /**
     * A quick check to see if a class can be loaded. Doesn't check if
     * it can be instantiated.
     *
     * @param className full class name to check
     * @return true if the class is found
     */
    private static boolean classExists(final String className)
    {
      try {
        Class clazz = Class.forName(className);
        return true;
      } catch (ClassNotFoundException | ClassCastException e) {
        return false;
      }
    }

    protected PlatformIMPL()
    {
    }



    private final void deleteAlias(KeyStore ks, String ksPath, String alias,
        char[] pwd) throws KeyStoreException
    {
      try
      {
        if (ks == null)
        {
          LocalizableMessage msg = ERR_CERTMGR_KEYSTORE_NONEXISTANT.get();
          throw new KeyStoreException(msg.toString());
        }
        ks.deleteEntry(alias);
        try (final FileOutputStream fs = new FileOutputStream(ksPath))
        {
          ks.store(fs, pwd);
        }
      }
      catch (Exception e)
      {
        throw new KeyStoreException(ERR_CERTMGR_DELETE_ALIAS.get(alias, e.getMessage()).toString(), e);
      }
    }



    private final void addCertificate(KeyStore ks, String ksType, String ksPath,
        String alias, char[] pwd, String certPath) throws KeyStoreException
    {
      try
      {
        CertificateFactory cf = CertificateFactory.getInstance("X509");
        InputStream inStream = new FileInputStream(certPath);
        if (ks == null)
        {
          ks = KeyStore.getInstance(ksType);
          ks.load(null, pwd);
        }
        // Do not support certificate replies.
        if (ks.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class))
        {
          LocalizableMessage msg = ERR_CERTMGR_CERT_REPLIES_INVALID.get(alias);
          throw new KeyStoreException(msg.toString());
        }
        else if (!ks.containsAlias(alias)
            || ks
                .entryInstanceOf(alias, KeyStore.TrustedCertificateEntry.class))
        {
          trustedCert(alias, cf, ks, inStream);
        }
        else
        {
          LocalizableMessage msg = ERR_CERTMGR_ALIAS_INVALID.get(alias);
          throw new KeyStoreException(msg.toString());
        }
        FileOutputStream fileOutStream = new FileOutputStream(ksPath);
        ks.store(fileOutStream, pwd);
        fileOutStream.close();
        inStream.close();
      }
      catch (Exception e)
      {
        throw new KeyStoreException(ERR_CERTMGR_ADD_CERT.get(alias, e.getMessage()).toString(), e);
      }
    }



    private static final KeyStore generateSelfSignedCertificate(KeyStore ks,
        String ksType, String ksPath, KeyType keyType, String alias, char[] pwd, String dn,
        int validity) throws KeyStoreException
    {
      try
      {
        if (ks == null)
        {
          ks = KeyStore.getInstance(ksType);
          ks.load(null, pwd);
        }
        else if (ks.containsAlias(alias))
        {
          LocalizableMessage msg = ERR_CERTMGR_ALIAS_ALREADY_EXISTS.get(alias);
          throw new KeyStoreException(msg.toString());
        }

        try (final FileOutputStream fileOutStream = new FileOutputStream(ksPath))
        {
            final Object keypair = certKeyGenCons.newInstance(keyType.keyAlgorithm, keyType.signatureAlgorithm);

            final Method certAndKeyGenGenerate = certKeyGenClass.getMethod(GENERATE_METHOD, int.class);
            certAndKeyGenGenerate.invoke(keypair, keyType.keySize);

            final Method certAndKeyGetPrivateKey = certKeyGenClass.getMethod(GET_PRIVATE_KEY_METHOD);
            final Certificate[] certificateChain = new Certificate[1];
            final Method getSelfCertificate =
                certKeyGenClass.getMethod(GET_SELFSIGNED_CERT_METHOD, X500NameClass, long.class);

            final int days = validity * SEC_IN_DAY;
            final Object subject = X500NameCons.newInstance(dn);
            certificateChain[0] = (Certificate) getSelfCertificate.invoke(keypair, subject, days);
            ks.setKeyEntry(alias , (PrivateKey) certAndKeyGetPrivateKey.invoke(keypair), pwd, certificateChain);

            ks.store(fileOutStream, pwd);
        }
      }
      catch (Exception e)
      {
        throw new KeyStoreException(ERR_CERTMGR_GEN_SELF_SIGNED_CERT.get(alias, e.getMessage()).toString(), e);
      }
      return ks;
    }



    /**
     * Generate a x509 certificate from the input stream. Verification is done
     * only if it is self-signed.
     */
    private void trustedCert(String alias, CertificateFactory cf, KeyStore ks,
        InputStream in) throws KeyStoreException
    {
      try
      {
        if (ks.containsAlias(alias))
        {
          LocalizableMessage msg = ERR_CERTMGR_ALIAS_ALREADY_EXISTS.get(alias);
          throw new KeyStoreException(msg.toString());
        }
        X509Certificate cert = (X509Certificate) cf.generateCertificate(in);
        if (isSelfSigned(cert))
        {
          cert.verify(cert.getPublicKey());
        }
        ks.setCertificateEntry(alias, cert);
      }
      catch (Exception e)
      {
        throw new KeyStoreException(ERR_CERTMGR_TRUSTED_CERT.get(alias, e.getMessage()).toString(), e);
      }
    }



    /**
     * Check that the issuer and subject DNs match.
     */
    private boolean isSelfSigned(X509Certificate cert)
    {
      return cert.getSubjectDN().equals(cert.getIssuerDN());
    }



    private long getUsableMemoryForCaching()
    {
      long youngGenSize = 0;
      long oldGenSize = 0;

      List<MemoryPoolMXBean> mpools = ManagementFactory.getMemoryPoolMXBeans();
      for (MemoryPoolMXBean mpool : mpools)
      {
        MemoryUsage usage = mpool.getUsage();
        if (usage != null)
        {
          String name = mpool.getName();
          if (name.equalsIgnoreCase("PS Eden Space"))
          {
            // Parallel.
            youngGenSize = usage.getMax();
          }
          else if (name.equalsIgnoreCase("PS Old Gen"))
          {
            // Parallel.
            oldGenSize = usage.getMax();
          }
          else if (name.equalsIgnoreCase("Par Eden Space"))
          {
            // CMS.
            youngGenSize = usage.getMax();
          }
          else if (name.equalsIgnoreCase("CMS Old Gen"))
          {
            // CMS.
            oldGenSize = usage.getMax();
          }
        }
      }

      if (youngGenSize > 0 && oldGenSize > youngGenSize)
      {
        // We can calculate available memory based on GC info.
        return oldGenSize - youngGenSize;
      }
      else if (oldGenSize > 0)
      {
        // Small old gen. It is going to be difficult to avoid full GCs if the
        // young gen is bigger.
        return oldGenSize * 40 / 100;
      }
      else
      {
        // Unknown GC (G1, JRocket, etc).
        Runtime runTime = Runtime.getRuntime();
        runTime.gc();
        runTime.gc();
        return (runTime.freeMemory() + (runTime.maxMemory() - runTime
            .totalMemory())) * 40 / 100;
      }
    }
  }



  /** Prevent instantiation. */
  private Platform()
  {
  }



  /**
   * Add the certificate in the specified path to the provided keystore;
   * creating the keystore with the provided type and path if it doesn't exist.
   *
   * @param ks
   *          The keystore to add the certificate to, may be null if it doesn't
   *          exist.
   * @param ksType
   *          The type to use if the keystore is created.
   * @param ksPath
   *          The path to the keystore if it is created.
   * @param alias
   *          The alias to store the certificate under.
   * @param pwd
   *          The password to use in saving the certificate.
   * @param certPath
   *          The path to the file containing the certificate.
   * @throws KeyStoreException
   *           If an error occurred adding the certificate to the keystore.
   */
  public static void addCertificate(KeyStore ks, String ksType, String ksPath,
      String alias, char[] pwd, String certPath) throws KeyStoreException
  {
    IMPL.addCertificate(ks, ksType, ksPath, alias, pwd, certPath);
  }



  /**
   * Delete the specified alias from the provided keystore.
   *
   * @param ks
   *          The keystore to delete the alias from.
   * @param ksPath
   *          The path to the keystore.
   * @param alias
   *          The alias to use in the request generation.
   * @param pwd
   *          The keystore password to use.
   * @throws KeyStoreException
   *           If an error occurred deleting the alias.
   */
  public static void deleteAlias(KeyStore ks, String ksPath, String alias,
      char[] pwd) throws KeyStoreException
  {
    IMPL.deleteAlias(ks, ksPath, alias, pwd);
  }



  /**
   * Generate a self-signed certificate using the specified alias, dn string and
   * validity period. If the keystore does not exist, it will be created using
   * the specified keystore type and path.
   *
   * @param ks
   *          The keystore to save the certificate in. May be null if it does
   *          not exist.
   * @param keyType
   *          The keystore type to use if the keystore is created.
   * @param ksPath
   *          The path to the keystore if the keystore is created.
   * @param ksType
   *          Specify the key size, key algorithm and signature algorithms used.
   * @param alias
   *          The alias to store the certificate under.
   * @param pwd
   *          The password to us in saving the certificate.
   * @param dn
   *          The dn string used as the certificate subject.
   * @param validity
   *          The validity of the certificate in days.
   * @throws KeyStoreException
   *           If the self-signed certificate cannot be generated.
   */
  public static void generateSelfSignedCertificate(KeyStore ks, String ksType,
      String ksPath, KeyType keyType, String alias, char[] pwd, String dn, int validity)
      throws KeyStoreException
  {
    PlatformIMPL.generateSelfSignedCertificate(ks, ksType, ksPath, keyType, alias, pwd, dn, validity);
  }

  /**
   * Default platform class.
   */
  private static class DefaultPlatformIMPL extends PlatformIMPL
  {
  }



  /**
   * Test if a platform java vendor property starts with the specified vendor
   * string.
   *
   * @param vendor
   *          The vendor to check for.
   * @return {@code true} if the java vendor starts with the specified vendor
   *         string.
   */
  public static boolean isVendor(String vendor)
  {
    String javaVendor = System.getProperty("java.vendor");
    return javaVendor.startsWith(vendor);
  }



  /**
   * Calculates the usable memory which could potentially be used by the
   * application for caching objects. This method <b>does not</b> look at the
   * amount of free memory, but instead tries to query the JVM's GC settings in
   * order to determine the amount of usable memory in the old generation (or
   * equivalent). More specifically, applications may also need to take into
   * account the amount of memory already in use, for example by performing the
   * following:
   *
   * <pre>
   * Runtime runTime = Runtime.getRuntime();
   * runTime.gc();
   * runTime.gc();
   * long freeCommittedMemory = runTime.freeMemory();
   * long uncommittedMemory = runTime.maxMemory() - runTime.totalMemory();
   * long freeMemory = freeCommittedMemory + uncommittedMemory;
   * </pre>
   *
   * @return The usable memory which could potentially be used by the
   *         application for caching objects.
   */
  public static long getUsableMemoryForCaching()
  {
    return IMPL.getUsableMemoryForCaching();
  }

  /**
   * Computes the number of replay/worker/cleaner threads based on the number of cpus in the system.
   * Allows for a multiplier to be specified and a minimum value to be returned if not enough processors
   * are present in the system.
   *
   * @param minimumValue at least this value should be returned.
   * @param cpuMultiplier the scaling multiplier of the number of threads to return
   * @return the number of threads based on the number of cpus in the system.
   * @throws IllegalArgumentException if {@code cpuMultiplier} is a non positive number
   */
  public static int computeNumberOfThreads(int minimumValue, float cpuMultiplier)
  {
    Reject.ifTrue(cpuMultiplier < 0, "Multiplier must be a positive number");
    return Math.max(minimumValue, (int)(Runtime.getRuntime().availableProcessors() * cpuMultiplier));
  }
}
