/* -*- Mode: C; c-basic-offset:4 ; indent-tabs-mode:nil -*- */
/*
 * Copyright (c) 2004-2005 The Trustees of Indiana University and Indiana
 *                         University Research and Technology
 *                         Corporation.  All rights reserved.
 * Copyright (c) 2004-2005 The University of Tennessee and The University
 *                         of Tennessee Research Foundation.  All rights
 *                         reserved.
 * Copyright (c) 2004-2005 High Performance Computing Center Stuttgart,
 *                         University of Stuttgart.  All rights reserved.
 * Copyright (c) 2004-2005 The Regents of the University of California.
 *                         All rights reserved.
 * Copyright (c) 2008-2022 Cisco Systems, Inc.  All rights reserved
 * Copyright (c) 2015      Los Alamos National Security, LLC. All rights
 *                         reserved.
 * Copyright (c) 2015      Research Organization for Information Science
 *                         and Technology (RIST). All rights reserved.
 * Copyright (c) 2016-2020 Intel, Inc.  All rights reserved.
 * Copyright (c) 2021-2023 Nanook Consulting.  All rights reserved.
 * $COPYRIGHT$
 *
 * Additional copyrights may follow
 *
 * $HEADER$
 */

#include "src/include/pmix_config.h"
#ifdef HAVE_SYS_TYPES_H
#    include <sys/types.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_UNISTD_H
#    include <unistd.h>
#endif

#include "pmix_common.h"
#include "src/class/pmix_hash_table.h"
#include "src/class/pmix_list.h"
#include "src/include/pmix_globals.h"
#include "src/mca/base/pmix_base.h"
#include "src/mca/base/pmix_mca_base_component_repository.h"
#include "src/mca/mca.h"
#include "src/mca/pdl/base/base.h"
#include "src/util/pmix_printf.h"
#include "src/util/pmix_basename.h"
#include "src/util/pmix_show_help.h"

#if PMIX_HAVE_PDL_SUPPORT

/*
 * Private types
 */
static void ri_constructor(pmix_mca_base_component_repository_item_t *ri);
static void ri_destructor(pmix_mca_base_component_repository_item_t *ri);
PMIX_CLASS_INSTANCE(pmix_mca_base_component_repository_item_t, pmix_list_item_t, ri_constructor,
                    ri_destructor);

#endif /* PMIX_HAVE_PDL_SUPPORT */

static void clf_constructor(pmix_object_t *obj)
{
    pmix_mca_base_failed_component_t *cli = (pmix_mca_base_failed_component_t *) obj;
    cli->comp = NULL;
    cli->error_msg = NULL;
}

static void clf_destructor(pmix_object_t *obj)
{
    pmix_mca_base_failed_component_t *cli = (pmix_mca_base_failed_component_t *) obj;
    cli->comp = NULL;
    if (NULL != cli->error_msg) {
        free(cli->error_msg);
        cli->error_msg = NULL;
    }
}

PMIX_CLASS_INSTANCE(pmix_mca_base_failed_component_t, pmix_list_item_t, clf_constructor,
                    clf_destructor);

/*
 * Private variables
 */
static bool initialized = false;

#if PMIX_HAVE_PDL_SUPPORT

static pmix_hash_table_t pmix_mca_base_component_repository;

/* two-level macro for stringifying a number */
#    define STRINGIFYX(x) #    x
#    define STRINGIFY(x)  STRINGIFYX(x)

