/* vifm
 * Copyright (C) 2001 Ken Steen.
 * Copyright (C) 2011 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 "status.h"

#include <sys/types.h> /* ino_t */

#include <assert.h> /* assert() */
#include <limits.h> /* INT_MIN */
#include <stddef.h> /* NULL */
#include <string.h> /* memcpy() memmove() */
#include <time.h> /* time_t time() */

#include "cfg/config.h"
#include "compat/fs_limits.h"
#include "compat/pthread.h"
#include "compat/reallocarray.h"
#include "lua/vlua.h"
#include "modes/modes.h"
#include "ui/colors.h"
#include "ui/ui.h"
#include "utils/env.h"
#include "utils/fs.h"
#include "utils/fsdata.h"
#include "utils/log.h"
#include "utils/macros.h"
#include "utils/mem.h"
#include "utils/path.h"
#include "utils/str.h"
#include "utils/string_array.h"
#include "utils/test_helpers.h"
#include "utils/utils.h"
#include "cmd_core.h"
#include "filelist.h"
#include "filetype.h"
#include "ipc.h"
#include "opt_handlers.h"
#include "plugins.h"
#include "running.h"

/* Environment variables by which application hosted by terminal multiplexer can
 * identify the host. */
#define SCREEN_ENVVAR "STY"
#define TMUX_ENVVAR "TMUX"

/* dcache entry. */
typedef struct
{
	uint64_t value;   /* Stored value. */
#ifndef _WIN32
	ino_t inode;      /* Inode number. */
#endif
	time_t timestamp; /* When the value was set. */
}
dcache_data_t;

/* Saved view selection. */
typedef struct
{
	char *location; /* Path at which selection was done. */

	char **paths; /* Names of selected files. */
	int length;   /* Number of items in paths. */
}
selection_t;

static void load_def_values(status_t *stats, config_t *config);
static void determine_fuse_umount_cmd(status_t *stats);
static int reset_dircache(void);
static void set_last_cmdline_command(const char cmd[]);
static void dcache_get(const char path[], time_t mtime, uint64_t inode,
		dcache_result_t *size, dcache_result_t *nitems);
static void size_updater(void *data, void *arg);
TSTATIC time_t dcache_get_size_timestamp(const char path[]);
TSTATIC void dcache_set_size_timestamp(const char path[], time_t ts);

status_t curr_stats;

/* Whether redraw operation is scheduled. */
static int pending_redraw;
/* Whether reload operation is scheduled.  Redrawing is then assumed to be
 * scheduled too, as it's part of reloading. */
static int pending_refresh;
static int inside_screen;
static int inside_tmux;

/* Thread-safety guard for dcache_size variable. */
static pthread_mutex_t dcache_size_mutex = PTHREAD_MUTEX_INITIALIZER;
/* Thread-safety guard for dcache_nitems variable. */
static pthread_mutex_t dcache_nitems_mutex = PTHREAD_MUTEX_INITIALIZER;
/* Cache for directory sizes. */
static fsdata_t *dcache_size;
/* Cache for directory item count. */
static fsdata_t *dcache_nitems;

/* Whether UI updates should be "paused" (a counter, not a flag). */
static int silent_ui;
/* Whether silencing UI led to skipping of screen updates. */
static int silence_skipped_updates;

/* Selection history.  Most recently updated entry comes first. */
static selection_t sel_hist[10];

int
stats_init(config_t *config)
{
	inside_screen = !is_null_or_empty(env_get(SCREEN_ENVVAR));
	inside_tmux = !is_null_or_empty(env_get(TMUX_ENVVAR));

	load_def_values(&curr_stats, config);
	determine_fuse_umount_cmd(&curr_stats);
	curr_stats.exec_env_type = get_exec_env_type();
	stats_update_shell_type(config->shell);

	update_string(&curr_stats.term_name, env_get("TERM"));

	(void)hist_init(&curr_stats.cmd_hist, config->history_len);
	(void)hist_init(&curr_stats.menucmd_hist, config->history_len);
	(void)hist_init(&curr_stats.exprreg_hist, config->history_len);
	(void)hist_init(&curr_stats.search_hist, config->history_len);
	(void)hist_init(&curr_stats.prompt_hist, config->history_len);
	(void)hist_init(&curr_stats.filter_hist, config->history_len);

	hists_resize(config->history_len);

	return stats_reset(config);
}

