#include "lualcm_hash.h"
#include "lua_ver_helper.h"
#include "stdio.h"

/** @file */

/* hash userdata */
typedef struct impl_hash_userdata {
    uint64_t hash; /**< The actual 64-bit integer used as the hash. */
} impl_hash_userdata_t;

/* methods */
static int impl_hash_new(lua_State *);
static int impl_hash_tobytes(lua_State *);
static int impl_hash_rotate(lua_State *);

/* metamethods */
static int impl_hash_add(lua_State *);
static int impl_hash_tostring(lua_State *);

/* supporting functions */
static impl_hash_userdata_t *impl_hash_newuserdata(lua_State *);
static impl_hash_userdata_t *impl_hash_checkudata(lua_State *, int);

/**
 * Makes the hash userdata's metatable. The metatable is named "lcm._hash".
 *
 * @post A metatable exists named "lcm._hash" and contains
 *     all of the hash userdata's member functions.
 *
 * @param L The Lua state.
 */
void ll_hash_makemetatable(lua_State *L)
{
    /* create empty meta table */
    if (!luaL_newmetatable(L, "lcm._hash")) {
        lua_pushstring(L, "cannot create metatable");
        lua_error(L);
    }

    const struct luaL_Reg metas[] = {
        {"__add", impl_hash_add}, {"__tostring", impl_hash_tostring}, {NULL, NULL},
    };

    /* register to meta */
    luaX_registertable(L, metas);

    const struct luaL_Reg methods[] = {
        {"tobytes", impl_hash_tobytes}, {"rotate", impl_hash_rotate}, {NULL, NULL},
    };

    /* register methods to new table, set __index */
    lua_pushstring(L, "__index");
    lua_newtable(L);
    luaX_registertable(L, methods);
    lua_rawset(L, -3);

    /* TODO hide metatable */
    /*lua_pushstring(L, "__metatable");
    lua_pushnil(L);
    lua_rawset(L, -3);*/

    /* pop the metatable */
    lua_pop(L, 1);
}

/**
 * Registers the hash userdata's "new" function.
 *
 * @post All LCM functions have been added to the LCM module.
 *     The modules table is on the top of the stack.
 *
 * @param L The Lua state.
 */
void ll_hash_register_new(lua_State *L)
{
    const struct luaL_Reg new_function[] = {
        {"new", impl_hash_new}, {NULL, NULL},
    };

    luaX_registerglobal(L, "lcm._hash", new_function);
}

/**
 *
 */
void ll_hash_fromvalue(lua_State *L, uint64_t hash)
{
    impl_hash_userdata_t *hashu = impl_hash_newuserdata(L);
    hashu->hash = hash;
}

/**
 *
 */
uint64_t ll_hash_tovalue(lua_State *L, int index)
{
    impl_hash_userdata_t *hashu = impl_hash_checkudata(L, index);
    return hashu->hash;
}

/**
 * Creates and initializes an hash userdata.
 *
 * Lua does not support 64-bit integers, so the hash is initialized using a
 * string representing 64-bit hex number using hexdecimal,
 * i.e. "0x0123456701234567". The number is parsed by sscanf.
 *
 * @pre The Lua arguments on the stack:
 *     A string representing a 64-bit number.
 *
 * @post The Lua return values on the stack:
 *     The new hash userdata.
 *
 * @param L The Lua state.
 * @return The number of return values on the Lua stack.
 *
 * @throws Lua error if hash userdata cannot be created.
 */
static int impl_hash_new(lua_State *L)
{
    /* we expect 1 argument */
    lua_settop(L, 1);

    /* first arg is a string containing the provider */
    const char *hash_str = luaL_checkstring(L, 1);

    /* use sscanf to parse the string */
    /* should use uint64_t here, but it causes a warning  with sscanf */
    unsigned long long hash = 0;
    int matches = sscanf(hash_str, "%llx", &hash);
    if (matches != 1) {
        lua_pushstring(L, "error creating hash");
        lua_error(L);
    }

    /* create hash userdata */
    impl_hash_userdata_t *hashu = impl_hash_newuserdata(L);
    hashu->hash = hash;

    /* return hash userdata, which is on top of the stack */

    return 1;
}

/**
 * Convert the hash userdata to bytes.
 *
 * TODO Endianness?
 *
 * @pre The Lua arguments on the stack:
 *     A hash userdata (self).
 *
 * @post The Lua return values on the stack:
 *     Eight integers representing the bytes of the hash.
 *
 * @param L The Lua state.
 * @return The number of return values on the Lua stack.
 */
static int impl_hash_tobytes(lua_State *L)
{
    /* we expect 1 argument */
    lua_settop(L, 1);

    /* get the hash userdata */
    impl_hash_userdata_t *hashu = impl_hash_checkudata(L, 1);

    uint8_t *bytes = (uint8_t *) &hashu->hash;

    int i;
    for (i = 0; i < sizeof(hashu->hash); i++) {
        lua_pushinteger(L, bytes[i]);
    }

    return i;
}

