/*
 * SPDX-License-Identifier: ISC
 *
 * Copyright (C) 2021-2022 Michael Drake <tlsa@netsurf-browser.org>
 */

/**
 * \file
 * \brief Command line argument handling.
 */

#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "cli.h"

/**
 * CLI parsing context.
 */
struct cli_ctx {
	const struct cli_table *cli; /**< Client CLI spec. */
	size_t pos_count; /**< The number of positional arguments found. */
	bool no_pos; /**< Have an argument that negates min_positional. */
};

/**
 * Check whether a CLI argument type should have a numerical value.
 *
 * \param[in]  type  An argument type.
 * \return true if the argument needs a numerical value, or false otherwise.
 */
static inline bool cli__arg_is_numerical(enum cli_arg_type type)
{
	return (type != CLI_STRING && type != CLI_BOOL);
}

/**
 * Parse a signed integer value from an argument.
 *
 * \param[in]     str  String containing value to parse.
 * \param[out]    i    Pointer to place to store parsed value.
 * \param[in,out] pos  Current position in str, updated on exit.
 * \return true on success, or false otherwise.
 */
static bool cli__parse_value_int(
		const char *str,
		int64_t *i,
		size_t *pos)
{
	long long temp;
	char *end = NULL;

	str += *pos;
	errno = 0;
	temp = strtoll(str, &end, 0);

	if (end == str || errno == ERANGE ||
	    temp > INT64_MAX || temp < INT64_MIN) {
		fprintf(stderr, "Failed to parse integer from '%s'\n", str);
		return false;
	}

	*i = (int64_t)temp;
	*pos += (size_t)(end - str);
	return true;
}

/**
 * Parse an unsigned integer value from an argument.
 *
 * \param[in]     str  String containing value to parse.
 * \param[out]    u    Pointer to place to store parsed value.
 * \param[in,out] pos  Current position in str, updated on exit.
 * \return true on success, or false otherwise.
 */
static bool cli__parse_value_uint(
		const char *str,
		uint64_t *u,
		size_t *pos)
{
	unsigned long long temp;
	char *end = NULL;

	str += *pos;
	errno = 0;
	temp = strtoull(str, &end, 0);

	if (end == str || errno == ERANGE || temp > UINT64_MAX) {
		fprintf(stderr, "Failed to parse unsigned from '%s'\n", str);
		return false;
	}

	*u = (uint64_t)temp;
	*pos += (size_t)(end - str);
	return true;
}

/**
 * Parse an enum value from an argument.
 *
 * \param[in]     str  String containing value to parse.
 * \param[out]    e    Enum details.
 * \param[in,out] pos  Current position in str, updated on exit.
 * \return true on success, or false otherwise.
 */
static bool cli__parse_value_enum(
		const char *str,
		const struct cli_enum *e,
		size_t *pos)
{
	str += *pos;
	*pos += strlen(str);

	for (const struct cli_str_val *sv = e->desc; sv->str != NULL; sv++) {
		if (strcmp(str, sv->str) == 0) {
			*e->e = sv->val;
			return true;
		}
	}

	fprintf(stderr, "ERROR: Unknown enum value '%s'.\n", str);

	return false;
}

/**
 * Parse a string value from an argument.
 *
 * \param[in]     str  String containing value to parse.
 * \param[out]    s    Pointer to place to store parsed value.
 * \param[in,out] pos  Current position in str, updated on exit.
 * \return true on success, or false otherwise.
 */
static bool cli__parse_value_string(
		const char *str,
		const char **s,
		size_t *pos)
{
	*s = str + *pos;
	*pos += strlen(*s);
	return true;
}

/**
 * Parse a value from an argument.
 *
 * \param[in]     entry  Client command line interface argument specification.
 * \param[in]     arg    Argument to parse a value from.
 * \param[in,out] pos    Current position in argument, updated on exit.
 * \return true on success, or false otherwise.
 */
static bool cli__parse_value(
		const struct cli_table_entry *entry,
		const char *arg,
		size_t *pos)
{
	switch (entry->t) {
	case CLI_CMD:
		if (strcmp(arg + *pos, entry->l) == 0) {
			*pos += strlen(arg);
			return true;
		}
		return false;

	case CLI_INT:
		return cli__parse_value_int(arg, entry->v.i, pos);

	case CLI_UINT:
		return cli__parse_value_uint(arg, entry->v.u, pos);

	case CLI_ENUM:
		return cli__parse_value_enum(arg, &entry->v.e, pos);

	case CLI_STRING:
		return cli__parse_value_string(arg, entry->v.s, pos);

	default:
		fprintf(stderr, "Unexpected value for '%s': %s\n",
				entry->l, arg);
		break;
	}

	return false;
}