/* Initializes most fields of the status structure, some are left to be
 * initialized by the stats_reset() function. */
static void
load_def_values(status_t *stats, config_t *config)
{
	pending_redraw = 0;
	pending_refresh = 0;

	stats->last_char = 0;
	stats->save_msg = 0;
	stats->use_register = 0;
	stats->curr_register = -1;
	stats->register_saved = 0;
	stats->number_of_windows = 2;
	stats->use_input_bar = 1;
	stats->drop_new_dir_hist = 0;
	stats->load_stage = 0;
	update_string(&stats->term_name, NULL);
	stats->term_state = TS_NORMAL;
	stats->ch_pos = 1;
	stats->confirmed = 0;
	stats->skip_shellout_redraw = 0;
	stats->cs = &config->cs;
	strcpy(stats->color_scheme, "");

	stats->preview.on = 0;
	stats->preview.kind = VK_TEXTUAL;
	update_string(&stats->preview.cleanup_cmd, NULL);
	stats->preview.clearing = 0;

	stats->direct_color = 0;

	stats->msg_head = 0;
	stats->msg_tail = 0;
	stats->save_msg_in_list = 1;
	stats->allow_sb_msg_truncation = 1;
	size_t i;
	for(i = 0U; i < ARRAY_LEN(stats->msgs); ++i)
	{
		update_string(&stats->msgs[i], NULL);
	}

	stats->scroll_bind_off = 0;
	stats->split = VSPLIT;
	stats->splitter_pos = -1;
	stats->splitter_ratio = 0.5;

	stats->sourcing_state = SOURCING_NONE;

	stats->restart_in_progress = RT_NONE;

	stats->reusing_statusline = 0;

	stats->exec_env_type = EET_EMULATOR;

	stats->term_multiplexer = TM_NONE;

	stats->initial_lines = INT_MIN;
	stats->initial_columns = INT_MIN;

	stats->ellipsis = "...";

	stats->shell_type = ST_POSIX;

	stats->fuse_umount_cmd = "";

	stats->original_stdout = NULL;

	update_string(&stats->chosen_files_out, NULL);
	update_string(&stats->chosen_dir_out , NULL);
	(void)replace_string(&stats->output_delimiter, "\n");

	update_string(&stats->on_choose, NULL);

	update_string(&stats->last_session, NULL);

	stats->preview_hint = NULL;

	free_string_array(stats->plugins_dirs.items, stats->plugins_dirs.nitems);
	stats->plugins_dirs.items = NULL;
	stats->plugins_dirs.nitems = 0;

	stats->global_local_settings = 0;

	stats->history_size = 0;

	ipc_free(stats->ipc);
	plugs_free(stats->plugs);
	vlua_finish(stats->vlua);

	stats->ipc = NULL;
	stats->vlua = NULL;
	stats->plugs = NULL;
}

/* Initializes stats->fuse_umount_cmd field of the stats. */
static void
determine_fuse_umount_cmd(status_t *stats)
{
	if(rn_cmd_exists("fusermount"))
	{
		stats->fuse_umount_cmd = "fusermount -u";
	}
	else if(rn_cmd_exists("umount"))
	{
		/* Some systems use regular umount command for FUSE. */
		stats->fuse_umount_cmd = "umount";
	}
	else
	{
		/* Leave default value. */
	}
}

int
stats_reset(const config_t *config)
{
	set_last_cmdline_command("");

	curr_stats.initial_lines = config->lines;
	curr_stats.initial_columns = config->columns;

	return reset_dircache();
}

/* Returns non-zero on error. */
static int
reset_dircache(void)
{
	fsdata_free(dcache_size);
	dcache_size = fsdata_create(0, 1);

	fsdata_free(dcache_nitems);
	dcache_nitems = fsdata_create(0, 1);

	return (dcache_size == NULL || dcache_nitems == NULL);
}

void
stats_set_use_multiplexer(int use_term_multiplexer)
{
	if(!use_term_multiplexer)
	{
		curr_stats.term_multiplexer = TM_NONE;
	}
	else if(inside_screen)
	{
		curr_stats.term_multiplexer = TM_SCREEN;
	}
	else if(inside_tmux)
	{
		curr_stats.term_multiplexer = TM_TMUX;
	}
	else
	{
		curr_stats.term_multiplexer = TM_NONE;
	}
}

