#include "test-utils.h"

#include <stic.h>

#include <sys/stat.h> /* chmod() */
#include <sys/time.h> /* timeval utimes() */
#include <unistd.h> /* access() geteuid() rmdir() symlink() usleep() */

#ifdef _WIN32
#include <windows.h>
#endif

#include <locale.h> /* LC_ALL setlocale() */
#include <stddef.h> /* NULL */
#include <stdio.h> /* FILE fclose() fopen() fread() remove() */
#include <stdlib.h> /* free() malloc() */
#include <string.h> /* memset() strcpy() strdup() */

#include "../../src/cfg/config.h"
#include "../../src/compat/os.h"
#include "../../src/compat/pthread.h"
#include "../../src/engine/cmds.h"
#include "../../src/engine/options.h"
#include "../../src/engine/var.h"
#include "../../src/engine/variables.h"
#include "../../src/ui/color_manager.h"
#include "../../src/ui/column_view.h"
#include "../../src/ui/ui.h"
#include "../../src/utils/dynarray.h"
#include "../../src/utils/env.h"
#include "../../src/utils/fs.h"
#include "../../src/utils/macros.h"
#include "../../src/utils/matcher.h"
#include "../../src/utils/path.h"
#include "../../src/utils/regexp.h"
#include "../../src/utils/str.h"
#include "../../src/utils/string_array.h"
#include "../../src/utils/utils.h"
#include "../../src/background.h"
#include "../../src/cmd_completion.h"
#include "../../src/filelist.h"
#include "../../src/filtering.h"
#include "../../src/opt_handlers.h"
#include "../../src/plugins.h"
#include "../../src/status.h"
#include "../../src/undo.h"

static OpsResult exec_func(OPS op, void *data, const char src[],
		const char dst[]);
static int op_avail(OPS op);
static void format_none(void *data, size_t buf_len, char buf[],
		const format_info_t *info);
static int complete_line_stub(const char cmd_line[], void *extra_arg);
static int complete_args_stub(int id, const cmd_info_t *cmd_info, int arg_pos,
		void *extra_arg);
static int swap_range(void);
static int resolve_mark(char mark);
static char * expand_macros(const char str[], int for_shell, int *usr1,
		int *usr2);
static char * cmds_expand_envvars(const char *str);
static void post(int id);
static void select_range(int id, const cmd_info_t *cmd_info);
static int skip_at_beginning(int id, const char *args);
static void init_list(view_t *view);
static int init_pair_stub(int pair, int f, int b);
static int pair_content_stub(int pair, int *f, int *b);
static int pair_in_use_stub(int pair);
static void pair_moved_stub(int from, int to);

void
fix_environ(void)
{
#ifdef _WIN32
	extern int _CRT_glob;
	extern void __wgetmainargs(int *, wchar_t ***, wchar_t ***, int, int *);

	wchar_t **envp, **argv;
	int argc, si = 0;
	__wgetmainargs(&argc, &argv, &envp, _CRT_glob, &si);
#endif
}

void
conf_setup(void)
{
	update_string(&cfg.slow_fs_list, "");
	update_string(&cfg.apropos_prg, "");
	update_string(&cfg.cd_path, "");
	update_string(&cfg.find_prg, "");
	update_string(&cfg.fuse_home, "");
	update_string(&cfg.time_format, "+");
	update_string(&cfg.vi_command, "");
	update_string(&cfg.vi_x_command, "");
	update_string(&cfg.ruler_format, "");
	update_string(&cfg.status_line, "");
	update_string(&cfg.tab_line, "");
	update_string(&cfg.grep_prg, "");
	update_string(&cfg.locate_prg, "");
	update_string(&cfg.media_prg, "");
	update_string(&cfg.vborder_filler, "");
	update_string(&cfg.hborder_filler, "");
	update_string(&cfg.millersep_filler, "");
	update_string(&cfg.tab_prefix, "");
	update_string(&cfg.tab_label, "");
	update_string(&cfg.tab_suffix, "");
	update_string(&cfg.delete_prg, "");

	cfg.sizefmt.base = 1;
	cfg.sizefmt.precision = 0;
	cfg.sizefmt.ieci_prefixes = 0;
	cfg.sizefmt.space = 0;

	cfg.tab_stop = 4;

#ifndef _WIN32
	replace_string(&cfg.shell, "/bin/sh");
	update_string(&cfg.shell_cmd_flag, "-c");
#else
	replace_string(&cfg.shell, "cmd");
	update_string(&cfg.shell_cmd_flag, "/C");
#endif
	stats_update_shell_type(cfg.shell);

	cfg.dot_dirs = DD_TREE_LEAFS_PARENT;

	if(cfg.wild_inc != NULL)
	{
		matcher_free(cfg.wild_inc);
	}

	char *error;
	cfg.wild_inc = matcher_alloc_glob("", &error);
	free(error);
}

