/*
 * \brief  Terminal service
 * \author Norman Feske
 * \date   2011-08-11
 */

/*
 * Copyright (C) 2011-2019 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.
 */

/* Genode includes */
#include <base/component.h>
#include <base/log.h>
#include <base/heap.h>
#include <gui_session/connection.h>
#include <timer_session/connection.h>
#include <base/attached_rom_dataspace.h>
#include <base/attached_ram_dataspace.h>
#include <input/event.h>
#include <os/reporter.h>
#include <os/vfs.h>
#include <gems/vfs_font.h>
#include <gems/cached_font.h>

/* terminal includes */
#include <terminal/decoder.h>
#include <terminal/types.h>

/* local includes */
#include "text_screen_surface.h"
#include "session.h"

namespace Terminal { struct Main; }


struct Terminal::Main : Character_consumer
{
	/*
	 * Noncopyable
	 */
	Main(Main const &);
	Main &operator = (Main const &);

	Env &_env;

	Attached_rom_dataspace _config { _env, "config" };

	Heap _heap { _env.ram(), _env.rm() };

	Root_directory _root_dir = _config.node().with_sub_node("vfs",
		[&] (Node const &config) -> Root_directory {
			return { _env, _heap, config }; },
		[&] () -> Root_directory {
			error("VFS not configured");
			return { _env, _heap, Node() }; });

	Cached_font::Limit _font_cache_limit { 0 };

	struct Font
	{
		Vfs_font    _vfs_font;
		Cached_font _cached_font;

		Font(Allocator &alloc, Directory &root_dir, Cached_font::Limit limit)
		:
			_vfs_font(alloc, root_dir, "font/monospace/regular"),
			_cached_font(alloc, _vfs_font, limit)
		{ }

		Text_painter::Font const &font() const { return _cached_font; }
	};

	Constructible<Font> _font { };

	void _handle_glyphs_changed()
	{
		/*
		 * Prevent call of '_handle_config' when the watch handler triggers
		 * at construction time.
		 */
		if (_font.constructed())
			_config_handler.local_submit();
	}

	/*
	 * XXX Currently an I/O-level watch handler is used
	 *     to prevent a config/watch handler cycle as
	 *     side-effect of '_root_dir.apply_config()' with
	 *     an application-level watch handler.
	 */
	Io::Watch_handler<Main> _glyphs_changed_handler {
		_root_dir, "font/monospace/regular/glyphs", *this,
		&Main::_handle_glyphs_changed };

	Color_palette _color_palette { };

	Constructible<Attached_rom_dataspace> _clipboard_rom      { };
	Constructible<Expanding_reporter>     _clipboard_reporter { };

	void _handle_config();

	Signal_handler<Main> _config_handler {
		_env.ep(), *this, &Main::_handle_config };

	Gui::Area _initial_mode { };

	Gui::Rect _gui_window_rect()
	{
		return _gui.window().convert<Gui::Rect>(
			[&] (Gui::Rect rect) { return rect; },
			[&] (Gui::Undefined) {
				Gui::Area const area =
					_initial_mode.valid() ? _initial_mode
					                      : Gui::Area { 1, 1 };
				return Gui::Rect { { }, area };
			});
	}

	void _handle_mode_change()
	{
		Rect const orig_win_rect = _win_rect;

		_win_rect = _gui_window_rect();

		/* shrink view before shrinking the buffer to prevent tiling artifacts */
		Rect const intersection = Rect::intersect(orig_win_rect, _win_rect);
		if (intersection != orig_win_rect) {
			_gui.enqueue<Gui::Session::Command::Geometry>(_view, intersection);
			_gui.execute();
		}

		_handle_config();
	}

	Signal_handler<Main> _mode_change_handler {
		_env.ep(), *this, &Main::_handle_mode_change };

	Gui::Connection   _gui   { _env };
	Timer::Connection _timer { _env };

	Constructible<Attached_dataspace> _fb_ds { };

	Gui::Rect _win_rect { };

	Gui::View_id _view { };

	Point _pointer { }; /* pointer positon in pixels */

	bool _shift_pressed = false, _alt_pressed = false;

	unsigned _ctrl_pressed = 0; /* number of control keys pressed */

	bool _selecting = false;

	using PT = Pixel_rgb888;

	Constructible<Text_screen_surface<PT>> _text_screen_surface { };

	Area _terminal_size { };

