/*
 * 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 __LINE_FILTER_KEYWORD__H__
#define __LINE_FILTER_KEYWORD__H__

#include <set>
#include <string>

#include "config.h"
#include "constants.h"
#include "explored_range.h"
#include "i_log_lines_events.h"
#include "log_lines.h"
#include "match.h"
#include "navigation.h"

using namespace std;
using namespace std::placeholders;

/* This class stores the notion of searching for a keyword in the log file. As
 * characters of the keyword are typed, that prefix is then sought so it is
 * responsive. Once the keyword is confirmed, it is set and all prefix searches
 * are removed */
class LineFilterKeyword : public ILogLinesEvents {
public:
	/* Constructor takes logline looking up lines and registering for
	 * updates, along with search parameters */
	LineFilterKeyword(const LogLines& log_lines,
			  int match_parms,
			  size_t whence)
		: _ll(log_lines),
		  _match(match_parms),
		  _state_changed(false) {
		set_whence(whence);
		EventBus::_()->enregister(&_ll, this);
		_exit = false;
		_worker.reset(new thread(&LineFilterKeyword::searcher, this));
	}

	/* unregister for updates if we are present, and join the worker thread
	 */
	virtual ~LineFilterKeyword() {
		// remove first to avoid lock contention
		EventBus::_()->deregister(&_ll, this);

		unique_lock<mutex> ul(_m);
		_state_changed = true;
		_exit = true;
		ul.unlock();
		_worker->join();
	}

	virtual void edit_line([[maybe_unused]] LogLines* ll,
			       size_t pos,
			       Line* line) override {
		unique_lock<mutex> ul(_m);
		if (!_finished) return;
		assert(ll == &_ll);

		if (_match.is_match(line->view())) {
			_partial_range.front()->add_finding(pos);
		} else {
			// in the very unlikely event where search already looked
			// at old version of a line and found a match, but
			// hasn't reported the results during which time we've
			// edited it to remove the match and are about to try to
			// remove it (but it is not there), but then it is later
			// added when the search finishes
			_state_changed = true;
			_partial_range.front()->remove_finding(pos);
		}
	}

        virtual void append_line([[maybe_unused]] LogLines* ll,
				 size_t pos,
                                 Line* line) override {
		unique_lock<mutex> ul(_m);
		if (!_finished) return;
		assert(ll == &_ll);

		assert(pos >= *_finished);

		if (_match.is_match(line->view())) {
			// add the current position to results
			assert(_partial_range.size() == 1);
			_partial_range.front()->post_end(pos);
		}

	}

        virtual void insertion([[maybe_unused]] LogLines* ll,
			       size_t pos,
                               size_t amount) override {
		unique_lock<mutex> ul(_m);
		assert(ll == &_ll);
		_state_changed = true;

		for (auto &x : _partial_range) {
			x->insert_or_delete_lines(pos, amount, true);
		}
	}

        virtual void deletion([[maybe_unused]] LogLines* ll,
			      size_t pos,
                              size_t amount) override {
		unique_lock<mutex> ul(_m);
		assert(ll == &_ll);
		_state_changed = true;

		for (auto &x : _partial_range) {
			x->insert_or_delete_lines(pos, amount, false);
		}
	}

	/* LogLines is clearing all the data in the filter */
	virtual void clear_lines([[maybe_unused]] LogLines* ll) override {
		unique_lock<mutex> ul(_m);
		assert(ll == &_ll);
		_state_changed = true;
		assert(_partial_range.size() == 1);
		_partial_range.front()->clear();
		_finished = 0;
	}

	// accessor for the string value
	operator string() const { return _keyword; }

