/**
 * @file completion.c
 * @author Michal Vasko <mvasko@cesnet.cz>
 * @brief libyang's yanglint tool auto completion
 *
 * Copyright (c) 2015 - 2025 CESNET, z.s.p.o.
 *
 * This source code is licensed under BSD 3-Clause License (the "License").
 * You may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://opensource.org/licenses/BSD-3-Clause
 */

#define _GNU_SOURCE
#if !defined (__FreeBSD__) /* hides asprintf */
# define _POSIX_C_SOURCE 200809L /* strdup */
#endif

#include <dirent.h>
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

#include "libyang.h"

#include "cmd.h"
#include "common.h"
#include "compat.h"
#include "linenoise/linenoise.h"

/**
 * @brief Fill path completion.
 *
 * @param[in] hint Path to directory.
 * @param[in] lc For adding completions.
 */
static void
path_completion(const char *hint, linenoiseCompletions *lc)
{
    const char *ptr;
    char *full_path, *hint_ptr, match[FILENAME_MAX + 2];
    DIR *dir;
    struct dirent *ent;
    struct stat st;

    lc->path = 1;

    ptr = strrchr(hint, '/');

    /* new relative path */
    if (ptr == NULL) {
        full_path = malloc(2 + FILENAME_MAX + 1);
        strcpy(full_path, "./");

        ptr = hint;
    } else {
        full_path = malloc((int)(ptr - hint) + FILENAME_MAX + 1);
        ++ptr;
        sprintf(full_path, "%.*s", (int)(ptr - hint), hint);
    }
    hint_ptr = full_path + strlen(full_path);

    dir = opendir(full_path);
    if (dir == NULL) {
        free(full_path);
        return;
    }

    while ((ent = readdir(dir))) {
        if (ent->d_name[0] == '.') {
            continue;
        }

        if (!strncmp(ptr, ent->d_name, strlen(ptr))) {
            /* is it a directory? */
            strcpy(hint_ptr, ent->d_name);
            if (stat(full_path, &st)) {
                /* skip this item */
                continue;
            }

            strcpy(match, ent->d_name);
            if (S_ISDIR(st.st_mode)) {
                strcat(match, "/");
            }

            linenoiseAddCompletion(lc, match);
        }
    }

    free(full_path);
    closedir(dir);
}

/* from the main.c */
extern struct ly_ctx *ctx;

/**
 * @brief Add a match to the completions array.
 *
 * @param[in] match Match to be added.
 * @param[in,out] matches Matches provided to the user as a completion hint.
 * @param[in,out] match_count Number of matches.
 */
static void
cmd_completion_add_match(const char *match, char ***matches, unsigned int *match_count)
{
    void *p;

    p = realloc(*matches, (*match_count + 1) * sizeof **matches);
    if (!p) {
        YLMSG_E("Memory allocation failed (%s:%d, %s).", __FILE__, __LINE__, strerror(errno));
        return;
    }
    *matches = p;
    (*matches)[*match_count] = strdup(match);
    if (!((*matches)[*match_count])) {
        YLMSG_E("Memory allocation failed (%s:%d, %s).", __FILE__, __LINE__, strerror(errno));
        return;
    }
    ++(*match_count);
}

/**
 * @brief Provides completion for command names.
 *
 * @param[in] hint User input.
 * @param[out] matches Matches provided to the user as a completion hint.
 * @param[out] match_count Number of matches.
 */
static void
get_cmd_completion(const char *hint, char ***matches, unsigned int *match_count)
{
    int i;

    *match_count = 0;
    *matches = NULL;

    for (i = 0; commands[i].name; i++) {
        if (!strncmp(hint, commands[i].name, strlen(hint))) {
            cmd_completion_add_match(commands[i].name, matches, match_count);
        }
    }
}

/**
 * @brief Provides completion for arguments.
 *
 * @param[in] hint User input.
 * @param[in] args Array of all possible arguments. The last element must be NULL.
 * @param[out] matches Matches provided to the user as a completion hint.
 * @param[out] match_count Number of matches.
 */
