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

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

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

using namespace std;

/* Filter renderer is the engine in charge of computing what each display line
 * should look like. Each new render epoch kicks off a filter renderer's
 * run_render method. It takes a snapshot copy of the render parameters in
 * RenderParms (e.g., width, height, search words, etc.) and computes the lines
 * to render, passing them to the renderer. If the epoch advances during its
 * render, it exits. */
class FilterRenderer {
public:
	/* Constructor takes the renderer and reference to the epoch */
	FilterRenderer(Renderer* renderer, const Epoch& epoch)
			: _down_context_end(G::NO_POS),
			  _up_context_end(G::NO_POS),
			  _renderer(renderer),
			  _epoch(epoch),
			  _cur(G::NO_POS) {
	}

	// default destructor
	virtual ~FilterRenderer() {}

	// Kick off a rendering. The parameter parms stores all the relevant
	// parameters. Takes ownership of parms
	virtual void run_render(RenderParms* parms) {
		prepare_display(parms);

		_parms.reset(parms);
		// there is a single filter renderer, so we need to signal which
		// loglines we are currently rendering
		_navi = _parms->navi;
		_cur = _navi->cur();  // remember for up/down optimization
		_ll = _parms->ll;
		_max_digits = nullopt;

reenter:
		// special case: we jumped into a loglines at a line,
		// but that line is not yet loaded.
		while (true) {
			// check if render is aborted
			if (_parms->cur_epoch != _epoch.cur()) return;
			// not an issue, we can ignore
			if (_navi->at_end()) break;
			if (_navi->cur() <= _ll->length()) break;
			// small wait
			this_thread::sleep_for(chrono::milliseconds(5));
		}

		// step 1. draw everything that is available now.
		{ // lock loglines
			auto lock = _ll->read_lock();
			_parms->total_length = _ll->length();

			fill_display();
			draw_display();
		} // unlock loglines

		// check if still useful to continue
		if (_parms->cur_epoch != _epoch.cur()) return;
		_navi->set_view(_display);

		// step 2. until the epoch advances, keep running to see if
		// searching has found more relevant lines we can render
		// set the endpoints until where we have successfully rendered
		// endpos points to the top and bottom lines in display we
		// havn't computed yet
		size_t endpos[2];
		set_endpos(endpos);
		while (endpos[0] != G::NO_POS || endpos[1] != _display.size()) {
			// abort checks
			if (_parms->cur_epoch != _epoch.cur()) return;

			// allow some progress, then check if new render wanted
			bool progress = false;
			size_t curlines = _ll->length();
			if (curlines != _parms->total_length) {
				_parms->total_length = curlines;
				// we're in tail mode, and new lines have come.
				// sleep a little and keep updating
				if (_navi->cur() == G::NO_POS) {
					this_thread::sleep_for(chrono::milliseconds(50));
					goto reenter;
				}
			}

			auto lock = _ll->read_lock();
			// if centre doesn't exist, draw it, otherwise try to
			// extend either in the directions.
			if (endpos[1] == _parms->radius) {
				if (_parms->cur_epoch != _epoch.cur()) return;
				if (set_centreline()) {
					draw_linenumber(_display[endpos[1]],
                                                        endpos[1]);
					++endpos[1];
					progress = true;
				}
			// otherwise we have a centre, draw the first below line
			} else if (endpos[1] < _display.size()) {
				if (_parms->cur_epoch != _epoch.cur()) return;
				if (set_below_centreline(endpos[1] - _parms->radius)) {
					draw_linenumber(_display[endpos[1]],
							endpos[1]);
					++endpos[1];
					progress = true;
				}
			}
			// if we can do an aboveline as well do it
			if (endpos[0] != G::NO_POS) {
				if (_parms->cur_epoch != _epoch.cur()) return;
				if (set_above_centreline(_parms->radius - endpos[0])) {
					draw_linenumber(_display[endpos[0]],
							endpos[0]);
					--endpos[0];
					progress = true;
				}
			}
			lock.unlock();
			if (!progress) {
				_navi->set_view(_display);
				this_thread::sleep_for(chrono::milliseconds(50));
				// abort checks happen at start of loop
			}
		}
		// we have now rendered all missing lines. set the
		// pageup/pagedown in the navi appropriate for the filtered
		// linue numbers
		if (_parms->cur_epoch != _epoch.cur()) return;
		_navi->set_view(_display);
	}

protected:
	/* tries to reuse previous work when the only change seems to be going
	 * up or down one line */
	virtual void prepare_display(RenderParms* parms) {
		if (_parms.get()) [[likely]] {
			// if only one epoch advanced and it was caused by
			// moving vertically
			if (parms->cur_epoch == _parms->cur_epoch + 1 &&
			   _parms->navi->cur() != _cur &&
			   _cur != G::NO_POS &&
			   _parms->radius * 2 + 1 == _display.size() &&
			   _parms->navi->cur() != G::NO_POS) {
				slide_display(parms->navi->cur());
				return;
			}
		}
		// default is all unset display since anything could have
		// changed
		_display = vector<size_t>(2 * parms->radius + 1, G::NO_POS);
	}

