package com.jscape.util.crypto;

import javax.crypto.Cipher;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.security.*;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @author Alex Usun
 */
public class CryptoServicesProvider {

    private static final Provider REGULAR_INSTANCE = ProviderHolder.REGULAR.provider;
    private static final Provider FIPS_INSTANCE = ProviderHolder.FIPS.provider;

    public static void init() {
        // empty
    }

    public static String name() {
        return REGULAR_INSTANCE != null ? REGULAR_INSTANCE.getName() : "";
    }

    public static Provider instance(boolean fipsModeRequired) {
        return fipsModeRequired ? FIPS_INSTANCE : REGULAR_INSTANCE;
    }

    public static Provider instance() {
        return currentThreadFipsModeEnabled() ? FIPS_INSTANCE : REGULAR_INSTANCE;
    }

    @SuppressWarnings("unchecked")
    public static void enableFipsModeForCurrentThread() {
        FipsThreadMode.enable();
    }

    @SuppressWarnings("unchecked")
    public static boolean currentThreadFipsModeEnabled() {
        return FipsThreadMode.enabled();
    }

    public static <R extends Runnable> R applyFipsMode(R task) {
        return FipsThreadMode.apply(task);
    }

    public static <R, C extends Callable<R>> R applyFipsMode(C task)
            throws Exception {
        return FipsThreadMode.apply(task);
    }

    public static <R extends Runnable> R applyRegularMode(R task) {
        return RegularThreadMode.apply(task);
    }

    public static <R, C extends Callable<R>> R applyRegularMode(C task)
            throws Exception {
        return RegularThreadMode.apply(task);
    }

    @SuppressWarnings("unchecked")
    public static Provider jsseProvider(boolean fipsModeRequired) {
        try {
            Class providerClass = Class.forName("org.bouncycastle.jsse.provider.BouncyCastleJsseProvider");
            Provider cryptoServicesProvider = fipsModeRequired ? FIPS_INSTANCE : REGULAR_INSTANCE;

            if (cryptoServicesProvider != null) {
                Constructor constructor = providerClass.getConstructor(boolean.class, Provider.class);
                return (Provider) constructor.newInstance(fipsModeRequired, cryptoServicesProvider);
            } else {
                return (Provider) providerClass.newInstance();
            }
        } catch (Exception e) {
            System.setProperty("jdk.tls.useExtendedMasterSecret", "false");
            return null;
        }
    }

    public static Provider jsseProvider() {
        return jsseProvider(currentThreadFipsModeEnabled());
    }
    
    public static KeyStore keyStoreFor(boolean fipsModeRequired)
            throws Exception {
        Provider cryptoServicesProvider = fipsModeRequired ? FIPS_INSTANCE : REGULAR_INSTANCE;
        KeyStore keyStore = keyStoreFor(cryptoServicesProvider, fipsModeRequired);
        keyStore.load(null, null);
        
        return keyStore;
    }

    public static SecureRandom secureRandom(boolean fipsModeRequired)
            throws Exception {
        Provider cryptoServicesProvider = fipsModeRequired ? FIPS_INSTANCE : REGULAR_INSTANCE;
        return fipsModeRequired && cryptoServicesProvider != null ? SecureRandom.getInstance("DEFAULT", cryptoServicesProvider) : new SecureRandom();
    }

    public static SecureRandom secureRandom()
            throws Exception {
        return secureRandom(currentThreadFipsModeEnabled());
    }

    public static boolean unlimitedJurisdictionPolicyFilesInstalled() {
        try {
            return Cipher.getMaxAllowedKeyLength("AES") == Integer.MAX_VALUE;
        } catch (Throwable e) {
            return false;
        }
    }

    private static KeyStore keyStoreFor(Provider cryptoServicesProvider, boolean fipsModeRequired)
            throws Exception {
        if (cryptoServicesProvider == null) {
            return KeyStore.getInstance("JKS");
        }

        return fipsModeRequired ? KeyStore.getInstance("BCFKS", cryptoServicesProvider) : KeyStore.getInstance("PKCS12", cryptoServicesProvider);
    }

    private CryptoServicesProvider() {
        // empty
    }

    private enum ProviderHolder {

        REGULAR("REGULAR"),
        FIPS("FIPS");

        public final Provider provider;

        ProviderHolder(String name) {
            this.provider = "FIPS".equals(name) ? ProviderFactory.availableFipsProvider() : ProviderFactory.availableRegularProvider();

            if ("REGULAR".equals(name) && this.provider != null) {
                Security.addProvider(this.provider);
            }
        }
    }

    private enum ProviderFactory {

