/*
 * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores
 * CA 94065 USA or visit www.oracle.com if you need additional information or
 * have any questions.
 */

package com.codename1.io;

import com.codename1.ui.Display;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

/**
 * A cache map is essentially a hashtable that indexes entries based on age and is
 * limited to a fixed size. Hence when an entry is placed into the cache map and the
 * cache size needs to increase, the least referenced entry is removed.
 * A cache hit is made both on fetching and putting, hence frequently fetched elements
 * will never be removed from a sufficiently large cache.
 * Cache can work purely in memory or swap data into storage based on user definitions.
 * Notice that this class isn't threadsafe.
 *
 * @author Shai Almog
 */
public class CacheMap {
    private int cacheSize = 10;
    private Hashtable memoryCache = new Hashtable();
    private Hashtable weakCache = new Hashtable();

    private int storageCacheSize = 0;
    private Vector storageCacheContentVec;
    private String cachePrefix = "";
    private boolean alwaysStore;
    private int storageKey = -1;
    
    private Vector getStorageCacheContent() {
        if(storageCacheContentVec == null) {
            storageCacheContentVec = (Vector)Storage.getInstance().readObject("$CACHE$Idx" + cachePrefix);
            if(storageCacheContentVec == null) {
                storageCacheContentVec = new Vector();
            }
        }
        return storageCacheContentVec;
    }
    
    /**
     * Default constructor
     */
    public CacheMap() {
    }
    

    /**
     * Creates a cache map with a prefix string
     * @param prefix string to prepend to the cache entries in storage
     */
    public CacheMap(String prefix) {
        this.cachePrefix = prefix;
    }
    
    
    
    /**
     * Indicates the size of the memory cache after which the cache won't grow further
     * Size is indicated by number of elements stored and not by KB or similar benchmark!
     *
     * @return the cacheSize
     */
    public int getCacheSize() {
        return cacheSize;
    }

    /**
     * Indicates the size of the memory cache after which the cache won't grow further
     * Size is indicated by number of elements stored and not by KB or similar benchmark!
     * 
     * @param cacheSize the cacheSize to set
     */
    public void setCacheSize(int cacheSize) {
        this.cacheSize = cacheSize;
    }

    /**
     * Puts the given key/value pair in the cache
     *
     * @param key the key
     * @param value the value
     */
    public void put(Object key, Object value) {
        if(cacheSize <= memoryCache.size()) {
            // we need to find the oldest entry
            Enumeration e = memoryCache.keys();
            long oldest = System.currentTimeMillis();
            Object oldestKey = null;
            Object[] oldestValue = null;
            while(e.hasMoreElements()) {
                Object currentKey = e.nextElement();
                Object[] currentValue = (Object[])memoryCache.get(currentKey);
                long currentAge = ((Long)currentValue[0]).longValue();
                if(currentAge <= oldest) {
                    oldest = currentAge;
                    oldestKey = currentKey;
                    oldestValue = currentValue;
                }
            }
            placeInStorageCache(oldestKey, oldest, oldestValue[1]);
            weakCache.put(oldestKey, Display.getInstance().createSoftWeakRef(oldestValue[1]));
            memoryCache.remove(oldestKey);
        }
        long lastAccess = System.currentTimeMillis();
        memoryCache.put(key, new Object[]{new Long(lastAccess), value});
        if(alwaysStore) {
            placeInStorageCache(key, lastAccess, value);
        }
    }


    /**
     * Deletes a cached entry
     * 
     * @param key entry to remove from the cache
     */
    public void delete(Object key) {
        memoryCache.remove(key);
        weakCache.remove(key);
        Vector storageCacheContent = getStorageCacheContent();
        int s = storageCacheContent.size();
        for(int iter = 0 ; iter < s ; iter++) {
            Object[] obj = (Object[])storageCacheContent.elementAt(iter);
            if(obj[1].equals(key)) {
                Storage.getInstance().deleteStorageFile("$CACHE$" + cachePrefix + key.toString());
                obj[0] = new Long(Long.MIN_VALUE);
                obj[1] = obj[0];                
                Storage.getInstance().writeObject("$CACHE$Idx" + cachePrefix, storageCacheContent);
                return;
            }
        }
    }
    
    /**
     * Returns the object matching the given key
     *
     * @param key key object
     * @return value from a previous put or null
     */
    public Object get(Object key) {
        Object[] o = (Object[])memoryCache.get(key);
        if(o != null) {
            return o[1];
        }
        Object ref = weakCache.get(key);
        if(ref != null) {
            ref = Display.getInstance().extractHardRef(ref);
            if(ref != null) {
                // cache hit! Promote it to the hard cache again
                put(key, ref);
                return ref;
            }
        }
        if(storageCacheSize > 0) {
            Vector storageCacheContent = getStorageCacheContent();
            for(int iter = 0 ; iter < storageCacheContent.size() ; iter++) {
                Object[] obj = (Object[])storageCacheContent.elementAt(iter);
                if(obj[1].equals(key)) {
                    // place the object back into the memory cache and return the value
                    Vector v = (Vector)Storage.getInstance().readObject("$CACHE$" + cachePrefix + key.toString());
                    if(v != null) {
                        Object val = v.elementAt(0);
                        put(key, val);
                        return val;
                    }
                    return null;
                }
            }
        }
        return null;
    }

    /**
     * Clears the caches for this cache object
     */
    public void clearAllCache() {
        clearMemoryCache();
        clearStorageCache();
    }

    /**
     * Clears the memory cache
     */
    public void clearMemoryCache() {
        memoryCache.clear();
        weakCache.clear();
    }

