/*
 * \brief  Nitpicker-based logging service
 * \author Norman Feske
 * \date   2006-09-18
 */

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

#include <util/arg_string.h>
#include <base/component.h>
#include <base/heap.h>
#include <base/rpc_server.h>
#include <base/session_label.h>
#include <root/component.h>
#include <log_session/log_session.h>
#include <nitpicker_session/connection.h>
#include <timer_session/connection.h>
#include <input/event.h>
#include <os/pixel_rgb565.h>

/*
 * Nitpicker's graphics backend
 */
#include <nitpicker_gfx/box_painter.h>
#include <nitpicker_gfx/tff_font.h>


enum { LOG_W = 80 };  /* number of visible characters per line */
enum { LOG_H = 25 };  /* number of lines of log window         */

typedef Text_painter::Font          Font;
typedef Genode::Surface_base::Point Point;
typedef Genode::Surface_base::Area  Area;
typedef Genode::Surface_base::Rect  Rect;
typedef Genode::Color               Color;


/*
 * Builtin font
 */
extern char _binary_mono_tff_start[];


namespace Nitlog {

	class Session_component;
	class Root;
	struct Main;

	using namespace Genode;
}




/**
 * Pixel-type-independent interface to graphics backend
 */
struct Canvas_base : Genode::Interface
{
	virtual void draw_string(Point, Font const &, Color, char const *) = 0;

	virtual void draw_box(Rect, Color) = 0;
};


/**
 * Pixel-type-specific graphics backend
 */
template <typename PT>
class Canvas : public Canvas_base
{
	private:

		Genode::Surface<PT> _surface;

	public:

		Canvas(PT *base, Area size) : _surface(base, size) { }

		void clip(Rect rect) { _surface.clip(rect); }

		void draw_string(Point p, Font const &font, Color color,
		                 char const *sstr) override
		{
			Text_painter::paint(_surface, Text_painter::Position(p.x(), p.y()),
			                    font, color, sstr);
		}

		void draw_box(Rect rect, Color color) override
		{
			Box_painter::paint(_surface, rect, color);
		}
};


class Log_entry
{
	private:

		typedef Genode::Color Color;

		char  _label[64];
		char  _text[LOG_W];
		char  _attr[LOG_W];
		Color _color { };
		int   _label_len = 0;
		int   _text_len  = 0;
		int   _id        = 0;

	public:

		/**
		 * Constructors
		 *
		 * The default constructor is used to build an array of log entries.
		 */
		Log_entry() { }

		Log_entry(Genode::Color color, const char *label, const char *log_text, const char *log_attr, int id):
			_color(color), _id(id)
		{
			Genode::strncpy(_label, label,    sizeof(_label));
			Genode::strncpy(_text,  log_text, sizeof(_text));

			_label_len = Genode::strlen(_label);
			_text_len  = Genode::strlen(_text);

			/* replace line feed at the end of the text with a blank */
			if (_text_len > 0 && _text[_text_len - 1] == '\n')
				_text[_text_len - 1] = ' ';

			Genode::memcpy(_attr, log_attr, _text_len);
		}

		/**
		 * Draw entry
		 *
		 * An entry consists of a label and text. The argument 'new_section'
		 * marks a transition of output from one session to another. This
		 * information is used to separate sessions visually.
		 */
		void draw(Canvas_base &canvas, Font const &font, int y, int new_section = false)
		{
			Color label_fgcol = Color(Genode::min(255, _color.r + 200),
			                          Genode::min(255, _color.g + 200),
			                          Genode::min(255, _color.b + 200));
			Color label_bgcol = Color(_color.r, _color.g, _color.b);
			Color text_fgcol  = Color(180, 180, 180);
			Color text_bgcol  = Color(_color.r / 2, _color.g / 2, _color.b / 2);

			/* calculate label dimensions */
			int label_w = font.string_width(_label).decimal();
			int label_h = font.bounding_box().h();

			if (new_section) {
				canvas.draw_box(Rect(Point(1, y), Area(label_w + 2, label_h - 1)), label_bgcol);
				canvas.draw_string(Point(1, y - 1), font, label_fgcol, _label);
				canvas.draw_box(Rect(Point(1, y + label_h - 1), Area(label_w + 2, 1)), Color(0, 0, 0));
				canvas.draw_box(Rect(Point(label_w + 2, y), Area(1, label_h - 1)), _color);
				canvas.draw_box(Rect(Point(label_w + 3, y), Area(1, label_h - 1)), Color(0, 0, 0));
				canvas.draw_box(Rect(Point(label_w + 4, y), Area(1000, label_h)), text_bgcol);
				canvas.draw_box(Rect(Point(label_w + 4, y), Area(1000, 1)), Color(0, 0, 0));
			} else
				canvas.draw_box(Rect(Point(1, y), Area(1000, label_h)), text_bgcol);

			/* draw log text */
			canvas.draw_string(Point(label_w + 6, y), font, text_fgcol, _text);
		}

