/**
 * Copyright (c) 2015-2017, Winter Lau ([email protected]).
 * <p>
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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 net.oschina.j2cache.caffeine;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import net.oschina.j2cache.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.ArrayList;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * Caffeine cache provider
 *
 * @author Winter Lau([email protected])
 */
public class CaffeineProvider implements CacheProvider {

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

    private final static String PREFIX_REGION = "region.";
    private final static String DEFAULT_REGION = "default";
    private ConcurrentHashMap<String, CaffeineCache> caches = new ConcurrentHashMap<>();
    private ConcurrentHashMap<String, CacheConfig> cacheConfigs = new ConcurrentHashMap<>();

    @Override
    public String name() {
        return "caffeine";
    }

    @Override
    public int level() {
        return CacheObject.LEVEL_1;
    }

    @Override
    public Collection<CacheChannel.Region> regions() {
        Collection<CacheChannel.Region> regions = new ArrayList<>();
        caches.forEach((k,c) -> regions.add(new CacheChannel.Region(k, c.size(), c.ttl())));
        return regions;
    }

    @Override
    public Cache buildCache(String region, CacheExpiredListener listener) {
        return caches.computeIfAbsent(region, v -> {
            CacheConfig config = cacheConfigs.get(region);
            if (config == null) {
                config = cacheConfigs.get(DEFAULT_REGION);
                if (config == null)
                    throw new CacheException(String.format("Undefined [default] caffeine cache"));

                log.warn("Caffeine cache [{}] not defined, using default.", region);
            }
            return newCaffeineCache(region, config.size, config.expire, listener);
        });
    }

    @Override
    public Cache buildCache(String region, long timeToLiveInSeconds, CacheExpiredListener listener) {
        CaffeineCache cache = caches.computeIfAbsent(region, v -> {
            CacheConfig config = cacheConfigs.get(region);
            if(config != null && config.expire != timeToLiveInSeconds)
                throw new IllegalArgumentException(String.format("Region [%s] TTL %d not match with %d", region, config.expire, timeToLiveInSeconds));

            if(config == null) {
                config = cacheConfigs.get(DEFAULT_REGION);
                if (config == null)
                    throw new CacheException(String.format("Undefined caffeine cache region name = %s", region));
            }

            log.info("Started caffeine region [{}] with TTL: {}", region, timeToLiveInSeconds);
            return newCaffeineCache(region, config.size, timeToLiveInSeconds, listener);
        });

        if(cache != null && cache.ttl() != timeToLiveInSeconds)
            throw new IllegalArgumentException(String.format("Region [%s] TTL %d not match with %d", region, cache.ttl(), timeToLiveInSeconds));

        return cache;
    }

    @Override
    public void removeCache(String region) {
        cacheConfigs.remove(region);
        caches.remove(region);
    }

    /**
     * 返回对 Caffeine cache 的 封装
     * @param region region name
     * @param size   max cache object size in memory
     * @param expire cache object expire time in millisecond
     *               if this parameter set to 0 or negative numbers
     *               means never expire
     * @param listener  j2cache cache listener
     * @return CaffeineCache
     */
    private CaffeineCache newCaffeineCache(String region, long size, long expire, CacheExpiredListener listener) {
        Caffeine<Object, Object> caffeine = Caffeine.newBuilder();
        caffeine = caffeine.maximumSize(size)
            .removalListener((k,v, cause) -> {
                /*
                 * 程序删除的缓存不做通知处理,因为上层已经做了处理
                 * 当缓存数据不是因为手工删除和超出容量限制而被删除的情况,就需要通知上层侦听器
                 */
                if(cause != RemovalCause.EXPLICIT && cause != RemovalCause.REPLACED && cause != RemovalCause.SIZE)
                    listener.notifyElementExpired(region, (String)k);
            });
        if (expire > 0) {
            caffeine = caffeine.expireAfterWrite(expire, TimeUnit.SECONDS);
        }
        com.github.benmanes.caffeine.cache.Cache<String, Object> loadingCache = caffeine.build();
        return new CaffeineCache(loadingCache, size, expire);
    }

    /**
     * <p>配置示例</p>
     * <ul>
     * <li>caffeine.region.default = 10000,1h</li>
     * <li>caffeine.region.Users = 10000,1h</li>
     * <li>caffeine.region.Blogs = 80000,30m</li>
     * </ul>
     * @param props current configuration settings.
     */
    @Override
    public void start(Properties props) {
        for(String region : props.stringPropertyNames()) {
            if(!region.startsWith(PREFIX_REGION))
                continue ;
            String s_config = props.getProperty(region).trim();
            region = region.substring(PREFIX_REGION.length());
            this.saveCacheConfig(region, s_config);
        }
        //加载 Caffeine 独立配置文件
        String propertiesFile = props.getProperty("properties");
        if (propertiesFile != null && propertiesFile.trim().length() > 0) {
            InputStream stream = null;
            try {
                stream = getClass().getResourceAsStream(propertiesFile);
                if (stream == null) {
                    stream = getClass().getClassLoader().getResourceAsStream(propertiesFile);
                }
                Properties regionsProps = new Properties();
                regionsProps.load(stream);
                for (String region : regionsProps.stringPropertyNames()) {
                    String s_config = regionsProps.getProperty(region).trim();
                    this.saveCacheConfig(region, s_config);
                }
            } catch (IOException e) {
                log.error("Failed to load caffeine regions define {}", propertiesFile, e);
            } finally {
                try {
                    if (stream != null) {
                        stream.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private void saveCacheConfig(String region, String region_config) {
        CacheConfig cfg = CacheConfig.parse(region_config);
        if(cfg == null)
            log.warn("Illegal caffeine cache config [{}={}]", region, region_config);
        else
            cacheConfigs.put(region, cfg);
    }

    @Override
    public void stop() {
        caches.clear();
        cacheConfigs.clear();
    }

    /**
     * 缓存配置
     */
    private static class CacheConfig {

        private long size = 0L;
        private long expire = 0L;

        public static CacheConfig parse(String cfg) {
            CacheConfig cacheConfig = null;
            String[] cfgs = cfg.split(",");
            if(cfgs.length == 1) {
                cacheConfig = new CacheConfig();
                String sSize = cfgs[0].trim();
                cacheConfig.size = Long.parseLong(sSize);
            }
            else if(cfgs.length == 2) {
                cacheConfig = new CacheConfig();
                String sSize = cfgs[0].trim();
                String sExpire = cfgs[1].trim();
                cacheConfig.size = Long.parseLong(sSize);
                char unit = Character.toLowerCase(sExpire.charAt(sExpire.length()-1));
                cacheConfig.expire = Long.parseLong(sExpire.substring(0, sExpire.length() - 1));
                switch(unit){
                    case 's'://seconds
                        break;
                    case 'm'://minutes
                        cacheConfig.expire *= 60;
                        break;
                    case 'h'://hours
                        cacheConfig.expire *= 3600;
                        break;
                    case 'd'://days
                        cacheConfig.expire *= 86400;
                        break;
                    default:
                        throw new IllegalArgumentException("Unknown expire unit:" + unit);
                }
            }
            return cacheConfig;
        }

        @Override
        public String toString() {
            return String.format("[SIZE:%d,EXPIRE:%d]", size, expire);
        }

    }

}