/*
 *
 *  Wireless daemon for Linux
 *
 *  Copyright (C) 2017-2019  Intel Corporation. All rights reserved.
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <getopt.h>
#include <ell/ell.h>
#include <readline/readline.h>

#include "client/command.h"
#include "client/display.h"

static struct l_queue *command_families;
static struct l_queue *command_options;
static int exit_status;
static bool interactive_mode;
static struct command_noninteractive {
	char **argv;
	int argc;
} command_noninteractive;

struct command_option {
	const char *name;
	char *value;
};

static void command_options_destroy(void *data)
{
	struct command_option *option = data;

	l_free(option->value);
	l_free(option);
}

bool command_option_get(const char *name, const char **value_out)
{
	const struct l_queue_entry *entry;

	for (entry = l_queue_get_entries(command_options); entry;
							entry = entry->next) {
		const struct command_option *option = entry->data;

		if (strcmp(option->name, name))
			continue;

		if (value_out)
			*value_out = option->value;

		return true;
	}

	return false;
}

bool command_needs_no_agent(void)
{
	return command_option_get(COMMAND_OPTION_DONTASK, NULL) &&
					(l_queue_length(command_options) == 1);
}

static enum cmd_status cmd_version(const char *entity,
						char **argv, int argc)
{
	display("IWD version %s\n", VERSION);

	return CMD_STATUS_DONE;
}

static enum cmd_status cmd_quit(const char *entity,
					char **argv, int argc)
{
	display_quit();

	l_main_quit();

	return CMD_STATUS_DONE;
}

static const struct command misc_commands[] = {
	{ NULL, "version", NULL, cmd_version, "Display version" },
	{ NULL, "quit",    NULL, cmd_quit,    "Quit program" },
	{ NULL, "exit",    NULL, cmd_quit },
	{ NULL, "help" },
	{ }
};

static char *cmd_generator(const char *text, int state)
{
	static const struct l_queue_entry *entry;
	static size_t index;
	static size_t len;
	const char *cmd;

	if (!state) {
		len = strlen(text);
		index = 0;
		entry = l_queue_get_entries(command_families);
	}

	while (entry) {
		const struct command_family *family = entry->data;

		entry = entry->next;

		if (strncmp(family->name, text, len))
			continue;

		return l_strdup(family->name);
	}

	while ((cmd = misc_commands[index].cmd)) {
		index++;

		if (strncmp(cmd, text, len))
			continue;

		return l_strdup(cmd);
	}

	return NULL;
}

static bool cmd_completion_cmd_has_arg(const char *cmd,
					const struct command_family *family)
{
	size_t i;
	char **matches = NULL;
	bool status;

	for (i = 0; family->command_list[i].cmd; i++) {
		if (strcmp(family->command_list[i].cmd, cmd))
			continue;

		return family->command_list[i].arg ? true : false;
	}

	if (!family->family_arg_completion)
		return false;

	matches = rl_completion_matches(cmd, family->family_arg_completion);
	if (!matches)
		return false;

	status = false;

	for (i = 0; matches[i]; i++) {
		if (strcmp(matches[i], cmd))
			continue;

		status = true;

		break;
	}

	l_strfreev(matches);

	return status;
}

static bool find_next_token(int *i, const char *token, int token_len)
{
	char *line = rl_line_buffer;

	while (*i && line[*i] == ' ')
		(*i)--;

	while (*i && line[*i] != ' ')
		(*i)--;

	return !strncmp(line + (line[*i] == ' ' ? *i + 1 : *i),
							token, token_len);
}

bool command_line_find_token(const char *token, uint8_t num_to_inspect)
{
	int i = rl_point - 1;
	int len = strlen(token);

	if (!len)
		return false;

	while (i && num_to_inspect) {
		if (find_next_token(&i, token, len))
			return true;

		num_to_inspect--;
	}

	return false;
}

/*
 * Work around readline limitations of not being able to pass a context pointer
 * to match functions. Set the command match function/entity to these globals
 * and call a generic match function which can call the _real_ match function
 * and include the entity.
 */
static command_completion_func_t cmd_current_completion_func = NULL;
static const char *cmd_current_entity = NULL;

