/**
 *  Cache.java
 *  Copyright 2006 by Michael Peter Christen
 *  First released 26.10.2006 at https://yacy.net
 *
 *  $LastChangedDate$
 *  $LastChangedRevision$
 *  $LastChangedBy$
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library 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
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program in the file lgpl21.txt
 *  If not, see <http://www.gnu.org/licenses/>.
 */

package net.yacy.kelondro.index;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import net.yacy.cora.order.CloneableIterator;
import net.yacy.cora.util.SpaceExceededException;
import net.yacy.kelondro.index.Row.Entry;
import net.yacy.kelondro.util.MemoryControl;


public final class Cache implements Index, Iterable<Row.Entry> {

    // this is a combined read cache and write buffer
    // we maintain four tables:
    // - a read-cache
    // - a miss-cache
    // - a write buffer for rows that are not contained in the target index
    // - a write buffer for rows that are known to be contained in target
    // furthermore, if we access a kelondroFlexTable, we can use the ram index of the underlying index

    // static object tracker; stores information about object cache usage
    private static final TreeMap<String, Cache> objectTracker = new TreeMap<>();
    private static final long memStopGrow    = 40 * 1024 * 1024; // a limit for the node cache to stop growing if less than this memory amount is available
    private static final long memStartShrink = 20 * 1024 * 1024; // a limit for the node cache to start with shrinking if less than this memory amount is available

    // class objects
    private final Index index;         // the back-end of the cache
    private       RowSet      readHitCache;  // contains a complete copy of the cached objects
    private       RowSet      readMissCache; // contains only the keys of the objects that had been a miss
    private       Row         keyrow;
    private       int         readHit, readMiss, writeUnique, writeDouble, cacheDelete, cacheFlush;
    private       int         hasnotHit, hasnotMiss, hasnotUnique, hasnotDouble, hasnotDelete;
    private final int         hitLimit, missLimit;

    /**
     * create a ObjectIndex cache. The cache may either limited by a number of entries in the hit/miss cache
     * or the cache size can only be limited by the available RAM
     * @param backupIndex the ObjectIndex that is cached
     * @param hitLimit a limit of cache hit entries. If given as value <= 0, then only the RAM limits the size
     * @param missLimit a limit of cache miss entries. If given as value <= 0, then only the RAM limits the size
     */
    public Cache(final Index backupIndex, final int hitLimit, final int missLimit) {
        this.index = backupIndex;
        this.hitLimit = hitLimit;
        this.missLimit = missLimit;
        this.init();
        objectTracker.put(backupIndex.filename(), this);
    }

    private void init() {
        final Row row = this.index.row();
        this.keyrow = new Row(new Column[]{row.column(0)}, row.objectOrder);
        this.readHitCache = new RowSet(row);
        this.readMissCache = new RowSet(this.keyrow);
        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;
    }

    @Override
    public long mem() {
        return this.index.mem() + this.readHitCache.mem() + this.readMissCache.mem();
    }

    @Override
    public void optimize() {
        this.index.optimize();
        this.readHitCache.optimize();
        this.readMissCache.optimize();
    }

    public final int writeBufferSize() {
        return 0;
    }

    public final static long getMemStopGrow() {
        return memStopGrow ;
    }

    public final static long getMemStartShrink() {
        return memStartShrink ;
    }

    public final int getHitLimit() {
        return this.hitLimit;
    }

    public final int getMissLimit() {
        return this.missLimit;
    }

    @Override
    public byte[] smallestKey() {
        return this.index.smallestKey();
    }

    @Override
    public byte[] largestKey() {
        return this.index.largestKey();
    }

    public static final Iterator<String> filenames() {
        // iterates string objects; all file names from record tracker
        return objectTracker.keySet().iterator();
    }

    public enum StatKeys {
        objectHitChunkSize,
        objectHitCacheCount,
        objectHitMem,
        objectHitCacheReadHit,
        objectHitCacheReadMiss,
        objectHitCacheWriteUnique,
        objectHitCacheWriteDouble,
        objectHitCacheDeletes,
        objectHitCacheFlushes,
        objectMissChunkSize,
        objectMissCacheCount,
        objectMissMem,
        objectMissCacheReadHit,
        objectMissCacheReadMiss,
        objectMissCacheWriteUnique,
        objectMissCacheWriteDouble,
        objectMissCacheDeletes,
        objectMissCacheFlushes;
    }