static int process_repository_item(const char *filename, void *data)
{
    char *project = (char*)data;
    char *name;
    char *type;
    pmix_mca_base_component_repository_item_t *ri;
    pmix_list_t *component_list;
    char *base, *prefix;
    int ret;
    size_t prefixlen;

    base = pmix_basename(filename);
    if (NULL == base) {
        return PMIX_ERROR;
    }

    /* check if the plugin has the appropriate prefix */
    pmix_asprintf(&prefix, "%s_mca_", project);
    pmix_asprintf(&type, "lib%s_mca_", project);
    if (0 != strncmp(base, prefix, strlen(prefix)) &&
        0 != strncmp(base, type, strlen(type))) {
        if (pmix_mca_base_show_load_errors(NULL, NULL)) {
            pmix_output(0, "mca:base:process_repository_item filename %s has bad prefix - expected:\n\t%s\nor\n\t%s",
                        filename, prefix, type);
        }
        free(base);
        free(prefix);
        free(type);
        return PMIX_SUCCESS;
    }
    free(type);

    /* read framework and component names. framework names may not include an _
     * but component names may */
    prefixlen = strlen(prefix);
    type = &base[prefixlen];
    name = strchr(type, '_');
    if (NULL == name) {
        free(base);
        free(prefix);
        return PMIX_ERR_BAD_PARAM;
    }
    *name = '\0';
    ++name;
    free(prefix);

    /* lookup the associated framework list and create if it doesn't already exist */
    ret = pmix_hash_table_get_value_ptr(&pmix_mca_base_component_repository, type, strlen(type),
                                        (void **) &component_list);
    if (PMIX_SUCCESS != ret) {
        component_list = PMIX_NEW(pmix_list_t);
        if (NULL == component_list) {
            free(base);
            /* OOM. nothing to do but fail */
            return PMIX_ERR_OUT_OF_RESOURCE;
        }

        ret = pmix_hash_table_set_value_ptr(&pmix_mca_base_component_repository, type, strlen(type),
                                            (void *) component_list);
        if (PMIX_SUCCESS != ret) {
            free(base);
            PMIX_RELEASE(component_list);
            return ret;
        }
    }

    /* check for duplicate components */
    PMIX_LIST_FOREACH (ri, component_list, pmix_mca_base_component_repository_item_t) {
        if (0 == strcmp(ri->ri_name, name)) {
            /* already scanned this component */
            free(base);
            return PMIX_SUCCESS;
        }
    }

    ri = PMIX_NEW(pmix_mca_base_component_repository_item_t);
    if (NULL == ri) {
        free(base);
        free(project);
        return PMIX_ERR_OUT_OF_RESOURCE;
    }

    ri->ri_base = base;
    ri->ri_project = strdup(project);

    ri->ri_path = strdup(filename);
    if (NULL == ri->ri_path) {
        PMIX_RELEASE(ri);
        return PMIX_ERR_OUT_OF_RESOURCE;
    }

    /* pmix_strncpy does not guarantee a \0 */
    ri->ri_type[PMIX_MCA_BASE_MAX_TYPE_NAME_LEN] = '\0';
    pmix_strncpy(ri->ri_type, type, PMIX_MCA_BASE_MAX_TYPE_NAME_LEN);

    ri->ri_name[PMIX_MCA_BASE_MAX_TYPE_NAME_LEN] = '\0';
    pmix_strncpy(ri->ri_name, name, PMIX_MCA_BASE_MAX_COMPONENT_NAME_LEN);

    pmix_list_append(component_list, &ri->super);

    return PMIX_SUCCESS;
}

static int file_exists(const char *filename, const char *ext)
{
    char *final;
    int ret;

    if (NULL == ext) {
        return access(filename, F_OK) == 0;
    }

    ret = asprintf(&final, "%s.%s", filename, ext);
    if (0 > ret || NULL == final) {
        return 0;
    }

    ret = access(final, F_OK);
    free(final);
    return (0 == ret);
}

#endif /* PMIX_HAVE_PDL_SUPPORT */

int pmix_mca_base_component_repository_add(const char *project,
                                           const char *path)
{
#if PMIX_HAVE_PDL_SUPPORT
    char *path_to_use = NULL, *dir, *ctx;
    const char sep[] = {PMIX_ENV_SEP, '\0'};

    if (NULL == path) {
        /* nothing to do */
        return PMIX_SUCCESS;
    }

    path_to_use = strdup(path);

    dir = strtok_r(path_to_use, sep, &ctx);
    do {
        if (0 != pmix_pdl_foreachfile(dir, process_repository_item, (void*)project) &&
            !(0 == strcmp(dir, pmix_mca_base_system_default_path) ||
              0 == strcmp(dir, pmix_mca_base_user_default_path))) {
            // It is not an error if a directory fails to add (e.g.,
            // if it doesn't exist).  But we should warn about it as
            // it is something related to "show_load_errors"
            pmix_show_help("help-pmix-mca-base.txt", "failed to add component dir", true, dir);
        }
    } while (NULL != (dir = strtok_r(NULL, sep, &ctx)));

    free(path_to_use);
#else
    PMIX_HIDE_UNUSED_PARAMS(project, path);
#endif /* PMIX_HAVE_PDL_SUPPORT */

    return PMIX_SUCCESS;
}