/**
 * Rotates a hash userdata to the left.
 *
 * The number of bits to rotate is given as an argument. Bits are shifted left,
 * bits shifted past the MSB wrap around to the LSB.
 *
 * The number of bits to rotate should be between 0 and 63, but other integers
 * are accepted, including negative values. The number of bits to rotate is
 * always adjusted to the range of 0 to 63.
 *
 * The rotation is actually done by:
 * @c hash = hash << rotation | hash >> 64 - rotation;
 *
 * @pre The Lua arguments on the stack:
 *     A hash userdata (self), an integer containing the number of bits to
 *     rotate.
 *
 * @post The Lua return values on the stack:
 *     The hash userdata.
 *
 * @param L The Lua state.
 * @return The number of return values on the Lua stack.
 */
static int impl_hash_rotate(lua_State *L)
{
    /* we expect 2 arguments */
    lua_settop(L, 2);

    /* get the hash userdata */
    impl_hash_userdata_t *hashu = impl_hash_checkudata(L, 1);

    /* get the rotation */
    lua_Integer rotation = luaL_checkinteger(L, 2);

    /* do a fancy modulo, because 64 is 2^8, result is always positive*/
    rotation &= 63;

    hashu->hash = hashu->hash << rotation | hashu->hash >> 64 - rotation;

    /* pop the rotation int from the stack, return the hash */
    lua_pop(L, 1);

    return 1;
}

/**
 * Adds two hash userdatas. This is the __add metamethod of the hash
 * userdata.
 *
 * @pre The Lua arguments on the stack:
 *     Two hash userdatas (one of them being self).
 *
 * @post The Lua return values on the stack:
 *     A sumed hash userdata.
 *
 * @param L The Lua state.
 * @return The number of return values on the Lua stack.
 */
static int impl_hash_add(lua_State *L)
{
    /* we expect 2 arguments */
    lua_settop(L, 2);

    /* get the hash userdatas */
    impl_hash_userdata_t *left_hashu = impl_hash_checkudata(L, 1);
    impl_hash_userdata_t *right_hashu = impl_hash_checkudata(L, 2);

    /* make result */
    impl_hash_userdata_t *new_hashu = impl_hash_newuserdata(L);
    new_hashu->hash = left_hashu->hash + right_hashu->hash;

    return 1;
}

/**
 * Creates a string from a hash userdata. This is the __tostring metamethod
 * of the hash userdata.
 *
 * @pre The Lua arguments on the stack:
 *     A hash userdata (self).
 *
 * @post The Lua return values on the stack:
 *     A string representing the hash userdata.
 *
 * @param L The Lua state.
 * @return The number of return values on the Lua stack.
 */
static int impl_hash_tostring(lua_State *L)
{
    /* we expect 1 argument */
    lua_settop(L, 1);

    /* get the hash userdata */
    impl_hash_userdata_t *hashu = impl_hash_checkudata(L, 1);

    /* convert uint64_t to string */
    /* use snprintf because lua_pushfstring can't handle %llx */
    /* have to cast uint64_t to avoid warning with snprintf */
    char hash_str[20];
#ifndef WIN32
    snprintf(hash_str, 20, "0x%llx", (unsigned long long) hashu->hash);
#else
    _snprintf(hash_str, 20, "0x%llx", (unsigned long long) hashu->hash);
#endif
    /* make the string */
    lua_pushfstring(L, "lcm._hash = %s (@ %p)", hash_str, hashu);

    return 1;
}

/**
 * Creates a new hash userdata. The userdata is defined using the
 * "lcm._hash" metatable.
 *
 * @pre The Lua arguments on the stack:
 *     Nothing.
 *
 * @pre The metatable "lcm._hash" is defined.
 *
 * @post The Lua return values on the stack:
 *     The new hash userdata.
 *
 * @param L The Lua state.
 * @return A pointer to the new hash userdata.
 */
static impl_hash_userdata_t *impl_hash_newuserdata(lua_State *L)
{
    /* make new user data */
    impl_hash_userdata_t *hashu =
        (impl_hash_userdata_t *) lua_newuserdata(L, sizeof(impl_hash_userdata_t));

    /* initialize struct */
    hashu->hash = 0;

    /* set the metatable */
    luaL_getmetatable(L, "lcm._hash");
    if (lua_isnil(L, -1)) {
        lua_pushstring(L, "cannot find metatable");
        lua_error(L);
    }
    lua_setmetatable(L, -2);

    return hashu;
}

/**
 * Checks for a hash userdata at the given index. Checks the userdata type
 * using the "lcm._hash" metatable.
 *
 * @pre The Lua arguments on the stack:
 *     A hash userdata at the given index.
 *
 * @pre The metatable "lcm._hash" is defined.
 *
 * @post The Lua return values on the stack:
 *     Nothing.
 *
 * @param L The Lua state.
 * @param index The index of the hash userdata.
 * @return A pointer to the hash userdata.
 */
static impl_hash_userdata_t *impl_hash_checkudata(lua_State *L, int index)
{
    return (impl_hash_userdata_t *) luaL_checkudata(L, index, "lcm._hash");
}
