package org.openrs2.crypto

import com.google.common.base.Preconditions
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufUtil

public fun ByteBuf.whirlpool(): ByteArray {
    return whirlpool(readerIndex(), readableBytes())
}

public fun ByteBuf.whirlpool(index: Int, len: Int): ByteArray {
    Preconditions.checkPositionIndexes(index, index + len, capacity())

    return if (hasArray()) {
        Whirlpool.whirlpool(array(), arrayOffset() + index, len)
    } else {
        Whirlpool.whirlpool(ByteBufUtil.getBytes(this, index, len, false))
    }
}

public class Whirlpool {
    private val bitLength = ByteArray(32)
    private val buffer = ByteArray(64)
    private var bufferBits = 0
    private var bufferPos = 0
    private val hash = LongArray(8)
    private val K = LongArray(8)
    private val L = LongArray(8)
    private val block = LongArray(8)
    private val state = LongArray(8)

    private fun processBuffer() {
        var j = 0
        for (i in 0 until 8) {
            block[i] = (buffer[j].toLong() shl 56) xor
                ((buffer[j + 1].toLong() and 0xFFL) shl 48) xor
                ((buffer[j + 2].toLong() and 0xFFL) shl 40) xor
                ((buffer[j + 3].toLong() and 0xFFL) shl 32) xor
                ((buffer[j + 4].toLong() and 0xFFL) shl 24) xor
                ((buffer[j + 5].toLong() and 0xFFL) shl 16) xor
                ((buffer[j + 6].toLong() and 0xFFL) shl 8) xor
                (buffer[j + 7].toLong() and 0xFFL)
            j += 8
        }

        for (i in 0 until 8) {
            K[i] = hash[i]
            state[i] = block[i] xor K[i]
        }

        for (r in 1..R) {
            for (i in 0 until 8) {
                L[i] = 0

                var s = 56
                for (t in 0 until 8) {
                    L[i] = L[i] xor C[t][(K[(i - t) and 7] ushr s).toInt() and 0xFF]
                    s -= 8
                }
            }

            for (i in 0 until 8) {
                K[i] = L[i]
            }

            K[0] = K[0] xor rc[r]

            for (i in 0 until 8) {
                L[i] = K[i]

                var s = 56
                for (t in 0 until 8) {
                    L[i] = L[i] xor C[t][(state[(i - t) and 7] ushr s).toInt() and 0xFF]
                    s -= 8
                }
            }

            for (i in 0 until 8) {
                state[i] = L[i]
            }
        }

        for (i in 0 until 8) {
            hash[i] = hash[i] xor state[i] xor block[i]
        }
    }

    public fun NESSIEinit() {
        bitLength.fill(0)
        bufferBits = 0
        bufferPos = 0
        buffer[0] = 0
        hash.fill(0)
    }

    public fun NESSIEadd(source: ByteArray, bits: Long) {
        var sourceBits = bits
        var sourcePos = 0
        val sourceGap = (8 - (sourceBits.toInt() and 7)) and 7
        val bufferRem = bufferBits and 7
        var b: Int

        var value = sourceBits
        var carry = 0
        for (i in 31 downTo 0) {
            carry += (bitLength[i].toInt() and 0xFF) + (value.toInt() and 0xFF)
            bitLength[i] = carry.toByte()
            carry = carry ushr 8
            value = value ushr 8
        }

        while (sourceBits > 8) {
            b = ((source[sourcePos].toInt() shl sourceGap) and 0xFF) or
                ((source[sourcePos + 1].toInt() and 0xFF) ushr (8 - sourceGap))
            check(b in 0..255)

            buffer[bufferPos] = (buffer[bufferPos].toInt() or (b ushr bufferRem)).toByte()
            bufferPos++
            bufferBits += 8 - bufferRem

            if (bufferBits == 512) {
                processBuffer()
                bufferBits = 0
                bufferPos = 0
            }

            buffer[bufferPos] = ((b shl (8 - bufferRem)) and 0xFF).toByte()
            bufferBits += bufferRem

            sourceBits -= 8
            sourcePos++
        }

        if (sourceBits > 0) {
            b = (source[sourcePos].toInt() shl sourceGap) and 0xFF
            buffer[bufferPos] = (buffer[bufferPos].toInt() or (b ushr bufferRem)).toByte()
        } else {
            b = 0
        }

        if (bufferRem + sourceBits < 8) {
            bufferBits += sourceBits.toInt()
        } else {
            bufferPos++
            bufferBits += 8 - bufferRem
            sourceBits -= 8 - bufferRem

            if (bufferBits == 512) {
                processBuffer()
                bufferBits = 0
                bufferPos = 0
            }

            buffer[bufferPos] = ((b shl (8 - bufferRem)) and 0xFF).toByte()
            bufferBits += sourceBits.toInt()
        }
    }

