/*
 *  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 org.apache.harmony.nio.internal;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;

/**
 * Static methods for I/O util. Used by io package and nio package.
 */
public final class IOUtil {

    private static final int DEFAULT_BUFFER_SIZE = 8192;

    /*
     * Not designed to be instantiated.
     */
    private IOUtil() {
        super();
    }

    /*
     * Read method for InputStreamReader and Channels.
     */
    public static int readInputStreamReader(InputStream in, ByteBuffer bytes,
            CharBuffer chars, CharsetDecoder decoder, Object lock)
            throws IOException {
        synchronized (lock) {
            if (in != null) {
                if (chars.limit() == chars.position()) {
                    fillBuf(in, bytes, chars, decoder);
                }
                if (chars.limit() == 0) {
                    return -1;
                }
                return chars.get();
            }
            throw new IOException("InputStreamReader is closed");
        }
    }

    private static void assertArrayIndex(int arrayLength, int offset, int length) {
        if (offset < 0 || length < 0) {
            throw new IndexOutOfBoundsException("Negative index specified");
        }
        if ((long) offset + (long) length > arrayLength) {
            throw new IndexOutOfBoundsException("Size mismatch");
        }
    }

    /*
     * Read method for InputStreamReader and Channels.
     */
    public static int readInputStreamReader(char[] buf, int offset, int length,
            InputStream in, ByteBuffer bytes, CharBuffer chars,
            CharsetDecoder decoder, Object lock) throws IOException {
        synchronized (lock) {
            if (in != null) {
                if (length == 0) {
                    return 0;
                }
                assertArrayIndex(buf.length, offset, length);
                // read at least once
                if (chars.limit() == chars.position()) {
                    fillBuf(in, bytes, chars, decoder);
                }
                int position = chars.position();
                int availableChars = chars.limit() - position;
                // read at least once for one byte
                int needChars = length;
                while (availableChars < needChars) {
                    System.arraycopy(chars.array(), position, buf, offset,
                            availableChars);
                    chars.position(position + availableChars);
                    needChars -= availableChars;
                    offset += availableChars;
                    if (in.available() <= 0) {
                        return needChars == length ? -1 : length - needChars;
                    }
                    fillBuf(in, bytes, chars, decoder);
                    position = chars.position();
                    availableChars = chars.limit();
                    if (availableChars == 0) {
                        return needChars == length ? -1 : length - needChars;
                    }
                }
                System.arraycopy(chars.array(), position, buf, offset,
                        needChars);
                chars.position(chars.position() + needChars);
                return length;
            }
            throw new IOException("InputStreamReader is closed");
        }
    }

    /*
     * Refill the buffer from wrapped InputStream.
     */
    private static void fillBuf(InputStream in, ByteBuffer bytes,
            CharBuffer chars, CharsetDecoder decoder) throws IOException {
        chars.clear();
        int read = 0;
        try {
            read = in.read(bytes.array());
        } catch (IOException e) {
            chars.limit(0);
            throw e;
        }
        if (read == -1) {
            chars.limit(0);
            return;
        }
        bytes.limit(read);
        boolean endOfInput = read < DEFAULT_BUFFER_SIZE;
        CoderResult result = decoder.decode(bytes, chars, endOfInput);
        if (result.isError()) {
            throw new IOException(result.toString());
        }
        bytes.clear();
        chars.flip();
    }

    /*
     * Write method for OutputStreamWriter and Channels.
     */
    public static void writeOutputStreamWriter(String str, int offset,
            int count, OutputStream out, ByteBuffer bytes,
            CharsetEncoder encoder, Object lock) throws IOException {
        assertArrayIndex(str.length(), offset, count);
        CharBuffer chars = CharBuffer.wrap(str, offset, count + offset);
        convert(lock, encoder, bytes, chars, out);
    }

    private static void checkEncoder(CharsetEncoder encoder) throws IOException {
        if (encoder == null) {
            throw new IOException("Writer is closed");
        }
    }

    /*
     * Write method for OutputStreamWriter and Channels.
     */
    public static void writeOutputStreamWriter(int oneChar, OutputStream out,
            ByteBuffer bytes, CharsetEncoder encoder, Object lock)
            throws IOException {
        synchronized (lock) {
            checkEncoder(encoder);
            CharBuffer chars = CharBuffer.wrap(new char[] { (char) oneChar });
            convert(lock, encoder, bytes, chars, out);
        }
    }

    /*
     * Write method for OutputStreamWriter and Channels.
     */
    public static void writeOutputStreamWriter(char[] buf, int offset,
            int count, OutputStream out, ByteBuffer bytes,
            CharsetEncoder encoder, Object lock) throws IOException {
        assertArrayIndex(buf.length, offset, count);
        CharBuffer chars = CharBuffer.wrap(buf, offset, count);
        convert(lock, encoder, bytes, chars, out);
    }

    /*
     * Flush method for OutputStreamWriter and Channels.
     */
    public static void flushOutputStreamWriter(OutputStream out,
            ByteBuffer bytes, CharsetEncoder encoder, Object lock)
            throws IOException {
        synchronized (lock) {
            checkEncoder(encoder);
            int position;
            if ((position = bytes.position()) > 0) {
                bytes.flip();
                out.write(bytes.array(), 0, position);
                bytes.clear();
            }
            out.flush();
        }
    }

    /*
     * Convert function used in write.
     */
    private static void convert(Object lock, CharsetEncoder encoder,
            ByteBuffer bytes, CharBuffer chars, OutputStream out)
            throws IOException {
        synchronized (lock) {
            checkEncoder(encoder);
            CoderResult result = encoder.encode(chars, bytes, true);
            while (true) {
                if (result.isError()) {
                    throw new IOException(result.toString());
                } else if (result.isOverflow()) {
                    // flush the output buffer
                    flushOutputStreamWriter(out, bytes, encoder, lock);
                    result = encoder.encode(chars, bytes, true);
                    continue;
                }
                break;
            }
        }
    }
}