/*
 * Initialize the repository
 */
int pmix_mca_base_component_repository_init(void)
{
    /* Setup internal structures */

#if PMIX_HAVE_PDL_SUPPORT
    char **projects = NULL, *pathstr;
    char project[PMIX_MCA_BASE_MAX_TYPE_NAME_LEN + 1];
    int m, n;
    int ret;

    if (!initialized) {

        /* Initialize the dl framework */
        ret = pmix_mca_base_framework_open(&pmix_pdl_base_framework,
                                               PMIX_MCA_BASE_OPEN_DEFAULT);
        if (PMIX_SUCCESS != ret) {
            pmix_output(0,
                        "%s %d:%s failed -- process will likely abort (open the dl framework "
                        "returned %d instead of PMIX_SUCCESS)\n",
                        __FILE__, __LINE__, __func__, ret);
            return ret;
        }
        pmix_pdl_base_select();

        PMIX_CONSTRUCT(&pmix_mca_base_component_repository, pmix_hash_table_t);
        ret = pmix_hash_table_init(&pmix_mca_base_component_repository, 128);
        if (PMIX_SUCCESS != ret) {
            (void) pmix_mca_base_framework_close(&pmix_pdl_base_framework);
            return ret;
        }
        initialized = true;
    }
    /* split on semi-colons to find projects */
    projects = PMIx_Argv_split(pmix_mca_base_component_path, ';');
    for (n=0; NULL != projects[n]; n++) {
        /* look for the '@' to delimit the project name */
        for (m=0; '@' != projects[n][m]; m++) {
            project[m] = projects[n][m];
        }
        project[m] = '\0';
        ++m;
        pathstr = &projects[n][m];
        ret = pmix_mca_base_component_repository_add(project, pathstr);
        if (PMIX_SUCCESS != ret) {
            PMIX_DESTRUCT(&pmix_mca_base_component_repository);
            (void) pmix_mca_base_framework_close(&pmix_pdl_base_framework);
            PMIx_Argv_free(projects);
            return ret;
        }
    }
    PMIx_Argv_free(projects);
#endif


    /* All done */

    return PMIX_SUCCESS;
}

int pmix_mca_base_component_repository_get_components(pmix_mca_base_framework_t *framework,
                                                      pmix_list_t **framework_components)
{
    *framework_components = NULL;
#if PMIX_HAVE_PDL_SUPPORT
    return pmix_hash_table_get_value_ptr(&pmix_mca_base_component_repository,
                                         framework->framework_name,
                                         strlen(framework->framework_name),
                                         (void **) framework_components);
#else
    PMIX_HIDE_UNUSED_PARAMS(framework);
    return PMIX_ERR_NOT_FOUND;
#endif
}

#if PMIX_HAVE_PDL_SUPPORT
static void
pmix_mca_base_component_repository_release_internal(pmix_mca_base_component_repository_item_t *ri)
{
    int group_id;

    group_id = pmix_mca_base_var_group_find(NULL, ri->ri_type, ri->ri_name);
    if (0 <= group_id) {
        /* ensure all variables are deregistered before we dlclose the component */
        pmix_mca_base_var_group_deregister(group_id);
    }

    /* Close the component (and potentially unload it from memory */
    if (ri->ri_dlhandle) {
        pmix_pdl_close(ri->ri_dlhandle);
        ri->ri_dlhandle = NULL;
    }
}
#endif

