IOBack.java

package space.sunqian.fs.io;

import space.sunqian.annotation.Nonnull;
import space.sunqian.annotation.Nullable;
import space.sunqian.fs.base.math.MathKit;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.Reader;
import java.io.Writer;
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.nio.charset.CodingErrorAction;

final class IOBack {

    private static final @Nonnull String MARK_NOT_SET = "Mark has not been set.";
    private static final @Nonnull String STREAM_CLOSED = "Stream closed.";
    private static final int CHARS_BUFFER_SIZE = 64;
    private static final int BYTES_BUFFER_SIZE = 64;

    private static @Nonnull String insufficientRemainingSpace(int len, int remaining) {
        return "Insufficient remaining space: " + len + " to " + remaining + ".";
    }

    private static @Nonnull String insufficientRemainingSpace(int len, long remaining) {
        return "Insufficient remaining space: " + len + " to " + remaining + ".";
    }

    private static @Nonnull String encodingFailed(CoderResult result) {
        return "Chars encoding failed: " + result + ".";
    }

    private static @Nonnull String decodingFailed(CoderResult result) {
        return "Bytes decoding failed: " + result + ".";
    }

    static @Nonnull InputStream inputStream(byte @Nonnull [] array) {
        return new BytesInputStream(array, 0, array.length);
    }

    static @Nonnull InputStream inputStream(
        byte @Nonnull [] array, int off, int len
    ) throws IndexOutOfBoundsException {
        IOChecker.checkOffLen(off, len, array.length);
        return new BytesInputStream(array, off, len);
    }

    static @Nonnull InputStream inputStream(@Nonnull ByteBuffer buffer) {
        return new BufferInputStream(buffer);
    }

