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

#include <optional>
#include <list>
#include <set>
#include <string>

#include "format_string.h"
#include "tab_data.h"

using namespace std;

#ifdef __USE_GEB__
#include "graph.h"
#include "map_strings.h"
using TGraph = Graph<MapStrings, string>;
#endif

#include <iostream>

/* stores a single line in the loglines. while normally a string, this allows
 * storing extra metadata per line. For example, if it was modified, what was
 * the original line so we can revert it back. As well, for GEB integration is
 * tracks a node in the graph tied to the line
 */
class Line {
public:
	// constructors take a string and a possible GEB node
	explicit Line(const string_view& line) : _line(line) {}
	explicit Line(const string& line) : _line(line) {}
	Line(const string& line, const string& fmt) : _line(line), _fmt(fmt) {}

#ifdef __USE_GEB__
	Line(const string_view& line, TGraph::Node* node) : _line(line), _node(node) {}
	Line(const string& line, TGraph::Node* node) : _line(line), _node(node) {}
#endif
	virtual ~Line() {}

	virtual void operator+=(const Line& other) {
		/* either string can have empty string for format, as a
		 * shorthand for all zeros to avoid duplicating storage for long
		 * string when formatting is not being provided by the input
		 * stream. handle these cases by degraded to stored */
		if (_fmt.empty() && other._fmt.empty()) {
			// both unset format
		} else if (_fmt.empty()) {
			_fmt = string(length(), '\0') + other._fmt;
		} else if (other._fmt.empty()) {
			_fmt += string(other.length(), '\0');
		} else {
			_fmt += other._fmt;
		}

		_line += other._line;
		_no_tabs = nullopt;
	}

	// length of the string
	virtual inline size_t length() const {
		return _line.length();
	}

	// string_view for the string
	virtual inline string_view view() const {
		return _line;
	}

	virtual inline string_view format_view() const {
		return _fmt;
	}

	// the string to show
	virtual inline const string& get() const {
		return _line;
	}

	// return to the original value
	virtual inline void revert() {
		if (_original.get() == nullptr) return;
		_line = std::move(*_original.get());
		_no_tabs = nullopt;
		_fmt = "";
		_original.reset();
	}

	// if we do not have an original value saved, save it now.
	// TODO: we could have a stack to support popping multiple times
	virtual inline void maybe_save() {
		if (_original.get() == nullptr) {
			_original.reset(new string(std::move(_line)));
		}
	}

	// set the string to a new value, save the old if needed
	virtual inline void set(const string& val) {
		maybe_save();
		_line = val;
		_fmt = "";
		_no_tabs = nullopt;
	}

	// change the string at character col to have value val
	virtual inline void mark(size_t col, char val) {
		maybe_save();
		_line[col] = val;
	}

	virtual void set_format(string&& format) {
		_fmt = format;
	}

	virtual list<size_t> tabstops(optional<char> tab_key) {
		assert(tab_key);
		list<size_t> ret;

		bool quote = false;
		bool escape = false;
		size_t i = 0;
		ret.push_back(0);
		for (const auto &x : _line) {
			if (x == *tab_key && !quote) {
				ret.push_back(i);
				escape = false;
			} else if (x == '\\') {
				escape = !escape;
			} else if (x == '\"' && !escape) {
				escape = false;
				quote = !quote;
			} else {
				escape = false;
			}
			++i;
		}
		return ret;
	}

	virtual Line* filter_tabs(optional<char> tab_key,
				  const std::set<size_t>& suppress_cols) {
		string result = "";
		list<size_t> stops = tabstops(tab_key);
		auto it = stops.begin();
		auto it_next = it;
		++it_next;
		size_t cur_tab_index = 0;
		bool first = true;
		while (it_next != stops.end()) {
			if (!suppress_cols.count(cur_tab_index)) {
				if (!first) result += *tab_key;
				first = false;
				if (*it == 0) {
					result += _line.substr(0, *it_next);
				} else {
					result += _line.substr(*it + 1,
							       *it_next - *it - 1);
				}
			}
			++it_next;
			++it;
			++cur_tab_index;
		}
		return new Line(string(result));
	}

