/*
 * \brief  MuPDF for Genode
 * \author Norman Feske
 * \date   2012-01-09
 */

/*
 * Copyright (C) 2012-2018 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/env.h>
#include <libc/component.h>
#include <framebuffer_session/client.h>
#include <base/sleep.h>
#include <util/reconstructible.h>
#include <base/attached_rom_dataspace.h>
#include <util/geometry.h>
#include <input_session/client.h>
#include <input/event.h>
#include <input/keycodes.h>
#include <nitpicker_session/connection.h>

/* MuPDF includes */
extern "C" {
#include <fitz.h>
#include <mupdf.h>
#include <muxps.h>
#include <pdfapp.h>
}

/* libc includes */
#include <dirent.h>
#include <unistd.h>


/***************
 ** Dithering **
 ***************/

/*
 * XXX blatantly copied from 'demo/src/app/backdrop/main.cc'
 *
 * We should factor-out the dithering support into a separate header file.
 * But where is a good place to put it?
 */

enum { DITHER_SIZE = 16, DITHER_MASK = DITHER_SIZE - 1 };

static const int dither_matrix[DITHER_SIZE][DITHER_SIZE] = {
  {   0,192, 48,240, 12,204, 60,252,  3,195, 51,243, 15,207, 63,255 },
  { 128, 64,176,112,140, 76,188,124,131, 67,179,115,143, 79,191,127 },
  {  32,224, 16,208, 44,236, 28,220, 35,227, 19,211, 47,239, 31,223 },
  { 160, 96,144, 80,172,108,156, 92,163, 99,147, 83,175,111,159, 95 },
  {   8,200, 56,248,  4,196, 52,244, 11,203, 59,251,  7,199, 55,247 },
  { 136, 72,184,120,132, 68,180,116,139, 75,187,123,135, 71,183,119 },
  {  40,232, 24,216, 36,228, 20,212, 43,235, 27,219, 39,231, 23,215 },
  { 168,104,152, 88,164,100,148, 84,171,107,155, 91,167,103,151, 87 },
  {   2,194, 50,242, 14,206, 62,254,  1,193, 49,241, 13,205, 61,253 },
  { 130, 66,178,114,142, 78,190,126,129, 65,177,113,141, 77,189,125 },
  {  34,226, 18,210, 46,238, 30,222, 33,225, 17,209, 45,237, 29,221 },
  { 162, 98,146, 82,174,110,158, 94,161, 97,145, 81,173,109,157, 93 },
  {  10,202, 58,250,  6,198, 54,246,  9,201, 57,249,  5,197, 53,245 },
  { 138, 74,186,122,134, 70,182,118,137, 73,185,121,133, 69,181,117 },
  {  42,234, 26,218, 38,230, 22,214, 41,233, 25,217, 37,229, 21,213 },
  { 170,106,154, 90,166,102,150, 86,169,105,153, 89,165,101,149, 85 }
};


static inline uint16_t rgb565(int r, int g, int b)
{
	enum {
		R_MASK = 0xf800, R_LSHIFT = 8,
		G_MASK = 0x07e0, G_LSHIFT = 3,
		B_MASK = 0x001f, B_RSHIFT = 3
	};
	return ((r << R_LSHIFT) & R_MASK)
	     | ((g << G_LSHIFT) & G_MASK)
	     | ((b >> B_RSHIFT) & B_MASK);
}


static void convert_line_rgba_to_rgb565(const unsigned char *rgba_src,
                                        uint16_t *dst, int num_pixels, int line)
{
	using namespace Genode;

	enum { CHANNEL_MAX = 255 };

	int const *dm = dither_matrix[line & DITHER_MASK];

	for (int i = 0; i < num_pixels; i++) {
		int v = dm[i & DITHER_MASK] >> 5;

		*dst++ = rgb565(min(v + (int)rgba_src[0], (int)CHANNEL_MAX),
		                min(v + (int)rgba_src[1], (int)CHANNEL_MAX),
		                min(v + (int)rgba_src[2], (int)CHANNEL_MAX));

		/* we ignore the alpha channel */

		rgba_src += 4; /* next pixel */
	}
}


static int pdf_select(const struct dirent *d)
{
	char const *name = d->d_name;
	int n = strlen(name);
	return (n > 4)
		? (!strncmp(&name[n-4], ".pdf", 4))
		: 0;
}


/**************
 ** PDF view **
 **************/

class Pdf_view
{
	public:

		/**
		 * Exception types
		 */
		class Non_supported_framebuffer_mode { };
		class Unexpected_document_color_depth { };

		typedef uint16_t pixel_t;
		typedef Framebuffer::Mode Mode;

	private:

		enum { NO_ALPHA = false };

		Genode::Env &_env;

		Nitpicker::Connection  _nitpicker { _env };
		Framebuffer::Session  &_framebuffer = *_nitpicker.framebuffer();
		Input::Session_client &_input       = *_nitpicker.input();

