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

#include <sys/time.h>
#include <sys/socket.h>
#include <time.h>

#include <algorithm>
#include <cassert>
#include <regex>
#include <map>
#include <memory>
#include <shared_mutex>
#include <sstream>
#include <string>
#include <thread>
#include <tuple>
#include <vector>

#include <poll.h>

#include "base64.h"
#include "config.h"
#include "constants.h"
#include "directory_line_provider.h"
#include "event_bus.h"
#include "fd_line_provider.h"
#include "file_line_provider.h"
#ifdef __USE_GEB__
#include "gdb_line_provider.h"
#include "gdb_node_line_provider.h"
#endif  // __USE_GEB__
#include "huge_vector.h"
#include "i_line_provider.h"
#include "line.h"
#include "line_intelligence.h"
#include "match.h"
#include "run.h"
#include "static_demo.h"
#include "static_help.h"
#include "xref.h"

using namespace std;

/* LogLines stores and controls access to the actual lines of data being
 * displayed. This includes operations on lines such as editing, breaking, and
 * decoding, and supporting xrefs for lines. Each log lines has a line provider,
 * which sources the lines themselves and adds them to the log, or is statically
 * prebuilt with the relevant lines.
 */
class LogLines : public ILogLines {
public:
	// default constructor, used in this class for custom initializing
	LogLines() : _type(LL_NONE) {
		init();
	}

	/* static log lines that displays the lines in parameter lines. */
	LogLines(const vector<string>& lines, const string& display_name)
			: _display_name(display_name), _type(LL_SYNTHETIC) {
		init();
		for (const auto& x : lines) {
			add_line(x);
		}
	}

	/* creates a loglines based on the contents of a filename in parameter
	 * name */
	explicit LogLines(const string& name)
	    : _filename(name), _display_name(name) {
		if (filesystem::is_directory(name)) {
			_type = LL_DIRECTORY;
			init();
			_lp.reset(new DirectoryLineProvider(this, name));
			_lp->start();
		} else {
			_type = LL_FILE;
			init();
			_lp.reset(new FileLineProvider(this, name));
			_lp->start();
		}
	}

	/* create a log lines by taking a file descriptor and streaming its
	 * lines to the loglines. This occurs when something is piping into
	 * logserver at startup */
	explicit LogLines(int fd) : _display_name("stdin"),
			            _type(LL_STDIN) {
		init();
		_lp.reset(new FDLineProvider(this, fd));
		_lp->start();
	}

	/* construct a loglines by running a command feeding an old loglines
	 * into it and producing the output as a new loglines */
	explicit LogLines(Run* run) : _display_name(run->command()),
			              _type(LL_PIPE) {
		init();
		_runner.reset(run);
		_lp.reset(new FDLineProvider(this, _runner->read_fd()));
		_lp->start();
	}

#ifdef __USE_GEB__
	LogLines(TGraph* gdb, const string& display_name, TGraph::Node* where)
			: _display_name(display_name),
			  _gdb(gdb) {
		// if displayname is not empty we are showing a file using the
		// next and prev relation. otherwise we are splaying out a node
		if (display_name.empty()) {
			assert(where);
			_type = LL_GDB_NODE;
			init();
			_lp.reset(new GdbNodeLineProvider(this, gdb, where));
		} else {
			_type = LL_GDB;
			init();
			_lp.reset(new GdbLineProvider(this, gdb, where));
		}
		_lp->start();
	}
#endif  // __USE_GEB__

	/* shared initialization for all constructors */
	void init() {
		unique_lock<shared_mutex> ul = write_lock();
#ifdef __USE_GEB__
		_gdb = nullptr;
#endif  // __USE_GEB__
		_lines.clear();
		EventBus::_()->clear_lines(this);
		_eof = false;
	}

	// default destructor
	virtual ~LogLines() {
		EventBus::_()->eventmaker_finished(this);

		unique_lock<shared_mutex> ul = write_lock();
		_lp.reset(nullptr);
		_lines.clear();
	}