static char *cmd_completion_generic(const char *text, int state)
{
	return cmd_current_completion_func(text, state, cmd_current_entity);
}

static char **cmd_completion_match_entity_cmd(const char *cmd, const char *text,
						const struct command *cmd_list)
{
	char **matches = NULL;
	size_t i;
	char *family = NULL;
	char *entity = NULL;
	char *prompt = NULL;

	for (i = 0; cmd_list[i].cmd; i++) {
		char *tmp;

		if (strcmp(cmd_list[i].cmd, cmd))
			continue;

		if (!cmd_list[i].completion)
			break;

		if (cmd_list[i].entity) {
			prompt = rl_copy_text(0, rl_end);

			family = strtok_r(prompt, " ", &tmp);
			if (!family)
				goto done;

			entity = strtok_r(NULL, " ", &tmp);
		}

done:
		cmd_current_completion_func = cmd_list[i].completion;
		cmd_current_entity = entity;

		matches = rl_completion_matches(text, cmd_completion_generic);

		l_free(prompt);
		cmd_current_completion_func = NULL;
		cmd_current_entity = NULL;

		break;
	}

	return matches;
}

static char **cmd_completion_match_family_cmd(const char *cmd_family,
						char *args, const char *text,
						bool ends_with_space)
{
	const struct l_queue_entry *entry;
	const char *arg1;
	const char *arg2;
	const char *arg3;
	char **matches = NULL;

	for (entry = l_queue_get_entries(command_families); entry;
							entry = entry->next) {
		const struct command_family *family = entry->data;

		if (strcmp(family->name, cmd_family))
			continue;

		arg1 = strtok_r(NULL, " ", &args);
		if (!arg1) {
			if (!family->family_arg_completion)
				break;

			matches = rl_completion_matches(text,
						family->family_arg_completion);

			break;
		}

		arg2 = strtok_r(NULL, " ", &args);
		if (!arg2 && !ends_with_space) {
			if (!family->family_arg_completion)
				break;

			matches = rl_completion_matches(text,
						family->family_arg_completion);
			break;
		} else if (!arg2 && ends_with_space) {
			if (!cmd_completion_cmd_has_arg(arg1, family))
				break;

			if (!family->entity_arg_completion)
				break;

			matches = rl_completion_matches(text,
						family->entity_arg_completion);
			break;
		}

		arg3 = strtok_r(NULL, " ", &args);
		if (!arg3 && !ends_with_space) {
			if (!family->entity_arg_completion)
				break;

			matches = rl_completion_matches(text,
						family->entity_arg_completion);
			break;
		}

		matches = cmd_completion_match_entity_cmd(arg2, text,
							family->command_list);

		break;
	}

	return matches;
}

char **command_completion(const char *text, int start, int end)
{
	char **matches = NULL;
	const char *family;
	char *args = NULL;
	char *prompt = NULL;
	bool ends_with_space = false;

	if (display_agent_is_active()) {
		rl_attempted_completion_over = 1;
		return NULL;
	}

	if (!start) {
		matches = rl_completion_matches(text, cmd_generator);

		goto done;
	}

	prompt = rl_copy_text(0, rl_end);

	family = strtok_r(prompt, " ", &args);
	if (!family)
		goto done;

	if (args) {
		int len = strlen(args);

		if (len > 0 && args[len - 1] == ' ')
			ends_with_space = true;
	}

	matches = cmd_completion_match_family_cmd(family, args, text,
							ends_with_space);

done:
	l_free(prompt);

	if (!matches)
		rl_attempted_completion_over = 1;

	return matches;
}

char *command_entity_arg_completion(const char *text, int state,
					const struct command *command_list)
{
	static size_t index;
	static size_t len;
	const char *cmd;

	if (!state) {
		index = 0;
		len = strlen(text);
	}

	while ((cmd = command_list[index].cmd)) {
		if (!command_list[index++].entity)
			continue;

		if (strncmp(cmd, text, len))
			continue;

		return l_strdup(cmd);
	}

	return NULL;
}

