/*
 *  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 java.nio.channels;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.spi.AbstractInterruptibleChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import org.apache.harmony.nio.internal.IOUtil;

/**
 * This class provides several utilities to get I/O streams from channels.
 */
public final class Channels {

    /*
     * Not intended to be instantiated.
     */
    private Channels() {
        super();
    }

    /**
     * Returns an input stream on the given channel. The resulting stream has
     * the following properties:
     * <ul>
     * <li>If the stream is closed, then the underlying channel is closed as
     * well.</li>
     * <li>It is thread safe.</li>
     * <li>It throws an {@link IllegalBlockingModeException} if the channel is
     * in non-blocking mode and {@code read} is called.</li>
     * <li>Neither {@code mark} nor {@code reset} is supported.</li>
     * <li>It is not buffered.</li>
     * </ul>
     *
     * @param channel
     *            the channel to be wrapped by an InputStream.
     * @return an InputStream that takes bytes from the given byte channel.
     */
    public static InputStream newInputStream(ReadableByteChannel channel) {
        return new ReadableByteChannelInputStream(channel);
    }

    /**
     * Returns an output stream on the given channel. The resulting stream has
     * the following properties:
     * <ul>
     * <li>If the stream is closed, then the underlying channel is closed as
     * well.</li>
     * <li>It is thread safe.</li>
     * <li>It throws an {@link IllegalBlockingModeException} if the channel is
     * in non-blocking mode and {@code write} is called.</li>
     * <li>It is not buffered.</li>
     * </ul>
     *
     * @param channel
     *            the channel to be wrapped by an OutputStream.
     * @return an OutputStream that puts bytes onto the given byte channel.
     */
    public static OutputStream newOutputStream(WritableByteChannel channel) {
        return new WritableByteChannelOutputStream(channel);
    }

    /**
     * Returns a readable channel on the given input stream. The resulting
     * channel has the following properties:
     * <ul>
     * <li>If the channel is closed, then the underlying stream is closed as
     * well.</li>
     * <li>It is not buffered.</li>
     * </ul>
     *
     * @param inputStream
     *            the stream to be wrapped by a byte channel.
     * @return a byte channel that reads bytes from the input stream.
     */
    public static ReadableByteChannel newChannel(InputStream inputStream) {
        return new ReadableByteChannelImpl(inputStream);
    }

    /**
     * Returns a writable channel on the given output stream.
     *
     * The resulting channel has following properties:
     * <ul>
     * <li>If the channel is closed, then the underlying stream is closed as
     * well.</li>
     * <li>It is not buffered.</li>
     * </ul>
     *
     * @param outputStream
     *            the stream to be wrapped by a byte channel.
     * @return a byte channel that writes bytes to the output stream.
     */
    public static WritableByteChannel newChannel(OutputStream outputStream) {
        return new WritableByteChannelImpl(outputStream);
    }

    /**
     * Returns a reader that decodes bytes from a channel.
     *
     * @param channel
     *            the Channel to be read.
     * @param decoder
     *            the Charset decoder to be used.
     * @param minBufferCapacity
     *            The minimum size of the byte buffer, -1 means to use the
     *            default size.
     * @return the reader.
     */
    public static Reader newReader(ReadableByteChannel channel,
            CharsetDecoder decoder, int minBufferCapacity) {
        return new ByteChannelReader(new ReaderInputStream(channel), decoder,
                minBufferCapacity);
    }

    /**
     * Returns a reader that decodes bytes from a channel. This method creates a
     * reader with a buffer of default size.
     *
     * @param channel
     *            the Channel to be read.
     * @param charsetName
     *            the name of the charset.
     * @return the reader.
     * @throws java.nio.charset.UnsupportedCharsetException
     *             if the given charset name is not supported.
     */
    public static Reader newReader(ReadableByteChannel channel,
            String charsetName) {
        return newReader(channel, Charset.forName(charsetName).newDecoder(), -1);
    }

    /**
     * Returns a writer that encodes characters with the specified
     * {@code encoder} and sends the bytes to the specified channel.
     *
     * @param channel
     *            the Channel to write to.
     * @param encoder
     *            the CharsetEncoder to be used.
     * @param minBufferCapacity
     *            the minimum size of the byte buffer, -1 means to use the
     *            default size.
     * @return the writer.
     */
    public static Writer newWriter(WritableByteChannel channel,
            CharsetEncoder encoder, int minBufferCapacity) {
        return new ByteChannelWriter(new WritableByteChannelOutputStream(
                channel), encoder, minBufferCapacity);
    }

