/*
 * Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
 * Version 1.0, and under the Eclipse Public License, Version 1.0
 * (http://h2database.com/html/license.html).
 * Initial Developer: H2 Group
 */
package org.h2.dev.store;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.Map;
import org.h2.store.fs.FileBase;
import org.h2.store.fs.FilePathWrapper;
import org.h2.util.SmallLRUCache;

/**
 * A file with a read cache.
 */
public class FilePathCache extends FilePathWrapper {

    public static FileChannel wrap(FileChannel f) throws IOException {
        return new FileCache(f);
    }

    public FileChannel open(String mode) throws IOException {
        return new FileCache(getBase().open(mode));
    }

    public String getScheme() {
        return "cache";
    }

    /**
     * A file with a read cache.
     */
    public static class FileCache extends FileBase {

        private static final int CACHE_BLOCK_SIZE = 4 * 1024;
        private final FileChannel base;
        private long pos, posBase, size;
        private final Map<Long, ByteBuffer> cache = SmallLRUCache.newInstance(16);

        FileCache(FileChannel base) throws IOException {
            this.base = base;
            this.size = base.size();
        }

        public FileChannel position(long newPosition) throws IOException {
            this.pos = newPosition;
            return this;
        }

        public long position() throws IOException {
            return pos;
        }

        private void positionBase(long pos) throws IOException {
            if (posBase != pos) {
                base.position(pos);
                posBase = pos;
            }
        }

        public int read(ByteBuffer dst) throws IOException {
            long cachePos = getCachePos(pos);
            int off = (int) (pos - cachePos);
            int len = CACHE_BLOCK_SIZE - off;
            ByteBuffer buff = cache.get(cachePos);
            if (buff == null) {
                buff = ByteBuffer.allocate(CACHE_BLOCK_SIZE);
                positionBase(cachePos);
                int read = base.read(buff);
                posBase += read;
                if (read == CACHE_BLOCK_SIZE) {
                    cache.put(cachePos, buff);
                } else {
                    if (read < 0) {
                        return -1;
                    }
                    len = Math.min(len, read);
                }
            }
            len = Math.min(len, dst.remaining());
            System.arraycopy(buff.array(), off, dst.array(), dst.position(), len);
            dst.position(dst.position() + len);
            pos += len;
            return len;
        }

        private long getCachePos(long pos) {
            return (pos / CACHE_BLOCK_SIZE) * CACHE_BLOCK_SIZE;
        }

        public long size() throws IOException {
            return size;
        }

        public FileChannel truncate(long newSize) throws IOException {
            cache.clear();
            base.truncate(newSize);
            size = Math.min(size, newSize);
            pos = Math.min(pos, newSize);
            posBase = pos;
            return this;
        }

        public int write(ByteBuffer src) throws IOException {
            if (cache.size() > 0) {
                for (long p = getCachePos(pos), len = src.remaining(); len > 0; p += CACHE_BLOCK_SIZE, len -= CACHE_BLOCK_SIZE) {
                    cache.remove(p);
                }
            }
            positionBase(pos);
            int len = base.write(src);
            posBase += len;
            pos += len;
            size = Math.max(size, pos);
            return len;
        }

        public void force(boolean metaData) throws IOException {
            base.force(metaData);
        }

        public FileLock tryLock(long position, long size, boolean shared) throws IOException {
            return base.tryLock(position, size, shared);
        }

        public String toString() {
            return "cache:" + base.toString();
        }

    }

}
