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

#include <sys/ioctl.h>
#include <stdio.h>

#include <list>
#include <map>
#include <string>
#include <sstream>
#include <vector>

#include <ncurses.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/un.h>
#include <sys/time.h>
#include <sys/resource.h>

#include "config.h"
#include "constants.h"
#include "curses_wrapper.h"
#include "epoch.h"
#include "key_groups.h"
#include "log_lines.h"
#include "match.h"
#include "navigation.h"
#include "renderer.h"
#include "run.h"
#include "story.h"
#include "typing_line.h"
#include "version.h"
#include "view_stack.h"

using namespace std;
using namespace std::placeholders;

/* Operates the user interface to logserver, calling getch() and processing the
 * event. It holds a ViewStack instance, which holds the data structure where
 * all non-interface events are processed, and holds the renderer and
 * epoch instances. */
class Interface {
public:
	// constructed with the initial LogLines instances
	explicit Interface(LogLines* ll)
			: _state(0),
			  _type(nullptr),
			  _no_d(true),
			  _type_offset(0),
			  _mem_timer(0) {
		unique_lock<mutex> ul(_m);
		_state = COMMAND;
		if (freopen("/dev/tty", "rw", stdin) == nullptr) {
			throw G::errno_string("cannot reopen /dev/tty", errno);
		}
		_renderer.reset(new Renderer(
			bind(&ViewStack::title_bar, &_views, _1, _2),
			bind(&Interface::status_bar, this, _1, _2),
			&_epoch));
		_views.init(_renderer.get(), &_epoch);
		_views.push(ll);
		_renderer->start();
	}

	/* signal we are exiting in case we havn't yet, and advance the epoch to
	 * trigger any waiting threads */
	virtual ~Interface() {
		ExitSignal::check(true);
		_epoch.shutdown();
	}

	/* fuzzes the interface for testing purposes */
	virtual void fuzz(size_t millis) {
		srand(time(NULL));
		_views.check();
		_epoch.advance();
		int ch = 0;
		while (!ExitSignal::check(false)) [[likely]] {
			// get an appropriate character to enter
			if (_type.get()) ch = KeyGroups::random_type();
			else ch = KeyGroups::random_action();
			process(ch);
			_epoch.advance();
			this_thread::sleep_for(chrono::milliseconds(millis));
		}
	}

	/* man looping thread. gets a char, processes it, and checks if we are
	 * exiting */
	virtual void run() {
		_views.check();
		_epoch.advance();
		int ch = 0;
		while (!ExitSignal::check(false)) [[likely]] {
			ch = CursesWrapper::getch_blocking();
			process(ch);

			/* when pasting in data keep reading characters and
			 * sending without doing a re-render.
			 * if we see a newline (enter) ignore it and everything
			 * after, so user has to deliberately hit enter on a
			 * pipe command */
			bool keepsending = true;

			while (true) {
				ch = CursesWrapper::getch_nonblocking();
				if (ch == ERR) break;
				if (ch == '\n') keepsending = false;
				try {
					if (keepsending) process(ch);
				} catch (const logic_error& le) {
				} catch (const runtime_error& re) {
					// TODO: this have are due to running
					// commands on an invalid line. add
					// info to a debug log to fix
				}
			}
			_epoch.advance();
		}
	}

protected:
	/* callback function for the renderer to draw the status bar */
	virtual bool status_bar(FormatString* fs, size_t cols) {
		unique_lock<mutex> ul(_m);

		stringstream ss;
		size_t highlight = 0;
		if (_state == COMMAND) {
			size_t len = _views.fm()->length();
			ss << "[COMMAND]        "
			   << _views.fm()->mode_string() << "  ";
			if (len != G::NO_POS) ss << len << " lines";
			else ss << "    ";
			if (_views.ll()->eof()) ss << " (eof).";
			else ss << ".";
			ss << " ";
			for (size_t i = 0; i < _views.fm()->total_context(); ++i) {
				if (i == 10) break;
				ss << '+';
			}
			ss << "  keywords: ";
		} else if (_state == TYPE_LINENO) {
			ss << " :";
			highlight = ss.str().length();
			ss << _type->typed();
		} else if (_state == TYPE_PIPE) {
			ss << " | ";
			highlight = ss.str().length();
			ss << _type->typed();
		} else if (_state == TYPE_MATCH) {
			ss << "> ";
			highlight = ss.str().length();
			if (_type_offset) ss << "!";
			ss << _views.fm()->current_keyword();
		} else if (_state == TYPE_COMMENT) {
			ss << "[enter comment] ";
			highlight = ss.str().length();
			ss << _type->typed();
		} else if (_state == TYPE_SAVE || _state == TYPE_SAVELINE) {
			ss << "[filename] ";
			highlight = ss.str().length();
			ss << _type->typed();
		} else if (_state == TYPE_EDIT) {
			ss << "[edit " << _views.navi()->cur() << "] ";
			highlight = ss.str().length();
			ss << _type->typed();
		} else if (_state == TAB_KEY) {
			ss << "[tab for char] ";
			highlight = ss.str().length();
		} else if (_state == BREAK_KEY) {
			ss << "[break line on char] ";
			highlight = ss.str().length();
		}

		ss << " ";
		fs->add(ss.str(), 0);
		if (typing_state()) {
			if (highlight && _type.get()) {
				assert(highlight <= ss.str().length());
				highlight += _type->get_pos() + _type_offset;
				fs->cursor(highlight);
			} else {
				highlight = 0;
			}
		}
		fs->add(_views.fm()->keyword_string());
		string usage = mem_usage();
		size_t offset = cols - 5 - usage.length();
		fs->align(offset);
		fs->add(" ", 7);
		fs->add(usage, 7);
		return true;
	}

