local Bit = require(".crypto.util.bit")
local Queue = require(".crypto.util.queue")

local AND = Bit.band
local OR = Bit.bor
local NOT = Bit.bnot
local XOR = Bit.bxor
local LROT = Bit.lrotate
local LSHIFT = Bit.lshift
local RSHIFT = Bit.rshift

--MD4 is little-endian
local bytes2word = function(b0, b1, b2, b3)
	local i = b3
	i = LSHIFT(i, 8)
	i = OR(i, b2)
	i = LSHIFT(i, 8)
	i = OR(i, b1)
	i = LSHIFT(i, 8)
	i = OR(i, b0)
	return i
end

local word2bytes = function(word)
	local b0, b1, b2, b3
	b0 = AND(word, 0xFF)
	word = RSHIFT(word, 8)
	b1 = AND(word, 0xFF)
	word = RSHIFT(word, 8)
	b2 = AND(word, 0xFF)
	word = RSHIFT(word, 8)
	b3 = AND(word, 0xFF)
	return b0, b1, b2, b3
end

local dword2bytes = function(i)
	local b4, b5, b6, b7 = word2bytes(math.floor(i / 0x100000000))
	local b0, b1, b2, b3 = word2bytes(i)
	return b0, b1, b2, b3, b4, b5, b6, b7
end

local F = function(x, y, z)
	return OR(AND(x, y), AND(NOT(x), z))
end
local G = function(x, y, z)
	return OR(AND(x, y), OR(AND(x, z), AND(y, z)))
end
local H = function(x, y, z)
	return XOR(x, XOR(y, z))
end

