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

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.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.math.MathUtils;

/**
 * 
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public final class BufferAdapter {
    
    private static final ByteBuffer EMPTY = ByteBuffer.wrap(new byte[0]);
    
    private BufferAdapter() {
        // utility class
    }

    public static SeekableByteChannel asChannel(ByteBuffer buffer) {
        return new BChannel(buffer);
    }
    
    public static ObjectInputStream asObjectInput(ByteBuffer buffer) throws IOException {
        return new ObjectInputStream(asInputStream(buffer));
    }
    
    public static ObjectOutputStream asObjectOutput(ByteBuffer buffer) throws IOException {
        return new ObjectOutputStream(asOutputStream(buffer));
    }
    
    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);
    }
    
    public static byte[] toArray(ByteBuffer buffer) {
        int n = buffer.remaining();
        int p = buffer.position();
        byte[] array = new byte[n];
        buffer.get(array);
        buffer.position(p);
        return array;
    }
    
    public static char[] toArray(CharBuffer buffer) {
        int n = buffer.remaining();
        int p = buffer.position();
        char[] array = new char[n];
        buffer.get(array);
        buffer.position(p);
        return array;
    }
    
    private static int setlimit(ByteBuffer buffer, int value) throws IOException {
        int prv = buffer.limit(); // NOPMD
        if(value != buffer.limit(value).limit()) {
            throw new IOException("ByteBuffer#limit failed");
        }
        return prv;
    }
    
    private static int setposition(ByteBuffer buffer, int value) throws IOException {
        int prv = buffer.position(); // NOPMD
        if(value != buffer.position(value).position()) {
            throw new IOException("ByteBuffer#position failed");
        }
        return prv;
    }
    
    private static int setposition(CharBuffer buffer, int value) throws IOException {
        int prv = buffer.position(); // NOPMD
        if(value != buffer.position(value).position()) {
            throw new IOException("CharBuffer#position failed");
        }
        return prv;
    }
	
	private static class BChannel implements SeekableByteChannel {
        
        private ByteBuffer buffer;

        public BChannel(ByteBuffer buffer) {
            this.buffer = buffer;
        }
        
        @Override
        public boolean isOpen() {
            return buffer != EMPTY; // NOPMD
        }
		
		private void assertIsOpen() throws IOException {
			if(!isOpen()) {
				throw new ClosedChannelException();
			}
		}

        @Override
        public void close() throws IOException {
            try {
                IOUtils.close(buffer, IOUtils::rethrow);
            } finally {
                buffer = EMPTY;
            }
        }

        @Override
        public int read(ByteBuffer dst) throws IOException {
			assertIsOpen();
            if(buffer.remaining() == 0) {
                return -1;
            }
            int count = Math.min(buffer.remaining(), dst.remaining());
            int prv = setlimit(buffer, buffer.position() + count);
            dst.put(buffer);
            setlimit(buffer, prv);
            return count;
        }

        @Override
        public int write(ByteBuffer src) throws IOException {
			assertIsOpen();
            try {
                int brem = buffer.remaining();
                if(brem == 0) {
                    throw new EOFException();
                }
                int srem = src.remaining();
                if( srem > brem) {
                    int prv = setlimit(src, src.position() + brem);
                    buffer.put(src);
                    setlimit(src, prv);
                    return brem;
                } else {
                    buffer.put(src);
                    return srem;
                }
            } catch(BufferOverflowException | ReadOnlyBufferException cause) {
                throw new IOException(cause);
            }
        }

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

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

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

        @Override
        public SeekableByteChannel truncate(long size) throws IOException {
			assertIsOpen();
            assert size == (int)size;
            if( size < buffer.limit()) {
                setlimit(buffer, (int)size);
            }
            return this;
        }
        
    }
    
    private static class BIStream extends InputStream {
        
        private ByteBuffer buffer;

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

        @Override
        public void close() throws IOException {
            try {
                IOUtils.close(buffer, IOUtils::rethrow);
            } finally {
                buffer = EMPTY;
            }
        }
        
        @Override
        public int read() throws IOException {
            if(0 == buffer.remaining()) {
                return -1;
            }
            return buffer.get();
        }
        
        @Override
        public int read(byte[] target, int offset, int len) throws IOException {
            len = Math.min(len, buffer.remaining());
            if(len == 0) {
                return -1;
            }
            buffer.get(target, offset, len);
            return len;
        }

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

        @Override
        public long skip(long count) throws IOException {
            assert count == (int) count;

            count = MathUtils.clip(count, 0, buffer.remaining());
            if(count <= 0) {
                return 0;
            }
            setposition(buffer, buffer.position()+(int)count);
            return count;
        }
    
    }
    
    private static class BOStream extends OutputStream {
        
        private ByteBuffer buffer;

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

        @Override
        public void close() throws IOException {
            try {
                IOUtils.close(buffer, IOUtils::rethrow);
            } finally {
                buffer = EMPTY;
            }
        }
        
        @Override
        public void flush() throws IOException {
            // do nothing
        }
        
        @Override
        public void write(int b) throws IOException {
            try {
                buffer.put( (byte)b );
            } catch(BufferOverflowException | ReadOnlyBufferException cause) {
                throw new IOException(cause);
            }
        }

        @Override
        public void write(byte[] bytes, int offset, int len) throws IOException {
            try {
                buffer.put(bytes, offset, len);
            } catch(BufferOverflowException | ReadOnlyBufferException cause) {
                throw new IOException(cause);
            }
        }
        
    }
    
    private static class CReader extends Reader {
        
        private static final CharBuffer EMPTY = CharBuffer.wrap(new char[0]);

        private CharBuffer buffer;

        public CReader(CharBuffer buffer) {
            this.buffer = buffer;
        }
        
        @Override
        public void close() throws IOException {
            try {
                IOUtils.close(buffer, IOUtils::rethrow);
            } finally {
                buffer = EMPTY;
            }
        }
        
        @Override
        public int read() throws IOException {
            if(0 == buffer.remaining()) {
                return -1;
            }
            return buffer.get();
        }

        @Override
        public int read(char[] target, int offset, int len) throws IOException {
            len = Math.min(len, buffer.remaining());
            if(len == 0) {
                return -1;
            }
            buffer.get(target, offset, len);
            return len;
        }

        @Override
        public long skip(long count) throws IOException {
            assert count == (int) count;

            count = MathUtils.clip(count, 0, buffer.remaining());
            if(count <= 0) {
                return 0;
            }
            setposition(buffer, buffer.position()+(int)count);
            return count;
        }

        @Override
        public boolean ready() throws IOException {
            return buffer.remaining() > 0;
        }
        
    }
    
    private static class CWriter extends Writer {
        
        private static final CharBuffer EMPTY = CharBuffer.allocate(0);
        
        private CharBuffer buffer;

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

        @Override
        public void close() throws IOException {
            try {
                flush();
                IOUtils.close(buffer, IOUtils::rethrow);
            } finally {
                buffer = EMPTY;
            }
        }
        
        @Override
        public Writer append(char c) throws IOException {
            try {
                buffer.put(c);
                return this;
            } catch(BufferOverflowException | 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 {
            CharSequence svalue = ValueUtils.or(value, "null");
            try {
                for(int i=start; i<end; i++) {
                    buffer.put(svalue.charAt(i));
                }
                return this;
            } catch(BufferOverflowException | 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 {
            write(target, 0, target.length);
        }
        
        @Override
        public void write(char[] target, int offset, int len) throws IOException {
            try {
                buffer.put(target, offset, len);
            } catch(BufferOverflowException | ReadOnlyBufferException cause) {
                throw new IOException(cause);
            }
        }
        
        @Override
        public void write(String value) throws IOException {
            try {
                buffer.put(value);
            } catch(BufferOverflowException | ReadOnlyBufferException cause) {
                throw new IOException(cause);
            }
        }
        
        @Override
        public void write(String value, int offset, int len) throws IOException {
            try {
                buffer.put(value, offset, offset+len);
            } catch(BufferOverflowException | ReadOnlyBufferException cause) {
                throw new IOException(cause);
            }
        }
        
    }
    
    private static class BWriter extends Writer {
        
        private final Charset charset;
        private ByteBuffer buffer;
        
        public BWriter(ByteBuffer buffer, Charset charset) {
            this.buffer = buffer;
            this.charset = charset;
        }
        
        @Override
        public void close() throws IOException {
            try {
                flush();
                IOUtils.close(buffer, IOUtils::rethrow);
            } finally {
                buffer = EMPTY;
            }
        }
        
        @Override
        public void flush() throws IOException {
            // do nothing
        }
        
        @Override
        public void write(char[] cbuf, int off, int len) throws IOException {
            buffer.put( charset.encode(CharBuffer.wrap(cbuf, off, len)) );
        }

    }
}
