/* 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 "filtering.h"

#include <assert.h> /* assert() */
#include <string.h> /* strdup() */

#include "cfg/config.h"
#include "compat/reallocarray.h"
#include "ui/tabs.h"
#include "ui/ui.h"
#include "utils/dynarray.h"
#include "utils/matcher.h"
#include "utils/path.h"
#include "utils/regexp.h"
#include "utils/str.h"
#include "utils/utils.h"
#include "filelist.h"
#include "flist_pos.h"
#include "flist_sel.h"
#include "opt_handlers.h"

static void reset_filter(filter_t *filter);
static int is_newly_filtered(view_t *view, const dir_entry_t *entry, void *arg);
static void replace_matcher(matcher_t **matcher, const char expr[]);
static int get_unfiltered_pos(const view_t *view, int pos);
static int load_unfiltered_list(view_t *view);
static int list_is_incomplete(view_t *view);
static void store_local_filter_position(view_t *view, int pos);
static int update_filtering_lists(view_t *view, int add, int clear);
static void reparent_tree_node(dir_entry_t *original, dir_entry_t *filtered);
static void ensure_filtered_list_not_empty(view_t *view,
		dir_entry_t *parent_entry);
static int extract_previously_selected_pos(view_t *view);
static void clear_local_filter_hist_after(view_t *view, int pos);
static int find_nearest_neighour(const view_t *view);
static void local_filter_finish(view_t *view);
static void append_slash(const char name[], char buf[], size_t buf_size);

void
filters_view_reset(view_t *view)
{
	view->invert = cfg.filter_inverted_by_default ? 1 : 0;
	view->prev_invert = view->invert;

	(void)replace_string(&view->prev_manual_filter, "");
	replace_matcher(&view->manual_filter, "");
	(void)replace_string(&view->prev_auto_filter, "");
	reset_filter(&view->auto_filter);

	(void)replace_string(&view->local_filter.prev, "");
	reset_filter(&view->local_filter.filter);
	view->local_filter.in_progress = 0;
	view->local_filter.saved = NULL;
	view->local_filter.poshist = NULL;
	view->local_filter.poshist_len = 0U;
}

/* Resets filter to empty state (either initializes or clears it). */
static void
reset_filter(filter_t *filter)
{
	if(filter->raw == NULL)
	{
		(void)filter_init(filter, FILTER_DEF_CASE_SENSITIVITY);
	}
	else
	{
		filter_clear(filter);
	}
}

void
dot_filter_set(view_t *view, int visible)
{
	view->hide_dot_g = view->hide_dot = !visible;
	ui_view_schedule_reload(view);
	if(view == curr_view)
	{
		load_dot_filter_option(view);
	}

	if(curr_stats.global_local_settings)
	{
		int i;
		tab_info_t tab_info;
		for(i = 0; tabs_enum_all(i, &tab_info); ++i)
		{
			if(tab_info.view != view)
			{
				tab_info.view->hide_dot_g = tab_info.view->hide_dot = !visible;
				ui_view_schedule_reload(tab_info.view);
				if(tab_info.view == curr_view)
				{
					load_dot_filter_option(tab_info.view);
				}
			}
		}
	}
}

void
dot_filter_toggle(view_t *view)
{
	view->hide_dot_g = view->hide_dot = !view->hide_dot;
	ui_view_schedule_reload(view);
	if(view == curr_view)
	{
		load_dot_filter_option(view);
	}

	if(curr_stats.global_local_settings)
	{
		int i;
		tab_info_t tab_info;
		for(i = 0; tabs_enum_all(i, &tab_info); ++i)
		{
			if(tab_info.view != view)
			{
				tab_info.view->hide_dot = !tab_info.view->hide_dot;
				tab_info.view->hide_dot_g = tab_info.view->hide_dot;
				ui_view_schedule_reload(tab_info.view);
				if(tab_info.view == curr_view)
				{
					load_dot_filter_option(tab_info.view);
				}
			}
		}
	}
}