    public fun NESSIEfinalize(digest: ByteArray) {
        buffer[bufferPos] = (buffer[bufferPos].toInt() or (0x80 ushr (bufferBits and 7))).toByte()
        bufferPos++

        if (bufferPos > 32) {
            while (bufferPos < 64) {
                buffer[bufferPos++] = 0
            }

            processBuffer()
            bufferPos = 0
        }

        while (bufferPos < 32) {
            buffer[bufferPos++] = 0
        }

        bitLength.copyInto(buffer, destinationOffset = 32, startIndex = 0, endIndex = 32)
        processBuffer()

        var j = 0
        for (i in 0 until 8) {
            val h = hash[i]
            digest[j] = (h ushr 56).toByte()
            digest[j + 1] = (h ushr 48).toByte()
            digest[j + 2] = (h ushr 40).toByte()
            digest[j + 3] = (h ushr 32).toByte()
            digest[j + 4] = (h ushr 24).toByte()
            digest[j + 5] = (h ushr 16).toByte()
            digest[j + 6] = (h ushr 8).toByte()
            digest[j + 7] = h.toByte()
            j += 8
        }
    }

    public companion object {
        private const val DIGESTBITS = 512
        public const val DIGESTBYTES: Int = DIGESTBITS ushr 3
        private const val R = 10
        private const val sbox = "\u1823\uC6E8\u87B8\u014F\u36A6\uD2F5\u796F\u9152" +
            "\u60BC\u9B8E\uA30C\u7B35\u1DE0\uD7C2\u2E4B\uFE57" +
            "\u1577\u37E5\u9FF0\u4ADA\u58C9\u290A\uB1A0\u6B85" +
            "\uBD5D\u10F4\uCB3E\u0567\uE427\u418B\uA77D\u95D8" +
            "\uFBEE\u7C66\uDD17\u479E\uCA2D\uBF07\uAD5A\u8333" +
            "\u6302\uAA71\uC819\u49D9\uF2E3\u5B88\u9A26\u32B0" +
            "\uE90F\uD580\uBECD\u3448\uFF7A\u905F\u2068\u1AAE" +
            "\uB454\u9322\u64F1\u7312\u4008\uC3EC\uDBA1\u8D3D" +
            "\u9700\uCF2B\u7682\uD61B\uB5AF\u6A50\u45F3\u30EF" +
            "\u3F55\uA2EA\u65BA\u2FC0\uDE1C\uFD4D\u9275\u068A" +
            "\uB2E6\u0E1F\u62D4\uA896\uF9C5\u2559\u8472\u394C" +
            "\u5E78\u388C\uD1A5\uE261\uB321\u9C1E\u43C7\uFC04" +
            "\u5199\u6D0D\uFADF\u7E24\u3BAB\uCE11\u8F4E\uB7EB" +
            "\u3C81\u94F7\uB913\u2CD3\uE76E\uC403\u5644\u7FA9" +
            "\u2ABB\uC153\uDC0B\u9D6C\u3174\uF646\uAC89\u14E1" +
            "\u163A\u6909\u70B6\uD0ED\uCC42\u98A4\u285C\uF886"
        private val C = Array(8) { LongArray(256) }
        private val rc = LongArray(R + 1)

        init {
            for (x in 0 until 256) {
                val c = sbox[x / 2]

                val v1 = if ((x and 1) == 0) {
                    c.code.toLong() ushr 8
                } else {
                    c.code.toLong() and 0xFF
                }

                var v2 = v1 shl 1
                if (v2 >= 0x100) {
                    v2 = v2 xor 0x11D
                }

                var v4 = v2 shl 1
                if (v4 >= 0x100) {
                    v4 = v4 xor 0x11D
                }

                val v5 = v4 xor v1

                var v8 = v4 shl 1
                if (v8 >= 0x100) {
                    v8 = v8 xor 0x11D
                }

                val v9 = v8 xor v1

                C[0][x] = (v1 shl 56) or (v1 shl 48) or (v4 shl 40) or (v1 shl 32) or
                    (v8 shl 24) or (v5 shl 16) or (v2 shl 8) or v9

                for (t in 1 until 8) {
                    C[t][x] = (C[t - 1][x] ushr 8) or (C[t - 1][x] shl 56)
                }
            }

            rc[0] = 0

            for (r in 1..R) {
                val i = 8 * (r - 1)
                rc[r] = (C[0][i] and -0x100000000000000L) xor
                    (C[1][i + 1] and 0x00FF000000000000L) xor
                    (C[2][i + 2] and 0x0000FF0000000000L) xor
                    (C[3][i + 3] and 0x000000FF00000000L) xor
                    (C[4][i + 4] and 0x00000000FF000000L) xor
                    (C[5][i + 5] and 0x0000000000FF0000L) xor
                    (C[6][i + 6] and 0x000000000000FF00L) xor
                    (C[7][i + 7] and 0x00000000000000FFL)
            }
        }

        @JvmOverloads
        @JvmStatic
        public fun whirlpool(data: ByteArray, off: Int = 0, len: Int = data.size): ByteArray {
            val source: ByteArray
            if (off <= 0) {
                source = data
            } else {
                source = ByteArray(len)
                for (i in 0 until len) {
                    source[i] = data[off + i]
                }
            }

            val whirlpool = Whirlpool()
            whirlpool.NESSIEinit()
            whirlpool.NESSIEadd(source, (len * 8).toLong())

            val digest = ByteArray(DIGESTBYTES)
            whirlpool.NESSIEfinalize(digest)
            return digest
        }
    }
}