        REGULAR("org.bouncycastle.jce.provider.BouncyCastleProvider"),
        FIPS("org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider");

        public final String className;

        public static Provider availableRegularProvider() {
            for (ProviderFactory factory : ProviderFactory.values()) {
                try {
                    return factory.provider();
                } catch (Exception e) {
                    // ignore
                }
            }

            return null;
        }

        public static Provider availableFipsProvider() {
            try {
                return FipsThreadMode.apply((Callable<Provider>) () -> availableRegularProvider());
            } catch (Exception e) {
                return null;
            }
        }

        ProviderFactory(String className) {
            this.className = className;
        }

        public Provider provider()
                throws Exception {
            Class providerClass = Class.forName(this.className);
            return (Provider) providerClass.newInstance();
        }

    }

    private static class FipsThreadMode {

        @SuppressWarnings("unchecked")
        private static void enable() {
            try {
                Class registrarClass = Class.forName("org.bouncycastle.crypto.CryptoServicesRegistrar");
                Method setApprovedOnlyModeMethod = registrarClass.getMethod("setApprovedOnlyMode", boolean.class);
                setApprovedOnlyModeMethod.invoke(registrarClass, true);
            } catch (Exception e) {
                // todo: process error
            }
        }

        @SuppressWarnings("unchecked")
        public static boolean enabled() {
            try {
                Class registrarClass = Class.forName("org.bouncycastle.crypto.CryptoServicesRegistrar");
                Method isInApprovedOnlyMode = registrarClass.getMethod("isInApprovedOnlyMode");
                return (Boolean) isInApprovedOnlyMode.invoke(registrarClass);
            } catch (Exception e) {
                return false;
            }
        }

        public static <R extends Runnable> R apply(final R task) {
            final CountDownLatch countDownLatch = new CountDownLatch(1);
            Thread thread = new Thread("FIPS Algorithm Initializer") {
                @Override
                public void run() {
                    FipsThreadMode.enable();

                    try {
                        task.run();
                    } finally {
                        countDownLatch.countDown();
                    }
                }
            };

            thread.setDaemon(true);
            thread.start();

            try {
                countDownLatch.await();
            } catch (Exception e) {
                // ignore
            }

            return task;
        }

        public static <R, C extends Callable<R>> R apply(final C task)
                throws Exception {
            final CountDownLatch countDownLatch = new CountDownLatch(1);
            final AtomicReference<R> resultReference = new AtomicReference<>();
            final AtomicReference<Exception> errorReference = new AtomicReference<>();
            Thread thread = new Thread("FIPS Algorithm Initializer") {
                @Override
                public void run() {
                    FipsThreadMode.enable();

                    try {
                        R result = task.call();
                        resultReference.set(result);
                    } catch (Exception e) {
                        errorReference.set(e);
                    } finally {
                        countDownLatch.countDown();
                    }
                }
            };

            thread.setDaemon(true);
            thread.start();

            try {
                countDownLatch.await();
            } catch (Exception e) {
                // ignore
            }

            Exception error = errorReference.get();

            if (error != null) {
                throw error;
            }

            return resultReference.get();
        }

        private FipsThreadMode() {
            // empty
        }
    }

    private static class RegularThreadMode {

        public static <R extends Runnable> R apply(final R task) {
            final CountDownLatch countDownLatch = new CountDownLatch(1);
            Thread thread = new Thread("FIPS Algorithm Initializer") {
                @Override
                public void run() {
                    try {
                        task.run();
                    } finally {
                        countDownLatch.countDown();
                    }
                }
            };

            thread.setDaemon(true);
            thread.start();

            try {
                countDownLatch.await();
            } catch (Exception e) {
                // ignore
            }

            return task;
        }

        public static <R, C extends Callable<R>> R apply(final C task)
                throws Exception {
            final CountDownLatch countDownLatch = new CountDownLatch(1);
            final AtomicReference<R> resultReference = new AtomicReference<>();
            final AtomicReference<Exception> errorReference = new AtomicReference<>();
            Thread thread = new Thread("FIPS Algorithm Initializer") {
                @Override
                public void run() {
                    try {
                        R result = task.call();
                        resultReference.set(result);
                    } catch (Exception e) {
                        errorReference.set(e);
                    } finally {
                        countDownLatch.countDown();
                    }
                }
            };

            thread.setDaemon(true);
            thread.start();

            try {
                countDownLatch.await();
            } catch (Exception e) {
                // ignore
            }

            Exception error = errorReference.get();

            if (error != null) {
                throw error;
            }

            return resultReference.get();
        }

        private RegularThreadMode() {
            // empty
        }
    }
}