	/* create a log lines consisting of the static data for a static page,
	 * like the help screen */
	static LogLines* show_static(const string& name) {
		unique_ptr<LogLines> ret = make_unique<LogLines>();
		ret->_type = LL_STATIC;
		ret->_display_name = name;

		if (name == StaticHelp::NAME)
			StaticHelp::render(ret.get());
		if (name == StaticDemo::NAME)
			StaticDemo::render(ret.get());
		return ret.release();
	}

	/* returns true if this is a static page like the help screen */
	virtual bool is_static(const string& name) const {
		if (_type != LL_STATIC) return false;
		return _display_name == name;
	}

	// locks and returns the line at parameter pos
	virtual inline string_view get_line_unlocked(size_t pos) const {
		shared_lock<shared_mutex> ul(_m);

		return get_line(pos);
	}

	virtual inline Line* get_line_object_unlocked(size_t pos) const {
		shared_lock<shared_mutex> ul(_m);

		return get_line_object(pos);
	}

	virtual inline Line* get_line_object(size_t pos) const {
		return _lines.at(pos).get();
	}

	// returns the line at parameter pos. requires a read_lock() held
	virtual inline string_view get_line_locked(size_t pos) const {
		return get_line(pos);
	}

	// sets the line at pos to the value val
	virtual inline void set_line_unlocked(size_t pos, const string& val) {
		unique_lock<shared_mutex> ul = write_lock();

		return set_line(pos, val);
	}

	// appends a batch of new Lines to the log lines
	virtual void add_lines(list<Line*>* lines) override {
		unique_lock<shared_mutex> ul = write_lock();
		while (lines->size()) {
			Line* line = lines->front();
			lines->pop_front();
			_lines.add(unique_ptr<Line>(line));
			EventBus::_()->append_line(this, _lines.length() - 1,
						   line);
		}
	}

	// appends a new Line to the log lines
	virtual size_t add_line(Line* line) override {
		unique_lock<shared_mutex> ul = write_lock();
		_lines.add(unique_ptr<Line>(line));
		EventBus::_()->append_line(this, _lines.length() - 1,
					   line);
		return _lines.length() - 1;
	}

	// adds a new string to the log lines
	virtual size_t add_line(const string& line) override {
		unique_lock<shared_mutex> ul = write_lock();
		Line* new_line = new Line(line);
		_lines.add(unique_ptr<Line>(new_line));
		EventBus::_()->append_line(this, _lines.length() - 1,
					   new_line);
		return _lines.length() - 1;
	}

	/* search [start, end) with the matching parameters */
	virtual size_t range_add_if(size_t start, size_t end,
				    set<size_t>* results,
				    function<bool(const string_view&)> fn) const {
		shared_lock<shared_mutex> ul(_m);
		return _lines.range_add_if(start, end, results, fn);
	}

	// inserts a line at a specific position in the log lines */
	virtual size_t insert_line(const string& line,
				   size_t pos) {
		unique_lock<shared_mutex> ul = write_lock();
		Line* new_line = new Line(line);
		_lines.insert(unique_ptr<Line>(new_line), pos);
		EventBus::_()->insertion(this, pos, 1);
		EventBus::_()->edit_line(this, pos, new_line);
		return pos;
	}

	virtual pair<size_t, size_t> get_range_unlocked(size_t pos, size_t radius) {
		size_t start, end;
		if (pos < radius) start = 0;
		else start = pos - radius;

		if (!length()) return make_pair(0, 0);

		if (pos + radius >= length())
			end = length() - 1;
		else
			end = pos + radius;

		return make_pair(start, end);
	}

	// TODO: refactor, only info a line. I should start a thread that infos
	// all the lines.
	virtual void info(size_t pos, bool full, int frustration) {
		if (pos == G::NO_POS) return;
		if (full) {
			pair<size_t, size_t> range = get_range_unlocked(
				pos, 20 * (frustration + 1));
			for (size_t i = range.first; i < range.second; ++i) {
				info(i, false, frustration >> 1);
			}
			return;
		}

		string_view line;
		{
			shared_lock<shared_mutex> ul(_m);
			line = get_line(pos);
		}
		optional<string> ret = LineIntelligence::apply_heuristics(
			line, frustration);
		if (ret) {
			unique_lock<shared_mutex> ul = write_lock();
			set_line(pos, *ret);
			split_locked(pos, '\n');
		}
	}

