/*
 * Copyright 2004-2023 H2 Group. Multiple-Licensed under the MPL 2.0,
 * and the EPL 1.0 (https://h2database.com/html/license.html).
 * Initial Developer: H2 Group
 */
package org.h2.compress;

import java.io.IOException;
import java.io.InputStream;
import org.h2.mvstore.DataUtils;
import org.h2.util.Utils;

/**
 * An input stream to read from an LZF stream.
 * The data is automatically expanded.
 */
public class LZFInputStream extends InputStream {

    private final InputStream in;
    private CompressLZF decompress = new CompressLZF();
    private int pos;
    private int bufferLength;
    private byte[] inBuffer;
    private byte[] buffer;

    public LZFInputStream(InputStream in) throws IOException {
        this.in = in;
        if (readInt() != LZFOutputStream.MAGIC) {
            throw new IOException("Not an LZFInputStream");
        }
    }

    private static byte[] ensureSize(byte[] buff, int len) {
        return buff == null || buff.length < len ? Utils.newBytes(len) : buff;
    }

    private void fillBuffer() throws IOException {
        if (buffer != null && pos < bufferLength) {
            return;
        }
        int len = readInt();
        if (decompress == null) {
            // EOF
            this.bufferLength = 0;
        } else if (len < 0) {
            len = -len;
            buffer = ensureSize(buffer, len);
            readFully(buffer, len);
            this.bufferLength = len;
        } else {
            inBuffer = ensureSize(inBuffer, len);
            int size = readInt();
            readFully(inBuffer, len);
            buffer = ensureSize(buffer, size);
            try {
                decompress.expand(inBuffer, 0, len, buffer, 0, size);
            } catch (ArrayIndexOutOfBoundsException e) {
                throw DataUtils.convertToIOException(e);
            }
            this.bufferLength = size;
        }
        pos = 0;
    }

    private void readFully(byte[] buff, int len) throws IOException {
        int off = 0;
        while (len > 0) {
            int l = in.read(buff, off, len);
            len -= l;
            off += l;
        }
    }

    private int readInt() throws IOException {
        int x = in.read();
        if (x < 0) {
            decompress = null;
            return 0;
        }
        x = (x << 24) + (in.read() << 16) + (in.read() << 8) + in.read();
        return x;
    }

    @Override
    public int read() throws IOException {
        fillBuffer();
        if (pos >= bufferLength) {
            return -1;
        }
        return buffer[pos++] & 255;
    }

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

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        if (len == 0) {
            return 0;
        }
        int read = 0;
        while (len > 0) {
            int r = readBlock(b, off, len);
            if (r < 0) {
                break;
            }
            read += r;
            off += r;
            len -= r;
        }
        return read == 0 ? -1 : read;
    }

    private int readBlock(byte[] b, int off, int len) throws IOException {
        fillBuffer();
        if (pos >= bufferLength) {
            return -1;
        }
        int max = Math.min(len, bufferLength - pos);
        max = Math.min(max, b.length - off);
        System.arraycopy(buffer, pos, b, off, max);
        pos += max;
        return max;
    }

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

}