static void execute_cmd(const char *family, const char *entity,
					const struct command *cmd,
					char **argv, int argc)
{
	enum cmd_status status;

	display_refresh_set_cmd(family, entity, cmd, argv, argc);

	status = cmd->function(entity, argv, argc);

	if (status != CMD_STATUS_TRIGGERED && status != CMD_STATUS_DONE)
		goto error;

	if (status == CMD_STATUS_DONE && !interactive_mode) {
		l_main_quit();

		return;
	}

	if (!interactive_mode)
		return;

	if (cmd->refreshable)
		display_refresh_timeout_set();

	return;

error:
	switch (status) {
	case CMD_STATUS_INVALID_ARGS:
		display("Invalid command. Use the following pattern:\n");
		display_command_line(family, cmd);
		break;

	case CMD_STATUS_INVALID_VALUE:
		break;

	case CMD_STATUS_UNSUPPORTED:
		display_refresh_reset();

		display("Unsupported command\n");
		break;

	case CMD_STATUS_FAILED:
		goto failure;

	case CMD_STATUS_TRIGGERED:
	case CMD_STATUS_DONE:
		l_error("Unknown command status.");
		break;
	}

	if (interactive_mode)
		return;

failure:
	exit_status = EXIT_FAILURE;

	l_main_quit();
}

static bool match_cmd(const char *family, const char *param,
				char **argv, int argc,
				const struct command *command_list)
{
	size_t i;

	for (i = 0; command_list[i].cmd; i++) {
		const char *entity;
		const char *cmd;
		int offset;

		if  (command_list[i].entity) {
			if (argc < 1)
				continue;

			entity = param;
			cmd = argv[0];
			offset = 1;
		} else {
			entity = NULL;
			cmd = param;
			offset = 0;
		}

		if (strcmp(command_list[i].cmd, cmd))
			continue;

		if (!command_list[i].function)
			return false;

		execute_cmd(family, entity, &command_list[i],
				argv + offset, argc - offset);

		return true;
	}

	return false;
}

static bool match_cmd_family(char **argv, int argc)
{
	const struct l_queue_entry *entry;

	if (argc < 2)
		return false;

	for (entry = l_queue_get_entries(command_families); entry;
							entry = entry->next) {
		const struct command_family *family = entry->data;

		if (strcmp(family->name, argv[0]))
			continue;

		return match_cmd(family->name, argv[1], argv + 2, argc - 2,
					family->command_list);
	}

	return false;
}

static void list_commands(const char *command_family,
						const struct command *cmd_list)
{
	size_t i;

	for (i = 0; cmd_list[i].cmd; i++) {
		if (!cmd_list[i].desc)
			continue;

		display_command_line(command_family, &cmd_list[i]);
	}
}

static void list_cmd_options(void)
{
	display(MARGIN "--%-*s  %s\n", 48, COMMAND_OPTION_USERNAME,
					"Provide username");
	display(MARGIN "--%-*s  %s\n", 48, COMMAND_OPTION_PASSWORD,
					"Provide password");
	display(MARGIN "--%-*s  %s\n", 48, COMMAND_OPTION_PASSPHRASE,
					"Provide passphrase");
	display(MARGIN "--%-*s  %s\n", 48, COMMAND_OPTION_DONTASK,
					"Don't ask for missing\n"
					"\t\t\t\t\t\t      credentials");
	display(MARGIN "--%-*s  %s\n", 48, "help", "Display help");
}

static void list_cmd_families(void)
{
	const struct l_queue_entry *entry;

	for (entry = l_queue_get_entries(command_families); entry;
							entry = entry->next) {
		const struct command_family *family = entry->data;

		display("\n%s:\n", family->caption);
		list_commands(family->name, family->command_list);
	}
}

static void command_display_help(void)
{
	display("\n");
	display_table_header("iwctl version " VERSION, MARGIN "%-*s",
								5, "Usage");
	display(MARGIN "iwctl [--options] [commands]\n");
	display_table_footer();

	display_table_header("Available options", MARGIN "%-*s  %-*s",
					50, "Options", 28, "Description");
	list_cmd_options();
	display_table_footer();

	display_table_header("Available commands", MARGIN "%-*s  %-*s",
					50, "Commands", 28, "Description");
	list_cmd_families();
	display_table_footer();

	if (!interactive_mode)
		return;

	display("\nMiscellaneous:\n");

	list_commands(NULL, misc_commands);
}