/**
 * Parse a value from an argument.
 *
 * \param[in]     entry    Client command line interface argument specification.
 * \param[in]     argc     Number of command line arguments.
 * \param[in]     argv     String vector containing command line arguments.
 * \param[in]     arg_pos  Current position in argv.
 * \param[in,out] pos      Current pos in current argument, updated on exit.
 * \return true on success, or false otherwise.
 */
static bool cli__parse_argv_value(const struct cli_table_entry *entry,
		int argc, const char **argv,
		int arg_pos, size_t *pos)
{
	const char *arg = argv[arg_pos];

	if (arg_pos >= argc) {
		fprintf(stderr, "Value not given for '%s'\n", entry->l);
		return false;
	}

	return cli__parse_value(entry, arg, pos);
}

/**
 * Check whether a CLI argument is a positional value.
 *
 * \param[in]  entry    Client command line interface argument specification.
 * \return true if the argument is positional, or false otherwise.
 */
static inline bool cli__entry_is_positional(const struct cli_table_entry *entry)
{
	return entry->p;
}

/**
 * Look up a short argument flag.
 *
 * \param[in]  cli  Client command line interface specification.
 * \param[in]  s    Argument flag to look up in client CLI spec.
 * \return Client CLI spec entry on success, or NULL otherwise.
 */
static const struct cli_table_entry *cli__lookup_short(
		const struct cli_table *cli, char s)
{
	for (size_t i = 0; i < cli->count; i++) {
		if (cli__entry_is_positional(&cli->entries[i])) {
			continue;
		}
		if (cli->entries[i].s == s) {
			return &cli->entries[i];
		}
	}

	fprintf(stderr, "Unknown flag: '%c'\n", s);
	return NULL;
}

/**
 * Handle an argument with a type that requires a value.
 *
 * This can handle the value being in the current argument, optionally split by
 * a separator, or in the next argument.
 *
 * \param[in]     entry    Client command line interface argument specification.
 * \param[in]     argc     Number of command line arguments.
 * \param[in]     argv     String vector containing command line arguments.
 * \param[in,out] arg_pos  Current position in argv, updated on exit.
 * \param[in]     pos      Current position in current argument string.
 * \param[in]     sep      Name/value separator character, or '\0' if none.
 * \return true on success, or false otherwise.
 */
static bool cli__handle_arg_value(const struct cli_table_entry *entry,
		int argc, const char **argv, int *arg_pos, size_t pos, char sep)
{
	const char *arg = argv[*arg_pos];
	size_t orig_pos;
	bool ret;

	if (arg[pos] == '\0') {
		(*arg_pos)++;
		pos = 0;
	} else if (arg[pos] == sep) {
		pos++;
	} else if (cli__arg_is_numerical(entry->t) == false) {
		fprintf(stderr, "Separator required for non-numerical value\n");
		return false;
	}

	if (isspace(argv[*arg_pos][pos])) {
		fprintf(stderr, "Unexpected white space in '%s' "
				"for argument '%s'\n",
				&argv[*arg_pos][pos], entry->l);
		return false;
	}

	orig_pos = pos;
	ret = cli__parse_argv_value(entry, argc, argv, *arg_pos, &pos);
	if (ret != true) {
		return ret;
	}

	if (argv[*arg_pos][pos] != '\0') {
		fprintf(stderr, "Invalid value '%s' for argument '%s'\n",
				&argv[*arg_pos][orig_pos], entry->l);
		return false;
	}

	return true;
}

static inline bool cli__is_negative(const char *arg)
{
	int64_t i;
	size_t pos = 0;

	return cli__parse_value_int(arg, &i, &pos)
			&& pos == strlen(arg)
			&& i < 0;
}

/**
 * Parse a positional argument according to the given CLI spec entry.
 *
 * \param[in] ctx    Command line interface parsing context.
 * \param[in] entry  Client command line interface argument specification.
 * \param[in] arg    Argument to parse.
 * \return true on success, or false otherwise.
 */