	/* returns a string for the memory usage */
	virtual string mem_usage() {
#ifdef __APPLE__
		return "";
#endif
		// don't poll every render
		if (!_mem_timer) {
			getrusage(RUSAGE_SELF, &_usage);
			_mem_timer = G::USAGE_TIMER;
		}
		--_mem_timer;
		stringstream ss;
		ss << (_usage.ru_maxrss / 1024) << " MiB";
		return ss.str();
	}

	/* process a keystroke. A static map of functions handles all the ones
	 * that we can call directly without arguments in this class or
	 * ViewStack. Similarly, the navigation instance has a function that
	 * checks if it is a keystroke it handles, like arrow directions.
	 * Otherwise a switch statement handles the other events */
	virtual void process(int ch) {
		static map<int, function<void()>> functions;
		unique_lock<mutex> ul(_m);

		if (functions.empty()) [[unlikely]] {
			// functions involving interface
			functions['|'] = bind(&Interface::start_pipe, this);
			functions[':'] = bind(&Interface::start_lineno, this);
			functions['/'] = bind(&Interface::start_match, this,
					      Match::PRESENT);
			functions['#'] = bind(&Interface::start_comment, this);
			functions['e'] = bind(&Interface::start_edit, this);
			functions['S'] = bind(&Interface::start_save, this);
			functions['s'] = bind(&Interface::start_saveline, this);
			functions['^'] = bind(&Interface::start_match, this,
					      Match::PRESENT |
					      Match::ANCHOR_LEFT);
			functions['$'] = bind(&Interface::start_match, this,
					      Match::PRESENT |
					      Match::ANCHOR_RIGHT);
			functions['\\'] = bind(&Interface::start_match, this,
					       Match::MISSING);
			functions['i'] = bind(&Interface::info, this, false);

			// functions involving view stack
			functions['f'] = bind(&ViewStack::follow, &_views, false);
			functions['F'] = bind(&ViewStack::follow, &_views, true);
			functions['!'] = bind(&ViewStack::insert_dash_line,
					      &_views);
			functions['\b'] =
				functions[127] =
				functions[KEY_BACKSPACE] =
				bind(&ViewStack::backspace, &_views);
			functions['b'] = bind(&ViewStack::break_line, &_views);
			functions[27] = bind(&ViewStack::pop, &_views);
			functions['h'] = bind(&ViewStack::help, &_views);
			functions['%'] = bind(&ViewStack::permafilter, &_views);
			functions['\n'] = bind(&ViewStack::enter, &_views);
			functions['C'] = bind(&ViewStack::clear, &_views);
			functions['<'] = bind(&ViewStack::lshift, &_views);
			functions['>'] = bind(&ViewStack::rshift, &_views);
			functions['*'] = bind(&ViewStack::pin_line, &_views);
			functions['m'] = bind(&ViewStack::merge, &_views);
			functions['t'] = bind(&ViewStack::tab_toggle, &_views);
			functions['1'] = bind(&ViewStack::tab_suppress, &_views, 0);
			functions['2'] = bind(&ViewStack::tab_suppress, &_views, 1);
			functions['3'] = bind(&ViewStack::tab_suppress, &_views, 2);
			functions['4'] = bind(&ViewStack::tab_suppress, &_views, 3);
			functions['5'] = bind(&ViewStack::tab_suppress, &_views, 4);
			functions['6'] = bind(&ViewStack::tab_suppress, &_views, 5);
			functions['7'] = bind(&ViewStack::tab_suppress, &_views, 6);
			functions['8'] = bind(&ViewStack::tab_suppress, &_views, 7);
			functions['9'] = bind(&ViewStack::tab_suppress, &_views, 8);
			functions['0'] = bind(&ViewStack::tab_suppress, &_views, 9);
		}

		// handle interface state
		// reset info frustration
		if (ch != 'i' && ch != 'I') _frustration = -1;
		if (ch != 'd') _no_d = true;

		// Check if we are in a different state that command mode, where
		// keystrokes are, e.g., typing a search string.
		if (_state == TYPE_MATCH) {
			assert(_type.get());
			if (_type->process(ch)) {
				finish_type();
				_type_offset = 0;
			} else {
				_views.fm()->current_type(_type->typed());
			}
		} else if (typing_state()) {
			assert(_type.get());
			if (_type->process(ch)) finish_type();
		} else if (waitchar_state()) {
			waitchar_finish(ch);
		} else if (_state == COMMAND) {
			/* These [[unlikely]] tags aren't necessarily true, but
			 * if they are true, then we go back to waiting on
			 * input, so we might as well pessimistically assume our
			 * work will continue. */
			// If command requires a line but we aren't on one skip
			if (KeyGroups::requires_line(ch) &&
			    (_views.navi()->at_end()
			     || _views.ll()->length() == 0)) [[unlikely]] return;

			// We are in command mode, process the keystroke
			// appropriately. First see if navigation handles it
			if (_views.navi()->process(ch)) [[unlikely]] return;

			// Second see if it is in our list of functions
			if (functions.count(ch)) [[unlikely]] {
				functions.at(ch)();
				return;
			}

			// handle remaining characters
			switch (ch) {
			case 'q':
				ExitSignal::check(true);
				_epoch.shutdown();
				break;
			case 337:
				_views.navi()->goto_line(
					_views.fm()->keyword_slide(
						G::DIR_UP),
					false);
				break;
			case 336:
				_views.navi()->goto_line(
					_views.fm()->keyword_slide(
						G::DIR_DOWN),
					false);
				break;
			case KEY_SRIGHT:
				_views.navi()->goto_pos(
					_views.fm()->find_next_match());
				break;
			case KEY_SLEFT:
				_views.navi()->goto_pos(
					_views.fm()->find_prev_match());
				break;
			case '\t':
				_views.fm()->toggle_mode();
				break;
			case '+':
				_views.fm()->add_context();
				break;
			case '-':
				_views.fm()->remove_context();
				break;
			case 'n':
				_renderer->line_numbers_toggle();
				break;
			case 'c':
				_renderer->colour_toggle();
				break;
			case 'B':
				_state = BREAK_KEY;
				break;
			case 'T':
				_state = TAB_KEY;
				break;
			case 'd':
				if (_no_d) _no_d = false;
				else if (!_views.navi()->at_end()) {
					_views.ll()->remove(_views.navi()->cur());
					_views.navi()->up();
					_no_d = true;
				}
				break;
			}
		}
	}