void
name_filters_add_active(view_t *view)
{
	filter_t filter;
	(void)filter_init(&filter, FILTER_DEF_CASE_SENSITIVITY);

	flist_set_marking(view, 0);

	/* Traverse items and update/create filter values. */
	dir_entry_t *entry = NULL;
	while(iter_marked_entries(view, &entry))
	{
		const char *name = entry->name;
		char name_with_slash[NAME_MAX + 1 + 1];

		if(fentry_is_dir(entry))
		{
			append_slash(entry->name, name_with_slash, sizeof(name_with_slash));
			name = name_with_slash;
		}

		(void)filter_append(&view->auto_filter, name);
		(void)filter_append(&filter, name);
	}

	/* Even current file might be unavailable for filtering.  In this case, just
	 * do nothing. */
	if(filter_is_empty(&filter))
	{
		filter_dispose(&filter);
		return;
	}

	if(view->custom.type == CV_DIFF)
	{
		(void)filter_in_compare(view, &filter, &is_newly_filtered);
		ui_view_schedule_redraw(view);
		filter_dispose(&filter);
		return;
	}

	/* Update entry lists to remove entries that must be filtered out now.  No
	 * view reload is needed. */
	int filtered = zap_entries(view, view->dir_entry, &view->list_rows,
			&is_newly_filtered, &filter, 0, 1);
	if(flist_custom_active(view))
	{
		/* Name exclusion from a custom filter is not reversible. */
		(void)zap_entries(view, view->custom.full.entries,
				&view->custom.full.nentries, &is_newly_filtered, &filter, 1, 1);
	}

	if(flist_is_fs_backed(view))
	{
		view->filtered += filtered;
	}

	filter_dispose(&filter);

	fpos_ensure_valid_pos(view);
	ui_view_schedule_redraw(view);
}

/* zap_entries() filter to filter-out files that match filter passed in the
 * arg. */
static int
is_newly_filtered(view_t *view, const dir_entry_t *entry, void *arg)
{
	filter_t *const filter = arg;

	/* FIXME: some very long file names won't be matched against some regexps. */
	char name_with_slash[NAME_MAX + 1 + 1];
	const char *filename = entry->name;

	if(fentry_is_dir(entry))
	{
		append_slash(filename, name_with_slash, sizeof(name_with_slash));
		filename = name_with_slash;
	}

	return filter_matches(filter, filename) == 0;
}

void
name_filters_remove(view_t *view)
{
	if(name_filters_empty(view))
	{
		return;
	}

	(void)replace_string(&view->prev_manual_filter,
			matcher_get_expr(view->manual_filter));
	(void)replace_string(&view->prev_auto_filter, view->auto_filter.raw);
	view->prev_invert = view->invert;

	name_filters_drop(view);
	view->invert = cfg.filter_inverted_by_default ? 1 : 0;

	ui_view_schedule_reload(view);
}

int
name_filters_empty(view_t *view)
{
	return matcher_is_empty(view->manual_filter)
	    && filter_is_empty(&view->auto_filter);
}

void
name_filters_drop(view_t *view)
{
	filter_clear(&view->auto_filter);
	replace_matcher(&view->manual_filter, "");
	view->invert = 1;
}

void
name_filters_restore(view_t *view)
{
	if(view->prev_manual_filter[0] == '\0' && view->prev_auto_filter[0] == '\0')
	{
		return;
	}

	replace_matcher(&view->manual_filter, view->prev_manual_filter);

	(void)filter_set(&view->auto_filter, view->prev_auto_filter);
	view->invert = view->prev_invert;
	ui_view_schedule_reload(view);
}

/* Changes *matcher to have the value of the expr.  The operation is assumed to
 * succeed, but it's not guaranteed. */
static void
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);
}

void
filters_invert(view_t *view)
{
	view->invert = !view->invert;
	load_dir_list(view, 1);
	fpos_set_pos(view, 0);
}