	/* looks in the display for a line at to_pos and recentres display */
	virtual void slide_display(size_t to_pos) {
		vector<size_t> new_display(_display.size(), G::NO_POS);
		size_t index;
		for (index = 0; index < _display.size(); ++index) {
			if (_display[index] == to_pos) break;
		}
		// we didn't find it somehow
		if (index == _display.size()) [[unlikely]] {
			_display = new_display;
			return;
		}
		size_t to_index = _display.size() / 2;
		assert(to_index * 2 + 1 == _display.size());

		size_t from_index = index;
		index = 0;
		while (true) {
			if (from_index + index >= _display.size()) break;
			if (to_index + index >= new_display.size()) break;
			new_display[to_index + index] = _display[from_index + index];
			++index;
		}
		index = 1;
		while (true) {
			if (index > from_index) break;
			if (index > to_index) break;
			new_display[to_index - index] = _display[from_index - index];
			++index;
		}
		_display = new_display;
	}

	// display is a list of numbers, which may have continuous G::NO_POS
	// from the top and bottom. This function sets endpos to the top and
	// bottom indices where this is the case, and to (G::NO_POS,
	// display.size()) otherwise, which are one past the ends
	virtual void set_endpos(size_t endpos[2]) const {
		// fast path, we have a full screen
		if (_display.front() != G::NO_POS &&
		    _display.back() != G::NO_POS) [[likely]] {
			endpos[0] = G::NO_POS;
			endpos[1] = _display.size();
			return;
		}

		// slow path, compute boundary
		endpos[0] = _parms->radius - 1;
		endpos[1] = _parms->radius;
		while (endpos[0] != G::NO_POS && _display[endpos[0]] != G::NO_POS) {
			// we drew to top line
			if (_display[endpos[0]] == 0) endpos[0] = G::NO_POS;
			else --endpos[0];
		}
		while (endpos[1] < _display.size()
		       && _display[endpos[1]] != G::NO_POS) {
			++endpos[1];
		}
	}

	/* TODO: these functions are more than double the size just for handling
	 * context. Should refactor to be simpler, and handle context lines
	 * another way if possible.
	 */

	// computes the loglines position that should be rendered on the
	// centreline and sets it in display. Typically this is navi->cur(),
	// however if we have a filter applied and navi->cur() is not actually
	// rendered for that filter, then it is the next line after that would
	// be rendered. sets it to G::NO_POS if there is no line.
	virtual bool set_centreline() {
		_down_context_end = G::NO_POS;
		_up_context_end = G::NO_POS;
		if (!_parms->total_length) {
			_display[_parms->radius] = G::NO_POS;
			return false;
		}

		_display[_parms->radius] = _navi->cur();
		if (_parms->filter_keywords == G::FILTER_NONE) {
			// pass
		} else if (_parms->filter_keywords == G::FILTER_OR ||
			   _parms->filter_keywords == G::FILTER_AND) {
			size_t search = _display[_parms->radius];
			if (search == G::NO_POS) search = _parms->total_length;
			// slide to nearest
			_display[_parms->radius] = keyword_junction(search,
							      G::DIR_DOWN);
			// e.g. the rest of this function only handles context
			// length. refactor so it is easier to deal with
			if (_parms->context_length) {
				size_t up = keyword_junction(search,
							     G::DIR_UP);
				if (up != G::NO_POS && up + _parms->context_length > search) {
					_up_context_end = safeup(up, _parms->context_length);
					_down_context_end = safedown(up,
								     _parms->context_length,
								     _parms->total_length);
				}
				if (search + _parms->context_length >=
				    _display[_parms->radius]) {
					if (_up_context_end == G::NO_POS)
						_up_context_end =
						    safeup(_display[_parms->radius],
							   _parms->context_length);
					_down_context_end =
                                                safedown(_display[_parms->radius],
                                                         _parms->context_length,
							 _parms->total_length);
					_display[_parms->radius] = search;
				} else if (up != G::NO_POS &&
					   up + _parms->context_length >= search) {
					_display[_parms->radius] = search;
				} else {
					assert(_parms->context_length <
					       _display[_parms->radius]);
					_display[_parms->radius] =
					    safeup(_display[_parms->radius],
						   _parms->context_length);
				}
			}

		}
		return _display[_parms->radius] != G::NO_POS;
	}

