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

#include <mutex>
#include <optional>

#include "constants.h"

using namespace std;

/* PinManager stores the lines that have been pinned down. Updates positions
 * during insertions. Returns nearest pin relative to a whence and direction. */
class PinManager {
public:
	/* default constructor */
	PinManager() {}

	/* default destructor */
	virtual ~PinManager() {}

	// Given a position pos and a direction dir, and the pair [pos, range]
	// corresponding to the result of a keyword search, pinsider considers
	// pinned lines and whether it should interfere. It either returns the
	// result.pos value it was going to display, or the position of the
	// first pinned line that is between pos < pin < result.first
	virtual size_t pinsider(size_t pos, int dir,
				pair<size_t, size_t> result) {
		shared_lock<shared_mutex> ul(_m);
		optional<size_t> pin = nearest_pin(pos, dir);
		size_t match = result.first;
		if (!pin) return match;
		size_t range = result.second;

		// we have a pin before the result, return pin
		if (dir == G::DIR_DOWN && match != G::NO_POS && *pin < match) return *pin;
		if (dir == G::DIR_UP && match != G::NO_POS && *pin > match) return *pin;

		// we have no result and we have maximal range so no result will
		// come, show the pin.
		if (match == G::NO_POS && range == G::NO_POS) return *pin;

		// we have no match within an explored range, but we do have a
		// pin within that range return the pin
		if (dir == G::DIR_DOWN && match == G::NO_POS && *pin < range) return *pin;
		if (dir == G::DIR_UP && match == G::NO_POS && *pin > range) return *pin;

		// no relevant pin. return top if there was no match and we're
		// moving up, otherwise return the match (or go to end)
		if (dir == G::DIR_UP && match == G::NO_POS) return 0;
		return match;
	}

	// new lines have been inserted. adjust the pins appropriately
	virtual void insertion(size_t pos, size_t amount) {
		unique_lock<shared_mutex> ul(_m);
		set<size_t> result;
		for (const auto& x : _pins) {
			if (x < pos) result.insert(result.end(), x);
			else result.insert(result.end(), x + amount);
		}
		_pins.swap(result);
	}

	// clear out the lines because log lines is cleared
	virtual void clear() {
		unique_lock<shared_mutex> ul(_m);
		_pins.clear();
	}

	// adds a new pinned line
	virtual void set_pin(size_t pos) {
		assert(pos != G::NO_POS);
		unique_lock<shared_mutex> ul(_m);
		_pins.insert(pos);
	}

	// toggles whether a line is pinned
	virtual void toggle_pin(size_t pos) {
		assert(pos != G::NO_POS);
		unique_lock<shared_mutex> ul(_m);
		if (_pins.count(pos)) _pins.erase(pos);
		else _pins.insert(pos);
	}

	// returns true if the line is pinned
	virtual bool is_pinned(size_t pos) {
		shared_lock<shared_mutex> ul(_m);
		return _pins.count(pos);
	}

	// returns nearest pin starting from parameter whence position and
	// moving in parameter dir direction. Returns nullopt if no pin is found
	// in that direction
	virtual optional<size_t> nearest_pin(size_t whence, int dir) {
		shared_lock<shared_mutex> ul(_m);
		if (dir == G::DIR_DOWN) {
			// search down
			auto it = _pins.lower_bound(whence);
			if (it == _pins.end()) return nullopt;
			return *it;
		} else {
			assert(dir == G::DIR_UP);
			// search up
			auto it = _pins.lower_bound(whence);
			if (it == _pins.begin()) return nullopt;
			--it;
			return *it;
		}
	}

protected:
	set<size_t> _pins;

	// thread safety
	shared_mutex _m;
};

#endif  // __PIN_MANAGER__H__
