local Bit = require("lockbox.util.bit");
local Array = require("lockbox.util.array");
local Stream = require("lockbox.util.stream");
local Math = require("math");

local AND = Bit.band;
local RSHIFT = Bit.rshift;

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

local PBKDF2 = function()

    local public = {};

    local blockLen = 16;
    local dKeyLen = 256;
    local iterations = 4096;

    local salt;
    local password;


    local PRF;

    local dKey;


    public.setBlockLen = function(len)
        blockLen = len;
        return public;
    end

    public.setDKeyLen = function(len)
        dKeyLen = len
        return public;
    end

    public.setIterations = function(iter)
        iterations = iter;
        return public;
    end

    public.setSalt = function(saltBytes)
        salt = saltBytes;
        return public;
    end

    public.setPassword = function(passwordBytes)
        password = passwordBytes;
        return public;
    end

    public.setPRF = function(prf)
        PRF = prf;
        return public;
    end

    local buildBlock = function(i)
        local b0, b1, b2, b3 = word2bytes(i);
        local ii = {b0, b1, b2, b3};
        local s = Array.concat(salt, ii);

        local out = {};

        PRF.setKey(password);
        for c = 1, iterations do
            PRF.init()
                .update(Stream.fromArray(s));

            s = PRF.finish().asBytes();
            if(c > 1) then
                out = Array.XOR(out, s);
            else
                out = s;
            end
        end

        return out;
    end

    public.finish = function()
        local blocks = Math.ceil(dKeyLen / blockLen);

        dKey = {};

        for b = 1, blocks do
            local block = buildBlock(b);
            dKey = Array.concat(dKey, block);
        end

        if(Array.size(dKey) > dKeyLen) then dKey = Array.truncate(dKey, dKeyLen); end

        return public;
    end

    public.asBytes = function()
        return dKey;
    end

    public.asHex = function()
        return Array.toHex(dKey);
    end

    public.asString = function()
        return Array.toString(dKey);
    end

    return public;
end

return PBKDF2;
