/* vifm
 * Copyright (C) 2012 xaizek.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 */

#include "variables.h"

#include <assert.h> /* assert() */
#include <ctype.h>
#include <stddef.h> /* NULL size_t */
#include <stdio.h>
#include <stdlib.h> /* free() realloc() */
#include <string.h> /* strcmp() */

#include "../compat/reallocarray.h"
#include "../utils/env.h"
#include "../utils/macros.h"
#include "../utils/str.h"
#include "../utils/string_array.h"
#include "completion.h"
#include "options.h"
#include "parsing.h"
#include "text_buffer.h"
#include "var.h"

#define VAR_NAME_MAX 64
#define VAL_LEN_MAX 2048

/* Types of supported variables. */
typedef enum
{
	VT_ENVVAR,        /* Environment variable. */
	VT_GVAR,          /* Global variable. */
	VT_ANY_OPTION,    /* Global and local options (if local exists). */
	VT_GLOBAL_OPTION, /* Global option. */
	VT_LOCAL_OPTION,  /* Local option. */
}
VariableType;

/* Supported operations. */
typedef enum
{
	VO_ASSIGN, /* Assigning a variable (=). */
	VO_APPEND, /* Appending to a string (.=). */
	VO_ADD,    /* Adding to numbers or composite values (+=). */
	VO_SUB,    /* Subtracting from numbers or removing from composites (-=). */
}
VariableOperation;

/* Description of a variable. */
typedef struct
{
	char *name; /* Name of the variable (including "[gv]:" prefix). */
	var_t val;  /* Current value of the variable. */
}
var_info_t;

typedef struct
{
	char *name;
	char *val;
	char *initial;
	int from_parent;
	int removed;
}
envvar_t;

const char ENV_VAR_NAME_FIRST_CHAR[] = "abcdefghijklmnopqrstuvwxyz"
	"ABCDEFGHIJKLMNOPQRSTUVWXYZ_";
const char ENV_VAR_NAME_CHARS[] = "abcdefghijklmnopqrstuvwxyz"
	"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";

static void init_var(const char name[], const char value[]);
static void clear_internal_vars(void);
static int extract_name(const char **in, VariableType *type, size_t buf_len,
		char buf[]);
static int extract_op(const char **in, VariableOperation *vo);
static int parse_name(const char **in, const char first[], const char other[],
		size_t buf_len, char buf[]);
static int is_valid_op(const char name[], VariableType vt,
		VariableOperation vo);
static int perform_op(const char name[], VariableType vt, VariableOperation vo,
		var_t val, const char value[]);
static void append_envvar(const char *name, const char *val);
static void set_envvar(const char *name, const char *val);
static int append_internal_var(var_info_t *var, var_t val);
static int perform_opt_op(const char name[], VariableType vt,
		VariableOperation vo, const char value[]);
static envvar_t * get_record(const char *name);
static char * skip_non_whitespace(const char str[]);
static envvar_t * find_record(const char *name);
static void free_record(envvar_t *record);
static void clear_record(envvar_t *record);
static void complete_envvars(const char var[], const char **start);
static void complete_internal_vars(const char var[], const char **start);
static var_info_t * find_internal_var(const char varname[], int create);

static int initialized;
static envvar_t *env_vars;
static size_t env_var_count;
/* List of internal variables. */
static var_info_t *internal_vars;
/* Number of internal variables. */
static size_t internal_var_count;

void
init_variables(void)
{
	int env_count;
	char **env_lst;

	if(env_var_count > 0)
	{
		clear_envvars();
	}

	env_lst = env_list(&env_count);
	if(env_count > 0)
	{
		int i;

		/* Allocate memory for environment variables. */
		env_vars = reallocarray(NULL, env_count, sizeof(*env_vars));
		assert(env_vars != NULL && "Failed to allocate memory for env vars.");

		/* Initialize variable list. */
		for(i = 0; i < env_count; ++i)
		{
			init_var(env_lst[i], env_get(env_lst[i]));
		}
	}

	free_string_array(env_lst, env_count);

	vle_parser_init(&local_getenv);

	initialized = 1;
}