void
conf_teardown(void)
{
	update_string(&cfg.slow_fs_list, NULL);
	update_string(&cfg.apropos_prg, NULL);
	update_string(&cfg.cd_path, NULL);
	update_string(&cfg.find_prg, NULL);
	update_string(&cfg.fuse_home, NULL);
	update_string(&cfg.time_format, NULL);
	update_string(&cfg.vi_command, NULL);
	update_string(&cfg.vi_x_command, NULL);
	update_string(&cfg.ruler_format, NULL);
	update_string(&cfg.status_line, NULL);
	update_string(&cfg.tab_line, NULL);
	update_string(&cfg.grep_prg, NULL);
	update_string(&cfg.locate_prg, NULL);
	update_string(&cfg.media_prg, NULL);
	update_string(&cfg.vborder_filler, NULL);
	update_string(&cfg.hborder_filler, NULL);
	update_string(&cfg.millersep_filler, NULL);
	update_string(&cfg.tab_prefix, NULL);
	update_string(&cfg.tab_label, NULL);
	update_string(&cfg.tab_suffix, NULL);
	update_string(&cfg.delete_prg, NULL);
	update_string(&cfg.shell, NULL);
	update_string(&cfg.shell_cmd_flag, NULL);

	cfg.dot_dirs = 0;

	cfg.sizefmt.base = 0;
	cfg.sizefmt.precision = 0;
	cfg.sizefmt.ieci_prefixes = 0;
	cfg.sizefmt.space = 0;

	matcher_free(cfg.wild_inc);
	cfg.wild_inc = NULL;
}

void
opt_handlers_setup(void)
{
	update_string(&lwin.view_columns, "");
	update_string(&lwin.view_columns_g, "");
	update_string(&lwin.sort_groups, "");
	update_string(&lwin.sort_groups_g, "");
	update_string(&lwin.preview_prg, "");
	update_string(&lwin.preview_prg_g, "");
	update_string(&rwin.view_columns, "");
	update_string(&rwin.view_columns_g, "");
	update_string(&rwin.sort_groups, "");
	update_string(&rwin.sort_groups_g, "");
	update_string(&rwin.preview_prg, "");
	update_string(&rwin.preview_prg_g, "");

	conf_setup();

	init_option_handlers();
}

void
opt_handlers_teardown(void)
{
	vle_opts_reset();

	conf_teardown();

	update_string(&lwin.view_columns, NULL);
	update_string(&lwin.view_columns_g, NULL);
	update_string(&lwin.sort_groups, NULL);
	update_string(&lwin.sort_groups_g, NULL);
	update_string(&lwin.preview_prg, NULL);
	update_string(&lwin.preview_prg_g, NULL);
	update_string(&rwin.view_columns, NULL);
	update_string(&rwin.view_columns_g, NULL);
	update_string(&rwin.sort_groups, NULL);
	update_string(&rwin.sort_groups_g, NULL);
	update_string(&rwin.preview_prg, NULL);
	update_string(&rwin.preview_prg_g, NULL);
}

void
undo_setup(void)
{
	static int max_undo_levels = 0;
	un_init(&exec_func, &op_avail, NULL, &max_undo_levels);
}

static OpsResult
exec_func(OPS op, void *data, const char *src, const char *dst)
{
	return OPS_SUCCEEDED;
}

static int
op_avail(OPS op)
{
	return 0;
}

void
undo_teardown(void)
{
	un_reset();
}