local MD4 = function(stream)
	local queue = Queue()

	local A = 0x67452301
	local B = 0xefcdab89
	local C = 0x98badcfe
	local D = 0x10325476
	local public = {}

	local processBlock = function()
		local a = A
		local b = B
		local c = C
		local d = D

		local X = {}

		for i = 0, 15 do
			X[i] = bytes2word(queue.pop(), queue.pop(), queue.pop(), queue.pop())
		end

		a = LROT(a + F(b, c, d) + X[0], 3)
		d = LROT(d + F(a, b, c) + X[1], 7)
		c = LROT(c + F(d, a, b) + X[2], 11)
		b = LROT(b + F(c, d, a) + X[3], 19)

		a = LROT(a + F(b, c, d) + X[4], 3)
		d = LROT(d + F(a, b, c) + X[5], 7)
		c = LROT(c + F(d, a, b) + X[6], 11)
		b = LROT(b + F(c, d, a) + X[7], 19)

		a = LROT(a + F(b, c, d) + X[8], 3)
		d = LROT(d + F(a, b, c) + X[9], 7)
		c = LROT(c + F(d, a, b) + X[10], 11)
		b = LROT(b + F(c, d, a) + X[11], 19)

		a = LROT(a + F(b, c, d) + X[12], 3)
		d = LROT(d + F(a, b, c) + X[13], 7)
		c = LROT(c + F(d, a, b) + X[14], 11)
		b = LROT(b + F(c, d, a) + X[15], 19)

		a = LROT(a + G(b, c, d) + X[0] + 0x5A827999, 3)
		d = LROT(d + G(a, b, c) + X[4] + 0x5A827999, 5)
		c = LROT(c + G(d, a, b) + X[8] + 0x5A827999, 9)
		b = LROT(b + G(c, d, a) + X[12] + 0x5A827999, 13)

		a = LROT(a + G(b, c, d) + X[1] + 0x5A827999, 3)
		d = LROT(d + G(a, b, c) + X[5] + 0x5A827999, 5)
		c = LROT(c + G(d, a, b) + X[9] + 0x5A827999, 9)
		b = LROT(b + G(c, d, a) + X[13] + 0x5A827999, 13)

		a = LROT(a + G(b, c, d) + X[2] + 0x5A827999, 3)
		d = LROT(d + G(a, b, c) + X[6] + 0x5A827999, 5)
		c = LROT(c + G(d, a, b) + X[10] + 0x5A827999, 9)
		b = LROT(b + G(c, d, a) + X[14] + 0x5A827999, 13)

		a = LROT(a + G(b, c, d) + X[3] + 0x5A827999, 3)
		d = LROT(d + G(a, b, c) + X[7] + 0x5A827999, 5)
		c = LROT(c + G(d, a, b) + X[11] + 0x5A827999, 9)
		b = LROT(b + G(c, d, a) + X[15] + 0x5A827999, 13)

		a = LROT(a + H(b, c, d) + X[0] + 0x6ED9EBA1, 3)
		d = LROT(d + H(a, b, c) + X[8] + 0x6ED9EBA1, 9)
		c = LROT(c + H(d, a, b) + X[4] + 0x6ED9EBA1, 11)
		b = LROT(b + H(c, d, a) + X[12] + 0x6ED9EBA1, 15)

		a = LROT(a + H(b, c, d) + X[2] + 0x6ED9EBA1, 3)
		d = LROT(d + H(a, b, c) + X[10] + 0x6ED9EBA1, 9)
		c = LROT(c + H(d, a, b) + X[6] + 0x6ED9EBA1, 11)
		b = LROT(b + H(c, d, a) + X[14] + 0x6ED9EBA1, 15)

		a = LROT(a + H(b, c, d) + X[1] + 0x6ED9EBA1, 3)
		d = LROT(d + H(a, b, c) + X[9] + 0x6ED9EBA1, 9)
		c = LROT(c + H(d, a, b) + X[5] + 0x6ED9EBA1, 11)
		b = LROT(b + H(c, d, a) + X[13] + 0x6ED9EBA1, 15)

		a = LROT(a + H(b, c, d) + X[3] + 0x6ED9EBA1, 3)
		d = LROT(d + H(a, b, c) + X[11] + 0x6ED9EBA1, 9)
		c = LROT(c + H(d, a, b) + X[7] + 0x6ED9EBA1, 11)
		b = LROT(b + H(c, d, a) + X[15] + 0x6ED9EBA1, 15)

		A = AND(A + a, 0xFFFFFFFF)
		B = AND(B + b, 0xFFFFFFFF)
		C = AND(C + c, 0xFFFFFFFF)
		D = AND(D + d, 0xFFFFFFFF)
	end

	for s in stream do
		queue.push(s)
		if queue.size() >= 64 then
			processBlock()
		end
	end

	local bits = queue.getHead() * 8

	queue.push(0x80)
	while ((queue.size() + 7) % 64) < 63 do
		queue.push(0x00)
	end

	local b0, b1, b2, b3, b4, b5, b6, b7 = dword2bytes(bits)

	queue.push(b0)
	queue.push(b1)
	queue.push(b2)
	queue.push(b3)
	queue.push(b4)
	queue.push(b5)
	queue.push(b6)
	queue.push(b7)

	while queue.size() > 0 do
		processBlock()
	end

	public.asBytes = function()
		local b0, b1, b2, b3 = word2bytes(A)
		local b4, b5, b6, b7 = word2bytes(B)
		local b8, b9, b10, b11 = word2bytes(C)
		local b12, b13, b14, b15 = word2bytes(D)

		return { b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15 }
	end

	public.asHex = function()
		local b0, b1, b2, b3 = word2bytes(A)
		local b4, b5, b6, b7 = word2bytes(B)
		local b8, b9, b10, b11 = word2bytes(C)
		local b12, b13, b14, b15 = word2bytes(D)

		return string.format(
			"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
			b0,
			b1,
			b2,
			b3,
			b4,
			b5,
			b6,
			b7,
			b8,
			b9,
			b10,
			b11,
			b12,
			b13,
			b14,
			b15
		)
	end

	public.asString = function()
		local b0, b1, b2, b3 = word2bytes(A)
		local b4, b5, b6, b7 = word2bytes(B)
		local b8, b9, b10, b11 = word2bytes(C)
		local b12, b13, b14, b15 = word2bytes(D)

		return string.pack(string.rep("B", 16), b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15)
	end

	return public
end

return MD4