const char *
local_getenv(const char envname[])
{
	const char *value = local_getenv_null(envname);
	return (value == NULL ? "" : value);
}

const char *
local_getenv_null(const char envname[])
{
	envvar_t *record = find_record(envname);
	return (record == NULL || record->removed) ? NULL : record->val;
}

/* Initializes environment variable inherited from parent process. */
static void
init_var(const char name[], const char value[])
{
	envvar_t *const record = get_record(name);
	if(record == NULL)
	{
		return;
	}

	record->from_parent = 1;

	(void)replace_string(&record->initial, value);
	(void)replace_string(&record->val, value);
	if(record->initial == NULL || record->val == NULL)
	{
		free_record(record);
	}
}

void
clear_variables(void)
{
	assert(initialized);

	clear_internal_vars();
	clear_envvars();
}

/* Clears the list of internal variables. */
static void
clear_internal_vars(void)
{
	size_t i;
	for(i = 0U; i < internal_var_count; ++i)
	{
		free(internal_vars[i].name);
		var_free(internal_vars[i].val);
	}
	internal_var_count = 0U;
	free(internal_vars);
	internal_vars = NULL;
}

void
clear_envvars(void)
{
	size_t i;
	assert(initialized);

	/* Free memory. */
	for(i = 0U; i < env_var_count; ++i)
	{
		if(env_vars[i].name == NULL)
			continue;

		if(env_vars[i].from_parent)
		{
			set_envvar(env_vars[i].name, env_vars[i].initial);
		}
		else
		{
			env_remove(env_vars[i].name);
		}
		free_record(&env_vars[i]);
	}

	env_var_count = 0;
	free(env_vars);
	env_vars = NULL;
}

int
let_variables(const char cmd[])
{
	char name[VAR_NAME_MAX + 1];
	int error;
	char *str_val;
	VariableType type;
	VariableOperation op;

	assert(initialized);

	if(extract_name(&cmd, &type, sizeof(name), name) != 0)
	{
		return 1;
	}

	cmd = skip_whitespace(cmd);

	if(extract_op(&cmd, &op) != 0)
	{
		return -1;
	}

	cmd = skip_whitespace(cmd);

	parsing_result_t result = vle_parser_eval(cmd, /*interactive=*/1);
	if(result.error != PE_NO_ERROR)
	{
		vle_parser_report(&result);
		goto fail;
	}

	if(result.last_position != NULL && result.last_position[0] != '\0')
	{
		vle_tb_append_linef(vle_err, "%s: %s", "Incorrect :let statement",
				"trailing characters");
		goto fail;
	}

	if(!is_valid_op(name, type, op))
	{
		vle_tb_append_line(vle_err, "Wrong variable type for this operation");
		goto fail;
	}

	str_val = var_to_str(result.value);

	error = perform_op(name, type, op, result.value, str_val);
	free(str_val);

	if(error)
	{
		vle_tb_append_line(vle_err, "Failed to perform an operation");
		goto fail;
	}

	var_free(result.value);
	return error;

fail:
	var_free(result.value);
	return -1;
}

/* Extracts name from the string.  Returns zero on success, otherwise non-zero
 * is returned. */
static int
extract_name(const char **in, VariableType *type, size_t buf_len, char buf[])
{
	int error;

	/* Copy variable name. */
	if(**in == '$')
	{
		++*in;
		error = (parse_name(in, ENV_VAR_NAME_FIRST_CHAR, ENV_VAR_NAME_CHARS,
				buf_len, buf) != 0);
		*type = VT_ENVVAR;
	}
	else if((*in)[0] == 'g' && (*in)[1] == ':')
	{
		*in += 2;
		copy_str(buf, buf_len, "g:");
		error = (parse_name(in, ENV_VAR_NAME_FIRST_CHAR, ENV_VAR_NAME_CHARS,
				buf_len - 2, buf + 2) != 0);
		*type = VT_GVAR;
	}
	else if(**in == '&')
	{
		++*in;

		*type = VT_ANY_OPTION;
		if(((*in)[0] == 'l' || (*in)[0] == 'g') && (*in)[1] == ':')
		{
			*type = (*in[0] == 'l') ? VT_LOCAL_OPTION : VT_GLOBAL_OPTION;
			*in += 2;
		}

		error = (parse_name(in, OPT_NAME_FIRST_CHAR, OPT_NAME_CHARS, buf_len,
					buf) != 0);
	}
	else
	{
		/* Currently we support only environment variables and options. */
		vle_tb_append_line(vle_err, "Incorrect variable type");
		return -1;
	}
	if(error)
	{
		vle_tb_append_line(vle_err, "Incorrect variable name");
		return -1;
	}

	/* Test for empty variable name. */
	if(buf[0] == '\0')
	{
		vle_tb_append_linef(vle_err, "%s: %s", "Unsupported variable name",
				"empty name");
		return -1;
	}

	return 0;
}

