/*
 * @author Ranides Atterwim <ranides@gmail.com>
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.nio;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.ReadOnlyBufferException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.Charset;
import net.ranides.assira.generic.ValueUtils;
import net.ranides.assira.io.FileHelper;
import net.ranides.assira.io.RIOException;

/**
 * 
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public final class BufferAdapter {
    
    private BufferAdapter() {
        // utility class
    }

    public static SeekableByteChannel asChannel(ByteBuffer buffer) {
        return new BChannel(buffer);
    }
    
    public static ObjectInputStream asObjectInput(ByteBuffer buffer) {
        try {
            return new ObjectInputStream(asInputStream(buffer));
        } catch(IOException cause) {
            throw new RIOException(cause);
        }
    }
    
    public static ObjectOutputStream asObjectOutput(ByteBuffer buffer) {
        try {
            return new ObjectOutputStream(asOutputStream(buffer));
        } catch(IOException cause) {
            throw new RIOException(cause);
        }
    }
    
    public static InputStream asInputStream(ByteBuffer buffer) {
        return new BIStream(buffer);
    }
    
    public static OutputStream asOutputStream(ByteBuffer buffer) {
        return new BOStream(buffer);
    }
    
    public static Reader asReader(ByteBuffer buffer, Charset charset) {
        return asReader(charset.decode(buffer));
    }
    
    public static Reader asReader(CharBuffer buffer) {
        return new CReader(buffer);
    }
    
    /**
     * Returns unbuffered Writer which use charset encoder on the fly.
     * It's probably ineffective if you want to write char by char.
     * @param buffer
     * @param charset
     * @return 
     */
    public static Writer asWriter(ByteBuffer buffer, Charset charset) {
        return new BWriter(buffer, charset);
    }
    
    public static Writer asWriter(CharBuffer buffer) {
        return new CWriter(buffer);
    }
    
    private static class BChannel implements SeekableByteChannel {
        
        private ByteBuffer buffer;

        public BChannel(ByteBuffer buffer) {
            this.buffer = buffer;
        }
        
        @Override
        public boolean isOpen() {
            return buffer != null;
        }

        @Override
        public void close() throws IOException {
            nullcheck();
            FileHelper.close(buffer);
            buffer = null;
        }

        @Override
        public int read(ByteBuffer dst) throws IOException {
            nullcheck();
            if(buffer.remaining() == 0) {
                return -1;
            }
            int count = Math.min(buffer.remaining(), dst.remaining());
            int prv = buffer.limit();
            int max = buffer.position() + count;
            if( max != buffer.limit(max).limit() ) {
                throw new IOException("ByteBufferChannel can't set internal Buffer limits.");
            }
            dst.put(buffer);
            if(prv != buffer.limit(prv).limit()) {
                throw new IOException("ByteBufferChannel can't restore internal Buffer limits.");
            }
            return count;
        }

        @Override
        public int write(ByteBuffer src) throws IOException {
            try {
                nullcheck();
                int brem = buffer.remaining();
                if(brem == 0) {
                    throw new EOFException();
                }
                int srem = src.remaining();
                if( srem > brem) {
                    int prv = src.limit();
                    int max = src.position() + brem;
                    if(max != src.limit(max).limit()) {
                        throw new IOException("ByteBufferChannel can't set source Buffer limits.");
                    }
                    buffer.put(src);
                    if(prv != src.limit(prv).limit()) {
                        throw new IOException("ByteBufferChannel can't restore source Buffer limits.");
                    }
                    return brem;
                } else {
                    buffer.put(src);
                    return srem;
                }
            } catch(BufferOverflowException | ReadOnlyBufferException cause) {
                throw new IOException(cause);
            }
        }

        @Override
        public long position() throws IOException {
            nullcheck();
            return buffer.position();
        }

        @Override
        public SeekableByteChannel position(long newPosition) throws IOException {
            nullcheck();
            assert newPosition == (int)newPosition;
            if( newPosition < buffer.limit() ) {
                buffer.position((int)newPosition);
            } else {
                buffer.position(buffer.limit());
            }
            return this;
        }

        @Override
        public long size() throws IOException {
            nullcheck();
            return buffer.limit();
        }

        @Override
        public SeekableByteChannel truncate(long size) throws IOException {
            nullcheck();
            assert size == (int)size;
            if( size < buffer.limit()) {
                buffer.limit((int)size);
            }
            return this;
        }
        
        private void nullcheck() throws IOException  {
            if(null == buffer) {
                throw new ClosedChannelException();
            }
        }
        
    }
    
    private static class BIStream extends InputStream {
        
        private ByteBuffer buffer;

        public BIStream(ByteBuffer buffer) {
            this.buffer = buffer;
        }

        @Override
        public void close() throws IOException {
            nullcheck();
            FileHelper.close(buffer);
            buffer = null;
        }
        
        @Override
        public int read() throws IOException {
            nullcheck();
            try {
                return buffer.get();
            } catch(BufferUnderflowException cause) {
                throw new IOException(cause);
            }
        }
        
        @Override
        public int read(byte[] target, int offset, int len) throws IOException {
            nullcheck();
            try {
                buffer.get(target, offset, len);
                return len;
            } catch(BufferUnderflowException cause) {
                throw new IOException(cause);
            }
        }

        @Override
        public int available() throws IOException {
            nullcheck();
            return buffer.remaining();
        }

        @Override
        public long skip(long n) throws IOException {
            nullcheck();
            int np = buffer.position()+(int)n;
            return np == buffer.position(np).position() ? n : 0;
        }
        
        private void nullcheck() throws IOException  {
            if(null == buffer) {
                throw new IOException("InputStream is closed");
            }
        }
    
    }
    
    private static class BOStream extends OutputStream {
        
        private ByteBuffer buffer;

        public BOStream(ByteBuffer buffer) {
            this.buffer = buffer;
        }

        @Override
        public void close() throws IOException {
            nullcheck();
            FileHelper.close(buffer);
            buffer = null;
        }
        
        @Override
        public void flush() throws IOException {
            nullcheck();
        }
        
        @Override
        public void write(int b) throws IOException {
            nullcheck();
            try {
                buffer.put( (byte)b );
            } catch(BufferUnderflowException | ReadOnlyBufferException cause) {
                throw new IOException(cause);
            }
        }

        @Override
        public void write(byte[] bytes, int offset, int len) throws IOException {
            nullcheck();
            try {
                buffer.put(bytes, offset, len);
            } catch(BufferUnderflowException | ReadOnlyBufferException cause) {
                throw new IOException(cause);
            }
        }
        
        private void nullcheck() throws IOException  {
            if(null == buffer) {
                throw new IOException("OutputStream is closed");
            }
        }
        
    }
    
    private static class CReader extends Reader {

        private CharBuffer buffer;

        public CReader(CharBuffer buffer) {
            this.buffer = buffer;
        }
        
        @Override
        public void close() throws IOException {
            nullcheck();
            FileHelper.close(buffer);
            buffer = null;
        }
        
        @Override
        public int read() throws IOException {
            nullcheck();
            try {
                return buffer.get();
            } catch(BufferUnderflowException cause) {
                throw new IOException(cause);
            }
        }

        @Override
        public int read(char[] target, int offset, int len) throws IOException {
            nullcheck();
            try {
                buffer.get(target, offset, len);
                return len;
            } catch(BufferUnderflowException cause) {
                throw new IOException(cause);
            }
        }

        @Override
        public long skip(long count) throws IOException {
            nullcheck();
            int np = buffer.position()+(int)count;
            return np == buffer.position(np).position() ? count : 0;
        }
        
        private void nullcheck() throws IOException  {
            if(null == buffer) {
                throw new IOException("Reader is closed");
            }
        }
        
    }
    
    private static class CWriter extends Writer {
        
        private CharBuffer buffer;

        public CWriter(CharBuffer buffer) {
            this.buffer = buffer;
        }
        
        @Override
        public void flush() throws IOException {
            // do nothing
        }

        @Override
        public void close() throws IOException {
            nullcheck();
            flush();
            FileHelper.close(buffer);
            buffer = null;
        }
        
        @Override
        public Writer append(char c) throws IOException {
            try {
                nullcheck();
                buffer.put(c);
                return this;
            } catch(BufferUnderflowException | ReadOnlyBufferException cause) {
                throw new IOException(cause);
            }
        }
        
        @Override
        public Writer append(CharSequence value) throws IOException {
            CharSequence svalue = ValueUtils.or(value, "null");
            return append(svalue, 0, svalue.length());
        }
        
        @Override
        public Writer append(CharSequence value, int start, int end) throws IOException {
            nullcheck();
            CharSequence svalue = ValueUtils.or(value, "null");
            try {
                for(int i=start; i<end; i++) {
                    buffer.put(svalue.charAt(i));
                }
                return this;
            } catch(BufferUnderflowException | ReadOnlyBufferException cause) {
                throw new IOException(cause);
            }
        }

        @Override
        public void write(int c) throws IOException {
            append((char)c);
        }
        
        @Override
        public void write(char[] target) throws IOException {
            nullcheck();
            write(target, 0, target.length);
        }
        
        @Override
        public void write(char[] target, int offset, int len) throws IOException {
            try {
                nullcheck();
                buffer.put(target, offset, len);
            } catch(BufferUnderflowException | ReadOnlyBufferException cause) {
                throw new IOException(cause);
            }
        }
        
        @Override
        public void write(String value) throws IOException {
            try {
                nullcheck();
                buffer.put(value);
            } catch(BufferUnderflowException | ReadOnlyBufferException cause) {
                throw new IOException(cause);
            }
        }
        
        @Override
        public void write(String value, int offset, int len) throws IOException {
            try {
                nullcheck();
                buffer.put(value, offset, offset+len);
            } catch(BufferUnderflowException | ReadOnlyBufferException cause) {
                throw new IOException(cause);
            }
        }
        
        private void nullcheck() throws IOException  {
            if(null == buffer) {
                throw new IOException("Writer is closed");
            }
        }
        
    }
    
    private static class BWriter extends Writer {
        
        Charset charset;
        ByteBuffer buffer;
        
        public BWriter(ByteBuffer buffer, Charset charset) {
            this.buffer = buffer;
            this.charset = charset;
        }
        
        @Override
        public void close() throws IOException {
            nullcheck();
            flush();
            FileHelper.close(buffer);
            buffer = null;
        }
        
        @Override
        public void flush() throws IOException {
            // do nothing
        }
        
        @Override
        public void write(char[] cbuf, int off, int len) throws IOException {
            nullcheck();
            buffer.put( charset.encode(CharBuffer.wrap(cbuf, off, len)) );
        }
        
        private void nullcheck() throws IOException  {
            if(null == buffer) {
                throw new IOException("Writer is closed");
            }
        }

    }
}