    static @Nonnull InputStream inputStream(
        @Nonnull RandomAccessFile raf, long seek
    ) throws IllegalArgumentException, IORuntimeException {
        IOChecker.checkSeek(seek);
        try {
            return new RafInputStream(raf, seek);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    static @Nonnull InputStream inputStream(@Nonnull Reader reader, @Nonnull Charset charset) {
        return new CharsInputStream(reader, charset);
    }

    static @Nonnull InputStream inputStream(@Nonnull InputStream in, long limit) throws IllegalArgumentException {
        IOChecker.checkLimit(limit);
        return new LimitedInputStream(in, limit);
    }

    static @Nonnull InputStream emptyInputStream() {
        return EmptyInputStream.SINGLETON;
    }

    static @Nonnull Reader reader(char @Nonnull [] array) {
        return new CharsReader(array, 0, array.length);
    }

    static @Nonnull Reader reader(char @Nonnull [] array, int off, int len) throws IndexOutOfBoundsException {
        IOChecker.checkOffLen(off, len, array.length);
        return new CharsReader(array, off, len);
    }

    static @Nonnull Reader reader(@Nonnull CharSequence chars) {
        return new BufferReader(CharBuffer.wrap(chars));
    }

    static @Nonnull Reader reader(@Nonnull CharSequence chars, int start, int end) throws IndexOutOfBoundsException {
        IOChecker.checkStartEnd(start, end, chars.length());
        return new BufferReader(CharBuffer.wrap(chars, start, end));
    }

    static @Nonnull Reader reader(@Nonnull CharBuffer buffer) {
        return new BufferReader(buffer);
    }

    static @Nonnull Reader reader(@Nonnull InputStream inputStream, @Nonnull Charset charset) {
        return new BytesReader(inputStream, charset);
    }

    static @Nonnull Reader reader(@Nonnull Reader in, long limit) throws IllegalArgumentException {
        IOChecker.checkLimit(limit);
        return new LimitedReader(in, limit);
    }

    static @Nonnull Reader emptyReader() {
        return EmptyReader.SINGLETON;
    }

    static @Nonnull OutputStream outputStream(byte @Nonnull [] array) {
        return new BytesOutputStream(array, 0, array.length);
    }

    static @Nonnull OutputStream outputStream(
        byte @Nonnull [] array, int off, int len
    ) throws IndexOutOfBoundsException {
        IOChecker.checkOffLen(off, len, array.length);
        return new BytesOutputStream(array, off, len);
    }

    static @Nonnull OutputStream outputStream(@Nonnull ByteBuffer buffer) {
        return new BufferOutputStream(buffer);
    }

    static @Nonnull OutputStream outputStream(
        @Nonnull RandomAccessFile raf, long seek
    ) throws IllegalArgumentException, IORuntimeException {
        IOChecker.checkSeek(seek);
        try {
            return new RafOutputStream(raf, seek);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    static @Nonnull OutputStream outputStream(@Nonnull Appendable appender, @Nonnull Charset charset) {
        return new AppenderOutputStream(appender, charset);
    }

    static @Nonnull OutputStream outputStream(@Nonnull OutputStream in, long limit) throws IllegalArgumentException {
        IOChecker.checkLimit(limit);
        return new LimitedOutputStream(in, limit);
    }

    static @Nonnull OutputStream nullOutputStream() {
        return NullOutputStream.SINGLETON;
    }

    static @Nonnull Writer writer(char @Nonnull [] array) {
        return new CharsWriter(array, 0, array.length);
    }

    static @Nonnull Writer writer(char @Nonnull [] array, int off, int len) throws IndexOutOfBoundsException {
        IOChecker.checkOffLen(off, len, array.length);
        return new CharsWriter(array, off, len);
    }

    static @Nonnull Writer writer(@Nonnull CharBuffer buffer) {
        return new BufferWriter(buffer);
    }

    static @Nonnull Writer writer(@Nonnull OutputStream outputStream, Charset charset) {
        return new BytesWriter(outputStream, charset);
    }

    static @Nonnull Writer writer(@Nonnull Writer in, long limit) throws IllegalArgumentException {
        IOChecker.checkLimit(limit);
        return new LimitedWriter(in, limit);
    }

    static @Nonnull Writer nullWriter() {
        return NullWriter.SINGLETON;
    }

    private static final class BytesInputStream extends DoReadStream {

        private final byte @Nonnull [] buf;
        private int pos;
        private final int end;
        private int mark = -1;

        private BytesInputStream(byte @Nonnull [] buf, int off, int len) {
            this.buf = buf;
            this.pos = off;
            this.end = off + len;
        }

        @Override
        public int read() {
            return (pos < end) ? (buf[pos++] & 0xff) : -1;
        }

        @Override
        protected int doRead(byte @Nonnull [] b, int off, int len) {
            if (len == 0) {
                return 0;
            }
            if (pos >= end) {
                return -1;
            }
            int avail = end - pos;
            avail = Math.min(len, avail);
            System.arraycopy(buf, pos, b, off, avail);
            pos += avail;
            return avail;
        }

        @Override
        public long skip(long n) {
            if (n <= 0) {
                return 0;
            }
            int avail = end - pos;
            avail = (int) Math.min(n, avail);
            if (avail <= 0) {
                return 0;
            }
            pos += avail;
            return avail;
        }

        @Override
        public int available() {
            return end - pos;
        }

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

        @Override
        public void mark(int readAheadLimit) {
            mark = pos;
        }

        @Override
        public void reset() throws IOException {
            if (mark < 0) {
                throw new IOException(MARK_NOT_SET);
            }
            pos = mark;
        }
    }

    private static final class BufferInputStream extends DoReadStream {

        private final @Nonnull ByteBuffer buffer;

        private BufferInputStream(@Nonnull ByteBuffer buffer) {
            this.buffer = buffer;
        }

        @Override
        public int read() {
            if (!buffer.hasRemaining()) {
                return -1;
            }
            return buffer.get() & 0xff;
        }

        @Override
        protected int doRead(byte @Nonnull [] b, int off, int len) {
            if (len == 0) {
                return 0;
            }
            if (!buffer.hasRemaining()) {
                return -1;
            }
            int avail = Math.min(buffer.remaining(), len);
            buffer.get(b, off, avail);
            return avail;
        }

        @Override
        public long skip(long n) {
            if (n <= 0) {
                return 0;
            }
            int avail = (int) Math.min(buffer.remaining(), n);
            if (avail <= 0) {
                return 0;
            }
            buffer.position(buffer.position() + avail);
            return avail;
        }

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

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

        @Override
        public void mark(int readlimit) {
            buffer.mark();
        }

        @Override
        public void reset() throws IOException {
            try {
                buffer.reset();
            } catch (Exception e) {
                throw new IOException(e);
            }
        }
    }

    private static final class RafInputStream extends DoReadStream {

        private final @Nonnull RandomAccessFile raf;
        private long mark = -1;

        private RafInputStream(@Nonnull RandomAccessFile raf, long seek) throws IOException {
            this.raf = raf;
            this.raf.seek(seek);
        }

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

        @Override
        protected int doRead(byte @Nonnull [] b, int off, int len) throws IOException {
            if (len == 0) {
                return 0;
            }
            return raf.read(b, off, len);
        }

        @Override
        public long skip(long n) throws IOException {
            if (n <= 0) {
                return 0;
            }
            return raf.skipBytes(MathKit.safeInt(n));
        }

        @Override
        public int available() throws IOException {
            return MathKit.safeInt(raf.length() - raf.getFilePointer());
        }

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

        @Override
        public void mark(int readlimit) {
            try {
                this.mark = raf.getFilePointer();
            } catch (IOException ignored) {
            }
        }

        @Override
        public void reset() throws IOException {
            if (mark < 0) {
                throw new IOException(MARK_NOT_SET);
            }
            raf.seek(mark);
        }

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

    private static final class CharsInputStream extends DoReadStream {

        private final @Nonnull Reader reader;
        private final @Nonnull CharsetEncoder encoder;
        private @Nonnull CharBuffer inBuffer;
        private @Nonnull ByteBuffer outBuffer;
        private boolean endOfInput = false;
        private boolean closed = false;
        private final byte @Nonnull [] buf = {0};

        // snapshot for mark/reset
        private CharBuffer inCopy;
        private ByteBuffer outCopy;
        private boolean endCopy;

        private CharsInputStream(
            @Nonnull Reader reader,
            @Nonnull CharsetEncoder encoder,
            int inBufferSize, int outBufferSize
        ) {
            this.reader = reader;
            this.encoder = encoder;
            this.inBuffer = CharBuffer.allocate(inBufferSize);
            this.inBuffer.flip();
            this.outBuffer = ByteBuffer.allocate(outBufferSize);
            this.outBuffer.flip();
        }

        private CharsInputStream(
            @Nonnull Reader reader,
            @Nonnull Charset charset,
            int inBufferSize, int outBufferSize
        ) {
            this(
                reader,
                charset.newEncoder()
                    .onMalformedInput(CodingErrorAction.REPORT)
                    .onUnmappableCharacter(CodingErrorAction.REPORT),
                inBufferSize,
                outBufferSize
            );
        }

        private CharsInputStream(@Nonnull Reader reader, @Nonnull Charset charset) {
            this(reader, charset, CHARS_BUFFER_SIZE, BYTES_BUFFER_SIZE);
        }

        @Override
        public int read() throws IOException {
            int readNum = read(buf, 0, 1);
            return readNum < 0 ? -1 : (buf[0] & 0xff);
        }

        @Override
        protected int doRead(byte @Nonnull [] b, int off, int len) throws IOException {
            checkClosed();
            if (len == 0) {
                return 0;
            }
            int readNum = (int) read0(b, off, len);
            return readNum == 0 ? -1 : readNum;
        }

        @Override
        public long skip(long n) throws IOException {
            checkClosed();
            if (n <= 0) {
                return 0;
            }
            return read0(null, 0, n);
        }

        @Override
        public int available() {
            return outBuffer.remaining();
        }

        @Override
        public void close() throws IOException {
            reader.close();
            closed = true;
        }

        @Override
        public boolean markSupported() {
            return reader.markSupported();
        }

        @Override
        public void mark(int readlimit) {
            try {
                reader.mark(readlimit);
            } catch (IOException ignored) {
            }
            inCopy = BufferKit.copy(inBuffer);
            outCopy = BufferKit.copy(outBuffer);
            endCopy = endOfInput;
        }

        @Override
        public void reset() throws IOException {
            if (inCopy == null) {
                throw new IOException(MARK_NOT_SET);
            }
            reader.reset();
            inBuffer = inCopy;
            outBuffer = outCopy;
            endOfInput = endCopy;
            encoder.reset();
            inCopy = null;
            outCopy = null;
        }

        private long read0(byte @Nullable [] b, int off, long len) throws IOException {
            long count = 0;
            while (count < len) {
                if (outBuffer.hasRemaining()) {
                    int avail = (int) Math.min(outBuffer.remaining(), len - count);
                    if (b != null) {
                        outBuffer.get(b, (int) (off + count), avail);
                    } else {
                        outBuffer.position(outBuffer.position() + avail);
                    }
                    count += avail;
                } else if (endOfInput) {
                    if (inBuffer.hasRemaining()) {
                        flushOutBuffer();
                    } else {
                        break;
                    }
                } else {
                    readToInBuffer();
                }
            }
            return count;
        }

        private void readToInBuffer() throws IOException {
            flushInBuffer();
            flushOutBuffer();
        }

        private void flushInBuffer() throws IOException {
            inBuffer.compact();
            int readSize = reader.read(inBuffer);
            if (readSize < 0) {
                endOfInput = true;
            }
            inBuffer.flip();
        }

        private void flushOutBuffer() throws IOException {
            outBuffer.compact();
            CoderResult coderResult = encoder.encode(inBuffer, outBuffer, endOfInput);
            if (coderResult.isUnderflow() || coderResult.isOverflow()) {
                outBuffer.flip();
                return;
            }
            throw new IOException(encodingFailed(coderResult));
        }

        private void checkClosed() throws IOException {
            if (closed) {
                throw new IOException(STREAM_CLOSED);
            }
        }
    }

    private static final class LimitedInputStream extends DoReadStream {

        private final @Nonnull InputStream in;
        private final long limit;

        private long pos = 0;
        private long mark = 0;

        private LimitedInputStream(@Nonnull InputStream in, long limit) {
            this.in = in;
            this.limit = limit;
        }

        @Override
        public int read() throws IOException {
            if (pos >= limit) {
                return -1;
            }
            int ret = in.read();
            if (ret == -1) {
                return -1;
            }
            pos++;
            return ret;
        }

        @Override
        protected int doRead(byte @Nonnull [] b, int off, int len) throws IOException {
            if (pos >= limit) {
                return -1;
            }
            int ret = in.read(b, off, (int) Math.min(limit - pos, len));
            if (ret < 0) {
                return -1;
            }
            pos += ret;
            return ret;
        }

        @Override
        public long skip(long n) throws IOException {
            long remaining = limit - pos;
            long ret = in.skip(Math.min(n, remaining));
            pos += ret;
            return ret;
        }

        @Override
        public int available() throws IOException {
            return Math.min(in.available(), MathKit.safeInt(limit - pos));
        }

        @Override
        public boolean markSupported() {
            return in.markSupported();
        }

        @Override
        public void mark(int readAheadLimit) {
            in.mark(readAheadLimit);
            mark = pos;
        }

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

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

    private static final class EmptyInputStream extends DoReadStream {

        private static final @Nonnull EmptyInputStream SINGLETON = new EmptyInputStream();

        @Override
        public int read() {
            return -1;
        }

        @Override
        protected int doRead(byte @Nonnull [] b, int off, int len) {
            return len == 0 ? 0 : -1;
        }

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

    private static final class CharsReader extends DoReadReader {

        private final char @Nonnull [] buf;
        private int pos;
        private final int end;
        private int mark = -1;

        private CharsReader(char @Nonnull [] buf, int off, int len) {
            this.buf = buf;
            this.pos = off;
            this.end = off + len;
        }

        @Override
        public int read() {
            return (pos < end) ? buf[pos++] : -1;
        }

        @Override
        protected int doRead(char @Nonnull [] b, int off, int len) {
            if (len == 0) {
                return 0;
            }
            if (pos >= end) {
                return -1;
            }
            int avail = end - pos;
            avail = Math.min(len, avail);
            System.arraycopy(buf, pos, b, off, avail);
            pos += avail;
            return avail;
        }

        @Override
        public int read(@Nonnull CharBuffer target) {
            if (pos >= end) {
                return -1;
            }
            int avail = end - pos;
            avail = Math.min(target.remaining(), avail);
            target.put(buf, pos, avail);
            pos += avail;
            return avail;
        }

        @Override
        public long skip(long n) throws IllegalArgumentException {
            IOChecker.checkSkip(n);
            if (n == 0) {
                return 0;
            }
            int avail = end - pos;
            avail = (int) Math.min(n, avail);
            if (avail <= 0) {
                return 0;
            }
            pos += avail;
            return avail;
        }

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

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

        @Override
        public void mark(int readAheadLimit) {
            mark = pos;
        }

        @Override
        public void reset() throws IOException {
            if (mark < 0) {
                throw new IOException(MARK_NOT_SET);
            }
            pos = mark;
        }

        @Override
        public void close() {
        }
    }

    private static final class BufferReader extends DoReadReader {

        private final @Nonnull CharBuffer buffer;

        private BufferReader(@Nonnull CharBuffer buffer) {
            this.buffer = buffer;
        }

        @Override
        public int read() {
            if (buffer.remaining() <= 0) {
                return -1;
            }
            return buffer.get();
        }

        @Override
        protected int doRead(char @Nonnull [] c, int off, int len) {
            if (len == 0) {
                return 0;
            }
            if (!buffer.hasRemaining()) {
                return -1;
            }
            int avail = Math.min(buffer.remaining(), len);
            buffer.get(c, off, avail);
            return avail;
        }

        @Override
        public int read(@Nonnull CharBuffer target) throws IOException {
            return buffer.read(target);
        }

        @Override
        public long skip(long n) throws IllegalArgumentException {
            IOChecker.checkSkip(n);
            if (n == 0) {
                return 0;
            }
            int avail = (int) Math.min(buffer.remaining(), n);
            if (avail <= 0) {
                return 0;
            }
            buffer.position(buffer.position() + avail);
            return avail;
        }

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

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

        @Override
        public void mark(int readlimit) {
            buffer.mark();
        }

        @Override
        public void reset() throws IOException {
            try {
                buffer.reset();
            } catch (Exception e) {
                throw new IOException(e);
            }
        }

        @Override
        public void close() {
        }
    }

    private static final class BytesReader extends DoReadReader {

        private final @Nonnull InputStream inputStream;
        private final @Nonnull CharsetDecoder decoder;
        private ByteBuffer inBuffer;
        private CharBuffer outBuffer;
        private boolean endOfInput = false;
        private boolean closed = false;
        private final char @Nonnull [] cbuf = {0};

        // snapshot for mark/reset
        private ByteBuffer inCopy;
        private CharBuffer outCopy;
        private boolean endCopy;

        private BytesReader(
            @Nonnull InputStream inputStream,
            @Nonnull CharsetDecoder decoder,
            int inBufferSize, int outBufferSize
        ) {
            this.inputStream = inputStream;
            this.decoder = decoder;
            this.inBuffer = ByteBuffer.allocate(inBufferSize);
            this.inBuffer.flip();
            this.outBuffer = CharBuffer.allocate(outBufferSize);
            this.outBuffer.flip();
        }

        private BytesReader(
            @Nonnull InputStream inputStream,
            @Nonnull Charset charset,
            int inBufferSize, int outBufferSize
        ) {
            this(
                inputStream,
                charset.newDecoder()
                    .onMalformedInput(CodingErrorAction.REPORT)
                    .onUnmappableCharacter(CodingErrorAction.REPORT),
                inBufferSize,
                outBufferSize
            );
        }

        private BytesReader(@Nonnull InputStream inputStream, @Nonnull Charset charset) {
            this(inputStream, charset, BYTES_BUFFER_SIZE, CHARS_BUFFER_SIZE);
        }

        @Override
        public int read() throws IOException {
            int readNum = read(cbuf, 0, 1);
            return readNum < 0 ? -1 : cbuf[0];
        }

        @Override
        protected int doRead(char @Nonnull [] c, int off, int len) throws IOException {
            checkClosed();
            if (len == 0) {
                return 0;
            }
            int readNum = (int) read0(c, off, len);
            return readNum == 0 ? -1 : readNum;
        }

        @Override
        public long skip(long n) throws IllegalArgumentException, IOException {
            IOChecker.checkSkip(n);
            checkClosed();
            if (n == 0) {
                return 0;
            }
            return read0(null, 0, n);
        }

        @Override
        public boolean ready() {
            return outBuffer.hasRemaining();
        }

        @Override
        public void close() throws IOException {
            inputStream.close();
            closed = true;
        }

        @Override
        public boolean markSupported() {
            return inputStream.markSupported();
        }

        @Override
        public void mark(int readAheadLimit) {
            inputStream.mark(readAheadLimit);
            inCopy = BufferKit.copy(inBuffer);
            outCopy = BufferKit.copy(outBuffer);
            endCopy = endOfInput;
        }

        @Override
        public void reset() throws IOException {
            if (inCopy == null) {
                throw new IOException(MARK_NOT_SET);
            }
            inputStream.reset();
            inBuffer = inCopy;
            outBuffer = outCopy;
            endOfInput = endCopy;
            decoder.reset();
            inCopy = null;
            outCopy = null;
        }

        private long read0(char @Nullable [] b, int off, long len) throws IOException {
            long count = 0;
            while (count < len) {
                if (outBuffer.hasRemaining()) {
                    int avail = (int) Math.min(outBuffer.remaining(), len - count);
                    if (b != null) {
                        outBuffer.get(b, (int) (off + count), avail);
                    } else {
                        outBuffer.position(outBuffer.position() + avail);
                    }
                    count += avail;
                } else if (endOfInput) {
                    if (inBuffer.hasRemaining()) {
                        flushOutBuffer();
                    } else {
                        break;
                    }
                } else {
                    flushBuffer();
                }
            }
            return count;
        }

        private void flushBuffer() throws IOException {
            flushInBuffer();
            flushOutBuffer();
        }

        private void flushInBuffer() throws IOException {
            inBuffer.compact();
            int readSize = inputStream.read(inBuffer.array(), inBuffer.position(), inBuffer.remaining());
            if (readSize < 0) {
                endOfInput = true;
            }
            if (readSize > 0) {
                inBuffer.position(inBuffer.position() + readSize);
            }
            inBuffer.flip();
        }

        private void flushOutBuffer() throws IOException {
            outBuffer.compact();
            CoderResult coderResult = decoder.decode(inBuffer, outBuffer, endOfInput);
            if (coderResult.isUnderflow() || coderResult.isOverflow()) {
                outBuffer.flip();
                return;
            }
            throw new IOException(decodingFailed(coderResult));
        }

        private void checkClosed() throws IOException {
            if (closed) {
                throw new IOException(STREAM_CLOSED);
            }
        }
    }

    private static final class LimitedReader extends DoReadReader {

        private final @Nonnull Reader in;
        private final long limit;

        private long pos = 0;
        private long mark = 0;

        private LimitedReader(@Nonnull Reader in, long limit) {
            this.in = in;
            this.limit = limit;
        }

        @Override
        public int read() throws IOException {
            if (pos >= limit) {
                return -1;
            }
            int ret = in.read();
            if (ret == -1) {
                return -1;
            }
            pos++;
            return ret;
        }

        @Override
        protected int doRead(char @Nonnull [] b, int off, int len) throws IOException {
            if (pos >= limit) {
                return -1;
            }
            int ret = in.read(b, off, (int) Math.min(limit - pos, len));
            if (ret < 0) {
                return -1;
            }
            pos += ret;
            return ret;
        }

        @Override
        public long skip(long n) throws IllegalArgumentException, IOException {
            IOChecker.checkSkip(n);
            long remaining = limit - pos;
            long ret = in.skip(Math.min(n, remaining));
            pos += ret;
            return ret;
        }

        @Override
        public boolean ready() throws IOException {
            return in.ready();
        }

        @Override
        public boolean markSupported() {
            return in.markSupported();
        }

        @Override
        public void mark(int readAheadLimit) throws IOException {
            in.mark(readAheadLimit);
            mark = pos;
        }

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

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

    private static final class EmptyReader extends DoReadReader {

        private static final @Nonnull EmptyReader SINGLETON = new EmptyReader();

        @Override
        public int read(@Nonnull CharBuffer target) {
            return -1;
        }

        @Override
        public int read() {
            return -1;
        }

        @Override
        protected int doRead(char @Nonnull [] cbuf, int off, int len) {
            return len == 0 ? 0 : -1;
        }

        @Override
        public long skip(long n) throws IllegalArgumentException {
            IOChecker.checkSkip(n);
            return 0;
        }

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

        @Override
        public void close() {
        }
    }

    private static final class BytesOutputStream extends DoWriteStream {

        private final byte @Nonnull [] buf;
        private int pos;
        private final int end;

        private BytesOutputStream(byte @Nonnull [] buf, int off, int len) {
            this.buf = buf;
            this.pos = off;
            this.end = off + len;
        }

        @Override
        public void write(int b) throws IOException {
            int remaining = end - pos;
            if (remaining < 1) {
                throw new IOException(insufficientRemainingSpace(1, 0));
            }
            buf[pos] = (byte) b;
            pos++;
        }

        @Override
        protected void doWrite(byte @Nonnull [] b, int off, int len) throws IOException {
            if (len == 0) {
                return;
            }
            int remaining = end - pos;
            if (remaining < len) {
                throw new IOException(insufficientRemainingSpace(len, remaining));
            }
            System.arraycopy(b, off, buf, pos, len);
            pos += len;
        }
    }

    private static final class BufferOutputStream extends DoWriteStream {

        private final @Nonnull ByteBuffer buffer;

        private BufferOutputStream(@Nonnull ByteBuffer buffer) {
            this.buffer = buffer;
        }

        @Override
        public void write(int b) throws IOException {
            try {
                buffer.put((byte) b);
            } catch (Exception e) {
                throw new IOException(e);
            }
        }

        @Override
        protected void doWrite(byte @Nonnull [] b, int off, int len) throws IOException {
            if (len == 0) {
                return;
            }
            try {
                buffer.put(b, off, len);
            } catch (Exception e) {
                throw new IOException(e);
            }
        }
    }

    private static final class RafOutputStream extends DoWriteStream {

        private final @Nonnull RandomAccessFile raf;

        private RafOutputStream(@Nonnull RandomAccessFile raf, long seek) throws IOException {
            this.raf = raf;
            this.raf.seek(seek);
        }

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

        @Override
        protected void doWrite(byte @Nonnull [] b, int off, int len) throws IOException {
            if (len == 0) {
                return;
            }
            raf.write(b, off, len);
        }

        @Override
        public void flush() throws IOException {
            raf.getFD().sync();
        }

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

    private static final class AppenderOutputStream extends DoWriteStream {

        private final @Nonnull Appendable appender;
        private final @Nonnull CharsetDecoder decoder;
        private final @Nonnull ByteBuffer inBuffer;
        private final @Nonnull CharBuffer outBuffer;

        private boolean closed = false;
        private byte @Nullable [] writeOneBuf;

        private AppenderOutputStream(
            @Nonnull Appendable appender,
            @Nonnull CharsetDecoder decoder,
            int inBufferSize,
            int outBufferSize
        ) {
            this.appender = appender;
            this.decoder = decoder;
            this.inBuffer = ByteBuffer.allocate(inBufferSize);
            this.outBuffer = CharBuffer.allocate(outBufferSize);
        }

        private AppenderOutputStream(
            @Nonnull Appendable appender,
            @Nonnull Charset charset,
            int inBufferSize,
            int outBufferSize
        ) {
            this(
                appender,
                charset.newDecoder()
                    .onMalformedInput(CodingErrorAction.REPORT)
                    .onUnmappableCharacter(CodingErrorAction.REPORT),
                inBufferSize,
                outBufferSize
            );
        }

        private AppenderOutputStream(@Nonnull Appendable appender, @Nonnull Charset charset) {
            this(appender, charset, BYTES_BUFFER_SIZE, CHARS_BUFFER_SIZE);
        }

        @Override
        public void write(int b) throws IOException {
            if (writeOneBuf == null) {
                writeOneBuf = new byte[1];
            }
            writeOneBuf[0] = (byte) b;
            write(writeOneBuf, 0, 1);
        }

        @Override
        protected void doWrite(byte @Nonnull [] b, int off, int len) throws IOException {
            checkClosed();
            if (len == 0) {
                return;
            }
            int count = 0;
            while (count < len) {
                int actualLen = Math.min(inBuffer.remaining(), len - count);
                inBuffer.put(b, off + count, actualLen);
                inBuffer.flip();
                while (true) {
                    CoderResult coderResult = decoder.decode(inBuffer, outBuffer, false);
                    if (coderResult.isOverflow()) {
                        outBuffer.flip();
                        appender.append(outBuffer);
                        outBuffer.clear();
                    } else if (coderResult.isUnderflow()) {
                        outBuffer.flip();
                        appender.append(outBuffer);
                        outBuffer.clear();
                        break;
                    } else {
                        throw new IOException(decodingFailed(coderResult));
                    }
                }
                count += actualLen;
                inBuffer.compact();
            }
        }

        @Override
        public void flush() throws IOException {
            checkClosed();
            IOKit.flush(appender);
        }

        @Override
        public void close() throws IOException {
            if (closed) {
                return;
            }
            IOKit.close(appender);
            closed = true;
        }

        private void checkClosed() throws IOException {
            if (closed) {
                throw new IOException(STREAM_CLOSED);
            }
        }
    }

    private static final class LimitedOutputStream extends DoWriteStream {

        private final @Nonnull OutputStream out;
        private final long limit;

        private long pos;

        private LimitedOutputStream(@Nonnull OutputStream out, long limit) {
            this.out = out;
            this.limit = limit;
        }

        @Override
        public void write(int b) throws IOException {
            if (pos >= limit) {
                throw new IOException(insufficientRemainingSpace(1, 0));
            }
            out.write(b);
            pos++;
        }

        @Override
        protected void doWrite(byte @Nonnull [] b, int off, int len) throws IOException {
            long remaining = limit - pos;
            if (remaining < len) {
                throw new IOException(insufficientRemainingSpace(len, remaining));
            }
            out.write(b, off, len);
            pos += len;
        }

        @Override
        public void flush() throws IOException {
            out.flush();
        }

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

    private static final class NullOutputStream extends DoWriteStream {

        private static final @Nonnull NullOutputStream SINGLETON = new NullOutputStream();

        @Override
        public void write(int b) {
        }

        @Override
        protected void doWrite(byte @Nonnull [] b, int off, int len) {
        }
    }

    private static final class CharsWriter extends DoWriteWriter {

        private final char @Nonnull [] buf;
        private int pos;
        private final int end;

        private CharsWriter(char @Nonnull [] buf, int off, int len) {
            this.buf = buf;
            this.pos = off;
            this.end = off + len;
        }

        @Override
        public void write(int c) throws IOException {
            int remaining = end - pos;
            if (remaining < 1) {
                throw new IOException(insufficientRemainingSpace(1, remaining));
            }
            buf[pos] = (char) c;
            pos++;
        }

        @Override
        protected void doWrite(char @Nonnull [] cbuf, int off, int len) throws IOException {
            if (len == 0) {
                return;
            }
            int remaining = end - pos;
            if (remaining < len) {
                throw new IOException(insufficientRemainingSpace(len, remaining));
            }
            System.arraycopy(cbuf, off, buf, pos, len);
            pos += len;
        }

        @Override
        protected void doWrite(@Nonnull String str, int off, int len) throws IOException {
            if (len == 0) {
                return;
            }
            int remaining = end - pos;
            if (remaining < len) {
                throw new IOException(insufficientRemainingSpace(len, remaining));
            }
            str.getChars(off, off + len, buf, pos);
            pos += len;
        }

        @Override
        public void flush() {
        }

        @Override
        public void close() {
        }
    }

    private static final class BufferWriter extends DoWriteWriter {

        private final CharBuffer buffer;

        private BufferWriter(@Nonnull CharBuffer buffer) {
            this.buffer = buffer;
        }

        @Override
        public void write(int c) throws IOException {
            try {
                buffer.put((char) c);
            } catch (Exception e) {
                throw new IOException(e);
            }
        }

        @Override
        protected void doWrite(char @Nonnull [] c, int off, int len) throws IOException {
            try {
                buffer.put(c, off, len);
            } catch (Exception e) {
                throw new IOException(e);
            }
        }

        @Override
        protected void doWrite(@Nonnull String str, int off, int len) throws IOException {
            try {
                buffer.put(str, off, off + len);
            } catch (Exception e) {
                throw new IOException(e);
            }
        }

        @Override
        public void flush() {
        }

        @Override
        public void close() {
        }
    }

    private static final class BytesWriter extends DoWriteWriter {

        private final @Nonnull OutputStream outputStream;
        private final @Nonnull CharsetEncoder encoder;
        private final @Nonnull CharBuffer inBuffer;
        private final @Nonnull ByteBuffer outBuffer;

        private boolean closed = false;
        private char @Nullable [] writeOneBuf;

        private BytesWriter(@Nonnull OutputStream outputStream, @Nonnull CharsetEncoder encoder, int inBufferSize, int outBufferSize) {
            this.outputStream = outputStream;
            this.encoder = encoder;
            this.inBuffer = CharBuffer.allocate(inBufferSize);
            this.inBuffer.flip();
            this.outBuffer = ByteBuffer.allocate(outBufferSize);
            this.outBuffer.flip();
        }

        private BytesWriter(@Nonnull OutputStream outputStream, @Nonnull Charset charset, int inBufferSize, int outBufferSize) {
            this(
                outputStream,
                charset.newEncoder()
                    .onMalformedInput(CodingErrorAction.REPORT)
                    .onUnmappableCharacter(CodingErrorAction.REPORT),
                inBufferSize,
                outBufferSize
            );
        }

        private BytesWriter(@Nonnull OutputStream outputStream, @Nonnull Charset charset) {
            this(outputStream, charset, 64, 64);
        }

        @Override
        public void write(int c) throws IOException {
            if (writeOneBuf == null) {
                writeOneBuf = new char[1];
            }
            writeOneBuf[0] = (char) c;
            write(writeOneBuf, 0, 1);
        }

        @Override
        protected void doWrite(char @Nonnull [] cbuf, int off, int len) throws IOException {
            doWrite0(cbuf, off, len);
        }

        @Override
        protected void doWrite(@Nonnull String str, int off, int len) throws IOException {
            doWrite0(str, off, len);
        }

        private void doWrite0(@Nonnull Object cbuf, int off, int len) throws IOException {
            checkClosed();
            if (len == 0) {
                return;
            }
            int count = 0;
            while (count < len) {
                int actualLen = Math.min(inBuffer.remaining(), len - count);
                if (cbuf instanceof char[]) {
                    inBuffer.put((char[]) cbuf, off + count, actualLen);
                } else {
                    inBuffer.put((String) cbuf, off + count, off + count + actualLen);
                }
                inBuffer.flip();
                while (true) {
                    CoderResult coderResult = encoder.encode(inBuffer, outBuffer, false);
                    if (coderResult.isOverflow()) {
                        outBuffer.flip();
                        BufferKit.readTo(outBuffer, outputStream);
                        // outputStream.append(outBuffer);
                        outBuffer.clear();
                    } else if (coderResult.isUnderflow()) {
                        outBuffer.flip();
                        BufferKit.readTo(outBuffer, outputStream);
                        // appender.append(outBuffer);
                        outBuffer.clear();
                        break;
                    } else {
                        throw new IOException(decodingFailed(coderResult));
                    }
                }
                count += actualLen;
                inBuffer.compact();
            }
        }

        @Override
        public void flush() throws IOException {
            checkClosed();
            outputStream.flush();
        }

        @Override
        public void close() throws IOException {
            if (closed) {
                return;
            }
            outputStream.close();
            closed = true;
        }

        private void checkClosed() throws IOException {
            if (closed) {
                throw new IOException(STREAM_CLOSED);
            }
        }
    }

    private static final class LimitedWriter extends DoWriteWriter {

        private final @Nonnull Writer out;
        private final long limit;

        private long pos;

        private LimitedWriter(@Nonnull Writer out, long limit) {
            this.out = out;
            this.limit = limit;
        }

        @Override
        public void write(int c) throws IOException {
            if (pos >= limit) {
                throw new IOException(insufficientRemainingSpace(1, 0));
            }
            out.write(c);
            pos++;
        }

        @Override
        protected void doWrite(char @Nonnull [] cbuf, int off, int len) throws IOException {
            long remaining = limit - pos;
            if (remaining < len) {
                throw new IOException(insufficientRemainingSpace(len, remaining));
            }
            out.write(cbuf, off, len);
            pos += len;
        }

        @Override
        protected void doWrite(@Nonnull String str, int off, int len) throws IOException {
            long remaining = limit - pos;
            if (remaining < len) {
                throw new IOException(insufficientRemainingSpace(len, remaining));
            }
            out.write(str, off, len);
            pos += len;
        }

        @Override
        public void flush() throws IOException {
            out.flush();
        }

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

    private static final class NullWriter extends DoWriteWriter {

        private static final @Nonnull NullWriter SINGLETON = new NullWriter();

        @Override
        public void write(int c) {
        }

        @Override
        protected void doWrite(char @Nonnull [] cbuf, int off, int len) {
        }

        @Override
        protected void doWrite(@Nonnull String str, int off, int len) {
        }

        @Override
        public Writer append(CharSequence csq) {
            return this;
        }

        @Override
        public Writer append(CharSequence csq, int start, int end) {
            IOChecker.checkOffLen(start, end - start, csq.length());
            return this;
        }

        @Override
        public void flush() {
        }

        @Override
        public void close() {
        }
    }

    private IOBack() {
    }
}