/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 org.apache.jena.dboe.base.block;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.apache.jena.atlas.lib.Cache;
import org.apache.jena.atlas.lib.CacheFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Caching block manager - this is an LRU cache */
public class BlockMgrCache extends BlockMgrSync {
    // Actually, this is two cache one on the read blocks and one on the write
    // blocks.
    // The overridden public operations are sync'ed.
    // As sync is on "this", it also covers all the other operations via
    // BlockMgrSync

    private static Logger            log            = LoggerFactory.getLogger(BlockMgrCache.class);
    // Read cache : always present.
    private final Cache<Long, Block> readCache;

    // Delayed dirty writes. May be present, may not.
    private final Cache<Long, Block> writeCache;

    // Also enable the logging level.
    public static boolean            globalLogging  = false;
    private boolean                  logging        = false;
    // ---- stats
    long                             cacheReadHits  = 0;
    long                             cacheMisses    = 0;
    long                             cacheWriteHits = 0;

    static BlockMgr create(int readSlots, int writeSlots, final BlockMgr blockMgr) {
        if ( readSlots < 0 && writeSlots < 0 )
            return blockMgr;
        return new BlockMgrCache(readSlots, writeSlots, blockMgr);
    }

    private BlockMgrCache(int readSlots, int writeSlots, final BlockMgr blockMgr) {
        super(blockMgr);
        // Caches are related so we can't use a Getter for cache management.
        if ( readSlots < -1 )
            readCache = CacheFactory.createNullCache();
        else
            readCache = CacheFactory.createCache(readSlots);
        if ( writeSlots <= 0 )
            writeCache = null;
        else {
            writeCache = CacheFactory.createCache(writeSlots);
            writeCache.setDropHandler((id, block) -> {
                // We're inside a synchronized operation at this point.
                log("Cache spill: write block: %d", id);
                if ( block == null ) {
                    log.warn("Write cache: " + id + " dropping an entry that isn't there");
                    return;
                }
                // Force the block to be writtern
                // by sending it to the wrapped BlockMgr
                BlockMgrCache.super.write(block);
            });
        }
    }

    @Override
    synchronized public void resetAlloc(long boundary) {
        // On abort, need to clear the caches of inaccesible blocks.
        // An abort is rare (?). We do very carefully.
        // Could (probably) delete in the loop or use Iteator.remove on keys().
        // (Check the Cache contract)
        List<Long> removals = new ArrayList<>();
        readCache.keys().forEachRemaining((x)->{
            if ( x >= boundary )
                removals.add(x);
        });
        removals.forEach(readCache::remove);
        removals.clear();
        writeCache.keys().forEachRemaining((x)->{
            if ( x >= boundary )
                removals.add(x);
        });
        removals.forEach(writeCache::remove);
        super.resetAlloc(boundary);
    }

    @Override
    synchronized public Block getRead(long id) {
        // A Block may be in the read cache or the write cache.
        // It can be just in the write cache because the read cache is finite.
        Block blk = readCache.getIfPresent(id);
        if ( blk != null ) {
            cacheReadHits++;
            log("Hit(r->r) : %d", id);
            return blk;
        }

        // A requested block may be in the other cache.
        // Writable blocks are readable.
        // readable blocks are not writable (see below).
        if ( writeCache != null )
            // Might still be in the dirty blocks.
            // Leave in write cache
            blk = writeCache.getIfPresent(id);
        if ( blk != null ) {
            cacheWriteHits++;
            log("Hit(r->w) : %d", id);
            return blk;
        }

        cacheMisses++;
        log("Miss/r: %d", id);
        blk = super.getRead(id);
        readCache.put(id, blk);
        return blk;
    }

    @Override
    synchronized public Block getWrite(long _id) {
        Long id = _id;
        Block blk = null;
        if ( writeCache != null )
            blk = writeCache.getIfPresent(id);
        if ( blk != null ) {
            cacheWriteHits++;
            log("Hit(w->w) : %d", id);
            return blk;
        }

        // blk is null.
        // A requested block may be in the other cache. Promote it.

        if ( readCache.containsKey(id) ) {
            blk = readCache.getIfPresent(id);
            cacheReadHits++;
            log("Hit(w->r) : %d", id);
            blk = promote(blk);
            return blk;
        }

        // Did not find.
        cacheMisses++;
        log("Miss/w: %d", id);
        // Pass operation to wrapper.
        blk = super.getWrite(id);
        if ( writeCache != null )
            writeCache.put(id, blk);
        return blk;
    }

