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

#include <memory>
#include <shared_mutex>
#include <string>
#include <sstream>
#include <vector>

#include "config.h"
#include "constants.h"
#include "epoch.h"
#include "filter_renderer.h"
#include "filter_manager.h"
#include "log_lines.h"
#include "match.h"
#include "navigation.h"
#include "renderer.h"
#include "view.h"

using namespace std;
using namespace std::placeholders;

/* ViewStacks holds a list of Views and tracks which one is the current one that
 * should be rendered. It also holds a render thread worker that goes into the
 * current view and runs its rendering code */
class ViewStack {
public:
	/* empty constructor */
	ViewStack() : _tab(false), _tab_key('\t') {}

	/* initialize the view stack with the renderer and epoch, and starts the
	 * worker thread */
	virtual void init(Renderer* renderer, Epoch* epoch) {
		_renderer = renderer;
		_epoch = epoch;
		_cur = nullopt;
		_filter_renderer.reset(new FilterRenderer(_renderer, *_epoch));
		_worker.reset(new thread(bind(&ViewStack::worker, this)));
	}

	/* destructor clears cur, notifies the condition and waits for thread to
	 * finish */
	virtual ~ViewStack() {
		unique_lock<shared_mutex> ul(_m);
		_cur = nullopt;
		_epoch->shutdown();
		_cur_cond.notify_all();
		ul.unlock();
		_worker->join();
	}

	/* accessor functions to get the view or its constituent filterer,
	 * loglines, and navigation based on the current element in the stack */
	FilterManager* fm() const {
		shared_lock<shared_mutex> ul(_m);
		return (**_cur)->fm();
	}

	// gets current loglines
	LogLines* ll() const {
		shared_lock<shared_mutex> ul(_m);
		return (**_cur)->ll();
	}

	// gets current navi
	Navigation* navi() const {
		shared_lock<shared_mutex> ul(_m);
		return (**_cur)->navi();
	}

	// gets current view
	View* view() const {
		shared_lock<shared_mutex> ul(_m);
		return (**_cur).get();
	}

	/* trims all views past the current one */
	virtual void trim_later() {
		unique_lock<shared_mutex> ul(_m);
		auto it = _cur;
		stop_render(&ul);
		trim_later_locked();
		_cur = it;
		start_render();
	}

	/* trims all views past the current ones, assumes write lock is held */
	virtual void trim_later_locked() {
		if (!_cur) return;
		if (*_cur == _views.end()) return;
		auto it = *_cur;
		++it;
		_views.erase(it, _views.end());
	}

	/* appends a new view to the stack based around the provided loglines */
	virtual void push(LogLines* ll) {
		_tab_data.clear();
		_tab = false;
		unique_lock<shared_mutex> ul(_m);
		stop_render(&ul);
		trim_later_locked();
		unique_ptr<Navigation> navi(new Navigation());
		unique_ptr<FilterManager> filter(new FilterManager(
			ll, navi.get()));
		_views.push_back(make_unique<View>(
			ll, filter.release(), navi.release()));
		_cur = _views.end();
		// _views will have one element at least from push
		--*_cur;
		start_render();
	}

	/* removes the top element of the loglines stack */
	virtual void pop() {
		_tab_data.clear();
		_tab = false;
		unique_lock<shared_mutex> ul(_m);
		if (_views.size() <= 1) return;
		stop_render(&ul);
		_views.pop_back();
		_cur = _views.end();
		// _views will have one element at least from check
		--*_cur;
		start_render();
	}

	/* returns the title bar for the stack of views and emboldens the
	 * current one */
	bool title_bar(FormatString* fs, size_t cols) const {
		shared_lock<shared_mutex> ul(_m);
		// wait until cur is set if rendering is suspended
		while (!_cur) _cur_cond.wait(ul);

		fs->add(Version::VERSION, 0);
		fs->add(" ", 0);
		for (const auto& x : _views) {
			int weight = 0;
			string suffix;
			if (**_cur == x) {
				weight = G::BOLD;
				suffix = tab_summary();
			}
			fs->add("(", weight);
			fs->add(x->ll()->display_name(), weight);
			fs->add(suffix);
			fs->add(") ", weight);
		}
		fs->truncate(cols);
		return true;
	}

	/* checks that everything is okay statewise */
	bool check() {
		if (!fm() || !ll() || !navi() || !view()) return false;

		shared_lock<shared_mutex> ul(_m);
		return _cur && _renderer && _epoch;
	}

	/* clears current loglines */
	virtual void clear() {
		if (!navi()->at_end()) navi()->start();
		ll()->init();
		_tab_data.clear();
	}

	/* moves current view left (down) on the stack */
	virtual void lshift() {
		_tab_data.clear();
		_tab = false;
		if (ll()->is_static("help")) return pop();

		unique_lock<shared_mutex> ul(_m);
		if (*_cur == _views.begin()) return;
		auto it = *_cur;
		stop_render(&ul);
		--it;
		_cur = it;
		start_render();
	}

