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

#include <cstdint>
#include <string>
#include <ncurses.h>

#include "constants.h"

using namespace std;

/* TypingLine encapsulates the notion of the user typing, e.g., a search term,
 * and handles the keystrokes and what the string is being created */
class TypingLine {
public:
	TypingLine() : TypingLine(true) {}

	explicit TypingLine(bool completable) : _completable(completable) {
		reset();
	}

	virtual ~TypingLine() {}

	/* process a keystroke from curses's getch(). returns true if it has a
	 * result, which happens when either enter or escape was hit, meaning
	 * that the interface should terminate the use of the typing line and
	 * accept or reject input */
	virtual bool process(int c) {
		if (c == 27) {
			// escape
			set_result(false);
			return true;
		} else if (c == 330) {
			// delete
			del_char();
		} else if (c == KEY_ENTER || c == '\n') {
			set_result(true);
			return true;
		} else if (c == KEY_BACKSPACE || c == 127 || c == '\b') {
			// backspace, remove a character
			pop_char();
		} else if (c > 0 && c < 256 && isprint(c)) {
			// add the character to what is typed
			push_char(c);
		} else if (c == KEY_LEFT) {
			// move cursor left unless at start of line
			commit_complete();
			if (_pos == G::NO_POS) _pos = _type.length();
			--_pos;  // returns to NO_POS if empty
		} else if (c == KEY_RIGHT) {
			// move cursor right unless at end of line
			commit_complete();
			if (_pos != G::NO_POS) {
				++_pos;
				if (_pos == _type.length()) _pos = G::NO_POS;
			}
		} else if (c == KEY_HOME) {
			// go to start of line
			commit_complete();
			if (_type.size()) _pos = 0;
		} else if (c == KEY_END) {
			// go to end (append pos)
			commit_complete();
			_pos = G::NO_POS;
		} else if (c == '\t' && _completable) {
			// try to tab complete the word if that is enabled
			tab_complete();
		}
		return _result == false;
	}

	// true if _result is not nullopt
	virtual bool has_result() const {
		return _result != nullopt;
	}

	// true if it ended with enter, false if escape
	virtual bool result() const {
		assert(_result);
		return *_result;
	}

	/* returns what was typed so far */
	virtual string typed() const {
		if (_completed.empty()) return _type;
		return _completed;
	}

	/* sets what should be considered typed */
	virtual void set_type(const string_view& val) {
		_type = string(val);
		_completed = "";
	}

	/* clears what was typed and resets the cursor to the end */
	virtual void reset() {
		_type = "";
		_completed = "";
		_pos = G::NO_POS;
		_result = nullopt;
		_only_numbers = false;
	}

	/* for search on a line number to drop other letters */
	virtual void only_numbers() {
		_only_numbers = true;
		_completable = false;
	}

	/* returns the cursor position in the line, for e.g., insertion.
	 * position of G::NO_POS means its at the end of what is typed */
	virtual size_t get_pos() const {
		if (_pos != G::NO_POS) return _pos;
		if (!_completed.empty()) return _completed.length();
		return _type.length();
	}

protected:
	virtual void commit_complete() {
		if (_completed.empty()) return;
		_type = _completed;
		_completed = "";
	}

	/* the user has finished typing. result is true for enter meaning accept
	 * the typed value, false for escape meaning reject it */
	virtual void set_result(bool result) {
		_result = result;
		commit_complete();
		if (*_result) {
			auto* results = completes();
			results->insert(_type);
		}

	}

	virtual void pop_char() {
		commit_complete();
		if (_pos == G::NO_POS && _type.length()) {
			_type = _type.substr(0, _type.length() - 1);
		} else if (_pos != G::NO_POS && _pos != 0) {
			assert(_pos < _type.length());
			_type = _type.substr(0, _pos - 1) + _type.substr(_pos);
			--_pos;
		}
	}

	// removes a character from the typed value
	virtual void del_char() {
		commit_complete();
		if (_pos == G::NO_POS) return;
		if (_pos + 1 == _type.length()) {
			_type = _type.substr(0, _pos);
		} else {
			_type = _type.substr(0, _pos) + _type.substr(_pos + 1);
		}
		if (_pos == _type.length()) _pos = G::NO_POS;
	}

	// appends a character to the typed value
	virtual void push_char(char c) {
		commit_complete();
		if (_only_numbers && (c < '0' || c > '9')) return;
		if (_pos == G::NO_POS) {
			_type += c;
		} else {
			assert(_pos < _type.length());
			_type = _type.substr(0, _pos) + c + _type.substr(_pos);
			++_pos;
			assert(_pos < _type.length());
		}
	}

	/* while iterating over possible tab values, return true if this
	 * candidate has the current typed value as prefix */
	virtual bool tab_valid(const string& completion) {
		return completion.find(_type) == 0;
	}

	/* use prior search terms in session as tab completes to make the
	 * stack model of adding and removing searchterms easier to remove one
	 * from the top */
	virtual void tab_complete() {
		set<string>* options = completes();
		if (!_completed.empty()) {
			/* with repeated tabs, progress to next choice */
			auto it = options->lower_bound(_completed);
			++it;
			if (it != options->end() && tab_valid(*it)) {
				_completed = *it;
				return;
			}
			// otherwise we've hit the last candidate. restart the
			// candidate search at the first result, same as if
			// there was not value
		}
		auto it = options->lower_bound(_type);
		if (it == options->end()) return;
		_completed = *it;

	}

	/* TODO: future search term configurable defaults for personal work load */
	static inline set<string>* completes() {
		// TODO have persistent options and a file for specific use
		// cases
		static set<string> _completes = {};
		return &_completes;
	}

	// what the user has typed or accepted
	string _type;
	// a tentative tab completion value that can be iterated over with tabs
	// or accepted with other keystrokes
	string _completed;
	// cursor insertion point
	size_t _pos;
	// set if this line has a result, true if result reached with enter,
	// false if reached with esc.
	optional<bool> _result;
	// true if typing should only allow numbers
	bool _only_numbers;
	// true if tab complete is allowed
	bool _completable;
};

#endif  // __TYPING_LINE__H__