int
filters_file_is_visible(const view_t *view, const char dir[], const char name[],
		int is_dir, int apply_local_filter)
{
	/* FIXME: some very long file names won't be matched against some regexps. */
	char name_with_slash[NAME_MAX + 1 + 1];
	char path[PATH_MAX + sizeof(name_with_slash)];

	if(is_dir)
	{
		append_slash(name, name_with_slash, sizeof(name_with_slash));
		name = name_with_slash;
	}

	if(filter_matches(&view->auto_filter, name) > 0)
	{
		return 0;
	}

	if(apply_local_filter &&
			filter_matches(&view->local_filter.filter, name) == 0)
	{
		return 0;
	}

	if(matcher_is_empty(view->manual_filter))
	{
		return 1;
	}

	if(matcher_is_full_path(view->manual_filter))
	{
		const size_t nchars = copy_str(path, sizeof(path) - 1, dir);
		path[nchars - 1U] = '/';
		copy_str(path + nchars, sizeof(path) - nchars, name);
		name = path;
	}

	return matcher_matches(view->manual_filter, name)
	     ? !view->invert
	     : view->invert;
}

void
filters_dir_updated(view_t *view)
{
	filter_clear(&view->local_filter.filter);
}

void
filters_drop_temporaries(view_t *view, dir_entry_t entries[])
{
	/* This is basically a simplified version of update_filtering_lists().  Not
	 * sure if it's worth merging them. */

	int i;
	size_t list_size = 0U;

	for(i = 0; i < view->list_rows; ++i)
	{
		dir_entry_t *new_entry;
		dir_entry_t *const entry = &view->dir_entry[i];

		/* The tag field links to position of nodes passed through filter in the
		 * list of visible files.  Removed nodes have -1. */
		entry->tag = -1;

		if(entry->temporary)
		{
			fentry_free(entry);
			continue;
		}

		new_entry = add_dir_entry(&entries, &list_size, entry);
		if(new_entry != NULL)
		{
			entry->tag = list_size - 1U;
			/* We basically grow the tree node by node while performing
			 * reparenting. */
			reparent_tree_node(entry, new_entry);
		}
	}

	dynarray_free(view->dir_entry);
	view->dir_entry = entries;
	view->list_rows = list_size;
}

int
local_filter_set(view_t *view, const char filter[])
{
	int result;
	const int current_file_pos = view->local_filter.in_progress
	                           ? get_unfiltered_pos(view, view->list_pos)
	                           : load_unfiltered_list(view);

	if(current_file_pos >= 0)
	{
		store_local_filter_position(view, current_file_pos);
	}

	result = (filter_change(&view->local_filter.filter, filter,
			!regexp_should_ignore_case(filter)) ? -1 : 0);

	if(update_filtering_lists(view, 1, 0) != 0 && result == 0)
	{
		result = 1;
	}
	return result;
}

/* Gets position of an item in dir_entry list at position pos in the unfiltered
 * list.  Returns index on success, otherwise -1 is returned. */
static int
get_unfiltered_pos(const view_t *view, int pos)
{
	const int count = (int)view->local_filter.unfiltered_count;
	const char *const filename = view->dir_entry[pos].name;
	while(pos < count)
	{
		/* Compare pointers, which are the same for same entry in two arrays. */
		if(view->local_filter.unfiltered[pos].name == filename)
		{
			return pos;
		}
		++pos;
	}
	return -1;
}

/* Loads full list of files into unfiltered list of the view.  Returns position
 * of file under cursor in the unfiltered list. */
