/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#include "mod_lua.h"
#include "http_log.h"
#include "apr_uuid.h"
#include "lua_config.h"
#include "apr_file_info.h"
#include "mod_auth.h"

APLOG_USE_MODULE(lua);

#ifndef AP_LUA_MODULE_EXT
#if defined(NETWARE) 
#define AP_LUA_MODULE_EXT ".nlm"
#elif defined(WIN32)
#define AP_LUA_MODULE_EXT ".dll"
#elif (defined(__hpux__) || defined(__hpux)) && !defined(__ia64)
#define AP_LUA_MODULE_EXT ".sl"
#else
#define AP_LUA_MODULE_EXT ".so"
#endif
#endif

#if APR_HAS_THREADS
    apr_thread_mutex_t *ap_lua_mutex;
    
void ap_lua_init_mutex(apr_pool_t *pool, server_rec *s) 
{
    apr_thread_mutex_create(&ap_lua_mutex, APR_THREAD_MUTEX_DEFAULT, pool);
}
#endif

/* forward dec'l from this file */

#if 0
static void pstack_dump(lua_State *L, apr_pool_t *r, int level,
                        const char *msg)
{
    int i;
    int top = lua_gettop(L);

    ap_log_perror(APLOG_MARK, level, 0, r, APLOGNO(03211)
                  "Lua Stack Dump: [%s]", msg);

    for (i = 1; i <= top; i++) {
        int t = lua_type(L, i);
        switch (t) {
        case LUA_TSTRING:{
                ap_log_perror(APLOG_MARK, level, 0, r, APLOGNO(03212)
                              "%d:  '%s'", i, lua_tostring(L, i));
                break;
            }
        case LUA_TUSERDATA:{
                ap_log_perror(APLOG_MARK, level, 0, r, APLOGNO(03213)
                              "%d:  userdata", i);
                break;
            }
        case LUA_TLIGHTUSERDATA:{
                ap_log_perror(APLOG_MARK, level, 0, r, APLOGNO(03214)
                              "%d:  lightuserdata", i);
                break;
            }
        case LUA_TNIL:{
                ap_log_perror(APLOG_MARK, level, 0, r, APLOGNO(03215)
                              "%d:  NIL", i);
                break;
            }
        case LUA_TNONE:{
                ap_log_perror(APLOG_MARK, level, 0, r, APLOGNO(03216)
                              "%d:  None", i);
                break;
            }
        case LUA_TBOOLEAN:{
                ap_log_perror(APLOG_MARK, level, 0, r, APLOGNO(03217)
                              "%d:  %s",
                              i, lua_toboolean(L, i) ? "true" : "false");
                break;
            }
        case LUA_TNUMBER:{
                ap_log_perror(APLOG_MARK, level, 0, r, APLOGNO(03218)
                              "%d:  %g", i, lua_tonumber(L, i));
                break;
            }
        case LUA_TTABLE:{
                ap_log_perror(APLOG_MARK, level, 0, r, APLOGNO(03219)
                              "%d:  <table>", i);
                break;
            }
        case LUA_TTHREAD:{
                ap_log_perror(APLOG_MARK, level, 0, r, APLOGNO(03220)
                              "%d:  <thread>", i);
                break;
            }
        case LUA_TFUNCTION:{
                ap_log_perror(APLOG_MARK, level, 0, r, APLOGNO(03221)
                              "%d:  <function>", i);
                break;
            }
        default:{
                ap_log_perror(APLOG_MARK, level, 0, r, APLOGNO(03222)
                              "%d:  unknown: [%s]", i, lua_typename(L, i));
                break;
            }
        }
    }
}
#endif

/* BEGIN modules*/

/* BEGIN apache lmodule  */

#define makeintegerfield(L, n) lua_pushinteger(L, n); lua_setfield(L, -2, #n)