	/* gives the line at position pos, is there an xref to follow? if there
	 * is more than one, or display_all is true, then create a synthetic
	 * loglines that lists those options. Otherwise jump to the unique
	 * target. Returns a pair of loglines and line number, or (nullptr, 0)
	 * if there is no xref to follow */
	virtual pair<LogLines*, size_t> follow(size_t pos,
					       bool display_all) {
		// jumps have to have a valid line
		if (pos == G::NO_POS) return make_pair(nullptr, 0);

		// special handler to perform the jump to the demo page in help
		if (_type == LL_STATIC && _display_name == StaticHelp::NAME) {
			const string_view& line = get_line(pos);
			if (line.find("@DEMOTIME") != string::npos) {
				return make_pair(show_static("demo"), 0);
			}
		}

		// use the xref to get the jump
		auto ret = follow_impl(pos, display_all);

		/* signal on the synthetic page that we have taken this path, to
		 * help with analysis of all options */
		if (ret.first != nullptr && _type == LL_SYNTHETIC) {
			unique_lock<shared_mutex> lock = write_lock();
                        mark(pos, 0, 'x');
		}

		/* if there is no xref, check if the line itself has a xref
		 * format we use, such as a grep -rn line or an entry in a file
		 * we already added a comment for */
		if (ret.first == nullptr) {
			const string_view& line = get_line(pos);
			string file;
			size_t lineno = G::NO_POS;
			if (XRef::grep_line(line, &file, &lineno) ||
			    XRef::storytime_line(line, &file, &lineno) ||
			    XRef::link_line(line, &file, &lineno) ||
			    XRef::ctags_line(_filename, line, &file, &lineno)) {
				return make_pair(
					new LogLines(file),
					lineno);
			}
		}

		return ret;
	}

	// split a long line based on heursitics
	virtual void split(size_t pos) {
		return split(pos, '\0');
	}

	// splits a long line by the character c as delimiter, unless c == '\0'
	// in which case apply relevant heuristics
	virtual void split(size_t pos, char c) {
		unique_lock<shared_mutex> ul = write_lock();
		split_locked(pos, c);
	}

	// implement split with the lock held
	virtual void split_locked(size_t pos, char c) {
		if (!exists_line_locked(pos)) return;

		list<unique_ptr<Line>> to_add;
		to_add = LineIntelligence::split(get_line(pos), c);
		assert(to_add.size() > 0);
		unique_ptr<Line> last = std::move(to_add.back());
		assert(last.get());
		to_add.pop_back();

		// TODO: split should allow reassembly with backspace by storing
		// in the Line type with the lines to delete
		size_t amount = to_add.size();
		_lines.insert(to_add, pos);
		// note to_add is moved at this point

		EventBus::_()->insertion(this, pos, amount);
		for (size_t i = pos; i < pos + amount; ++i) {
			EventBus::_()->edit_line(this, i, _lines[i].get());

		}
		_lines[pos + amount] = std::move(last);
		EventBus::_()->edit_line(this, pos + amount,
					 _lines[pos + amount].get());
	}

	// acquires the shared lock for the loglines
	virtual shared_lock<shared_mutex> read_lock() const {
		return shared_lock(_m);
	}

	// acquires the unique lock for the loglines
	virtual unique_lock<shared_mutex> write_lock() const {
#ifdef __APPLE__
		unique_lock<shared_mutex> lock(_m, defer_lock);
		while (!lock.try_lock()) {
			this_thread::yield();
		}
		return lock;
#else
		return unique_lock(_m);
#endif
	}

	virtual void merge(size_t pos) {
		if (pos == G::NO_POS) [[unlikely]] return;
		unique_lock<shared_mutex> ul = write_lock();
		if (pos + 1 >= length_locked()) [[unlikely]] return;

		(*_lines[pos]) += (*_lines[pos + 1]);
		EventBus::_()->edit_line(this, pos, _lines[pos].get());
		_lines.remove(pos + 1);
		EventBus::_()->deletion(this, pos + 1, 1);
	}

