/*
 * Logserver
 * Copyright (C) 2017-2025 Joel Reardon
 *
 * 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 3 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, see <https://www.gnu.org/licenses/>.
 */

#ifndef __FILTER_MANAGER__H__
#define __FILTER_MANAGER__H__

#include <condition_variable>
#include <functional>
#include <future>
#include <string>
#include <vector>

#include "config.h"
#include "constants.h"
#include "format_string.h"
#include "i_log_lines_events.h"
#include "log_lines.h"
#include "line_filter_keyword.h"
#include "navigation.h"
#include "pin_manager.h"
#include "render_queue.h"
#include "renderer.h"

using namespace std;

/* FilterManager stores the line filter keywords for the current view and
 * manages growing the context window */
class FilterManager : public ILogLinesEvents {
public:
	/* construct with loglines and navi for the filter manager */
	FilterManager(LogLines* ll, Navigation* navi)
		: _current_keyword(nullptr),
		  _ll(ll),
		  _navi(navi),
		  _gentle_and(true),
		  _context_length(0),
		  _display_length(G::NO_POS),
		  _keyword_string_updating(true) {
		unique_lock<mutex> ul(_mutex);
		_display_length_future = async(&FilterManager::compute_length,
					       this);
		_length_result.resize(G::FILTER_TOTAL);
		EventBus::_()->enregister(_ll, this);
	}

	// default destructor
	virtual ~FilterManager() {
		EventBus::_()->deregister(_ll, this);
	}

	/* event bus functions. just invalidate length result and force them to
	 * recompute */
	virtual void clear_lines([[maybe_unused]] LogLines* ll) override {
		_clear_results = true;
		_pins.clear();
	}

	virtual void edit_line([[maybe_unused]] LogLines* ll,
			       [[maybe_unused]] size_t pos,
			       [[maybe_unused]] Line* line) override {
		_clear_results = true;
	}

	virtual void append_line([[maybe_unused]] LogLines* ll,
			         [[maybe_unused]] size_t pos,
			         [[maybe_unused]] Line* line) override {
		_clear_results = true;
	}

	virtual void insertion([[maybe_unused]] LogLines* ll,
			       size_t pos,
			       size_t amount) override {
		_clear_results = true;
		_pins.insertion(pos, amount);
	}

	virtual void deletion([[maybe_unused]] LogLines* ll,
			      [[maybe_unused]] size_t pos,
                              [[maybe_unused]] size_t amount) override {
		_clear_results = true;
	}

	// if the screen size was sufficiently big, this sets lines to be all
	// the positions that we would render at this moment based on our filter
	// status.
	virtual void get_view(set<size_t>* lines) {
		unique_lock<mutex> ul(_mutex);
		get_view_locked(lines);
	}

	/* gets the length of the lines that we would display. For no filter it
	 * is just length, but for a search it is the number of results.
	 * Implemented as a future since it can take time to compute and we do
	 * not want to delay rendering for this */
	virtual size_t length() {
		unique_lock<mutex> ul(_mutex);
		future_status status = future_status::deferred;
		if (_display_length_future.valid()) {
			ul.unlock();
			status = _display_length_future.wait_for(
				chrono::milliseconds(1));
			ul.lock();
		}
		if (status == future_status::ready) {
			_display_length = _display_length_future.get();
		}
		return _display_length;
	}

	/* Called when the user hits enter on the current typing of a keyword.
	 * Tell the keyword we are locked in and add it to the list we search
	 * for. Parameter accepted means whether user ended with enter. */
	virtual void finish_match(bool accepted) {
		unique_lock<mutex> ul(_mutex);
		_keyword_string = nullopt;

		// pop keyword if appropriate
		string keyword = _current_keyword->get_keyword();
		if (!accepted || keyword.empty()) {
			pop_keyword_locked();
			return;
		}

		// handle new keyword
		_clear_results = true;
		_current_keyword->finish();
		_keyword_vals.push_back(G::to_lower(
			_current_keyword->get_keyword()));
		_current_keyword = nullptr;
		_display_length_future = async(&FilterManager::compute_length,
					       this);
	}

	virtual const FormatString& keyword_string() {
		unique_lock<mutex> ul(_mutex);
		if (!_keyword_string || _keyword_string_updating) {
			_keyword_string_updating = false;
			_keyword_string = FormatString();
			int colour = 0;
			for (const auto& x : _keywords) {
				string val = '[' + x->get_description() + "] ";
				// the string is volatile, keep recomputing it
				if (!x->completed())
					_keyword_string_updating = true;
				_keyword_string->add(
					val,
					Colour::keyword_number_to_colour(colour));
				++colour;
			}
		}
		return *_keyword_string;
	}