static bool cli__parse_positional_entry(struct cli_ctx *ctx,
		const struct cli_table_entry *entry,
		const char *arg)
{
	size_t pos = 0;
	bool ret;

	ret = cli__parse_value(entry, arg, &pos);
	if (ret != true) {
		return ret;
	} else if (arg[pos] != '\0') {
		fprintf(stderr, "Failed to parse value '%s' for arg '%s'\n",
				arg, entry->l);
		return false;
	}

	ctx->pos_count++;
	return true;
}

/**
 * Parse a positional argument.
 *
 * \param[in] ctx    Command line interface parsing context.
 * \param[in] arg    Argument to parse.
 * \return true on success, or false otherwise.
 */
static bool cli__parse_positional(struct cli_ctx *ctx,
		const char *arg)
{
	const struct cli_table *cli = ctx->cli;
	size_t positional = 0;

	for (size_t i = 0; i < cli->count; i++) {
		if (cli__entry_is_positional(&cli->entries[i])) {
			if (positional == ctx->pos_count) {
				return cli__parse_positional_entry(ctx,
						&cli->entries[i], arg);
			}

			positional++;
		}
	}

	fprintf(stderr, "Unexpected positional argument: '%s'\n", arg);
	return false;
}

/**
 * Parse a flags argument.
 *
 * \param[in]  ctx      Command line interface parsing context.
 * \param[in]  argc     Number of command line arguments.
 * \param[in]  argv     String vector containing command line arguments.
 * \param[out] arg_pos  Current position in argv, updated on exit.
 * \return true on success, or false otherwise.
 */
static bool cli__parse_short(struct cli_ctx *ctx,
		int argc, const char **argv, int *arg_pos)
{
	const char *arg = argv[*arg_pos];
	size_t pos = 1;

	if (arg[0] != '-') {
		return false;
	}

	while (arg[pos] != '\0') {
		const struct cli_table_entry *entry;

		entry = cli__lookup_short(ctx->cli, arg[pos]);
		if (entry == NULL) {
			if (cli__is_negative(argv[pos])) {
				return cli__parse_positional(ctx, argv[pos]);
			}
			return false;
		}

		if (entry->no_pos) {
			ctx->no_pos = true;
		}

		if (entry->t == CLI_BOOL) {
			*entry->v.b = true;
		} else {
			return cli__handle_arg_value(entry, argc, argv,
					arg_pos, pos + 1, '\0');
		}

		pos++;
	}

	return true;
}

/**
 * Look up a long argument name.
 *
 * \param[in]     cli  Client command line interface specification.
 * \param[in]     arg  Argument name to look up in cli spec.
 * \param[in,out] pos  Current position in arg, updated on exit.
 * \return Client CLI spec entry on success, or NULL otherwise.
 */
static const struct cli_table_entry *cli__lookup_long(
		const struct cli_table *cli,
		const char *arg,
		size_t *pos)
{
	arg += *pos;

	for (size_t i = 0; i < cli->count; i++) {
		if (cli__entry_is_positional(&cli->entries[i]) == false) {
			const char *name = cli->entries[i].l;
			size_t name_len = strlen(cli->entries[i].l);

			if (strncmp(name, arg, name_len) == 0) {
				if (arg[name_len] != '\0' &&
				    arg[name_len] != '=') {
					continue;
				}
				*pos += name_len;
				return &cli->entries[i];
			}
		}
	}

	fprintf(stderr, "Unknown argument: '%s'\n", arg);
	return NULL;
}

/**
 * Parse a long argument.
 *
 * \param[in]  ctx      Command line interface parsing context.
 * \param[in]  argc     Number of command line arguments.
 * \param[in]  argv     String vector containing command line arguments.
 * \param[out] arg_pos  Current position in argv, updated on exit.
 * \return true on success, or false otherwise.
 */
static bool cli__parse_long(struct cli_ctx *ctx,
		int argc, const char **argv, int *arg_pos)
{
	const struct cli_table_entry *entry;
	const char *arg = argv[*arg_pos];
	size_t pos = 2;

	if (arg[0] != '-' ||
	    arg[1] != '-') {
		return false;
	}

	entry = cli__lookup_long(ctx->cli, arg, &pos);
	if (entry == NULL) {
		return false;
	}

	if (entry->no_pos) {
		ctx->no_pos = true;
	}

	if (entry->t == CLI_BOOL) {
		if (arg[pos] != '\0') {
			fprintf(stderr, "Unexpected value for argument '%s'\n",
					arg);
			return false;
		}
		*entry->v.b = true;
	} else {
		bool ret;

		ret = cli__handle_arg_value(entry, argc, argv,
				arg_pos, pos, '=');
		if (ret != true) {
			return ret;
		}
	}

	return true;
}