	/* sets the value display[CENTER - pos + 1] to be the loglines position
	 * that should be displayed on that position in the screen. Pos is set
	 * to the number of lines above centerline we are displaying. For no
	 * filter it is just the sequential line number until we reach zero. For
	 * a filter we have to compute the next line that matches, unless we
	 * havn't searched all the way for all our keywords in which case we
	 * stop at the first gap and set it to G::NO_POS
	 */
	virtual bool set_above_centreline(size_t pos) {
		assert(pos >= 1);
		if (_parms->filter_keywords == G::FILTER_NONE) {
			if (pos == 1 && _navi->cur() == G::NO_POS && _parms->total_length) {
				_display[_parms->radius - pos] = _parms->total_length - 1;
				return true;
			}
			if (_display[_parms->radius - pos + 1] == G::NO_POS) return false;
			_display[_parms->radius - pos] = _display[_parms->radius - pos + 1] - 1;
			return true;
		} else if (_parms->filter_keywords == G::FILTER_OR ||
			   _parms->filter_keywords == G::FILTER_AND) {
			size_t last_pos = _display[_parms->radius - pos + 1];
			if (_parms->context_length && _up_context_end != G::NO_POS &&
			    last_pos != G::NO_POS && last_pos > _up_context_end) {
				// we are within context. accept line
				_display[_parms->radius - pos] = last_pos - 1;

				if (last_pos - _up_context_end <= _parms->context_length) {
					// in second half of context
					// check for range extend
					size_t next = keyword_junction(
						last_pos, G::DIR_UP);
					if (next != G::NO_POS &&
					    (next > _up_context_end //within context
					    || _up_context_end - next <= _parms->context_length)) { // will overlap
						_up_context_end = safeup(
							next, _parms->context_length);
					}
				}

				if (_up_context_end + 1 == last_pos)
					_up_context_end = G::NO_POS;

				// if last_pos is zero, this is actually -1 so
				// not a line
				return last_pos;
			}
			if (pos == 1) {
				// special cases
				size_t searchpos = _display[_parms->radius];
				// either not found or at end
				if (searchpos == G::NO_POS) searchpos = _navi->cur();
				// at end
				if (searchpos == G::NO_POS) searchpos = _parms->total_length;
				// if still -1, then zero lines
				_display[_parms->radius - pos] =
					keyword_junction(searchpos, G::DIR_UP);

			} else if (_display[_parms->radius - pos + 1] != G::NO_POS) {
				_display[_parms->radius - pos] = keyword_junction(
					_display[_parms->radius - pos + 1], G::DIR_UP);
			}

			if (_display[_parms->radius - pos] != G::NO_POS &&
			    _parms->context_length) {
				_up_context_end = safeup(
					_display[_parms->radius - pos],
					_parms->context_length);

				if (_display[_parms->radius - pos + 1] != G::NO_POS &&
				    _display[_parms->radius - pos + 1] <=
				    _display[_parms->radius - pos] + _parms->context_length) {
					_display[_parms->radius - pos] =
						_display[_parms->radius - pos + 1] - 1;
				} else {
					_display[_parms->radius - pos] = safedown(
						_display[_parms->radius - pos],
						_parms->context_length,
						_parms->total_length);
				}
			}
			return _display[_parms->radius - pos] != G::NO_POS;
		}
		assert(0 && "filter is somehow not set to NONE/AND/OR");
		return false;
	}

