/*
 * @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.DataInput;
import java.io.DataOutput;
import java.io.EOFException;
import java.io.IOException;
import java.io.UTFDataFormatException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import net.ranides.assira.collection.arrays.ArrayAllocator;
import net.ranides.assira.collection.arrays.ArrayUtils;
import net.ranides.assira.collection.arrays.NativeArray;
import net.ranides.assira.collection.arrays.NativeArrayAllocator;
import net.ranides.assira.text.Charsets;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public class DataBuffer implements DataInput, DataOutput {
    
    NativeArray array;
    ByteBuffer buffer;

    public DataBuffer() {
        byte[] data = new byte[16];
        this.array = NativeArray.wrap(data);
        this.buffer = ByteBuffer.wrap(data);
    }
    
    public ByteBuffer data() {
        return ByteBuffer.wrap(array.$array(), 0, buffer.position());
    }
    
    public byte[] bytes() {
        int n = buffer.position();
        return ArrayUtils.copy(array.$array(), 0, new byte[n], 0, n);
    }
    
    public DataBuffer seek(int position) throws IOException {
        try {
            if(position != buffer.position(position).position()) {
                throw new IOException("EOF ?");
            }
        } catch(IllegalArgumentException cause) {
            throw new IOException(cause);
        }
        return this;
    }
    
    public int seek() {
        return buffer.position();
    }
    
    @Override
    public void readFully(byte[] b) throws IOException {
        readFully(b, 0, b.length);
    }

    @Override
    public void readFully(byte[] b, int off, int len) throws IOException {
        checkEOF(len);
        buffer.get(b, off, len);
    }

    @Override
    public int skipBytes(int n) throws IOException {
        reserve(n);
        seek(seek()+n);
        return n;
    }

    @Override
    public boolean readBoolean() throws IOException {
        checkEOF(1);
        return buffer.get() != 0;
    }

    @Override
    public byte readByte() throws IOException {
        checkEOF(1);
        return buffer.get();
    }

    @Override
    public int readUnsignedByte() throws IOException {
        checkEOF(1);
        return buffer.get() & 0xFF;
    }

    @Override
    public short readShort() throws IOException {
        checkEOF(2);
        return buffer.getShort();
    }

    @Override
    public int readUnsignedShort() throws IOException {
        checkEOF(2);
        return buffer.getShort() & 0xFFFF;
    }

    @Override
    public char readChar() throws IOException {
        checkEOF(2);
        return buffer.getChar();
    }

    @Override
    public int readInt() throws IOException {
        checkEOF(4);
        return buffer.getInt();
    }

    @Override
    public long readLong() throws IOException {
        checkEOF(8);
        return buffer.getLong();
    }

    @Override
    public float readFloat() throws IOException {
        checkEOF(4);
        return buffer.getFloat();
    }

    @Override
    public double readDouble() throws IOException {
        checkEOF(8);
        return buffer.getDouble();
    }
    
    public String readLine(Charset cs) throws IOException {
        if(buffer.remaining() <=0) {
            return null;
        }
        CharsetDecoder cd = cs.newDecoder();
        SBuffer out = new SBuffer();

        while(true) {
            out.reserve();
            CharBuffer cb = out.cbuf();
            int p = buffer.position(); // NOPMD
            CoderResult cr = cd.decode(buffer, cb, true);
            if(cr.isError()) {
                cr.throwException();
            }
            int end = out.endl();
            if(end >= 0) {
                buffer.position(p);
                cd.decode(buffer, out.cbuf(end), true);
                return out.toString();
            }
            if(cr.isUnderflow()) {
                out.end = out.position + cb.position();
                return out.toString();
            }
        }
    }
    
    /**
     * Reads the next line of text from the input stream.
     * <b>Uses UTF-8 charset</b>. 
     * <p>
     * If end of file is encountered
     * before even one byte can be read, then {@code null}
     * is returned. Otherwise, bytes are converted to type {@code char}
     * using UTF-8 charset. If the character {@code '\n'}
     * is encountered, it is discarded and reading
     * ceases. If the character {@code '\r'}
     * is encountered, it is discarded and, if
     * the following byte converts &#32;to the
     * character {@code '\n'}, then that is
     * discarded also; reading then ceases. If
     * end of file is encountered before either
     * of the characters {@code '\n'} and
     * {@code '\r'} is encountered, reading
     * ceases. Once reading has ceased, a {@code String}
     * is returned that contains all the characters
     * read and not discarded, taken in order.
     * 
     * @return the next line of text from the input stream,
     *         or {@code null} if the end of file is
     *         encountered before a byte can be read.
     * @exception  IOException  if an I/O error occurs.
     */
    @Override
    public String readLine() throws IOException {
        return readLine(Charsets.UTF8);
    }

    @Override
    public String readUTF() throws IOException {
        checkEOF(2);
        int size = readUnsignedShort();
        byte[] bytes = new byte[size];
        readFully(bytes);
        return new String(bytes, Charsets.UTF8);
    }
    
    public void readChars(char[] data) throws IOException {
        checkEOF(2 * data.length);
        for(int i=0; i<data.length; i++) {
            data[i] = buffer.getChar();
        }
    }
    
    public void readBuffer(ByteBuffer target) throws IOException {
        int n = target.remaining();
        checkEOF(n);

        int p = buffer.limit();        
        buffer.limit(buffer.position()+n);
        target.put(buffer);
        buffer.limit(p);
    }
    
    @Override
    public void write(int b) throws IOException {
        writeByte(b);
    }

    @Override
    public void write(byte[] b, int off, int len) throws IOException {
        reserve(len);
        buffer.put(b, off, len);
    }

    @Override
    public void write(byte[] b) throws IOException {
        write(b, 0, b.length);
    }

    @Override
    public void writeBoolean(boolean v) throws IOException {
        reserve(1);
        buffer.put(v ? (byte)1 : (byte)0);
    }

    @Override
    public void writeByte(int v) throws IOException {
        reserve(1);
        buffer.put((byte)v);
    }

    @Override
    public void writeShort(int v) throws IOException {
        reserve(2);
        buffer.putShort((short)v);
    }

    @Override
    public void writeChar(int v) throws IOException {
        reserve(2);
        buffer.putChar((char)v);
    }

    @Override
    public void writeInt(int v) throws IOException {
        reserve(4);
        buffer.putInt(v);
    }

    @Override
    public void writeLong(long v) throws IOException {
        reserve(8);
        buffer.putLong(v);
    }

    @Override
    public void writeFloat(float v) throws IOException {
        reserve(4);
        buffer.putFloat(v);
    }

    @Override
    public void writeDouble(double v) throws IOException {
        reserve(8);
        buffer.putDouble(v);
    }

    @Override
    public void writeBytes(String s) throws IOException {
        int len = s.length();
        reserve(len);
        for (int i = 0 ; i < len ; i++) {
            write((byte)s.charAt(i));
        }
    }

    @Override
    public void writeChars(String s) throws IOException {
        int len = s.length();
        reserve(2*len);
        for (int i = 0 ; i < len ; i++) {
            buffer.putChar(s.charAt(i));
        }
    }

    @Override
    public void writeUTF(String str) throws IOException {
        byte[] data = str.getBytes(Charsets.UTF8);
        int size = data.length;
        if (size > 65535) {
            throw new UTFDataFormatException("encoded string too long: " + size + " bytes");
        }
        
        reserve(2 + size);
        buffer.putShort((short)size);
        buffer.put(data);
    }
    
    public void writeBuffer(ByteBuffer data) {
        reserve(data.remaining());
        buffer.put(data);
    }
    
    private void reserve(int size) {
        if( buffer.remaining() >= size) {
            return;
        }
        int rem = buffer.capacity() - buffer.position();
        if( rem >= size) {
            buffer.limit(buffer.position() + size);
            return;
        }

        array = NativeArrayAllocator.grow(array, array.size()+size);
        ByteBuffer newbuf = ByteBuffer.wrap(array.$array());
        newbuf.position(buffer.position());
        newbuf.limit(buffer.position()+size);
        buffer = newbuf;
    }
    
    private void checkEOF(int size) throws EOFException {
        if(buffer.remaining() < size) {
            buffer.position(buffer.limit());
            throw new EOFException();
        }
    }
    
    private static class SBuffer {
        
        char[] chars = new char[80];
        int position = 0;
        int end = -1;
        
        CharBuffer cbuf() {
            return CharBuffer.wrap(chars, position, chars.length-position);
        }
        
        CharBuffer cbuf(int size) {
            return CharBuffer.wrap(chars, position, size);
        }
        
        void reserve() {
            chars = ArrayAllocator.ensureCapacity(chars, position+80);
        }
        
        boolean peek(int index, char c) {
            return (index < (chars.length)) && (chars[index] == c);
        }
        
        int endl() {
            for(int i=position; i<chars.length; i++) {
                char c = chars[i];
                if(c=='\n') {
                    this.end = i;
                    return i+1;
                }
                if(c=='\r') {
                    this.end = i;
                    return peek(i+1, '\n') ? i+2 : i+1;
                }
            }
            return -1;
        }

        @Override
        public String toString() {
            return new String(chars, 0, end);
        }
        
    }
    
}