/**
 * Get the string to indicate type of value expected for an argument.
 *
 * \param[in] type  The argument type.
 * \return String for value type.
 */
static const char *cli__string_from_type(enum cli_arg_type type)
{
	static const char *const strings[] = {
		[CLI_BOOL]   = "",
		[CLI_INT]    = "INT",
		[CLI_UINT]   = "UINT",
		[CLI_ENUM]   = "ENUM",
		[CLI_STRING] = "STRING",
	};

	if (type >= CLI_ARRAY_LEN(strings) || strings[type] == NULL) {
		return "";
	}

	return strings[type];
}

/**
 * Helper to update a maximum adjusted string length if new values is greater.
 *
 * \param[in]  str         String to check.
 * \param[in]  adjustment  Amount to modify length of string by (bytes).
 * \param[out] len         Returns the maximum of existing and this length.
 */
static void cli__max_len(const char *str, size_t adjustment, size_t *len)
{
	size_t str_len = strlen(str) + adjustment;

	if (str_len > *len) {
		*len = str_len;
	}
}

/**
 * Count up various properties of the client CLI interface specification.
 *
 * \param[in]  cli        Client command line interface specification.
 * \param[out] count      Returns number of non-positional arguments.
 * \param[out] pcount     Returns number of positional arguments.
 * \param[out] max_len    Returns max string length of non-positional arguments.
 * \param[out] pmax_len   Returns max string length of positional arguments.
 * \param[out] phas_desc  Returns number of positional args with descriptions.
 */
static void cli__count(const struct cli_table *cli,
		size_t *count,
		size_t *pcount,
		size_t *max_len,
		size_t *pmax_len,
		size_t *phas_desc)
{
	if (count != NULL) *count = 0;
	if (pcount != NULL) *pcount = 0;
	if (max_len != NULL) *max_len = 0;
	if (pmax_len != NULL) *pmax_len = 0;
	if (phas_desc != NULL) *phas_desc = 0;

	for (size_t i = 0; i < cli->count; i++) {
		const struct cli_table_entry *entry = &cli->entries[i];

		if (cli__entry_is_positional(entry)) {
			if (pcount != NULL) {
				(*pcount)++;
			}
			if (pmax_len != NULL) {
				cli__max_len(entry->l, 0, pmax_len);
			}
			if (phas_desc != NULL) {
				(*phas_desc)++;
			}
		} else {
			if (count != NULL) {
				(*count)++;
			}
			if (max_len != NULL) {
				const char *type_str;
				size_t type_len;

				type_str = cli__string_from_type(entry->t);
				type_len = strlen(type_str);

				cli__max_len(entry->l, type_len, max_len);
			}
		}
	}
}

/* Documented in cli.h */
bool cli_parse(const struct cli_table *cli, int argc, const char **argv)
{
	struct cli_ctx ctx = {
		.cli = cli,
	};
	enum {
		ARG_PROG_NAME,
		ARG_FIRST,
	};

	for (int i = ARG_FIRST; i < argc; i++) {
		const char *arg = argv[i];
		bool ret;

		if (arg[0] == '-') {
			if (arg[1] == '-') {
				ret = cli__parse_long(&ctx, argc, argv, &i);
			} else {
				ret = cli__parse_short(&ctx, argc, argv, &i);
			}
		} else {
			ret = cli__parse_positional(&ctx, argv[i]);
		}

		if (ret != true) {
			return ret;
		}
	}

	if (ctx.no_pos == false && ctx.pos_count < cli->min_positional) {
		fprintf(stderr, "Insufficient positional arguments found.\n");
		return false;
	}

	return true;
}

/**
 * Get terminal width.
 *
 * \return terminal width in characters.
 */
static size_t cli__terminal_width(void)
{
	return 80;
}

/**
 * Print a wrapped string, with a given indent.
 *
 * The indent is assumed to already be applied for the first line of the
 * output by the caller.
 *
 * \param[in] str     The string to print.
 * \param[in] indent  The number of spaces to pad the left margin with.
 */
static void cli__print_wrapping_string(const char *str, size_t indent)
{
	size_t terminal_width = cli__terminal_width();
	size_t avail = (indent > terminal_width) ? 0 : terminal_width - indent;
	size_t space = avail;

	while (*str != '\0') {
		size_t word_len = strcspn(str, " \n\t");
		if (word_len <= space || space == avail) {
			fprintf(stderr, "%*.*s",
					(int)word_len,
					(int)word_len, str);
			str += word_len;
			if (word_len <= space) {
				space -= word_len;
			}
			if (space > 0) {
				fprintf(stderr, " ");
				space--;
			}
		} else {
			fprintf(stderr, "\n%*s", (int)indent, "");
			space = avail;
		}
		str += strspn(str, " \n\t");
	}
}

