
local ffi = require "ffi"
local bit = require "bit"
local bxor = bit.bxor
local bnot = bit.bnot
local band = bit.band
local bor = bit.bor
local rshift = bit.rshift
local lshift = bit.lshift

--local mutils = require "memutils"
--local stringz = require "stringzutils"

ffi.cdef[[
typedef struct MD5Context {
  uint32_t buf[4];
  uint32_t bits[2];
  unsigned char input[64];
} MD5_CTX;
]]
local MD5_CTX = ffi.typeof("MD5_CTX");


local function byteReverse(buf, len)
end

function F1(x, y, z) return bxor(z, band(x, bxor(y, z))) end
function F2(x, y, z) return F1(z, x, y) end
function F3(x, y, z) return bxor(x, y, z) end
function F4(x, y, z) return bxor(y, bor(x, bnot(z))) end

function MD5STEP(f, w, x, y, z, data, s)
	w = w + f(x, y, z) + data;
	w = bor(lshift(w,s), rshift(w,(32-s)))
	w = w + x;

	return w;
end

function printmd5ctx(ctx)
	for i=0,3 do
		print(string.format("ctx.buf[%d]: 0x%x", i, ctx.buf[i]));
	end

	print(string.format("ctx.bits[0]: %d", ctx.bits[0]));
	print(string.format("ctx.bits[1]: %d", ctx.bits[1]));
end

-- Start MD5 accumulation.  Set bit count to 0 and buffer to mysterious
-- initialization constants.
function MD5Init(ctx)
	ctx.buf[0] = 0x67452301;
	ctx.buf[1] = 0xefcdab89;
	ctx.buf[2] = 0x98badcfe;
	ctx.buf[3] = 0x10325476;

	ctx.bits[0] = 0;
	ctx.bits[1] = 0;
end

function MD5Transform(buf, input)
	local a = buf[0];
	local b = buf[1];
	local c = buf[2];
	local d = buf[3];

	a = MD5STEP(F1, a, b, c, d, input[0] + 0xd76aa478, 7);
	d = MD5STEP(F1, d, a, b, c, input[1] + 0xe8c7b756, 12);
	c = MD5STEP(F1, c, d, a, b, input[2] + 0x242070db, 17);
	b = MD5STEP(F1, b, c, d, a, input[3] + 0xc1bdceee, 22);
	a = MD5STEP(F1, a, b, c, d, input[4] + 0xf57c0faf, 7);
	d = MD5STEP(F1, d, a, b, c, input[5] + 0x4787c62a, 12);
	c = MD5STEP(F1, c, d, a, b, input[6] + 0xa8304613, 17);
	b = MD5STEP(F1, b, c, d, a, input[7] + 0xfd469501, 22);
	a = MD5STEP(F1, a, b, c, d, input[8] + 0x698098d8, 7);
	d = MD5STEP(F1, d, a, b, c, input[9] + 0x8b44f7af, 12);
	c = MD5STEP(F1, c, d, a, b, input[10] + 0xffff5bb1, 17);
	b = MD5STEP(F1, b, c, d, a, input[11] + 0x895cd7be, 22);
	a = MD5STEP(F1, a, b, c, d, input[12] + 0x6b901122, 7);
	d = MD5STEP(F1, d, a, b, c, input[13] + 0xfd987193, 12);
	c = MD5STEP(F1, c, d, a, b, input[14] + 0xa679438e, 17);
	b = MD5STEP(F1, b, c, d, a, input[15] + 0x49b40821, 22);

  a = MD5STEP(F2, a, b, c, d, input[1] + 0xf61e2562, 5);
  d = MD5STEP(F2, d, a, b, c, input[6] + 0xc040b340, 9);
  c = MD5STEP(F2, c, d, a, b, input[11] + 0x265e5a51, 14);
  b = MD5STEP(F2, b, c, d, a, input[0] + 0xe9b6c7aa, 20);
  a = MD5STEP(F2, a, b, c, d, input[5] + 0xd62f105d, 5);
  d = MD5STEP(F2, d, a, b, c, input[10] + 0x02441453, 9);
  c = MD5STEP(F2, c, d, a, b, input[15] + 0xd8a1e681, 14);
  b = MD5STEP(F2, b, c, d, a, input[4] + 0xe7d3fbc8, 20);
  a = MD5STEP(F2, a, b, c, d, input[9] + 0x21e1cde6, 5);
  d = MD5STEP(F2, d, a, b, c, input[14] + 0xc33707d6, 9);
  c = MD5STEP(F2, c, d, a, b, input[3] + 0xf4d50d87, 14);
  b = MD5STEP(F2, b, c, d, a, input[8] + 0x455a14ed, 20);
  a = MD5STEP(F2, a, b, c, d, input[13] + 0xa9e3e905, 5);
  d = MD5STEP(F2, d, a, b, c, input[2] + 0xfcefa3f8, 9);
  c = MD5STEP(F2, c, d, a, b, input[7] + 0x676f02d9, 14);
  b = MD5STEP(F2, b, c, d, a, input[12] + 0x8d2a4c8a, 20);

  a = MD5STEP(F3, a, b, c, d, input[5] + 0xfffa3942, 4);
  d = MD5STEP(F3, d, a, b, c, input[8] + 0x8771f681, 11);
  c = MD5STEP(F3, c, d, a, b, input[11] + 0x6d9d6122, 16);
  b = MD5STEP(F3, b, c, d, a, input[14] + 0xfde5380c, 23);
  a = MD5STEP(F3, a, b, c, d, input[1] + 0xa4beea44, 4);
  d = MD5STEP(F3, d, a, b, c, input[4] + 0x4bdecfa9, 11);
  c = MD5STEP(F3, c, d, a, b, input[7] + 0xf6bb4b60, 16);
  b = MD5STEP(F3, b, c, d, a, input[10] + 0xbebfbc70, 23);
  a = MD5STEP(F3, a, b, c, d, input[13] + 0x289b7ec6, 4);
  d = MD5STEP(F3, d, a, b, c, input[0] + 0xeaa127fa, 11);
  c = MD5STEP(F3, c, d, a, b, input[3] + 0xd4ef3085, 16);
  b = MD5STEP(F3, b, c, d, a, input[6] + 0x04881d05, 23);
  a = MD5STEP(F3, a, b, c, d, input[9] + 0xd9d4d039, 4);
  d = MD5STEP(F3, d, a, b, c, input[12] + 0xe6db99e5, 11);
  c = MD5STEP(F3, c, d, a, b, input[15] + 0x1fa27cf8, 16);
  b = MD5STEP(F3, b, c, d, a, input[2] + 0xc4ac5665, 23);

  a = MD5STEP(F4, a, b, c, d, input[0] + 0xf4292244, 6);
  d = MD5STEP(F4, d, a, b, c, input[7] + 0x432aff97, 10);
  c = MD5STEP(F4, c, d, a, b, input[14] + 0xab9423a7, 15);
  b = MD5STEP(F4, b, c, d, a, input[5] + 0xfc93a039, 21);
  a = MD5STEP(F4, a, b, c, d, input[12] + 0x655b59c3, 6);
  d = MD5STEP(F4, d, a, b, c, input[3] + 0x8f0ccc92, 10);
  c = MD5STEP(F4, c, d, a, b, input[10] + 0xffeff47d, 15);
  b = MD5STEP(F4, b, c, d, a, input[1] + 0x85845dd1, 21);
  a = MD5STEP(F4, a, b, c, d, input[8] + 0x6fa87e4f, 6);
  d = MD5STEP(F4, d, a, b, c, input[15] + 0xfe2ce6e0, 10);
  c = MD5STEP(F4, c, d, a, b, input[6] + 0xa3014314, 15);
  b = MD5STEP(F4, b, c, d, a, input[13] + 0x4e0811a1, 21);
  a = MD5STEP(F4, a, b, c, d, input[4] + 0xf7537e82, 6);
  d = MD5STEP(F4, d, a, b, c, input[11] + 0xbd3af235, 10);
  c = MD5STEP(F4, c, d, a, b, input[2] + 0x2ad7d2bb, 15);
  b = MD5STEP(F4, b, c, d, a, input[9] + 0xeb86d391, 21);

  buf[0] = buf[0] + a;
  buf[1] = buf[1] + b;
  buf[2] = buf[2] + c;
  buf[3] = buf[3] + d;
