/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.commons.jcs.jcache;

import org.apache.commons.jcs.engine.behavior.ICompositeCacheAttributes;
import org.apache.commons.jcs.engine.behavior.IElementAttributes;
import org.apache.commons.jcs.engine.control.CompositeCache;
import org.apache.commons.jcs.engine.control.CompositeCacheConfigurator;
import org.apache.commons.jcs.engine.control.CompositeCacheManager;
import org.apache.commons.jcs.jcache.lang.Subsitutor;
import org.apache.commons.jcs.jcache.proxy.ClassLoaderAwareCache;

import javax.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.configuration.Configuration;
import javax.cache.spi.CachingProvider;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.Enumeration;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import static org.apache.commons.jcs.jcache.Asserts.assertNotNull;

public class JCSCachingManager implements CacheManager
{
    private static final Subsitutor SUBSTITUTOR = Subsitutor.Helper.INSTANCE;
    private static final String DEFAULT_CONFIG =
        "jcs.default=DC\n" +
        "jcs.default.cacheattributes=org.apache.commons.jcs.engine.CompositeCacheAttributes\n" +
        "jcs.default.cacheattributes.MaxObjects=200001\n" +
        "jcs.default.cacheattributes.MemoryCacheName=org.apache.commons.jcs.engine.memory.lru.LRUMemoryCache\n" +
        "jcs.default.cacheattributes.UseMemoryShrinker=true\n" +
        "jcs.default.cacheattributes.MaxMemoryIdleTimeSeconds=3600\n" +
        "jcs.default.cacheattributes.ShrinkerIntervalSeconds=60\n" +
        "jcs.default.elementattributes=org.apache.commons.jcs.engine.ElementAttributes\n" +
        "jcs.default.elementattributes.IsEternal=false\n" +
        "jcs.default.elementattributes.MaxLife=700\n" +
        "jcs.default.elementattributes.IdleTime=1800\n" +
        "jcs.default.elementattributes.IsSpool=true\n" +
        "jcs.default.elementattributes.IsRemote=true\n" +
        "jcs.default.elementattributes.IsLateral=true\n";

    private static class InternalManager extends CompositeCacheManager
    {
        protected static InternalManager create()
        {
            return new InternalManager();
        }

        protected CompositeCacheConfigurator newConfigurator()
        {
            return new CompositeCacheConfigurator()
            {
                @Override
                protected <K, V> CompositeCache<K, V> newCache(
                        final ICompositeCacheAttributes cca, final IElementAttributes ea)
                {
                    return new ExpiryAwareCache<K, V>( cca, ea );
                }
            };
        }

        @Override // needed to call it from JCSCachingManager
        protected void initialize() {
            super.initialize();
        }
    }

    private final CachingProvider provider;
    private final URI uri;
    private final ClassLoader loader;
    private final Properties properties;
    private final ConcurrentMap<String, Cache<?, ?>> caches = new ConcurrentHashMap<String, Cache<?, ?>>();
    private final Properties configProperties;
    private volatile boolean closed = false;
    private InternalManager delegate = InternalManager.create();

    public JCSCachingManager(final CachingProvider provider, final URI uri, final ClassLoader loader, final Properties properties)
    {
        this.provider = provider;
        this.uri = uri;
        this.loader = loader;
        this.properties = readConfig(uri, loader, properties);
        this.configProperties = properties;

        delegate.setJmxName(CompositeCacheManager.JMX_OBJECT_NAME
                + ",provider=" + provider.hashCode()
                + ",uri=" + uri.toString().replaceAll(",|:|=|\n", ".")
                + ",classloader=" + loader.hashCode()
                + ",properties=" + this.properties.hashCode());
        delegate.initialize();
        delegate.configure(this.properties);
    }

    private Properties readConfig(final URI uri, final ClassLoader loader, final Properties properties) {
        final Properties props = new Properties();
        try {
            if (JCSCachingProvider.DEFAULT_URI.toString().equals(uri.toString()) || uri.toURL().getProtocol().equals("jcs"))
            {

                final Enumeration<URL> resources = loader.getResources(uri.getPath());
                if (!resources.hasMoreElements()) // default
                {
                    props.load(new ByteArrayInputStream(DEFAULT_CONFIG.getBytes("UTF-8")));
                }
                else
                {
                    do
                    {
                        addProperties(resources.nextElement(), props);
                    }
                    while (resources.hasMoreElements());
                }
            }
            else
            {
                props.load(uri.toURL().openStream());
            }
        } catch (final IOException e) {
            throw new IllegalStateException(e);
        }

        if (properties != null)
        {
            props.putAll(properties);
        }

        for (final Map.Entry<Object, Object> entry : props.entrySet()) {
            if (entry.getValue() == null)
            {
                continue;
            }
            final String substitute = SUBSTITUTOR.substitute(entry.getValue().toString());
            if (!substitute.equals(entry.getValue()))
            {
                entry.setValue(substitute);
            }
        }
        return props;
    }

    private void addProperties(final URL url, final Properties aggregator)
    {
        InputStream inStream = null;
        try
        {
            inStream = url.openStream();
            aggregator.load(inStream);
        }
        catch (final IOException e)
        {
            throw new IllegalArgumentException(e);
        }
        finally
        {
            if (inStream != null)
            {
                try
                {
                    inStream.close();
                }
                catch (final IOException e)
                {
                    // no-op
                }
            }
        }
    }