void ap_lua_load_apache2_lmodule(lua_State *L)
{
    lua_getglobal(L, "package");
    lua_getfield(L, -1, "loaded");
    lua_newtable(L);
    lua_setfield(L, -2, "apache2");
    lua_setglobal(L, "apache2");
    lua_pop(L, 1);              /* empty stack */

    lua_getglobal(L, "apache2");

    lua_pushstring(L, ap_get_server_banner());
    lua_setfield(L, -2, "version");

    makeintegerfield(L, OK);
    makeintegerfield(L, DECLINED);
    makeintegerfield(L, DONE);
    makeintegerfield(L, HTTP_MOVED_TEMPORARILY);
    makeintegerfield(L, PROXYREQ_NONE);
    makeintegerfield(L, PROXYREQ_PROXY);
    makeintegerfield(L, PROXYREQ_REVERSE);
    makeintegerfield(L, PROXYREQ_RESPONSE);
    makeintegerfield(L, PROXYREQ_RESPONSE);
    makeintegerfield(L, AUTHZ_DENIED);
    makeintegerfield(L, AUTHZ_GRANTED);
    makeintegerfield(L, AUTHZ_NEUTRAL);
    makeintegerfield(L, AUTHZ_GENERAL_ERROR);
    makeintegerfield(L, AUTHZ_DENIED_NO_USER);
    
    /*
       makeintegerfield(L, HTTP_CONTINUE);
       makeintegerfield(L, HTTP_SWITCHING_PROTOCOLS);
       makeintegerfield(L, HTTP_PROCESSING);
       makeintegerfield(L, HTTP_OK);
       makeintegerfield(L, HTTP_CREATED);
       makeintegerfield(L, HTTP_ACCEPTED);
       makeintegerfield(L, HTTP_NON_AUTHORITATIVE);
       makeintegerfield(L, HTTP_NO_CONTENT);
       makeintegerfield(L, HTTP_RESET_CONTENT);
       makeintegerfield(L, HTTP_PARTIAL_CONTENT);
       makeintegerfield(L, HTTP_MULTI_STATUS);
       makeintegerfield(L, HTTP_ALREADY_REPORTED);
       makeintegerfield(L, HTTP_IM_USED);
       makeintegerfield(L, HTTP_MULTIPLE_CHOICES);
       makeintegerfield(L, HTTP_MOVED_PERMANENTLY);
       makeintegerfield(L, HTTP_MOVED_TEMPORARILY);
       makeintegerfield(L, HTTP_SEE_OTHER);
       makeintegerfield(L, HTTP_NOT_MODIFIED);
       makeintegerfield(L, HTTP_USE_PROXY);
       makeintegerfield(L, HTTP_TEMPORARY_REDIRECT);
       makeintegerfield(L, HTTP_PERMANENT_REDIRECT);
       makeintegerfield(L, HTTP_BAD_REQUEST);
       makeintegerfield(L, HTTP_UNAUTHORIZED);
       makeintegerfield(L, HTTP_PAYMENT_REQUIRED);
       makeintegerfield(L, HTTP_FORBIDDEN);
       makeintegerfield(L, HTTP_NOT_FOUND);
       makeintegerfield(L, HTTP_METHOD_NOT_ALLOWED);
       makeintegerfield(L, HTTP_NOT_ACCEPTABLE);
       makeintegerfield(L, HTTP_PROXY_AUTHENTICATION_REQUIRED);
       makeintegerfield(L, HTTP_REQUEST_TIME_OUT);
       makeintegerfield(L, HTTP_CONFLICT);
       makeintegerfield(L, HTTP_GONE);
       makeintegerfield(L, HTTP_LENGTH_REQUIRED);
       makeintegerfield(L, HTTP_PRECONDITION_FAILED);
       makeintegerfield(L, HTTP_REQUEST_ENTITY_TOO_LARGE);
       makeintegerfield(L, HTTP_REQUEST_URI_TOO_LARGE);
       makeintegerfield(L, HTTP_UNSUPPORTED_MEDIA_TYPE);
       makeintegerfield(L, HTTP_RANGE_NOT_SATISFIABLE);
       makeintegerfield(L, HTTP_EXPECTATION_FAILED);
       makeintegerfield(L, HTTP_UNPROCESSABLE_ENTITY);
       makeintegerfield(L, HTTP_LOCKED);
       makeintegerfield(L, HTTP_FAILED_DEPENDENCY);
       makeintegerfield(L, HTTP_UPGRADE_REQUIRED);
       makeintegerfield(L, HTTP_PRECONDITION_REQUIRED);
       makeintegerfield(L, HTTP_TOO_MANY_REQUESTS);
       makeintegerfield(L, HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE);
       makeintegerfield(L, HTTP_INTERNAL_SERVER_ERROR);
       makeintegerfield(L, HTTP_NOT_IMPLEMENTED);
       makeintegerfield(L, HTTP_BAD_GATEWAY);
       makeintegerfield(L, HTTP_SERVICE_UNAVAILABLE);
       makeintegerfield(L, HTTP_GATEWAY_TIME_OUT);
       makeintegerfield(L, HTTP_VERSION_NOT_SUPPORTED);
       makeintegerfield(L, HTTP_VARIANT_ALSO_VARIES);
       makeintegerfield(L, HTTP_INSUFFICIENT_STORAGE);
       makeintegerfield(L, HTTP_LOOP_DETECTED);
       makeintegerfield(L, HTTP_NOT_EXTENDED);
       makeintegerfield(L, HTTP_NETWORK_AUTHENTICATION_REQUIRED);
     */
}