    @Override
    synchronized public Block promote(Block block) {
        Long id = block.getId();
        readCache.remove(id);
        Block block2 = super.promote(block);
        if ( writeCache != null )
            writeCache.put(id, block2);
        return block;
    }

    @Override
    synchronized public void write(Block block) {
        writeCache(block);
        super.write(block);
    }

    @Override
    synchronized public void overwrite(Block block) {
        Long id = block.getId();
        // It can be a read block (by the transaction), now being written for
        // real (enacting a transaction).
        super.overwrite(block);
        // Keep read cache up-to-date.
        // Must at least expel the read block (which is not the overwrite
        // block).
        readCache.put(id, block);
    }

    private void writeCache(Block block) {
        Long id = block.getId();
        log("WriteCache : %d", id);
        // Should not be in the read cache due to a getWrite earlier.
        if ( readCache.containsKey(id) )
            log.warn("write: Block in the read cache");
        if ( writeCache != null ) {
            writeCache.put(id, block);
            return;
        }
    }

    @Override
    synchronized public void free(Block block) {
        Long id = block.getId();
        log("Free  : %d", id);
        if ( readCache.containsKey(id) ) {
            log.warn("Freeing block from read cache");
            readCache.remove(id);
        }
        if ( writeCache != null )
            writeCache.remove(id);
        super.free(block);
    }

    @Override
    synchronized public void sync() {
        _sync(false);
    }

    @Override
    synchronized public void syncForce() {
        _sync(true);
    }

    @Override
    synchronized public void close() {
        if ( writeCache != null )
            log("close (" + writeCache.size() + " blocks)");
        syncFlush();
        super.close();
    }

    @Override
    public String toString() {
        return "Cache:" + super.blockMgr.toString();
    }

    private void log(String fmt, Object... args) {
        if ( !logging && !globalLogging )
            return;
        String msg = String.format(fmt, args);
        if ( getLabel() != null )
            msg = getLabel() + " : " + msg;
        log.debug(msg);
    }

    private void _sync(boolean force) {
        if ( true ) {
            String x = "";
            if ( getLabel() != null )
                x = getLabel() + " : ";
            log("%sH=%d, M=%d, W=%d", x, cacheReadHits, cacheMisses, cacheWriteHits);
        }

        if ( writeCache != null )
            log("sync (%d blocks)", writeCache.size());
        else
            log("sync");
        boolean somethingWritten = syncFlush();

        if ( force ) {
            log("syncForce underlying BlockMgr");
            super.syncForce();
        } else if ( somethingWritten ) {
            log("sync underlying BlockMgr");
            super.sync();
        } else
            log("Empty sync");
    }

    private boolean syncFlush() {
        if ( writeCache == null )
            return false;

        boolean didSync = false;

        log("Flush (write cache)");

        long N = writeCache.size();
        Long[] ids = new Long[(int)N];

        // Single writer (sync is a write operation MRSW)
        // Iterating is safe.

        Iterator<Long> iter = writeCache.keys();
        if ( iter.hasNext() )
            didSync = true;

        // Need to get all then delete else concurrent modification exception.
        for ( int i = 0; iter.hasNext() ; i++ )
            ids[i] = iter.next();

        for ( int i = 0; i < N ; i++ ) {
            Long id = ids[i];
            expelEntry(id);
        }
        if ( didSync )
            super.sync();
        return didSync;
    }

    // Write out when flushed.
    // Do not call from drop handler.
    private void expelEntry(Long id) {
        Block block = writeCache.getIfPresent(id);
        if ( block == null ) {
            log.warn("Write cache: " + id + " expelling entry that isn't there");
            return;
        }
        log("Expel (write cache): %d", id);
        // This pushes the block to the BlockMgr being cached.
        super.write(block);
        writeCache.remove(id);

        // Move it into the readCache because it's often read after writing
        // and the read cache is often larger.
        readCache.put(id, block);
    }
}