/* Extracts operation from the string.  Sets operation advancing the
 * pointer if needed.  Returns zero on success, otherwise non-zero returned. */
static int
extract_op(const char **in, VariableOperation *vo)
{
	/* Check first operator char and skip it. */
	if(**in == '.')
	{
		++*in;
		*vo = VO_APPEND;
	}
	else if(**in == '+')
	{
		*vo = VO_ADD;
		++*in;
	}
	else if(**in == '-')
	{
		*vo = VO_SUB;
		++*in;
	}
	else
	{
		*vo = VO_ASSIGN;
	}

	/* Check for equal sign and skip it. */
	if(**in != '=')
	{
		vle_tb_append_linef(vle_err, "%s: '=' expected at %s",
				"Incorrect :let statement", *in);
		return 1;
	}
	++*in;

	return 0;
}

/* Parses name of the form `first { other }`.  Returns zero on success,
 * otherwise non-zero is returned. */
static int
parse_name(const char **in, const char first[], const char other[],
		size_t buf_len, char buf[])
{
	if(buf_len == 0UL || !char_is_one_of(first, **in))
	{
		return 1;
	}

	buf[0] = '\0';

	do
	{
		strcatch(buf, **in);
		++*in;
	}
	while(--buf_len > 1UL && char_is_one_of(other, **in));

	return 0;
}

/* Validates operation on a specific variable type.  Returns non-zero for valid
 * operation, otherwise zero is returned. */
static int
is_valid_op(const char name[], VariableType vt, VariableOperation vo)
{
	opt_t *opt;

	if(vt == VT_ENVVAR)
	{
		return (vo == VO_ASSIGN || vo == VO_APPEND);
	}

	if(vt == VT_GVAR)
	{
		var_info_t *var = find_internal_var(name, /*create=*/0);
		if(var == NULL)
		{
			/* Let this error be handled somewhere else. */
			return (vo == VO_ASSIGN);
		}

		switch(var->val.type)
		{
			case VTYPE_ERROR:
				/* Unreachable. */
				return 0;
			case VTYPE_STRING:
				return ONE_OF(vo, VO_ASSIGN, VO_APPEND);
			case VTYPE_INT:
				return ONE_OF(vo, VO_ASSIGN, VO_ADD, VO_SUB);
		}
		/* Unreachable. */
		return 0;
	}

	opt = vle_opts_find(name, OPT_GLOBAL);
	if(opt == NULL)
	{
		/* Let this error be handled somewhere else. */
		return 1;
	}

	if(opt->type == OPT_BOOL)
	{
		return 0;
	}

	if(opt->type == OPT_STR)
	{
		return (vo == VO_ASSIGN || vo == VO_APPEND);
	}

	return (vo == VO_ASSIGN || vo == VO_ADD || vo == VO_SUB);
}

/* Performs operation on a value.  Returns zero on success, otherwise non-zero
 * is returned. */