    /**
     * Returns a writer that encodes characters with the specified
     * {@code encoder} and sends the bytes to the specified channel. This method
     * creates a writer with a buffer of default size.
     *
     * @param channel
     *            the Channel to be written to.
     * @param charsetName
     *            the name of the charset.
     * @return the writer.
     * @throws java.nio.charset.UnsupportedCharsetException
     *             if the given charset name is not supported.
     */
    public static Writer newWriter(WritableByteChannel channel,
            String charsetName) {
        return newWriter(channel, Charset.forName(charsetName).newEncoder(), -1);
    }

    /*
     * wrap a byte array to a ByteBuffer
     */
    static ByteBuffer wrapByteBuffer(byte[] bytes, int offset, int length) {
        ByteBuffer buffer = ByteBuffer.wrap(bytes);
        int newLimit = offset + length <= buffer.capacity() ? offset + length
                : buffer.capacity();
        buffer.limit(newLimit);
        buffer.position(offset);
        return buffer;
    }

    private static class ChannelInputStream extends InputStream {

        protected ReadableByteChannel channel;

        public ChannelInputStream(ReadableByteChannel aChannel) {
            super();
            channel = aChannel;
        }

        @Override
        public synchronized int read() throws IOException {
            byte[] oneByte = new byte[1];
            int n = read(oneByte);
            if (n == 1) {
                // reads a single byte 0-255
                return oneByte[0] & 0xff;
            }
            return -1;
        }

        @Override
        public synchronized void close() throws IOException {
            channel.close();
        }
    }

    /*
     * Wrapper class used for newInputStream(ReadableByteChannel channel)
     */
    private static class ReadableByteChannelInputStream extends ChannelInputStream {

        public ReadableByteChannelInputStream(ReadableByteChannel aChannel) {
            super(aChannel);
        }

        @Override
        public synchronized int read(byte[] target, int offset, int length)
                throws IOException {
            // avoid int overflow, check null target
            if (length + offset > target.length || length < 0 || offset < 0) {
                throw new ArrayIndexOutOfBoundsException();
            }
            if (0 == length) {
                return 0;
            }
            if (channel instanceof SelectableChannel) {
                if (!((SelectableChannel) channel).isBlocking()) {
                    throw new IllegalBlockingModeException();
                }
            }
            ByteBuffer buffer = ByteBuffer.wrap(target, offset, length);
            return channel.read(buffer);
        }
    }

    /*
     * Wrapper class used for newReader(ReadableByteChannel channel,
     * CharsetDecoder decoder, int minBufferCapacity)
     */
    private static class ReaderInputStream extends ChannelInputStream {

        public ReaderInputStream(ReadableByteChannel aChannel) {
            super(aChannel);
        }

        @Override
        public synchronized int read(byte[] target, int offset, int length)
                throws IOException {
            // avoid int overflow, check null target
            if (length + offset > target.length || length < 0 || offset < 0) {
                throw new ArrayIndexOutOfBoundsException();
            }
            if (0 == length) {
                return 0;
            }
            ByteBuffer buffer = ByteBuffer.wrap(target, offset, length);
            return channel.read(buffer);
        }
    }

    /*
     * Wrapper class used for newOutputStream(WritableByteChannel channel)
     */
    private static class WritableByteChannelOutputStream extends OutputStream {

        private WritableByteChannel channel;

        public WritableByteChannelOutputStream(WritableByteChannel aChannel) {
            super();
            channel = aChannel;
        }

        @Override
        public synchronized void write(int oneByte) throws IOException {
            byte[] wrappedByte = new byte[1];
            wrappedByte[0] = (byte) oneByte;
            write(wrappedByte);
        }

        @Override
        public synchronized void write(byte[] source, int offset, int length)
                throws IOException {
            // avoid int overflow, check null source
            if (length + offset > source.length || length < 0 || offset < 0) {
                throw new ArrayIndexOutOfBoundsException();
            }
            if (0 == length) {
                return;
            }
            if (channel instanceof SelectableChannel) {
                if (!((SelectableChannel) channel).isBlocking()) {
                    throw new IllegalBlockingModeException();
                }
            }
            ByteBuffer buffer = ByteBuffer.wrap(source, offset, length);
            channel.write(buffer);
        }

        @Override
        public synchronized void close() throws IOException {
            channel.close();
        }
    }

    /*
     * Wrapper class used for newChannel(InputStream inputStream)
     */
    private static class ReadableByteChannelImpl extends
            AbstractInterruptibleChannel implements ReadableByteChannel {
        private InputStream inputStream;

        ReadableByteChannelImpl(InputStream aInputStream) {
            super();
            inputStream = aInputStream;
        }

        public synchronized int read(ByteBuffer target) throws IOException {
            if (!isOpen()) {
                throw new ClosedChannelException();
            }
            int bytesRemain = target.remaining();
            byte[] bytes = new byte[bytesRemain];
            int readCount = 0;
            try {
                begin();
                readCount = inputStream.read(bytes);
            } finally {
                end(readCount >= 0);
            }
            if (readCount > 0) {
                target.put(bytes, 0, readCount);
            }
            return readCount;
        }

        @Override
        protected void implCloseChannel() throws IOException {
            inputStream.close();
        }
    }

