/* Copyright 2015, the original author or authors.
 *
 * 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 com.mc.hibernate.memcached;

import com.mc.hibernate.memcached.cache.CacheManager;
import com.mc.hibernate.memcached.cache.ConfigSettings;
import com.mc.hibernate.memcached.cache.MissingCacheStrategy;
import com.mc.hibernate.memcached.cache.StorageAccessImpl;
import org.hibernate.boot.registry.classloading.spi.ClassLoaderService;
import org.hibernate.boot.spi.SessionFactoryOptions;
import org.hibernate.cache.CacheException;
import org.hibernate.cache.cfg.spi.DomainDataRegionBuildingContext;
import org.hibernate.cache.cfg.spi.DomainDataRegionConfig;
import org.hibernate.cache.internal.DefaultCacheKeysFactory;
import org.hibernate.cache.spi.CacheKeysFactory;
import org.hibernate.cache.spi.DomainDataRegion;
import org.hibernate.cache.spi.SecondLevelCacheLogger;
import org.hibernate.cache.spi.support.*;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.Map;

public class MemcachedRegionFactory extends RegionFactoryTemplate {

    private static final Logger log = LoggerFactory.getLogger(MemcachedRegionFactory.class);

    private final CacheKeysFactory cacheKeysFactory;

    private volatile CacheManager cacheManager;
    private volatile MissingCacheStrategy missingCacheStrategy;

    public MemcachedRegionFactory() {
        this(DefaultCacheKeysFactory.INSTANCE);
    }

    public MemcachedRegionFactory(CacheKeysFactory cacheKeysFactory) {
        this.cacheKeysFactory = cacheKeysFactory;
    }

    @Override
    protected CacheKeysFactory getImplicitCacheKeysFactory() {
        return cacheKeysFactory;
    }

    @Override
    public DomainDataRegion buildDomainDataRegion(DomainDataRegionConfig regionConfig, DomainDataRegionBuildingContext buildingContext) {
        return new DomainDataRegionImpl(regionConfig, this, createDomainDataStorageAccess(regionConfig, buildingContext), cacheKeysFactory, buildingContext);
    }

    @Override
    protected DomainDataStorageAccess createDomainDataStorageAccess(DomainDataRegionConfig regionConfig, DomainDataRegionBuildingContext buildingContext) {
        return new StorageAccessImpl(getOrCreateCache(regionConfig.getRegionName(), buildingContext.getSessionFactory()));
    }

    @Override
    protected StorageAccess createQueryResultsRegionStorageAccess(String regionName, SessionFactoryImplementor sessionFactory) {
        String defaultedRegionName = defaultRegionName(regionName, sessionFactory, DEFAULT_QUERY_RESULTS_REGION_UNQUALIFIED_NAME, LEGACY_QUERY_RESULTS_REGION_UNQUALIFIED_NAMES);
        return new StorageAccessImpl(getOrCreateCache(defaultedRegionName, sessionFactory));
    }

    @Override
    protected StorageAccess createTimestampsRegionStorageAccess(String regionName, SessionFactoryImplementor sessionFactory) {
        String defaultedRegionName = defaultRegionName(regionName, sessionFactory, DEFAULT_UPDATE_TIMESTAMPS_REGION_UNQUALIFIED_NAME, LEGACY_UPDATE_TIMESTAMPS_REGION_UNQUALIFIED_NAMES);
        return new StorageAccessImpl(getOrCreateCache(defaultedRegionName, sessionFactory));
    }

    protected final String defaultRegionName(String regionName, SessionFactoryImplementor sessionFactory,
                                             String defaultRegionName, List<String> legacyDefaultRegionNames) {
        if (defaultRegionName.equals(regionName) && !cacheExists(regionName, sessionFactory)) {
            // Maybe the user configured caches explicitly with legacy names; try them and use the first that exists
            for (String legacyDefaultRegionName : legacyDefaultRegionNames) {
                if (cacheExists(legacyDefaultRegionName, sessionFactory)) {
                    SecondLevelCacheLogger.INSTANCE.usingLegacyCacheName(defaultRegionName, legacyDefaultRegionName);
                    return legacyDefaultRegionName;
                }
            }
        }

        return regionName;
    }

    protected MemcachedCache getOrCreateCache(String unqualifiedRegionName, SessionFactoryImplementor sessionFactory) {

        verifyStarted();
        assert !RegionNameQualifier.INSTANCE.isQualified(unqualifiedRegionName, sessionFactory.getSessionFactoryOptions());

        final String qualifiedRegionName = RegionNameQualifier.INSTANCE.qualify(
                unqualifiedRegionName,
                sessionFactory.getSessionFactoryOptions()
        );

        final MemcachedCache cache = cacheManager.getCache(qualifiedRegionName);
        if (cache == null) {
            return createCache(qualifiedRegionName);
        }
        return cache;
    }

    protected MemcachedCache createCache(String regionName) {
        switch (missingCacheStrategy) {
            case CREATE_WARN:
                log.warn("Creating new cache region " + regionName);
                cacheManager.addCache(regionName);
                return cacheManager.getCache(regionName);
            case CREATE:
                cacheManager.addCache(regionName);
                return cacheManager.getCache(regionName);
            case FAIL:
                throw new CacheException("On-the-fly creation of Cache objects is not supported [" + regionName + "]");
            default:
                throw new IllegalStateException("Unsupported missing cache strategy: " + missingCacheStrategy);
        }
    }

    protected boolean cacheExists(String unqualifiedRegionName, SessionFactoryImplementor sessionFactory) {
        final String qualifiedRegionName = RegionNameQualifier.INSTANCE.qualify(
                unqualifiedRegionName,
                sessionFactory.getSessionFactoryOptions()
        );
        return cacheManager.getCache(qualifiedRegionName) != null;
    }


    protected boolean isStarted() {
        return super.isStarted() && cacheManager != null;
    }

    @Override
    protected void prepareForUse(SessionFactoryOptions settings, Map configValues) {
        synchronized (this) {
            this.cacheManager = resolveCacheManager(settings, configValues);
            if (this.cacheManager == null) {
                throw new CacheException("Could not start CacheManager");
            }
            this.missingCacheStrategy = MissingCacheStrategy.interpretSetting(configValues.get(ConfigSettings.MISSING_CACHE_STRATEGY));
        }
    }

    protected CacheManager resolveCacheManager(SessionFactoryOptions settings, Map properties) {
        final Object explicitCacheManager = properties.get(ConfigSettings.CACHE_MANAGER);
        if (explicitCacheManager != null) {
            return useExplicitCacheManager(settings, explicitCacheManager);
        }

        return useNormalCacheManager(settings, properties);
    }

    /**
     * Locate the CacheManager during start-up.  protected to allow for subclassing
     */
    protected static CacheManager useNormalCacheManager(SessionFactoryOptions settings, Map properties) {
        return new CacheManager(settings, properties);
    }

    private CacheManager useExplicitCacheManager(SessionFactoryOptions settings, Object setting) {
        if (setting instanceof CacheManager) {
            return (CacheManager) setting;
        }

        final Class<? extends CacheManager> cacheManagerClass;
        if (setting instanceof Class) {
            cacheManagerClass = (Class<? extends CacheManager>) setting;
        } else {
            cacheManagerClass = settings.getServiceRegistry().getService(ClassLoaderService.class).classForName(setting.toString());
        }

        try {
            return cacheManagerClass.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            throw new CacheException("Could not use explicit CacheManager : " + setting);
        }
    }

    @Override
    protected void releaseFromUse() {
        try {
            cacheManager.releaseFromUse();
        } finally {
            cacheManager = null;
        }
    }
}