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

#include <mutex>
#include <thread>

#include "colour.h"
#include "constants.h"
#include "curses_wrapper.h"
#include "epoch.h"
#include "format_string.h"
#include "render_parms.h"
#include "render_queue.h"

typedef function<bool(FormatString*, size_t)> TLineCB;

/* Renderer is the class that takes lines to draw and actually ncurses them onto
 * the screen. It has a RenderQueue object where lines that are computed from
 * filterrunner are put into. It tracks the epoch and extracts and draws only
 * lines corresponding to the current epoch. It also has two callback functions,
 * _title_bar and _status_bar, which are set at contruction, and use the
 * interface's context to set correctly.
 */
class Renderer {
public:
	/* takes callbacks to get data for title and status bars and the epoch
	 * instance. gets the screen size and setups the geometry. */
	Renderer(const TLineCB& title_bar,
		 const TLineCB& status_bar,
		 Epoch* epoch)
			: _line_number_off(false),
			  _epoch(epoch), _resting_y(0),
			  _resting_x(0), _cur_epoch(0),
			  _colour(true),
			  _title_bar(title_bar), _status_bar(status_bar) {
		unique_lock<mutex> ul(_m);
		_rows = 0;
		_cols = 0;
		setup_screen();
	}

	/* joins threads and ends ncurses */
	virtual ~Renderer() {
		_renderer_thread->join();
		_redrawer_thread->join();
		CursesWrapper::end_win();
	}

	// starts the renderer and the redrawer. renderer gets computed lines
	// from the filter runner and puts them in the right position. These can
	// come continually as there may be ongoing searches for keyword matches
	// that havn't completed. The redrawer on epoch changes draws the title
	// and status bars
	virtual void start() {
		_renderer_thread.reset(new thread(&Renderer::renderer, this));
		_redrawer_thread.reset(new thread(&Renderer::redrawer, this));
	}

	/* instance of the render queue to insert lines for the renderer */
	virtual RenderQueue* render_queue() {
		return &_rq;
	}

	/* setup the screen and the colours */
	virtual void setup_screen() {
		// get curses lock and call init functions
		unique_lock<mutex> ul(CursesWrapper::get_mutex());
		set_escdelay(25);
		initscr();
		Colour::init_curses_colours();
		keypad(stdscr, true);
		noecho();
		cbreak();
		setup_screensize();
	}

	/* returns the radius, which is the number of lines above and below the
	 * centre bar but not the status and title bars */
	virtual size_t radius() const {
		return _radius;
	}

	/* turn on and off colouring of lines */
	virtual void colour_toggle() {
		_colour = !_colour;
	}

	/* turn on and off rendering of line numbers */
	virtual void line_numbers_toggle() {
		_line_number_off = !_line_number_off;
	}

	/* returns whether line numbers are not rendered */
	virtual bool line_numbers_off() const {
		return _line_number_off;
	}

	/* returns the width of the screen */
	virtual size_t cols() const {
		unique_lock<mutex> ul(_m);
		return _cols;
	}

	/* fills out the RenderParms struct with the relevant information in
	 * this class */
	virtual void set_render_parms(RenderParms* rp) {
		rp->colour = _colour;
		rp->line_numbers_off = line_numbers_off();
		rp->cols = cols();
		rp->radius = radius();
	}

protected:
	/* uses ioctl to get the scree size. sets up the geometry based on that.
	 * resting_xy is where we keep the cursor.
	 * */
	virtual void setup_screensize() {
		struct winsize size;
		if (ioctl(0, TIOCGWINSZ, (char*) &size) < 0) {
			// error
		} else if (_cols != size.ws_col || _rows != size.ws_row) {
			_cols = size.ws_col;
			_rows = size.ws_row;
			G::h_shift(_cols / 2);
			compute_geometry();
			_resting_y = 0;
			_resting_x = 0;
		}
	}

	// computes radius based on screen size and where the status line goes
	virtual void compute_geometry() {
		size_t half = _rows - 5;
		if (half % 2) --half;
		_radius = half >> 1;
		_status_line = 2 * _radius + 3;
	}

	/* renderer draws lines that take some time to compute
	 *  e.g., because a search on a huge file is not completed
	 * new lines appear in the render queue. */
	virtual void renderer() {
		while (!ExitSignal::check(false)) [[likely]] {
			// wait until there is a line we may want to render
			while (_rq.empty()) {
				_rq.wait_for_work();
				if (ExitSignal::check(false)) [[unlikely]] return;
			}
			unique_lock<mutex> ul(_m);
			// extract the line from the render queue
			size_t pos = G::NO_POS;
			FormatString* fs = nullptr;
			size_t i = 0;
			while (_rq.remove(_cur_epoch, &pos, &fs)) {
				// we got a line for this epoch. draw it
				draw(pos + 2, 0, *fs);
				delete fs;
				++i;
			}
			// if we actually rendered something, refresh
			if (i) CursesWrapper::refresh();
		}
	}

