package com.jscape.util.io;

import com.jscape.util.IOTools;

import java.io.IOException;
import java.io.InputStream;
import java.util.zip.Deflater;

import static com.jscape.util.Parameters.assertNotNull;

/**
 * @author Alex Usun
 */
public class DeflaterInputStream
        extends InputStream {

    private static final int DEFAULT_BUFFER_SIZE = 512;
    private static final int INITIAL_BUFFER_SIZE = 1;
    private static final int EOF = -1;

    private final InputStream base;
    private final Deflater deflater;
    private final byte[] deflaterBuffer;
    private byte[] buffer;
    private boolean eofReached;

    public DeflaterInputStream(InputStream base, Deflater deflater, int bufferSize) {
        assertNotNull(base);
        this.base = base;

        assertNotNull(deflater);
        this.deflater = deflater;

        this.deflaterBuffer = new byte[bufferSize];
        this.buffer = new byte[INITIAL_BUFFER_SIZE];
    }

    public DeflaterInputStream(InputStream base, Deflater deflater) {
        this(base, deflater, DEFAULT_BUFFER_SIZE);
    }

    public DeflaterInputStream(InputStream base) {
        this(base, new Deflater());
    }

    public void close()
            throws IOException {
        try {
            this.deflater.end();
        } finally {
            IOTools.close(this.base);
        }
    }

    public int read()
            throws IOException {
        int read = read(this.buffer, 0, INITIAL_BUFFER_SIZE);
        return read == EOF ? EOF : this.buffer[0] & 0xFF;
    }

    public int read(byte[] data, int offset, int length)
            throws IOException {
        int read = 0;
        while (length > 0 && !this.deflater.finished()) {
            updateDeflator();

            int compressedDataLength = this.deflater.deflate(data, offset, length);
            read += compressedDataLength;
            offset += compressedDataLength;
            length -= compressedDataLength;
        }

        if (read == 0 && this.deflater.finished()) {
            this.eofReached = true;
            read = EOF;
        }

        return read;
    }

    public long skip(long n)
            throws IOException {
        if (this.buffer.length < DEFAULT_BUFFER_SIZE) {
            this.buffer = new byte[DEFAULT_BUFFER_SIZE];
        }

        int total = (int) Math.min(n, Integer.MAX_VALUE);
        long skipped = 0;

        while (total > 0) {
            int read = read(this.buffer, 0, Math.min(total, this.buffer.length));

            if (read < 0) {
                break;
            }

            skipped += read;
            total -= read;
        }

        return skipped;
    }

    public int available()
            throws IOException {
        return this.eofReached ? 0 : INITIAL_BUFFER_SIZE;
    }

    private void updateDeflator()
            throws IOException {
        if (this.deflater.needsInput()) {
            int read = this.base.read(this.deflaterBuffer, 0, this.deflaterBuffer.length);

            if (read > 0) {
                this.deflater.setInput(this.deflaterBuffer, 0, read);
            } else if (read < 0) {
                this.deflater.finish();
            }
        }
    }

}
