/*
 * \brief  Window layouter
 * \author Norman Feske
 * \date   2013-02-14
 */

/*
 * Copyright (C) 2013-2017 Genode Labs GmbH
 *
 * This file is part of the Genode OS framework, which is distributed
 * under the terms of the GNU Affero General Public License version 3.
 */

#ifndef _USER_STATE_H_
#define _USER_STATE_H_

/* local includes */
#include "operations.h"
#include "key_sequence_tracker.h"

namespace Window_layouter { class User_state; }


class Window_layouter::User_state
{
	public:

		struct Hover_state
		{
			Window_id       window_id;
			Window::Element element { Window::Element::UNDEFINED };

			Hover_state(Window_id id, Window::Element element)
			:
				window_id(id), element(element)
			{ }

			bool operator != (Hover_state const &other) const
			{
				return window_id != other.window_id
				    || element   != other.element;
			}
		};

	private:

		Window_id _hovered_window_id { };
		Window_id _focused_window_id { };
		Window_id _dragged_window_id { };

		unsigned  _key_cnt = 0;

		Key_sequence_tracker _key_sequence_tracker { };

		Window::Element _hovered_element = Window::Element::UNDEFINED;
		Window::Element _dragged_element = Window::Element::UNDEFINED;

		/*
		 * True while drag operation in progress
		 */
		bool _drag_state = false;

		/*
		 * False if the hover state (hovered window and element) was not known
		 * at the initial click of a drag operation. In this case, the drag
		 * operation is initiated as soon as the hover state becomes known.
		 */
		bool _drag_init_done = false;

		/*
		 * Pointer position at the beginning of a drag operation
		 */
		Point _pointer_clicked { };

		/*
		 * Current pointer position
		 */
		Point _pointer_curr { };

		Operations &_operations;

		Focus_history &_focus_history;

		/*
		 * Return true if key is potentially part of a key sequence
		 */
		static bool _key(Input::Keycode key) { return key != Input::BTN_LEFT; }

		bool _key(Input::Event const &ev) const
		{
			bool relevant = false;

			ev.handle_press([&] (Input::Keycode key, Codepoint) {
				relevant |= _key(key); });

			ev.handle_release([&] (Input::Keycode key) {
				relevant |= _key(key); });

			return relevant;
		}

		inline void _handle_event(Input::Event const &, Xml_node);

		void _initiate_drag(Window_id       hovered_window_id,
		                    Window::Element hovered_element)
		{
			/*
			 * This function must never be called without the hover state to be
			 * defined. This assertion checks this precondition.
			 */
			if (!hovered_window_id.valid()) {
				struct Drag_with_undefined_hover_state { };
				throw  Drag_with_undefined_hover_state();
			}

			_drag_init_done    = true;
			_dragged_window_id = hovered_window_id;
			_dragged_element   = hovered_element;

			/*
			 * Toggle maximized (fullscreen) state
			 */
			if (_hovered_element == Window::Element::MAXIMIZER) {

				_dragged_window_id = _hovered_window_id;
				_focused_window_id = _hovered_window_id;
				_focus_history.focus(_focused_window_id);

				_operations.toggle_fullscreen(_hovered_window_id);

				_hovered_element   = Window::Element::UNDEFINED;
				_hovered_window_id = Window_id();
				return;
			}

			/*
			 * Bring hovered window to front when clicked
			 */
			if (_focused_window_id != _hovered_window_id) {

				_focused_window_id = _hovered_window_id;
				_focus_history.focus(_focused_window_id);

				_operations.to_front(_hovered_window_id);
				_operations.focus(_hovered_window_id);
			}

			_operations.drag(_dragged_window_id, _dragged_element,
			                 _pointer_clicked, _pointer_curr);
		}

	public:

		User_state(Operations &operations, Focus_history &focus_history)
		:
			_operations(operations), _focus_history(focus_history)
		{ }

		void handle_input(Input::Event const events[], unsigned num_events,
		                  Xml_node const &config)
		{
			Point const pointer_last = _pointer_curr;

			for (size_t i = 0; i < num_events; i++)
				_handle_event(events[i], config);

			/*
			 * Issue drag operation when in dragged state
			 */
			if (_drag_state && _drag_init_done && (_pointer_curr != pointer_last))
				_operations.drag(_dragged_window_id, _dragged_element,
				                 _pointer_clicked, _pointer_curr);
		}

