package io.netty.zlib;

import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

public class InflaterInputStream extends FilterInputStream {
    protected final Inflater inflater;
    protected byte[] buf;

    private boolean closed = false;

    private boolean eof = false;

    private boolean close_in = true;

    protected static final int DEFAULT_BUFSIZE = 512;

    public InflaterInputStream(InputStream in) throws IOException {
        this(in, false);
    }

    public InflaterInputStream(InputStream in, boolean nowrap) throws IOException {
        this(in, new Inflater(nowrap));
        myinflater = true;
    }

    public InflaterInputStream(InputStream in, Inflater inflater) throws IOException {
        this(in, inflater, DEFAULT_BUFSIZE);
    }

    public InflaterInputStream(InputStream in,
                               Inflater inflater, int size) throws IOException {
        this(in, inflater, size, true);
    }

    public InflaterInputStream(InputStream in,
                               Inflater inflater,
                               int size, boolean close_in) throws IOException {
        super(in);
        if (in == null || inflater == null) {
            throw new NullPointerException();
        } else if (size <= 0) {
            throw new IllegalArgumentException("buffer size must be greater than 0");
        }
        this.inflater = inflater;
        buf = new byte[size];
        this.close_in = close_in;
    }

    protected boolean myinflater = false;

    private final byte[] byte1 = new byte[1];

    public int read() throws IOException {
        if (closed) {
            throw new IOException("Stream closed");
        }
        return read(byte1, 0, 1) == -1 ? -1 : byte1[0] & 0xff;
    }

    public int read(byte[] b, int off, int len) throws IOException {
        if (closed) {
            throw new IOException("Stream closed");
        }
        if (b == null) {
            throw new NullPointerException();
        } else if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return 0;
        } else if (eof) {
            return -1;
        }

        int n = 0;
        inflater.setOutput(b, off, len);
        while (!eof) {
            if (inflater.avail_in == 0)
                fill();
            int err = inflater.inflate(JZlib.Z_NO_FLUSH);
            n += inflater.next_out_index - off;
            off = inflater.next_out_index;
            switch (err) {
                case JZlib.Z_DATA_ERROR:
                    throw new IOException(inflater.msg);
                case JZlib.Z_STREAM_END:
                case JZlib.Z_NEED_DICT:
                    eof = true;
                    if (err == JZlib.Z_NEED_DICT)
                        return -1;
                    break;
                default:
            }
            if (inflater.avail_out == 0)
                break;
        }
        return n;
    }

    public int available() throws IOException {
        if (closed) {
            throw new IOException("Stream closed");
        }
        if (eof) {
            return 0;
        } else {
            return 1;
        }
    }

    private final byte[] b = new byte[512];

    public long skip(long n) throws IOException {
        if (n < 0) {
            throw new IllegalArgumentException("negative skip length");
        }

        if (closed) {
            throw new IOException("Stream closed");
        }

        int max = (int) Math.min(n, Integer.MAX_VALUE);
        int total = 0;
        while (total < max) {
            int len = max - total;
            if (len > b.length) {
                len = b.length;
            }
            len = read(b, 0, len);
            if (len == -1) {
                eof = true;
                break;
            }
            total += len;
        }
        return total;
    }

    public void close() throws IOException {
        if (!closed) {
            if (myinflater)
                inflater.end();
            if (close_in)
                in.close();
            closed = true;
        }
    }

    protected void fill() throws IOException {
        if (closed) {
            throw new IOException("Stream closed");
        }
        int len = in.read(buf, 0, buf.length);
        if (len == -1) {
            if (inflater.istate.wrap == 0 &&
                    !inflater.finished()) {
                buf[0] = 0;
                len = 1;
            } else if (inflater.istate.was != -1) {  // in reading trailer
                throw new IOException("footer is not found");
            } else {
                throw new EOFException("Unexpected end of ZLIB input stream");
            }
        }
        inflater.setInput(buf, 0, len, true);
    }

    public boolean markSupported() {
        return false;
    }

    public synchronized void mark(int readlimit) {
    }

    public synchronized void reset() throws IOException {
        throw new IOException("mark/reset not supported");
    }

    public long getTotalIn() {
        return inflater.getTotalIn();
    }

    public long getTotalOut() {
        return inflater.getTotalOut();
    }

    public byte[] getAvailIn() {
        if (inflater.avail_in <= 0)
            return null;
        byte[] tmp = new byte[inflater.avail_in];
        System.arraycopy(inflater.next_in, inflater.next_in_index,
                tmp, 0, inflater.avail_in);
        return tmp;
    }

    public void readHeader() throws IOException {

        byte[] empty = "".getBytes();
        inflater.setInput(empty, 0, 0, false);
        inflater.setOutput(empty, 0, 0);

        int err = inflater.inflate(JZlib.Z_NO_FLUSH);
        if (!inflater.istate.inParsingHeader()) {
            return;
        }

        byte[] b1 = new byte[1];
        do {
            int i = in.read(b1);
            if (i <= 0)
                throw new IOException("no input");
            inflater.setInput(b1);
            err = inflater.inflate(JZlib.Z_NO_FLUSH);
            if (err != 0/*Z_OK*/)
                throw new IOException(inflater.msg);
        }
        while (inflater.istate.inParsingHeader());
    }

    public Inflater getInflater() {
        return inflater;
    }
}