static bool command_match_misc_commands(char **argv, int argc)
{
	if (match_cmd(NULL, argv[0], argv + 1, argc - 1, misc_commands))
		return true;

	if (strcmp(argv[0], "help"))
		return false;

	command_display_help();

	return true;
}

void command_process_prompt(char **argv, int argc)
{
	if (argc == 0)
		return;

	if (match_cmd_family(argv, argc))
		return;

	if (!interactive_mode) {
		if (command_match_misc_commands(argv, argc)) {
			exit_status = EXIT_SUCCESS;
			goto quit;
		}

		display_error("Invalid command\n");
		exit_status = EXIT_FAILURE;
quit:
		l_main_quit();
		return;
	}

	display_refresh_reset();

	if (command_match_misc_commands(argv, argc))
		return;

	display_error("Invalid command\n");
}

void command_noninteractive_trigger(void)
{
	if (!command_noninteractive.argc)
		return;

	command_process_prompt(command_noninteractive.argv,
						command_noninteractive.argc);
}

bool command_is_interactive_mode(void)
{
	return interactive_mode;
}

void command_set_exit_status(int status)
{
	exit_status = status;
}

int command_get_exit_status(void)
{
	return exit_status;
}

void command_family_register(const struct command_family *family)
{
	l_queue_push_tail(command_families, (void *) family);
}

void command_family_unregister(const struct command_family *family)
{
	l_queue_remove(command_families, (void *) family);
}

static const struct option command_opts[] = {
	{ COMMAND_OPTION_USERNAME,	required_argument, NULL, 'u' },
	{ COMMAND_OPTION_PASSWORD,	required_argument, NULL, 'p' },
	{ COMMAND_OPTION_PASSPHRASE,	required_argument, NULL, 'P' },
	{ COMMAND_OPTION_DONTASK,	no_argument,	   NULL, 'd' },
	{ "help",			no_argument,	   NULL, 'h' },
	{ }
};

extern struct command_family_desc __start___command[];
extern struct command_family_desc __stop___command[];

bool command_init(char **argv, int argc)
{
	struct command_family_desc *desc;
	int opt;

	command_families = l_queue_new();
	command_options = l_queue_new();

	for (desc = __start___command; desc < __stop___command; desc++) {
		if (!desc->init)
			continue;

		desc->init();
	}

	for (;;) {
		struct command_option *option;

		opt = getopt_long(argc, argv, "u:p:P:dh", command_opts, NULL);

		switch (opt) {
		case 'u':
			option = l_new(struct command_option, 1);
			option->name = COMMAND_OPTION_USERNAME;
			option->value = l_strdup(optarg);

			l_queue_push_tail(command_options, option);

			break;
		case 'p':
			option = l_new(struct command_option, 1);
			option->name = COMMAND_OPTION_PASSWORD;
			option->value = l_strdup(optarg);

			l_queue_push_tail(command_options, option);

			break;
		case 'P':
			option = l_new(struct command_option, 1);
			option->name = COMMAND_OPTION_PASSPHRASE;
			option->value = l_strdup(optarg);

			l_queue_push_tail(command_options, option);

			break;
		case 'd':
			option = l_new(struct command_option, 1);
			option->name = COMMAND_OPTION_DONTASK;

			l_queue_push_tail(command_options, option);

			break;
		case 'h':
			command_display_help();

			l_main_quit();

			return true;
		case -1:
			goto options_parsed;
		case '?':
			exit_status = EXIT_FAILURE;

			return true;
		}
	}

options_parsed:
	argv += optind;
	argc -= optind;

	if (argc < 1) {
		interactive_mode = true;
		return false;
	}

	command_noninteractive.argv = argv;
	command_noninteractive.argc = argc;

	return false;
}

void command_exit(void)
{
	struct command_family_desc *desc;

	for (desc = __start___command; desc < __stop___command; desc++) {
		if (!desc->exit)
			continue;

		desc->exit();
	}

	l_queue_destroy(command_families, NULL);
	command_families = NULL;

	l_queue_destroy(command_options, command_options_destroy);
	command_options = NULL;
}