#if PMIX_HAVE_PDL_SUPPORT
static pmix_mca_base_component_repository_item_t *find_component(const char *type, const char *name)
{
    pmix_mca_base_component_repository_item_t *ri;
    pmix_list_t *component_list;
    int ret;

    ret = pmix_hash_table_get_value_ptr(&pmix_mca_base_component_repository, type, strlen(type),
                                        (void **) &component_list);
    if (PMIX_SUCCESS != ret) {
        /* component does not exist in the repository */
        return NULL;
    }

    PMIX_LIST_FOREACH (ri, component_list, pmix_mca_base_component_repository_item_t) {
        if (0 == strcmp(ri->ri_name, name)) {
            return ri;
        }
    }

    return NULL;
}
#endif

void pmix_mca_base_component_repository_release(const pmix_mca_base_component_t *component)
{
#if PMIX_HAVE_PDL_SUPPORT
    pmix_mca_base_component_repository_item_t *ri;

    ri = find_component(component->pmix_mca_type_name, component->pmix_mca_component_name);
    if (NULL != ri && !(--ri->ri_refcnt)) {
        pmix_mca_base_component_repository_release_internal(ri);
    }
#else
    PMIX_HIDE_UNUSED_PARAMS(component);
#endif
}

int pmix_mca_base_component_repository_retain_component(const char *type, const char *name)
{
#if PMIX_HAVE_PDL_SUPPORT
    pmix_mca_base_component_repository_item_t *ri = find_component(type, name);

    if (NULL != ri) {
        ++ri->ri_refcnt;
        return PMIX_SUCCESS;
    }

    return PMIX_ERR_NOT_FOUND;
#else
    PMIX_HIDE_UNUSED_PARAMS(type, name);
    return PMIX_ERR_NOT_SUPPORTED;
#endif
}