	/* Sets display[CENTRE + pos] to store the loglines postion that should
	 * be displayed on that line in the screen. For no filter, it is just
	 * the next line until the end is reached. For a filter applied it
	 * searches for the next appropriate line and displays it. If the search
	 * range has not each the end for all keywords, it sets it to G::NO_POS. */
	virtual bool set_below_centreline(size_t pos) {
		assert(pos >= 1);
		if (_parms->filter_keywords == G::FILTER_NONE) {
			if (_display[pos + _parms->radius - 1] + 1 >= _parms->total_length
			    || _display[pos + _parms->radius - 1] == G::NO_POS)
				return false;
			_display[pos + _parms->radius] =
				_display[pos + _parms->radius - 1] + 1;
			return true;
		} else if (_parms->filter_keywords == G::FILTER_OR ||
			   _parms->filter_keywords == G::FILTER_AND) {
			if (_display[_parms->radius + pos - 1] == G::NO_POS) return false;
			size_t last_pos = _display[_parms->radius + pos - 1];
			if (_parms->context_length &&
			    _down_context_end != G::NO_POS &&
			    last_pos < _down_context_end) {
				_display[_parms->radius + pos] = last_pos + 1;
				if (_down_context_end - last_pos <= _parms->context_length) {
					// we're in second half of context
					// check if the next hit extends range
					size_t next = keyword_junction(last_pos + 1,
								       G::DIR_DOWN);
					if (next < _down_context_end // next is within context
					    || next - _down_context_end <=
					    _parms->context_length) { // next's context will overlap
						_down_context_end =
							safedown(next,
								 _parms->context_length,
								 _parms->total_length);
					}
				}
				if (last_pos + 1 == _down_context_end)
					_down_context_end = G::NO_POS;
				return true;
			}

			size_t next = keyword_junction(
				_display[_parms->radius + pos - 1] + 1, G::DIR_DOWN);
			if (next == G::NO_POS) {
				// no further matches
				_display[_parms->radius + pos] = G::NO_POS;
				return false;
			}
			_down_context_end = safedown(next,
						     _parms->context_length,
						     _parms->total_length);
			if (_parms->context_length > next ||
			    next - _parms->context_length < last_pos + 1) {
				// context brings us to top
				_display[_parms->radius + pos] = last_pos + 1;
			} else {
				// take the first in the context
				_display[_parms->radius + pos] =
					next - _parms->context_length;
			}
			assert (_display[_parms->radius + pos] != G::NO_POS);
			return true;
                }
		assert(0);
		return false;
	}

	// returns the next line to display based on keywords, given that we are
	// at position pos and moving in direction dir.
	virtual size_t keyword_junction(size_t pos, int dir) {
		// if we're at the end already, return no pase
		if (dir == G::DIR_DOWN && pos == _parms->total_length) return G::NO_POS;
		if (_parms->filter_keywords == G::FILTER_AND) {
			return _parms->pins->pinsider(
				pos, dir,
				SearchRange::keyword_conjunction(
					pos, dir, _parms->keywords,
					_parms->total_length));
		}
		assert(_parms->filter_keywords == G::FILTER_OR);
                return _parms->pins->pinsider(pos, dir,
				     SearchRange::keyword_disjunction(
						pos, dir,
						_parms->keywords,
						_parms->total_length));
	}

	/* fill display populates the initial display with the loglines
	 * positions that should be rendered on the screen. If the value cannot
	 * be computed sets it to G::NO_POS so we can display something now and
	 * update it as the searching proceeds.
	 */
	virtual void fill_display() {
		set_centreline();
		for (size_t i = 1; i <= _parms->radius; ++i) {
			if (!set_above_centreline(i))
				break;
		}
		for (size_t i = 1; i <= _parms->radius; ++i) {
			if (!set_below_centreline(i))
				break;
		}
		for (const auto& x : _display) {
			if (x != G::NO_POS) consider_digit_length(x);
		}
	}

	/* Draws the display to the screen. For each line in display, calls
	 * draw_linenumber for it. */
	virtual void draw_display() const {
		if (_parms->tab_data) {
			_parms->tab_data->maybe_evict();
			for (size_t i = 0; i < _display.size(); ++i) {
				if (_display[i] == G::NO_POS ||
				    _display[i] >= _parms->total_length) continue;
				Line* line_object = _ll->get_line_object(
					_display[i]);
				line_object->measure_tabs(_parms->tab_key,
							  _parms->tab_data);
			}
		}
		for (size_t i = 0; i < _display.size(); ++i) {
			if (!draw_linenumber(_display[i], i)) {
				return;
			}
		}
	}

