package org.openrs2.compress.gzip

import java.io.DataInputStream
import java.io.IOException
import java.io.InputStream
import java.util.zip.Inflater

public class GzipLaxInputStream(
    private val input: InputStream
) : InputStream() {
    private val inflater = Inflater(true)
    private val buffer = ByteArray(4096)
    private var checkedTrailer = false

    init {
        val dataInput = DataInputStream(input)
        if (dataInput.readUnsignedShort() != HEADER_MAGIC) {
            throw IOException("Invalid GZIP header magic")
        } else if (dataInput.readUnsignedByte() != METHOD_DEFLATE) {
            throw IOException("Unsupported compression method")
        }

        val flags = dataInput.readUnsignedByte()
        dataInput.skip(6)

        if ((flags and FLAG_EXTRA) != 0) {
            dataInput.skip(dataInput.readUnsignedShort().toLong())
        }

        if ((flags and FLAG_NAME) != 0) {
            while (dataInput.readUnsignedByte() != 0) {
                // empty
            }
        }

        if ((flags and FLAG_COMMENT) != 0) {
            while (dataInput.readUnsignedByte() != 0) {
                // empty
            }
        }

        if ((flags and FLAG_HEADER_CRC) != 0) {
            dataInput.skip(2)
        }
    }

    override fun read(): Int {
        val n = read(buffer, 0, 1)
        return if (n < 0) {
            -1
        } else {
            buffer[0].toInt() and 0xFF
        }
    }

    override fun read(b: ByteArray, off: Int, len: Int): Int {
        while (true) {
            val n = inflater.inflate(b, off, len)
            if (n != 0) {
                return n
            }

            when {
                inflater.finished() -> {
                    checkTrailer()
                    return -1
                }
                inflater.needsInput() -> fill()
                inflater.needsDictionary() -> throw IOException("Dictionaries not supported")
            }
        }
    }

    override fun close() {
        inflater.end()
        input.close()
    }

    private fun fill() {
        val n = input.read(buffer, 0, buffer.size)
        if (n < 0) {
            throw IOException("Compressed data truncated")
        }
        inflater.setInput(buffer, 0, n)
    }

    private fun checkTrailer() {
        if (checkedTrailer) {
            return
        }

        checkedTrailer = true

        var len = TRAILER_LEN
        len -= inflater.remaining

        if (len < 0) {
            throw IOException("Compressed data overflow")
        } else if (input.skip(len) != len) {
            throw IOException("GZIP trailer missing")
        }
    }

    private companion object {
        private const val HEADER_MAGIC = 0x1F8B

        private const val METHOD_DEFLATE = 8

        private const val FLAG_HEADER_CRC = 0x2
        private const val FLAG_EXTRA = 0x4
        private const val FLAG_NAME = 0x8
        private const val FLAG_COMMENT = 0x10

        private const val TRAILER_LEN = 8L
    }
}