int pmix_mca_base_component_repository_open(pmix_mca_base_framework_t *framework,
                                            pmix_mca_base_component_repository_item_t *ri)
{
#if PMIX_HAVE_PDL_SUPPORT
    pmix_mca_base_component_t *component_struct;
    pmix_mca_base_component_list_item_t *mitem = NULL;
    char *struct_name = NULL;
    int vl, ret;

    pmix_output_verbose(PMIX_MCA_BASE_VERBOSE_INFO, 0,
                        "pmix_mca_base_component_repository_open: examining dynamic "
                        "%s MCA component \"%s\" at path %s",
                        ri->ri_type, ri->ri_name, ri->ri_path);

    vl = pmix_mca_base_show_load_errors(ri->ri_type,
                                        ri->ri_name) ? PMIX_MCA_BASE_VERBOSE_ERROR : PMIX_MCA_BASE_VERBOSE_INFO;;

    /* Ensure that this component is not already loaded (should only happen
       if it was statically loaded).  It's an error if it's already
       loaded because we're evaluating this file -- not this component.
       Hence, returning PMIX_ERR_PARAM indicates that the *file* failed
       to load, not the component. */

    PMIX_LIST_FOREACH (mitem, &framework->framework_components,
                       pmix_mca_base_component_list_item_t) {
        if (0 == strcmp(mitem->cli_component->pmix_mca_component_name, ri->ri_name)) {
            pmix_output_verbose(
                PMIX_MCA_BASE_VERBOSE_INFO, 0,
                "pmix_mca_base_component_repository_open: already loaded (ignored)");
            return PMIX_ERR_BAD_PARAM;
        }
    }

    /* silence coverity issue (invalid free) */
    mitem = NULL;

    if (NULL != ri->ri_dlhandle) {
        pmix_output_verbose(
            PMIX_MCA_BASE_VERBOSE_INFO, 0,
            "pmix_mca_base_component_repository_open: already loaded. returning cached component");
        mitem = PMIX_NEW(pmix_mca_base_component_list_item_t);
        if (NULL == mitem) {
            return PMIX_ERR_OUT_OF_RESOURCE;
        }

        mitem->cli_component = ri->ri_component_struct;
        pmix_list_append(&framework->framework_components, &mitem->super);

        return PMIX_SUCCESS;
    }

    if (0 != strcmp(ri->ri_type, framework->framework_name)) {
        /* shouldn't happen. attempting to open a component belonging to
         * another framework. if this happens it is likely a MCA base
         * bug so assert */
        assert(0);
        return PMIX_ERR_NOT_SUPPORTED;
    }

    /* Now try to load the component */

    char *err_msg = NULL;
    if (PMIX_SUCCESS != pmix_pdl_open(ri->ri_path, true, false, &ri->ri_dlhandle, &err_msg)) {
        if (NULL == err_msg) {
            err_msg = strdup("pmix_dl_open() error message was NULL!");
        } else if (file_exists(ri->ri_path, "lo") || file_exists(ri->ri_path, "so")
                   || file_exists(ri->ri_path, "dylib") || file_exists(ri->ri_path, "dll")) {
            char *tmp;
            /* Because libltdl erroneously says "file not found" for any
             * type of error -- which is especially misleading when the file
             * is actually there but cannot be opened for some other reason
             * (e.g., missing symbol) -- do some simple huersitics and if
             * the file [probably] does exist, print a slightly better error
             * message. */
            pmix_asprintf(&tmp,
                          "\n    dlopen error: %s\n    Perhaps a missing symbol, or compiled for a different version of %s?",
                          err_msg, framework->framework_project);
            err_msg = tmp;
        }
        pmix_output_verbose(
            vl, 0, "pmix_mca_base_component_repository_open: unable to open %s: %s (ignored)",
            ri->ri_path, err_msg);

        if (pmix_mca_base_component_track_load_errors) {
            pmix_mca_base_failed_component_t *f_comp = PMIX_NEW(pmix_mca_base_failed_component_t);
            f_comp->comp = ri;
            if (0 > asprintf(&(f_comp->error_msg), "%s", err_msg)) {
                PMIX_RELEASE(f_comp);
                free(err_msg);
                return PMIX_ERR_BAD_PARAM;
            }
            pmix_list_append(&framework->framework_failed_components, &f_comp->super);
        }

        free(err_msg);
        return PMIX_ERR_BAD_PARAM;
    }

    /* Successfully opened the component; now find the public struct.
       Malloc out enough space for it. */

    do {
        pmix_asprintf(&struct_name, "%s_mca_%s_%s_component",
                      ri->ri_project, ri->ri_type, ri->ri_name);

        mitem = PMIX_NEW(pmix_mca_base_component_list_item_t);
        if (NULL == mitem) {
            ret = PMIX_ERR_OUT_OF_RESOURCE;
            break;
        }

        err_msg = NULL;
        ret = pmix_pdl_lookup(ri->ri_dlhandle, struct_name, (void **) &component_struct, &err_msg);
        if (PMIX_SUCCESS != ret || NULL == component_struct) {
            if (NULL == err_msg) {
                err_msg = "pmix_dl_lookup() error message was NULL!";
            }
            pmix_output_verbose(
                vl, 0,
                "pmix_mca_base_component_repository_open: \"%s\" does not appear to be a valid "
                "%s MCA dynamic component (ignored):\n    %s (ret %d)",
                ri->ri_base, ri->ri_type, err_msg, ret);

            ret = PMIX_ERR_BAD_PARAM;
            break;
        }

        /* done with the structure name */
        free(struct_name);
        struct_name = NULL;

        /* We found the public struct.  Make sure its MCA major.minor
           version is the same as ours. TODO -- add checks for project version (from framework) */
        if (!(PMIX_MCA_BASE_VERSION_MAJOR == component_struct->pmix_mca_major_version
              && PMIX_MCA_BASE_VERSION_MINOR == component_struct->pmix_mca_minor_version)) {
            pmix_output_verbose(
                vl, 0,
                "pmix_mca_base_component_repository_open: %s\n    \"%s\" uses an MCA interface that is "
                "not recognized (component MCA v%d.%d.%d != supported MCA v%d.%d.%d) -- ignored",
                ri->ri_type, ri->ri_path, component_struct->pmix_mca_major_version,
                component_struct->pmix_mca_minor_version,
                component_struct->pmix_mca_release_version, PMIX_MCA_BASE_VERSION_MAJOR,
                PMIX_MCA_BASE_VERSION_MINOR, PMIX_MCA_BASE_VERSION_RELEASE);
            ret = PMIX_ERR_BAD_PARAM;
            break;
        }

        /* Also check that the component struct framework and component
           names match the expected names from the filename */
        if (0 != strcmp(component_struct->pmix_mca_type_name, ri->ri_type)
            || 0 != strcmp(component_struct->pmix_mca_component_name, ri->ri_name)) {
            pmix_output_verbose(
                vl, 0,
                "Component file data does not match filename:\n    %s (%s / %s) != %s %s -- ignored",
                ri->ri_path, ri->ri_type, ri->ri_name, component_struct->pmix_mca_type_name,
                component_struct->pmix_mca_component_name);
            ret = PMIX_ERR_BAD_PARAM;
            break;
        }

        /* Alles gut.  Save the component struct, and register this
           component to be closed later. */

        ri->ri_component_struct = mitem->cli_component = component_struct;
        ri->ri_refcnt = 1;
        pmix_list_append(&framework->framework_components, &mitem->super);

        pmix_output_verbose(PMIX_MCA_BASE_VERBOSE_INFO, 0,
                            "pmix_mca_base_component_repository_open: opened dynamic %s MCA "
                            "component \"%s\"",
                            ri->ri_type, ri->ri_name);

        return PMIX_SUCCESS;
    } while (0);

    if (mitem) {
        PMIX_RELEASE(mitem);
    }

    if (struct_name) {
        free(struct_name);
    }

    pmix_pdl_close(ri->ri_dlhandle);
    ri->ri_dlhandle = NULL;

    return ret;
#else

    /* no dlopen support */
    PMIX_HIDE_UNUSED_PARAMS(framework, ri);
    return PMIX_ERR_NOT_SUPPORTED;
#endif
}