static int
perform_op(const char name[], VariableType vt, VariableOperation vo, var_t val,
		const char value[])
{
	if(vt == VT_ENVVAR)
	{
		/* Update environment variable. */
		if(vo == VO_APPEND)
		{
			append_envvar(name, value);
		}
		else
		{
			set_envvar(name, value);
		}
		return 0;
	}

	if(vt == VT_GVAR)
	{
		var_info_t *var = find_internal_var(name, vo == VO_ASSIGN);
		if(var == NULL)
		{
			/* Must have failed to allocate new variable. */
			return 0;
		}

		switch(vo)
		{
			int a, b;

			case VO_ASSIGN:
				var_free(var->val);
				var->val = var_clone(val);
				return 0;
			case VO_ADD:
			case VO_SUB:
				a = var_to_int(var->val);
				b = var_to_int(val);
				var_free(var->val);
				var->val = var_from_int(vo == VO_ADD ? a + b : a - b);
				return 0;
			case VO_APPEND:
				return append_internal_var(var, val);
		}
		/* Unreachable. */
		return 1;
	}

	/* Update an option. */

	if(vt == VT_ANY_OPTION || vt == VT_LOCAL_OPTION)
	{
		if(perform_opt_op(name, vt, vo, value) != 0)
		{
			return 1;
		}
	}

	if(vt == VT_ANY_OPTION || vt == VT_GLOBAL_OPTION)
	{
		if(perform_opt_op(name, VT_GLOBAL_OPTION, vo, value) != 0)
		{
			return 1;
		}
	}

	return 0;
}

static void
append_envvar(const char *name, const char *val)
{
	envvar_t *record;
	char *p;

	record = find_record(name);
	if(record == NULL)
	{
		set_envvar(name, val);
		return;
	}

	p = realloc(record->val, strlen(record->val) + strlen(val) + 1);
	if(p == NULL)
	{
		vle_tb_append_line(vle_err, "Not enough memory");
		return;
	}
	record->val = p;

	strcat(record->val, val);
	env_set(name, record->val);
}

static void
set_envvar(const char *name, const char *val)
{
	envvar_t *record;
	char *p;

	record = get_record(name);
	if(record == NULL)
	{
		vle_tb_append_line(vle_err, "Not enough memory");
		return;
	}

	p = strdup(val);
	if(p == NULL)
	{
		vle_tb_append_line(vle_err, "Not enough memory");
		return;
	}
	free(record->val);
	record->val = p;
	env_set(name, val);
}

/* Appends two variables as strings, updating LHS in place.  Returns zero on
 * success. */
static int
append_internal_var(var_info_t *var, var_t val)
{
	char *current = var_to_str(var->val);
	char *suffix = var_to_str(val);

	if(current == NULL || suffix == NULL)
	{
		goto oom;
	}

	char *final = format_str("%s%s", current, suffix);
	if(final == NULL)
	{
		goto oom;
	}

	var_free(var->val);
	var->val = var_out_of_str(final);

	free(current);
	free(suffix);
	return 0;

oom:
	vle_tb_append_line(vle_err, "Not enough memory");
	free(current);
	free(suffix);
	return 1;
}

/* Performs operation on an option.  Returns zero on success, otherwise non-zero
 * is returned. */
static int
perform_opt_op(const char name[], VariableType vt, VariableOperation vo,
		const char value[])
{
	OPT_SCOPE scope = (vt == VT_ANY_OPTION || vt == VT_LOCAL_OPTION)
	                ? OPT_LOCAL
	                : OPT_GLOBAL;

	opt_t *const opt = vle_opts_find(name, scope);
	if(opt == NULL)
	{
		if(vt == VT_ANY_OPTION)
		{
			return 0;
		}

		vle_tb_append_linef(vle_err, "Unknown %s option name: %s",
				(scope == OPT_LOCAL) ? "local" : "global", name);
		return 1;
	}

	switch(vo)
	{
		case VO_ASSIGN:
			if(vle_opt_assign(opt, value) != 0)
			{
				return 1;
			}
			break;
		case VO_ADD:
		case VO_APPEND:
			if(vle_opt_add(opt, value) != 0)
			{
				return 1;
			}
			break;
		case VO_SUB:
			if(vle_opt_remove(opt, value) != 0)
			{
				return 1;
			}
			break;
	}
	return 0;
}

/* Searches for variable and creates new record if it doesn't exist yet.
 * Returns a record or NULL on error.*/
