/**
 * **** BEGIN LICENSE BLOCK *****
 * Version: CPL 1.0/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Common Public
 * License Version 1.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.eclipse.org/legal/cpl-v10.html
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 *
 * Copyright (C) 2009 Yoko Harada <yokolet@gmail.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either of the GNU General Public License Version 2 or later (the "GPL"),
 * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the CPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the CPL, the GPL or the LGPL.
 * **** END LICENSE BLOCK *****
 */
package org.jruby.embed.io;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;

/**
 * A WriterOutputStream converts java.io.Writer to java.io.OutputStream.
 *
 * @author Yoko Harada <yokolet@gmail.com>
 */
public class WriterOutputStream extends OutputStream {

    private final Writer writer;
    private boolean isOpen = true;
    private CharsetDecoder decoder;

    /**
     * Creates WriterOutputStream from given java.io.Writer object with a default encoding.
     *
     * @param writer java.io.Writer object to be converted to.
     */
    public WriterOutputStream(Writer writer) {
        this(writer, null);
    }

    /**
     * Creates WriterOutputStream from given java.io.Writer object with a specified encoding.
     *
     * @param writer java.io.Writer object to be converted to.
     */
    public WriterOutputStream(Writer writer, String encoding) {
        this.writer = writer;       
        if (encoding == null && writer instanceof OutputStreamWriter) {
            // this encoding might be null when writer has been closed
            encoding = ((OutputStreamWriter) writer).getEncoding();
        }
        if (encoding == null) {
            encoding = Charset.defaultCharset().name();
        } else if (!Charset.isSupported(encoding)) {
            throw new IllegalArgumentException(encoding + " is not supported");
        }
        decoder = Charset.forName(encoding).newDecoder();
        decoder.onMalformedInput(CodingErrorAction.REPLACE);
        decoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
    }

    /**
     * Closes this output stream and releases any system resources
     * associated with this stream. The general contract of <code>close</code>
     * is that it closes the output stream. A closed stream cannot perform
     * output operations and cannot be reopened.
     * <p>
     * 
     * @exception  IOException  if an I/O error occurs.
     */
    @Override
    public void close() throws IOException {
        synchronized (writer) {
            if (!isOpen) {
                throw new IOException("This stream has been already closed.");
            }
            isOpen = false;
            decoder = null;
            writer.close();
        }
    }

    /**
     * Flushes this output stream and forces any buffered output bytes
     * to be written out. The general contract of <code>flush</code> is
     * that calling it is an indication that, if any bytes previously
     * written have been buffered by the implementation of the output
     * stream, such bytes should immediately be written to their
     * intended destination.
     * <p>
     * If the intended destination of this stream is an abstraction provided by
     * the underlying operating system, for example a file, then flushing the
     * stream guarantees only that bytes previously written to the stream are
     * passed to the operating system for writing; it does not guarantee that
     * they are actually written to a physical device such as a disk drive.
     * <p>
     *
     * @exception  IOException  if an I/O error occurs.
     */
    @Override
    public void flush() throws IOException {
        synchronized (writer) {
            if (!isOpen) {
                return;
            }
            writer.flush();
        }
    }

    /**
     * Writes the specified byte to this output stream. The general
     * contract for <code>write</code> is that one byte is written
     * to the output stream. The byte to be written is the eight
     * low-order bits of the argument <code>b</code>. The 24
     * high-order bits of <code>b</code> are ignored.
     *
     * @param      b   the <code>byte</code>.
     * @exception  IOException  if an I/O error occurs. In particular,
     *             an <code>IOException</code> may be thrown if the
     *             output stream has been closed.
     */
    @Override
    public void write(int b) throws IOException {
        byte[] bb = new byte[]{(byte) b};
        write(bb, 0, 1);
    }

    /**
     * Writes <code>b.length</code> bytes from the specified byte array
     * to this output stream. The general contract for <code>write(b)</code>
     * is that it should have exactly the same effect as the call
     * <code>write(b, 0, b.length)</code>.
     *
     * @param      b   the data.
     * @exception  IOException  if an I/O error occurs.
     * @see        java.io.OutputStream#write(byte[], int, int)
     */
    @Override
    public void write(byte[] b) throws IOException {
        write(b, 0, b.length);
    }

    /**
     * Writes <code>len</code> bytes from the specified byte array
     * starting at offset <code>off</code> to this output stream.
     * The general contract for <code>write(b, off, len)</code> is that
     * some of the bytes in the array <code>b</code> are written to the
     * output stream in order; element <code>b[off]</code> is the first
     * byte written and <code>b[off+len-1]</code> is the last byte written
     * by this operation.
     * <p>
     * If <code>off</code> is negative, or <code>len</code> is negative, or
     * <code>off+len</code> is greater than the length of the array
     * <code>b</code>, then an <tt>IndexOutOfBoundsException</tt> is thrown.
     *
     * @param      b     the data.
     * @param      off   the start offset in the data.
     * @param      len   the number of bytes to write.
     * @exception  IOException  if an I/O error occurs. In particular,
     *             an <code>IOException</code> is thrown if the output
     *             stream is closed.
     */
    @Override
    public void write(byte[] b, int off, int len) throws IOException {
        synchronized (writer) {
            if (!isOpen) {
                return;
            }
            if (off < 0 || len <= 0 || (off + len) > b.length) {
                throw new IndexOutOfBoundsException();
            }
            ByteBuffer bytes = ByteBuffer.wrap(b, off, len);
            CharBuffer chars = CharBuffer.allocate(len);
            byte2char(bytes, chars);
            char[] cbuf = new char[chars.length()];
            chars.get(cbuf, 0, chars.length());
            writer.write(cbuf);
            writer.flush();
        }
    }

    private void byte2char(ByteBuffer bytes, CharBuffer chars) throws IOException {
        decoder.reset();
        chars.clear();
        CoderResult result = decoder.decode(bytes, chars, true);
        if (result.isError() || result.isOverflow()) {
            throw new IOException(result.toString());
        } else if (result.isUnderflow()) {
            chars.flip();
        }
    }
}