	virtual string filter_string() const {
		unique_lock<mutex> ul(_mutex);
		string ret;
		auto it = _keywords.begin();
		if (it == _keywords.end()) return ret;
		ret += (*it)->get_description();
		++it;

		while (it != _keywords.end()) {
			ret += mode_char() + (*it)->get_description();
			++it;
		}
		return ret;
	}

	// returns the string that the user has currently typed in the context
	// of searching for a new keyword.
	virtual void current_type(const string& typed) {
		unique_lock<mutex> ul(_mutex);
		_clear_results = true;
		assert(_current_keyword);
		_current_keyword->current_type(typed);
	}

	// remove the last keyword on our list. This can be because the user is
	// removing it deliberatly, or they ended their match with escape
	// instead of enter
	virtual bool pop_keyword() {
		unique_lock<mutex> ul(_mutex);
		return pop_keyword_locked();
	}

	/* start a new search. not_inverted means normal search, otherwise a
	 * reverse (remove) search, anchor can be left, right, or none for where
	 * to search on the line, and whence signals to the line filter keyword
	 * where to start searching in the loglines so that matches are promptly
	 * displayed to the user. */
	virtual void start_match(int match_parms, size_t whence) {
		unique_lock<mutex> ul(_mutex);
		_clear_results = true;
		assert(!_current_keyword);
		_current_keyword = new LineFilterKeyword(
		    *_ll, match_parms, whence);

		_keywords.push_back(nullptr);
		_keywords.back().reset(_current_keyword);

		// automatically switch modes. First the first keyword switch to
		// and, and for the second, switch to or if its the first time
		// doing that since 0 keywords. This is because the user has not
		// specifically seleted AND search yet. If they do to go AND
		// after and go back to two, keep it in AND mode.
		if (_keywords.size() == 1) set_mode_conjunction();
		else if (_filter_keywords == G::FILTER_AND &&
			   _gentle_and &&
			   _keywords.size() == 2) {
			set_mode_disjunction();
			_gentle_and = false;
		}
	}

	// returns the string for the current keyword
	virtual string current_keyword() {
		unique_lock<mutex> ul(_mutex);
		assert(_current_keyword);
		return _current_keyword->get_keyword();
	}

	// handle tab to switch between or, and, and none modes
	virtual void toggle_mode() {
		unique_lock<mutex> ul(_mutex);

		_filter_keywords = (_filter_keywords + 1)
			% (!multiple() ? 2 : 3);
		_display_length_future = async(&FilterManager::compute_length,
					       this);
		return;
	}

	// returns true if filter mode is set to NONE
	virtual bool is_mode_all() const {
		unique_lock<mutex> ul(_mutex);
		return _filter_keywords == G::FILTER_NONE;
	}

	// string to display on the status bar that names the mode
	virtual string mode_string() const {
		unique_lock<mutex> ul(_mutex);
		if (_filter_keywords == G::FILTER_NONE)
			return "ALL";   // no filter
		if (_keywords.size() == 0)
			return "TAG";   // only tagged / pinned lines
		if (_keywords.size() == 1)
			return "MATCH"; // only one search term
		if (_filter_keywords == G::FILTER_AND)
			return "AND";   // AND mode
		if (_filter_keywords == G::FILTER_OR)
			return " OR";   // OR mode
		assert(0);
		return "NO SUCH MODE";
	}

	/* Searches the current line from the current tab position forwards to
	 * the end of the line, and returns the position of the next
	 * matching keyword on that line, among all possible keywords. Useful in
	 * finding matches on very long lines without breaking them */
	virtual size_t find_next_match() const {
		unique_lock<mutex> ul(_mutex);
		size_t cur = _navi->cur();
		size_t tab = _navi->tab();
		size_t ret = string::npos;
		for (const auto& x : _keyword_vals) {
			size_t i = _ll->find(cur, tab + G::h_shift(), x);
			if (i < ret) ret = i;
		}
		if (ret == string::npos) return tab;
		return ret;
	}