static envvar_t *
get_record(const char *name)
{
	envvar_t *p = NULL;
	size_t i;

	/* Search for existing variable. */
	for(i = 0U; i < env_var_count; ++i)
	{
		if(env_vars[i].name == NULL)
			p = &env_vars[i];
		else if(strcmp(env_vars[i].name, name) == 0)
			return &env_vars[i];
	}

	if(p == NULL)
	{
		/* Try to reallocate list of variables. */
		p = realloc(env_vars, sizeof(*env_vars)*(env_var_count + 1U));
		if(p == NULL)
			return NULL;
		env_vars = p;
		p = &env_vars[env_var_count];
		++env_var_count;
	}

	/* Initialize new record. */
	p->initial = strdup("");
	p->name = strdup(name);
	p->val = strdup("");
	p->from_parent = 0;
	p->removed = 0;
	if(p->initial == NULL || p->name == NULL || p->val == NULL)
	{
		free_record(p);
		return NULL;
	}
	return p;
}

int
unlet_variables(const char cmd[])
{
	int error = 0;
	assert(initialized);

	while(*cmd != '\0')
	{
		/* Options can't be removed, use this to mean "no value". */
		VariableType type = VT_ANY_OPTION;

		char name[VAR_NAME_MAX + 1];
		char *p = name;

		/* Check if it's environment variable. */
		if(*cmd == '$')
		{
			type = VT_ENVVAR;
			cmd++;
		}
		else if(starts_with_lit(cmd, "g:"))
		{
			type = VT_GVAR;
			*p++ = *cmd++;
			*p++ = *cmd++;
		}
		else if(starts_with_lit(cmd, "v:"))
		{
			/* Parse the name, but don't set the type to get a sensible error message
			 * about unsupported variable type. */
			*p++ = *cmd++;
			*p++ = *cmd++;
		}

		/* Copy variable name. */
		while(*cmd != '\0' && char_is_one_of(ENV_VAR_NAME_CHARS, *cmd) &&
				(size_t)(p - name) < sizeof(name) - 1)
		{
			*p++ = *cmd++;
		}
		*p = '\0';

		if(*cmd != '\0' && !isspace(*cmd))
		{
			vle_tb_append_line(vle_err, "Trailing characters");
			error++;
			break;
		}

		cmd = skip_whitespace(cmd);

		if(type == VT_ANY_OPTION)
		{
			vle_tb_append_linef(vle_err, "%s: %s", "Unsupported variable type", name);

			cmd = skip_non_whitespace(cmd);
			error++;
			continue;
		}

		/* Test for empty variable name. */
		if(name[0] == '\0')
		{
			vle_tb_append_linef(vle_err, "%s: %s", "Unsupported variable name",
					"empty name");
			error++;
			continue;
		}

		if(type == VT_ENVVAR)
		{
			envvar_t *record = find_record(name);
			if(record == NULL || record->removed)
			{
				vle_tb_append_linef(vle_err, "%s: %s", "No such variable", name);
				error++;
				continue;
			}

			if(record->from_parent)
				record->removed = 1;
			else
				free_record(record);
			env_remove(name);
		}
		else if(type == VT_GVAR)
		{
			var_info_t *var = find_internal_var(name, /*create=*/0);
			if(var == NULL)
			{
				vle_tb_append_linef(vle_err, "%s: %s", "No such variable", name);
				error++;
				continue;
			}

			free(var->name);
			var_free(var->val);
			*var = internal_vars[internal_var_count - 1];
			--internal_var_count;
		}
	}

	return error;
}

/* Skips consecutive non-whitespace characters.  Returns pointer to the next
 * character in the str. */
static char *
skip_non_whitespace(const char str[])
{
	while(!isspace(*str) && *str != '\0')
	{
		++str;
	}
	return (char *)str;
}

/* searches for existent variable */
static envvar_t *
find_record(const char *name)
{
	size_t i;
	for(i = 0U; i < env_var_count; ++i)
	{
		if(env_vars[i].name != NULL && stroscmp(env_vars[i].name, name) == 0)
			return &env_vars[i];
	}
	return NULL;
}

static void
free_record(envvar_t *record)
{
	free(record->initial);
	free(record->name);
	free(record->val);

	clear_record(record);
}

