/**
 * @file load_module.c
 * @author Ambroz Bizjak <ambrop7@gmail.com>
 * 
 * @section LICENSE
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the author nor the
 *    names of its contributors may be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * @section DESCRIPTION
 * 
 * Synopsis:
 *   load_module(string name)
 */

#include <stddef.h>
#include <limits.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <dlfcn.h>

#include <misc/balloc.h>
#include <misc/concat_strings.h>
#include <misc/strdup.h>
#include <misc/offset.h>
#include <misc/debug.h>
#include <structure/LinkedList0.h>
#include <ncd/NCDModule.h>

#include <generated/blog_channel_ncd_load_module.h>

#define ModuleLog(i, ...) NCDModuleInst_Backend_Log((i), BLOG_CURRENT_CHANNEL, __VA_ARGS__)
#define ModuleGlobal(i) ((i)->m->group->group_state)

struct global {
    LinkedList0 modules_list;
};

struct module {
    char *name;
    void *lib_handle;
    int ncdmodule_loaded;
    LinkedList0Node modules_list_node;
};

static struct module * find_module (const char *name, struct global *g)
{
    for (LinkedList0Node *ln = LinkedList0_GetFirst(&g->modules_list); ln; ln = LinkedList0Node_Next(ln)) {
        struct module *mod = UPPER_OBJECT(ln, struct module, modules_list_node);
        if (!strcmp(mod->name, name)) {
            return mod;
        }
    }
    return NULL;
}

static struct module * module_init (const char *name, NCDModuleInst *i)
{
    struct global *g = ModuleGlobal(i);
    ASSERT(!find_module(name, g))
    
    struct module *mod = BAlloc(sizeof(*mod));
    if (!mod) {
        ModuleLog(i, BLOG_ERROR, "BAlloc failed");
        goto fail0;
    }
    
    mod->name = b_strdup(name);
    if (!mod->name) {
        ModuleLog(i, BLOG_ERROR, "b_strdup failed");
        goto fail1;
    }
    
    mod->lib_handle = NULL;
    mod->ncdmodule_loaded = 0;
    LinkedList0_Prepend(&g->modules_list, &mod->modules_list_node);
    
    return mod;
    
fail1:
    BFree(mod);
fail0:
    return NULL;
}

static void module_free (struct module *mod, struct global *g)
{
    LinkedList0_Remove(&g->modules_list, &mod->modules_list_node);
    if (mod->lib_handle) {
        if (dlclose(mod->lib_handle) != 0) {
            BLog(BLOG_ERROR, "dlclose failed");
        }
    }
    BFree(mod->name);
    BFree(mod);
}

static char * x_read_link (const char *path)
{
    size_t size = 32;
    char *buf = BAlloc(size + 1);
    if (!buf) {
        goto fail0;
    }
    
    ssize_t link_size;
    while (1) {
        link_size = readlink(path, buf, size);
        if (link_size < 0) {
            goto fail1;
        }
        if (link_size >= 0 && link_size < size) {
            break;
        }
        if (size > SIZE_MAX / 2 || 2 * size > SIZE_MAX - 1) {
            goto fail1;
        }
        size *= 2;
        char *new_buf = BRealloc(buf, size + 1);
        if (!new_buf) {
            goto fail1;
        }
        buf = new_buf;
    }
    
    buf[link_size] = '\0';
    return buf;
    
fail1:
    BFree(buf);
fail0:
    return NULL;
}

static char * find_module_library (NCDModuleInst *i, const char *module_name)
{
    char *ret = NULL;
    
    char *self = x_read_link("/proc/self/exe");
    if (!self) {
        ModuleLog(i, BLOG_ERROR, "failed to read /proc/self/exe");
        goto fail0;
    }
    
    char *slash = strrchr(self, '/');
    if (!slash) {
        ModuleLog(i, BLOG_ERROR, "contents of /proc/self/exe do not have a slash");
        goto fail1;
    }
    *slash = '\0';
    
    const char *paths[] = {"../lib/badvpn-ncd", "../mcvpn", NULL};
    
    size_t j;
    for (j = 0; paths[j]; j++) {
        char *module_path = concat_strings(6, self, "/", paths[j], "/libncdmodule_", module_name, ".so");
        if (!module_path) {
            ModuleLog(i, BLOG_ERROR, "concat_strings failed");
            goto fail1;
        }
        
        if (access(module_path, F_OK) == 0) {
            ret = module_path;
            break;
        }
        
        BFree(module_path);
    }
    
    if (!paths[j]) {
        ModuleLog(i, BLOG_ERROR, "failed to find module");
    }
    
fail1:
    BFree(self);
fail0:
    return ret;
}