	// Removes position pos from the log lines
	virtual void remove(size_t pos) {
		unique_lock<shared_mutex> ul = write_lock();
		_lines.remove(pos);
		EventBus::_()->deletion(this, pos, 1);
	}

	/* Returns a new loglines consistent of the tabbed data with column
	 * suppression */
	virtual LogLines* tabfilter_clone(
			const string& name,
			char tab_char,
			set<size_t> suppress_cols) {
		unique_lock<shared_mutex> ul = write_lock();

		unique_ptr<LogLines> ret = make_unique<LogLines>();
		ret->_type = LL_PERMAFILTER;
		ret->_display_name = name;
		for (size_t i = 0; i < _lines.length(); ++i) {
			ret->add_line(_lines[i]->filter_tabs(tab_char,
							     suppress_cols));
		}
		return ret.release();
	}

	/* Returns a new loglines consisting of all the lines between the two
	 * nearest pin locations up and down from the cursor. Caller responsible
	 * to delete
	 */
	 virtual LogLines* pinfilter_clone(
			const string& name,
			optional<size_t> pin_up,
			optional<size_t> pin_down) {
		unique_lock<shared_mutex> ul = write_lock();

		unique_ptr<LogLines> ret = make_unique<LogLines>();
		ret->_type = LL_PERMAFILTER;
		ret->_display_name = name;

		if (!pin_up) pin_up = 0;
		else ++*pin_up;
		if (!pin_down) pin_down = _lines.length();
		assert(*pin_up < *pin_down || (*pin_up == 0 && *pin_down == 0));
		assert(*pin_down <= _lines.length());

		for (size_t i = *pin_up; i < *pin_down; ++i) {
			ret->add_line(string(get_line(i)));
		}
		return ret.release();
	}

	// caller responsible to delete
	virtual LogLines* permafilter_clone(set<size_t>& lines,
					    const string& name) {
		unique_lock<shared_mutex> ul = write_lock();

		unique_ptr<LogLines> ret = make_unique<LogLines>();
		ret->_type = LL_PERMAFILTER;
		ret->_display_name = name;
		for (const auto &x : lines) {
			ret->add_line(string(get_line(x)));
		}
		ret->add_line("");
		return ret.release();
	}

	// returns number of lines in log lines
	virtual size_t length() const {
		shared_lock<shared_mutex> ul(_m);
		return length_locked();
	}

	// returns number of lines in log lines, assumes lock is held
	virtual size_t length_locked() const {
		return _lines.length();
	}

	/* Requires that keyword is lower case */
	virtual size_t find(size_t cur, size_t tab, const string& keyword) const {
		if (!_lines.valid(cur)) return string::npos;
		shared_lock<shared_mutex> ul(_m);
		return G::to_lower(get_line(cur)).find(keyword, tab);
	}

	/* Requires that keyword is lower case */
	virtual size_t rfind(size_t cur, size_t tab, const string& keyword) const {
		if (!_lines.valid(cur)) return string::npos;
		shared_lock<shared_mutex> ul(_m);
		return G::to_lower(get_line(cur)).rfind(keyword, tab);
	}

	/* Writes the entire current log lines to a file that is passed in, or a
	 * default file otherwise.
	 * */
	virtual void save(const string& filename) const {
		shared_lock<shared_mutex> ul(_m);
		ofstream fout;
		if (filename.empty()) {
			fout.open(Config::_()->get("initcwd") + "/LOGSERVER_FILE");
		} else if (filename[0] == '/') {
			fout.open(filename);
		} else {
			fout.open(Config::_()->get("initcwd") + "/" + filename);
		}


		if (!fout.good()) fout.open("/tmp/LOGSERVER_FILE");
		_lines.write(fout);
	}

	/* saves the current line to a file
	   TODO: if navi cur above middle line, should it be the middle line?
	   */
	virtual void save_line(const string& filename, size_t line) const {
		if (line == G::NO_POS) return;
		shared_lock<shared_mutex> ul(_m);
		ofstream fout;
		if (filename.empty()) {
			fout.open(Config::_()->get("initcwd") + "/LOGSERVER_LINE");
		} else if (filename[0] == '/') {
			fout.open(filename);
		} else {
			fout.open(Config::_()->get("initcwd") + "/" + filename);
		}

		if (!fout.good()) fout.open("/tmp/LOGSERVER_LINE");
		fout << get_line(line) << endl;
	}