/**
 * Print an entry's description, with a given indent.
 *
 * The indent is assumed to already be applied for the first line of the
 * output by the caller.
 *
 * \param[in] entry   The entry to print the description for.
 * \param[in] indent  The number of spaces to pad the left margin with.
 */
static void cli__print_description(const struct cli_table_entry *entry,
		size_t indent)
{
	if (entry->d != NULL) {
		cli__print_wrapping_string(entry->d, indent);
	}

	fprintf(stderr, "\n");

	if (entry->t == CLI_ENUM) {
		size_t max_len = 0;

		for (const struct cli_str_val *e = entry->v.e.desc;
				e->str != NULL; e++) {
			size_t len = strlen(e->str);
			if (max_len < len) {
				max_len = len;
			}
		}

		fprintf(stderr, "\n");

		for (const struct cli_str_val *e = entry->v.e.desc;
				e->str != NULL; e++) {
			fprintf(stderr, "        ");

			if (e->d == NULL || e->d[0] == '\0') {
				fprintf(stderr, "%s\n",
						e->str);
			} else {
				fprintf(stderr, "%-*s - ",
						(int)(max_len),
						e->str);
				cli__print_wrapping_string(e->d,
						8 + max_len + 3);
				fprintf(stderr, "\n");
			}
		}
	}
}

/* Documented in cli.h */
void cli_help(const struct cli_table *cli, const char *prog_name)
{
	size_t count;
	size_t pcount;
	size_t max_len;
	size_t pmax_len;
	size_t phas_desc;
	size_t required = 0;
	enum {
		ARG_PROG_NAME,
	};

	cli__count(cli, &count, &pcount, &max_len, &pmax_len, &phas_desc);

	if (cli->d != NULL) {
		fprintf(stderr, "\n");
		cli__print_wrapping_string(cli->d, 0);
		fprintf(stderr, "\n");
	}

	fprintf(stderr, "\nUsage: %s", prog_name);

	if (pcount > 0) {
		for (size_t i = 0; i < cli->count; i++) {
			if (cli__entry_is_positional(&cli->entries[i])) {
				const char *punctuation =
					(required == cli->min_positional) ?
					" [" : " ";

				if (cli->entries[i].t == CLI_CMD) {
					fprintf(stderr, "%s%s", punctuation,
							cli->entries[i].l);
				} else {
					fprintf(stderr, "%s<%s>", punctuation,
							cli->entries[i].l);
				}
				required++;
			}
		}
		if (required == pcount && required > cli->min_positional) {
			fprintf(stderr, "]");
		}
	}

	if (count > 0) {
		fprintf(stderr, " [options]");
	}

	fprintf(stderr, "\n\n");

	if (phas_desc > 0) {
		fprintf(stderr, "Where:\n\n");

		for (size_t i = 0; i < cli->count; i++) {
			const struct cli_table_entry *entry = &cli->entries[i];

			if (entry->d == NULL) {
				continue;
			}

			if (cli__entry_is_positional(entry)) {
				fprintf(stderr, "  %*.*s  ",
						(int)pmax_len,
						(int)pmax_len,
						entry->l);
				cli__print_description(entry, pmax_len + 4);
				fprintf(stderr, "\n");
			}
		}
	}

	if (count > 0) {
		fprintf(stderr, "Options:\n\n");

		for (size_t i = 0; i < cli->count; i++) {
			const struct cli_table_entry *entry = &cli->entries[i];
			const char *type_str;
			size_t type_len;
			size_t arg_len;

			if (cli__entry_is_positional(entry)) {
				continue;
			}

			if (entry->s != '\0') {
				fprintf(stderr, "  -%c", entry->s);
			} else {
				fprintf(stderr, "    ");
			}

			type_str = cli__string_from_type(entry->t);
			type_len = strlen(type_str);
			arg_len = strlen(entry->l);

			fprintf(stderr, "  --%s %s%*.s  ", entry->l, type_str,
					(int)(max_len - arg_len - type_len),
					"");
			cli__print_description(entry, max_len + 11);
			fprintf(stderr, "\n");
		}
	}
}