	/*
	 * Time in milliseconds between a change of the terminal content and the
	 * update of the pixels. By delaying the update, multiple intermediate
	 * changes result in only one rendering step.
	 */
	uint64_t const _flush_delay = 5;

	bool _flush_scheduled = false;

	bool _focused = false;

	Gui::Rect _flushed_win_rect { };

	void _handle_flush()
	{
		_flush_scheduled = false;

		if (_text_screen_surface.constructed() && _fb_ds.constructed()) {

			Surface<PT> surface(_fb_ds->local_addr<PT>(), _win_rect.area);

			Rect const dirty =
				_text_screen_surface->redraw(surface, { .focused = _focused });

			_gui.framebuffer.refresh(dirty);
		}

		/* update view geometry after mode change */
		if (_win_rect != _flushed_win_rect) {

			using Command = Gui::Session::Command;
			_gui.enqueue<Command::Geometry>(_view, _win_rect);
			_gui.enqueue<Command::Front>(_view);
			_gui.execute();

			_flushed_win_rect = _win_rect;
		}
	}

	Signal_handler<Main> _flush_handler {
		_env.ep(), *this, &Main::_handle_flush };

	void _schedule_flush()
	{
		if (!_flush_scheduled) {
			_timer.trigger_once((uint64_t)1000*_flush_delay);
			_flush_scheduled = true;
		}
	}

	/**
	 * Character_consumer interface, called from 'Terminal::Session_component'
	 */
	void consume_character(Character c) override
	{
		// XXX distinguish between normal and alternative display mode (smcup)
		if (_text_screen_surface.constructed())
			_text_screen_surface->apply_character(c);

		_schedule_flush();
	}

	/* input read buffer */
	Read_buffer _read_buffer { };

	/* create root interface for service */
	Root_component _root { _env, _heap, _read_buffer, *this };

	void _handle_input();

	Signal_handler<Main> _input_handler {
		_env.ep(), *this, &Main::_handle_input };

	void _report_clipboard_selection();
	void _paste_clipboard_content();

	Main(Env &env) : _env(env)
	{
		_gui.view(_view, { });

		_timer .sigh(_flush_handler);
		_config.sigh(_config_handler);

		_gui.input.sigh(_input_handler);
		_gui.info_sigh(_mode_change_handler);

		/* apply initial size from config, if provided */
		_config.node().with_optional_sub_node("initial", [&] (Node const &initial) {
			_initial_mode = { initial.attribute_value("width",  _win_rect.w()),
			                  initial.attribute_value("height", _win_rect.h()) };
		});

		_win_rect = _gui_window_rect();

		_handle_config();

		/* announce service at our parent */
		_env.parent().announce(_env.ep().manage(_root));
	}
};


void Terminal::Main::_handle_config()
{
	_config.update();

	Node const config = _config.node();

	_color_palette.apply_config(config);

	_font.destruct();

	_config.node().with_optional_sub_node("vfs", [&] (Node const &vfs_config) {
		_root_dir.apply_config(vfs_config); });

	Cached_font::Limit const cache_limit {
		config.attribute_value("cache", Number_of_bytes(256*1024)) };

	_font.construct(_heap, _root_dir, cache_limit);

	_clipboard_reporter.conditional(config.attribute_value("copy", false),
	                                _env, "clipboard", "clipboard");

	_clipboard_rom.conditional(config.attribute_value("paste", false),
	                           _env, "clipboard");

	/*
	 * Adapt terminal to font or framebuffer mode changes
	 */
	_gui.buffer({ .area = _win_rect.area, .alpha = false });

	if (_win_rect.valid())
		_fb_ds.construct(_env.rm(), _gui.framebuffer.dataspace());

	/*
	 * Distinguish the case where the framebuffer change affects the character
	 * grid size from the case where merely the pixel position of the character
	 * grid within the framebuffer changed.
	 *
	 * In the former case, the text-screen surface is reallocated and cleared.
	 * Clients (like ncurses) are expected to respond to a terminal-size change
	 * with a redraw. In the latter case, the client would skip the redraw. So
	 * we need to preserve the content and just reposition the character grid.
	 */

	try {
		Text_screen_surface<PT>::Geometry const new_geometry(_font->font(), _win_rect.area);

		bool const reconstruct = !_text_screen_surface.constructed() ||
		                          _text_screen_surface->size() != new_geometry.size();
		if (reconstruct) {

			using Snapshot = Text_screen_surface<PT>::Snapshot;
			Constructible<Snapshot> snapshot { };

			size_t const snapshot_bytes  = _text_screen_surface.constructed()
			                             ? Snapshot::bytes_needed(*_text_screen_surface)
			                             : 0,
			             preserved_bytes = 32*1024,
			             needed_bytes    = snapshot_bytes + preserved_bytes,
			             avail_bytes     = _env.pd().avail_ram().value;

			bool const preserve_content = (needed_bytes < avail_bytes);

			if (!preserve_content)
				warning("not enough spare RAM to preserve content (",
				        "need ", Number_of_bytes(needed_bytes), ", "
				        "have ", Number_of_bytes(avail_bytes), ")");

			if (preserve_content && _text_screen_surface.constructed())
				snapshot.construct(_heap, *_text_screen_surface);

			Position const orig_cursor_pos = _text_screen_surface.constructed()
			                               ? _text_screen_surface->cursor_pos()
			                               : Position();

			_text_screen_surface.construct(_heap, _font->font(),
			                               _color_palette, _win_rect.area);

			if (snapshot.constructed())
				_text_screen_surface->import(*snapshot);

			_text_screen_surface->cursor_pos(orig_cursor_pos);

			_terminal_size = _text_screen_surface->size();

		} else {
			_text_screen_surface->geometry(new_geometry);
		}
	}
	catch (Text_screen_surface<PT>::Geometry::Invalid) {

		/*
		 * Make sure to never operate on an invalid-sized framebuffer
		 *
		 * If the exception is thrown by the construction of 'new_geometry',
		 * there may still be a stale '_text_screen_surface'.
		 */
		_text_screen_surface.destruct();
		_terminal_size = Area(0, 0);
	}

	_root.notify_resized(Session::Size(_terminal_size.w, _terminal_size.h));
	_schedule_flush();
}