	/* user hit 'i' or 'I' for line intelligence. Handle it and track
	 * frustation */
	virtual void info(bool full) {
		if (_views.ll()->length() == 0) return;
		_views.ll()->info(_views.navi()->cur(), full, ++_frustration);
	}

	/* User hit either enter or escape, to accept or reject the current
	 * typing line state. Handle the result appropriately based on what
	 * started the typing line.
	 */
	virtual void finish_type() {
		bool accepted = _type->result();
		const string& typed = _type->typed();

		if (_state == TYPE_MATCH) {
			// finish a keyword search
			_views.finish_match(accepted);
		} else if (_state == TYPE_PIPE) {
			// run a pipe command and push a new loglines
			if (accepted && !Tokenizer::trim(typed, " |\t\n").empty()) {
				_views.run_pipe_command(typed);
			}
		} else if (_state == TYPE_LINENO) {
			// just to a line number
			if (accepted) {
				try {
					size_t lineno = stoi(typed.c_str());
					if (lineno) --lineno;
					if (lineno > _views.ll()->length())
						lineno = _views.ll()->length() - 1;
					_views.navi()->goto_line(lineno, true);
				} catch (const logic_error& lg) {}
			}
		} else if (_state == TYPE_COMMENT) {
			// add a comment to the story log
			if (accepted) {
				set<size_t> view;
				if (!_views.fm()->is_mode_all())  {
					_views.fm()->get_view(&view);
				}

				_story.write(typed, _views.ll(),
					     _views.navi(), view);
			}
		} else if (_state == TYPE_SAVE) {
			// save filename
			if (accepted) {
				_views.ll()->save(typed);
			}
		} else if (_state == TYPE_SAVELINE) {
			// save filename
			if (accepted) {
				_views.ll()->save_line(typed, _views.navi()->cur());
			}
		} else if (_state == TYPE_EDIT) {
			// edit the line, replace it in loglines
			if (accepted) {
				_views.ll()->set_line_unlocked(_views.navi()->cur(), typed);
			}
		}
		_type.reset(nullptr);
		_state = COMMAND;
	}

