package kr.pe.kwonnam.hibernate4memcached.regions;

import kr.pe.kwonnam.hibernate4memcached.memcached.CacheNamespace;
import kr.pe.kwonnam.hibernate4memcached.memcached.MemcachedAdapter;
import kr.pe.kwonnam.hibernate4memcached.timestamper.HibernateCacheTimestamper;
import kr.pe.kwonnam.hibernate4memcached.util.OverridableReadOnlyProperties;
import org.hibernate.cache.CacheException;
import org.hibernate.cache.spi.CacheDataDescription;
import org.hibernate.cache.spi.GeneralDataRegion;
import org.hibernate.cfg.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static kr.pe.kwonnam.hibernate4memcached.Hibernate4MemcachedRegionFactory.REGION_EXPIRY_SECONDS_PROPERTY_KEY_PREFIX;

/**
 * @author KwonNam Son ([email protected])
 */
public class GeneralDataMemcachedRegion extends MemcachedRegion implements GeneralDataRegion {
    private Logger log = LoggerFactory.getLogger(GeneralDataMemcachedRegion.class);

    private int expirySeconds;

    public GeneralDataMemcachedRegion(CacheNamespace cacheNamespace, OverridableReadOnlyProperties properties, CacheDataDescription metadata,
                                      Settings settings, MemcachedAdapter memcachedAdapter, HibernateCacheTimestamper hibernateCacheTimestamper) {
        super(cacheNamespace, properties, metadata, settings, memcachedAdapter, hibernateCacheTimestamper);
        populateExpirySeconds(properties);
    }

    void populateExpirySeconds(OverridableReadOnlyProperties properties) {
        String regionExpirySecondsKey = REGION_EXPIRY_SECONDS_PROPERTY_KEY_PREFIX + "." + getCacheNamespace().getName();
        String expirySecondsProperty = properties.getProperty(regionExpirySecondsKey);
        if (expirySecondsProperty == null) {
            expirySecondsProperty = properties.getProperty(REGION_EXPIRY_SECONDS_PROPERTY_KEY_PREFIX);
        }
        if (expirySecondsProperty == null) {
            throw new IllegalStateException(regionExpirySecondsKey + " or " + REGION_EXPIRY_SECONDS_PROPERTY_KEY_PREFIX
                    + "(for default expiry seconds) required!");
        }

        expirySeconds = Integer.parseInt(expirySecondsProperty);
        log.info("expirySeconds of cache region [{}] - {} seconds.", getCacheNamespace().getName(), expirySeconds);
    }

    @Override
    public Object get(Object key) throws CacheException {
        String refinedKey = refineKey(key);

        log.debug("Cache get [{}] : key[{}]", getCacheNamespace(), refinedKey);

        Object cachedData;
        try {
            cachedData = getMemcachedAdapter().get(getCacheNamespace(), refinedKey);
        } catch (Exception ex) {
            log.warn("Failed to get from memcached.", ex);
            cachedData = null;
        }

        if (cachedData == null) {
            return null;
        }

        if (!(cachedData instanceof CacheItem)) {
            log.debug("get cachedData is not CacheItem.");
            return cachedData;
        }

        CacheItem cacheItem = (CacheItem) cachedData;
        boolean targetClassAndCurrentJvmTargetClassMatch = cacheItem.isTargetClassAndCurrentJvmTargetClassMatch();
        log.debug("cacheItem and targetClassAndCurrentJvmTargetClassMatch : {} / {}", targetClassAndCurrentJvmTargetClassMatch, cacheItem);

        if (cacheItem.isTargetClassAndCurrentJvmTargetClassMatch()) {
            return cacheItem.getCacheEntry();
        }

        return null;
    }

    @Override
    public void put(Object key, Object value) throws CacheException {

        Object valueToCache = value;

        boolean classVersionApplicable = CacheItem.checkIfClassVersionApplicable(value, getSettings().isStructuredCacheEntriesEnabled());

        if (classVersionApplicable) {
            valueToCache = new CacheItem(value, getSettings().isStructuredCacheEntriesEnabled());
        }

        String refinedKey = refineKey(key);
        log.debug("Cache put [{}] : key[{}], value[{}], classVersionApplicable : {}", getCacheNamespace(), refinedKey,
                valueToCache, classVersionApplicable);
        try {
            getMemcachedAdapter().set(getCacheNamespace(), refinedKey, valueToCache, getExpiryInSeconds());
        } catch (Exception ex) {
            log.warn("Failed to set memcached value.", ex);
        }
    }

    @Override
    public void evict(Object key) throws CacheException {
        String refinedKey = refineKey(key);
        log.debug("Cache evict[{}] : key[{}]", getCacheNamespace(), refinedKey);
        try {
            getMemcachedAdapter().delete(getCacheNamespace(), refinedKey);
        } catch (Exception ex) {
            log.warn("Failed to delete memcached value.", ex);
        }
    }

    @Override
    public void evictAll() throws CacheException {
        log.debug("Cache evictAll [{}].", getCacheNamespace());
        try {
            getMemcachedAdapter().evictAll(getCacheNamespace());
        } catch (Exception ex) {
            log.warn("Failed to evictAll.", ex);
        }
    }

    /**
     * Read expiry seconds from configuration properties
     */
    protected int getExpiryInSeconds() {
        return expirySeconds;
    }

    /**
     * Memcached has limitation of key size. Shorten the key to avoid the limitation if needed.
     */
    protected String refineKey(Object key) {
        return String.valueOf(key);
    }
}