		/**
		 * Accessors
		 */
		int label_len() { return _label_len; }
		int id()        { return _id; }
};


class Log_window
{
	private:

		Canvas_base &_canvas;
		Font  const &_font;
		Log_entry    _entries[LOG_H];    /* log entries                           */
		int          _dst_entry = 0;     /* destination entry for next write      */
		int          _view_pos  = 0;     /* current view port on the entry array  */
		bool         _scroll    = false; /* scroll mode (when text hits bottom)   */
		char         _attr[LOG_W];       /* character attribute buffer            */
		bool         _dirty     = true;  /* schedules the log window for a redraw */
		Genode::Lock _dirty_lock { };

	public:

		/**
		 * Constructor
		 */
		Log_window(Canvas_base &canvas, Font const &font)
		: _canvas(canvas), _font(font) { }

		/**
		 * Write log entry
		 *
		 * \param color  base color for highlighting the session.
		 * \param sid    unique ID of the log session. This ID is used to
		 *               determine section transitions in the log output.
		 */
		void write(Genode::Color color, const char *label,
		           const char *log_text, int sid)
		{
			_entries[_dst_entry] = Log_entry(color, label, log_text, _attr, sid);

			if (_scroll)
				_view_pos++;

			/* cycle through log entries */
			_dst_entry = (_dst_entry + 1) % LOG_H;

			/* start scrolling when the dst entry wraps for the first time */
			if (_dst_entry == 0)
				_scroll = true;

			/* schedule log window for redraw */
			Genode::Lock::Guard lock_guard(_dirty_lock);
			_dirty |= 1;
		}

		/**
		 * Draw log window
		 *
		 * \retval true  drawing operations had been performed
		 */
		bool draw()
		{
			{
				Genode::Lock::Guard lock_guard(_dirty_lock);
				if (!_dirty) return false;
				_dirty = false;
			}

			int line_h = _font.bounding_box().h();
			int curr_session_id = -1;

			for (int i = 0, y = 0; i < LOG_H; i++, y += line_h) {
				Log_entry *le = &_entries[(i + _view_pos) % LOG_H];
				le->draw(_canvas, _font, y, curr_session_id != le->id());
				curr_session_id = le->id();
			}

			return true;
		}
};


class Nitlog::Session_component : public Rpc_object<Log_session>
{
	private:

		Log_window &_log_window;

		Session_label const _label;

		int const _id;

		static int _bit(int v, int bit_num) { return (v >> bit_num) & 1; }

		/**
		 * Compute session color
		 */
		static Color _session_color(int id)
		{
			int const scale  = 32;
			int const offset = 64;

			int r = (_bit(id, 3) + 2*_bit(id, 0))*scale + offset;
			int g = (_bit(id, 4) + 2*_bit(id, 1))*scale + offset;
			int b = (_bit(id, 5) + 2*_bit(id, 2))*scale + offset;

			return Color(r, g, b);
		}

		Color const _color = _session_color(_id);

	public:

		/**
		 * Constructor
		 */
		Session_component(Session_label const &label,
		                  Log_window &log_window, int &cnt)
		:
			_log_window(log_window), _label(label), _id(cnt++)
		{ }


		/***************************
		 ** Log session interface **
		 ***************************/

		size_t write(String const &log_text) override
		{
			if (!log_text.valid_string()) {
				error("corrupted string");
				return 0;
			}

			_log_window.write(_color, _label.string(), log_text.string(), _id);
			return strlen(log_text.string());
		}
};


class Nitlog::Root : public Root_component<Session_component>
{
	private:

		Log_window &_log_window;

		/* session counter, used as a key to generate session colors */
		int _session_cnt = 0;

	protected:

		Session_component *_create_session(const char *args) override
		{
			log("create log session args: ", args);

			return new (md_alloc())
				Session_component(label_from_args(args),
				                  _log_window, _session_cnt);
		}

	public:

		/**
		 * Constructor
		 */
		Root(Entrypoint &ep, Allocator &md_alloc, Log_window &log_window)
		:
			Root_component<Session_component>(ep, md_alloc),
			_log_window(log_window)
		{ }
};


class Log_view
{
	private:

