// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright The Music Player Daemon Project

#pragma once

#include <algorithm>

/**
 * The bounds of a range selection, see list_window_get_range().
 */
struct ListWindowRange {
	/**
	 * The index of the first selected item.
	 */
	unsigned start_index;

	/**
	 * The index after the last selected item.  The selection is
	 * empty when this is the same as "start".
	 */
	unsigned end_index;

	constexpr bool empty() const noexcept {
		return start_index >= end_index;
	}

	constexpr bool Contains(unsigned i) const noexcept {
		return i >= start_index && i < end_index;
	}

	struct const_iterator {
		unsigned value;

		const_iterator &operator++() noexcept {
			++value;
			return *this;
		}

		constexpr bool operator==(const const_iterator &) const noexcept = default;

		const unsigned &operator *() const noexcept {
			return value;
		}
	};

	constexpr const_iterator begin() const noexcept {
		return {start_index};
	}

	constexpr const_iterator end() const noexcept {
		return {end_index};
	}
};

class ListCursor {
	unsigned height;

	/**
	 * A clamped copy of #Options::scroll_offset.
	 */
	unsigned scroll_offset = 0;

	/**
	 * Number of items in this list.
	 */
	unsigned length = 0;

	unsigned start = 0;
	unsigned selected = 0;

	/**
	 * Represents the base item.
	 */
	unsigned range_base = 0;

	/**
	 * Range selection activated?
	 */
	bool range_selection = false;

	bool show_cursor = true;

	/**
	 * @see HighlightCursor()
	 */
	bool highlight_cursor = false;

public:
	explicit ListCursor(unsigned _height) noexcept;

	constexpr unsigned GetHeight() const noexcept {
		return height;
	}

	constexpr unsigned GetOrigin() const noexcept {
		return start;
	}

	constexpr bool IsVisible(unsigned i) const noexcept {
		return i >= GetOrigin() && i < GetOrigin() + GetHeight();
	}

	void SetOrigin(unsigned new_orign) noexcept {
		start = new_orign;
	}

	void HideCursor() noexcept {
		show_cursor = false;
	}

	void ShowCursor() noexcept {
		show_cursor = true;
	}

	/**
	 * Make the cursor visible temporarily (until it is moved)?
	 * This is useful for highlighting a matching line after
	 * searching in a (cursorless) text page.
	 */
	void HighlightCursor() noexcept {
		highlight_cursor = true;
	}

	constexpr bool HasCursor() const noexcept {
		return show_cursor;
	}

	constexpr bool IsCursorVisible() const noexcept {
		return show_cursor || highlight_cursor;
	}

	constexpr bool HasRangeSelection() const noexcept {
		return range_selection;
	}

	/**
	 * Is the cursor currently pointing to a single valid item?
	 */
	constexpr bool IsSingleCursor() const noexcept {
		return !HasRangeSelection() && selected < length;
	}

	constexpr unsigned GetCursorIndex() const noexcept {
		return selected;
	}

	void SelectionMovedUp() noexcept {
		selected--;
		range_base--;

		EnsureSelectionVisible();
	}

	void SelectionMovedDown() noexcept {
		selected++;
		range_base++;

		EnsureSelectionVisible();
	}

	void EnsureSelectionVisible() noexcept {
		if (range_selection)
			ScrollTo(range_base);
		ScrollTo(selected);
	}

	/** reset a list window (selected=0, start=0) */
	void Reset() noexcept;

	void SetHeight(unsigned _height) noexcept;

	void SetLength(unsigned length) noexcept;

	constexpr unsigned GetLength() const noexcept {
		return length;
	}

	/**
	 * Centers the visible range around item n on the list.
	 */
	void Center(unsigned n) noexcept;

	/**
	 * Scrolls the view to item n, as if the cursor would have been moved
	 * to the position.
	 */
	void ScrollTo(unsigned n) noexcept;

	/**
	 * Sets the position of the cursor.  Disables range selection.
	 */
	void SetCursor(unsigned i) noexcept;

	void SetCursorFromOrigin(unsigned i) noexcept {
		SetCursor(GetOrigin() + i);
	}

	void EnableRangeSelection() noexcept {
		range_base = selected;
		range_selection = true;
	}

	void DisableRangeSelection() noexcept {
		SetCursor(GetCursorIndex());
	}

	/**
	 * Moves the cursor.  Modifies the range if range selection is
	 * enabled.
	 */
	void MoveCursor(unsigned n) noexcept;

	void MoveCursorNext() noexcept;
	void MoveCursorPrevious() noexcept;
	void MoveCursorTop() noexcept;
	void MoveCursorMiddle() noexcept;
	void MoveCursorBottom() noexcept;
	void MoveCursorFirst() noexcept;
	void MoveCursorLast() noexcept;
	void MoveCursorNextPage() noexcept;
	void MoveCursorPreviousPage() noexcept;

	void ScrollUp(unsigned n) noexcept;
	void ScrollDown(unsigned n) noexcept;

	void ScrollNextPage() noexcept;
	void ScrollPreviousPage() noexcept;

	void ScrollNextHalfPage() noexcept;
	void ScrollPreviousHalfPage() noexcept;

	void ScrollToBottom() noexcept {
		start = length > GetHeight()
			? GetLength() - GetHeight()
			: 0;
	}

	/**
	 * Ensures that the cursor is visible on the screen, i.e. it is not
	 * outside the current scrolling range.
	 */
	void FetchCursor() noexcept;

	/**
	 * Determines the lower and upper bound of the range selection.  If
	 * range selection is disabled, it returns the cursor position (range
	 * length is 1).
	 */
	[[gnu::pure]]
	ListWindowRange GetRange() const noexcept;

	template<typename ItemList>
	[[gnu::pure]]
	auto GetCursorHash(const ItemList &items) const noexcept -> decltype(items.front().GetHash()) {
		if (IsSingleCursor())
			return items[GetCursorIndex()].GetHash();
		else
			return {};
	}

	template<typename ItemList>
	bool SetCursorHash(const ItemList &items,
			   decltype(items.front().GetHash()) hash) noexcept {
		if (hash == decltype(hash){})
			return false;

		unsigned idx = 0;
		for (const auto &item : items) {
			if (item.GetHash() == hash) {
				SetCursor(idx);
				return true;
			}

			++idx;
		}

		return false;
	}

private:
	/**
	 * Clamp the scroll-offset setting to slightly less than half
	 * of the screen height.
	 */
	static constexpr unsigned ClampScrollOffset(unsigned scroll_offset,
						    unsigned height) noexcept
	{
		return std::min(scroll_offset, height / 2);
	}

	[[gnu::pure]]
	unsigned ValidateIndex(unsigned i) const noexcept;

	void CheckSelected() noexcept;

	/**
	 * Scroll after the cursor was moved, the list was changed or
	 * the window was resized.
	 */
	void CheckOrigin() noexcept {
		ScrollTo(selected);
	}
};
