package com.softcorporation.util;

import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;

public class ExpiringCache<K, V> {
    private int maxSize;
    private long lastLoaded;
    private long itemTTL;
    @SuppressWarnings("unused")
	private long cacheTTL;
    private Map<Object, CacheObject> cacheMap;

    private class CacheObject {
        public long lastAccessed = System.currentTimeMillis();
        public V value;

        protected CacheObject(V value) {
            this.value = value;
        }

        @Override
        public String toString() {
            return value.toString();
        }

    }

    public ExpiringCache(final int maxSize) {
        this(maxSize, 0, 0, 0);
    }

    public ExpiringCache(final int maxSize, final int itemTTL) {
        this(maxSize, itemTTL, 0, 1);
    }

    public ExpiringCache(final int maxSize, final int itemTTL, final int cacheTTL) {
        this(maxSize, itemTTL, cacheTTL, 1);
    }

    public ExpiringCache(final int maxSize, final int itemTTL, final int cacheTTL, final int timerInterval) {
        this.maxSize = maxSize;
        cacheMap = new LRUCache<Object, CacheObject>(maxSize);
        lastLoaded = System.currentTimeMillis();
        this.load();
        if (timerInterval > 0 && (itemTTL > 0 || cacheTTL > 0)) {
            this.itemTTL = itemTTL * 1000;
            long cTTL = cacheTTL * 1000;
            this.cacheTTL = cTTL;
            long tick = timerInterval * 1000;
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    while (true) {
                        try {
                            Thread.sleep(tick);
                        } catch (InterruptedException ex) {
                        }
                        if (cTTL > 0 && (System.currentTimeMillis() - lastLoaded > cTTL)) {
                            reload();
                        } else if (itemTTL > 0) {
                            cleanup();
                        }
                    }
                }
            });
            thread.setDaemon(true);
            thread.start();
        }
    }

    public synchronized void put(K key, V value) {
        cacheMap.put(key, new CacheObject(value));
    }

    public synchronized V get(K key) {
        CacheObject cacheObject = cacheMap.get(key);
        if (cacheObject == null) {
            return null;
        } else {
            cacheObject.lastAccessed = System.currentTimeMillis();
            return cacheObject.value;
        }
    }

    public synchronized void remove(K key) {
        cacheMap.remove(key);
    }

    public synchronized int size() {
        return cacheMap.size();
    }

    public synchronized void cleanup() {
        long now = System.currentTimeMillis();
        for (Iterator<Entry<Object, ExpiringCache<K, V>.CacheObject>> itr = cacheMap.entrySet().iterator(); itr.hasNext();) {
            CacheObject cacheObject = itr.next().getValue();
            if (cacheObject != null && (now - cacheObject.lastAccessed > itemTTL)) {
                itr.remove();
            }
        }
    }
    
    public synchronized void reload() {
        LRUCache<Object, CacheObject> cacheMap = new LRUCache<Object, CacheObject>(maxSize);
        this.cacheMap = cacheMap;
        this.load();
        lastLoaded = System.currentTimeMillis();
    }

    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder();
        buf.append(cacheMap.size());
        buf.append(": ");
        buf.append(Arrays.toString(cacheMap.entrySet().toArray()));
        return buf.toString();
    }

    public int load() {
        return 0;
    }

}

class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private static final long serialVersionUID = 1L;
    private final int maxEntries;

    public LRUCache(final int maxEntries) {
        super(maxEntries + 1, 1.0f, true);
        this.maxEntries = maxEntries;
    }

    /**
     * Returns true if LRUCache has more entries than specified maximum.
     * @param eldest the Entry
     * @return true if the oldest
     */
    @Override
    protected boolean removeEldestEntry(final Map.Entry<K, V> eldest) {
        return super.size() > maxEntries;
    }
}