    public static final Map<StatKeys, String> memoryStats(final String filename) {
        // returns a map for each file in the tracker;
        // the map represents properties for each record oobjects,
        // i.e. for cache memory allocation
        final Cache theObjectsCache = objectTracker.get(filename);
        return theObjectsCache.memoryStats();
    }

    private final Map<StatKeys, String> memoryStats() {
        // returns statistical data about this object
        final HashMap<StatKeys, String> map = new HashMap<>(20);
        map.put(StatKeys.objectHitChunkSize, (this.readHitCache == null) ? "0" : Integer.toString(this.readHitCache.rowdef.objectsize));
        map.put(StatKeys.objectHitCacheCount, (this.readHitCache == null) ? "0" : Integer.toString(this.readHitCache.size()));
        map.put(StatKeys.objectHitMem, (this.readHitCache == null) ? "0" : Long.toString(this.readHitCache.rowdef.objectsize * this.readHitCache.size()));
        map.put(StatKeys.objectHitCacheReadHit, Integer.toString(this.readHit));
        map.put(StatKeys.objectHitCacheReadMiss, Integer.toString(this.readMiss));
        map.put(StatKeys.objectHitCacheWriteUnique, Integer.toString(this.writeUnique));
        map.put(StatKeys.objectHitCacheWriteDouble, Integer.toString(this.writeDouble));
        map.put(StatKeys.objectHitCacheDeletes, Integer.toString(this.cacheDelete));
        map.put(StatKeys.objectHitCacheFlushes, Integer.toString(this.cacheFlush));

        map.put(StatKeys.objectMissChunkSize, (this.readMissCache == null) ? "0" : Integer.toString(this.readMissCache.rowdef.objectsize));
        map.put(StatKeys.objectMissCacheCount, (this.readMissCache == null) ? "0" : Integer.toString(this.readMissCache.size()));
        map.put(StatKeys.objectMissMem, (this.readMissCache == null) ? "0" : Long.toString(this.readMissCache.rowdef.objectsize * this.readMissCache.size()));
        map.put(StatKeys.objectMissCacheReadHit, Integer.toString(this.hasnotHit));
        map.put(StatKeys.objectMissCacheReadMiss, Integer.toString(this.hasnotMiss));
        map.put(StatKeys.objectMissCacheWriteUnique, Integer.toString(this.hasnotUnique));
        map.put(StatKeys.objectMissCacheWriteDouble, Integer.toString(this.hasnotDouble));
        map.put(StatKeys.objectMissCacheDeletes, Integer.toString(this.hasnotDelete));
        map.put(StatKeys.objectMissCacheFlushes, "0"); // a miss cache flush can only happen if we have a deletion cache (which we dont have)

        // future feature .. map.put("objectElderTimeRead", index.profile().)
        return map;
    }


    /**
     * checks for space in the miss cache
     * @return true if it is allowed to write into this cache
     */
    private final boolean checkMissSpace() {
        // returns true if it is allowed to write into this cache
        if (this.readMissCache == null) return false;

        // check given limitation
        if (this.missLimit > 0 && this.readMissCache.size() >= this.missLimit) return false;

        // check memory
        long available = MemoryControl.available();
        if (MemoryControl.shortStatus() || available - 2 * 1024 * 1024 < this.readMissCache.memoryNeededForGrow()) {
            this.readMissCache.clear();
        }
        available = MemoryControl.available();
        return (available - 2 * 1024 * 1024 > this.readMissCache.memoryNeededForGrow());
    }

    /**
     * checks for space in the hit cache
     * @return true if it is allowed to write into this cache
     */
    private final boolean checkHitSpace() {
        // returns true if it is allowed to write into this cache
        if (this.readHitCache == null) return false;

        // check given limitation
        if (this.hitLimit > 0 && this.readHitCache.size() >= this.hitLimit) return false;

        // check memory
        long available = MemoryControl.available();
        if (MemoryControl.shortStatus() || available - 2 * 1024 * 1024 < this.readHitCache.memoryNeededForGrow()) {
            this.readHitCache.clear();
        }
        available = MemoryControl.available();
        return (available - 2 * 1024 * 1024 > this.readHitCache.memoryNeededForGrow());
    }