	virtual void measure_tabs(optional<char> tab_key,
				  TabData* tab_data) {
		assert(tab_key);
		list<size_t> stops = tabstops(tab_key);
		auto it = stops.begin();
		auto it_next = it;
		++it_next;
		size_t cur_tab_index = 0;
		while (it_next != stops.end()) {
			tab_data->observe_col(cur_tab_index,
					      *it_next - *it + 1);
			++it;
			++it_next;
			++cur_tab_index;
		}
	}

	virtual size_t render(size_t remain,
			      bool need_length,
			      optional<char> tab_key,
			      size_t navi_tab,
			      const std::set<size_t>& suppressed_tabs,
			      TabData* tab_data,
			      FormatString* fs) {
		check_notabs();
		if (tab_key == nullopt && _no_tabs == true) [[likely]] {
			// too far right (or empty line)
			if (navi_tab > _line.size()) return _line.size();
			// add remaining string
			if (_fmt.empty()) {
				fs->add(_line.substr(navi_tab, remain), 0);
			} else {
				fs->add(_line.substr(navi_tab, remain),
					_fmt.substr(navi_tab, remain));
			}
			return _line.size();
		}

		// slow path. we have to worry about tabs.
		string detabbed;
		string detabbed_format;
		detabbed.reserve(2 * _line.size());
		detabbed_format.reserve(2 * _line.size());
		size_t tab_index = 0;
		// just simplifies logic by normalizing optional to tab
		if (tab_key == nullopt) tab_key = '\t';
		list<size_t> stops = tabstops(tab_key);
		bool omit = suppressed_tabs.count(tab_index);
		size_t tab_start = 0;
		auto it = stops.begin();
		++it;
		for (size_t i = 0; i < _line.size(); ++i) {
			char x = _line[i];
			if (it != stops.end() && i == *it) {
				++it;
				if (!omit) {
					detabbed += ' ';
					detabbed_format += '\0';
					if (tab_data) {
						size_t align = tab_data->width(tab_index);
						while (tab_start + align >
						       detabbed.size()) {
							detabbed += ' ';
							detabbed_format += '\0';
						}
						tab_start = detabbed.size();
					} else {
						while (detabbed.size() % 8) {
							detabbed += ' ';
							detabbed_format += '\0';
						}
					}

				}
				if (tab_data) {
					++tab_index;
					omit = suppressed_tabs.count(tab_index);
				}
			} else if (!omit) [[likely]] {
				detabbed += x;
				if (_fmt.empty()) detabbed_format += '\0';
				else detabbed_format += _fmt[i];
			}
			assert(detabbed.size() == detabbed_format.size());
			// if the caller needs the length to support navigation
			// jumping to the end of line then compute the whole
			// length. otherwise we can stop here since we've got
			// the view of the string
			if (!need_length && detabbed.size() >= navi_tab + remain)
				[[unlikely]]
				break;
		}
		if (detabbed.size() > navi_tab)
			fs->add(detabbed.substr(navi_tab, remain),
				detabbed_format.substr(navi_tab, remain));
		return detabbed.size();
	}

#ifdef __USE_GEB__
	// the GEB node
	virtual inline TGraph::Node* node() const { return _node; }
#endif

protected:
	virtual inline void check_notabs() {
		if (_no_tabs != nullopt) return;
		_no_tabs = _line.find('\t') == string::npos;
	}

	// TODO: store the line, bitset for original, isall lower,
	// then string for original and lower. or craft lower matching alg
	// the string we will see
	string _line;
	// pointer to original string if relevant
	unique_ptr<string> _original;
	string _fmt;
	optional<bool> _no_tabs;

#ifdef __USE_GEB__
	// GEB node for GEB integration
	TGraph::Node* _node;
#endif

};

#endif  // __LINE__H__