end

local function MD5Update(ctx, buf, len)
	local t;

	t = ctx.bits[0];
	ctx.bits[0] = t + lshift( len, 3)
	if (ctx.bits[0] < t) then
		ctx.bits[1] = ctx.bits[1] + 1;
	end

	ctx.bits[1] = ctx.bits[1] + rshift(len, 29);

	t = band(rshift(t, 3), 0x3f);

	if (t > 0) then
		p = ffi.cast("unsigned char *", ctx.input + t);

		t = 64 - t;
		if (len < t) then
			memcpy(p, buf, len);
			return;
		end

		memcpy(p, buf, t);
		byteReverse(ctx.input, 16);
		MD5Transform(ctx.buf, ffi.cast("uint32_t *", ctx.input));
		buf = buf + t;
		len = len - t;
	end

	while (len >= 64) do
		memcpy(ctx.input, buf, 64);
		byteReverse(ctx.input, 16);
		MD5Transform(ctx.buf, ffi.cast("uint32_t *", ctx.input));
		buf = buf + 64;
		len = len - 64;
	end

	memcpy(ctx.input, buf, len);
end

local function MD5Final(digest, ctx)

	local count;
	local p;

	count = band(rshift(ctx.bits[0], 3), 0x3F);

	p = ctx.input + count;
	p[0] = 0x80;
	p = p + 1;
	count = 64 - 1 - count;

	if (count < 8) then
		memset(p, 0, count);
		byteReverse(ctx.input, 16);
		MD5Transform(ctx.buf, ffi.cast("uint32_t *", ctx.input));
		memset(ctx.input, 0, 56);
	else
		memset(p, 0, count - 8);
	end

	byteReverse(ctx.input, 14);

	ffi.cast("uint32_t *", ctx.input)[14] = ctx.bits[0];
	ffi.cast("uint32_t *", ctx.input)[15] = ctx.bits[1];

	MD5Transform(ctx.buf, ffi.cast("uint32_t *", ctx.input));
	byteReverse(ffi.cast("unsigned char *",ctx.buf), 4);
	memcpy(digest, ctx.buf, 16);
	memset(ffi.cast("char *", ctx), 0, ffi.sizeof(ctx));
end


local function md5(luastr)
	local buf = ffi.new("char[33]");
	local hash = ffi.new("uint8_t[16]");
	local len = #luastr
	local p = ffi.cast("const char *", luastr);

	local ctx = MD5_CTX();

	MD5Init(ctx);

	MD5Update(ctx, p, len);
	MD5Final(hash, ctx);
	bin2str(buf, hash, ffi.sizeof(hash));

	return ffi.string(buf);
end


return md5;

