/*
 * JBoss, Home of Professional Open Source
 * Copyright 2012, Red Hat Middleware LLC, and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * 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.jboss.shrinkwrap.api.nio.file;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SeekableByteChannel;

/**
 * {@link SeekableByteChannel} implementation backed by an auto-resizing byte array; thread-safe. Can hold a maxiumum of
 * {@link Integer#MAX_VALUE} bytes.
 *
 * @author <a href="mailto:alr@jboss.org">Andrew Lee Rubinger</a>
 */
public class SeekableInMemoryByteChannel implements SeekableByteChannel {

    /**
     * Current position; guarded by "this"
     */
    private int position;

    /**
     * Whether or not this {@link SeekableByteChannel} is open; volatile instead of sync is acceptable because this
     * field participates in no compound computations or invariants with other instance members.
     */
    private volatile boolean open;

    /**
     * Internal buffer for contents; guarded by "this"
     */
    private byte[] contents;

    /**
     * Creates a new instance with 0 size and 0 position, and open.
     */
    public SeekableInMemoryByteChannel() {
        this.open = true;

        // Set fields
        synchronized (this) {
            this.position = 0;
            this.contents = new byte[0];
        }
    }

    /**
     * {@inheritDoc}
     *
     * @see java.nio.channels.Channel#isOpen()
     */
    @Override
    public boolean isOpen() {
        return this.open;
    }

    /**
     * {@inheritDoc}
     *
     * @see java.nio.channels.Channel#close()
     */
    @Override
    public void close() throws IOException {
        this.open = false;
    }

    /**
     * {@inheritDoc}
     *
     * @see java.nio.channels.SeekableByteChannel#read(java.nio.ByteBuffer)
     */
    @Override
    public int read(final ByteBuffer destination) throws IOException {

        // Precondition checks
        this.checkClosed();
        if (destination == null) {
            throw new IllegalArgumentException("Destination buffer must be supplied");
        }

        // Init
        final int spaceInBuffer = destination.remaining();
        final int numBytesRemainingInContent, numBytesToRead;

        // Sync up before getting at shared mutable state
        synchronized (this) {
            numBytesRemainingInContent = this.contents.length - this.position;

            // Set position was greater than the size? Just return.
            if (numBytesRemainingInContent <= 0) {
                return -1;
            }

            // We'll read in either the number of bytes remaining in content or the amount of space in the buffer,
            // whichever is smaller
            numBytesToRead = numBytesRemainingInContent >= spaceInBuffer ? spaceInBuffer : numBytesRemainingInContent;
            // Copy a sub-array of the bytes we'll put into the buffer
            destination.put(this.contents, this.position, numBytesToRead);

            // Set the new position
            this.position += numBytesToRead;
        }

        // Return the number of bytes read
        return numBytesToRead;
    }

    /**
     * {@inheritDoc}
     *
     * @see java.nio.channels.SeekableByteChannel#write(java.nio.ByteBuffer)
     */
    @Override
    public int write(final ByteBuffer source) throws IOException {

        // Precondition checks
        this.checkClosed();
        if (source == null) {
            throw new IllegalArgumentException("Source buffer must be supplied");
        }

        // Put the bytes to be written into a byte[]
        final int totalBytes = source.remaining();
        final byte[] readContents = new byte[totalBytes];
        source.get(readContents);

        // Sync up, we're gonna access shared mutable state
        synchronized (this) {

            // Append the read contents to our internal contents
            this.contents = this.concat(this.contents, readContents, this.position);

            // Increment the position of this channel
            this.position += totalBytes;

        }

        // Return the number of bytes read
        return totalBytes;
    }

    /**
     * Creates a new array which is the concatenated result of the two inputs, at the designated position (to be filled
     * with 0x00) in the case of a gap).
     *
     * @param input1
     * @param input2
     * @param position
     * @return
     */
    private byte[] concat(final byte[] input1, final byte[] input2, final int position) {
        // Preconition checks
        assert input1 != null : "Input 1 must be specified";
        assert input2 != null : "Input 2 must be specified";
        assert position >= 0 : "Position must be 0 or higher";
        // Allocate a new array of enough space (either current size or position + input2.length, whichever is greater)
        final int newSize = position < input1.length ? input1.length + input2.length : position + input2.length;
        final byte[] merged = new byte[newSize];
        // Copy in the contents of input 1 with 0 offset
        System.arraycopy(input1, 0, merged, 0, input1.length);
        // Copy in the contents of input2 with offset the length of input 1
        System.arraycopy(input2, 0, merged, position, input2.length);
        return merged;
    }

    /**
     * {@inheritDoc}
     *
     * @see java.nio.channels.SeekableByteChannel#position()
     */
    @Override
    public long position() throws IOException {
        synchronized (this) {
            return this.position;
        }
    }

    /**
     * {@inheritDoc}
     *
     * @see java.nio.channels.SeekableByteChannel#position(long)
     */
    @Override
    public SeekableByteChannel position(final long newPosition) throws IOException {
        // Precondition checks
        if (newPosition > Integer.MAX_VALUE || newPosition < 0) {
            throw new IllegalArgumentException("Valid position for this channel is between 0 and " + Integer.MAX_VALUE);
        }
        synchronized (this) {
            this.position = (int) newPosition;
        }
        return this;
    }

    /**
     * {@inheritDoc}
     *
     * @see java.nio.channels.SeekableByteChannel#size()
     */
    @Override
    public long size() throws IOException {
        synchronized (this) {
            return this.contents.length;
        }
    }

    /**
     * {@inheritDoc}
     *
     * @see java.nio.channels.SeekableByteChannel#truncate(long)
     */
    @Override
    public SeekableByteChannel truncate(final long size) throws IOException {

        // Precondition checks
        if (size < 0 || size > Integer.MAX_VALUE) {
            throw new IllegalArgumentException("This implementation permits a size of 0 to " + Integer.MAX_VALUE
                + " inclusive");
        }

        // Sync up for mucking w/ shared mutable state
        synchronized (this) {

            final int newSize = (int) size;
            final int currentSize = (int) this.size();

            // If the current position is greater than the given size, set to the given size (by API spec)
            if (this.position > newSize) {
                this.position = newSize;
            }

            // If we've been given a size smaller than we currently are
            if (currentSize > newSize) {
                // Make new array
                final byte[] newContents = new byte[newSize];
                // Copy in the contents up to the new size
                System.arraycopy(this.contents, 0, newContents, 0, newSize);
                // Set the new array as our contents
                this.contents = newContents;
            }

            // If we've been given a size greater than or equal to us then do nothing
            // if (newSize >= currentSize) {
            // // do nothing
            // }
        }

        // Return this reference
        return this;
    }

    /**
     * Obtain a copy of the contents of this {@link Channel} as an {@link InputStream}
     *
     * @return
     */
    InputStream getContents() {
        final byte[] copy;
        synchronized (this) {
            final int length = this.contents.length;
            copy = new byte[length];
            System.arraycopy(this.contents, 0, copy, 0, this.contents.length);
        }
        return new ByteArrayInputStream(copy);
    }

    /**
     * Throws a {@link ClosedChannelException} if this {@link SeekableByteChannel} is closed.
     *
     * @throws ClosedChannelException
     */
    private void checkClosed() throws ClosedChannelException {
        if (!this.isOpen()) {
            throw new ClosedChannelException();
        }
    }
}