/* END apache2 lmodule */

/*  END library functions */

/* callback for cleaning up a lua vm when pool is closed */
static apr_status_t cleanup_lua(void *l)
{
    AP_DEBUG_ASSERT(l != NULL);
    lua_close((lua_State *) l);
    return APR_SUCCESS;
}

static apr_status_t server_cleanup_lua(void *resource)
{
    ap_lua_server_spec* spec = (ap_lua_server_spec*) resource;
    AP_DEBUG_ASSERT(spec != NULL);
    if (spec->L != NULL) {
        lua_close((lua_State *) spec->L);
    }
    return APR_SUCCESS;
}

/*
        munge_path(L, 
                   "path", 
                   "?.lua", 
                   "./?.lua", 
                   lifecycle_pool,
                   spec->package_paths, 
                   spec->file);
*/
/**
 * field -> "path" or "cpath"
 * sub_pat -> "?.lua"
 * rep_pat -> "./?.lua"
 * pool -> lifecycle pool for allocations
 * paths -> things to add
 * file -> ???
 */
static void munge_path(lua_State *L,
                       const char *field,
                       const char *sub_pat,
                       const char *rep_pat,
                       apr_pool_t *pool,
                       apr_array_header_t *paths,
                       const char *file)
{
    const char *current;
    const char *parent_dir;
    const char *pattern;
    const char *modified;
    char *part;

    lua_getglobal(L, "package");
    lua_getfield(L, -1, field);
    
    current = lua_tostring(L, -1);

    parent_dir = ap_make_dirstr_parent(pool, file);
 
    pattern = apr_pstrcat(pool, parent_dir, sub_pat, NULL);

    luaL_gsub(L, current, rep_pat, pattern);
    lua_setfield(L, -3, field);
    lua_getfield(L, -2, field);
    modified = lua_tostring(L, -1);


    lua_pop(L, 2);

    part = apr_pstrcat(pool, modified, ";", apr_array_pstrcat(pool, paths, ';'),
                       NULL);

    lua_pushstring(L, part);
    lua_setfield(L, -2, field);
    lua_pop(L, 1);              /* pop "package" off the stack     */
}

#ifdef AP_ENABLE_LUAJIT
static int loadjitmodule(lua_State *L, apr_pool_t *lifecycle_pool) {
    lua_getglobal(L, "require");
    lua_pushliteral(L, "jit.");
    lua_pushvalue(L, -3);
    lua_concat(L, 2);
    if (lua_pcall(L, 1, 1, 0)) {
        const char *msg = lua_tostring(L, -1);
        ap_log_perror(APLOG_MARK, APLOG_DEBUG, 0, lifecycle_pool, APLOGNO(01480)
                      "Failed to init LuaJIT: %s", msg);
        return 1;
    }
    lua_getfield(L, -1, "start");
    lua_remove(L, -2);  /* drop module table */
    return 0;
}

#endif