void
stats_update_shell_type(const char shell_cmd[])
{
	curr_stats.shell_type = get_shell_type(shell_cmd);
}

TermState
stats_update_term_state(int screen_x, int screen_y)
{
	if(screen_x < MIN_TERM_WIDTH || screen_y < MIN_TERM_HEIGHT)
	{
		curr_stats.term_state = TS_TOO_SMALL;
	}
	else if(curr_stats.term_state != TS_NORMAL)
	{
		curr_stats.term_state = TS_BACK_TO_NORMAL;
	}

	return curr_stats.term_state;
}

void
stats_set_chosen_files_out(const char output[])
{
	(void)replace_string(&curr_stats.chosen_files_out, output);
}

void
stats_set_chosen_dir_out(const char output[])
{
	(void)replace_string(&curr_stats.chosen_dir_out, output);
}

void
stats_set_output_delimiter(const char delimiter[])
{
	(void)replace_string(&curr_stats.output_delimiter, delimiter);
}

void
stats_set_on_choose(const char command[])
{
	(void)replace_string(&curr_stats.on_choose, command);
}

int
stats_file_choose_action_set(void)
{
	return !is_null_or_empty(curr_stats.chosen_files_out)
	    || !is_null_or_empty(curr_stats.on_choose);
}

void
stats_save_msg(const char msg[])
{
	if(!curr_stats.save_msg_in_list || msg[0] == '\0')
	{
		return;
	}

	if(curr_stats.msg_tail != curr_stats.msg_head &&
			strcmp(curr_stats.msgs[curr_stats.msg_tail], msg) == 0)
	{
		return;
	}

	curr_stats.msg_tail = (curr_stats.msg_tail + 1)%ARRAY_LEN(curr_stats.msgs);
	if(curr_stats.msg_tail == curr_stats.msg_head)
	{
		free(curr_stats.msgs[curr_stats.msg_head]);
		curr_stats.msg_head = (curr_stats.msg_head + 1)%ARRAY_LEN(curr_stats.msgs);
	}
	curr_stats.msgs[curr_stats.msg_tail] = strdup(msg);
}

void
stats_set_quickview(int on)
{
	curr_stats.preview.on = on;
	load_quickview_option();
}

void
stats_set_splitter_pos(int position)
{
	double max = (curr_stats.split == HSPLIT ? cfg.lines : cfg.columns);
	double ratio = (position < 0 ? 0.5 : (max == INT_MIN ? -1 : position/max));

	curr_stats.splitter_ratio = ratio;
	if(curr_stats.splitter_pos != position)
	{
		curr_stats.splitter_pos = position;
		stats_redraw_later();
	}
}

void
stats_set_splitter_ratio(double ratio)
{
	if(ratio == -1)
	{
		stats_set_splitter_pos(curr_stats.splitter_pos);
		return;
	}

	curr_stats.splitter_ratio = ratio;

	int max = (curr_stats.split == HSPLIT ? cfg.lines : cfg.columns);
	if(max == INT_MIN)
	{
		/* Can't compute position from ratio, so leave it as is. */
		return;
	}

	int position = (ratio == 0.5 ? -1 : max*ratio + 0.5);
	if(curr_stats.splitter_pos != position)
	{
		curr_stats.splitter_pos = position;
		stats_redraw_later();
	}
}

void
stats_redraw_later(void)
{
	pending_redraw = 1;
}

void
stats_refresh_later(void)
{
	pending_refresh = 1;
}

UpdateType
stats_update_fetch(void)
{
	if(pending_refresh)
	{
		pending_refresh = 0;
		pending_redraw = 0;
		return UT_FULL;
	}
	if(pending_redraw)
	{
		pending_redraw = 0;
		return UT_REDRAW;
	}
	return UT_NONE;
}

int
stats_redraw_planned(void)
{
	return pending_refresh != 0
	    || pending_redraw != 0;
}

void
stats_silence_ui(int more)
{
	if(more)
	{
		++silent_ui;
	}
	else if(--silent_ui == 0)
	{
		stats_unsilence_ui();
	}
	else if(silent_ui < 0)
	{
		silent_ui = 0;
	}
}