	// returns true if the eof flag has been set for the log lines
	virtual bool eof() const override {
		return _eof;
	}

	// sets the eof flag to the parameter value
	virtual void set_eof(bool value) override {
		_eof = value;
	}

	/* flush writes a line to the fd and appends a newline */
	static void stream_write_line(int fd, const string_view& line) {
		ssize_t r = ::write(fd, line.data(), line.length());
		if (r < 0) return;
		if (r < static_cast<ssize_t>(line.length())) [[unlikely]] {
			string l = string(line.substr(r));
			while (l.size()) {
				r = ::write(fd, l.data(), l.length());
				if (r < 0) return;
				l = l.substr(r);
			}
		}
		while (true) {
			r = ::write(fd, "\n", 1);
			if (r != 0) return;
		}
	}

	/* writes the lines corresponding to the set of positions in the
	 * paramter lines to the input stream for the process in parameter run.
	 * Used to stream current loglines to a pipe process and take output as
	 * a new loglines
	 */
	virtual void stream_write(Run* run, const set<size_t>& lines) {
		// TODO: only write matching lines
		unique_lock<shared_mutex> ul = write_lock();

		// TODO: refactor
		int fd = run->write_fd();
		if (lines.size()) {
			for (const auto &x : lines) {
				stream_write_line(fd, get_line(x));
			}
		} else {
			for (const auto &x : _lines) {
				stream_write_line(fd, x->view());
			}
		}
		Config::_()->set("info", "");
		run->close_write();
	}

	/* gets a snippet of lines around the current one for tracking in the
	 * story file */
	virtual void quote(size_t cur, const set<size_t>& view,
			   size_t width, stringstream* ss) {
		size_t total_lines = length();
		// we have a clean view, go by lines
		if (view.empty()) {
			size_t i = 0;
			if (cur > width) i = cur - width;

			for (; i < cur + width; ++i) {
				if (i >= total_lines) break;
				string_view line = get_line_unlocked(i);
				if (line.empty()) {
					++width;
				} else {
					*ss << "\t" << line << endl;
				}
			}
			return;
		}

		// else we have a filtered view, use that
		auto x = view.lower_bound(cur);
		for (size_t i = 0; i < width; ++i) {
			if (x != view.cbegin()) --x;
		}
		for (size_t i = 0; i < 2 * width + 1; ++i) {
			if (x == view.cend()) break;
			string_view line = get_line_unlocked(*x);
			if (!line.empty()) {
				*ss << "\t" << line << endl;
			} else {
				++width;
			}
			++x;
		}
	}

	/* return the line back to original if it has changed */
	virtual void revert(size_t pos) {
		shared_lock<shared_mutex> ul(_m);

		if (!exists_line_locked(pos)) return;
		_lines[pos]->revert();
		EventBus::_()->edit_line(this, pos, _lines[pos].get());
	}

	/* returns the display name for the log lines */
	virtual string display_name() const {
		shared_lock<shared_mutex> ul(_m);
		return _display_name;
	}

	/* returns the filename for the log lines */
	virtual string file_name() const {
		shared_lock<shared_mutex> ul(_m);
		return _filename;
	}

	/* when used as a media server, plays the file at this line */
	virtual void enter(size_t pos) {
		unique_lock<shared_mutex> ul = write_lock();
		if (_type == LL_DIRECTORY) {
			ofstream fout(_filename + "/.mediaserver", ios::app);
                        mark(pos, 0, 'x');
			string path = _lp->get_line(pos);
			fout << path << endl;
			fout.close();
			string play = "mplayer -fs -msglevel all=-1 \"" + path + "\"";
			Run run(play);
			run();
			run.read(200000);
		}
	}

