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

#include <cassert>
#include <list>
#include <memory>
#include <mutex>
#include <string>
#include <thread>

#include "colour.h"
#include "constants.h"
#include "exit_signal.h"
#include "i_line_provider.h"
#include "i_log_lines.h"
#include "line.h"
#include "utf8.h"

#include <iostream>

using namespace std;

/* BaseLineProvider provides common functionality for all line providers sub
 * classes but is not a full implementation itself
 */
class BaseLineProvider : public ILineProvider {
public:
	/* Takes a LogLines interface which just allows adding new lines and
	 * querying/setting end-of-file */
	explicit BaseLineProvider(ILogLines* ll) : _ll(ll), _exit(false) {}

	/* starts thread that runs the implementing provider */
	virtual inline void start() override {
		_runner.reset(new thread(&BaseLineProvider::loader, this));
	}

	/* must be overriden */
	virtual string get_line([[maybe_unused]] size_t pos) override {
		assert(0);
		return "";
	}

	/* tells thread to stop and waits */
	virtual ~BaseLineProvider() {
		_exit = true;
		if (_runner.get()) _runner->join();
		for (const auto& x : _lines) delete x;
	}

	static pair<string, string> get_string_and_format(const string_view& in) {
		pair<string, string> ret;
		bool underline = false;
		bool bold = false;
		int cur_colour = 0;
		optional<int> colour;

		for (size_t i = 0; i < in.length(); ++i) {
			char c = in[i];
			if (i && c == '\b' && i + 1 < in.length()) [[unlikely]] {
				char left = in[i - 1];
				char right = in[i + 1];
				if (left == right) {
					ret.second[ret.second.length() - 1] = '\32';
				} else {
					// use right if we stored an underline
					if (left == '_') {
						ret.first[ret.first.length() - 1] =
							right;
					}
					ret.second[ret.second.length() - 1] = '\64';
				}
				++i;
			} else if (c == 0x1b) [[unlikely]] {
				colour = nullopt;
				i = parse_ansi(in, i, &bold, &underline,
					       &colour);
				if (colour) cur_colour = *colour;
				continue;
			} else if (!isspace(c) && isprint(c)
				   && (cur_colour || bold || underline)) [[unlikely]] {
				char to_add = cur_colour;
				if (underline) to_add |= G::UNDERLINE;
				else if (bold) to_add |= G::BOLD;
				ret.first += c;
				ret.second += to_add;
			} else {
				optional<string> decode = UTF8::simplify(in, &i);
				if (decode) [[unlikely]] {
					ret.first += *decode;
					ret.second += string(decode->size(),
							    '\0');
				} else [[likely]] {
					ret.first += c;
					ret.second += '\0';
				}
			}
		}
		return ret;
	}
protected:
	/* makes a Line type from a string, considering possible terminal
	 * escapes and printing */
	static Line* make_line(const string& line) {
		if (line.find_first_of("\b\x1b") == string::npos)
			return new Line(line);
		// we have either backspace or ansi, or both.
		pair<string, string> stringformat = get_string_and_format(line);
		return new Line(stringformat.first, stringformat.second);
	}

	/* returns a first number position in in, or nullopt if none present */
	static optional<string> move_to_number(const string_view& in) {
		size_t i = 0;
		while (i < in.size()) {
			if (in[i] >= '0' && in[i] <= '9')
				return string(in.substr(i));
			++i;
		}
		return nullopt;
	}

	/* if data[pos] is escape and what follows is valid ansi code, then set
	 * the boolean bold and underline appropriately and return index of
	 * string at the end of the sequence. return input position if it cannot
	 * be parsed or it is not ending as an 'm' ansi code
	 */
	static size_t parse_ansi(const string_view& data, size_t pos,
				  bool* bold, bool* underline,
				  optional<int>* colour) {
		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;
		pos += 2;
		// check valid ansi sequence until m
		for (size_t i = pos; i < end; ++i) {
			if (data[i] != ';' && data[i] < '0' && data[i] > '9')
				return pos;
		}
		optional<string> format = move_to_number(
			data.substr(pos, end - pos));
		size_t subpos;
		while (format) {
			try {
				int val = stoi(*format, &subpos);
				if (val == 4) {
					*underline = true;
				} else if (val == 1) {
					*bold = true;
				} else if (val == 24) {
					*underline = false;
				} else if (val == 22) {
					*bold = false;
				} else if (val == 0) {
					*underline = false;
					*bold = false;
				} else {
					*colour = Colour::ansi_colour(val);
				}
				format = move_to_number(format->substr(subpos));
			} catch (...) {
				*underline = false;
				*bold = false;
				*colour = 0;
				break;
			}
		}
		return end;
	}

	/* adds a new string to the LogLines. returns false if user has signalled
	 * to quit so the thread stop */
	virtual inline bool add_line(const string& line) {
		return add_line(make_line(line));
	}

	/* queue a string to add as a line to avoid locking loglines */
	virtual inline bool queue_line(const string& line) {
		return queue_line(make_line(line));
	}

	/* queue a line to add to avoid locking loglines */
	virtual inline bool queue_line(Line* line) {
		if (exit()) [[unlikely]] return false;
		_lines.emplace_back(line);
		if (_lines.size() > 4096) flush_lines();
		return true;
	}

	/* flush the queued lines to loglines */
	virtual inline void flush_lines() {
		if (_lines.empty()) [[unlikely]] return;
		_ll->add_lines(&_lines);
		assert(_lines.empty());
	}

	/* takes ownership and adds a new Line string to the LogLines. returns
	 * false if the user has signalled to quit  */
	virtual inline bool add_line(Line* line) {
		if (exit()) [[unlikely]] {
			delete line;
			return false;
		}
		_ll->add_line(line);
		return true;
	}

	/* signals we have no more lines to come */
	virtual inline void eof() {
		_ll->set_eof(true);
	}

	/* signals we may be wrong about no more lines and more are coming,
	 * e.g, if we are serving a file, and that file has been appended */
	virtual inline void unset_eof() {
		_ll->set_eof(false);
	}

	/* returns true if we are shutting down. */
	virtual inline bool exit() const {
		if (_exit) return true;
		return ExitSignal::check(false);
	}

	/* default does nothing */
	virtual void loader() {
	}

	/* LogLines we push our lines to */
	ILogLines* _ll;

	/* thread that finds lines for LogLines to serve */
	unique_ptr<thread> _runner;

	/* true if we are shutting down */
	bool _exit;

	/* list of queued lines to add */
	list<Line*> _lines;
};

#endif // __BASE_LINE_PROVIDER__H__