void
view_setup(view_t *view)
{
	char *error;

	view->list_rows = 0;
	view->filtered = 0;
	view->list_pos = 0;
	view->dir_entry = NULL;
	view->hide_dot = 0;
	view->hide_dot_g = 0;
	view->invert = 1;
	view->selected_files = 0;
	view->ls_view = 0;
	view->ls_view_g = 0;
	view->miller_view = 0;
	view->miller_view_g = 0;
	view->window_rows = 0;
	view->run_size = 1;
	view->pending_marking = 0;

	assert_success(filter_init(&view->local_filter.filter, 1));
	assert_non_null(view->manual_filter = matcher_alloc("", 0, 0, "", &error));
	assert_success(filter_init(&view->auto_filter, 1));

	strcpy(view->curr_dir, "/path");
	update_string(&view->custom.orig_dir, NULL);

	view_set_sort(view->sort, SK_BY_NAME, SK_NONE);
	view_set_sort(view->sort_g, SK_BY_NAME, SK_NONE);

	/* The code assumes that this field is initialized.  At OS X and other
	 * BSD-like refuse to compile empty regular expression. */
	assert_success(regexp_compile(&view->primary_group, ".*", REG_ICASE));
	view->primary_group_set = 1;

	view->custom.entry_count = 0;
	view->custom.entries = NULL;

	view->custom.full.nentries = 0;
	view->custom.full.entries = NULL;

	view->timestamps_mutex = malloc(sizeof(*view->timestamps_mutex));
	pthread_mutex_init(view->timestamps_mutex, NULL);
}

void
view_teardown(view_t *view)
{
	flist_free_view(view);
	view->last_search[0] = '\0';
}

void
view_set_sort(signed char sort_keys[], signed char primary_key,
		signed char secondary_key)
{
	memset(sort_keys, SK_NONE, SK_COUNT);
	sort_keys[0] = primary_key;
	sort_keys[1] = secondary_key;
}

void
columns_setup_column(int id)
{
	columns_add_column_desc(id, &format_none, NULL);
}

static void
format_none(void *data, size_t buf_len, char buf[], const format_info_t *info)
{
	buf[0] = '\0';
}

void
columns_teardown(void)
{
	columns_clear_column_descs();
	columns_set_line_print_func(NULL);
	columns_set_line_match_func(NULL);
}

void
engine_cmds_setup(int real_completion)
{
	static cmds_conf_t cmds_conf = {
		.complete_line = &complete_line_stub,
		.swap_range = &swap_range,
		.resolve_mark = &resolve_mark,
		.expand_macros = &expand_macros,
		.expand_envvars = &cmds_expand_envvars,
		.post = &post,
		.select_range = &select_range,
		.skip_at_beginning = &skip_at_beginning,
	};

	cmds_conf.complete_args = real_completion ? &complete_args
	                                          : &complete_args_stub;
	vle_cmds_init(1, &cmds_conf);
}

static int
complete_line_stub(const char cmd_line[], void *extra_arg)
{
	return 0;
}

static int
complete_args_stub(int id, const cmd_info_t *cmd_info, int arg_pos,
		void *extra_arg)
{
	return 0;
}

static int
swap_range(void)
{
	return 1;
}

static int
resolve_mark(char mark)
{
	return -1;
}

static char *
expand_macros(const char str[], int for_shell, int *usr1, int *usr2)
{
	return strdup(str);
}

static char *
cmds_expand_envvars(const char *str)
{
	return strdup(str);
}

static void
post(int id)
{
}

static void
select_range(int id, const cmd_info_t *cmd_info)
{
}

static int
skip_at_beginning(int id, const char *args)
{
	return -1;
}

void
engine_cmds_teardown(void)
{
	vle_cmds_reset();
}

void
histories_init(int size)
{
	cfg_resize_histories(0);
	cfg_resize_histories(size);
}

void
create_dir(const char path[])
{
	assert_success(os_mkdir(path, 0700));
	assert_true(is_dir(path));
}

void
create_file(const char path[])
{
	FILE *const f = fopen(path, "w");
	assert_non_null(f);
	if(f != NULL)
	{
		fclose(f);
	}
}

void
create_executable(const char path[])
{
	create_file(path);
	assert_success(access(path, F_OK));
	chmod(path, 0755);
	assert_success(access(path, X_OK));
}

void
make_file(const char path[], const char contents[])
{
	FILE *fp = fopen(path, "wb");
	assert_non_null(fp);

	if(fp != NULL)
	{
		fputs(contents, fp);
		fclose(fp);
	}
}

int
make_symlink(const char target[], const char linkpath[])
{
#ifndef _WIN32
	return symlink(target, linkpath);
#else
	assert_fail("No creation of symbolic links on Windows.");
	return 1;
#endif
}

void
remove_dir(const char path[])
{
	assert_success(rmdir(path));
}

void
no_remove_dir(const char path[])
{
	assert_failure(rmdir(path));
}

void
remove_file(const char path[])
{
	assert_success(remove(path));
}

void
no_remove_file(const char path[])
{
	assert_failure(remove(path));
}