/*
 * Finalize the repository -- close everything that's still open.
 */
void pmix_mca_base_component_repository_finalize(void)
{
    if (!initialized) {
        return;
    }

    initialized = false;

#if PMIX_HAVE_PDL_SUPPORT
    pmix_list_t *component_list;
    void *node, *key;
    size_t key_size;
    int ret;

    ret = pmix_hash_table_get_first_key_ptr(&pmix_mca_base_component_repository, &key, &key_size,
                                            (void **) &component_list, &node);
    while (PMIX_SUCCESS == ret) {
        PMIX_LIST_RELEASE(component_list);
        ret = pmix_hash_table_get_next_key_ptr(&pmix_mca_base_component_repository, &key, &key_size,
                                               (void **) &component_list, node, &node);
    }

    (void) pmix_mca_base_framework_close(&pmix_pdl_base_framework);
    PMIX_DESTRUCT(&pmix_mca_base_component_repository);
#endif
}

#if PMIX_HAVE_PDL_SUPPORT

/*
 * Basic sentinel values, and construct the inner list
 */
static void ri_constructor(pmix_mca_base_component_repository_item_t *ri)
{
    memset(ri->ri_type, 0, sizeof(ri->ri_type));
    ri->ri_dlhandle = NULL;
    ri->ri_component_struct = NULL;
    ri->ri_path = NULL;
}

/*
 * Close a component
 */
static void ri_destructor(pmix_mca_base_component_repository_item_t *ri)
{
    /* dlclose the component if it is still open */
    pmix_mca_base_component_repository_release_internal(ri);

    /* It should be obvious, but I'll state it anyway because it bit me
       during debugging: after the dlclose(), the pmix_mca_base_component_t
       pointer is no longer valid because it has [potentially] been
       unloaded from memory.  So don't try to use it.  :-) */

    if (NULL != ri->ri_project) {
        free(ri->ri_project);
    }
    if (NULL != ri->ri_path) {
        free(ri->ri_path);
    }

    if (NULL != ri->ri_base) {
        free(ri->ri_base);
    }
}

#endif /* PMIX_HAVE_PDL_SUPPORT */