	/* moves current view right (up) on the stack */
	virtual void rshift() {
		_tab_data.clear();
		_tab = false;
		auto it = *_cur;
		unique_lock<shared_mutex> ul(_m);
		stop_render(&ul);
		++it;
		if (it == _views.end()) --it;
		_cur = it;
		start_render();
	}

	/* hits enter on logline's current line */
	virtual void enter() {
		ll()->enter(navi()->cur());
	}

	/* breaks the current line */
	virtual void break_line() {
		if (ll()->length() == 0) return;
		ll()->split(navi()->cur());
	}

	/* toggles pinning on the current line */
	virtual void pin_line() {
		if (ll()->length() == 0) return;
		if (navi()->at_end()) return;
		fm()->toggle_pin(navi()->cur());
	}

	/* creates a new loglines based on the current view. If we have search
	 * terms applied, use that filtered view as the current view. If we do
	 * not have them applied, e.g., we are in ALL mode, go to the nearest
	 * pinned lines in both directions and use that as the border for the
	 * new loglines */
	virtual void permafilter() {
		if (ll()->length() <= 1) return;
		if (_tab && tab_suppressed()) {
			trim_later();
			push(ll()->tabfilter_clone(
				tab_summary().substr(6),
				_tab_key, _tab_suppress));
			unique_lock<shared_mutex> ul(_m);
			_tab_suppress.clear();
			_tab_data.clear();
		} else if (fm()->is_mode_all()) {
			// only permafilter between pins
			trim_later();
			auto pins = fm()->nearest_pins(navi()->cur());
			push(ll()->pinfilter_clone("-", pins.first,
						   pins.second));
		} else {
			set<size_t> lines;
			fm()->get_view(&lines);
			if (lines.empty()) return;

			trim_later();
			push(ll()->permafilter_clone(lines,
						     fm()->filter_string()));
		}
	}

	// show the help loglines
	virtual void help() {
		if (ll()->is_static("help")) return;
		push(LogLines::show_static("help"));
		navi()->start();
		navi()->goto_line(10, false);
	}

	// inserts a dashed line either appending if we are at the end or
	// inserting at our position
	virtual void insert_dash_line() {
		if (ll()->length() == 0) return;
		if (navi()->at_end()) {
			fm()->set_pin(ll()->add_line(G::DASH));
		} else {
			size_t pos = navi()->cur();
			ll()->insert_line(G::DASH, pos);
			fm()->set_pin(pos);
		}
	}

	// if we've changed the current line, revert it to the original value
	virtual void backspace() {
		unique_lock<shared_mutex> ul(_m);
		auto it = _cur;
		// we need to stop rendering to get workers outside of the line
		// filter keyword that we may be removing
		stop_render(&ul);
		if ((**it)->fm()->pop_keyword()) {
			// don't do the else
		} else if (!(**it)->navi()->at_end()) {
			(**it)->ll()->revert((**it)->navi()->cur());
		}
		_cur = it;
		start_render();
	}

	// if we've changed the current line, revert it to the original value
	virtual void finish_match(bool accepted) {
		unique_lock<shared_mutex> ul(_m);
		auto it = _cur;
		// we need to stop rendering to get workers outside of the line
		// filter keyword that we may be removing if not accepted or the
		// keyword is empty
		stop_render(&ul);
		(**it)->fm()->finish_match(accepted);
		_cur = it;
		start_render();
	}

	// follow xrefs for the line we are one
	virtual void follow(bool display_all) {
		if (ll()->length() == 0) return;
		pair<LogLines*, size_t> where = ll()->follow(
			navi()->cur(), display_all);
		if (!where.first) {
			navi()->jump_back();
			return;
		}

		if (where.first != ll()) {
			trim_later();
			push(where.first);
			navi()->goto_line(where.second, false);
		} else {
			navi()->goto_line(where.second, true);
		}
	}

	/* Runs a pipe command and puts the stdout as a new view */
	virtual void run_pipe_command(const string& command) {
		try {
			Run *runner = new Run(command);
                        (*runner)();
                        trim_later();
                        // write cur loglines into next
                        set<size_t> lines;
                        fm()->get_view(&lines);
                        ll()->stream_write(runner, lines);
                        push(new LogLines(runner));
               } catch (const logic_error& lg) {}
	}

	/* Merge the next line to the end of hte current one */
	virtual void merge() {
		ll()->merge(navi()->cur());
	}

	virtual void tab_toggle() {
		_tab = !_tab;
		if (!_tab) return;
		_tab_data.clear();

		unique_lock<shared_mutex> ul(_m);
		_tab_suppress.clear();
	}

	virtual void tab_key(char c) {
		_tab_key = c;
		_tab = true;
		_tab_data.clear();
	}