	/* different types of log lines based on how it got data */
	enum LL_TYPE {
		LL_STDIN,  // read from stream
		LL_FILE,  // read from a file
		LL_PERMAFILTER,  // filtered view of prior log lines
		LL_PIPE,  // read from a command pipeline output
		LL_SYNTHETIC,  // created static list of items
		LL_DIRECTORY,  // created from a directory list
		LL_GDB,  // GEB summary page
		LL_GDB_NODE,  // GEB node page
		LL_STATIC,  // static page like help
		LL_NONE  // no log lines
	};

	/* returns the type of the log lines */
	virtual LL_TYPE type() const { return _type; }

protected:
	/* helper function to ensure that pos can be mapped to a real line.
	 * assumes lock is held */
	virtual bool exists_line_locked(size_t pos) {
		return !(pos == G::NO_POS
		    || pos > length_locked()
		    || !length_locked());
	}

	/* implementation of follow. main function handles edge cases */
	virtual pair<LogLines*, size_t> follow_impl(
			[[maybe_unused]] size_t pos,
			[[maybe_unused]] bool display_all) {
		shared_lock<shared_mutex> ul(_m);

#ifdef __USE_GEB__
		if (_type == LL_GDB) {
			TGraph::Node* node = _lines.at(pos)->node();
			if (!node) return make_pair(nullptr, 0);
			// TODO: in metadata
			auto edges =
			    node->edges(GdbNodeLineProvider::GDB_EDGES);
			if (edges.empty()) return make_pair(nullptr, 0);
			if (edges.size() == 1 && edges.begin()->second.size() == 1) {
				TGraph::Node* target = *edges.begin()->second.begin();
				size_t lineno = target->data().get<size_t>("lineno");
				// TODO: if in the same file, do it as a link
				if (lineno) --lineno;
				return make_pair(
					new LogLines(
						_gdb, "TODO", target),
					lineno);
			}
			// return a splay page of all edges
			return make_pair(new LogLines(_gdb, "", node), 4);
		} else if (_type == LL_GDB_NODE) {
			// splayed node
			TGraph::Node* node = _lines.at(pos)->node();
			if (!node) return make_pair(nullptr, 0);
			size_t lineno = node->data().get<size_t>("lineno");
			if (!lineno) {
				return make_pair(nullptr, 0);
			}
			--lineno;
			return make_pair(new LogLines(
				_gdb, node->data().at(""), node),
				lineno);
		}
#endif  // __USE_GEB__

		return make_pair(nullptr, 0);
	}

	// set the value of the line at position pos to the value val
	virtual inline void set_line(size_t pos, const string& val) {
		_lines.at(pos)->set(val);
		EventBus::_()->edit_line(this, pos, _lines[pos].get());
	}

	// returns a string_view of the line at position pos
	virtual inline const string_view get_line(size_t pos) const {
		return _lines.view_at(pos);
	}

	/* edit a character in a line, used to add 'x' markers in xref for
	 * visited links */
	virtual void mark(size_t pos, size_t col, char val) {
		// TODO: mark the stored object
		if (!exists_line_locked(pos)) return;
		assert(col < _lines[pos]->length());
		_lines.at(pos)->mark(col, val);
	}

	// returns true if this is a synthetic list of choices that the user can
	// select from to advance, e.g., during xref mode
	virtual inline bool is_choice_display() const {
		return _type == LL_SYNTHETIC && _display_name == _choice_name;
	}

	// the filename if this is for a directory or a file
	string _filename;

	// true if loglines is signalled by line provider that no more lines are
	// coming
	atomic<bool> _eof;

	// the actual line data itself, stored in a huge vector
	huge_vector<unique_ptr<Line>> _lines;

	// shared mutex so multiple readers can progress, e.g., searching for
	// new data. writes are only needed for editing and inserting lines
	mutable shared_mutex _m;

	// for xref link list, the name the loglines will appear as
	static constexpr const char * _choice_name = "?";

	// name to display in the top line
	string _display_name;

	// Run instance when running a command on the loglines
	unique_ptr<Run> _runner;

	// Interface to the instance that is feeding us lines
	unique_ptr<ILineProvider> _lp;

	// the type of the log lines, is const once first set
	LL_TYPE _type;

#ifdef __USE_GEB__
	// pointer to graph object if displaying graph data
	TGraph* _gdb;
#endif  // __USE_GEB__
};

#endif  // __LOG_LINES__H__