	/* Takes a single line and produces a FormatString for the render queue
	 * to accept.
	 * number: line number in loglines, G::NO_POS for no line to show
	 * pos: line number on the screen
	 * centre: whether the line is the centre one for highlighting
	 * returns true if the render queue accepted the line and false if it
	 * rejected it (due to bad epoch)
	 */
	virtual bool draw_linenumber(size_t number, size_t pos) const {
		FormatString *fs = new FormatString();
		string prefix;
		if (number != G::NO_POS && number < _parms->total_length) {
			if (!_parms->line_numbers_off) {
				stringstream ss;
				zero_pad(number, &ss);
				ss << (number + 1) << " ";
				prefix = ss.str();
			}
			if (_parms->pins->is_pinned(number)) prefix += '*';
			else prefix += ' ';
			fs->add(prefix, 0);
			if (prefix.length() > _parms->cols) [[unlikely]] {
				// unlikely: display too narrow to show data
			} else {
				size_t remain = _parms->cols - prefix.length();
				Line* line_object = _ll->get_line_object_unlocked(number);
				size_t line_length = line_object->render(
					remain,
					pos == _parms->radius,
					_parms->tab_key,
					_navi->tab(),
					_parms->suppressed_tabs,
					_parms->tab_data,
					fs);
				if (pos == _parms->radius) {
					_navi->set_cur_length(prefix.length() +
							      line_length);
				}

			}
			highlight_keywords(fs);
		} else {
			// display the empty line
			prefix = " ~";
			fs->init(prefix);
			highlight_keywords(fs);
		}
		// highlight if we are the centre line and that centre line is
		// also the one navi is on
		if (pos == _parms->radius && number == _navi->cur()) {
			fs->highlight();
		}
		return _renderer->render_queue()->add(fs, pos, _parms->cur_epoch);
	}

	// Applies formatting rules for the line, such as highlighting keywords
        virtual void highlight_keywords(FormatString* fs) const {
                int colour = 0;
                for (const auto& x : _parms->keywords) {
                        fs->mark(Colour::keyword_number_to_colour(colour),
				 (static_cast<string>(*x)));
			++colour;

                }
                if (_parms->colour) fs->colour_function();
        }

	// Move up amount lines from whence safely. Returns 0 if amount is more
	// than whence.
	static size_t safeup(size_t whence, size_t amount) {
		if (whence == G::NO_POS) return whence;
		if (amount > whence) return 0;
		return whence - amount;
	}

	// Move down amount lines starting at whence safely.
	// retuns whence + amount if it is less than len, len - 1 if it is more,
	// and G::NO_POS if it is G::NO_POS
	static size_t safedown(size_t whence, size_t amount, size_t len) {
		if (whence == G::NO_POS) return whence;
		if (amount + whence >= len)
			return len - 1;
		return whence + amount;
	}

	/* gets the number of decimal digits in pos and sets max digits if it is
	 * longer than it */
	virtual void consider_digit_length(size_t pos) {
		// displayed number is 1-indexed but pos is 0-indexed
		++pos;
		size_t len = 1;
		while (pos >= 10) {
			++len;
			pos /= 10;
		}
		if (!_max_digits || *_max_digits < len) {
			_max_digits = len;
		}
	}

	// considers the line numbers that are being displayed and adds zeros to
	// small ones for alignment
	virtual void zero_pad(size_t number, stringstream* ss) const {
		// if we don't have a digit set or it is 1, then still pad the
		// number from 1-9 because they will have to shift as soon as
		// there is a 10th line.
		if (!_max_digits || *_max_digits < 2) {
			if (number + 1 < 10) *ss << '0';
			return;
		}

		// add zeros based on the largest number we will display
		size_t cur = 10;
		for (size_t i = 1; i < *_max_digits; ++i) {
			// number is zero indexed but displayed as one indexed
			if (number + 1 < cur) *ss << '0';
			cur *= 10;
		}
	}

	// viewport mapping lines on the display to the line numbers in
	// loglines.
	vector<size_t> _display;

	// the largest number of digits we have on the display so far for
	// aligning the display
	optional<size_t> _max_digits;

	// pointer to the log lines for getting line data
	LogLines* _ll;

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

	size_t _down_context_end;
	size_t _up_context_end;

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

	// reference to global render epoch, to notice when we are rendering for
	// an expired epoch
	const Epoch& _epoch;

	// remember previous position of navigation for up/down optimization
	size_t _cur;

	// copy of relevant render parameters and pointers to key classes locked
	// in at the start of rendering.
	unique_ptr<RenderParms> _parms;
};

#endif  // __FILTER_RENDERER__H__