    private void placeInStorageCache(Object key, long lastAccessed, Object value) {
        if(storageCacheSize < 1) {
            return;
        }

        Vector storageCacheContent = getStorageCacheContent();
        int vecSize = storageCacheContent.size();

        // find the best place
        for(int iter = 0 ; iter < vecSize ; iter++) {
            Object[] index = (Object[])storageCacheContent.elementAt(iter);
            Object indexKey = index[1];
            if(indexKey.equals(key)) {
                // already in storage just update the data
                placeInStorageCache(iter, key, lastAccessed, value);
                return;
            }
        }
        
        if(storageCacheContent.size() < storageCacheSize) {
            placeInStorageCache(storageCacheContent.size(), key, lastAccessed, value);
        } else {
            long smallest = Long.MAX_VALUE;
            int offset = 0;

            // find the best place
            for(int iter = 0 ; iter < vecSize ; iter++) {
                Object[] index = (Object[])storageCacheContent.elementAt(iter);
                long current = ((Long)index[0]).longValue();
                if(smallest > current) {
                    smallest = current;
                    offset = iter;
                }
            }
            placeInStorageCache(offset, key, lastAccessed, value);
        }
    }

    private void placeInStorageCache(int offset, Object key, long lastAccessed, Object value) {
        Vector v = new Vector();
        v.addElement(value);
        Long l = new Long(lastAccessed);
        v.addElement(l);
        v.addElement(key);
        Storage.getInstance().writeObject("$CACHE$" + cachePrefix + key.toString(), v);
        Vector storageCacheContent = getStorageCacheContent();
        if(storageCacheContent.size() > offset) {
            storageCacheContent.setElementAt(new Object[] {l, key}, offset);
        } else {
            storageCacheContent.insertElementAt(new Object[] {l, key}, offset);
        }
        Storage.getInstance().writeObject("$CACHE$Idx" + cachePrefix, storageCacheContent);
    }
    
    /**
     * Returns the keys for all the objects currently in cache, this is useful
     * to traverse all the objects and refresh them without actually deleting
     * the cache and fetching them from scratch.<br>
     * <b>Important</b> this vector is a copy of a current state, keys might 
     * not exist anymore or might change, others might be added in the interim.
     * 
     * @return a vector containing a snapshot of the current elements within the 
     * cache.
     */
    public Vector getKeysInCache() {
        Vector r = new Vector();
        Enumeration en = memoryCache.keys();
        while(en.hasMoreElements()) {
            r.addElement(en.nextElement());
        }
        Vector storageCacheContent = getStorageCacheContent();
        for(int iter = 0 ; iter < storageCacheContent.size() ; iter++) {
            Object[] o = (Object[])storageCacheContent.elementAt(iter);
            if(!r.contains(o[1])) {
                r.addElement(o[1]);
            }
        }
        return r;
    }

    private Vector fetchFromStorageCache(int offset) {
        Vector v = getStorageCacheContent();
        Object[] arr = (Object[])v.elementAt(offset);
        return (Vector)Storage.getInstance().readObject("$CACHE$" + cachePrefix + arr[1].toString());
    }

    /**
     * Clears the storage cache
     */
    public void clearStorageCache() {
        if(storageCacheSize > 0) {
            Vector v = getStorageCacheContent();
            int s = v.size();
            for(int iter = 0 ; iter < s ; iter++) {
                Object[] arr = (Object[])v.elementAt(iter);
                Storage.getInstance().deleteStorageFile("$CACHE$" + cachePrefix + arr[iter].toString());
            }
            Storage.getInstance().deleteStorageFile("$CACHE$Idx" + cachePrefix);
            storageCacheContentVec = new Vector();
        }
    }

    /**
     * Indicates the size of the storage cache after which the cache won't grow further
     * Size is indicated by number of elements stored and not by KB or similar benchmark!
     *
     * @return the storageCacheSize
     */
    public int getStorageCacheSize() {
        return storageCacheSize;
    }

    /**
     * Indicates the size of the storage cache after which the cache won't grow further
     * Size is indicated by number of elements stored and not by KB or similar benchmark!
     *
     * @param storageCacheSize the storageCacheSize to set
     */
    public void setStorageCacheSize(int storageCacheSize) {
        this.storageCacheSize = storageCacheSize;
        /*for(int iter = 0 ; iter < storageCacheSize ; iter++) {
            Vector v = fetchFromStorageCache(iter);
            if(v != null) {
                Object o = new Object[] {v.elementAt(1), v.elementAt(2)};
                Vector storageCacheContent = getStorageCacheContent();
                if(iter >= storageCacheContent.size()) {
                    storageCacheContent.addElement(o);
                } else {
                    storageCacheContent.insertElementAt(o, iter);
                }
            }
        }*/
        if(storageCacheSize == 0) {
            alwaysStore = false;
        }
    }

    /**
     * A prefix prepended to storage entries to differentiate them
     * @return the cachePrefix
     */
    public String getCachePrefix() {
        return cachePrefix;
    }

    /**
     * A prefix prepended to storage entries to differentiate them
     * @param cachePrefix the cachePrefix to set
     */
    public void setCachePrefix(String cachePrefix) {
        this.cachePrefix = cachePrefix;
    }

    /**
     * When set to true indicates that all entries should be persisted to storage
     * for a constantly persisting cache
     * @return the alwaysStore
     */
    public boolean isAlwaysStore() {
        return alwaysStore;
    }

    /**
     * When set to true indicates that all entries should be persisted to storage
     * for a constantly persisting cache
     * @param alwaysStore the alwaysStore to set
     */
    public void setAlwaysStore(boolean alwaysStore) {
        this.alwaysStore = alwaysStore;
    }
}