int
stats_silenced_ui(void)
{
	if(silent_ui)
	{
		silence_skipped_updates = 1;
		return 1;
	}
	return 0;
}

void
stats_unsilence_ui(void)
{
	silent_ui = 0;
	if(silence_skipped_updates)
	{
		silence_skipped_updates = 0;
		pending_redraw = 0;
		modes_redraw();
	}
}

void
hists_resize(int new_size)
{
	curr_stats.history_size = new_size;

	hist_resize(&curr_stats.cmd_hist, new_size);
	hist_resize(&curr_stats.menucmd_hist, new_size);
	hist_resize(&curr_stats.exprreg_hist, new_size);
	hist_resize(&curr_stats.search_hist, new_size);
	hist_resize(&curr_stats.prompt_hist, new_size);
	hist_resize(&curr_stats.filter_hist, new_size);
}

void
hists_commands_save(const char command[])
{
	if(cmds_goes_to_history(command))
	{
		if(curr_stats.restart_in_progress == RT_NONE && curr_stats.load_stage == 3)
		{
			set_last_cmdline_command(command);
		}
		hist_add(&curr_stats.cmd_hist, command, -1);
	}
}

/* Sets last_cmdline_command field of the status structure. */
static void
set_last_cmdline_command(const char cmd[])
{
	const int err = replace_string(&curr_stats.last_cmdline_command, cmd);
	if(err != 0)
	{
		LOG_ERROR_MSG("replace_string() failed on duplicating: %s", cmd);
	}
	assert(curr_stats.last_cmdline_command != NULL &&
			"The field was not initialized properly");
}

void
hists_menucmd_save(const char cmd[])
{
	hist_add(&curr_stats.menucmd_hist, cmd, -1);
}

void
hists_exprreg_save(const char pattern[])
{
	hist_add(&curr_stats.exprreg_hist, pattern, -1);
}

void
hists_search_save(const char pattern[])
{
	hist_add(&curr_stats.search_hist, pattern, -1);
}

void
hists_prompt_save(const char input[])
{
	hist_add(&curr_stats.prompt_hist, input, -1);
}

void
hists_filter_save(const char input[])
{
	hist_add(&curr_stats.filter_hist, input, -1);
}

const char *
hists_search_last(void)
{
	return hist_is_empty(&curr_stats.search_hist)
	     ? ""
	     : curr_stats.search_hist.items[0].text;
}

void
dcache_get_at(const char path[], time_t mtime, uint64_t inode, uint64_t *size,
		uint64_t *nitems)
{
	dcache_result_t size_res, nitems_res;
	dcache_get(path, mtime, inode, (size == NULL ? NULL : &size_res),
			(nitems == NULL ? NULL : &nitems_res));

	if(size != NULL)
	{
		*size = (size_res.is_valid ? size_res.value : DCACHE_UNKNOWN);
	}
	if(nitems != NULL)
	{
		*nitems = (nitems_res.is_valid ? nitems_res.value : DCACHE_UNKNOWN);
	}
}

void
dcache_get_of(const dir_entry_t *entry, dcache_result_t *size,
		dcache_result_t *nitems)
{
	if(size == NULL && nitems == NULL)
	{
		return;
	}

	char full_path[PATH_MAX + 1];
	get_full_path_of(entry, sizeof(full_path), full_path);

	uint64_t inode = get_true_inode(entry);
	dcache_get(full_path, entry->mtime, inode, size, nitems);
}

/* Retrieves information about the path checking whether it's outdated.  size
 * and/or nitems can be NULL. */
static void
dcache_get(const char path[], time_t mtime, uint64_t inode,
		dcache_result_t *size, dcache_result_t *nitems)
{
	if(size != NULL)
	{
		size->value = DCACHE_UNKNOWN;
		size->is_valid = 0;

		pthread_mutex_lock(&dcache_size_mutex);
		dcache_data_t size_data;
		if(fsdata_get(dcache_size, path, &size_data, sizeof(size_data)) == 0)
		{
			size->value = size_data.value;
			/* We check strictly for less than to handle scenario when multiple
			 * changes occurred during the same second. */
			size->is_valid = (mtime < size_data.timestamp);
#ifndef _WIN32
			size->is_valid &= (inode == size_data.inode);
#endif
		}
		pthread_mutex_unlock(&dcache_size_mutex);
	}

	if(nitems != NULL)
	{
		nitems->value = DCACHE_UNKNOWN;
		nitems->is_valid = 0;

		pthread_mutex_lock(&dcache_nitems_mutex);
		dcache_data_t nitems_data;
		if(fsdata_get(dcache_nitems, path, &nitems_data, sizeof(nitems_data)) == 0)
		{
			nitems->value = nitems_data.value;
			/* We check strictly for less than to handle scenario when multiple
			 * changes occurred during the same second. */
			nitems->is_valid = (mtime < nitems_data.timestamp);
#ifndef _WIN32
			nitems->is_valid &= (inode == nitems_data.inode);
#endif
		}
		pthread_mutex_unlock(&dcache_nitems_mutex);
	}
}

