/*
 *
 * Apache Geronimo JCache Spec 1.0
 *
 * Copyright (C) 2003 - 2014 The Apache Software Foundation
 *
 * 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 javax.cache;

import javax.cache.spi.CachingProvider;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ServiceLoader;
import java.util.WeakHashMap;

public final class Caching {
    public static final String JAVAX_CACHE_CACHING_PROVIDER = "javax.cache.spi.CachingProvider";

    private static final CachingProviderRegistry CACHING_PROVIDERS = new CachingProviderRegistry();

    private Caching() {
        // no-op
    }

    public static ClassLoader getDefaultClassLoader() {
        return CACHING_PROVIDERS.getDefaultClassLoader();
    }

    public static void setDefaultClassLoader(ClassLoader classLoader) {
        CACHING_PROVIDERS.setDefaultClassLoader(classLoader);
    }

    public static CachingProvider getCachingProvider() {
        return CACHING_PROVIDERS.getCachingProvider();
    }

    public static CachingProvider getCachingProvider(ClassLoader classLoader) {
        return CACHING_PROVIDERS.getCachingProvider(classLoader);
    }

    public static Iterable<CachingProvider> getCachingProviders() {
        return CACHING_PROVIDERS.getCachingProviders();
    }

    public static Iterable<CachingProvider> getCachingProviders(ClassLoader classLoader) {
        return CACHING_PROVIDERS.getCachingProviders(classLoader);
    }

    public static CachingProvider getCachingProvider(String fullyQualifiedClassName) {
        return CACHING_PROVIDERS.getCachingProvider(fullyQualifiedClassName);
    }

    public static CachingProvider getCachingProvider(String fullyQualifiedClassName, ClassLoader classLoader) {
        return CACHING_PROVIDERS.getCachingProvider(fullyQualifiedClassName, classLoader);
    }

    public static <K, V> Cache<K, V> getCache(String cacheName, Class<K> keyType, Class<V> valueType) {
        return getCachingProvider().getCacheManager().getCache(cacheName, keyType, valueType);
    }


    // logic taken from RI API
    private static class CachingProviderRegistry {
        private WeakHashMap<ClassLoader, LinkedHashMap<String, CachingProvider>> cachingProviders;

        private volatile ClassLoader classLoader;

        public CachingProviderRegistry() {
            this.cachingProviders = new WeakHashMap<ClassLoader, LinkedHashMap<String, CachingProvider>>();
            this.classLoader = null;
        }

        public ClassLoader getDefaultClassLoader() {
            ClassLoader loader = classLoader;
            return loader == null ? Thread.currentThread().getContextClassLoader() : loader;
        }

        public void setDefaultClassLoader(ClassLoader classLoader) {
            this.classLoader = classLoader;
        }

        public CachingProvider getCachingProvider() {
            return getCachingProvider(getDefaultClassLoader());
        }

        public CachingProvider getCachingProvider(ClassLoader classLoader) {
            Iterator<CachingProvider> iterator = getCachingProviders(classLoader).iterator();
            if (iterator.hasNext()) {
                CachingProvider provider = iterator.next();
                if (iterator.hasNext()) {
                    throw new CacheException("Multiple CachingProviders have been configured when only a single CachingProvider is expected");
                } else {
                    return provider;
                }
            }
            throw new CacheException("No CachingProviders have been configured");
        }

        public Iterable<CachingProvider> getCachingProviders() {
            return getCachingProviders(getDefaultClassLoader());
        }

        public synchronized Iterable<CachingProvider> getCachingProviders(ClassLoader classLoader) {
            final ClassLoader serviceClassLoader = classLoader == null ? getDefaultClassLoader() : classLoader;
            LinkedHashMap<String, CachingProvider> providers = cachingProviders.get(serviceClassLoader);
            if (providers == null) {
                if (System.getProperties().containsKey(JAVAX_CACHE_CACHING_PROVIDER)) {
                    final String className = System.getProperty(JAVAX_CACHE_CACHING_PROVIDER);
                    providers = new LinkedHashMap<String, CachingProvider>();
                    providers.put(className, loadCachingProvider(className, serviceClassLoader));

                } else {
                    providers = AccessController.doPrivileged(new PrivilegedAction<LinkedHashMap<String, CachingProvider>>() {
                        public LinkedHashMap<String, CachingProvider> run() {
                            final LinkedHashMap<String, CachingProvider> result = new LinkedHashMap<String, CachingProvider>();
                            try {
                                final Class<?> clazz = Class.forName("org.apache.geronimo.osgi.locator.ProviderLocator");
                                final Method getServices = clazz.getDeclaredMethod("getServices", String.class, Class.class, ClassLoader.class);
                                for (final CachingProvider provider : (List<CachingProvider>) getServices.invoke(null, CachingProvider.class.getName(), CachingProvider.class, serviceClassLoader)) {
                                    result.put(provider.getClass().getName(), provider);
                                }
                            } catch (final Throwable e) {
                                // locator not available, try normal mode
                            }
                            final ServiceLoader<CachingProvider> serviceLoader = ServiceLoader.load(CachingProvider.class, serviceClassLoader);
                            for (final CachingProvider provider : serviceLoader) {
                                result.put(provider.getClass().getName(), provider);
                            }
                            return result;
                        }
                    });

                }
                cachingProviders.put(serviceClassLoader, providers);
            }
            return providers.values();
        }

        public CachingProvider getCachingProvider(String fullyQualifiedClassName) {
            return getCachingProvider(fullyQualifiedClassName, getDefaultClassLoader());
        }

        protected CachingProvider loadCachingProvider(String fullyQualifiedClassName, ClassLoader classLoader) throws CacheException {
            synchronized (classLoader) {
                try {
                    Class<?> clazz = classLoader.loadClass(fullyQualifiedClassName);
                    if (CachingProvider.class.isAssignableFrom(clazz)) {
                        return ((Class<CachingProvider>) clazz).newInstance();
                    } else {
                        throw new CacheException("The specified class [" + fullyQualifiedClassName + "] is not a CachingProvider");
                    }
                } catch (Exception e) {
                    throw new CacheException("Failed to load the CachingProvider [" + fullyQualifiedClassName + "]", e);
                }
            }
        }

        public synchronized CachingProvider getCachingProvider(String fullyQualifiedClassName, ClassLoader classLoader) {
            ClassLoader serviceClassLoader = classLoader == null ? getDefaultClassLoader() : classLoader;
            LinkedHashMap<String, CachingProvider> providers = cachingProviders.get(serviceClassLoader);
            if (providers == null) {
                getCachingProviders(serviceClassLoader);
                providers = cachingProviders.get(serviceClassLoader);
            }

            CachingProvider provider = providers.get(fullyQualifiedClassName);
            if (provider == null) {
                provider = loadCachingProvider(fullyQualifiedClassName, serviceClassLoader);
                providers.put(fullyQualifiedClassName, provider);
            }

            return provider;
        }
    }
}