void Terminal::Main::_handle_input()
{
	if (!_text_screen_surface.constructed())
		return;

	_gui.input.for_each_event([&] (Input::Event const &event) {

		bool const orig_focused = _focused;

		if (event.focus_enter()) _focused = true;
		if (event.focus_leave()) _focused = false;

		if (orig_focused != _focused) {
			_text_screen_surface->focus_changed();
			_schedule_flush();
		}

		event.handle_absolute_motion([&] (int x, int y) {

			_pointer = Point(x, y);

			if (_shift_pressed) {
				_text_screen_surface->pointer(_pointer);
				_schedule_flush();
			}

			if (_selecting) {
				_text_screen_surface->define_selection(_pointer);
				_schedule_flush();
			}
		});

		if (event.key_press(Input::KEY_LEFTSHIFT)) {
			if (_clipboard_reporter.constructed()) {
				_shift_pressed = true;
				_text_screen_surface->clear_selection();
				_text_screen_surface->pointer(_pointer);
				_schedule_flush();
			}
		}

		if (event.key_release(Input::KEY_LEFTSHIFT)) {
			_shift_pressed = false;
			_text_screen_surface->pointer(Point(-1, -1));
			_schedule_flush();
		}

		if (event.key_press  (Input::KEY_LEFTALT)) _alt_pressed = true;
		if (event.key_release(Input::KEY_LEFTALT)) _alt_pressed = false;

		if (event.key_press(Input::BTN_LEFT)) {
			if (_shift_pressed) {
				_selecting = true;
				_text_screen_surface->start_selection(_pointer);
			} else {
				_text_screen_surface->clear_selection();
			}
			_schedule_flush();
		}

		if (event.key_release(Input::BTN_LEFT)) {
			if (_selecting) {
				_selecting = false;
				_report_clipboard_selection();
			}
		}

		if (event.key_press(Input::KEY_LEFTCTRL)
		 || event.key_press(Input::KEY_RIGHTCTRL))
			++_ctrl_pressed;

		if (event.key_release(Input::KEY_LEFTCTRL)
		 || event.key_release(Input::KEY_RIGHTCTRL))
			--_ctrl_pressed;

		if (event.key_press(Input::BTN_MIDDLE))
			_paste_clipboard_content();

		event.handle_press([&] (Input::Keycode, Codepoint codepoint) {

			/* add ESC prefix to characters typed while alt is held */
			if (_alt_pressed && codepoint.valid())
				_read_buffer.add(27);

			/* control-key combinations */
			if (_ctrl_pressed
			 && codepoint.value >= 'a' && codepoint.value <= 'z') {
				_read_buffer.add(Codepoint { codepoint.value - 'a' + 1 } );
				return;
			}

			/* function-key unicodes */
			enum {
				CODEPOINT_UP     = 0xf700, CODEPOINT_DOWN     = 0xf701,
				CODEPOINT_LEFT   = 0xf702, CODEPOINT_RIGHT    = 0xf703,
				CODEPOINT_F1     = 0xf704, CODEPOINT_F2       = 0xf705,
				CODEPOINT_F3     = 0xf706, CODEPOINT_F4       = 0xf707,
				CODEPOINT_F5     = 0xf708, CODEPOINT_F6       = 0xf709,
				CODEPOINT_F7     = 0xf70a, CODEPOINT_F8       = 0xf70b,
				CODEPOINT_F9     = 0xf70c, CODEPOINT_F10      = 0xf70d,
				CODEPOINT_F11    = 0xf70e, CODEPOINT_F12      = 0xf70f,
				CODEPOINT_HOME   = 0xf729, CODEPOINT_INSERT   = 0xf727,
				CODEPOINT_DELETE = 0xf728, CODEPOINT_END      = 0xf72b,
				CODEPOINT_PAGEUP = 0xf72c, CODEPOINT_PAGEDOWN = 0xf72d,
			};

			char const *special_sequence = nullptr;
			switch (codepoint.value) {
			case CODEPOINT_UP:       special_sequence = "\EOA";   break;
			case CODEPOINT_DOWN:     special_sequence = "\EOB";   break;
			case CODEPOINT_LEFT:     special_sequence = "\EOD";   break;
			case CODEPOINT_RIGHT:    special_sequence = "\EOC";   break;
			case CODEPOINT_F1:       special_sequence = "\EOP";   break;
			case CODEPOINT_F2:       special_sequence = "\EOQ";   break;
			case CODEPOINT_F3:       special_sequence = "\EOR";   break;
			case CODEPOINT_F4:       special_sequence = "\EOS";   break;
			case CODEPOINT_F5:       special_sequence = "\E[15~"; break;
			case CODEPOINT_F6:       special_sequence = "\E[17~"; break;
			case CODEPOINT_F7:       special_sequence = "\E[18~"; break;
			case CODEPOINT_F8:       special_sequence = "\E[19~"; break;
			case CODEPOINT_F9:       special_sequence = "\E[20~"; break;
			case CODEPOINT_F10:      special_sequence = "\E[21~"; break;
			case CODEPOINT_F11:      special_sequence = "\E[23~"; break;
			case CODEPOINT_F12:      special_sequence = "\E[24~"; break;
			case CODEPOINT_HOME:     special_sequence = "\E[1~";  break;
			case CODEPOINT_INSERT:   special_sequence = "\E[2~";  break;
			case CODEPOINT_DELETE:   special_sequence = "\E[3~";  break;
			case CODEPOINT_END:      special_sequence = "\E[4~";  break;
			case CODEPOINT_PAGEUP:   special_sequence = "\E[5~";  break;
			case CODEPOINT_PAGEDOWN: special_sequence = "\E[6~";  break;
			};

			if (special_sequence)
				_read_buffer.add(special_sequence);
			else if (codepoint.valid())
				_read_buffer.add(codepoint);
		});
	});
}


