/*
 * @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.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import net.ranides.assira.generic.ValueUtils;

/**
 * Klasa wrapująca {@link DataOutput} lub {@link DataInput} dodając stream functionality.
 * Czyli na odwrót niż to jest w gotowych klasach {@link java.io.DataOutputStream}
 * lub {@link java.io.DataInputStream}. W tamtych klasach to do istniejącego streamu
 * jest dodawana funcjonalność.
 * <p>
 * Podejście {@code java.io} byłoby sensowne: jak coś potrafi pracować z bajtami, to rozszerzmy
 na pracę z prymitywami. Podejście DataAdapter-a: jak coś potrafi pracować ze
 wszystkim, to "pseudo-rozszerzmy" żeby pracowało z bajtami... wydaje się dziwne.
 I byłoby dziwne, gdyby Input/OutputStream były interfejsami. Niestety to są klasy 
 abstrakcyjne, i czasem ktoś tworząc obiekt stream-like może nie mieć  możliwości
 "zaimplementowania" Input/OutputStream. I dostajemy wtedy taką patologię:
 obiekt stream-like czyta "wszystko", ale nie ma w interfejsie powiedziane, że
 z bajtami sobie radzi ;)
 </p><p>
 * Na przykład {@link java.io.RandomAccessFile} jest takim typem, który reprezentując
 * strumień, nie może go zaimplementować, bo jest jednocześnie input/output, a java
 * wielodziedziczenia nie obsługuje.
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public final class DataAdapter {
    
    private DataAdapter() {
        // utility class
    }

    public static OutputStream asOutput(DataOutput output) {
        return new DOStream(output);
    }
    
    public static InputStream asInput(DataInput output) {
        return new DIStream(output);
    }
    
    public static Writer asWriter(DataOutput output) {
        return new DOWriter(output);
    }
    
    public static Reader asReader(DataInput input) {
        return new DIReader(input);
    }
    
    private static class DOStream extends OutputStream {

        private final DataOutput odata;

        public DOStream(DataOutput odata) {
            this.odata = odata;
        }

        @Override
        public void write(int data) throws IOException {
            odata.writeByte(data);
        }

        @Override
        public void write(byte[] data, int offset, int len) throws IOException {
            odata.write(data, offset, len);
        }

        @Override
        public void write(byte[] data) throws IOException {
            odata.write(data);
        }

        @Override
        public void close() throws IOException {
            IOUtils.close(odata, IOUtils::rethrow);
        }

    }

    private static class DIStream extends InputStream {

        private final DataInput idata;

        public DIStream(DataInput idata) {
            this.idata = idata;
        }

        @Override
        public int read() throws IOException {
            try {
                return idata.readByte();
            } catch(EOFException cause) {
                return -1;
            }
        }

        @Override
        public int read(byte[] target, int offset, int len) throws IOException {
            // tutaj musimy w rękach byte-by-byte czytać, bo readFully nie potrafi kawałka pobrać
            // ma zupełnie inną semantykę niż #read
            int ret = 0;
            try {
                target[offset++] = idata.readByte();
                ret++;
            } catch(EOFException cause) {
                return -1;
            }
            try {
                while(ret < len) {
                    target[offset++] = idata.readByte();
                    ret++;
                }
            } catch (EOFException cause) { // NOPMD
                // do nothing
            }
            return ret;
        }

        @Override
        public int read(byte[] target) throws IOException {
            return read(target, 0, target.length);
        }

        @Override
        public long skip(long count) throws IOException {
            assert count == (int) count;
            return idata.skipBytes((int) count);
        }

        @Override
        public int available() throws IOException {
            return 0;
        }

        @Override
        public void close() throws IOException {
            IOUtils.close(idata, IOUtils::rethrow);
        }

    }
    
    private static class DOWriter extends Writer {
        
        private final DataOutput odata;

        public DOWriter(DataOutput odata) {
            this.odata = odata;
        }
        
        @Override
        public void flush() throws IOException {
            // do nothing
        }

        @Override
        public void close() throws IOException {
            flush();
            IOUtils.close(odata, IOUtils::rethrow);
        }
        
        @Override
        public Writer append(char c) throws IOException {
            odata.writeChar(c);
            return this;
        }
        
        @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");
            for(int i=start; i<end; i++) {
                odata.writeChar(svalue.charAt(i));
            }
            return this;
        }

        @Override
        public void write(int c) throws IOException {
            odata.writeChar(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 {
            for(int i=offset, n=offset+len; i<n; i++) {
                odata.writeChar(target[i]);
            }
        }
        
        @Override
        public void write(String value) throws IOException {
            odata.writeChars(value);
        }
        
        @Override
        public void write(String value, int offset, int len) throws IOException {
            append(value, offset, offset+len);
        }
        
    }
    
    private static class DIReader extends Reader {
        
        private final DataInput idata;
        private Character last;
        
        public DIReader(DataInput idata) {
            this.idata = idata;
        }
        
        @Override
        public void close() throws IOException {
            IOUtils.close(idata, IOUtils::rethrow);
        }
        
        @Override
        public int read() throws IOException {
            if(last != null) {
                int ret = last;
                last = null;
                return ret;
            }
            try {
                return idata.readChar();
            } catch(EOFException cause) {
                return -1;
            }
        }

        @Override
        public int read(char[] target, int offset, int len) throws IOException {
            int ret = 0;
            if(last != null) {
                target[offset++] = last;
                ret++;
                last = null;
            } else {
                try {
                    target[offset++] = idata.readChar();
                    ret++;
                } catch(EOFException cause) {
                    return -1;
                }
            }
            try {
                while(ret < len) {
                    target[offset++] = idata.readChar();
                    ret++;
                }
            } catch (EOFException cause) { // NOPMD
                // do nothing
            }
            return ret;
        }

        @Override
        public long skip(long count) throws IOException {
            assert count == (int) count;
            if(count <=0 ) {
                return 0;
            }
            if(last != null) {
                last = null;
                return idata.skipBytes(2*(int)count - 2) / 2 + 1;
            } else {
                return idata.skipBytes(2*(int)count) / 2;
            }
        }

        @Override
        public boolean ready() throws IOException {
            if(last != null) {
                return true;
            }
            try {
                last = idata.readChar();
                return true;
            } catch(EOFException cause) {
                return false;
            }
        }
        
        
                
    }
}