static int
load_unfiltered_list(view_t *view)
{
	int current_file_pos = view->list_pos;

	view->local_filter.in_progress = 1;

	view->local_filter.saved = strdup(view->local_filter.filter.raw);

	if(list_is_incomplete(view))
	{
		char full_path[PATH_MAX + 1];
		dir_entry_t *entry;

		get_current_full_path(view, sizeof(full_path), full_path);

		filter_clear(&view->local_filter.filter);
		(void)populate_dir_list(view, 1);

		/* Resolve current file position in updated list. */
		entry = entry_from_path(view, view->dir_entry, view->list_rows, full_path);
		if(entry != NULL)
		{
			current_file_pos = entry_to_pos(view, entry);
		}

		if(current_file_pos >= view->list_rows)
		{
			current_file_pos = view->list_rows - 1;
		}
	}

	flist_custom_save(view);

	view->local_filter.unfiltered = view->dir_entry;
	view->local_filter.unfiltered_count = view->list_rows;
	view->local_filter.prefiltered_count = view->filtered;
	view->dir_entry = NULL;

	return current_file_pos;
}

/* Checks whether list of files is incomplete.  Returns non-zero if so,
 * otherwise zero is returned. */
static int
list_is_incomplete(view_t *view)
{
	if(view->filtered > 0 && !filter_is_empty(&view->local_filter.filter))
	{
		return 1;
	}

	if(!flist_is_fs_backed(view))
	{
		return 0;
	}

	/* Check if there are any directories without leaf nodes.  They aren't counted
	 * as filtered out, so need to check separately (or start counting them in
	 * some way). */
	int i;
	for(i = 0; i < view->list_rows; ++i)
	{
		dir_entry_t *const entry = &view->dir_entry[i];
		if(entry->type == FT_DIR && entry->child_count == 0 &&
				!is_parent_dir(entry->name))
		{
			return 1;
		}
	}

	return 0;
}

/* Adds local filter position (in unfiltered list) to position history. */
static void
store_local_filter_position(view_t *view, int pos)
{
	size_t *const len = &view->local_filter.poshist_len;
	int *arr = reallocarray(view->local_filter.poshist, *len + 1, sizeof(int));
	if(arr != NULL)
	{
		view->local_filter.poshist = arr;
		arr[*len] = pos;
		++*len;
	}
}

/* Copies/moves elements of the unfiltered list into dir_entry list.  add
 * parameter controls whether entries matching filter are copied into dir_entry
 * list.  clear parameter controls whether entries not matching filter are
 * cleared in unfiltered list.  Returns zero unless addition is performed in
 * which case can return non-zero when all files got filtered out. */
static int
update_filtering_lists(view_t *view, int add, int clear)
{
	/* filters_drop_temporaries() is a similar function. */

	size_t i;
	size_t list_size = 0U;
	dir_entry_t *parent_entry = NULL;
	int parent_added = 0;

	for(i = 0; i < view->local_filter.unfiltered_count; ++i)
	{
		/* FIXME: some very long file names won't be matched against some
		 * regexps. */
		char name_with_slash[NAME_MAX + 1 + 1];

		dir_entry_t *const entry = &view->local_filter.unfiltered[i];
		const char *name = entry->name;

		if(is_parent_dir(name))
		{
			if(entry->child_pos == 0)
			{
				parent_entry = entry;
				if(add && cfg_parent_dir_is_visible(is_root_dir(view->curr_dir)))
				{
					(void)add_dir_entry(&view->dir_entry, &list_size, entry);

					parent_added = 1;
				}
				continue;
			}
			else if(!filter_is_empty(&view->local_filter.filter))
			{
				if(clear)
				{
					fentry_free(entry);
				}
				continue;
			}
		}

		if(fentry_is_dir(entry))
		{
			append_slash(name, name_with_slash, sizeof(name_with_slash));
			name = name_with_slash;
		}

		/* tag links to position of nodes passed through filter in list of visible
		 * files.  Nodes that didn't pass have -1. */
		entry->tag = -1;
		if(filter_matches(&view->local_filter.filter, name) != 0)
		{
			if(add)
			{
				dir_entry_t *e = add_dir_entry(&view->dir_entry, &list_size, entry);
				if(e != NULL)
				{
					entry->tag = list_size - 1U;
					/* We basically grow the tree node by node while performing
					 * reparenting. */
					reparent_tree_node(entry, e);
				}
			}
		}
		else
		{
			if(clear)
			{
				fentry_free(entry);
			}
		}
	}

	if(clear)
	{
		/* XXX: the check of name pointer is horrible, but is needed to prevent
		 *      freeing of entry in use. */
		if(!parent_added && parent_entry != NULL && list_size != 0U &&
				view->dir_entry[0].name != parent_entry->name)
		{
			fentry_free(parent_entry);
		}
	}
	if(add)
	{
		view->list_rows = list_size;
		view->filtered = view->local_filter.prefiltered_count
		               + view->local_filter.unfiltered_count - list_size;
		ensure_filtered_list_not_empty(view, parent_entry);
		return list_size == 0U
		    || (list_size == 1U && parent_added &&
						(filter_matches(&view->local_filter.filter, "../") == 0));
	}
	return 0;
}