static apr_status_t vm_construct(lua_State **vm, void *params, apr_pool_t *lifecycle_pool)
{
    lua_State* L;

    ap_lua_vm_spec *spec = params;

    L = luaL_newstate();
#ifdef AP_ENABLE_LUAJIT
    luaopen_jit(L);
#endif
    luaL_openlibs(L);
    if (spec->package_paths) {
        munge_path(L, 
                   "path", "?.lua", "./?.lua", 
                   lifecycle_pool,
                   spec->package_paths, 
                   spec->file);
    }
    if (spec->package_cpaths) {
        munge_path(L,
                   "cpath", "?" AP_LUA_MODULE_EXT, "./?" AP_LUA_MODULE_EXT,
                   lifecycle_pool,
                   spec->package_cpaths,
                   spec->file);
    }

    if (spec->cb) {
        spec->cb(L, lifecycle_pool, spec->cb_arg);
    }


    if (spec->bytecode && spec->bytecode_len > 0) {
        luaL_loadbuffer(L, spec->bytecode, spec->bytecode_len, spec->file);
        lua_pcall(L, 0, LUA_MULTRET, 0);
    }
    else {
        int rc;
        ap_log_perror(APLOG_MARK, APLOG_DEBUG, 0, lifecycle_pool, APLOGNO(01481)
            "loading lua file %s", spec->file);
        rc = luaL_loadfile(L, spec->file);
        if (rc != 0) {
            ap_log_perror(APLOG_MARK, APLOG_ERR, 0, lifecycle_pool, APLOGNO(01482)
                          "Error loading %s: %s", spec->file,
                          rc == LUA_ERRMEM ? "memory allocation error"
                                           : lua_tostring(L, 0));
            return APR_EBADF;
        }
        if ( lua_pcall(L, 0, LUA_MULTRET, 0) == LUA_ERRRUN ) {
            ap_log_perror(APLOG_MARK, APLOG_ERR, 0, lifecycle_pool, APLOGNO(02613)
                          "Error loading %s: %s", spec->file,
                            lua_tostring(L, -1));
            return APR_EBADF;
        }
    }

#ifdef AP_ENABLE_LUAJIT
    loadjitmodule(L, lifecycle_pool);
#endif
    lua_pushlightuserdata(L, lifecycle_pool);
    lua_setfield(L, LUA_REGISTRYINDEX, "Apache2.Wombat.pool");
    *vm = L;

    return APR_SUCCESS;
}

static ap_lua_vm_spec* copy_vm_spec(apr_pool_t* pool, ap_lua_vm_spec* spec) 
{
    ap_lua_vm_spec* copied_spec = apr_pcalloc(pool, sizeof(ap_lua_vm_spec));
    copied_spec->bytecode_len = spec->bytecode_len;
    copied_spec->bytecode = apr_pstrdup(pool, spec->bytecode);
    copied_spec->cb = spec->cb;
    copied_spec->cb_arg = NULL;
    copied_spec->file = apr_pstrdup(pool, spec->file);
    copied_spec->package_cpaths = apr_array_copy(pool, spec->package_cpaths);
    copied_spec->package_paths = apr_array_copy(pool, spec->package_paths);
    copied_spec->pool = pool;
    copied_spec->scope = AP_LUA_SCOPE_SERVER;
    copied_spec->codecache = spec->codecache;
    return copied_spec;
}

static apr_status_t server_vm_construct(lua_State **resource, void *params, apr_pool_t *pool)
{
    lua_State* L;
    ap_lua_server_spec* spec = apr_pcalloc(pool, sizeof(ap_lua_server_spec));
    *resource = NULL;
    if (vm_construct(&L, params, pool) == APR_SUCCESS) {
        spec->finfo = apr_pcalloc(pool, sizeof(ap_lua_finfo));
        if (L != NULL) {
            spec->L = L;
            *resource = (void*) spec;
            lua_pushlightuserdata(L, spec);
            lua_setfield(L, LUA_REGISTRYINDEX, "Apache2.Lua.server_spec");
            return APR_SUCCESS;
        }
    }
    return APR_EGENERAL;
}

/**
 * Function used to create a lua_State instance bound into the web
 * server in the appropriate scope.
 */
