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

#include <atomic>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <set>
#include <string>
#include <sstream>
#include <vector>

#include <ncurses.h>

// can deprecate
#include <iostream>
#include "config.h"
#include "constants.h"

using namespace std;
using namespace std::placeholders;

/* Stores line number and column offset. Each loglines has its own navigation so
 * as new ones are pushed they track their own position. */
class Navigation {
public:
	/* constructor sets values to zero */
	Navigation() : _cur(0), _tab(0),
		       _max_length(0) {}

	/* copy constructor used in interface when a new loglines is added with
	 * same dimensions but will be changed promptly if necessary */
	Navigation(const Navigation& copy)
			: _lines(copy._lines),
			  _max_length(0) {
		  _cur.store(copy._cur.load());
		  _tab.store(copy._tab.load());
	}

	virtual ~Navigation() {
	}

	/* process a keystroke binding it to the appropriate function */
	virtual bool process(int ch) {
		static map<int, function<void(Navigation*)>> functions;
		if (functions.empty()) {
			functions[KEY_UP] = bind(&Navigation::up, _1);
			functions[KEY_DOWN] = bind(&Navigation::down, _1);
			functions[KEY_LEFT] = bind(&Navigation::left, _1);
			functions[KEY_RIGHT] = bind(&Navigation::right, _1);
			functions[KEY_PPAGE] = bind(&Navigation::page_up, _1);
			functions[KEY_NPAGE] = bind(&Navigation::page_down, _1);
			functions[KEY_HOME] = bind(&Navigation::start, _1);
			functions[KEY_END] = bind(&Navigation::end, _1);
			functions[KEY_SHOME] = bind(&Navigation::line_start, _1);
			functions[KEY_SEND] = bind(&Navigation::line_end, _1);
		}
		if (!functions.count(ch)) return false;
		functions.at(ch)(this);
		return true;
	}

	virtual void set_cur_length(size_t length) {
		_max_length = length;
	}

	virtual void set_view(const vector<size_t>& lines) {
		unique_lock<mutex> ul(_m);
		assert(lines.size());
		_lines = lines;
	}

	/* puts cursor at a line end position, past the last line and accepting
	 * streaming data */
	virtual bool at_end() const {
		return _cur == G::NO_POS;
	}

	/* put horizontal shift to leftmost */
	virtual void line_start() {
		_tab = 0;
	}

	/* returns true if at leftmost column */
	virtual bool at_line_start() const {
		return _tab == 0;
	}

	/* puts line at rightmost based on the max length we have set */
	virtual void line_end() {
		_tab = (_max_length / G::h_shift()) * G::h_shift() - 2 * G::h_shift();
		if (_tab > _max_length) _tab = 0;
	}

	/* go to top line */
	virtual void start() {
		set(0);
	}

	/* go to tail (following) line past the last one */
	virtual void end() {
		set(G::NO_POS);
	}

	/* slide horizontal shift left */
	virtual void left() {
		if (_tab >= G::h_shift()) _tab -= G::h_shift();
		else _tab = 0;
	}

	/* slide horizontal shift right */
	virtual void right() {
		_tab += G::h_shift();
	}

	/* jumps to a specific line number. if save_jump is true then the
	 * current line and the destination are linked so they can be jumped
	 * between then later */
	virtual void goto_line(size_t line, bool save_jump) {
		unique_lock<mutex> ul(_m);
		if (save_jump) {
			_jumps[line] = _cur;
			_jumps[_cur] = line;
		}
		set(line);
	}

	/* if there is a backjump marker then follow it back to the source */
	virtual void jump_back() {
		unique_lock<mutex> ul(_m);
		if (_jumps.count(_cur)) {
			set(_jumps[_cur]);
		}
	}

	/* Sets the column to the lower bound multiple of h_shift() for pos */
	virtual void goto_pos(size_t pos) {
		if (pos == string::npos) return;
		_tab = pos - (pos % G::h_shift());
	}

	/* returns current horizontal offset position */
	virtual size_t tab() const {
		return _tab;
	}

	// up arrow pressed
	virtual void up() {
		unique_lock<mutex> ul(_m);
		if (_lines.empty()) return;
		set(safe_up(midpos() - 1));
	}

	// down arrow pressed
	virtual void down() {
		unique_lock<mutex> ul(_m);
		if (_lines.empty()) return;
		if (_cur != _lines[midpos()]) {
			set(safe_down(midpos()));
		} else {
			set(safe_down(midpos() + 1));
		}
	}

	// pageup pressed
	virtual void page_up() {
		unique_lock<mutex> ul(_m);
		if (_lines.empty()) return;
		set(safe_up(0));
	}

	// pagedown pressed
	virtual void page_down() {
		unique_lock<mutex> ul(_m);
		if (_lines.empty()) return;
		set(safe_down(_lines.size() - 1));
	}

	// returns the current line number
	virtual size_t cur() const {
		return _cur;
	}

	// trace the stored data
	virtual inline void trace(ostream& out) {
		unique_lock<mutex> ul(_m);
		trace_locked(out);
	}

protected:
	// implementation of tracing with lock held
	virtual inline void trace_locked(ostream& out) {
		out << "(navi) cur=" << _cur
		    << " lines=" << _lines.size() << endl;
	}

	/* look for a move we can make upwards. start to the first non-NO_POS
	 * value in _lines starting at pos and looking downwards in _lines */
	virtual size_t safe_up(size_t pos) {
		while (safe(pos) && _lines[pos] == G::NO_POS) ++pos;
		if (!safe(pos)) return -1;
		return _lines[pos];
	}

	/* look for a move we can make downwards. start at the first non-NO_POS
	 * value in _lines starting at pos and looking upwards in _lines */
	virtual size_t safe_down(size_t pos) const {
		if (pos == G::NO_POS) return _lines.size() - 1;
		while (safe(pos) && _lines[pos] == G::NO_POS) --pos;
		if (!safe(pos)) return -1;
		return _lines[pos];
	}

	/* check that array access at pos in _lines will be okay */
	virtual bool safe(size_t pos) const {
		return (pos < _lines.size());
	}

	/* returns the middle of the _lines vector, indicating the line in the
	 * centre of the screen */
	virtual size_t midpos() const {
		if (_lines.size() == 0) return 0;
		return (_lines.size() - 1) / 2;
	}

	/* move the line number to this position */
	virtual void set(size_t pos) {
		assert(pos < G::EOF_POS || pos == G::NO_POS);
		_cur = pos;
	}

	/* viewport from the filter runner for navigation */
	vector<size_t> _lines;

	/* current vertical offset */
	atomic<size_t> _cur;

	/* current horizontal offset */
	atomic<size_t> _tab;

	/* max line length that we have seen to manage horizontal moving */
	atomic<size_t> _max_length;

	/* for thread safety */
	mutable mutex _m;

	/* map of positios that we have set jumps points between */
	map<size_t, size_t> _jumps;
};

#endif  // __NAVIGATION__H__