	/* Sets what the user has currently typed for the keyword. This can be
	 * the same as before with a new letter added or a letter removed. It
	 * need not be at the end of the previous word (e.g., cursor pos). */
	virtual void current_type(const string& typed) {
		unique_lock<mutex> ul(_m);
		_state_changed = true;

		// step 1: scan the word from the start to look for differences
		size_t i = 0;
		while (i < typed.length()) {
			// words differ in middle due to insertion or mid-way
			// deletion. remove all partial ranges after the
			// difference
			if (i < _keyword.length() && typed[i] != _keyword[i]) {
				// truncate keyword to prefix of typed
				_keyword = _keyword.substr(0, i);
				// pop extra partial ranges
				while (_partial_range.size() > i) {
					_partial_range.pop_front();
				}
			}

			// if we still have more typed and its longer than
			// keyword, treat them as appends
			if (i >= _keyword.length()) {
				assert(i == _keyword.length());
				_keyword += typed[i];
				_partial_range.push_front(make_unique<ExploredRange>(
					G::SEARCH_RANGE));
			}
			++i;
		}
		// if we processed all of typed and it matched keyword, then
		// _keyword is longer and updated was to remove a character
		if (_keyword != typed) {
			assert(_keyword.length() > typed.length());
			_keyword = typed;
			while (_partial_range.size() > _keyword.length()) {
				_partial_range.pop_front();
			}
		}

		_match.commit_keyword(_keyword);
	}

	/* tell where the user's current line number is so that we can prioritize
	 * searching around that position */
	virtual inline void set_whence(size_t whence) {
		unique_lock<mutex> ul(_m);
		assert(whence <= G::EOF_POS || whence == G::NO_POS);
		_whence = whence;
	}

	/* called when the user has finished typing the keyword. we can get rid
	 * of all the per-character searches we won't need them now */
	virtual inline void finish() {
		auto lock = _ll.read_lock();
		unique_lock<mutex> ul(_m);
		_state_changed = true;
		// there weren't any characters
		if (_partial_range.size() == 0) {
			_exit = true;
			return;
		}
		assert(!_keyword.empty());
		// remove all explored ranges for strict prefixes of keyword
		while (_partial_range.size() >= 2) {
			_partial_range.pop_back();
		}

		// mark the end of log lines and register a listener for new
		// updates
		size_t end = _ll.length_locked();
		_finished = end;
		_partial_range.front()->mark_end(end);
		_match.commit_keyword(_keyword);
	}

	/* accessor for the keyword */
	virtual inline string get_keyword() const {
		unique_lock<mutex> ul(_m);
		return _keyword;
	}

	/* indicates a match was found after the end of log lines, e.g.,
	 * because we are tailing new appends */
	virtual inline void post_end(size_t pos) {
		unique_lock<mutex> ul(_m);
		assert(_partial_range.size() == 1);
		_partial_range.front()->post_end(pos);
	}

	/* for debug tracing out contents */
	virtual inline void trace(ostream& out) {
		unique_lock<mutex> ul(_m);
		assert(_partial_range.size());
		_partial_range.front()->trace(out);
	}

	/* calls next_match on the front partial range, reflecting the current
	 * keyword */
	virtual inline size_t next_match(size_t whence, bool dir) {
		unique_lock<mutex> ul(_m);
		assert(_partial_range.size());
		return _partial_range.front()->next_match(whence, dir);
	}

	/* returns true if the current keyword is empty so we have nothing in
	 * the explored range sets */
	virtual inline bool empty() {
		unique_lock<mutex> ul(_m);
		if (_partial_range.empty()) {
			assert(_keyword.empty());
			return true;
		}
		assert(!_keyword.empty());
		return false;
	}

	/* takes a set of positions, and intersects them with those that match
	 * our keyword */
	virtual inline void conjunctive_join(set<size_t>* lines) const {
		unique_lock<mutex> ul(_m);
		if (!_partial_range.size()) return;
		_partial_range.front()->conjunctive_join(lines);
	}

	/* takes a set of positions, and unions them with those that match our
	 * keyword */
	virtual inline void disjunctive_join(set<size_t>* lines) const {
		unique_lock<mutex> ul(_m);
		if (!_partial_range.size()) return;
		_partial_range.front()->disjunctive_join(lines);
	}