	/* Searches the current line backwards from the current tab position for
	 * the first matching keyword among all possible. */
	virtual size_t find_prev_match() const {
		unique_lock<mutex> ul(_mutex);
		size_t cur = _navi->cur();
		size_t tab = _navi->tab();
		size_t ret = string::npos;
		if (tab == 0) return 0;
		// TODO: instead we can get the logline once, lower it once, and
		// then search the keywords
		for (const auto& x : _keyword_vals) {
			size_t i = _ll->rfind(cur, tab - 1, x);
			assert(i == string::npos || i < tab);
			// if we have a match and the current match is
			// either not set or we have a closer one, use it
			if (i != string::npos && (i > ret || ret ==
						  string::npos)) {
				ret = i;
			}
		}
		if (ret == string::npos) return tab;
		return ret;
	}

	/* increments the number of context lines */
	virtual void add_context() {
		++_context_length;
	}

	/* decrements the number of context lines */
	virtual void remove_context() {
		if (_context_length) --_context_length;
	}

	/* returns the total number of context lines */
	virtual size_t total_context() const {
		return _context_length;
	}

	/* implements shift-up and shift-down in the various modes.
	 * For NONE, goes in direction dir to the next line that has a matching
	 * keyword.
	 * For AND just goes in dir
	 * For OR, goes in dir to the next line that does not contain a keyword
	 * already matching on this line. This allows there to be huge numbers
	 * of one keyword and few of another with large chunks of the first
	 * keyword easily skipped
	 */
	virtual size_t keyword_slide(int dir) {
		unique_lock<mutex> ul(_mutex);
		size_t length = _ll->length();
		size_t pos = _navi->cur();
		size_t use_pos = dir ? pos + 1 : pos;
		if (!use_pos) return pos;
		if (use_pos == G::NO_POS) {
			pos = length;
			use_pos = pos;
		}
		if (_filter_keywords == G::FILTER_AND)
			return _pins.pinsider(
				use_pos, dir,
				SearchRange::keyword_conjunction(
					pos, dir, _keywords, length));

		// if we are in disjuction, we this would just go to next line.
		// instead go to next lines that isn't matching the same keywork
		if (_filter_keywords == G::FILTER_OR) {
			// step 1. find all the keywords not present in current
			// line
			set<LineFilterKeyword*> notpresent;
			// TODO: why is this vals here
			for (size_t i = 0; i < _keywords.size(); ++i) {
				if (!_keywords.at(i)->is_match(pos)) {
					notpresent.insert(_keywords.at(i).get());
				}
			}
			// step 2. search for the nearest line with any of those
			// keywords not on the current line
			if (!notpresent.empty()) {
				return _pins.pinsider(
					use_pos, dir,
					SearchRange::keyword_disjunction(
						use_pos, dir,
						G::set_to_vec(notpresent),
						length));
			}
		}
		// in disjuction or all, return next matching line
                return _pins.pinsider(use_pos, dir,
				     SearchRange::keyword_disjunction(
						use_pos, dir,
						_keywords, length));
	}

	/* fills out the RenderParms structure with the relevant information
	 * contained in this class */
	virtual void set_render_parms(RenderParms* rp) {
		unique_lock<mutex> ul(_mutex);
		rp->context_length = _context_length;
		rp->filter_keywords = _filter_keywords;
		for (const auto& x : _keywords) {
			x->set_whence(_navi->cur());
			rp->keywords.push_back(x.get());
		}
		rp->pins = &_pins;
	}

	/* gets the closest pins up and down */
	virtual pair<optional<size_t>, optional<size_t>> nearest_pins(
			size_t whence) {
		return make_pair(_pins.nearest_pin(whence, G::DIR_UP),
				 _pins.nearest_pin(whence, G::DIR_DOWN));
	}

	/* turns on a pin at a position */
	virtual void set_pin(size_t pos) {
		_pins.set_pin(pos);
	}

	/* toggles a pin at a position */
	virtual void toggle_pin(size_t pos) {
		_pins.toggle_pin(pos);
	}

protected:
	// returns a char separator for keywords when making a permafilter
	// loglines. uses & for AND and | for OR.
	virtual char mode_char() const {
		assert(_keywords.size() >= 2);
		if (_filter_keywords == G::FILTER_AND) return '&';
		if (_filter_keywords == G::FILTER_OR) return '|';
		assert(0 && "permafilter with invalid mode");
		return '?';
	}