		Framebuffer::Mode _nit_mode = _nitpicker.mode();
		Framebuffer::Mode  _fb_mode {};

		Genode::Constructible<Genode::Attached_dataspace> _fb_ds { };

		Genode::Signal_handler<Pdf_view> _nit_mode_handler {
			_env.ep(), *this, &Pdf_view::_handle_nit_mode };

		Genode::Signal_handler<Pdf_view> _sync_handler {
			_env.ep(), *this, &Pdf_view::_refresh };

		Genode::Signal_handler<Pdf_view> _input_handler {
			_env.ep(), *this, &Pdf_view::_handle_input_events };

		Nitpicker::Session::View_handle _view = _nitpicker.create_view();

		pixel_t *_fb_base() { return _fb_ds->local_addr<pixel_t>(); }

		void _rebuffer()
		{
			using namespace Nitpicker;

			_nit_mode = _nitpicker.mode();

			int max_x = Genode::max(_nit_mode.width(),  _fb_mode.width());
			int max_y = Genode::max(_nit_mode.height(), _fb_mode.height());

			if (max_x > _fb_mode.width() || max_y > _fb_mode.height()) {
				_fb_mode = Mode(max_x, max_y, _nit_mode.format());
				_nitpicker.buffer(_fb_mode, NO_ALPHA);
				if (_fb_ds.constructed())
					_fb_ds.destruct();
				_fb_ds.construct(_env.rm(), _framebuffer.dataspace());
			}

			_pdfapp.scrw = _nit_mode.width();
			 _pdfapp.scrh = _nit_mode.height();

			/*
			 * XXX replace heuristics with a meaningful computation
			 *
			 * The magic values are hand-tweaked manually to accommodating the
			 * use case of showing slides.
			 */
			_pdfapp.resolution = Genode::min(_nit_mode.width()/5,
			                                 _nit_mode.height()/3.8);

			typedef Nitpicker::Session::Command Command;
			_nitpicker.enqueue<Command::Geometry>(
				_view, Rect(Point(), Area(_nit_mode.width(), _nit_mode.height())));
			_nitpicker.enqueue<Command::To_front>(_view, Nitpicker::Session::View_handle());
			_nitpicker.execute();
		}

		void _handle_nit_mode()
		{
			_rebuffer();
			pdfapp_onresize(&_pdfapp, _nit_mode.width(), _nit_mode.height());
		}

		pdfapp_t _pdfapp { };

		int _motion_x = 0;
		int _motion_y = 0;

		void _handle_input_event(Input::Event const &ev)
		{
			using namespace Input;

			ev.handle_relative_motion([&] (int x, int y) {
				_motion_x += x;
				_motion_y += y;
				//pdfapp_onmouse(&_pdfapp, _motion_x, _motion_y, 0, 0, 0);
			});

			ev.handle_absolute_motion([&] (int x, int y) {
				_motion_x = x;
				_motion_y = y;
				//pdfapp_onmouse(&_pdfapp, _motion_x, _motion_y, 0, 0, 0);
			});

			if (ev.key_press(BTN_LEFT))
				pdfapp_onmouse(&_pdfapp, _motion_x, _motion_y, 1, 0, -1);

			else
			if (ev.key_release(BTN_LEFT))
				pdfapp_onmouse(&_pdfapp, _motion_x, _motion_y, 1, 0, 1);

			else
			if (ev.key_press(KEY_PAGEDOWN) || ev.key_press(KEY_RIGHT))
				pdfapp_onkey(&_pdfapp, '.');
			else
			if (ev.key_press(KEY_PAGEUP) || ev.key_press(KEY_LEFT))
				pdfapp_onkey(&_pdfapp, ',');
			else
			if (ev.key_press(KEY_DOWN))
				pdfapp_onkey(&_pdfapp, 'j');
			else
			if (ev.key_press(KEY_UP))
				pdfapp_onkey(&_pdfapp, 'k');

			ev.handle_press([&] (Keycode, Codepoint glyph) {
				if ((glyph.value & 0x7f) && !(glyph.value & 0x80)) {
					pdfapp_onkey(&_pdfapp, glyph.value);
				}
			});

			/*
			ev.handle_wheel([&] (int, int y) {
				pdfapp_onmouse(
					&_pdfapp, _motion_x, _motion_y,
					y > 0 ? 4 : 5, 0, 1);
			});
			 */
		}

		void _handle_input_events()
		{
			Libc::with_libc([&] () {
				_input.for_each_event([&] (Input::Event const &ev) {
					_handle_input_event(ev); }); });
		}

		void _refresh()
		{
			_framebuffer.refresh(0, 0, _nit_mode.width(), _nit_mode.height());

			/* handle one sync signal only */
			_framebuffer.sync_sigh(Genode::Signal_context_capability());
		}

	public:

		/**
		 * Constructor
		 *
		 * \throw Non_supported_framebuffer_mode
		 * \throw Unexpected_document_color_depth
		 */
		Pdf_view(Genode::Env &env) : _env(env)
		{
			_nitpicker.mode_sigh(_nit_mode_handler);
			_input.sigh(_input_handler);

			pdfapp_init(&_pdfapp);
			_pdfapp.userdata = this;
			_pdfapp.pageno   = 0;

			_rebuffer();

			{
				struct dirent **list = NULL;
				if (scandir("/", &list, pdf_select, alphasort) > 0) {

					char const *file_name = list[0]->d_name;
					int fd = open(file_name, O_BINARY | O_RDONLY, 0666);
					if (fd < 0) {
						Genode::error("Could not open input file \"", file_name, "\", Exiting.");
						exit(fd);
					}
					pdfapp_open(&_pdfapp, (char *)file_name, fd, 0);

				} else {
					Genode::error("failed to find a PDF to open");
					exit(~0);
				}
			}

			if (_pdfapp.image->n != 4) {
				Genode::error("Unexpected color depth, expected 4, got ",
				              _pdfapp.image->n, ", Exiting.");
				throw Unexpected_document_color_depth();
			}

			Genode::log(Genode::Cstring(pdfapp_version(&_pdfapp)));
		}

		void title(char const *msg)
		{
			typedef Nitpicker::Session::Command Command;
			_nitpicker.enqueue<Command::Title>(_view, msg);
			_nitpicker.execute();
		}

		void show();

		void exit(int code = 0) { _env.parent().exit(code); }
};


void Pdf_view::show()
{
	Genode::Area<> const fb_size(_fb_mode.width(), _fb_mode.height());
	int const x_max = Genode::min((int)fb_size.w(), _pdfapp.image->w);
	int const y_max = Genode::min((int)fb_size.h(), _pdfapp.image->h);

	/* clear framebuffer */
	memset(_fb_base(), 0, _fb_ds->size());

	Genode::size_t src_line_bytes   = _pdfapp.image->n * _pdfapp.image->w;
	unsigned char *src_line         = _pdfapp.image->samples;

	Genode::size_t dst_line_width   = fb_size.w(); /* in pixels */
	pixel_t *dst_line = _fb_base();

	/* skip first two lines as they contain white (XXX) */
	src_line += 2*src_line_bytes;
	dst_line += 2*dst_line_width;
	int const tweaked_y_max = y_max - 2;

	/* center vertically if the dst buffer is higher than the image */
	if (_pdfapp.image->h < _nit_mode.height())
		dst_line += dst_line_width*((_nit_mode.height() - _pdfapp.image->h)/2);

	/* center horizontally if the dst buffer is wider than the image */
	if (_pdfapp.image->w < _nit_mode.width())
		dst_line += (_nit_mode.width() - _pdfapp.image->w)/2;

	for (int y = 0; y < tweaked_y_max; y++) {
		convert_line_rgba_to_rgb565(src_line, dst_line, x_max, y);
		src_line += src_line_bytes;
		dst_line += dst_line_width;
	}

	/* refresh after the next sync signal */
	_framebuffer.sync_sigh(_sync_handler);
}


extern "C" void _sigprocmask()
{
	/* suppress debug message by default "not-implemented" implementation */
}


/**************************
 ** Called from pdfapp.c **
 **************************/

void winrepaint(pdfapp_t *pdfapp)
{
	Pdf_view *pdf_view = (Pdf_view *)pdfapp->userdata;
	pdf_view->show();
}


void winrepaintsearch(pdfapp_t *pdfapp)
{
	Pdf_view *pdf_view = (Pdf_view *)pdfapp->userdata;
	pdf_view->show();
}


void wincursor(pdfapp_t *, int)
{
}


void windocopy(pdfapp_t*) { }


void winerror(pdfapp_t *pdfapp, fz_error error)
{
	Genode::error("winerror: error=", error);
	Pdf_view *pdf_view = (Pdf_view *)pdfapp->userdata;
	pdf_view->exit();
}


void winwarn(pdfapp_t *, char *msg)
{
	Genode::warning("MuPDF: ", Genode::Cstring(msg));
}


void winhelp(pdfapp_t *pdfapp)
{
	Genode::log(Genode::Cstring(pdfapp_usage(pdfapp)));
}


char *winpassword(pdfapp_t *, char *)
{
	Genode::warning(__func__, " not implemented");
	return NULL;
}


void winopenuri(pdfapp_t*, char *s)
{
	Genode::log(Genode::Cstring(s));
}


void winclose(pdfapp_t *pdfapp)
{
	Pdf_view *pdf_view = (Pdf_view *)pdfapp->userdata;
	pdf_view->exit();
}


void winreloadfile(pdfapp_t *)
{
	Genode::warning(__func__, " not implemented");
}


void wintitle(pdfapp_t *pdfapp, char *s)
{
	Pdf_view *pdf_view = (Pdf_view *)pdfapp->userdata;
	pdf_view->title(s);
}


void winresize(pdfapp_t*, int, int)
{
}


void Libc::Component::construct(Libc::Env &env)
{
	Libc::with_libc([&] () { static Pdf_view main(env); });
}