lua_State *ap_lua_get_lua_state(apr_pool_t *lifecycle_pool,
                                               ap_lua_vm_spec *spec, request_rec* r)
{
    lua_State *L = NULL;
    ap_lua_finfo *cache_info = NULL;
    int tryCache = 0;
    
    if (spec->scope == AP_LUA_SCOPE_SERVER) {
        char *hash;
        apr_reslist_t* reslist = NULL;
        ap_lua_server_spec* sspec = NULL;
        hash = apr_psprintf(r->pool, "reslist:%s", spec->file);
#if APR_HAS_THREADS
        apr_thread_mutex_lock(ap_lua_mutex);
#endif
        if (apr_pool_userdata_get((void **)&reslist, hash,
                                  r->server->process->pool) == APR_SUCCESS) {
            if (reslist != NULL) {
                if (apr_reslist_acquire(reslist, (void**) &sspec) == APR_SUCCESS) {
                    L = sspec->L;
                    cache_info = sspec->finfo;
                }
            }
        }
        if (L == NULL) {
            ap_lua_vm_spec* server_spec = copy_vm_spec(r->server->process->pool, spec);
            if (
                    apr_reslist_create(&reslist, spec->vm_min, spec->vm_max, spec->vm_max, 0, 
                                (apr_reslist_constructor) server_vm_construct, 
                                (apr_reslist_destructor) server_cleanup_lua, 
                                server_spec, r->server->process->pool)
                    == APR_SUCCESS && reslist != NULL) {
                apr_pool_userdata_set(reslist, hash, NULL,
                                            r->server->process->pool);
                if (apr_reslist_acquire(reslist, (void**) &sspec) == APR_SUCCESS) {
                    L = sspec->L;
                    cache_info = sspec->finfo;
                }
                else {
                    return NULL;
                }
            }
        }
#if APR_HAS_THREADS
        apr_thread_mutex_unlock(ap_lua_mutex);
#endif
    }
    else {
        if (apr_pool_userdata_get((void **)&L, spec->file,
                              lifecycle_pool) != APR_SUCCESS) {
            L = NULL;
        }
    }
    if (L == NULL) {
        ap_log_perror(APLOG_MARK, APLOG_DEBUG, 0, lifecycle_pool, APLOGNO(01483)
                        "creating lua_State with file %s", spec->file);
        /* not available, so create */

        if (!vm_construct(&L, spec, lifecycle_pool)) {
            AP_DEBUG_ASSERT(L != NULL);
            apr_pool_userdata_set(L, spec->file, cleanup_lua, lifecycle_pool);
        }
    }

    if (spec->codecache == AP_LUA_CACHE_FOREVER || (spec->bytecode && spec->bytecode_len > 0)) {
        tryCache = 1;
    }
    else {
        char* mkey;
        if (spec->scope != AP_LUA_SCOPE_SERVER) {
            mkey = apr_psprintf(r->pool, "ap_lua_modified:%s", spec->file);
            apr_pool_userdata_get((void **)&cache_info, mkey, lifecycle_pool);
            if (cache_info == NULL) {
                cache_info = apr_pcalloc(lifecycle_pool, sizeof(ap_lua_finfo));
                apr_pool_userdata_set((void*) cache_info, mkey, NULL, lifecycle_pool);
            }
        }
        if (spec->codecache == AP_LUA_CACHE_STAT) {
            apr_finfo_t lua_finfo;
            apr_stat(&lua_finfo, spec->file, APR_FINFO_MTIME|APR_FINFO_SIZE, lifecycle_pool);

            /* On first visit, modified will be zero, but that's fine - The file is 
            loaded in the vm_construct function.
            */
            if ((cache_info->modified == lua_finfo.mtime && cache_info->size == lua_finfo.size)
                    || cache_info->modified == 0) {
                tryCache = 1;
            }
            cache_info->modified = lua_finfo.mtime;
            cache_info->size = lua_finfo.size;
        }
        else if (spec->codecache == AP_LUA_CACHE_NEVER) {
            if (cache_info->runs == 0)
                tryCache = 1;
        }
        cache_info->runs++;
    }
    if (tryCache == 0 && spec->scope != AP_LUA_SCOPE_ONCE) {
        int rc;
        ap_log_perror(APLOG_MARK, APLOG_DEBUG, 0, lifecycle_pool, APLOGNO(02332)
            "(re)loading lua file %s", spec->file);
        rc = luaL_loadfile(L, spec->file);
        if (rc != 0) {
            ap_log_perror(APLOG_MARK, APLOG_ERR, 0, lifecycle_pool, APLOGNO(02333)
                          "Error loading %s: %s", spec->file,
                          rc == LUA_ERRMEM ? "memory allocation error"
                                           : lua_tostring(L, 0));
            return 0;
        }
        lua_pcall(L, 0, LUA_MULTRET, 0);
    }

    return L;
}