	/* redrawer draws title and status. It waits on an epoch change, and
	 * then triggers a redrawing */
	virtual void redrawer() {
		while (!ExitSignal::check(false)) [[likely]] {
			unique_lock<mutex> ul(_m);
			// note the current epoch, if its still the same when
			// we finish wait for it to advance
			_cur_epoch = _epoch->cur();
			// check for screen change
			setup_screensize();

			// unlock and get data from interface
			ul.unlock();
			// runs the callbacks for status and title
			FormatString title;
			FormatString status;
			_title_bar(&title, _cols);
			_status_bar(&status, _cols);

			// lock and draw
			ul.lock();

			draw(0, 0, title);
			_resting_x = 0;
			_resting_y = 0;
			draw(_status_line + 1, 0, status);
			// gets where we left the cursor on the status bar. if
			// we draw something else we want to go back to this
			// position
			CursesWrapper::get_yx(&_resting_y, &_resting_x);
			CursesWrapper::refresh();

			// wait until epoch advances past the valid it was when
			// we started the render
			while (!ExitSignal::check(false) && _cur_epoch == _epoch->cur()) {
				_epoch->wait(&ul);
			}
		}
	}

	static optional<string> move_to_number(const string& in) {
		size_t i = 0;
		while (i < in.size()) {
			if (in[i] >= '0' && in[i] <= '9') return in.substr(i);
			++i;
		}
		return nullopt;
	}

	size_t parse_ansi(const FormatString& data, size_t pos) const {
		assert(data[pos] == 0x1b);
		if (pos + 2 >= data.size()) return pos;

		if (data[pos + 1] != '[') return pos;
		size_t end = data.find('m',  pos);
		if (end == string::npos) return pos;
		optional<string> format = move_to_number(
			data.substr(pos, end - pos));
		size_t subpos;
		while (format) {
			try {
			 	int val = stoi(*format, &subpos);
				int attr = 0;
				bool mode = false;
				if (val == 4) {
					attr |= A_UNDERLINE;
					mode = true;
				} else if (val == 2) {
					attr |= A_BOLD;
					mode = true;
				} else if (val == 24) {
					attr |= A_UNDERLINE;
					mode = false;
				} else if (val == 22) {
					attr |= A_BOLD;
					mode = false;
				} else if (val == 0) {
					attr = A_UNDERLINE | A_BOLD;
					mode = false;
				}
				CursesWrapper::attr(attr, mode);
				format = move_to_number(format->substr(subpos));

				// handle
			} catch (...) {
				break;
			}
		}
		return end;
	}

	/* draws the FormatString data to the screen at postions in the
	 * parameters y and x. */
	virtual void draw(size_t y, size_t x, const FormatString& data) const {
		CursesWrapper::move_yx(y, x);
                size_t screen_i = 0;
                for (size_t i = 0; i < data.length(); ++i, ++screen_i) {
                        if (screen_i >= _cols) break;
                        int code = data.code(i);
                        char c = data.at(i);

                        if (_colour) {
				int attr = COLOR_PAIR(code % G::LOWEST_MODIFIER);
                                if (code & G::BOLD) attr |= A_BOLD;
				if (code & G::UNDERLINE) attr |= A_UNDERLINE;
				CursesWrapper::attr(attr, true);
                        }

			// replace non tab whitespace with space
                        if (c == '\f' || c == '\n' || c == '\r') c = ' ';
			// implement tab by clearing until 8-aligned
			if (c == '\t') {
				for (size_t clearpos = 0;
				     clearpos < 8 - (screen_i % 8);
				     ++clearpos) {
					CursesWrapper::add_ch(y, screen_i +
							      clearpos, ' ');
				}
				screen_i += 8 - (screen_i % 8);
			}
			// suppress unprintable characters
                        if (!isprint(c)) c = ' ';
			// actually draw the char
			CursesWrapper::add_ch(y, screen_i, c);
			// if we turned on an attr, turn it off
                        if (_colour) {
				int attr = COLOR_PAIR(code % G::LOWEST_MODIFIER);
                                if (code & G::BOLD) attr |= A_BOLD;
				if (code & G::UNDERLINE) attr |= A_UNDERLINE;
				CursesWrapper::attr(attr, false);
                        }
                }
		// clear the rest of the line
                if (screen_i < _cols) CursesWrapper::clear_eol();
		// put the cursor back to where we want it
		if (_resting_y || _resting_x)
			CursesWrapper::move_yx(_resting_y, _resting_x);
        }

	// the render queue for computed lines as they arrive
	RenderQueue _rq;

	// thread safety
	mutable mutex _m;

	// thread that draws lines as they are computed
	unique_ptr<thread> _renderer_thread;

	// thread that draws the title and status bar on epoch change
	unique_ptr<thread> _redrawer_thread;

	// current radius, meaning lines above centreline we display
	atomic<size_t> _radius;

	// the position of the status line
	size_t _status_line;

	// whether line numbers should be rendered
	atomic<bool> _line_number_off;

	// pointer to current epoch
	Epoch *_epoch;

	// screen size in rows
	size_t _rows;

	// screen size in columns
	size_t _cols;

	// cursor x-y position at end of statusbar so we can return it
	size_t _resting_y;
	size_t _resting_x;

	// the current epoch we are drawing. set by the redrawer thread when it
	// starts, and used to know when to trigger a redraw and for the
	// renderer thread to only accept lines for this epoch
	atomic<size_t> _cur_epoch;

	// whether we should have colour in our display
	atomic<bool> _colour;

	// callback for rendering the title bar
	TLineCB _title_bar;

	// callback for rendering the status bar
	TLineCB _status_bar;
};

#endif  // __RENDERER__H__
