// kelondroObjectCache.java
// ------------------------
// (C) by Michael Peter Christen; mc@anomic.de
// first published on http://www.anomic.de
// Frankfurt, Germany, 2006
//
// This is a part of the kelondro database, which is a part of YaCy
//
// $LastChangedDate: 2006-04-02 22:40:07 +0200 (So, 02 Apr 2006) $
// $LastChangedRevision: 1986 $
// $LastChangedBy: orbiter $
//
//
// LICENSE
// 
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program 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 for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
//
// A NOTE FROM THE AUTHOR TO THE USERS:
//
// Using this software in any meaning (reading, learning, copying, compiling,
// running) means that you agree that the Author(s) is (are) not responsible
// for cost, loss of data or any harm that may be caused directly or indirectly
// by usage of this softare or this documentation. The usage of this software
// is on your own risk. The installation and usage (starting/running) of this
// software may allow other people or application to access your computer and
// any attached devices and is highly dependent on the configuration of the
// software which must be done by the user of the software; the author(s) is
// (are) also not responsible for proper configuration and usage of the
// software, even if provoked by documentation provided together with
// the software.
//
// 
// A NOTE FROM THE AUTHOR TO DEVELOPERS:
//
// Contributions and changes to the program code should be marked as such:
// Please enter your own (C) notice below; they must be compatible with the GPL.
// Please mark also all changes in the code; if you don't mark them then they
// can't be identified; thus all unmarked code belong to the copyright holder
// as mentioned above. A good documentation of code authorities will also help
// to maintain the code and the project.
// A re-distribution must contain the intact and unchanged copyright statement.


package de.anomic.kelondro;

import java.util.TreeMap;

public class kelondroObjectCache {

    private final  TreeMap cache;
    private final  kelondroMScoreCluster ages, hasnot;
    private long   startTime;
    private int    maxSize;
    private long   maxAge;
    private long   minMem;
    private int    readHit, readMiss, writeUnique, writeDouble, cacheDelete, cacheFlush;
    private int    hasnotHit, hasnotMiss, hasnotUnique, hasnotDouble, hasnotDelete, hasnotFlush;
    private String name;
    
    public kelondroObjectCache(String name, int maxSize, long maxAge, long minMem) {
        this.name = name;
        this.cache = new TreeMap();
        this.ages  = new kelondroMScoreCluster();
        this.hasnot  = new kelondroMScoreCluster();
        this.startTime = System.currentTimeMillis();
        this.maxSize = Math.max(maxSize, 1);
        this.maxAge = Math.max(maxAge, 10000);
        this.minMem = Math.max(minMem, 1024 * 1024);
        this.readHit = 0;
        this.readMiss = 0;
        this.writeUnique = 0;
        this.writeDouble = 0;
        this.cacheDelete = 0;
        this.cacheFlush = 0;
        this.hasnotHit = 0;
        this.hasnotMiss = 0;
        this.hasnotUnique = 0;
        this.hasnotDouble = 0;
        this.hasnotDelete = 0;
        this.hasnotFlush = 0;
    }

    public String getName() {
        return name;
    }
    
    public void setMaxAge(long maxAge) {
        this.maxAge = maxAge;
    }
    
    public void setMaxSize(int maxSize) {
        this.maxSize = maxSize;
    }
    
    public int maxSize() {
        return this.maxSize;
    }
    
    public void setMinMem(int minMem) {
        this.minMem = minMem;
    }
    
    public long minAge() {
        if (ages.size() == 0) return 0;
        return System.currentTimeMillis() - longEmit(ages.getMaxScore());
    }
    
    public long maxAge() {
        if (ages.size() == 0) return 0;
        return System.currentTimeMillis() - longEmit(ages.getMinScore());
    }
    
    public int hitsize() {
        return cache.size();
    }
    
    public int misssize() {
        return hasnot.size();
    }
    
    public String[] status() {
        return new String[]{
                Integer.toString(maxSize()),
                Integer.toString(hitsize()),
                Integer.toString(misssize()),
                Long.toString(this.maxAge),
                Long.toString(minAge()),
                Long.toString(maxAge()),
                Integer.toString(readHit),
                Integer.toString(readMiss),
                Integer.toString(writeUnique),
                Integer.toString(writeDouble),
                Integer.toString(cacheDelete),
                Integer.toString(cacheFlush),
                Integer.toString(hasnotHit),
                Integer.toString(hasnotMiss),
                Integer.toString(hasnotUnique),
                Integer.toString(hasnotDouble),
                Integer.toString(hasnotDelete),
                Integer.toString(hasnotFlush)
                };
    }
    