	/* calls next_range on the exploreod range */
	virtual inline size_t next_range(size_t whence, bool dir) {
		unique_lock<mutex> ul(_m);
		assert(_partial_range.size());
		return _partial_range.front()->next_range(whence, dir);
	}

	/* returns true if pos is a match we've identified */
	virtual inline bool is_match(size_t pos) {
		unique_lock<mutex> ul(_m);
                assert(_partial_range.size());
                return _partial_range.front()->is_match(pos);
	}

	/* used to generate the line of information regarding current keywords
	 * being sought. ellipsis are used to indicate that the search is
	 * ongoing */
	virtual string get_description() const {
		unique_lock<mutex> ul(_m);
		string ret = _match.get_description();
		if (_finished && (!_partial_range.front()->completed())) {
			ret += "..";
		}
		return ret;
	}

	virtual bool completed() const {
		unique_lock<mutex> ul(_m);
		if (!_finished) return false;
		return _partial_range.front()->completed();
	}

protected:
	/* main searching thread. Acquires a read lock from loglines, gets the
	 * current whence as the starting point, and asks for a range. It then
	 * matches on that range and tells explored_range the results. When we
	 * are finished searching, or we are being destructed, it exits */
	virtual void searcher() {
		unique_lock<mutex> lock(_m);
		while (!_exit) {
			size_t start, end;
			if (_partial_range.size() > 0) {
				size_t whence = _whence;
				// search at the end of the log, unless we've
				// already set the finish line

				// avoid deadlock
				lock.unlock();
				if (whence == G::NO_POS)
					whence = _ll.length();
				lock.lock();

				if (_finished != nullopt && whence >= *_finished)
					whence = *_finished;
				// cache front range in case it changes during
				// unlocking while searching
				ExploredRange* range =
					_partial_range.front().get();
				if (range->completed())
					break;
				range->explore(whence, &start, &end);
				set<size_t> results;
				size_t lines = 0;

				// our matching callback to pass loglines
				function<bool(const string_view&)> fn =
					bind(&Match::is_match, _match, _1);
				// we will unlock for the duration of the match.
				// however if something changes that would make
				// our results invalid (e.g., line insertions)
				// just abandom these results and try again
				_state_changed = false;
				lock.unlock();

				lines = _ll.range_add_if(start, end, &results, fn);
				if (_exit) [[unlikely]] break;
				lock.lock();

				// check if anything changed such that we should
				// ignore this search results
				if (_state_changed == true) [[unlikely]] continue;

				if (end > lines) end = lines;
				if (start != end) {
					range->extend(start, end, results);
				}
				// if we have the end position and searched it
				// all, this thread can finish
				if (_finished != nullopt &&
				    _partial_range.front().get() == range &&
				    _partial_range.front()->completed())
					break;
			}
			lock.unlock();
			this_thread::sleep_for(chrono::milliseconds(1));
			lock.lock();
		}
	}

	// used to retrieve line values and register for update callbacks
	const LogLines& _ll;

	// encapsulates the search parameters
	Match _match;

	// keywor being sough
	string _keyword;

	// this list is per character in the search word so that searching is
	// responsive as the characters come in, and partial results are useful
	// when backspace is hit
	list<unique_ptr<ExploredRange>> _partial_range;

	// for thread safety
	mutable mutex _m;

	// signals to worker thread this class is being deconstructed if it is
	// still searching
	bool _exit;

	// current line being viewed, to priorize searching so the user sees
	// relevant lines sooner
	size_t _whence;

	// searching thread
	unique_ptr<thread> _worker;

	// size of loglines at the moment the keysearch search is locked in and
	// a listener is registered
	optional<size_t> _finished;

	// used to signal the worker thread that the state changed such that its
	// search results may be invalid or the range it wants to send them to
	// is gone
	atomic<bool> _state_changed;
};

#endif  // __LINE_FILTER_KEYWORD__H__