void
dcache_update_parent_sizes(const char path[], uint64_t by)
{
	pthread_mutex_lock(&dcache_size_mutex);
	(void)fsdata_map_parents(dcache_size, path, &size_updater, &by);
	pthread_mutex_unlock(&dcache_size_mutex);
}

/* Updates cached value by a fixed amount. */
static void
size_updater(void *data, void *arg)
{
	const uint64_t *const by = arg;
	dcache_data_t *const what = data;

	what->value += *by;
}

int
dcache_set_at(const char path[], uint64_t inode, uint64_t size, uint64_t nitems)
{
	int ret = 0;
	const time_t ts = time(NULL);

	if(size != DCACHE_UNKNOWN)
	{
		dcache_data_t data = { .value = size, .timestamp = ts };
#ifndef _WIN32
		data.inode = (ino_t)inode;
#endif

		pthread_mutex_lock(&dcache_size_mutex);
		ret |= fsdata_set(dcache_size, path, &data, sizeof(data));
		pthread_mutex_unlock(&dcache_size_mutex);
	}

	if(nitems != DCACHE_UNKNOWN)
	{
		dcache_data_t data = { .value = nitems, .timestamp = ts };
#ifndef _WIN32
		data.inode = (ino_t)inode;
#endif

		pthread_mutex_lock(&dcache_nitems_mutex);
		ret |= fsdata_set(dcache_nitems, path, &data, sizeof(data));
		pthread_mutex_unlock(&dcache_nitems_mutex);
	}

	return ret;
}

TSTATIC time_t
dcache_get_size_timestamp(const char path[])
{
	dcache_data_t size_data;
	if(fsdata_get(dcache_size, path, &size_data, sizeof(size_data)) == 0)
	{
		return size_data.timestamp;
	}

	return -1;
}

TSTATIC void
dcache_set_size_timestamp(const char path[], time_t ts)
{
	dcache_data_t size_data;
	if(fsdata_get(dcache_size, path, &size_data, sizeof(size_data)) == 0)
	{
		size_data.timestamp = ts;
		(void)fsdata_set(dcache_size, path, &size_data, sizeof(size_data));
	}
}

void
selhist_put(const char location[], char *paths[], int path_count)
{
	if(path_count == 0)
	{
		free_string_array(paths, 0);
		return;
	}

	char *location_copy = strdup(location);
	if(location_copy == NULL)
	{
		free_string_array(paths, path_count);
		return;
	}

	unsigned int i;
	for(i = 0; i < ARRAY_LEN(sel_hist) - 1; ++i)
	{
		if(sel_hist[i].location != NULL &&
				paths_are_same(sel_hist[i].location, location))
		{
			break;
		}
	}

	/* At this point we have either found a match among all but the last element
	 * or will use the last element (whether it's a match doesn't matter). */

	free(sel_hist[i].location);
	free_string_array(sel_hist[i].paths, sel_hist[i].length);

	sel_hist[i].location = location_copy;
	sel_hist[i].paths = paths;
	sel_hist[i].length = path_count;

	mem_ror(sel_hist, i + 1, sizeof(sel_hist[0]));
}

int
selhist_get(const char location[], char ***paths, int *path_count)
{
	unsigned int i;

	for(i = 0; i < ARRAY_LEN(sel_hist); ++i)
	{
		if(sel_hist[i].location != NULL &&
				paths_are_same(sel_hist[i].location, location))
		{
			*paths = copy_string_array(sel_hist[i].paths, sel_hist[i].length);
			*path_count = sel_hist[i].length;
			return 0;
		}
	}

	return 1;
}

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