    public final synchronized void clearCache() {
        if (this.readMissCache != null) this.readMissCache.clear();
        if (this.readHitCache != null) this.readHitCache.clear();
    }

    @Override
    public final synchronized void close() {
        this.index.close();
        this.readHitCache = null;
        this.readMissCache = null;
    }

    @Override
    public final synchronized boolean has(final byte[] key) {
        // first look into the miss cache
        if (this.readMissCache != null) {
            if (this.readMissCache.has(key)) {
                this.hasnotHit++;
                return false;
            }
            this.hasnotMiss++;
        }

        // then try the hit cache and the buffers
        if (this.readHitCache != null) {
            if (this.readHitCache.has(key)) {
                this.readHit++;
                return true;
            }
            this.readMiss++;
        }

        // finally ask the back-end index
        return this.index.has(key);
    }

    @Override
    public final synchronized Row.Entry get(final byte[] key, final boolean cachecopy) throws IOException {
        // first look into the miss cache
        if (this.readMissCache != null) {
            if (this.readMissCache.has(key)) {
                this.hasnotHit++;
                return null;
            }
            this.hasnotMiss++;
        }

        Row.Entry entry = null;

        // then try the hit cache and the buffers
        if (this.readHitCache != null) {
            entry = this.readHitCache.get(key, cachecopy);
            if (entry != null) {
                this.readHit++;
                return entry;
            }
        }

        // finally ask the back-end index
        this.readMiss++;
        entry = this.index.get(key, cachecopy);
        // learn from result
        if (entry == null) {
            if (this.checkMissSpace()) try {
                final Row.Entry dummy = this.readMissCache.replace(this.readMissCache.row().newEntry(key));
                if (dummy == null) this.hasnotUnique++; else this.hasnotDouble++;
            } catch (final SpaceExceededException e) {
                this.clearCache();
            }
            return null;
        }

        if (this.checkHitSpace()) try {
            final Row.Entry dummy = this.readHitCache.replace(entry);
            if (dummy == null) this.writeUnique++; else this.writeDouble++;
        } catch (final SpaceExceededException e) {
            this.clearCache();
        }
        return entry;
    }

    @Override
    public Map<byte[], Row.Entry> getMap(final Collection<byte[]> keys, final boolean forcecopy) throws IOException, InterruptedException {
        final Map<byte[], Row.Entry> map = new TreeMap<>(this.row().objectOrder);
        Row.Entry entry;
        for (final byte[] key: keys) {
            entry = this.get(key, forcecopy);
            if (entry != null) map.put(key, entry);
        }
        return map;
    }

    @Override
    public List<Row.Entry> getList(final Collection<byte[]> keys, final boolean forcecopy) throws IOException, InterruptedException {
        final List<Row.Entry> list = new ArrayList<>(keys.size());
        Row.Entry entry;
        for (final byte[] key: keys) {
            entry = this.get(key, forcecopy);
            if (entry != null) list.add(entry);
        }
        return list;
    }

    @Override
    public final synchronized boolean put(final Row.Entry row) throws IOException, SpaceExceededException {
        assert (row != null);
        assert (row.columns() == this.row().columns());
        //assert (!(serverLog.allZero(row.getColBytes(index.primarykey()))));

        final byte[] key = row.getPrimaryKeyBytes();
        this.checkHitSpace();

        // remove entry from miss- and hit-cache
        if (this.readMissCache != null) {
            if (this.readMissCache.delete(key)) {
                this.hasnotHit++;
            }
        }

        // write to the back-end
        boolean c;
        try {
            c = this.index.put(row);
        } catch (final SpaceExceededException e1) {
            // flush all caches to get more memory
            this.clearCache();
            c = this.index.put(row); // try again
        }
        if (this.checkHitSpace()) try {
            final Row.Entry dummy = this.readHitCache.replace(row); // overwrite old entry
            if (dummy == null) this.writeUnique++; else this.writeDouble++;
        } catch (final SpaceExceededException e) {
            this.clearCache();
        }
        return c;
    }