    /*
     * Wrapper class used for newChannel(OutputStream outputStream)
     */
    private static class WritableByteChannelImpl extends
            AbstractInterruptibleChannel implements WritableByteChannel {
        private OutputStream outputStream;

        WritableByteChannelImpl(OutputStream aOutputStream) {
            super();
            outputStream = aOutputStream;
        }

        public synchronized int write(ByteBuffer source) throws IOException {
            if (!isOpen()) {
                throw new ClosedChannelException();
            }
            int bytesRemain = source.remaining();
            if (bytesRemain == 0) {
                return 0;
            }
            byte[] buf = new byte[bytesRemain];
            source.get(buf);
            try {
                begin();
                outputStream.write(buf, 0, bytesRemain);
            } finally {
                end(bytesRemain >= 0);
            }
            return bytesRemain;
        }

        @Override
        protected void implCloseChannel() throws IOException {
            outputStream.close();
        }
    }

    /*
     * Wrapper class used for newReader(ReadableByteChannel channel,
     * CharsetDecoder decoder, int minBufferCapacity)
     */
    private static class ByteChannelReader extends Reader {

        private InputStream inputStream;

        private static final int BUFFER_SIZE = 8192;

        CharsetDecoder decoder;

        ByteBuffer bytes;

        CharBuffer chars;

        public ByteChannelReader(InputStream aInputStream,
                CharsetDecoder aDecoder, int minBufferCapacity) {
            super(aInputStream);
            aDecoder.reset();
            inputStream = aInputStream;
            int bufferSize = Math.max(minBufferCapacity, BUFFER_SIZE);
            bytes = ByteBuffer.allocate(bufferSize);
            chars = CharBuffer.allocate(bufferSize);
            decoder = aDecoder;
            chars.limit(0);
        }

        @Override
        public void close() throws IOException {
            synchronized (lock) {
                decoder = null;
                if (inputStream != null) {
                    inputStream.close();
                    inputStream = null;
                }
            }
        }

        @Override
        public boolean ready() {
            synchronized (lock) {
                if (null == inputStream) {
                    return false;
                }
                try {
                    return chars.limit() > chars.position()
                            || inputStream.available() > 0;
                } catch (IOException e) {
                    return false;
                }
            }
        }

        @Override
        public int read() throws IOException {
            return IOUtil.readInputStreamReader(inputStream, bytes, chars,
                    decoder, lock);
        }

        @Override
        public int read(char[] buf, int offset, int length) throws IOException {
            return IOUtil.readInputStreamReader(buf, offset, length,
                    inputStream, bytes, chars, decoder, lock);
        }
    }

    /*
     * Wrapper class used for newWriter(WritableByteChannel channel,
     * CharsetEncoder encoder, int minBufferCapacity)
     */
    private static class ByteChannelWriter extends Writer {

        private static final int BUFFER_SIZE = 8192;

        private OutputStream outputStream;

        private CharsetEncoder encoder;

        private ByteBuffer byteBuf;

        public ByteChannelWriter(OutputStream aOutputStream,
                CharsetEncoder aEncoder, int minBufferCap) {
            super(aOutputStream);
            aEncoder.charset();
            outputStream = aOutputStream;
            byteBuf = ByteBuffer.allocate(Math.max(minBufferCap, BUFFER_SIZE));
            encoder = aEncoder;
        }

        @Override
        public void close() throws IOException {
            synchronized (lock) {
                if (encoder != null) {
                    flush();
                    outputStream.flush();
                    outputStream.close();
                    encoder = null;
                    byteBuf = null;
                }
            }
        }

        @Override
        public void flush() throws IOException {
            IOUtil
                    .flushOutputStreamWriter(outputStream, byteBuf, encoder,
                            lock);
        }

        @Override
        public void write(char[] buf, int offset, int count) throws IOException {
            IOUtil.writeOutputStreamWriter(buf, offset, count, outputStream,
                    byteBuf, encoder, lock);
        }

        @Override
        public void write(int oneChar) throws IOException {
            IOUtil.writeOutputStreamWriter(oneChar, outputStream, byteBuf,
                    encoder, lock);
        }

        @Override
        public void write(String str, int offset, int count) throws IOException {
            IOUtil.writeOutputStreamWriter(str, offset, count, outputStream,
                    byteBuf, encoder, lock);
        }
    }
}
