/*
    See license.txt in the root of this project.
*/

# include "luametatex.h"
# include "lmtoptional.h"

typedef void           mysql_instance;
typedef void           mysql_result;
typedef char         **mysql_row;
typedef unsigned int   mysql_offset;

typedef struct mysql_field {
    char          *name;
    char          *org_name;
    char          *table;
    char          *org_table;
    char          *db;
    char          *catalog;
    char          *def;
    unsigned long  length;
    unsigned long  max_length;
    unsigned int   name_length;
    unsigned int   org_name_length;
    unsigned int   table_length;
    unsigned int   org_table_length;
    unsigned int   db_length;
    unsigned int   catalog_length;
    unsigned int   def_length;
    unsigned int   flags;
    unsigned int   decimals;
    unsigned int   charsetnr;
    int            type;
    void          *extension;
} mysql_field;

# define MYSQLLIB_METATABLE  "luatex.mysqllib"

typedef struct mysqllib_data {
    mysql_instance * db;
} mysqllib_data ;

typedef struct mysqllib_state_info {

    int initialized;
    int padding;

    mysql_instance * (*mysql_init) (
        mysql_instance *mysql
    );

    mysql_instance * (*mysql_real_connect) (
        mysql_instance *mysql,
        const char     *host,
        const char     *user,
        const char     *passwd,
        const char     *db,
        unsigned int    port,
        const char     *unix_socket,
        unsigned long   clientflag
    );

    unsigned int (*mysql_errno) (
        mysql_instance *mysql
    );

    const char * (*mysql_error) (
        mysql_instance *mysql
    );

    int (*mysql_real_query) (
        mysql_instance *mysql,
        const char     *q,
        unsigned long   length
    );

    mysql_result * (*mysql_store_result) (
        mysql_instance *mysql
    );

    void (*mysql_free_result) (
        mysql_result *result
    );

    unsigned long long (*mysql_num_rows) (
        mysql_result *res
    );

    mysql_row (*mysql_fetch_row) (
        mysql_result *result
    );

    unsigned int (*mysql_affected_rows) (
        mysql_instance *mysql
    );

    unsigned int (*mysql_field_count) (
        mysql_instance *mysql
    );

    unsigned int (*mysql_num_fields) (
        mysql_result *res
    );

    mysql_field * (*mysql_fetch_fields) (
        mysql_result *res
    );

    mysql_offset (*mysql_field_seek) (
        mysql_result *result,
        mysql_offset  offset
    );

    void (*mysql_close) (
        mysql_instance *sock
    );

} mysqllib_state_info;

static mysqllib_state_info mysqllib_state = {
    .initialized         = 0,
    .padding             = 0,
    .mysql_init          = NULL,
    .mysql_real_connect  = NULL,
    .mysql_errno         = NULL,
    .mysql_error         = NULL,
    .mysql_real_query    = NULL,
    .mysql_store_result  = NULL,
    .mysql_free_result   = NULL,
    .mysql_num_rows      = NULL,
    .mysql_fetch_row     = NULL,
    .mysql_affected_rows = NULL,
    .mysql_field_count   = NULL,
    .mysql_num_fields    = NULL,
    .mysql_fetch_fields  = NULL,
    .mysql_field_seek    = NULL,
    .mysql_close         = NULL,
};

static int mysqllib_initialize(lua_State * L)
{
    if (! mysqllib_state.initialized) {
        const char *filename = lua_tostring(L, 1);
        if (filename != NULL) {

            lmt_library lib = lmt_library_load(filename);

            mysqllib_state.mysql_init          = lmt_library_find(lib, "mysql_init" );
            mysqllib_state.mysql_real_connect  = lmt_library_find(lib, "mysql_real_connect" );
            mysqllib_state.mysql_errno         = lmt_library_find(lib, "mysql_errno" );
            mysqllib_state.mysql_error         = lmt_library_find(lib, "mysql_error" );
            mysqllib_state.mysql_real_query    = lmt_library_find(lib, "mysql_real_query" );
            mysqllib_state.mysql_store_result  = lmt_library_find(lib, "mysql_store_result" );
            mysqllib_state.mysql_free_result   = lmt_library_find(lib, "mysql_free_result" );
            mysqllib_state.mysql_num_rows      = lmt_library_find(lib, "mysql_num_rows" );
            mysqllib_state.mysql_fetch_row     = lmt_library_find(lib, "mysql_fetch_row" );
            mysqllib_state.mysql_affected_rows = lmt_library_find(lib, "mysql_affected_rows" );
            mysqllib_state.mysql_field_count   = lmt_library_find(lib, "mysql_field_count" );
            mysqllib_state.mysql_num_fields    = lmt_library_find(lib, "mysql_num_fields" );
            mysqllib_state.mysql_fetch_fields  = lmt_library_find(lib, "mysql_fetch_fields" );
            mysqllib_state.mysql_field_seek    = lmt_library_find(lib, "mysql_field_seek" );
            mysqllib_state.mysql_close         = lmt_library_find(lib, "mysql_close" );

            mysqllib_state.initialized = lmt_library_okay(lib);
        }
    }
    lua_pushboolean(L, mysqllib_state.initialized);
    return 1;
}