    private void assertNotClosed()
    {
        if (isClosed())
        {
            throw new IllegalStateException("cache manager closed");
        }
    }

    @Override
    // TODO: use configuration + handle not serializable key/values
    public <K, V, C extends Configuration<K, V>> Cache<K, V> createCache(final String cacheName, final C configuration)
            throws IllegalArgumentException
    {
        assertNotClosed();
        assertNotNull(cacheName, "cacheName");
        assertNotNull(configuration, "configuration");
        final Class<?> keyType = configuration == null ? Object.class : configuration.getKeyType();
        final Class<?> valueType = configuration == null ? Object.class : configuration.getValueType();
        if (!caches.containsKey(cacheName))
        {
            final Cache<K, V> cache = ClassLoaderAwareCache.wrap(loader,
                    new JCSCache/*<K, V>*/(
                            loader, this, cacheName,
                            new JCSConfiguration/*<K, V>*/(configuration, keyType, valueType),
                            properties,
                            ExpiryAwareCache.class.cast(delegate.getCache(cacheName))));
            caches.putIfAbsent(cacheName, cache);
        }
        else
        {
            throw new javax.cache.CacheException("cache " + cacheName + " already exists");
        }
        return (Cache<K, V>) getCache(cacheName, keyType, valueType);
    }

    @Override
    public void destroyCache(final String cacheName)
    {
        assertNotClosed();
        assertNotNull(cacheName, "cacheName");
        final Cache<?, ?> cache = caches.remove(cacheName);
        if (cache != null && !cache.isClosed())
        {
            cache.clear();
            cache.close();
        }
    }

    @Override
    public void enableManagement(final String cacheName, final boolean enabled)
    {
        assertNotClosed();
        assertNotNull(cacheName, "cacheName");
        final JCSCache<?, ?> cache = getJCSCache(cacheName);
        if (cache != null)
        {
            if (enabled)
            {
                cache.enableManagement();
            }
            else
            {
                cache.disableManagement();
            }
        }
    }

    private JCSCache<?, ?> getJCSCache(final String cacheName)
    {
        final Cache<?, ?> cache = caches.get(cacheName);
        return JCSCache.class.cast(ClassLoaderAwareCache.getDelegate(cache));
    }

    @Override
    public void enableStatistics(final String cacheName, final boolean enabled)
    {
        assertNotClosed();
        assertNotNull(cacheName, "cacheName");
        final JCSCache<?, ?> cache = getJCSCache(cacheName);
        if (cache != null)
        {
            if (enabled)
            {
                cache.enableStatistics();
            }
            else
            {
                cache.disableStatistics();
            }
        }
    }

    @Override
    public synchronized void close()
    {
        if (isClosed())
        {
            return;
        }

        assertNotClosed();
        for (final Cache<?, ?> c : caches.values())
        {
            c.close();
        }
        caches.clear();
        closed = true;
        if (JCSCachingProvider.class.isInstance(provider))
        {
            JCSCachingProvider.class.cast(provider).remove(this);
        }
        delegate.shutDown();
    }

    @Override
    public <T> T unwrap(final Class<T> clazz)
    {
        if (clazz.isInstance(this))
        {
            return clazz.cast(this);
        }
        throw new IllegalArgumentException(clazz.getName() + " not supported in unwrap");
    }

    @Override
    public boolean isClosed()
    {
        return closed;
    }

    @Override
    public <K, V> Cache<K, V> getCache(final String cacheName)
    {
        assertNotClosed();
        assertNotNull(cacheName, "cacheName");
        return (Cache<K, V>) doGetCache(cacheName, Object.class, Object.class);
    }

    @Override
    public Iterable<String> getCacheNames()
    {
        return new ImmutableIterable<String>(caches.keySet());
    }

    @Override
    public <K, V> Cache<K, V> getCache(final String cacheName, final Class<K> keyType, final Class<V> valueType)
    {
        assertNotClosed();
        assertNotNull(cacheName, "cacheName");
        assertNotNull(keyType, "keyType");
        assertNotNull(valueType, "valueType");
        try
        {
            return doGetCache(cacheName, keyType, valueType);
        }
        catch (final IllegalArgumentException iae)
        {
            throw new ClassCastException(iae.getMessage());
        }
    }

    private <K, V> Cache<K, V> doGetCache(final String cacheName, final Class<K> keyType, final Class<V> valueType)
    {
        final Cache<K, V> cache = (Cache<K, V>) caches.get(cacheName);
        if (cache == null)
        {
            return null;
        }

        final Configuration<K, V> config = cache.getConfiguration(Configuration.class);
        if ((keyType != null && !config.getKeyType().isAssignableFrom(keyType))
                || (valueType != null && !config.getValueType().isAssignableFrom(valueType)))
        {
            throw new IllegalArgumentException("this cache is <" + config.getKeyType().getName() + ", " + config.getValueType().getName()
                    + "> " + " and not <" + keyType.getName() + ", " + valueType.getName() + ">");
        }
        return cache;
    }

    @Override
    public CachingProvider getCachingProvider()
    {
        return provider;
    }

    @Override
    public URI getURI()
    {
        return uri;
    }

    @Override
    public ClassLoader getClassLoader()
    {
        return loader;
    }

    @Override
    public Properties getProperties()
    {
        return configProperties;
    }

    public void release(final String name) {
        caches.remove(name);
    }
}