static void
get_arg_completion(const char *hint, const char **args, char ***matches, unsigned int *match_count)
{
    int i;

    *match_count = 0;
    *matches = NULL;

    for (i = 0; args[i]; i++) {
        if (!strncmp(hint, args[i], strlen(hint))) {
            cmd_completion_add_match(args[i], matches, match_count);
        }
    }
    if (*match_count == 0) {
        for (i = 0; args[i]; i++) {
            cmd_completion_add_match(args[i], matches, match_count);
        }
    }
}

/**
 * @brief Provides completion for module names.
 *
 * @param[in] hint User input.
 * @param[out] matches Matches provided to the user as a completion hint.
 * @param[out] match_count Number of matches.
 */
static void
get_model_completion(const char *hint, char ***matches, unsigned int *match_count)
{
    LY_ARRAY_COUNT_TYPE u;
    uint32_t idx = 0;
    const struct lys_module *module;

    *match_count = 0;
    *matches = NULL;

    while ((module = ly_ctx_get_module_iter(ctx, &idx))) {
        if (!strncmp(hint, module->name, strlen(hint))) {
            cmd_completion_add_match(module->name, matches, match_count);
        }

        LY_ARRAY_FOR(module->parsed->includes, u) {
            if (!strncmp(hint, module->parsed->includes[u].submodule->name, strlen(hint))) {
                cmd_completion_add_match(module->parsed->includes[u].submodule->name, matches, match_count);
            }
        }
    }
}

/**
 * @brief Add all child nodes of a single node to the completion hint.
 *
 * @param[in] parent Node of which children will be added to the hint.
 * @param[out] matches Matches provided to the user as a completion hint.
 * @param[out] match_count Number of matches.
 */
static void
single_hint_add_children(const struct lysc_node *parent, char ***matches, unsigned int *match_count)
{
    const struct lysc_node *node = NULL;
    char *match;

    if (!parent) {
        return;
    }

    while ((node = lys_getnext(node, parent, NULL, LYS_GETNEXT_WITHCASE | LYS_GETNEXT_WITHCHOICE))) {
        match = lysc_path(node, LYSC_PATH_LOG, NULL, 0);
        cmd_completion_add_match(match, matches, match_count);
        free(match);
    }
}

/**
 * @brief Add module and/or node's children names to the hint.
 *
 * @param[in] module Compiled schema module.
 * @param[in] parent Parent node of which children are potential matches.
 * @param[in] hint_node_name Node name contained within the hint specified by user.
 * @param[in,out] matches Matches provided to the user as a completion hint.
 * @param[in,out] match_count Number of matches.
 * @param[out] last_node Last processed node.
 */
static void
add_all_children_nodes(const struct lysc_module *module, const struct lysc_node *parent,
        const char *hint_node_name, char ***matches, unsigned int *match_count, const struct lysc_node **last_node)
{
    const struct lysc_node *node;
    char *match, *node_name = NULL;

    *last_node = NULL;

    if (!parent && !module) {
        return;
    }

    node = NULL;
    while ((node = lys_getnext(node, parent, module, LYS_GETNEXT_WITHCASE | LYS_GETNEXT_WITHCHOICE))) {
        if (parent && (node->module != parent->module)) {
            /* augmented node */
            if (asprintf(&node_name, "%s:%s", node->module->name, node->name) == -1) {
                YLMSG_E("Memory allocation failed (%s:%d, %s).", __FILE__, __LINE__, strerror(errno));
                break;
            }
        } else {
            node_name = strdup(node->name);
            if (!node_name) {
                YLMSG_E("Memory allocation failed (%s:%d, %s).", __FILE__, __LINE__, strerror(errno));
                break;
            }
        }
        if (!hint_node_name || !strncmp(hint_node_name, node_name, strlen(hint_node_name))) {
            /* adding just module names + their top level node(s) to the hint */
            *last_node = node;
            match = lysc_path(node, LYSC_PATH_LOG, NULL, 0);
            cmd_completion_add_match(match, matches, match_count);
            free(match);
        }
        free(node_name);
    }
}

/**
 * @brief Provides completion for schemas.
 *
 * @param[in] hint User input.
 * @param[out] matches Matches provided to the user as a completion hint.
 * @param[out] match_count Number of matches.
 */
