/*
 * @author Ranides Atterwim {@literal <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 lombok.experimental.UtilityClass;
import net.ranides.assira.generic.ValueUtils;

/**
 * These methods wrap {@link DataOutput} lub {@link DataInput} adding Stream functionality.
 * It is reversed operation to {@link java.io.DataOutputStream} or {@link java.io.DataInputStream}.
 * JDK classes allows you to read/write all primitive data types using byte operations.
 * Those wrappers allows you to read/write bytes using stream methods.
 *
 * We overcome disputable decision, that OutputStream and InputStream are classes, not interfaces.
 *
 * If you want to implement I/O stream, you have to subclass stream class.
 * Sometimes it is not possible, because Java does not allow multiple inheritance.
 * In effect, if our class implements DataInput, we have strange situation, where class is able
 * to read any primitive type (including bytes) but cannot be used as stream.
 *
 * This is especially obvious if you look at {@link java.io.RandomAccessFile}.
 * This class should be usable as both input and output stream, but it isn't.
 * If you wrap it using methods below, you can do that.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@UtilityClass
public class DataAdapter {

    /**
     * Creates new OutputStream which delegates I/O to provided DataOutput.
     *
     * @param output output
     * @return stream
     */
    public static OutputStream asOutput(DataOutput output) {
        return new DOStream(output);
    }

    /**
     * Creates new InputStream which delegates I/O to provided DataInput.
     *
     * @param input input
     * @return stream
     */
    public static InputStream asInput(DataInput input) {
        return new DIStream(input);
    }

    /**
     * Creates new Writer which delegates I/O to provided DataOutput.
     * We don't use any charset conversion, but use {@link DataOutput#writeChar(int)}
     *
     * @param output output
     * @return writer
     */
    public static Writer asWriter(DataOutput output) {
        return new DOWriter(output);
    }

    /**
     * Creates new Reader which delegates I/O to provided DataInput.
     * We don't use any charset conversion, but use {@link DataInput#readChar()}
     *
     * @param input input
     * @return reader
     */
    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() {
            IOUtils.closeAny(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() {
            return 0;
        }

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

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

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

        @Override
        public void close() {
            flush();
            IOUtils.closeAny(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() {
            IOUtils.closeAny(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;
            }
        }
        
        
                
    }
}
