/*
 * Copyright 2009 the original author or authors.
 *
 * Licensed 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.gradle.cache.internal.btree;

import org.gradle.api.UncheckedIOException;

import java.io.*;
import java.util.zip.CRC32;

public class FileBackedBlockStore implements BlockStore {
    private RandomAccessFile file;
    private final File cacheFile;
    private long nextBlock;
    private Factory factory;

    public FileBackedBlockStore(File cacheFile) {
        this.cacheFile = cacheFile;
    }

    @Override
    public String toString() {
        return String.format("cache '%s'", cacheFile);
    }

    public void open(Runnable runnable, Factory factory) {
        this.factory = factory;
        try {
            file = new RandomAccessFile(cacheFile, "rw");
            nextBlock = file.length();
            if (file.length() == 0) {
                runnable.run();
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public void close() {
        try {
            file.close();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public void clear() {
        try {
            file.setLength(0);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
        nextBlock = 0;
    }

    public void attach(BlockPayload block) {
        if (block.getBlock() == null) {
            block.setBlock(new BlockImpl(block));
        }
    }

    public void remove(BlockPayload block) {
        BlockImpl blockImpl = (BlockImpl) block.getBlock();
        blockImpl.detach();
    }

    public void flush() {
    }

    public <T extends BlockPayload> T readFirst(Class<T> payloadType) {
        return read(new BlockPointer(0), payloadType);
    }

    public <T extends BlockPayload> T read(BlockPointer pos, Class<T> payloadType) {
        assert !pos.isNull();
        try {
            T payload = payloadType.cast(factory.create(payloadType));
            BlockImpl block = new BlockImpl(payload, pos);
            block.read();
            return payload;
        } catch (CorruptedCacheException e) {
            throw e;
        } catch (Exception e) {
            throw new UncheckedIOException(e);
        }
    }

    public void write(BlockPayload block) {
        BlockImpl blockImpl = (BlockImpl) block.getBlock();
        try {
            blockImpl.write();
        } catch (CorruptedCacheException e) {
            throw e;
        } catch (Exception e) {
            throw new UncheckedIOException(e);
        }
    }

    private long alloc(long length) {
        long pos = nextBlock;
        nextBlock += length;
        return pos;
    }

    private final class BlockImpl extends Block {
        private static final int HEADER_SIZE = 2 + INT_SIZE;
        private static final int TAIL_SIZE = LONG_SIZE;
        static final int BLOCK_MARKER = 0xCC;

        private BlockPointer pos;
        private int payloadSize;

        private BlockImpl(BlockPayload payload, BlockPointer pos) {
            this(payload);
            setPos(pos);
        }

        public BlockImpl(BlockPayload payload) {
            super(payload);
            pos = null;
            payloadSize = -1;
        }

        @Override
        public boolean hasPos() {
            return pos != null;
        }

        @Override
        public BlockPointer getPos() {
            if (pos == null) {
                pos = new BlockPointer(alloc(getSize()));
            }
            return pos;
        }

        @Override
        public void setPos(BlockPointer pos) {
            assert this.pos == null && !pos.isNull();
            this.pos = pos;
        }

        public int getSize() {
            if (payloadSize < 0) {
                payloadSize = getPayload().getSize();
            }
            return payloadSize + HEADER_SIZE + TAIL_SIZE;
        }

        @Override
        public void setSize(int size) {
            int newPayloadSize = size - HEADER_SIZE - TAIL_SIZE;
            assert newPayloadSize >= payloadSize;
            payloadSize = newPayloadSize;
        }

        public void write() throws Exception {
            long pos = getPos().getPos();
            file.seek(pos);

            Crc32OutputStream checkSumOutputStream = new Crc32OutputStream(new BufferedOutputStream(
                    new RandomAccessFileOutputStream(file)));
            DataOutputStream outputStream = new DataOutputStream(checkSumOutputStream);

            BlockPayload payload = getPayload();

            // Write header
            outputStream.writeByte(BLOCK_MARKER);
            outputStream.writeByte(payload.getType());
            outputStream.writeInt(payloadSize);
            long finalSize = pos + HEADER_SIZE + TAIL_SIZE + payloadSize;

            // Write body
            payload.write(outputStream);

            // Write checksum
            outputStream.writeLong(checkSumOutputStream.checksum.getValue());
            outputStream.close();

            // Pad
            if (file.length() < finalSize) {
                file.setLength(finalSize);
            }
        }

        public void read() throws Exception {
            long pos = getPos().getPos();
            assert pos >= 0;
            if (pos + HEADER_SIZE >= file.length()) {
                throw blockCorruptedException();
            }
            file.seek(pos);

            Crc32InputStream checkSumInputStream = new Crc32InputStream(new BufferedInputStream(
                    new RandomAccessFileInputStream(file)));
            DataInputStream inputStream = new DataInputStream(checkSumInputStream);

            BlockPayload payload = getPayload();

            // Read header
            byte type = inputStream.readByte();
            if (type != (byte) BLOCK_MARKER) {
                throw blockCorruptedException();
            }
            type = inputStream.readByte();
            if (type != (byte) payload.getType()) {
                throw blockCorruptedException();
            }

            // Read body
            payloadSize = inputStream.readInt();
            if (pos + HEADER_SIZE + TAIL_SIZE + payloadSize > file.length()) {
                throw blockCorruptedException();
            }
            payload.read(inputStream);

            // Read and verify checksum
            long actualChecksum = checkSumInputStream.checksum.getValue();
            long checksum = inputStream.readLong();
            if (actualChecksum != checksum) {
                throw blockCorruptedException();
            }
            inputStream.close();
        }

        public RuntimeException blockCorruptedException() {
            return new CorruptedCacheException(String.format("Corrupted %s found in %s.", this,
                    FileBackedBlockStore.this));
        }
    }

    private static class RandomAccessFileInputStream extends InputStream {
        private final RandomAccessFile file;

        private RandomAccessFileInputStream(RandomAccessFile file) {
            this.file = file;
        }

        @Override
        public int read(byte[] bytes) throws IOException {
            return file.read(bytes);
        }

        @Override
        public int read() throws IOException {
            return file.read();
        }

        @Override
        public int read(byte[] bytes, int offset, int length) throws IOException {
            return file.read(bytes, offset, length);
        }
    }

    private static class RandomAccessFileOutputStream extends OutputStream {
        private final RandomAccessFile file;

        private RandomAccessFileOutputStream(RandomAccessFile file) {
            this.file = file;
        }

        @Override
        public void write(int i) throws IOException {
            file.write(i);
        }

        @Override
        public void write(byte[] bytes) throws IOException {
            file.write(bytes);
        }

        @Override
        public void write(byte[] bytes, int offset, int length) throws IOException {
            file.write(bytes, offset, length);
        }
    }

    private static class Crc32InputStream extends FilterInputStream {
        private final CRC32 checksum;

        private Crc32InputStream(InputStream inputStream) {
            super(inputStream);
            checksum = new CRC32();
        }

        @Override
        public int read() throws IOException {
            int b = in.read();
            if (b >= 0) {
                checksum.update(b);
            }
            return b;
        }

        @Override
        public int read(byte[] bytes) throws IOException {
            int count = in.read(bytes);
            if (count > 0) {
                checksum.update(bytes, 0, count);
            }
            return count;
        }

        @Override
        public int read(byte[] bytes, int offset, int max) throws IOException {
            int count = in.read(bytes, offset, max);
            if (count > 0) {
                checksum.update(bytes, offset, count);
            }
            return count;
        }
    }

    private static class Crc32OutputStream extends FilterOutputStream {
        private final CRC32 checksum;

        private Crc32OutputStream(OutputStream outputStream) {
            super(outputStream);
            this.checksum = new CRC32();
        }

        @Override
        public void write(int b) throws IOException {
            checksum.update(b);
            out.write(b);
        }

        @Override
        public void write(byte[] bytes) throws IOException {
            checksum.update(bytes);
            out.write(bytes);
        }

        @Override
        public void write(byte[] bytes, int offset, int count) throws IOException {
            checksum.update(bytes, offset, count);
            out.write(bytes, offset, count);
        }
    }
}