	/* we are rejecting the current type or removing top keyword in the stack */
	virtual bool pop_keyword_locked() {
		_keyword_string = nullopt;
		_clear_results = true;
		if (_keywords.empty()) return false;
		string word = static_cast<string>(*_keywords.back());
		// pop keyword_vals if it matches the line filter keyword we
		// will pop
		if (_keyword_vals.empty() ||
		    _keyword_vals.back() != G::to_lower(_keywords.back()->get_keyword())) {
			assert(_current_keyword != nullptr);
			_current_keyword = nullptr;
		} else {
			assert(_current_keyword == nullptr);
			_keyword_vals.pop_back();
		}
		_keywords.pop_back();

		if (empty()) {
			set_mode_none();
			_gentle_and = true;
		} else if (_keywords.size() == 1) {
			if (_filter_keywords == G::FILTER_OR) {
				set_mode_conjunction();
			}
		}
		return true;
	}

	/* can we memoize this when the tab changes */
	virtual size_t compute_length() {
		unique_lock<mutex> ul(_mutex);
		for (const auto& x : _keywords) {
			if (!x->completed()) {
				_clear_results = true;
				break;
			}
		}

		if (_clear_results) {
			for (size_t i = 0; i < _length_result.size(); ++i) {
				_length_result[i] = nullopt;
			}
			_clear_results = false;
		}

		if (_length_result.at(_filter_keywords) == nullopt) {
			size_t ret = G::NO_POS;
			if (_filter_keywords == G::FILTER_NONE) {
				ret = _ll->length();
			} else {
				set<size_t> lines;
				get_view_locked(&lines);
				ret = lines.size();
			}
			_length_result[_filter_keywords] = ret;
		}
		return *_length_result.at(_filter_keywords);
	}

	// returns the whole set of lines that would be seen with a long enough
	// screen, based on the current filtering mode.
	virtual void get_view_locked(set<size_t>* lines) {
		if (_filter_keywords == G::FILTER_NONE) return;
		if (_filter_keywords == G::FILTER_OR) {
			for (auto & x : _keywords) {
				x->disjunctive_join(lines);
			}
		} else if (_filter_keywords == G::FILTER_AND) {
			bool first = true;
			for (auto & x : _keywords) {
				if (first) x->disjunctive_join(lines);
				else x->conjunctive_join(lines);
				first = false;
			}
		}
	}

	// sets the mode to no filter
	virtual inline void set_mode_none() {
		_filter_keywords = G::FILTER_NONE;
	}

	// sets the mode to AND filter
	virtual inline void set_mode_conjunction() {
		_filter_keywords = G::FILTER_AND;
	}

	// sets the mode to OR filter
	virtual inline void set_mode_disjunction() {
		_filter_keywords = G::FILTER_OR;
	}

	// returns true if there are no keywords
	virtual bool empty() const {
		return _keywords.size() == 0;
	}

	// returns true if there are more than one keyword
	virtual bool multiple() const {
		return _keywords.size() > 1;
	}

	// set of pinned positions during search
	PinManager _pins;

	// the current keyword that is being edited, to direct keystrokes to. If
	// it is nullptr than we are not currently editing a keyword.
	LineFilterKeyword *_current_keyword;

	// the list of all keywords we are searching for. the current keyword
	// above will be the last on this list
	vector<unique_ptr<LineFilterKeyword>> _keywords;

	// the keywords themselves as strings
	vector<string> _keyword_vals;

	// current filtering mode
	int _filter_keywords = G::FILTER_NONE;

	// pointer to the log lines this filter manager is tied with
	LogLines* _ll;

	// thread safety
	mutable mutex _mutex;

	// navigation object attached to this runner
	Navigation* _navi;

	// first time we have two search terms, keep disjunction
	// afterwards, use conjuction if that is what the mode is
	bool _gentle_and;

	// length of the extra context around matching lines
	atomic<size_t> _context_length;

	/* compute the number of matching lines, can take time so we implement
	 * it async and display the result when it is finished */
	future<size_t> _display_length_future;

	// stores the computed length results until invalidated
	vector<optional<size_t>> _length_result;

	// signals that we need to recount the length because it has changed
	atomic<bool> _clear_results;

	// stored value from the future since getting it moves it out
	size_t _display_length;

	// renderer holds the render queue where we put our rendered lines
	Renderer* _renderer;

	// status bar line for the list of keywords
	optional<FormatString> _keyword_string;

	// true if the keyword string is volatile, such as an ongoing search
	bool _keyword_string_updating;
};

#endif  // __FILTER_MANAGER__H__