	/* start a save typing line */
	virtual void start_save() {
		_state = TYPE_SAVE;
		_type.reset(new TypingLine());
	}

	/* start a editline typing line */
	virtual void start_edit() {
		assert(_views.ll()->length() != 0);
		assert(!_views.navi()->at_end());
		_state = TYPE_EDIT;
		_type.reset(new TypingLine());
		_type->set_type(_views.ll()->get_line_unlocked(_views.navi()->cur()));
	}

	/* start a saveline typing line */
	virtual void start_saveline() {
		assert(_views.ll()->length() != 0);
		assert(!_views.navi()->at_end());
		_state = TYPE_SAVELINE;
		_type.reset(new TypingLine());
	}

	/* start a comment typing line */
	virtual void start_comment() {
		assert(_views.ll()->length() != 0);
		assert(!_views.navi()->at_end());
		_state = TYPE_COMMENT;
		_type.reset(new TypingLine());
	}

	/* start a pipe command typing line */
	virtual void start_pipe() {
		_state = TYPE_PIPE;
		_type.reset(new TypingLine());
	}

	/* start a line number jump typing line */
	virtual void start_lineno() {
		_state = TYPE_LINENO;
		_type.reset(new TypingLine());
		_type->only_numbers();
	}

	/* Returns true if we are currently in a typing line state where
	 * keystrokes are meant to edit the typing line */
	virtual bool typing_state() const {
		return _state == TYPE_COMMENT
		    || _state == TYPE_MATCH
		    || _state == TYPE_PIPE
		    || _state == TYPE_LINENO
		    || _state == TYPE_SAVE
		    || _state == TYPE_SAVELINE
		    || _state == TYPE_EDIT;
	}

	/* Returns true if we are waiting on a single character to complete the
	 * event, such as break-with-particular-char. */
	virtual bool waitchar_state() const {
		return _state == BREAK_KEY || _state == TAB_KEY;
	}

	/* Handle arrival of a char in the waitchar state */
	virtual void waitchar_finish(char c) {
		if (_state == BREAK_KEY) {
			_views.ll()->split(_views.navi()->cur(), c);
		} else if (_state == TAB_KEY) {
			_views.tab_key(c);
		} else {
			assert(0);
		}
		_state = COMMAND;
	}

	/* Begin a new string match event, with the anchor and reverse flags
	 * based on how it was launched */
	virtual void start_match(int match_criteria) {
		_state = TYPE_MATCH;
		// TODO simplify this. if true it adds an extra space to account
		// for the bang before search time but logic shouldn't be so
		// coupled here
		_type_offset = match_criteria & Match::MISSING;
		_views.fm()->start_match(match_criteria,
					 _views.navi()->cur());
		_type.reset(new TypingLine());
	}

	// current state of the interface controls how keystrokes are processed
	int _state;

	/* states for logserver interface */
	static constexpr int COMMAND = 0;
	static constexpr int TYPE_MATCH = 1;
	static constexpr int TYPE_COMMENT = 2;
	static constexpr int TYPE_PIPE = 3;
	static constexpr int TYPE_LINENO = 4;
	static constexpr int TYPE_SAVE = 5;
	static constexpr int TYPE_SAVELINE = 6;
	static constexpr int BREAK_KEY = 7;
	static constexpr int TYPE_EDIT = 8;
	static constexpr int TAB_KEY = 9;
	/* end states */

	// main epoch class, last to be destructed since worker threads will
	// check the epoch during exit checks
	Epoch _epoch;

	// renderer object, behaviour can be changed by interface
	unique_ptr<Renderer> _renderer;

	// holds all the loglines, filterers, and navigation
	ViewStack _views;

	// if nullptr, we are not in a typing state, otherwise holds the
	// instance of what the typing line is
	unique_ptr<TypingLine> _type;

	// for thread safety when the renderer invokes the status and title bar
	// callbacks
	mutex _m;

	// memory usage, reuses old result and updates periodically
	struct rusage _usage;


	// how many times the user has pressed 'i' to relax heuristics
	int _frustration;

	// whether last char was a 'd', for the 'dd' delete line command
	bool _no_d;

	// used when adding comments based on exploration
	Story _story;

	// stores offset for a typing line that is beyond default of 2
	int _type_offset;

	// couter to check memory usage
	int _mem_timer;

};

#endif  //  __INTERFACE__H__