void
make_abs_path(char buf[], size_t buf_len, const char base[], const char sub[],
		const char cwd[])
{
	char local_buf[buf_len];

	if(is_path_absolute(base))
	{
		snprintf(local_buf, buf_len, "%s%s%s", base, (sub[0] == '\0' ? "" : "/"),
				sub);
	}
	else
	{
		char cwd_buf[PATH_MAX + 1];
		if(cwd == NULL)
		{
			assert_non_null(get_cwd(cwd_buf, sizeof(cwd_buf)));
			cwd = cwd_buf;
		}
		snprintf(local_buf, buf_len, "%s/%s%s%s", cwd, base,
				(sub[0] == '\0' ? "" : "/"), sub);
	}

	canonicalize_path(local_buf, buf, buf_len);
	if(!ends_with_slash(sub) && !is_root_dir(buf))
	{
		chosp(buf);
	}
}

void
copy_file(const char src[], const char dst[])
{
	char buf[4*1024];
	size_t nread;
	FILE *const in = os_fopen(src, "rb");
	FILE *const out = os_fopen(dst, "wb");

	assert_non_null(in);
	assert_non_null(out);

	while((nread = fread(&buf, 1, sizeof(buf), in)) != 0U)
	{
		assert_int_equal(nread, fwrite(&buf, 1, nread, out));
	}

	fclose(out);
	fclose(in);
}

int
windows(void)
{
#ifdef _WIN32
	return 1;
#else
	return 0;
#endif
}

int
not_windows(void)
{
#ifdef _WIN32
	return 0;
#else
	return 1;
#endif
}

int
not_wine(void)
{
#ifndef _WIN32
	return 1;
#else
	const HMODULE dll = GetModuleHandle("ntdll.dll");
	if(dll == INVALID_HANDLE_VALUE)
	{
		return 0;
	}

	/* Newer WINE has some issues with spawning second instance. */
	return (GetProcAddress(dll, "wine_get_version") == NULL);
#endif
}

int
regular_unix_user(void)
{
#ifdef _WIN32
	return 0;
#else
	return (geteuid() != 0);
#endif
}

int
have_cat(void)
{
	return (find_cmd_in_path("cat", 0, NULL) == 0);
}

void
try_enable_utf8_locale(void)
{
	(void)setlocale(LC_ALL, "");
	if(!utf8_locale())
	{
		(void)setlocale(LC_ALL, "en_US.utf8");
	}
}

int
utf8_locale(void)
{
	return (vifm_wcwidth(L'丝') == 2);
}

int
replace_matcher(matcher_t **matcher, const char expr[])
{
	char *error;

	matcher_free(*matcher);
	*matcher = matcher_alloc(expr, FILTER_DEF_CASE_SENSITIVITY, 0, "", &error);
	free(error);

	return (*matcher == NULL);
}

void
setup_grid(view_t *view, int column_count, int list_rows, int init)
{
	view->window_cols = column_count;
	view->ls_view = 1;
	view->miller_view = 0;
	view->ls_transposed = 0;
	view->list_rows = list_rows;
	view->column_count = column_count;
	view->run_size = column_count;
	view->window_cells = column_count*view->window_rows;

	if(init)
	{
		init_list(view);
	}
}

void
setup_transposed_grid(view_t *view, int column_count, int list_rows, int init)
{
	view->window_cols = column_count;
	view->ls_view = 1;
	view->miller_view = 0;
	view->ls_transposed = 1;
	view->list_rows = list_rows;
	view->column_count = column_count;
	view->run_size = view->window_rows;
	view->window_cells = column_count*view->window_rows;

	if(init)
	{
		init_list(view);
	}
}

void
init_view_list(view_t *view)
{
	view->list_rows = 1;
	init_list(view);
}

static void
init_list(view_t *view)
{
	int i;

	view->dir_entry = dynarray_cextend(NULL,
			view->list_rows*sizeof(*view->dir_entry));

	for(i = 0; i < view->list_rows; ++i)
	{
		view->dir_entry[i].name = strdup("");
		view->dir_entry[i].type = FT_REG;
		view->dir_entry[i].origin = view->curr_dir;
	}
}

void
check_compare_invariants(int expected_len)
{
	assert_true(flist_custom_active(&lwin));
	assert_true(flist_custom_active(&rwin));

	assert_true(cv_compare(lwin.custom.type));
	assert_true(cv_compare(rwin.custom.type));

	assert_int_equal(expected_len, lwin.list_rows);
	assert_int_equal(expected_len, rwin.list_rows);

	int i;
	for(i = 0; i < expected_len; ++i)
	{
		assert_int_equal(lwin.dir_entry[i].id, rwin.dir_entry[i].id);
	}
}