		Nitpicker::Session_client      &_nitpicker;
		Nitpicker::Point                _pos;
		Nitpicker::Area                 _size;
		Nitpicker::Session::View_handle _handle;

		typedef Nitpicker::Session::Command     Command;
		typedef Nitpicker::Session::View_handle View_handle;

	public:

		Log_view(Nitpicker::Session_client &nitpicker, Nitpicker::Rect geometry)
		:
			_nitpicker(nitpicker),
			_pos(geometry.p1()),
			_size(geometry.area()),
			_handle(nitpicker.create_view())
		{
			move(_pos);
			top();
		}

		void top()
		{
			_nitpicker.enqueue<Command::To_front>(_handle, View_handle());
			_nitpicker.execute();
		}

		void move(Nitpicker::Point pos)
		{
			_pos = pos;

			Nitpicker::Rect rect(_pos, _size);
			_nitpicker.enqueue<Command::Geometry>(_handle, rect);
			_nitpicker.execute();
		}

		Nitpicker::Point pos() const { return _pos; }
};


struct Nitlog::Main
{
	Env &_env;

	Tff_font::Static_glyph_buffer<4096> _glyph_buffer { };

	Tff_font _font { _binary_mono_tff_start, _glyph_buffer };

	/* calculate size of log view in pixels */
	unsigned const _win_w = _font.bounding_box().w() * LOG_W + 2;
	unsigned const _win_h = _font.bounding_box().h() * LOG_H + 2;

	/* init sessions to the required external services */
	Nitpicker::Connection _nitpicker { _env };
	Timer::Connection     _timer     { _env };

	void _init_nitpicker_buffer()
	{
		_nitpicker.buffer(Framebuffer::Mode(_win_w, _win_h,
		                  Framebuffer::Mode::RGB565), false);
	}

	bool const _nitpicker_buffer_initialized = (_init_nitpicker_buffer(), true);

	Sliced_heap _sliced_heap { _env.ram(), _env.rm() };

	/* create log window */
	Attached_dataspace _fb_ds { _env.rm(), _nitpicker.framebuffer()->dataspace() };

	Canvas<Pixel_rgb565> _canvas { _fb_ds.local_addr<Pixel_rgb565>(),
	                               ::Area(_win_w, _win_h) };

	Log_window _log_window { _canvas, _font };

	void _init_canvas()
	{
		/*
		 * We clip a border of one pixel off the canvas. This way, the
		 * border remains unaffected by the drawing operations and
		 * acts as an outline for the log window.
		 */
		_canvas.clip(::Rect(::Point(1, 1), ::Area(_win_w - 2, _win_h - 2)));
	}

	bool const _canvas_initialized = (_init_canvas(), true);

	/* create view for log window */
	Nitpicker::Rect const _view_geometry { Nitpicker::Point(20, 20),
	                                       Nitpicker::Area(_win_w, _win_h) };
	Log_view _view { _nitpicker, _view_geometry };

	/* create root interface for service */
	Root _root { _env.ep(), _sliced_heap, _log_window };

	Attached_dataspace _ev_ds { _env.rm(), _nitpicker.input()->dataspace() };

	Nitpicker::Point const _initial_mouse_pos { -1, -1 };

	Nitpicker::Point _old_mouse_pos = _initial_mouse_pos;

	unsigned _key_cnt = 0;

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

	void _handle_input()
	{
		Input::Event const *ev_buf = _ev_ds.local_addr<Input::Event const>();

		for (int i = 0, num_ev = _nitpicker.input()->flush(); i < num_ev; i++) {

			Input::Event const &ev = ev_buf[i];

			if (ev.press())   _key_cnt++;
			if (ev.release()) _key_cnt--;

			/* move view */
			ev.handle_absolute_motion([&] (int x, int y) {

				Nitpicker::Point const mouse_pos(x, y);

				if (_key_cnt && _old_mouse_pos != _initial_mouse_pos)
					_view.move(_view.pos() + mouse_pos - _old_mouse_pos);

				_old_mouse_pos = mouse_pos;
			});

			/* find selected view and bring it to front */
			if (ev.press() && _key_cnt == 1)
				_view.top();

		}
	}

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

	void _handle_timer()
	{
		if (_log_window.draw())
			_nitpicker.framebuffer()->refresh(0, 0, _win_w, _win_h);
	}

	Main(Env &env) : _env(env)
	{
		/* announce service at our parent */
		_env.parent().announce(_env.ep().manage(_root));

		_timer.sigh(_timer_handler);
		_timer.trigger_periodic(20*1000);

		_nitpicker.input()->sigh(_input_handler);
	}
};


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