static int func_globalinit (struct NCDInterpModuleGroup *group, const struct NCDModuleInst_iparams *params)
{
    struct global *g = BAlloc(sizeof(*g));
    if (!g) {
        BLog(BLOG_ERROR, "BAlloc failed");
        return 0;
    }
    
    group->group_state = g;
    LinkedList0_Init(&g->modules_list);
    
    return 1;
}

static void func_globalfree (struct NCDInterpModuleGroup *group)
{
    struct global *g = group->group_state;
    
    LinkedList0Node *ln;
    while ((ln = LinkedList0_GetFirst(&g->modules_list))) {
        struct module *mod = UPPER_OBJECT(ln, struct module, modules_list_node);
        module_free(mod, g);
    }
    
    BFree(g);
}

static void func_new (void *unused, NCDModuleInst *i, const struct NCDModuleInst_new_params *params)
{
    // check arguments
    NCDValRef name_arg;
    if (!NCDVal_ListRead(params->args, 1, &name_arg)) {
        ModuleLog(i, BLOG_ERROR, "wrong arity");
        goto fail0;
    }
    if (!NCDVal_IsStringNoNulls(name_arg)) {
        ModuleLog(i, BLOG_ERROR, "wrong type");
        goto fail0;
    }
    
    struct module *mod = find_module(NCDVal_StringData(name_arg), ModuleGlobal(i));
    ASSERT(!mod || mod->lib_handle)
    
    if (!mod) {
        mod = module_init(NCDVal_StringData(name_arg), i);
        if (!mod) {
            ModuleLog(i, BLOG_ERROR, "module_init failed");
            goto fail0;
        }
        
        // find module library
        char *module_path = find_module_library(i, NCDVal_StringData(name_arg));
        if (!module_path) {
            module_free(mod, ModuleGlobal(i));
            goto fail0;
        }
        
        // load it as a dynamic library
        mod->lib_handle = dlopen(module_path, RTLD_NOW);
        BFree(module_path);
        if (!mod->lib_handle) {
            ModuleLog(i, BLOG_ERROR, "dlopen failed");
            module_free(mod, ModuleGlobal(i));
            goto fail0;
        }
    }
    
    if (!mod->ncdmodule_loaded) {
        // build name of NCDModuleGroup structure symbol
        char *group_symbol = concat_strings(2, "ncdmodule_", NCDVal_StringData(name_arg));
        if (!group_symbol) {
            ModuleLog(i, BLOG_ERROR, "concat_strings failed");
            goto fail0;
        }
        
        // resolve NCDModuleGroup structure symbol
        void *group = dlsym(mod->lib_handle, group_symbol);
        BFree(group_symbol);
        if (!group) {
            ModuleLog(i, BLOG_ERROR, "dlsym failed");
            goto fail0;
        }
        
        // load module group
        if (!NCDModuleInst_Backend_InterpLoadGroup(i, (struct NCDModuleGroup *)group)) {
            ModuleLog(i, BLOG_ERROR, "NCDModuleInst_Backend_InterpLoadGroup failed");
            goto fail0;
        }
        
        mod->ncdmodule_loaded = 1;
    }
    
    // signal up
    NCDModuleInst_Backend_Up(i);
    return;
    
fail0:
    NCDModuleInst_Backend_DeadError(i);
}

static struct NCDModule modules[] = {
    {
        .type = "load_module",
        .func_new2 = func_new
    }, {
        .type = NULL
    }
};

const struct NCDModuleGroup ncdmodule_load_module = {
    .func_globalinit = func_globalinit,
    .func_globalfree = func_globalfree,
    .modules = modules
};