    @Override
    public final synchronized Row.Entry replace(final Row.Entry row) throws IOException, SpaceExceededException {
        assert (row != null);
        assert (row.columns() == this.row().columns());
        //assert (!(serverLog.allZero(row.getColBytes(index.primarykey()))));

        final byte[] key = row.getPrimaryKeyBytes();
        this.checkHitSpace();

        // remove entry from miss- and hit-cache
        if (this.readMissCache != null) {
            if (this.readMissCache.delete(key)) {
                this.hasnotHit++;
                // the entry does not exist before
                try {
                    this.index.put(row);
                } catch (final SpaceExceededException e1) {
                    // flush all caches to get more memory
                    this.clearCache();
                    this.index.put(row); // try again
                }
                // write to backend
                if (this.checkHitSpace()) try {
                    final Row.Entry dummy = this.readHitCache.replace(row); // learn that entry
                    if (dummy == null) this.writeUnique++; else this.writeDouble++;
                } catch (final SpaceExceededException e) {
                    this.clearCache();
                }
                return null;
            }
        }

        Row.Entry entry = null;
        // write to the back-end
        try {
            entry = this.index.replace(row);
        } catch (final SpaceExceededException e1) {
            // flush all caches to get more memory
            this.clearCache();
            this.index.replace(row); // try again
        }
        if (this.checkHitSpace()) try {
            final Row.Entry dummy = this.readHitCache.replace(row); // learn that entry
            if (dummy == null) this.writeUnique++; else this.writeDouble++;
        } catch (final SpaceExceededException e) {
            this.clearCache();
        }
        return entry;
    }

    @Override
    public final synchronized void addUnique(final Row.Entry row) throws IOException, SpaceExceededException {
        assert (row != null);
        assert (row.columns() == this.row().columns());
        //assert (!(serverLog.allZero(row.getColBytes(index.primarykey()))));

        final byte[] key = row.getPrimaryKeyBytes();
        this.checkHitSpace();

        // remove entry from miss- and hit-cache
        if (this.readMissCache != null) {
            this.readMissCache.delete(key);
            this.hasnotDelete++;
            // the entry does not exist before
        }

        // the worst case: we must write to the back-end directly
        try {
            this.index.addUnique(row);
        } catch (final SpaceExceededException e1) {
            // flush all caches to get more memory
            this.clearCache();
            this.index.addUnique(row); // try again
        }
        if (this.checkHitSpace()) try {
            final Row.Entry dummy = this.readHitCache.replace(row); // learn that entry
            if (dummy == null) this.writeUnique++; else this.writeDouble++;
        } catch (final SpaceExceededException e) {
            this.clearCache();
        }
    }

    public final synchronized void addUnique(final Row.Entry row, final Date entryDate) throws IOException, SpaceExceededException {
        if (entryDate == null) {
            this.addUnique(row);
            return;
        }

        assert (row != null);
        assert (row.columns() == this.row().columns());

        final byte[] key = row.getPrimaryKeyBytes();
        this.checkHitSpace();

        // remove entry from miss- and hit-cache
        if (this.readMissCache != null) {
            this.readMissCache.delete(key);
            this.hasnotDelete++;
        }

        // the worst case: we must write to the backend directly
        try {
            this.index.addUnique(row);
        } catch (final SpaceExceededException e1) {
            // flush all caches to get more memory
            this.clearCache();
            this.index.addUnique(row); // try again
        }
        if (this.checkHitSpace()) try {
            final Row.Entry dummy = this.readHitCache.replace(row); // learn that entry
            if (dummy == null) this.writeUnique++; else this.writeDouble++;
        } catch (final SpaceExceededException e) {
            this.clearCache();
        }
    }

    public final synchronized void addUnique(final List<Row.Entry> rows) throws IOException, SpaceExceededException {
        final Iterator<Row.Entry> i = rows.iterator();
        Row.Entry r;
        while (i.hasNext()) {
            r = i.next();
            try {
                this.addUnique(r);
            } catch (final SpaceExceededException e) {
                // flush all caches to get more memory
                this.clearCache();
                this.addUnique(r); // try again
            }
        }
    }