static int mysqllib_open(lua_State * L)
{
    if (mysqllib_state.initialized) {
        const char     *database = luaL_checkstring(L, 1);
        const char     *username = luaL_optstring(L, 2, NULL);
        const char     *password = luaL_optstring(L, 3, NULL);
        const char     *host     = luaL_optstring(L, 4, NULL);
        int             port     = lmt_optinteger(L, 5, 0);
        const char     *socket   = NULL; /* luaL_optstring(L, 6, NULL); */
        int             flag     = 0;    /* luaL_optinteger(L, 7, 0); */
        mysql_instance *db       = mysqllib_state.mysql_init(NULL);
        if (db != NULL) {
            if (mysqllib_state.mysql_real_connect(db, host, username, password, database, port, socket, flag)) {
                /* FIX 1: Allocated full structure size, not pointer size */
                mysqllib_data *data = lua_newuserdatauv(L, sizeof(mysqllib_data), 0);
                data->db = db ;
                luaL_getmetatable(L, MYSQLLIB_METATABLE);
                lua_setmetatable(L, -2);
                return 1;
            } else {
                mysqllib_state.mysql_close(db);
            }
        }
    }
    return 0;
}

static int mysqllib_close(lua_State * L)
{
    if (mysqllib_state.initialized) {
        mysqllib_data * data = luaL_checkudata(L, 1, MYSQLLIB_METATABLE);
        /* Check data AND data->db to prevent double-free during GC */
        if (data != NULL && data->db != NULL) {
            mysqllib_state.mysql_close(data->db);
            data->db = NULL;
        }
    }
    return 0;
}

/* execute(db_handle, querystring, [callback]) */

static int mysqllib_execute(lua_State * L)
{
    if (mysqllib_state.initialized) {
        mysqllib_data * data = luaL_checkudata(L, 1, MYSQLLIB_METATABLE);
        if (data != NULL && data->db != NULL) {
            size_t length = 0;
            const char *query = lua_tolstring(L, 2, &length);
            int has_callback = (lua_type(L, 3) == LUA_TFUNCTION);
            if (query != NULL) {
                int error = mysqllib_state.mysql_real_query(data->db, query, (int) length);
                if (! error) {
                    mysql_result * result = mysqllib_state.mysql_store_result(data->db);
                    if (result != NULL) {
                        int nofrows = (int) mysqllib_state.mysql_num_rows(result);
                        int nofcolumns = mysqllib_state.mysql_num_fields(result);
                        if (nofcolumns > 0 && nofrows > 0 && has_callback) {
                            mysqllib_state.mysql_field_seek(result, 0);

                            for (int r = 0; r < nofrows; r++) {
                                mysql_row row = mysqllib_state.mysql_fetch_row(result);
                                if (! row) break;
                                /* Push callback for each iteration */
                                lua_pushvalue(L, 3);
                                lua_pushinteger(L, nofcolumns);
                                /* Row values table */
                                lua_createtable(L, nofcolumns, 0);
                                for (int c = 0; c < nofcolumns; c++) {
                                    if (row[c]) {
                                        lua_pushstring(L, row[c]);
                                    } else {
                                        lua_pushnil(L); /* FIX 3: Safe handling for SQL NULL */
                                    }
                                    lua_rawseti(L, -2, (lua_Integer)c + 1);
                                }
                                if (r == 0) {
                                    /* First row gets column names as 3rd arg */
                                    mysql_field * fields = mysqllib_state.mysql_fetch_fields(result);
                                    lua_createtable(L, nofcolumns, 0);
                                    for (int c = 0; c < nofcolumns; c++) {
                                        lua_pushstring(L, fields[c].name ? fields[c].name : "");
                                        lua_rawseti(L, -2, (lua_Integer)c + 1);
                                    }
                                    lua_call(L, 3, 0);
                                } else {
                                    lua_call(L, 2, 0);
                                }
                            }
                        }
                        mysqllib_state.mysql_free_result(result);
                    }
                    lua_pushboolean(L, 1);
                    return 1;
                }
            }
        }
    }
    lua_pushboolean(L, 0);
    return 1;
}

static int mysqllib_getmessage(lua_State * L)
{
     if (mysqllib_state.initialized) {
         mysqllib_data * data = luaL_checkudata(L, 1, MYSQLLIB_METATABLE);
         if (data != NULL && data->db != NULL) {
             lua_pushstring(L, mysqllib_state.mysql_error(data->db));
             return 1;
         }
     }
     return 0;
}

static int mysqllib_free(lua_State * L)
{
    return mysqllib_close(L);
}

/* <string> = tostring(instance) */

static int mysqllib_tostring(lua_State * L)
{
    if (mysqllib_state.initialized) {
        mysqllib_data * data = luaL_checkudata(L, 1, MYSQLLIB_METATABLE);
        if (data != NULL) {
            (void) lua_pushfstring(L, "<mysqllib-instance %p>", data->db);
        } else {
            lua_pushnil(L);
        }
        return 1;
    } else {
        return 0;
    }
}

static const struct luaL_Reg mysqllib_metatable[] = {
    { "__tostring", mysqllib_tostring },
    { "__gc",       mysqllib_free     },
    { NULL,         NULL              },
};

static struct luaL_Reg mysqllib_function_list[] = {
    { "initialize", mysqllib_initialize },
    { "open",       mysqllib_open       },
    { "close",      mysqllib_close      },
    { "execute",    mysqllib_execute    },
    { "getmessage", mysqllib_getmessage },
    { NULL,         NULL                },
};

int luaopen_mysql(lua_State * L)
{
    luaL_newmetatable(L, MYSQLLIB_METATABLE);
    luaL_setfuncs(L, mysqllib_metatable, 0);
    lmt_library_register(L, "mysql", mysqllib_function_list);
    return 0;
}