		void hover(Window_id window_id, Window::Element element)
		{
			Window_id const last_hovered_window_id = _hovered_window_id;

			_hovered_window_id = window_id;
			_hovered_element   = element;

			/*
			 * Check if we have just received an update while already being in
			 * dragged state.
			 *
			 * This can happen when the user selects a new nitpicker domain by
			 * clicking on a window decoration. Prior the click, the new
			 * session is not aware of the current mouse position. So the hover
			 * model is not up to date. As soon as nitpicker assigns the focus
			 * to the new session and delivers the corresponding press event,
			 * we enter the drag state (in the 'handle_input' function. But we
			 * don't know which window is dragged until the decorator updates
			 * the hover model. Now, when the model is updated and we are still
			 * in dragged state, we can finally initiate the window-drag
			 * operation for the now-known window.
			 */
			if (_drag_state && !_drag_init_done && _hovered_window_id.valid())
				_initiate_drag(_hovered_window_id, _hovered_element);

			/*
			 * Let focus follows the pointer
			 *
			 * XXX obtain policy from config
			 */
			if (!_drag_state && _hovered_window_id.valid()
			 && _hovered_window_id != last_hovered_window_id) {

				_focused_window_id = _hovered_window_id;
				_focus_history.focus(_focused_window_id);
				_operations.focus(_focused_window_id);
			}
		}

		void reset_hover()
		{
			/* ignore hover resets when in drag state */
			if (_drag_state)
				return;

			_hovered_element   = Window::Element::UNDEFINED;
			_hovered_window_id = Window_id();
		}

		Window_id focused_window_id() const { return _focused_window_id; }

		void focused_window_id(Window_id id) { _focused_window_id = id; }

		Hover_state hover_state() const { return { _hovered_window_id, _hovered_element }; }
};


void Window_layouter::User_state::_handle_event(Input::Event const &e,
                                                Xml_node config)
{
	e.handle_absolute_motion([&] (int x, int y) {
		_pointer_curr = Point(x, y); });

	if (e.absolute_motion() || e.focus_enter()) {

		if (_drag_state && _drag_init_done)
			_operations.drag(_dragged_window_id, _dragged_element,
			                 _pointer_clicked, _pointer_curr);
	}

	/* track number of pressed buttons/keys */
	if (e.press())   _key_cnt++;
	if (e.release()) _key_cnt--;

	/* handle pointer click */
	if (e.key_press(Input::BTN_LEFT) && _key_cnt == 1) {

		/*
		 * Initiate drag operation if possible
		 */
		_drag_state      = true;
		_pointer_clicked = _pointer_curr;

		if (_hovered_window_id.valid()) {

			/*
			 * Initiate drag operation
			 *
			 * If the hovered window is known at the time of the press event,
			 * we can initiate the drag operation immediately. Otherwise,
			 * the initiation is deferred to the next update of the hover
			 * model.
			 */

			_initiate_drag(_hovered_window_id, _hovered_element);

		} else {

			/*
			 * If the hovering state is undefined at the time of the click,
			 * we defer the drag handling until the next update of the hover
			 * state. This intermediate state is captured by '_drag_init_done'.
			 */
			_drag_init_done    = false;
			_dragged_window_id = Window_id();
			_dragged_element   = Window::Element(Window::Element::UNDEFINED);
		}
	}

	/* detect end of drag operation */
	if (e.release() && _key_cnt == 0 && _dragged_window_id.valid()) {

		_drag_state = false;

		/*
		 * Issue resize to 0x0 when releasing the the window closer
		 */
		if (_dragged_element == Window::Element::CLOSER) {

			if (_dragged_element == _hovered_element)
				_operations.close(_dragged_window_id);
		}

		_operations.finalize_drag(_dragged_window_id, _dragged_element,
		                          _pointer_clicked, _pointer_curr);
	}

	/* handle key sequences */
	if (_key(e)) {

		if (e.press() && _key_cnt == 1)
			_key_sequence_tracker.reset();

		_key_sequence_tracker.apply(e, config, [&] (Action action) {

			switch (action.type()) {

			case Action::TOGGLE_FULLSCREEN:
				_operations.toggle_fullscreen(_focused_window_id);
				return;

			case Action::RAISE_WINDOW:
				_operations.to_front(_focused_window_id);
				return;

			case Action::NEXT_WINDOW:
				_focused_window_id = _focus_history.next(_focused_window_id);
				_operations.focus(_focused_window_id);
				return;

			case Action::PREV_WINDOW:
				_focused_window_id = _focus_history.prev(_focused_window_id);
				_operations.focus(_focused_window_id);
				return;

			default:
				warning("action ", (int)action.type(), " unhanded");
			}
		});
	}

	/* update focus history after key/button action is completed */
	if (e.release() && _key_cnt == 0)
		_focus_history.focus(_focused_window_id);
}

#endif /* _USER_STATE_H_ */