static void
clear_record(envvar_t *record)
{
	record->initial = NULL;
	record->name = NULL;
	record->val = NULL;
}

void
complete_variables(const char var[], const char **start)
{
	assert(initialized && "Variables unit is not initialized.");

	if(var[0] == '$')
	{
		complete_envvars(var, start);
	}
	else if(ONE_OF(var[0], 'v', 'g') && var[1] == ':')
	{
		complete_internal_vars(var, start);
	}
	else
	{
		*start = var;
		vle_compl_add_match(var, "");
	}
}

/* Completes environment variable name.  var should point to "$...".  *start is
 * set to completion insertion position in var. */
static void
complete_envvars(const char var[], const char **start)
{
	size_t i;
	size_t len;

	++var;
	*start = var;

	/* Add all variables that start with given beginning. */
	len = strlen(var);
	for(i = 0U; i < env_var_count; ++i)
	{
		if(env_vars[i].name == NULL)
			continue;
		if(env_vars[i].removed)
			continue;
		if(strnoscmp(env_vars[i].name, var, len) == 0)
			vle_compl_add_match(env_vars[i].name, env_vars[i].val);
	}
	vle_compl_finish_group();
	vle_compl_add_last_match(var);
}

/* Completes a variable name.  var should point to "[gv]:...".  *start is set to
 * completion insertion position in var. */
static void
complete_internal_vars(const char var[], const char **start)
{
	size_t i;
	size_t len;

	*start = var;

	/* Add all variables that start with given beginning. */
	len = strlen(var);
	for(i = 0U; i < internal_var_count; ++i)
	{
		if(strncmp(internal_vars[i].name, var, len) == 0)
		{
			char *const str_val = var_to_str(internal_vars[i].val);
			vle_compl_add_match(internal_vars[i].name, str_val);
			free(str_val);
		}
	}
	vle_compl_finish_group();
	vle_compl_add_last_match(var);
}

var_t
getvar(const char varname[])
{
	var_info_t *var = find_internal_var(varname, /*create=*/0);
	if(var == NULL)
	{
		return var_error();
	}

	return var->val;
}

int
setvar(const char varname[], var_t val)
{
	var_info_t new_var;
	void *p;

	if(!starts_with_lit(varname, "v:"))
	{
		return 1;
	}

	/* Initialize new variable before doing anything else. */
	new_var.name = strdup(varname);
	new_var.val = var_clone(val);
	if(new_var.name == NULL || new_var.val.type == VTYPE_ERROR)
	{
		free(new_var.name);
		var_free(new_var.val);
		return 1;
	}

	/* Try updating an existing variable. */
	var_info_t *var = find_internal_var(varname, /*create=*/0);
	if(var != NULL)
	{
		free(var->name);
		var_free(var->val);
		*var = new_var;
		return 0;
	}

	/* Try to reallocate list of variables. */
	p = realloc(internal_vars, sizeof(*internal_vars)*(internal_var_count + 1U));
	if(p == NULL)
	{
		free(new_var.name);
		var_free(new_var.val);
		return 1;
	}
	internal_vars = p;
	internal_vars[internal_var_count] = new_var;
	++internal_var_count;

	return 0;
}

/* Search for an existing variable by its name and optionally create it if
 * missing.  Returns NULL on unsuccessful search. */
static var_info_t *
find_internal_var(const char varname[], int create)
{
	size_t i;
	for(i = 0U; i < internal_var_count; ++i)
	{
		if(strcmp(internal_vars[i].name, varname) == 0)
		{
			return &internal_vars[i];
		}
	}

	if(!create)
	{
		return NULL;
	}

	void *p =
		realloc(internal_vars, sizeof(*internal_vars)*(internal_var_count + 1U));
	if(p == NULL)
	{
		return NULL;
	}

	internal_vars = p;

	internal_vars[internal_var_count].name = strdup(varname);
	if(internal_vars[internal_var_count].name == NULL)
	{
		return NULL;
	}

	internal_vars[internal_var_count].val = var_error();
	return &internal_vars[internal_var_count++];
}

/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
/* vim: set cinoptions+=t0 filetype=c : */