    @Override
    public final synchronized List<RowCollection> removeDoubles() throws IOException, SpaceExceededException {
        return this.index.removeDoubles();
        // todo: remove reported entries from the cache!!!
    }

    @Override
    public final synchronized boolean delete(final byte[] key) throws IOException {
        this.checkMissSpace();

        // add entry to miss-cache
        if (this.checkMissSpace()) try {
            // set the miss cache; if there was already an entry we know that the return value must be null
            final Row.Entry dummy = this.readMissCache.replace(this.readMissCache.row().newEntry(key));
            if (dummy == null) {
                this.hasnotUnique++;
            } else {
                this.hasnotHit++;
                this.hasnotDouble++;
            }
        } catch (final SpaceExceededException e) {
            this.clearCache();
        }

        // remove entry from hit-cache
        if (this.readHitCache != null) {
            final Row.Entry entry = this.readHitCache.remove(key);
            if (entry == null) {
                this.readMiss++;
            } else {
                this.readHit++;
                this.cacheDelete++;
            }
        }

        return this.index.delete(key);
    }

    @Override
    public final synchronized Row.Entry remove(final byte[] key) throws IOException {
        this.checkMissSpace();

        // add entry to miss-cache
        if (this.checkMissSpace()) try {
            // set the miss cache; if there was already an entry we know that the return value must be null
            final Row.Entry dummy = this.readMissCache.replace(this.readMissCache.row().newEntry(key));
            if (dummy == null) {
                this.hasnotUnique++;
            } else {
                this.hasnotHit++;
                this.hasnotDouble++;
            }
        } catch (final SpaceExceededException e) {
            this.clearCache();
        }

        // remove entry from hit-cache
        if (this.readHitCache != null) {
            final Row.Entry entry = this.readHitCache.remove(key);
            if (entry == null) {
                this.readMiss++;
            } else {
                this.readHit++;
                this.cacheDelete++;
            }
        }

        return this.index.remove(key);
    }

    @Override
    public final synchronized Row.Entry removeOne() throws IOException {

        this.checkMissSpace();

        final Row.Entry entry = this.index.removeOne();
        if (entry == null) return null;
        final byte[] key = entry.getPrimaryKeyBytes();
        if (this.checkMissSpace()) try {
            final Row.Entry dummy = this.readMissCache.replace(this.readMissCache.row().newEntry(key));
            if (dummy == null) this.hasnotUnique++; else this.hasnotDouble++;
        } catch (final SpaceExceededException e) {
            this.clearCache();
        }
        if (this.readHitCache != null) {
            if (this.readHitCache.delete(key)) this.cacheDelete++;
        }
        return entry;
    }

    @Override
    public synchronized List<Row.Entry> top(final int count) throws IOException {
        return this.index.top(count);
    }

    @Override
    public synchronized List<Row.Entry> random(final int count) throws IOException {
        return this.index.random(count);
    }

    @Override
    public final synchronized Row row() {
        return this.index.row();
    }

    @Override
    public final synchronized CloneableIterator<byte[]> keys(final boolean up, final byte[] firstKey) throws IOException {
        return this.index.keys(up, firstKey);
    }

    @Override
    public final synchronized CloneableIterator<Row.Entry> rows(final boolean up, final byte[] firstKey) throws IOException {
        return this.index.rows(up, firstKey);
    }

    @Override
    public final Iterator<Entry> iterator() {
        try {
            return this.rows();
        } catch (final IOException e) {
            return null;
        }
    }

    @Override
    public final synchronized CloneableIterator<Row.Entry> rows() throws IOException {
        return this.index.rows();
    }

    @Override
    public final int size() {
        return this.index.size();
    }

    @Override
    public final boolean isEmpty() {
        return this.index.isEmpty();
    }

    @Override
    public final String filename() {
        return this.index.filename();
    }

    @Override
    public final void clear() throws IOException {
        this.index.clear();
        this.init();
    }

    @Override
    public final void deleteOnExit() {
        this.index.deleteOnExit();
    }

}