static void
get_schema_completion(const char *hint, char ***matches, unsigned int *match_count)
{
    const struct lys_module *module;
    uint32_t idx;
    const char *start;
    char *end, *module_name = NULL, *path = NULL;
    const struct lysc_node *parent, *last_node = NULL;
    int rc = 0;
    size_t len;

    *match_count = 0;
    *matches = NULL;

    if (strlen(hint)) {
        if (hint[0] != '/') {
            return;
        }
        start = hint + 1;
    } else {
        start = hint;
    }

    end = strchr(start, ':');
    if (!end) {
        /* no module name */
        len = strlen(start);

        /* go through all the modules */
        idx = 0;
        while ((module = ly_ctx_get_module_iter(ctx, &idx))) {
            if (!module->implemented) {
                continue;
            }

            if (!len || !strncmp(start, module->name, len)) {
                /* add all their (matching) top level nodes */
                add_all_children_nodes(module->compiled, NULL, NULL, matches, match_count, &last_node);
            }
        }
    } else {
        /* module name known */
        module_name = strndup(start, end - start);
        if (!module_name) {
            YLMSG_E("Memory allocation failed (%s:%d, %s).", __FILE__, __LINE__, strerror(errno));
            rc = 1;
            goto cleanup;
        }

        module = ly_ctx_get_module_implemented(ctx, module_name);
        if (!module) {
            goto cleanup;
        }

        /* find the last '/', if it is at the beginning of the hint, only path up to the top level node is known,
         * else the name of the last node starts after the found '/' */
        start = strrchr(hint, '/');
        if (!start) {
            goto cleanup;
        }

        if (start == hint) {
            /* only the (incomplete) top level node path, add all (matching) top level nodes */
            add_all_children_nodes(module->compiled, NULL, end + 1, matches, match_count, &last_node);
            goto cleanup;
        }

        /* get rid of stuff after the last '/' to obtain the parent node */
        path = strndup(hint, start - hint);
        if (!path) {
            YLMSG_E("Memory allocation failed (%s:%d, %s).", __FILE__, __LINE__, strerror(errno));
            rc = 1;
            goto cleanup;
        }

        /* get the last parent in the hint (it may not exist) */
        parent = find_schema_path(ctx, path);

        /* add all (matching) child nodes of the parent */
        add_all_children_nodes(NULL, parent, start + 1, matches, match_count, &last_node);
    }

cleanup:
    if (!rc && (*match_count == 1)) {
        /* to avoid a single hint (space at the end), add all children as hints */
        single_hint_add_children(last_node, matches, match_count);
    }
    free(path);
    free(module_name);
}

/**
 * @brief Get all possible argument hints for option.
 *
 * @param[in] hint User input.
 * @param[out] matches Matches provided to the user as a completion hint.
 * @param[out] match_count Number of matches.
 */
static void
get_print_format_arg(const char *hint, char ***matches, unsigned int *match_count)
{
    const char *args[] = {"yang", "yin", "tree", "info", NULL};

    get_arg_completion(hint, args, matches, match_count);
}

/**
 * @copydoc get_print_format_arg
 */
static void
get_data_type_arg(const char *hint, char ***matches, unsigned int *match_count)
{
    const char *args[] = {"data", "config", "get", "getconfig", "edit", "rpc", "reply", "notif", NULL};

    get_arg_completion(hint, args, matches, match_count);
}

/**
 * @copydoc get_print_format_arg
 */
static void
get_data_in_format_arg(const char *hint, char ***matches, unsigned int *match_count)
{
    const char *args[] = {"xml", "json", "lyb", NULL};

    get_arg_completion(hint, args, matches, match_count);
}

/**
 * @copydoc get_print_format_arg
 */
static void
get_data_default_arg(const char *hint, char ***matches, unsigned int *match_count)
{
    const char *args[] = {"all", "all-tagged", "trim", "implicit-tagged", NULL};

    get_arg_completion(hint, args, matches, match_count);
}

/**
 * @copydoc get_print_format_arg
 */
static void
get_list_format_arg(const char *hint, char ***matches, unsigned int *match_count)
{
    const char *args[] = {"xml", "json", NULL};

    get_arg_completion(hint, args, matches, match_count);
}

/**
 * @copydoc get_print_format_arg
 */
static void
get_verb_arg(const char *hint, char ***matches, unsigned int *match_count)
{
    const char *args[] = {"error", "warning", "verbose", "debug", NULL};

    get_arg_completion(hint, args, matches, match_count);
}