    private static String[] combinedStatus(String[] a, String[] b) {
        return new String[]{
                Integer.toString(Integer.parseInt(a[0]) + Integer.parseInt(b[0])),
                Integer.toString(Integer.parseInt(a[1]) + Integer.parseInt(b[1])),
                Integer.toString(Integer.parseInt(a[2]) + Integer.parseInt(b[2])),
                Long.toString(Math.max(Long.parseLong(a[3]), Long.parseLong(b[3]))),
                Long.toString(Math.min(Long.parseLong(a[4]), Long.parseLong(b[4]))),
                Long.toString(Math.max(Long.parseLong(a[5]), Long.parseLong(b[5]))),
                Integer.toString(Integer.parseInt(a[6]) + Integer.parseInt(b[6])),
                Integer.toString(Integer.parseInt(a[7]) + Integer.parseInt(b[7])),
                Integer.toString(Integer.parseInt(a[8]) + Integer.parseInt(b[8])),
                Integer.toString(Integer.parseInt(a[9]) + Integer.parseInt(b[9])),
                Integer.toString(Integer.parseInt(a[10]) + Integer.parseInt(b[10])),
                Integer.toString(Integer.parseInt(a[11]) + Integer.parseInt(b[11])),
                Integer.toString(Integer.parseInt(a[12]) + Integer.parseInt(b[12])),
                Integer.toString(Integer.parseInt(a[13]) + Integer.parseInt(b[13])),
                Integer.toString(Integer.parseInt(a[14]) + Integer.parseInt(b[14])),
                Integer.toString(Integer.parseInt(a[15]) + Integer.parseInt(b[15])),
                Integer.toString(Integer.parseInt(a[16]) + Integer.parseInt(b[16])),
                Integer.toString(Integer.parseInt(a[17]) + Integer.parseInt(b[17]))
        };
    }
    
    public static String[] combinedStatus(String[][] a, int l) {
        if ((a == null) || (a.length == 0) || (l == 0)) return null;
        if ((a.length >= 1) && (l == 1)) return a[0];
        if ((a.length >= 2) && (l == 2)) return combinedStatus(a[0], a[1]);
        return combinedStatus(combinedStatus(a, l - 1), a[l - 1]);
    }
    
    private int intTime(long longTime) {
        return (int) Math.max(0, ((longTime - startTime) / 1000));
    }

    private long longEmit(int intTime) {
        return (((long) intTime) * (long) 1000) + startTime;
    }
    
    public void put(byte[] key, Object value) {
        if (key != null) put(new String(key), value);
    }
    
    public void put(String key, Object value) {
        if ((key == null) || (value == null)) return;
        Object prev = null;
        synchronized(cache) {
            prev = cache.put(key, value);
            ages.setScore(key, intTime(System.currentTimeMillis()));
            if (hasnot.deleteScore(key) != 0) hasnotDelete++;
        }
        if (prev == null) this.writeUnique++; else this.writeDouble++;
        flushc();
    }
    
    public Object get(byte[] key) {
        return get(new String(key));
    }
    
    public Object get(String key) {
        if (key == null) return null;
        Object r = null;
        synchronized(cache) {
            r = cache.get(key);
            if (r == null) {
                this.readMiss++;
            } else {
                this.readHit++;
                ages.setScore(key, intTime(System.currentTimeMillis())); // renew cache update time
            }
        }
        flushc();
        return r;
    }
    
    public void hasnot(byte[] key) {
        hasnot(new String(key));
    }
    
    public void hasnot(String key) {
        if (key == null) return;
        int prev = 0;
        synchronized(cache) {
            if (cache.remove(key) != null) cacheDelete++;
            ages.deleteScore(key);
            prev = hasnot.getScore(key);
            hasnot.setScore(key, intTime(System.currentTimeMillis()));
        }
        if (prev == 0) this.hasnotUnique++; else this.hasnotDouble++;
        flushh();
    }
    
    public int has(byte[] key) {
        return has(new String(key));
    }
    
    public int has(String key) {
        // returns a 3-value boolean:
        //  1 = key definitely exists
        // -1 = key definitely does not exist
        //  0 = unknown, if key exists
        if (key == null) return 0;
        synchronized(cache) {
            if (hasnot.getScore(key) > 0) {
                hasnot.setScore(key, intTime(System.currentTimeMillis())); // renew cache update time
                this.hasnotHit++;
                return -1;
            }
            this.hasnotMiss++;
            if (cache.get(key) != null) return 1;
        }
        flushh();
        return 0;
    }
    
    public void remove(byte[] key) {
        remove(new String(key));
    }
    
    public void remove(String key) {
        if (key == null) return;
        synchronized(cache) {
            if (cache.remove(key) != null) cacheDelete++;
            ages.deleteScore(key);
            hasnot.setScore(key, intTime(System.currentTimeMillis()));
        }
    }
    
    public void flushc() {
        String k;
        synchronized(cache) {
            while ((ages.size() > 0) &&
                   ((k = (String) ages.getMinObject()) != null) &&
                   ((ages.size() > maxSize) ||
                    (((System.currentTimeMillis() - longEmit(ages.getScore(k))) > maxAge) &&
                     (Runtime.getRuntime().freeMemory() < minMem)))
                  ) {
                cache.remove(k);
                ages.deleteScore(k);
                cacheFlush++;
            }
        }
    }
    
    public void flushh() {
        String k;
        synchronized(cache) {
            while ((hasnot.size() > 0) &&
                    ((k = (String) hasnot.getMinObject()) != null) &&
                    ((hasnot.size() > maxSize) ||
                      (((System.currentTimeMillis() - longEmit(hasnot.getScore(k))) > maxAge) &&
                       (Runtime.getRuntime().freeMemory() < minMem)))
                   ) {
                 hasnot.deleteScore(k);
                 hasnotFlush++;
             }
        }
    }
}