package com.maxmind.db;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;

import com.maxmind.db.Reader.FileMode;

final class BufferHolder {
    // DO NOT PASS OUTSIDE THIS CLASS. Doing so will remove thread safety.
    private final ByteBuffer buffer;

    BufferHolder(File database, FileMode mode) throws IOException {
        try (
                final RandomAccessFile file = new RandomAccessFile(database, "r");
                final FileChannel channel = file.getChannel()
        ) {
            if (mode == FileMode.MEMORY) {
                final ByteBuffer buf = ByteBuffer.wrap(new byte[(int) channel.size()]);
                if (channel.read(buf) != buf.capacity()) {
                    throw new IOException("Unable to read "
                            + database.getName()
                            + " into memory. Unexpected end of stream.");
                }
                this.buffer = buf.asReadOnlyBuffer();
            } else {
                this.buffer = channel.map(MapMode.READ_ONLY, 0, channel.size()).asReadOnlyBuffer();
            }
        }
    }

    /**
     * Construct a ThreadBuffer from the provided URL.
     *
     * @param stream the source of my bytes.
     * @throws IOException          if unable to read from your source.
     * @throws NullPointerException if you provide a NULL InputStream
     */
    BufferHolder(InputStream stream) throws IOException {
        if (null == stream) {
            throw new NullPointerException("Unable to use a NULL InputStream");
        }
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        final byte[] bytes = new byte[16 * 1024];
        int br;
        while (-1 != (br = stream.read(bytes))) {
            baos.write(bytes, 0, br);
        }
        this.buffer = ByteBuffer.wrap(baos.toByteArray()).asReadOnlyBuffer();
    }

    /*
     * Returns a duplicate of the underlying ByteBuffer. The returned ByteBuffer
     * should not be shared between threads.
     */
    ByteBuffer get() {
        // The Java API docs for buffer state:
        //
        //     Buffers are not safe for use by multiple concurrent threads. If a buffer is to be used by more than
        //     one thread then access to the buffer should be controlled by appropriate synchronization.
        //
        // As such, you may think that this should be synchronized. This used to be the case, but we had several
        // complaints about the synchronization causing contention, e.g.:
        //
        // * https://github.com/maxmind/MaxMind-DB-Reader-java/issues/65
        // * https://github.com/maxmind/MaxMind-DB-Reader-java/pull/69
        //
        // Given that we are not modifying the original ByteBuffer in any way and all currently known and most
        // reasonably imaginable implementations of duplicate() only do read operations on the original buffer object,
        // the risk of not synchronizing this call seems relatively low and worth taking for the performance benefit
        // when lookups are being done from many threads.
        return this.buffer.duplicate();
    }
}