/* Reparents *filtered node by attaching it to the closes ancestor of *original
 * mapped onto the list of filtered nodes.  tag field of entries is used to
 * perform the mapping. */
static void
reparent_tree_node(dir_entry_t *original, dir_entry_t *filtered)
{
	dir_entry_t *parent, *child;

	filtered->child_pos = 0;
	filtered->child_count = 0;

	/* Go through items in unfiltered list looking for the closest ancestor, which
	 * wasn't filtered out and make it the parent. */
	child = original;
	parent = child - child->child_pos;
	while(parent != child)
	{
		if(parent->tag >= 0)
		{
			filtered->child_pos = original->tag - parent->tag;
			parent = filtered - filtered->child_pos;
			while(parent != child)
			{
				++parent->child_count;

				child = parent;
				parent -= parent->child_pos;
			}
			break;
		}

		child = parent;
		parent -= parent->child_pos;
	}
}

/* Use parent_entry to make filtered list not empty, or create such entry (if
 * parent_entry is NULL) and put it to original list. */
static void
ensure_filtered_list_not_empty(view_t *view, dir_entry_t *parent_entry)
{
	if(view->list_rows != 0U)
	{
		return;
	}

	if(parent_entry == NULL)
	{
		add_parent_dir(view);
		if(view->list_rows > 0)
		{
			(void)add_dir_entry(&view->local_filter.unfiltered,
					&view->local_filter.unfiltered_count,
					&view->dir_entry[view->list_rows - 1]);
		}
	}
	else
	{
		size_t list_size = 0U;
		(void)add_dir_entry(&view->dir_entry, &list_size, parent_entry);
		view->list_rows = list_size;
	}
}

void
local_filter_update_pos(view_t *view)
{
	struct local_filter_t *lf = &view->local_filter;
	if(lf->poshist_len > 0)
	{
		const int current_file_pos = lf->in_progress
		                           ? get_unfiltered_pos(view, view->list_pos)
		                           : load_unfiltered_list(view);
		if(current_file_pos >= 0)
		{
			lf->poshist[lf->poshist_len - 1] = current_file_pos;
		}
	}
}

void
local_filter_update_view(view_t *view, int rel_pos)
{
	int pos = extract_previously_selected_pos(view);

	if(pos < 0)
	{
		pos = find_nearest_neighour(view);
	}

	if(pos >= 0)
	{
		if(pos == 0 && is_parent_dir(view->dir_entry[0].name) &&
				view->list_rows > 1 && !filter_is_empty(&view->local_filter.filter))
		{
			++pos;
		}

		view->list_pos = pos;
		view->top_line = pos - rel_pos;
	}
}

/* Finds one of previously selected files and updates list of visited files
 * if needed.  Returns file position in current list of files or -1. */
static int
extract_previously_selected_pos(view_t *view)
{
	size_t i;
	for(i = 0; i < view->local_filter.poshist_len; i++)
	{
		const int unfiltered_pos = view->local_filter.poshist[i];
		const dir_entry_t *entry = &view->local_filter.unfiltered[unfiltered_pos];
		const int filtered_pos = fpos_find_entry(view, entry->name, entry->origin);

		if(filtered_pos >= 0)
		{
			clear_local_filter_hist_after(view, i);
			return filtered_pos;
		}
	}
	return -1;
}