/**
 * @copydoc get_print_format_arg
 */
static void
get_debug_arg(const char *hint, char ***matches, unsigned int *match_count)
{
    const char *args[] = {"dict", "xpath", "dep-sets", NULL};

    get_arg_completion(hint, args, matches, match_count);
}

/**
 * @brief Get the string before the hint, which autocompletion is for.
 *
 * @param[in] buf Complete user input.
 * @param[in] hint Hint part of the user input.
 * @return Pointer to the last string.
 */
static const char *
get_last_str(const char *buf, const char *hint)
{
    const char *ptr;

    if (buf == hint) {
        return buf;
    }

    ptr = hint - 1;
    while (ptr[0] == ' ') {
        --ptr;
        if (buf == ptr) {
            return buf;
        }
    }

    while (ptr[-1] != ' ') {
        --ptr;
        if (buf == ptr) {
            return buf;
        }
    }

    return ptr;
}

/* callback */
void
complete_cmd(const char *buf, const char *hint, linenoiseCompletions *lc)
{
    struct autocomplete {
        enum COMMAND_INDEX ci;      /**< command index to global variable 'commands' */
        const char *opt;            /**< optional option */
        void (*yl_cb)(const char *, char ***, unsigned int *);  /**< yanglint callback to call */
    } ac[] = {
        {CMD_ADD,         NULL,    NULL},
        {CMD_PRINT,       "-f",    get_print_format_arg},
        {CMD_PRINT,       "-P",    get_schema_completion},
        {CMD_PRINT,       "-o",    NULL},
        {CMD_PRINT,       NULL,    get_model_completion},
        {CMD_SEARCHPATH,  NULL,    NULL},
        {CMD_EXTDATA,     NULL,    NULL},
        {CMD_CLEAR,       "-Y",    NULL},
        {CMD_DATA,        "-t",    get_data_type_arg},
        {CMD_DATA,        "-O",    NULL},
        {CMD_DATA,        "-R",    NULL},
        {CMD_DATA,        "-f",    get_data_in_format_arg},
        {CMD_DATA,        "-F",    get_data_in_format_arg},
        {CMD_DATA,        "-d",    get_data_default_arg},
        {CMD_DATA,        "-o",    NULL},
        {CMD_DATA,        NULL,    NULL},
        {CMD_LIST,        NULL,    get_list_format_arg},
        {CMD_FEATURE,     NULL,    get_model_completion},
        {CMD_VERB,        NULL,    get_verb_arg},
        {CMD_DEBUG,       NULL,    get_debug_arg},
    };
    size_t name_len;
    const char *last, *name, *getoptstr;
    char opt[3] = {'\0', ':', '\0'};
    char **matches = NULL;
    unsigned int match_count = 0, i;

    if (buf == hint) {
        /* command autocomplete */
        get_cmd_completion(hint, &matches, &match_count);

    } else {
        for (i = 0; i < (sizeof ac / sizeof *ac); ++i) {
            /* Find the right command. */
            name = commands[ac[i].ci].name;
            name_len = strlen(name);
            if (strncmp(buf, name, name_len) || (buf[name_len] != ' ')) {
                /* not this command */
                continue;
            }

            /* Select based on the right option. */
            last = get_last_str(buf, hint);
            opt[0] = (last[0] == '-') && last[1] ? last[1] : '\0';
            getoptstr = commands[ac[i].ci].optstring;
            if (!ac[i].opt && opt[0] && strstr(getoptstr, opt)) {
                /* completion for the argument must be defined */
                continue;
            } else if (ac[i].opt && opt[0] && strncmp(ac[i].opt, last, strlen(ac[i].opt))) {
                /* completion for (another) option */
                continue;
            } else if (ac[i].opt && !opt[0]) {
                /* completion is defined for option */
                continue;
            }

            /* callback */
            if (ac[i].yl_cb) {
                ac[i].yl_cb(hint, &matches, &match_count);
            } else {
                path_completion(hint, lc);
            }
            break;
        }
    }

    /* transform matches into autocompletion, if needed */
    for (i = 0; i < match_count; ++i) {
        linenoiseAddCompletion(lc, matches[i]);
        free(matches[i]);
    }
    free(matches);
}