void
check_bg_jobs(void)
{
	/* Won't hurt to exercise code for displaying errors which depends on thread
	 * synchronization. */
	bg_check(/*show_errors=*/1);
}

void
wait_for_bg(void)
{
	int counter = 0;
	while(bg_has_active_jobs(0))
	{
		usleep(5000);
		if(++counter > 100)
		{
			assert_fail("Waiting for too long.");
			break;
		}
	}
}

void
wait_for_all_bg(void)
{
	int counter = 0;

	var_t var = var_from_int(-1);
	setvar("v:jobcount", var);
	var_free(var);

	check_bg_jobs();
	while(bg_jobs != NULL)
	{
		if(++counter > 100)
		{
			assert_fail("Waiting for too long.");
			break;
		}

		usleep(5000);
		check_bg_jobs();
	}
}

int
wait_for_job(struct bg_job_t *job)
{
	assert_non_null(job);

	bg_job_incref(job);

	int counter = 0;
	while(bg_job_is_running(job))
	{
		usleep(5000);
		check_bg_jobs();
		if(++counter > 100)
		{
			assert_fail("Waiting for too long.");
			return -1;
		}
	}

	/* When the job is marked as not running, the callback might not yet been
	 * dispatched, so call check_bg_jobs() once again to be sure. */
	check_bg_jobs();

	int exit_code = job->exit_code;
	bg_job_decref(job);

	return exit_code;
}

void
file_is(const char path[], const char *lines[], int nlines)
{
	FILE *fp = fopen(path, "r");
	if(fp == NULL)
	{
		assert_non_null(fp);
		return;
	}

	int actual_nlines;
	char **actual_lines = read_stream_lines(fp, &actual_nlines,
			/*null_sep_heuristic=*/0, NULL, NULL);
	fclose(fp);

	assert_int_equal(nlines, actual_nlines);

	int i;
	for(i = 0; i < MIN(nlines, actual_nlines); ++i)
	{
		assert_string_equal(lines[i], actual_lines[i]);
	}

	free_string_array(actual_lines, actual_nlines);
}

char *
mock_env(const char env[], const char with[])
{
	char *value = NULL;
	update_string(&value, env_get("TMPDIR"));
	env_set("TMPDIR", with);
	return value;
}

void
unmock_env(const char env[], char old_value[])
{
	if(old_value != NULL)
	{
		env_set("TMPDIR", old_value);
	}
	else
	{
		env_remove("TMPDIR");
	}
	free(old_value);
}

void
stub_colmgr(void)
{
	const colmgr_conf_t colmgr_conf = {
		.max_color_pairs = 256,
		.max_colors = 16,
		.init_pair = &init_pair_stub,
		.pair_content = &pair_content_stub,
		.pair_in_use = &pair_in_use_stub,
		.pair_moved = &pair_moved_stub,
	};
	colmgr_init(&colmgr_conf);
}

static int
init_pair_stub(int pair, int f, int b)
{
	return 0;
}

static int
pair_content_stub(int pair, int *f, int *b)
{
	*f = 0;
	*b = 0;
	return 0;
}

static int
pair_in_use_stub(int pair)
{
	return 0;
}

static void
pair_moved_stub(int from, int to)
{
}

void
reset_timestamp(const char path[])
{
#ifndef _WIN32
	struct timeval tvs[2] = {};
	assert_success(utimes(path, tvs));
#else
	HANDLE file = CreateFileA(path, GENERIC_WRITE,
			FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
			FILE_ATTRIBUTE_NORMAL, NULL);

	assert_true(file != INVALID_HANDLE_VALUE);
	if(file != INVALID_HANDLE_VALUE)
	{
		FILETIME time = { 1, 0 };
		assert_true(SetFileTime(file, &time, &time, &time));
		CloseHandle(file);
	}
#endif
}

void
load_plugins(struct plugs_t *plugs, const char cfg_dir[])
{
	strlist_t plugins_dirs = { };
	plugins_dirs.nitems = put_into_string_array(&plugins_dirs.items,
			plugins_dirs.nitems, format_str("%s/plugins", cfg_dir));

	plugs_load(plugs, plugins_dirs);

	free_string_array(plugins_dirs.items, plugins_dirs.nitems);
}

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