/*
 * @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.*;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.util.Arrays;
import java.util.function.Consumer;

import lombok.experimental.UtilityClass;
import net.ranides.assira.events.EventListener;
import net.ranides.assira.functional.checked.CheckedRunnable;
import net.ranides.assira.generic.CompareUtils;
import net.ranides.assira.trace.ExceptionUtils;
import net.ranides.assira.trace.LoggerUtils;

/**
 * Utility methods for I/O streams
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@UtilityClass
public class IOStreams {

    /**
     * This output stream will silently discard every written data.
     */
    public static final OutputStream NULL = new NULLStream();

    /**
     * This input stream does not contains any data and signals EOF.
     */
    public static final InputStream EMPTY = new EmptyStream();
    
    private static final int COPY_BUFFER = 4096;
    
    private static final int READER_BUFFER = 512;
    
    private static final int WRITER_BUFFER = 512;
    
    private static final org.slf4j.Logger LOGGER = LoggerUtils.getLogger();

    /**
     * Reads all bytes from stream and returns them in array.
     * Blocks until EOF is reached.
     *
     * @param input input
     * @return data
     * @throws IOException on error
     */
    public static byte[] read(InputStream input) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream(input.available());
        copy(input, bos, null);
        return bos.toByteArray();
    }

    /**
     * Copies all bytes from input into output stream.
     * Blocks until EOF is reached.
     *
     * @param input input
     * @param output output
     * @return number of copied bytes
     * @throws IOException on error
     */
    public static long copy(InputStream input, OutputStream output) throws IOException {
        return copy(input, output, null);
    }

    /**
     * Copies at most "size" bytes form input to output stream.
     * It could copy less than "size" if EOF is reached.
     * Blocks until "size" bytes is copied or EOF is reached.
     *
     * @param input input
     * @param output output
     * @param size maximum number of bytes to copy
     * @return number of copied bytes
     * @throws IOException on error
     */
    public static long copy(InputStream input, OutputStream output, long size) throws IOException {
        return copy(input, output, size, null);
    }

    /**
     * Copies all bytes from input into output stream.
     * Blocks until EOF is reached.
     *
     * Calls "callback" about every successfully copied chunk of data.
     * Usefull if we want to be informed about progress.
     *
     * @param input input
     * @param output output
     * @param callback callback
     * @return number of copied bytes
     * @throws IOException on error
     */
    public static long copy(InputStream input, OutputStream output, Consumer<Long> callback) throws IOException {
        byte[] buffer = new byte[COPY_BUFFER];
        int n;
        long s = 0;
        while( (n = input.read(buffer)) > 0 ) {
            s += n;
            output.write(buffer, 0, n);
            if(null != callback) {
                callback.accept(s);
            }
        }
        return s;
    }

    /**
     * Copies at most "size" bytes form input to output stream.
     * It could copy less than "size" if EOF is reached.
     * Blocks until "size" bytes is copied or EOF is reached.
     *
     * Calls "callback" about every successfully copied chunk of data.
     * Usefull if we want to be informed about progress.
     *
     * @param input input
     * @param output output
     * @param size size
     * @param callback callback
     * @return number of copied bytes
     * @throws IOException on error
     */
    public static long copy(InputStream input, OutputStream output, final long size, Consumer<Long> callback) throws IOException {
        byte[] buffer = new byte[COPY_BUFFER];

        long remaining = size;
        while (true) {
            int n = buffer.length < remaining ? buffer.length : (int) remaining;

            n = input.read(buffer, 0, n);
            if (n < 0) {
                return size - remaining;
            }
            output.write(buffer, 0, n);

            if(null != callback) {
                callback.accept((long)n);
            }

            remaining -= n;

            if (remaining == 0L) {
                return size;
            }
        }
    }

    /**
     * Compares content of two streams and returns "tue" if they are equal.
     *
     * Please note, that content of both streams will be consumed.
     * It stops reading before EOF if difference is found.
     *
     * @param stream1 stream1
     * @param stream2 stream2
     * @return boolean
     * @throws IOException on error
     */
    public static boolean equals(InputStream stream1, InputStream stream2) throws IOException {
        return equals(stream1, stream2, COPY_BUFFER);
    }
    
    private static boolean equals(InputStream stream1, InputStream stream2, int bufsize) throws IOException {
        byte[] bleft  = new byte[bufsize];
        byte[] bright = new byte[bufsize];
        while (true) {
            int ileft  = stream1.read(bleft, 0, bufsize);
            int iright = stream2.read(bright, 0, bufsize);
            if (ileft != iright) {
                return false;
            }
            if (ileft <= 0) {
                return true;
            }
            if( !Arrays.equals(bleft, bright) ) {
                return false;
            }
        }
    }

    /**
     * Creates new stream which delegates all I/O operations to "is" and
     * sends event to listener after every I/O call.
     *
     * @param is is
     * @param listener listener
     * @return stream
     */
    public static InputStream observe(InputStream is, EventListener<? super IOEvent> listener) {
        return new InputObserver(is, listener);
    }

    /**
     * Creates new stream which delegates all I/O operations to "os" and
     * sends event to listener after every I/O call.
     *
     * @param os os
     * @param listener listener
     * @return stream
     */
    public static OutputStream observe(OutputStream os, EventListener<? super IOEvent> listener) {
        return new OutputObserver(os, listener);
    }

    /**
     * Creates multiplexed stream which writes data to all provided streams.
     *
     * @param streams streams
     * @return stream
     */
    public static OutputStream join(OutputStream... streams) {
        return new OutputJoiner(streams);
    }

    /**
     * Binds provided stream to stdout.
     * Every call to "System.out" will send data to stdout and provided stream.
     *
     * @param ostream ostream
     * @param <S> S
     * @return binded stream
     */
    public static <S extends OutputStream> S tee(S ostream) {
        try {
            System.setOut(new PrintStream(join(System.out, ostream), false, Charset.defaultCharset().name()));
            return ostream;
        } catch (UnsupportedEncodingException cause) {
            throw ExceptionUtils.rethrow(cause);
        }
    }

    /**
     * Creates new stream binded to stdout.
     * Every call to "System.out" will send data to stdout and created stream.
     *
     * @return binded stream
     */
    public static StringOutput tee() {
        return tee(new StringOutput());
    }

    /**
     * Creates new stream which will write all binary data into provided writer,
     * using provided charset to encode characters on the fly.
     *
     * @param writer writer
     * @param cs cs
     * @return stream
     */
    public static OutputStream stream(Writer writer, Charset cs) {
        return new WriterStream(writer, cs);
    }

    /**
     * Creates new stream which will read all characters from provided reader,
     * and convert them on the fly into bytes using provided charset.
     *
     * @param reader reader
     * @param cs cs
     * @return stream
     */
    public static InputStream stream(Reader reader, Charset cs) {
        CharsetEncoder ce = cs.newEncoder();
        float bpc = ce.maxBytesPerChar();
        
        if(CompareUtils.equals(bpc, 1, 0.1)) {
            return new ReaderStreamFBP(reader, ce, 1);
        }
        switch(cs.name()) {
            case "UTF-32":
            case "UTF-32BE":
            case "UTF-32LE":
            case "X-UTF-32BE-BOM":
            case "X-UTF-32LE-BOM":
                return new ReaderStreamFBP(reader, ce, 4);
            default:
                return new ReaderStream(reader, ce);
        }
    }

    /**
     * Creates new background thread which will copy all data from "input" to "output".
     * You should always terminate created "pipe" threads.
     * Created thread closes correctly both streams when terminated.
     *
     * Please note that returned thread is already started.
     *
     * @param input input
     * @param output output
     * @return thread
     */
    public static Thread pipe(InputStream input, OutputStream output) {
        return pipe(input, output, (Exception e) -> LOGGER.error("Exception ignored in #pipe", e));
    }

    /**
     * Creates new background thread which will copy all data from "input" to "output".
     * You should always terminate created "pipe" threads.
     * Created thread closes correctly both streams when terminated.
     *
     * It sends information about exceptions to "handler".
     *
     * Please note that returned thread is already started.
     *
     * @param input input
     * @param output output
     * @param handler handler
     * @return thread
     */
    public static Thread pipe(InputStream input, OutputStream output, Consumer<Exception> handler) {
        Thread thread = new Thread(() -> {
            try {
                copy(input, output);
            } catch(Exception cause) {
                handler.accept(cause);
            } finally {
                IOUtils.closeAny(input, handler);
                IOUtils.closeAny(output, handler);
            }
        });
        thread.start();
        return thread;
    }

    /**
     * Creates new background thread which will copy all data from "input" to "output".
     * You should always terminate created "pipe" threads.
     * Created thread closes correctly both streams when terminated.
     *
     * Please note that returned thread is already started.
     *
     * It sends information about operations to "listener".
     * It sends "seek" events when copy.
     * It sends "flush" event after copy.
     * It sends "failure" event on error.
     * It sends "close" event before termination.
     *
     * @param input input
     * @param output output
     * @param listener listener
     * @return thread
     */
    public static Thread pipe(InputStream input, OutputStream output, EventListener<? super IOEvent> listener) {
        if(null == listener) {
            return pipe(input, output);
        }
        Thread thread = new Thread(() -> {
            try {
                copy(input, output, p -> listener.handleEvent(IOEvent.seek(input, p)));
                listener.handleEvent(IOEvent.flush(output));
            } catch(IOException cause) {
                listener.handleEvent(IOEvent.failure(cause));
            } finally {
                IOUtils.close(input, listener.consumer(IOEvent::failure));
                IOUtils.close(output, listener.consumer(IOEvent::failure));
                listener.handleEvent(IOEvent.close(input));
                listener.handleEvent(IOEvent.close(output));
            }
        });
        thread.start();
        return thread;
    }
    
    /**
     * Creates wrapper stream with limited content to specified range.
     * Skips first "begin" bytes and signals EOF after reaching "end".
     *
     * Please note that it does not close "istream"!
     *
     * @param istream istream
     * @param begin begin
     * @param end end
     * @return stream
     * @throws IOException on error
     */
    public static InputStream substream(InputStream istream, long begin, long end) throws IOException {
        if(0 == begin && end == Long.MAX_VALUE) {
            return istream;
        }
        return new SubInput(istream, begin, end, false);
    }

    /**
     * Creates wrapper stream with limited content to specified range.
     * Skips first "begin" bytes and signals EOF after reaching "end".
     *
     * Please note that it closes "istream"!
     *
     * @param istream istream
     * @param begin begin
     * @param end end
     * @return stream
     * @throws IOException on error
     */
    public static InputStream limit(InputStream istream, long begin, long end) throws IOException {
        if(0 == begin && end == Long.MAX_VALUE) {
            return istream;
        }
        return new SubInput(istream, begin, end, true);
    }

    /**
     * Skips "n" bytes in stream.
     *
     * @param istream istream
     * @param n n
     * @throws IOException on error
     */
    public static void skip(InputStream istream, long n) throws IOException {
        long skipped = istream.skip(n);
        if(skipped != n) {
            throw new IOException("Skipped only " + skipped + " bytes instead of " + n);
        }
    }

    /**
     * Creates new stream wrapper which delegates all operations to "ostream" and
     * additionally calls provided callback when "close" is called.
     *
     * @param ostream ostream
     * @param onClose onClose
     * @return stream
     */
    public static OutputStream onClose(OutputStream ostream, CheckedRunnable<IOException> onClose) {
        return new OutputWrapper(ostream){
            @Override
            public void close() throws IOException {
                super.close();
                onClose.run();
            }
        };
    }

    /**
     * Creates new stream wrapper which delegates all operations to "ostream" and
     * additionally calls provided callback when "close" is called.
     *
     * @param istream istream
     * @param onClose onClose
     * @return stream
     */
    public static InputStream onClose(InputStream istream, CheckedRunnable<IOException> onClose) {
        return new InputWrapper(istream){
            @Override
            public void close() throws IOException {
                super.close();
                onClose.run();
            }
        };
    }
    
    private static final class SubInput extends FilterInputStream {
        
        private final boolean close;
        private final long max;
        private long pos;
        private long mark;
        
        public SubInput(InputStream in, long begin, long end, boolean close) throws IOException {
            super(in);
            IOStreams.skip(in, begin);
            this.max = end-begin;
            this.close = close;
        }

        @Override
        public void reset() throws IOException {
            super.reset();
            pos = mark;
        }

        @Override
        public void mark(int readlimit) {
            super.mark(readlimit);
            mark = pos;
        }

        @Override
        public void close() throws IOException {
            if(close) {
                super.close();
            }
        }

        @Override
        public int available() throws IOException {
            return Math.max(0, Math.min((int)(max - pos), super.available()));
        }

        @Override
        public long skip(long n) throws IOException {
            if (pos>=max) {
                return 0;
            }
            long r = super.skip( Math.min(n, max-pos) );
            pos += r;
            return r;
        }

        @Override
        public int read(byte[] b, int off, int len) throws IOException {
            if (pos>=max) {
                return -1;
            }
            int br = super.read(b, off, (int)Math.min(len, max-pos));
            if(-1 != br) {
                pos += br;
            }
            return br;
        }

        @Override
        public int read() throws IOException {
            if (pos>=max) {
                return -1;
            }
            int cr = super.read();
            pos += 1;
            return cr;
        }
        
    }
    
    private static final class InputObserver extends FilterInputStream {
        
        private final EventListener<? super IOEvent> listener;

        public InputObserver(InputStream in, EventListener<? super IOEvent> listener) {
            super(in);
            this.listener = listener;
        }

        @Override
        public void reset() throws IOException {
            try {
                super.reset();
                listener.handleEvent(IOEvent.reset(in));
            } catch(IOException cause) {
                throw rethrow(cause);
            }
        }

        @Override
        public void close() throws IOException {
            try {
                super.close();
                listener.handleEvent(IOEvent.close(in));
            } catch(IOException cause) {
                throw rethrow(cause);
            }
        }

        @Override
        public long skip(long n) throws IOException {
            try {
                long ret = super.skip(n);
                listener.handleEvent(IOEvent.skip(in, ret));
                return ret;
            } catch(IOException cause) {
                throw rethrow(cause);
            }
        }

        @Override
        public int read(byte[] b, int off, int len) throws IOException {
            try {
                int ret = super.read(b, off, len);
                if(-1==ret) {
                    listener.handleEvent(IOEvent.read(in, -1));
                } else {
                    listener.handleEvent(IOEvent.read(in, b, off, ret));
                }
                
                return ret;
            } catch(IOException cause) {
                throw rethrow(cause);
            }
        }

        @Override
        public int read(byte[] b) throws IOException {
            try {
                int ret = in.read(b);
                if(-1==ret) {
                    listener.handleEvent(IOEvent.read(in, -1));
                } else {
                    listener.handleEvent(IOEvent.read(in, b, 0, ret));
                }
                return ret;
            } catch(IOException cause) {
                throw rethrow(cause);
            }
        }

        @Override
        public int read() throws IOException {
            try {
                int ret = super.read();
                listener.handleEvent(IOEvent.read(in, ret));
                return ret;
            } catch(IOException cause) {
                throw rethrow(cause);
            }
        }
        
        private IOException rethrow(IOException cause) throws RuntimeException {
            listener.handleEvent(IOEvent.failure(cause));
            throw ExceptionUtils.rethrow(cause);
        }
        
    }
    
    private static final class OutputObserver extends FilterOutputStream {
        
        private final EventListener<? super IOEvent> listener;
        
        public OutputObserver(OutputStream out, EventListener<? super IOEvent> listener) {
            super(out);
            this.listener = listener;
        }

        @Override
        public void close() throws IOException {
            try {
                super.close();
                listener.handleEvent(IOEvent.close(out));
            } catch(IOException cause) {
                throw rethrow(cause);
            }
        }

        @Override
        public void flush() throws IOException {
            try {
                super.flush();
                listener.handleEvent(IOEvent.flush(out));
            } catch(IOException cause) {
                throw rethrow(cause);
            }
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException {
            try {
                out.write(b, off, len);
                listener.handleEvent(IOEvent.write(out, b, off, len));
            } catch(IOException cause) {
                throw rethrow(cause);
            }
        }

        @Override
        public void write(byte[] b) throws IOException {
            try {
                out.write(b);
                listener.handleEvent(IOEvent.write(out, b));
            } catch(IOException cause) {
                throw rethrow(cause);
            }
        }

        @Override
        public void write(int b) throws IOException {
            try {
                out.write(b);
                listener.handleEvent(IOEvent.write(out, b));
            } catch(IOException cause) {
                throw rethrow(cause);
            }
        }
        
        private IOException rethrow(IOException cause) throws RuntimeException {
            listener.handleEvent(IOEvent.failure(cause));
            throw ExceptionUtils.rethrow(cause);
        }
        
    }
    
    private static final class EmptyStream extends InputStream {

        @Override
        public boolean markSupported() {
            return true;
        }

        @Override
        public void reset() {
            // NOP
        }

        @Override
        public void close() {
            // NOP
        }

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

        @Override
        public long skip(long n) {
            return 0;
        }

        @Override
        public int read(byte[] b, int off, int len) {
            return -1;
        }
        
        @Override
        public int read() {
            return -1;
        }
        
    }
    
    private static final class NULLStream extends OutputStream {
    
        @Override
        public void close() {
            // do nothing
        }

        @Override
        public void write(int data){
            // do nothing
        }

        @Override
        public void write(byte[] data, int offset, int length) {
            // do nothing
        }

    }
    
    private static class OutputJoiner extends OutputStream {

        private final OutputStream[] streams;

        public OutputJoiner(OutputStream... streams) {
            this.streams = streams;
        }

        @Override
        public void write(int data) throws IOException {
            run(os -> os.write(data));
        }
        
        @Override
        public void write(byte[] data) throws IOException {
            run(os -> os.write(data));
        }

        @Override
        public void write(byte[] data, int offset, int length) throws IOException {
            run(os -> os.write(data, offset, length));
        }

        @Override
        public void close() throws IOException {
            run(os -> os.close());
        }

        @Override
        public void flush() throws IOException {
            run(os -> os.flush());
        }

        private void run(IOHandler handler) throws IOException {
            IOException out = null;
            for(OutputStream stream : streams) {
                try {
                    handler.run(stream);
                } catch(Exception cause) {
                    out = IOUtils.addSuppressed(out, cause);
                }
            }
            if(null != out) {
                throw out;
            }
        }

    }
    
    private interface IOHandler {

        void run(OutputStream os) throws IOException;

    }
    
    private static final class WriterStream extends OutputStream {
        
        private final CharsetDecoder cd;
        private final Writer writer;
        private final ByteBuffer buffer;

        public WriterStream(Writer writer, Charset cs) {
            assert writer != null : "writer is NULL";
            this.cd = cs.newDecoder();
            this.writer = writer;
            this.buffer = ByteBuffer.allocate(WRITER_BUFFER);
        }
        
        @Override
        public void write(int b) throws IOException {
            buffer.put((byte)b);
            if(buffer.remaining()==0) {
                flush();
            }
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException {
            if(buffer.remaining() < len) {
                flush();
                write(ByteBuffer.wrap(b, off, len), false);
                return;
            }
            buffer.put(b, off, len);
            if(buffer.remaining()==0) {
                flush();
            }
        }

        @Override
        public void flush() throws IOException {
            buffer.flip();
            write(buffer, false);
            writer.flush();
        }
        
        
        @Override
        public void close() throws IOException {
            buffer.flip();
            write(buffer, true);
            writer.flush();
            writer.close();
            super.close();
        }
        
        private void write(ByteBuffer src, boolean end) throws IOException {
            if (0 == src.remaining()) {
                return;
            }
            
            int n = (int)(src.remaining() * cd.averageCharsPerByte());
            CharBuffer out = CharBuffer.allocate(n);

            while(true) {
                CoderResult cr = src.hasRemaining() ?
                    cd.decode(src, out, true) : CoderResult.UNDERFLOW;
                if (end && cr.isUnderflow()) {
                    cr = cd.flush(out);
                }
                if (cr.isUnderflow()) {
                    writer.write(out.array(), 0, out.position());
                    break;
                }
                if (cr.isOverflow()) {
                    writer.write(out.array(), 0, out.position());
                    out.clear();
                    continue;
                }
                cr.throwException();
            }
            src.clear();
        }
        
    }
    
    private static class ReaderStreamFBP extends ReaderStream {
        
        private final int bytesize;

        public ReaderStreamFBP(Reader reader, CharsetEncoder ce, int bytesize) {
            super(reader, ce);
            this.bytesize = bytesize;
        }

        @Override
        public long skip(long len) throws IOException {
            long ret = 0;
            
            ret += skip(bytes, Math.min(len-ret, bytes.remaining()));
            if(ret == len) {
                return ret;
            }
            
            long charc = Math.min((len-ret), chars.remaining());
            ret += skip(chars, charc/bytesize)*bytesize;
            if(ret == len) {
                return ret;
            }
            
            ret += reader.skip((len-ret)/bytesize)*bytesize;
            if(ret == len || !readnext()) {
                return ret;
            }
            
            ret += skip(bytes, Math.min(len-ret, bytes.remaining()));
            return ret;
        }

        private long skip(Buffer buffer, long n) {
            buffer.position(buffer.position()+(int)n);
            return n;
        }

        
    }
    
    private static class ReaderStream extends InputStream {
        
        protected final CharsetEncoder ce;
        protected final Reader reader;
        protected final CharBuffer chars;
        protected final ByteBuffer bytes;
        
        protected boolean eof;

        public ReaderStream(Reader reader, CharsetEncoder ce) {
            assert reader != null : "reader is NULL";
            this.ce = ce;
            this.reader = reader;
            this.chars = CharBuffer.allocate((int)(READER_BUFFER / ce.averageBytesPerChar()));
            this.bytes = ByteBuffer.allocate(READER_BUFFER);
            this.chars.limit(0);
            this.bytes.limit(0);
        }

        @Override
        public int read() throws IOException {
            if(!bytes.hasRemaining() && !readnext()) {
                return -1;
            }
            return bytes.get();
        }
        
        @Override
        public int read(byte[] target, int off, int len) throws IOException {
            if(!bytes.hasRemaining() && !readnext()) {
                return -1;
            }
            int out = 0;
            while(true) {
                int c = Math.min(len-out, bytes.remaining());
                bytes.get(target, off+out, c);
                out += c;
                if(out == len || !readnext()) {
                    return out;
                }
            }
        }
        
        @Override
        public long skip(long len) throws IOException {
            int out = 0;
            while(true) {
                int c = (int)Math.min(len-out, bytes.remaining());
                bytes.position(bytes.position()+c);
                out += c;
                if(out == len || !readnext()) {
                    return out;
                }
            }
        }
        
        @Override
        public int available() {
            return bytes.remaining();
        }

        @Override
        public void close() throws IOException {
            reader.close();
            super.close();
        }
        
        protected boolean readnext() throws IOException {
            if(!chars.hasRemaining() && (eof || !readchars())) {
                return false;
            }
            readbytes();
            return true;
        }

        protected void readbytes() throws IOException {
            bytes.clear();
            CoderResult cr = ce.encode(chars, bytes, eof);
            bytes.flip();
            
            if( cr.isError()) {
                cr.throwException();
            }
        }

        protected boolean readchars() throws IOException {
            chars.clear();
            int count = reader.read(chars);
            chars.flip();
            
            if(count < chars.capacity()) {
                eof = true;
            }
            return count > 0;
        }

    }

    /**
     * This class guarantees, that all bulk write operations will be sent directly to output
     */
    private static class OutputWrapper extends FilterOutputStream {

        public OutputWrapper(OutputStream out) {
            super(out);
        }

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

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

        @Override
        public void write(int b) throws IOException {
            out.write(b);
        }

    }

    /**
     * This class guarantees, that all bulk read operations will be sent directly to input
     */
    private static class InputWrapper extends FilterInputStream {

        public InputWrapper(InputStream in) {
            super(in);
        }

        @Override
        public int read(byte[] b) throws IOException {
            return in.read(b);
        }

        @Override
        public int read() throws IOException {
            return in.read();
        }

        @Override
        public int read(byte[] b, int off, int len) throws IOException {
            return in.read(b, off, len);
        }
    }
}