	virtual void tab_suppress(size_t pos) {
		if (!_tab) return;

		unique_lock<shared_mutex> ul(_m);
		if (_tab_suppress.count(pos)) {
			_tab_suppress.erase(pos);
		} else {
			_tab_suppress.insert(pos);
		}
	}

protected:
	// stops rendering because we are going to change the current view.
	// tells the worker to stop and waits
	virtual void stop_render(unique_lock<shared_mutex>* ul) {
		_cur = nullopt;
		_epoch->advance();
		ul->unlock();
		// spin lock until the worker gets the message
		while (true) {
			while (_worker_busy) this_thread::yield();
			ul->lock();
			if (!_worker_busy) break;
		}
	}

	// signals worker or interface waiting on _cur being nullopt that it is no longer
	// the case
	virtual void start_render() {
		_cur_cond.notify_all();
	}

	// worker thread. checks the current epoch, when it advances triggers a
	// render, unless there is no _cur view. This happens when the views are
	// changing. Wait until there is a cur, check to make sure we aren't
	// shutting down, and then perform a render on the current view. If
	// epoch changes during the render, abort the current render and
	// restart
	void worker() {
		_worker_busy = false;
		// pretend we've render epoch 0 < first epoch (1)
		size_t cur_epoch = 0;
		while (true) {
			unique_lock<shared_mutex> ul(_m);

			while (cur_epoch == _epoch->cur()) {
				_epoch->wait(&ul);
			}
			if (_epoch->cur() == G::NO_POS) return;

			while (!_cur) {
				if (_epoch->cur() == G::NO_POS) return;
				_cur_cond.wait(ul);
			}
			assert(cur_epoch < _epoch->cur() && _cur);
			cur_epoch = _epoch->cur();
			// check if we are quitting and abort
			if (cur_epoch == G::NO_POS) return;

			RenderParms* rp = new RenderParms();
			_renderer->set_render_parms(rp);
			(**_cur)->fm()->set_render_parms(rp);
			rp->total_length = (**_cur)->ll()->length();
			rp->ll = (**_cur)->ll();
			rp->navi = (**_cur)->navi();
			rp->cur_epoch = cur_epoch;
			rp->tab_key = nullopt;
			rp->tab_data = nullptr;
			if (_tab) {
				rp->tab_key = _tab_key;
				rp->suppressed_tabs = _tab_suppress;
				rp->tab_data = &_tab_data;
			}

			_worker_busy = true;
			ul.unlock();
			// busy signals there is ongoing rendering of whatever
			// _cur pointed to when we first started.
			_filter_renderer->run_render(rp);
			_worker_busy = false;
		}
	}

	virtual string tab_summary() const {
		if (!_tab || _tab_suppress.empty()) return "";
		stringstream ret;
		auto it = _tab_suppress.end();
		--it;

		ret << " cols=";

		size_t i = 0;
		optional<size_t> start;
		for (; i < *it + 2; ++i) {
			if (_tab_suppress.count(i)) {
				if (start == nullopt) continue;
				if (start == i - 1) ret << i << ",";
				else ret << (*start + 1) << "-"
					 << i << ",";
				start = nullopt;
				continue;
			}
			if (start == nullopt) start = i;
		}
		if (start) ret << *start + 1;
		else ret << i + 2;
		ret << "+";
		return ret.str();
	}

	virtual bool tab_suppressed() const {
		unique_lock<shared_mutex> ul(_m);
		return _tab_suppress.size();
	}

	// thread safety for render thread and changing views
	mutable shared_mutex _m;

	// pointer to the renderer object
	Renderer* _renderer;

	// pointer to the epoch object
	Epoch* _epoch;

	// list of the Views in this stack. Each view manages it own filter
	// runner, loglines, navigation, and the matching keywords
	list<unique_ptr<View>> _views;

	// if nullopt, then no view is current. otherwise points to the current
	// view that should be rendered by the render worker
	optional<list<unique_ptr<View>>::iterator> _cur;

	// when _cur is set from  nullopt back to a valid value, this is
	// signalled. worker waits on a nullopt cur until it is set
	mutable condition_variable_any _cur_cond;

	// true when the worker is actively rendering. used to wait until an
	// aborted render is terminated and the worker goes back to waiting so
	// that cur can be changed with trying to render for another View
	atomic<bool> _worker_busy;

	// the thread that calls run_render when the epoch advances and there is
	// a valid current view
	unique_ptr<thread> _worker;

	// the renderer that draws onto the screen the current view
	unique_ptr<FilterRenderer> _filter_renderer;

	// whether custom tab char is enabled
	atomic<bool> _tab;

	// the tab char if enabled. not optional since tab key needs to be
	// remembered even if it is off
	atomic<char> _tab_key;

	// set of suppressed tab columns for rendering
	set<size_t> _tab_suppress;

	// tab data preserved during rendering to account col widths
	TabData _tab_data;

};

#endif  //  __VIEW__H_