/* Clears all positions after the pos. */
static void
clear_local_filter_hist_after(view_t *view, int pos)
{
	view->local_filter.poshist_len -= view->local_filter.poshist_len - (pos + 1);
}

/* Find nearest filtered neighbour.  Returns index of nearest unfiltered
 * neighbour of the entry initially pointed to by cursor. */
static int
find_nearest_neighour(const view_t *view)
{
	const int count = view->local_filter.unfiltered_count;

	if(view->local_filter.poshist_len > 0U)
	{
		const int unfiltered_orig_pos = view->local_filter.poshist[0U];
		int i;
		for(i = unfiltered_orig_pos; i < count; ++i)
		{
			const int filtered_pos = fpos_find_by_name(view,
					view->local_filter.unfiltered[i].name);
			if(filtered_pos >= 0)
			{
				return filtered_pos;
			}
		}
	}

	assert(view->list_rows > 0 && "List of files is empty.");
	return view->list_rows - 1;
}

void
local_filter_accept(view_t *view, int update_history)
{
	if(!view->local_filter.in_progress)
	{
		return;
	}

	update_filtering_lists(view, 0, 1);

	local_filter_finish(view);

	if(update_history)
	{
		hists_filter_save(view->local_filter.filter.raw);
	}

	/* Some of previously selected files could be filtered out, update number of
	 * selected files. */
	flist_sel_recount(view);
}

void
local_filter_apply(view_t *view, const char filter[])
{
	if(view->local_filter.in_progress)
	{
		assert(!view->local_filter.in_progress && "Wrong local filter applying.");
		return;
	}

	int case_sensitive = !regexp_should_ignore_case(filter);
	(void)filter_change(&view->local_filter.filter, filter, case_sensitive);
	hists_filter_save(view->local_filter.filter.raw);

	flist_custom_save(view);

	ui_view_schedule_reload(view);
}

void
local_filter_cancel(view_t *view)
{
	if(!view->local_filter.in_progress)
	{
		return;
	}

	(void)filter_set(&view->local_filter.filter, view->local_filter.saved);

	dynarray_free(view->dir_entry);
	view->dir_entry = NULL;
	view->list_rows = 0;

	update_filtering_lists(view, 1, 1);
	local_filter_finish(view);
}

/* Finishes filtering process and frees associated resources. */
static void
local_filter_finish(view_t *view)
{
	dynarray_free(view->local_filter.unfiltered);
	free(view->local_filter.saved);
	view->local_filter.in_progress = 0;

	free(view->local_filter.poshist);
	view->local_filter.poshist = NULL;
	view->local_filter.poshist_len = 0U;
}

void
local_filter_remove(view_t *view)
{
	(void)replace_string(&view->local_filter.prev, view->local_filter.filter.raw);
	filter_clear(&view->local_filter.filter);
	ui_view_schedule_reload(view);
}

void
local_filter_restore(view_t *view)
{
	(void)filter_set(&view->local_filter.filter, view->local_filter.prev);
	(void)replace_string(&view->local_filter.prev, "");
}

int
local_filter_matches(view_t *view, const dir_entry_t *entry)
{
	/* FIXME: some very long file names won't be matched against some regexps. */
	char name_with_slash[NAME_MAX + 1 + 1];
	const char *filename = entry->name;
	if(fentry_is_dir(entry))
	{
		append_slash(filename, name_with_slash, sizeof(name_with_slash));
		filename = name_with_slash;
	}

	return filter_matches(&view->local_filter.filter, filename) != 0;
}

/* Appends slash to the name and stores result in the buffer. */
static void
append_slash(const char name[], char buf[], size_t buf_size)
{
	const size_t nchars = copy_str(buf, buf_size - 1, name);
	buf[nchars - 1] = '/';
	buf[nchars] = '\0';
}

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