void Terminal::Main::_report_clipboard_selection()
{
	if (!_text_screen_surface.constructed() || !_clipboard_reporter.constructed())
		return;

	_clipboard_reporter->generate([&] (Generator &g) {
		_text_screen_surface->for_each_selected_character([&] (Codepoint c) {
			String<10> const utf8(c);
			if (utf8.valid())
				g.append_quoted(utf8.string(), utf8.length() - 1);
		});
	});
}


void Terminal::Main::_paste_clipboard_content()
{
	if (!_clipboard_rom.constructed())
		return;

	_clipboard_rom->update();

	if (_clipboard_rom->node().num_bytes() >= (size_t)_read_buffer.avail_capacity()) {
		warning("clipboard content exceeds read-buffer capacity");
		return;
	}

	_clipboard_rom->node().for_each_quoted_line([&] (auto const &line) {
		using Line = String<1000>;
		Line const unquoted { line };
		for (Utf8_ptr utf8(unquoted.string()); utf8.complete(); utf8 = utf8.next()) {
			Codepoint const c = utf8.codepoint();

			/* filter out control characters */
			if (c.value < 32 && c.value != 10)
				continue;

			_read_buffer.add(c);
		}
		if (!line.last)
			_read_buffer.add('\n');
	});
}


void Component::construct(Genode::Env &env) { static Terminal::Main main(env); }
