--[[--
MD5 hash library.
  ]]

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 copy = ffi.copy
local fill = ffi.fill

ffi.cdef[[
typedef struct MD5Context {
  uint32_t buf[4];
  uint32_t bits[2];
  unsigned char input[64];
} MD5_CTX;
]]

local function byteReverse(buf, len)
    -- TODO: implement for big-endian architectures?
end

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

local 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

-- Start MD5 accumulation.  Set bit count to 0 and buffer to mysterious
-- initialization constants.
local 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

local 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] = band(buf[0] + a, 0xFFFFFFFF)
    buf[1] = band(buf[1] + b, 0xFFFFFFFF)
    buf[2] = band(buf[2] + c, 0xFFFFFFFF)
    buf[3] = band(buf[3] + d, 0xFFFFFFFF)
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
            copy(p, buf, len)
            return
        end

        copy(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
        copy(ctx.input, buf, 64)
        byteReverse(ctx.input, 16)
        MD5Transform(ctx.buf, ffi.cast("uint32_t *", ctx.input))
        buf = buf + 64
        len = len - 64
    end

    copy(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
        fill(p, count, 0)
        byteReverse(ctx.input, 16)
        MD5Transform(ctx.buf, ffi.cast("uint32_t *", ctx.input))
        fill(ctx.input, 56, 0)
    else
        fill(p, count - 8, 0)
    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)
    copy(digest, ctx.buf, 16)
    fill(ffi.cast("char *", ctx), ffi.sizeof(ctx), 0)
end

local hex = ffi.new("const char[16]", "0123456789abcdef")
local function bin2str(output, input, len)
    if len > 0 then
        output[0] = hex[rshift(input[0], 4)]
        output[1] = hex[band(input[0], 0xF)]
        return bin2str(output+2, input+1, len-1)
    end
end

local md5 = {}

--- Create a new md5 hashing instance.
---- @return md5 instance
function md5:new()
    self.ctx = ffi.new("MD5_CTX")
    MD5Init(self.ctx)
end

--- Feed content to md5 hashing instance.
---- @param luastr Lua string
function md5:update(luastr)
    MD5Update(self.ctx, ffi.cast("const char*", luastr), #luastr)
end

--- Calcualte md5 sum.
---- @param luastr Lua string
---- @return md5 sum in Lua string
function md5:sum(luastr)
    local buf = ffi.new("char[33]")
    local hash = ffi.new("uint8_t[16]")
    if luastr then
        md5:new()
        md5:update(luastr)
    end
    MD5Final(hash, self.ctx)

    bin2str(buf, hash, ffi.sizeof(hash))

    return ffi.string(buf)
end

return md5
