/*
 * Copyright (c) 2013, 2014, 2015, 2016, 2018, 2013 Jonas 'Sortie' Termansen.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * trianglix.cpp
 * The Trianglix Desktop Environment.
 */

#include <sys/display.h>
#include <sys/keycodes.h>
#include <sys/stat.h>
#include <sys/termmode.h>
#include <sys/wait.h>

#include <assert.h>
#include <ctype.h>
#include <dirent.h>
#include <err.h>
#include <errno.h>
#include <error.h>
#include <fcntl.h>
#include <ioleast.h>
#include <libgen.h>
#include <limits.h>
#include <math.h>
#include <poll.h>
#include <pwd.h>
#include <signal.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <timespec.h>
#include <unistd.h>

#include "vector.h"

const int FONT_WIDTH = 8UL;
const int FONT_HEIGHT = 16UL;
const int FONT_NUMCHARS = 256UL;
const int FONT_CHARSIZE = FONT_WIDTH * FONT_HEIGHT / 8UL;

bool use_runes = false;
bool configured_use_runes = false;
uint8_t font[FONT_CHARSIZE * FONT_NUMCHARS];

__attribute__((format(printf, 1, 2)))
char* print_string(const char* format, ...)
{
	char* ret;
	va_list ap;
	va_start(ap, format);
	if ( vasprintf(&ret, format, ap) < 0 )
		ret = NULL;
	va_end(ap);
	return ret;
}

char* gibberish()
{
	size_t length = 1 + arc4random_uniform(15);
	char* result = (char*) malloc(length + 1);
	for ( size_t i = 0; i < length; i++ )
		result[i] = 'A' + arc4random_uniform(26);
	result[length] = '\0';
	return result;
}

// TODO: This might be out of sync with execvpe's exact semantics.
bool has_path_executable(const char* name)
{
	const char* path = getenv("PATH");
	struct stat st;
	if ( !path || strchr(name, '/') )
		return stat(name, &st) == 0;
	char* path_copy = strdup(path);
	if ( !path_copy )
		return false;
	char* input = path_copy;
	while ( char* component = strsep(&input, ":") )
	{
		char* fullpath = print_string("%s/%s", component, name);
		if ( !fullpath )
			return free(fullpath), free(path_copy), false;
		if ( stat(fullpath, &st) == 0 )
			return free(fullpath), free(path_copy), true;
		free(fullpath);
	}
	free(path_copy);
	return errno = ENOENT, false;
}

static float timespec_to_float(struct timespec ts)
{
	return (float) ts.tv_sec + (float) ts.tv_nsec / 1E9;
}

DIR* open_home_directory()
{
	if ( struct passwd* user = getpwuid(getuid()) )
		if ( DIR* dir = opendir(user->pw_dir) )
			return dir;
	return opendir("/");
}

static uint32_t MakeColor(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 255)
{
	return a << 24U | r << 16U | g << 8U | b << 0U;
}

static uint32_t BlendPixel(uint32_t bg, uint32_t fg)
{
	uint8_t fg_a = fg >> 24;
	if ( fg_a == 255 )
		return fg;
	if ( fg_a == 0 )
		return bg;
	uint8_t fg_r = fg >> 16;
	uint8_t fg_g = fg >> 8;
	uint8_t fg_b = fg >> 0;
	uint8_t bg_r = bg >> 16;
	uint8_t bg_g = bg >> 8;
	uint8_t bg_b = bg >> 0;
	uint8_t ret_a = 255;
	uint8_t ret_r = ((255-fg_a)*bg_r + fg_a*fg_r) / 256;
	uint8_t ret_g = ((255-fg_a)*bg_g + fg_a*fg_g) / 256;
	uint8_t ret_b = ((255-fg_a)*bg_b + fg_a*fg_b) / 256;
	return ret_a << 24U | ret_r << 16U | ret_g << 8U | ret_b << 0U;
}

static bool ForkAndWait()
{
	unsigned int old_termmode;
	gettermmode(0, &old_termmode);
	pid_t child_pid = fork();
	if ( child_pid < 0 )
		return false;
	if ( child_pid )
	{
		int status;
		waitpid(child_pid, &status, 0);
		sigset_t oldset, sigttou;
		sigemptyset(&sigttou);
		sigaddset(&sigttou, SIGTTOU);
		sigprocmask(SIG_BLOCK, &sigttou, &oldset);
		tcsetpgrp(0, getpgid(0));
		sigprocmask(SIG_SETMASK, &oldset, NULL);
		settermmode(0, old_termmode);
		return false;
	}
	close(0);
	close(1);
	close(2);
	open("/dev/tty", O_RDONLY);
	open("/dev/tty", O_WRONLY);
	open("/dev/tty", O_WRONLY);
	setpgid(0, 0);
	sigset_t oldset, sigttou;
	sigemptyset(&sigttou);
	sigaddset(&sigttou, SIGTTOU);
	sigprocmask(SIG_BLOCK, &sigttou, &oldset);
	tcsetpgrp(0, getpgid(0));
	sigprocmask(SIG_SETMASK, &oldset, NULL);
#if 1 /* Magic to somehow fix a weird keyboard-related bug nortti has. */
	settermmode(0, TERMMODE_NORMAL | TERMMODE_NONBLOCK);
	char c;
	while ( 0 <= read(0, &c, sizeof(c)) );
#endif
	settermmode(0, TERMMODE_NORMAL);
	printf("\e[m\e[2J\e[H");
	fflush(stdout);
	fsync(0);
	fsync(1);
	fsync(2);
	return true;
}

__attribute__((unused))
static void ExecuteFile(const char* path, int curdirfd)
{
	if ( ForkAndWait() )
	{
		fchdir(curdirfd);
		execl(path, path, (const char*) NULL);
		_exit(127);
	}
}

__attribute__((unused))
static void DisplayFile(const char* path, int curdirfd)
{
	if ( ForkAndWait() )
	{
		fchdir(curdirfd);
		execlp("editor", "editor", path, (const char*) NULL);
		_exit(127);
	}
}

__attribute__((unused))
static void ExecutePath(const char* path, int curdirfd)
{
	if ( ForkAndWait() )
	{
		fchdir(curdirfd);
		execlp(path, path, (const char*) NULL);
		_exit(127);
	}
}

void ExecuteShellCommand(const char* command, int curdirfd)
{
	if ( !strcmp(command, "exit") )
		exit(0);
	unsigned int old_termmode;
	gettermmode(0, &old_termmode);
	if ( ForkAndWait() )
	{
		fchdir(curdirfd);
		execlp("sh", "sh", "-c", command, (const char*) NULL);
		error(127, errno, "`%s'", "sh");
	}
	unsigned int cur_termmode;
	gettermmode(0, &cur_termmode);
	if ( old_termmode == cur_termmode )
	{
		printf("\e[30;47m\e[J");
		printf("-- Press enter to return to Trianglix --\n");
		printf("\e[25l");
		fflush(stdout);
		settermmode(0, TERMMODE_UNICODE | TERMMODE_UTF8 | TERMMODE_LINEBUFFER);
		char line[256];
		read(0, line, sizeof(line));
		printf("\e[25h");
		fflush(stdout);
	}
}

enum object_type
{
	TYPE_DIRECTORY,
	TYPE_FILE,
};

class action;
class object;

class action
{
public:
	action(const char* name, class object* object) : name(strdup(name)), object(object) { }
	~action() { free(name); }
	const char* get_name() { return name; }
	class object* get_object() { return object; }

private:
	char* name;
	class object* object;

};

class object
{
public:
	object() : parent_object(NULL), error_string("") { }
	virtual ~object() { }
	virtual enum object_type type() = 0;
	virtual class object* factory() { return NULL; }
	virtual const char* title() { return ""; }
	virtual const char* prompt() { return ""; }
	virtual bool is_password_prompt() { return false; }
	virtual bool is_core() { return false; }
	virtual bool is_unstable_core() { return false; }
	virtual const char* error_message() { return error_string; }
	virtual void clear_error() { error_string = ""; }
	virtual class action** list_actions(size_t* /*num_actions*/) { return NULL; }
	virtual void invoke() { }
	virtual class object* open(const char* /*path*/) { return NULL; }
	virtual class object* command_line(const char* /*command*/) { return NULL; }

public:
	class object* parent_object;
	const char* error_string;

};

class directory : public object
{
public:
	directory(DIR* dir)
		: dir(dir), path(canonicalize_file_name_at(dirfd(dir), ".")) { }
	virtual ~directory() { closedir(dir); free(path); }

public:
	virtual enum object_type type() { return TYPE_DIRECTORY; }
	virtual const char* title() { return path; }
	virtual class action** list_actions(size_t* num_actions);
	virtual class object* open(const char* path);
	virtual class object* command_line(const char* command);

private:
	DIR* dir;
	char* path;

};

class file : public object
{
public:
	file(const char* path) : path(strdup(path)) { }
	virtual ~file() { free(path); }

public:
	virtual enum object_type type() { return TYPE_FILE; }
	virtual void invoke();

private:
	char* path;

};

class object* directory::open(const char* path)
{
	if ( !strcmp(path, ".") )
		return this;
	int fd = openat(dirfd(dir), path, O_RDONLY | O_CLOEXEC);
	if ( fd < 0 )
		return NULL;
	if ( !strcmp(path, "..") )
	{
		struct stat old_stat, new_stat;
		if ( fstat(dirfd(dir), &old_stat) == 0 && fstat(fd, &new_stat) == 0 )
		{
			if ( old_stat.st_dev == new_stat.st_dev &&
			     old_stat.st_ino == new_stat.st_ino )
				return this;
		}
	}
	struct stat st;
	fstat(fd, &st);
	if ( S_ISDIR(st.st_mode) )
		return new directory(fdopendir(fd));
	close(fd);
	if ( char* file_path = canonicalize_file_name_at(dirfd(dir), path) )
	{
		class object* result = new file(file_path);
		free(file_path);
		return result;
	}
	else
	{
		error(0, errno, "canonicalize_file_name_at: %s", path);
		sleep(1);
	}
	return NULL;
}

class action** directory::list_actions(size_t* num_actions)
{
	struct dirent** entries;
	int num_entries = dscandir_r(dir, &entries, NULL, NULL, alphasort_r, NULL);
	if ( num_entries < 0 )
		return NULL;
	action** actions = new class action*[num_entries + 1];
	for ( int i = 0; i < num_entries; i++ )
		actions[i] = new action(entries[i]->d_name, open(entries[i]->d_name));
	for ( int i = 0; i < num_entries; i++ )
		free(entries[i]);
	free(entries);
	actions[num_entries] = new action("Back", parent_object);
	return *num_actions = (size_t) num_entries + 1, actions;
}

class object* directory::command_line(const char* command)
{
	if ( !strncmp(command, "cd ", 3) )
	{
		class object* new_object = open(command + 3);
		if ( new_object && new_object->type() == TYPE_DIRECTORY )
			return new_object;
		delete new_object;
	}

	ExecuteShellCommand(command, dirfd(dir));

	return NULL;
}

class object* directory_command_line(const char* command)
{
	if ( !strncmp(command, "cd ", 3) )
	{
		class object* user_directory = new directory(open_home_directory());
		class object* new_object = user_directory->open(command + 3);
		if ( new_object != user_directory )
			delete user_directory;
		if ( new_object && new_object->type() == TYPE_DIRECTORY )
			return new_object;
		delete new_object;
	}

	DIR* dir = open_home_directory();
	ExecuteShellCommand(command, dirfd(dir));
	closedir(dir);

	return NULL;
}

void file::invoke()
{
	struct stat st;
	if ( stat(path, &st) == 0 )
	{
		char* dir_path = strdup(path);
		dirname(dir_path);
		DIR* dir = opendir(dir_path);
		if ( !dir )
			dir = opendir("/");
		if ( S_ISREG(st.st_mode) && (st.st_mode & 0111) )
			ExecuteFile(path, dirfd(dir));
		else if ( S_ISREG(st.st_mode) && !(st.st_mode & 0111) )
			DisplayFile(path, dirfd(dir));
		closedir(dir);
	}
}

class shell_command : public object
{
public:
	shell_command(const char* command) : command(strdup(command)) { }
	virtual ~shell_command() { free(command); }

public:
	virtual enum object_type type() { return TYPE_FILE; }
	virtual void invoke();

private:
	char* command;

};

void shell_command::invoke()
{
	DIR* dir = NULL;
	if ( struct passwd* user = getpwuid(getuid()) )
		dir = opendir(user->pw_dir);
	if ( !dir )
		dir = opendir("/");
	ExecuteShellCommand(command, dirfd(dir));
	closedir(dir);
}

class path_program : public object
{
public:
	path_program(const char* filename) : filename(strdup(filename)) { }
	virtual ~path_program() { free(filename); }

public:
	virtual enum object_type type() { return TYPE_FILE; }
	virtual void invoke();

private:
	char* filename;

};

void path_program::invoke()
{
	DIR* dir = NULL;
	if ( struct passwd* user = getpwuid(getuid()) )
		dir = opendir(user->pw_dir);
	if ( !dir )
		dir = opendir("/");
	ExecutePath(filename, dirfd(dir));
	closedir(dir);
}

class exec_program : public object
{
public:
	exec_program(const char* program) : program(strdup(program)) { }
	virtual ~exec_program() { free(program); }

public:
	virtual enum object_type type() { return TYPE_FILE; }
	virtual void invoke();

private:
	char* program;

};

void exec_program::invoke()
{
	execlp(program, program, (const char*) NULL);
}

class development : public object
{
public:
	development() { }
	virtual ~development() { }

public:
	virtual enum object_type type() { return TYPE_DIRECTORY; }
	virtual const char* title() { return "Development"; }
	virtual class action** list_actions(size_t* num_actions);
	virtual class object* command_line(const char* command);

};

class action** development::list_actions(size_t* num_actions)
{
	struct stat st;
	class action** actions = new action*[4 + 1];
	size_t index = 0;
	if ( has_path_executable("editor") )
		actions[index++] = new action("Editor", new path_program("editor"));
	if ( has_path_executable("sh") )
		actions[index++] = new action("Shell", new path_program("sh"));
	if ( has_path_executable("make") &&
	     has_path_executable("colormake") &&
	     stat("/src/system", &st) == 0 )
		actions[index++] = new action("Recompile System",
		                              new shell_command("colormake -C /src/system system"));
	if ( stat("/src", &st) == 0 )
		actions[index++] = new action("Source Code", new directory(opendir("/src")));
	actions[index++] = new action("Reboot GUI", new exec_program("trianglix"));
	actions[index++] = new action("Back", parent_object);
	return *num_actions = index, actions;
}

class object* development::command_line(const char* command)
{
	return directory_command_line(command);
}

class games : public object
{
public:
	games() { }
	virtual ~games() { }

public:
	virtual enum object_type type() { return TYPE_DIRECTORY; }
	virtual const char* title() { return "Games"; }
	virtual class action** list_actions(size_t* num_actions);
	virtual class object* command_line(const char* command);

};

class action** games::list_actions(size_t* num_actions)
{
	class action** actions = new action*[3 + 1];
	size_t index = 0;
	if ( has_path_executable("asteroids") )
		actions[index++] = new action("Asteroids", new path_program("asteroids"));
	if ( has_path_executable("aquatinspitz") )
		actions[index++] = new action("Aquatinspitz", new path_program("aquatinspitz"));
	if ( has_path_executable("quake") )
		actions[index++] = new action("Quake", new path_program("quake"));
	actions[index++] = new action("Back", parent_object);
	return *num_actions = index, actions;
}

class object* games::command_line(const char* command)
{
	return directory_command_line(command);
}

class core : public object
{
public:
	core(size_t depth = 0) : error(NULL), title_string(gibberish()), depth(depth) { }
	virtual ~core() { free(error); free(title_string); }
	virtual enum object_type type() { return TYPE_DIRECTORY; }
	virtual bool is_core() { return true; }
	virtual bool is_unstable_core() { return 2 <= depth; }
	virtual const char* title() { return title_string; }
	virtual const char* prompt() { return "|ROOT>"; }
	virtual class action** list_actions(size_t* num_actions);
	virtual class object* command_line(const char* command);

private:
	char* error;
	char* title_string;
	size_t depth;

};

class action** core::list_actions(size_t* num_actions_ptr)
{
	if ( 2 <= depth && arc4random_uniform(10000) < 200 )
		write(1, NULL, 1);

	size_t num_actions = 1 + arc4random_uniform(31);
	class action** actions = new action*[num_actions];
	for ( size_t i = 0; i < num_actions; i++ )
	{
		char* name = gibberish();
		actions[i] = new action(name, new core(depth + 1));
		free(name);
	}
	return *num_actions_ptr = num_actions, actions;
}

class object* core::command_line(const char* /*command*/)
{
	if ( arc4random_uniform(2) == 0 )
		return new core();
	else
	{
		free(error);
		error_string = error = gibberish();
		return NULL;
	}
}

class decide_runes : public object
{
public:
	decide_runes(bool answer) : answer(answer) { }
	virtual ~decide_runes() { }
	virtual enum object_type type() { return TYPE_FILE; }
	virtual void invoke();

private:
	bool answer;

};

void decide_runes::invoke()
{
	use_runes = answer;
	configured_use_runes = true;
	if ( answer )
	{
		unlink("/etc/rune-disable");
		close(::open("/etc/rune-enable", O_WRONLY | O_CREAT, 0644));
	}
	else
	{
		unlink("/etc/rune-enable");
		close(::open("/etc/rune-disable", O_WRONLY | O_CREAT, 0644));
	}
}

struct create_user_prompt
{
	const char* prompt;
	bool is_password_prompt;
};

struct create_user_prompt create_user_prompts[] =
{
	{ "Enter username:", false },
	{ "Enter password:", true },
};

const size_t num_create_user_prompts = sizeof(create_user_prompts) / sizeof(create_user_prompts[0]);

class create_user : public object
{
public:
	create_user();
	virtual ~create_user();
	virtual enum object_type type() { return TYPE_DIRECTORY; }
	virtual const char* title() { return "Create user"; }
	virtual const char* prompt();
	virtual bool is_password_prompt();
	virtual class object* command_line(const char* command);

private:
	size_t prompt_index;
	char* answers[num_create_user_prompts];

};

create_user::create_user()
{
	prompt_index = 0;
}

create_user::~create_user()
{
	for ( size_t i = 0; i < prompt_index; i++ )
		free(answers[i]);
}

const char* create_user::prompt()
{
	return create_user_prompts[prompt_index].prompt;
}

bool create_user::is_password_prompt()
{
	return create_user_prompts[prompt_index].is_password_prompt;
}

class object* create_user::command_line(const char* command)
{
	if ( prompt_index == 0 && !command[0] )
		return error_string = "Username cannot be empty", (class object*) NULL;
	if ( prompt_index == 0 && strchr(command, '/') )
		return error_string = "Username cannot contain slash characters", (class object*) NULL;

	answers[prompt_index] = strdup(command);

	if ( prompt_index + 1 < num_create_user_prompts )
	{
		prompt_index++;
		return NULL;
	}

	uid_t highest_uid = 0;
	FILE* fp = openpw();
	while ( struct passwd* user = fgetpwent(fp) )
		if ( highest_uid < user->pw_uid )
			highest_uid = user->pw_uid;
	fclose(fp);

	struct passwd user;
	memset(&user, 0, sizeof(user));
	user.pw_uid = highest_uid + 1;
	user.pw_gid = (gid_t) user.pw_uid;
	user.pw_dir = print_string("/home/%s", answers[0]);
	user.pw_gecos = strdup(answers[0]);
	user.pw_name = strdup(answers[0]);
	user.pw_passwd = strdup(answers[1]);
	user.pw_shell = strdup("sh");

	mkdir(user.pw_dir, 0777);

	fp = fopen("/etc/passwd", "a");
	fprintf(fp, "%s:%s:%ju:%ju:%s:%s:%s\n",
		user.pw_name,
		user.pw_passwd,
		(uintmax_t) user.pw_uid,
		(uintmax_t) user.pw_gid,
		user.pw_gecos,
		user.pw_dir,
		user.pw_shell);
	fclose(fp);

	return parent_object;
}

class administration : public object
{
public:
	administration() { }
	virtual ~administration() { }

public:
	virtual enum object_type type() { return TYPE_DIRECTORY; }
	virtual const char* title() { return "Administration"; }
	virtual class action** list_actions(size_t* num_actions);

};

class action** administration::list_actions(size_t* num_actions)
{
	class action** actions = new action*[4 + 1];
	size_t index = 0;
#if 0 // TODO: Until crypt_newhash is used for the password.
	actions[index++] = new action("Create user", new create_user());
#endif
	actions[index++] = new action("Enable Runes", new decide_runes(true));
	actions[index++] = new action("Disable Runes", new decide_runes(false));
	actions[index++] = new action("Trinit Core", new core());
	actions[index++] = new action("Back", parent_object);
	return *num_actions = index, actions;
}

class exiter : public object
{
public:
	exiter() { }
	virtual ~exiter() { }

public:
	virtual enum object_type type() { return TYPE_FILE; }
	virtual void invoke();

};

void exiter::invoke()
{
	exit(0);
}

class desktop : public object
{
public:
	desktop() { }
	virtual ~desktop() { }

public:
	virtual enum object_type type() { return TYPE_DIRECTORY; }
	virtual const char* title() { return "Desktop"; }
	virtual class action** list_actions(size_t* num_actions);
	virtual class object* command_line(const char* command);

};

class action** desktop::list_actions(size_t* num_actions)
{
	class action** actions = new action*[*num_actions = 7];
	actions[0] = new action("Home", new directory(open_home_directory()));
	actions[1] = new action("Filesystem", new directory(opendir("/")));
	actions[2] = new action("Games", new games());
	actions[3] = new action("Shell", new path_program("sh"));
	actions[4] = new action("Development", new development());
	actions[5] = new action("Administration", new administration());
	actions[6] = new action("Logout", new exiter());
	return actions;
}

class object* desktop::command_line(const char* command)
{
	return directory_command_line(command);
}

class object* log_user_in(struct passwd* user)
{
	setuid(user->pw_uid);
	setenv("USERNAME", user->pw_name, 1);
	setenv("HOME", user->pw_dir, 1);
	setenv("SHELL", user->pw_shell, 1);
	setenv("DEFAULT_STUFF", "NO", 1);
	return new desktop();
}

class FrameBufferInfo;
struct Desktop;
struct RenderInfo;

struct Desktop
{
	struct timespec init_time;
	struct timespec since_init;
	struct timespec last_update;
	struct timespec experienced_time;
	class object* object;
	action** actions;
	size_t num_actions;
	size_t selected;
	float rotate_angle;
	float color_angle;
	float text_angle;
	float size_factor;
	bool control;
	bool shift;
	bool lshift;
	bool rshift;
	bool show_help;
	bool angry;
	char buffer_overflow_fix_me;
	char command[2048];
	char warning_buffer[64];
	const char* warning;
	bool critical_warning;
	bool flashing_warning;
	bool rune_warning;
};

struct RenderInfo
{
	int frame;
	struct Desktop desktop;
};

class FrameBufferInfo
{
public:
	FrameBufferInfo(uint8_t* buffer, size_t pitch, int xres, int yres) :
		buffer(buffer), pitch(pitch), xres(xres), yres(yres) { }
	FrameBufferInfo(const FrameBufferInfo& o) :
		buffer(o.buffer), pitch(o.pitch), xres(o.xres), yres(o.yres) { }
	~FrameBufferInfo() { }

public:
	uint8_t* buffer;
	size_t pitch;
	int xres;
	int yres;

public:
	uint32_t* GetLinePixels(int ypos) const
	{
		return (uint32_t*) (buffer + ypos * pitch);
	}

};

unsigned char char_to_rune(unsigned char c)
{
	switch ( tolower(c) )
	{
	case 'a': return 0x89;
	case 'b': return 0x8c;
	case 'c': return 0x85;
	case 'd': return 0x8b;
	case 'e': return 0x88;
	case 'f': return 0x80;
	case 'g': return 0x85;
	case 'h': return 0x86;
	case 'i': return 0x88;
	case 'j': return 0x88;
	case 'k': return 0x85;
	case 'l': return 0x8e;
	case 'm': return 0x8d;
	case 'n': return 0x87;
	case 'o': return 0x83;
	case 'p': return 0x8c;
	case 'q': return 0x85;
	case 'r': return 0x84;
	case 's': return 0x8a;
	case 't': return 0x8b;
	case 'u': return 0x81;
	case 'v': return 0x81;
	case 'w': return 0x81;
	case 'x': return 0x85;
	case 'y': return 0x81;
	case 'z': return 0x8a;
	default: return c;
	}
}

static void RenderChar(FrameBufferInfo fb, char c, uint32_t color,
                       int pos_x, int pos_y, int max_w, int max_h)
{
	unsigned char uc = c;
	if ( use_runes )
		uc = char_to_rune(uc);
	int xres = fb.xres;
	int yres = fb.yres;
	if ( xres <= pos_x || yres <= pos_y )
		return;
	int avai_x = xres - pos_x;
	int avai_y = yres - pos_y;
	if ( avai_x < max_w )
		max_w = avai_x;
	if ( avai_y < max_h )
		max_h = avai_y;
	int width = FONT_WIDTH + 1 < max_w ? FONT_WIDTH + 1 : max_w;
	int height = FONT_HEIGHT < max_h ? FONT_HEIGHT : max_h;
	for ( int y = 0; y < height; y++ )
	{
		int pixely = pos_y + y;
		if ( pixely < 0 )
			continue;
		uint32_t* pixels = fb.GetLinePixels(pixely);
		uint8_t linebitmap = font[uc * FONT_CHARSIZE + y];
		for ( int x = 0; x < width; x++ )
		{
			int pixelx = pos_x + x;
			if ( pixelx < 0 )
				continue;
			if ( x != FONT_WIDTH && linebitmap & 1U << (7-x) )
				pixels[pixelx] = BlendPixel(pixels[pixelx], color);
		}
	}
}

static void RenderCharShadow(FrameBufferInfo fb, char c, uint32_t color,
                             int pos_x, int pos_y, int max_w, int max_h)
{
	RenderChar(fb, c, MakeColor(0, 0, 0, 64), pos_x-1, pos_y-1, max_w, max_h);
	RenderChar(fb, c, MakeColor(0, 0, 0, 64), pos_x+1, pos_y-1, max_w, max_h);
	RenderChar(fb, c, MakeColor(0, 0, 0, 64), pos_x-1, pos_y+1, max_w, max_h);
	RenderChar(fb, c, MakeColor(0, 0, 0, 64), pos_x+1, pos_y+1, max_w, max_h);
	RenderChar(fb, c, color, pos_x+0, pos_y+0, max_w, max_h);
}

static void RenderText(FrameBufferInfo fb, const char* str, uint32_t color,
                       int pos_x, int pos_y, int max_w, int max_h)
{
	int char_width = FONT_WIDTH+1;
	int char_height = FONT_HEIGHT;
	while ( *str && 0 < max_w )
	{
		RenderChar(fb, *str++, color, pos_x, pos_y, max_w, max_h);
		pos_x += char_width;
		max_w -= char_width;
		(void) char_height;
	}
}

static void RenderTextShadow(FrameBufferInfo fb, const char* str, uint32_t color,
                             int pos_x, int pos_y, int max_w, int max_h)
{
	int char_width = FONT_WIDTH+1;
	int char_height = FONT_HEIGHT;
	while ( *str && 0 < max_w )
	{
		RenderCharShadow(fb, *str++, color, pos_x, pos_y, max_w, max_h);
		pos_x += char_width;
		max_w -= char_width;
		(void) char_height;
	}
}

typedef struct
{
	float src;
	float dst;
} fspan_t;

size_t ClampFloatToSize(float val, size_t max)
{
	if ( val < 0.0f )
		return 0;
	if ( (float) max <= val )
		return max;
	size_t ret = (size_t) val;
	if ( max < ret )
		ret = max;
	return ret;
}

int compare_triangle_points_by_height(const void* a, const void* b)
{
	Vector av = *(const Vector*) a;
	Vector bv = *(const Vector*) b;
	if ( av.y < bv.y )
		return -1;
	else if ( bv.y < av.y )
		return 1;
	return 0;
}

static void SortTrianglePointsByHeight(Vector points[3])
{
	qsort(points, 3, sizeof(Vector), compare_triangle_points_by_height);
}

static float SafeDivide(float a, float b)
{
	if ( b == 0.0f )
		return a < 0.0f ? -INFINITY : INFINITY;
	return a / b;
}

static size_t TriangleHorizontalSpans(fspan_t* spans, size_t numspans, float from,
                                      Vector points[3])
{
	assert(points[0].y <= points[1].y && points[1].y <= points[2].y);
	Vector vec01 = points[1] - points[0];
	Vector vec02 = points[2] - points[0];
	Vector vec20 = points[0] - points[2];
	Vector vec21 = points[1] - points[2];
	Vector vec01hat(-vec01.y, vec01.x);
	bool flipped = vec02.Dot(vec01hat) < 0.0f;
	float dirx_01 = SafeDivide(vec01.x, vec01.y);
	float dirx_02 = SafeDivide(vec02.x, vec02.y);
	float dirx_20 = -SafeDivide(vec20.x, vec20.y);
	float dirx_21 = -SafeDivide(vec21.x, vec21.y);
	for ( size_t i = 0; i < numspans; i++ )
	{
		float y = from + (float) i;
		if ( y < points[0].y ) { spans[i].src = spans[i].dst = 0.0f; continue; }
		if ( points[2].y < y ) { return i; /* Nothing beyond here. */ }
		float xpos01 = points[0].x + dirx_01 * (y - points[0].y);
		float xpos02 = points[0].x + dirx_02 * (y - points[0].y);
		float xpos20 = points[2].x + dirx_20 * (points[2].y - y);
		float xpos21 = points[2].x + dirx_21 * (points[2].y - y);
		float xsrc;
		float xdst;
		if ( flipped )
			xsrc = xpos01 > xpos21 ? xpos01 : xpos21,
			xdst = xpos20;
		else
			xsrc = xpos02,
			xdst = xpos01 < xpos21 ? xpos01 : xpos21;
		spans[i].src = xsrc;
		spans[i].dst = xdst;
	}
	return numspans;
}

static void RenderTriangle(uint8_t* fb, size_t pitch, size_t xres,
                            size_t yres, float x1, float y1, float x2,
                            float y2, float x3, float y3, uint32_t color)
{
	Vector points[3] = { {x1, y1}, {x2, y2}, {x3, y3}, };
	SortTrianglePointsByHeight(points);
	const size_t NUM_SPANS = 256UL;
	fspan_t spans[NUM_SPANS];
	float y0 = points[0].y;
	while ( true )
	{
		size_t numspans = TriangleHorizontalSpans(spans, NUM_SPANS, y0, points);
		if ( !numspans )
			break;
		for ( size_t i = 0; i < numspans; i++ )
		{
			float ypixelf = y0 + i * 1.0f;
			if ( ypixelf < 0.f || (float) yres <= ypixelf )
				continue;
			size_t ypixel = ypixelf;
			if ( yres <= ypixel )
				continue;
			size_t xsrc = ClampFloatToSize(spans[i].src, xres);
			size_t xdst = ClampFloatToSize(spans[i].dst, xres);
			uint32_t* line = (uint32_t*) (fb + ypixel * pitch);
			for ( size_t xpixel = xsrc; xpixel < xdst; xpixel++ )
				line[xpixel] = color;
		}
		y0 += numspans * 1.0f;
	}
}

const int SENARY_FONT_WIDTH = FONT_HEIGHT-1;
const int SENARY_FONT_HEIGHT = FONT_HEIGHT;

static void RenderSenaryDigit(FrameBufferInfo fb, unsigned int digit,
                              uint32_t pri_color, uint32_t sec_color, int top,
                              int left)
{
	int width = SENARY_FONT_WIDTH-2;
	int height = SENARY_FONT_HEIGHT-2;
	int bottom = top + height;
	int right = left + width;
	int x1, y1, x2, y2, x3, y3;
	if ( digit % 3 == 0 )
	{
		x3 = (left + right) / 2;
		y3 = top;
		x2 = left;
		y2 = bottom-1;
		x1 = right;
		y1 = bottom;
	}
	else if ( digit % 3 == 1 )
	{
		x1 = left+1;
		y1 = top;
		x2 = right-1;
		y2 = (top + bottom) / 2;
		x3 = left+1;
		y3 = bottom;
	}
	else if ( digit % 3 == 2 )
	{
		x1 = right-1;
		y1 = top;
		x2 = left+1;
		y2 = (top + bottom) / 2;
		x3 = right-1;
		y3 = bottom;
	}
	uint32_t color = digit < 3 ? pri_color : sec_color;
	RenderTriangle(fb.buffer, fb.pitch, fb.xres, fb.yres, x1, y1, x2, y2, x3, y3, color);
}

static void RenderBackground(FrameBufferInfo fb, struct Desktop* desktop)
{
	bool was_runes = use_runes;
	use_runes = use_runes || desktop->object->is_core();
	if ( desktop->object->is_unstable_core() )
		use_runes = arc4random_uniform(50) < 40;

	uint32_t background_color = desktop->object->is_core() ? MakeColor(0, 0, 0) :
	                            MakeColor(0x89 * 2/3, 0xc7 * 2/3, 0xff * 2/3);
	if ( desktop->since_init.tv_sec < 3 )
	{
		float factor = timespec_to_float(desktop->since_init) / 3;
		background_color = MakeColor(0x89 * 2/3 * factor, 0xc7 * 2/3 * factor, 0xff * 2/3 * factor);
	}

	for ( int y = 0; y < fb.yres; y++ )
	{
		uint32_t* pixels = fb.GetLinePixels(y);
		for ( int x = 0; x < fb.xres; x++ )
			pixels[x] = background_color;
	}

	if ( desktop->since_init.tv_sec < 3 )
	{

		const char* logo = "trianglix";
		size_t logo_length = strlen(logo);
		int logo_width = (int) logo_length * (FONT_WIDTH+1);
		int logo_height = FONT_HEIGHT;
		int logo_pos_x = (fb.xres - logo_width) / 2;
		int logo_pos_y = (fb.yres - logo_height) / 2;
		uint32_t logo_color = MakeColor(255, 255, 255);
		RenderTextShadow(fb, logo, logo_color, logo_pos_x, logo_pos_y, INT_MAX, INT_MAX);
		use_runes = was_runes;
		return;
	}

	struct timespec now;
	clock_gettime(CLOCK_REALTIME, &now);

	RenderSenaryDigit(fb, now.tv_sec/(6*6*6*6*6) % 6, MakeColor(255, 255, 255), MakeColor(255, 100, 100), 15, fb.xres-SENARY_FONT_WIDTH*(6+1));
	RenderSenaryDigit(fb, now.tv_sec/(6*6*6*6) % 6, MakeColor(255, 255, 255), MakeColor(255, 100, 100), 15, fb.xres-SENARY_FONT_WIDTH*(5+1));
	RenderSenaryDigit(fb, now.tv_sec/(6*6*6) % 6, MakeColor(255, 255, 255), MakeColor(255, 100, 100), 15, fb.xres-SENARY_FONT_WIDTH*(4+1));
	RenderSenaryDigit(fb, now.tv_sec/(6*6) % 6, MakeColor(255, 255, 255), MakeColor(255, 100, 100), 15, fb.xres-SENARY_FONT_WIDTH*(3+1));
	RenderSenaryDigit(fb, now.tv_sec/6 % 6, MakeColor(255, 255, 255), MakeColor(255, 100, 100), 15, fb.xres-SENARY_FONT_WIDTH*(2+1));
	RenderSenaryDigit(fb, now.tv_sec/1 % 6, MakeColor(255, 255, 255), MakeColor(255, 100, 100), 15, fb.xres-SENARY_FONT_WIDTH*(1+1));

	struct timespec xtime = desktop->experienced_time;

	size_t index = (now.tv_sec / 10) % 5;
	const char* slogans[] =
	{
		"trianglix",
		"you can do anything with trianglix",
		"welcome to trianglix",
		"the only limit is yourself",
		"the unattainable is unknown with trianglix",
	};

	const char* warnings[] =
	{
		"the triangle has been angered",
		"the triangle does not forgive",
		"the triangle never forgets",
		"the triangle pays no attention to the empty mind",
		"the triangle knows your limits",
		"the triangle hates being anthropomorphized",
	};

	bool is_angry = desktop->angry || desktop->object->is_unstable_core();
	const char* slogan = (is_angry ? warnings : slogans)[index];
	size_t slogan_length = strlen(slogan);
	int slogan_width = (int) slogan_length * (FONT_WIDTH+1);
	int slogan_pos_x = (fb.xres - slogan_width) / 2;
	int slogan_pos_y = 15;
	uint32_t slogan_color = desktop->angry ?
	                        MakeColor(255, 0, 0) :
	                        MakeColor(255, 255, 255);
	RenderText(fb, slogan, slogan_color, slogan_pos_x, slogan_pos_y, INT_MAX, INT_MAX);

	bool show_warning = !desktop->flashing_warning || xtime.tv_nsec < 500000000;
	if ( desktop->warning && show_warning && !desktop->show_help )
	{
		const char* warning = desktop->warning;
		size_t warning_length = strlen(warning);
		int warning_width = (int) warning_length * (FONT_WIDTH+1);
		int warning_pos_x = (fb.xres - warning_width) / 2;
		int warning_pos_y = 15 + FONT_HEIGHT * 2;
		uint32_t warning_color = desktop->critical_warning ?
			                    MakeColor(255, 0, 0) :
			                    MakeColor(255, 255, 255);
		bool used_runes = use_runes;
		use_runes = use_runes || desktop->rune_warning;
		RenderText(fb, warning, warning_color, warning_pos_x, warning_pos_y, INT_MAX, INT_MAX);
		use_runes = used_runes;
	}

	const char* title = desktop->object->title();
	if ( desktop->show_help )
		title = "Help";
	size_t title_length = strlen(title);
	int title_width = (int) title_length * (FONT_WIDTH+1);
	int title_pos_x = (fb.xres - title_width) / 2;
	int title_pos_y = fb.yres - 15 - FONT_HEIGHT;
	uint32_t title_color = MakeColor(255, 255, 255);
	RenderText(fb, title, title_color, title_pos_x, title_pos_y, INT_MAX, INT_MAX);

	int alpha = (uint8_t) ((sin(desktop->color_angle) + 1.0f) * 128.f);
	if ( alpha < 0 )
		alpha = 0;
	if ( 255 < alpha )
		alpha = 255;

	uint32_t pri_color = desktop->object->is_core() && !desktop->object->is_unstable_core() ?
	                     MakeColor(255, 255, 255, alpha) :
	                     is_angry ?
	                     MakeColor(114, 21, 36, alpha) :
	                     MakeColor(255, 222, 0, alpha);
	uint32_t sec_color = desktop->object->is_core() && !desktop->object->is_unstable_core() ?
	                     MakeColor(255, 255, 255, 255) :
	                     is_angry ?
	                     MakeColor(255, 0, 0, 255) :
	                     MakeColor(114, 255, 36, 255);
	uint32_t triangle_color = BlendPixel(sec_color, pri_color);

	float size = fb.xres / 10.0 * desktop->size_factor;

	float center_x = fb.xres / 2.0;
	float center_y = fb.yres / 2.0;

	float x1 = center_x + size * cos(desktop->rotate_angle + 2.0*M_PI * 0.0 / 3.0);
	float y1 = center_y + size * sin(desktop->rotate_angle + 2.0*M_PI * 0.0 / 3.0);
	float x2 = center_x + size * cos(desktop->rotate_angle + 2.0*M_PI * 1.0 / 3.0);
	float y2 = center_y + size * sin(desktop->rotate_angle + 2.0*M_PI * 1.0 / 3.0);
	float x3 = center_x + size * cos(desktop->rotate_angle + 2.0*M_PI * 2.0 / 3.0);
	float y3 = center_y + size * sin(desktop->rotate_angle + 2.0*M_PI * 2.0 / 3.0);

	RenderTriangle(fb.buffer, fb.pitch, fb.xres, fb.yres, x1, y1, x2, y2, x3, y3, triangle_color);

	if ( desktop->show_help )
	{
		const char* help_messages[] =
		{
			"Trianglix Basic Usage",
			"",
			"F1 - Basic Usage",
			"Escape - Return to parent object",
			"Arrow Keys - Select action in direction",
			"Enter (empty prompt) - Invoke action",
			"Enter (non-empty prompt) - Execute command",
			"Tab - Next object",
			"Shift-Tab - Previous object",
			"Control-D - Exit shell",
			"Control-Q - Exit editor",
			"<character> - Append character to command prompt",
			NULL,
		};

		for ( size_t i = 0; help_messages[i]; i++ )
		{
			const char* help = help_messages[i];
			int help_length = strlen(help);
			int help_width = (int) help_length * (FONT_WIDTH+1);
			int help_height = FONT_HEIGHT;
			int help_pos_x = (fb.xres - help_width) / 2;
			int help_pos_y = 15 + (3 + i) * (help_height);
			RenderTextShadow(fb, help, MakeColor(255, 255, 255), help_pos_x, help_pos_y, INT_MAX, INT_MAX);
		}

		use_runes = was_runes;
		return;
	}

	float normal_dist = size * 2.0;

	uint32_t unselected_color = MakeColor(255, 255, 255);
	uint32_t selected_color = MakeColor(255, 222, 0);
	for ( size_t i = 0; i < desktop->num_actions; i++ )
	{
		float dist = normal_dist;
		if ( desktop->object->is_unstable_core() )
			dist *= 0.5 + (sin(desktop->text_angle / 50.0 * i) + 1.0) / 2.0;
		const char* file_name = desktop->actions[i]->get_name();
		char* text = new char[1+strlen(file_name)+1+1];
		stpcpy(stpcpy(stpcpy(text, "|"), file_name), ">");
		float text_x = center_x + dist * cos(desktop->text_angle + 2.0*M_PI * (float) i / (float) desktop->num_actions);
		float text_y = center_y + dist * sin(desktop->text_angle + 2.0*M_PI * (float) i / (float) desktop->num_actions);
		int text_width = (int) strlen(text) * (FONT_WIDTH+1);
		int text_height = FONT_HEIGHT;
		float x = text_x - text_width / 2.0f;
		float y = text_y - text_height / 2.0f;
		RenderTextShadow(fb, text, i == desktop->selected ? selected_color : unselected_color, x, y, text_width, text_height);
		delete[] text;
	}

	bool is_password_prompt = desktop->object->is_password_prompt();
	char* password_command = NULL;
	const char* command = desktop->command;
	if ( is_password_prompt )
	{
		password_command = strdup(command);
		for ( size_t i = 0; command[i]; i++ )
			password_command[i] = '*';
		command = password_command;
	}
	int command_length = strlen(command);
	int command_width = (int) command_length * (FONT_WIDTH+1);
	int command_height = FONT_HEIGHT;
	int command_pos_x = (fb.xres - command_width) / 2;
	int command_pos_y = (fb.yres - command_height) / 2;
	RenderTextShadow(fb, command, MakeColor(255, 255, 255), command_pos_x, command_pos_y, INT_MAX, INT_MAX);
	if ( is_password_prompt )
		free(password_command);

	const char* prompt = desktop->object->prompt();
	int prompt_length = strlen(prompt);
	int prompt_width = (int) prompt_length * (FONT_WIDTH+1);
	int prompt_height = FONT_HEIGHT;
	int prompt_pos_x = (fb.xres - prompt_width) / 2;
	int prompt_pos_y = (fb.yres - prompt_height) / 2 - FONT_HEIGHT - 2;
	RenderTextShadow(fb, prompt, MakeColor(255, 255, 255), prompt_pos_x, prompt_pos_y, INT_MAX, INT_MAX);

	const char* error = desktop->object->error_message();
	int error_length = strlen(error);
	int error_width = (int) error_length * (FONT_WIDTH+1);
	int error_height = FONT_HEIGHT;
	int error_pos_x = (fb.xres - error_width) / 2;
	int error_pos_y = (fb.yres - error_height) / 2 + FONT_HEIGHT - 2;
	RenderText(fb, error, MakeColor(255, 0, 0), error_pos_x, error_pos_y, INT_MAX, INT_MAX);

	use_runes = was_runes;
}

static void Render(FrameBufferInfo fb, struct Desktop* desktop)
{
	RenderBackground(fb, desktop);
}

static bool Render(struct RenderInfo* info)
{
	struct dispmsg_get_crtc_mode get_mode_msg;
	memset(&get_mode_msg, 0, sizeof(get_mode_msg));
	get_mode_msg.msgid = DISPMSG_GET_CRTC_MODE;
	get_mode_msg.device = 0; // TODO: Multi-screen support!
	get_mode_msg.connector = 0; // TODO: Multi-screen support!
	if ( dispmsg_issue(&get_mode_msg, sizeof(get_mode_msg)) != 0 )
		err(1, "dispmsg_issue: dispmsg_get_crtc_mode");
	struct dispmsg_crtc_mode mode = get_mode_msg.mode;

	if ( !(mode.control & DISPMSG_CONTROL_VALID) )
		errx(1, "No valid video mode was set");
	if ( mode.control & DISPMSG_CONTROL_VGA )
		errx(1, "A VGA text mode was set");
	if ( mode.fb_format != 32 )
		errx(1, "A 32-bit video mode wasn't set");

	size_t pitch = 4 * mode.view_xres;
	size_t framebuffer_size = pitch * mode.view_yres;
	uint8_t* framebuffer = (uint8_t*) malloc(framebuffer_size);
	if ( !framebuffer )
		err(1, "malloc");

	FrameBufferInfo fbinfo(framebuffer, pitch, mode.view_xres, mode.view_yres);
	Render(fbinfo, &info->desktop);

	struct dispmsg_write_memory write_memory_msg;
	memset(&write_memory_msg, 0, sizeof(write_memory_msg));
	write_memory_msg.msgid = DISPMSG_WRITE_MEMORY;
	write_memory_msg.device = get_mode_msg.device;
	write_memory_msg.offset = get_mode_msg.connector;
	write_memory_msg.size = framebuffer_size;
	write_memory_msg.src = framebuffer;
	if ( dispmsg_issue(&write_memory_msg, sizeof(write_memory_msg)) != 0 )
		err(1, "dispmsg_issue: dispmsg_write_memory");

	free(framebuffer);

	return true;
}

void ClearActions(struct Desktop* desktop, class object* no_delete = NULL)
{
	for ( size_t i = 0; i < desktop->num_actions; i++ )
	{
		class object* object = desktop->actions[i]->get_object();
		if ( object != no_delete &&
		     object == desktop->object &&
		     object == desktop->object->parent_object )
			delete object;
		delete desktop->actions[i];
	}
	delete[] desktop->actions;
	desktop->actions = NULL;
	desktop->num_actions = 0;
	desktop->selected = 0;
}

static void UpdateActionList(struct Desktop* desktop)
{
	size_t old_selected = desktop->selected;
	ClearActions(desktop);
	desktop->actions = desktop->object->list_actions(&desktop->num_actions);
	if ( desktop->num_actions )
		desktop->selected = old_selected % desktop->num_actions;
	else
		desktop->selected = 0;
}

void PopObject(struct Desktop* desktop)
{
	if ( desktop->object->parent_object )
	{
		ClearActions(desktop);
		class object* poped_object = desktop->object;
		desktop->object = desktop->object->parent_object;
		delete poped_object;
	}
	UpdateActionList(desktop);
}

void PushObject(struct Desktop* desktop, class object* object)
{
	desktop->object->clear_error();

	while ( class object* product = object->factory() )
	{
		if ( object != desktop->object &&
		     object != desktop->object->parent_object &&
		     object != product )
			delete object;
		object = product;
	}

	if ( object == desktop->object )
		UpdateActionList(desktop);
	else if ( object == desktop->object->parent_object )
		PopObject(desktop);
	else
	{
		ClearActions(desktop, object);
		object->parent_object = desktop->object;
		desktop->object = object;
		UpdateActionList(desktop);
	}
}

static void HandleKeystroke(int kbkey, struct Desktop* desktop)
{
	int abskbkey = kbkey < 0 ? -kbkey : kbkey;

	switch ( abskbkey )
	{
	case KBKEY_LCTRL: desktop->control = kbkey > 0; break;
	case KBKEY_LSHIFT: desktop->lshift = kbkey > 0; break;
	case KBKEY_RSHIFT: desktop->rshift = kbkey > 0; break;
	}
	desktop->shift = desktop->lshift || desktop->rshift;

	if ( desktop->show_help )
	{
		if ( kbkey == KBKEY_F1 || kbkey == KBKEY_ESC )
			desktop->show_help = false;
		return;
	}

	if ( kbkey == KBKEY_F1 )
	{
		desktop->show_help = true;
		return;
	}

	if ( kbkey == KBKEY_ESC )
		PopObject(desktop);

	size_t& num_actions = desktop->num_actions;
	if ( !num_actions )
		return;

	float selection_x = cos(desktop->text_angle + 2.0*M_PI * (float) desktop->selected / (float) num_actions);
	float selection_y = sin(desktop->text_angle + 2.0*M_PI * (float) desktop->selected / (float) num_actions);

	if ( kbkey == KBKEY_RIGHT )
	{
		float best_diff = 2.0f;
		size_t best = num_actions;
		for ( size_t i = 0; i < num_actions; i++ )
		{
			float x = cos(desktop->text_angle + 2.0*M_PI * (float) i / (float) num_actions);
			float y = sin(desktop->text_angle + 2.0*M_PI * (float) i / (float) num_actions);
			if ( x <= selection_x )
				continue;
			float diff = fabsf(y - selection_y);
			if ( best_diff < diff )
				continue;
			best_diff = diff;
			best = i;
		}
		if ( best != num_actions )
			desktop->selected = best;
	}

	if ( kbkey == KBKEY_LEFT )
	{
		float best_diff = 2.0f;
		size_t best = num_actions;
		for ( size_t i = 0; i < num_actions; i++ )
		{
			float x = cos(desktop->text_angle + 2.0*M_PI * (float) i / (float) num_actions);
			float y = sin(desktop->text_angle + 2.0*M_PI * (float) i / (float) num_actions);
			if ( selection_x <= x )
				continue;
			float diff = fabsf(y - selection_y);
			if ( best_diff < diff )
				continue;
			best_diff = diff;
			best = i;
		}
		if ( best != num_actions )
			desktop->selected = best;
	}

	if ( kbkey == KBKEY_DOWN )
	{
		float best_diff = 2.0f;
		size_t best = num_actions;
		for ( size_t i = 0; i < num_actions; i++ )
		{
			float x = cos(desktop->text_angle + 2.0*M_PI * (float) i / (float) num_actions);
			float y = sin(desktop->text_angle + 2.0*M_PI * (float) i / (float) num_actions);
			if ( y <= selection_y )
				continue;
			float diff = fabsf(x - selection_x);
			if ( best_diff < diff )
				continue;
			best_diff = diff;
			best = i;
		}
		if ( best != num_actions )
			desktop->selected = best;
	}

	if ( kbkey == KBKEY_UP )
	{
		float best_diff = 2.0f;
		size_t best = num_actions;
		for ( size_t i = 0; i < num_actions; i++ )
		{
			float x = cos(desktop->text_angle + 2.0*M_PI * (float) i / (float) num_actions);
			float y = sin(desktop->text_angle + 2.0*M_PI * (float) i / (float) num_actions);
			if ( selection_y <= y )
				continue;
			float diff = fabsf(x - selection_x);
			if ( best_diff < diff )
				continue;
			best_diff = diff;
			best = i;
		}
		if ( best != num_actions )
			desktop->selected = best;
	}

	if ( num_actions && !desktop->shift && kbkey == KBKEY_TAB )
		desktop->selected = (desktop->selected + num_actions - 1) % num_actions;

	if ( num_actions && desktop->shift && kbkey == KBKEY_TAB )
		desktop->selected = (desktop->selected + num_actions + 1) % num_actions;

	if ( kbkey == KBKEY_ENTER && !desktop->command[0] )
	{
		class action* action = desktop->actions[desktop->selected];
		class object* object = action->get_object();
		if ( object->type() == TYPE_DIRECTORY )
			PushObject(desktop, object);
		else if ( object->type() == TYPE_FILE )
		{
			object->clear_error();
			object->invoke();
			UpdateActionList(desktop);
		}
	}
}

void HandleCodepoint(uint32_t codepoint, struct Desktop* desktop)
{
	if ( 128 <= codepoint )
		return;
	char c = (char) codepoint;
	if ( c == '\t' )
		return;
	if ( desktop->show_help )
	{
		if ( c == '\n' )
			desktop->show_help = false;
		return;
	}
	size_t column = 0;
	while ( desktop->command[column] )
		column++;
	if ( c == '\b' || c == 127 )
	{
		if ( column )
			desktop->command[column-1] = '\0';
	}
	else if ( c == '\n' )
	{
		if ( !desktop->command[0] && desktop->num_actions != 0 )
			return;
		if ( class object* new_object = desktop->object->command_line(desktop->command) )
			PushObject(desktop, new_object);
		memset(&desktop->command, 0, sizeof(desktop->command));
		return;
	}
	else
		desktop->command[column] = c;
}

static void InitializeDesktop(struct Desktop* desktop)
{
	memset(desktop, 0, sizeof(*desktop));
	clock_gettime(CLOCK_MONOTONIC, &desktop->init_time);
	desktop->last_update = desktop->init_time;
	desktop->angry = false;
	desktop->rotate_angle = M_PI_2;
	desktop->selected = 0;
	desktop->color_angle = 0.f;
	desktop->text_angle = 0.f;
	desktop->size_factor = 1.f;
	desktop->control = false;
	desktop->lshift = false;
	desktop->rshift = false;
	desktop->actions = NULL;
	desktop->num_actions = 0;
	desktop->object = new class desktop();
	UpdateActionList(desktop);
}

static void HandleKeyboardEvents(int kbfd, struct Desktop* desktop)
{
	if ( desktop->since_init.tv_sec < 3 )
		return;

	const size_t BUFFER_LENGTH = 64;
	uint32_t input[BUFFER_LENGTH];
	while ( true )
	{
		settermmode(kbfd, TERMMODE_KBKEY | TERMMODE_UNICODE | TERMMODE_NONBLOCK);
		ssize_t num_bytes = read(kbfd, input, sizeof(input));
		if ( num_bytes < 0 )
			return;
		size_t num_kbkeys = num_bytes / sizeof(uint32_t);
		for ( size_t i = 0; i < num_kbkeys; i++ )
		{
			int kbkey = KBKEY_DECODE(input[i]);
			if ( kbkey )
				HandleKeystroke(kbkey, desktop);
			else
				HandleCodepoint(input[i], desktop);
		}
	}
}

static void HandleEvents(int kbfd, struct Desktop* desktop)
{
	const nfds_t NFDS = 1;
	struct pollfd fds[NFDS];
	fds[0].fd = kbfd;
	fds[0].events = POLLIN;
	fds[0].revents = 0;
	if ( 0 < poll(fds, NFDS, 0) )
	{
		if ( fds[0].revents )
			HandleKeyboardEvents(kbfd, desktop);
	}

	struct timespec now;
	clock_gettime(CLOCK_MONOTONIC, &now);
	struct timespec delta_ts = timespec_sub(now, desktop->last_update);
	desktop->last_update = now;
	float delta = delta_ts.tv_sec * 1E0f + delta_ts.tv_nsec * 1E-9f;

	time_t xtime_sec = desktop->experienced_time.tv_sec;
	if ( !desktop->show_help && delta_ts.tv_sec == 0 )
		desktop->experienced_time = timespec_add(desktop->experienced_time, delta_ts);

	desktop->warning = NULL;
	desktop->critical_warning = false;
	desktop->flashing_warning = false;
	desktop->rune_warning = false;
	desktop->angry = false;
	if ( desktop->object->is_unstable_core()  )
	{
		char* stuff = gibberish();
		snprintf(desktop->warning_buffer, sizeof(desktop->warning_buffer),
			     "!! TRINIT CORE UNSTABLE: %s !!", stuff);
		desktop->warning = desktop->warning_buffer;
		desktop->critical_warning = true;
		desktop->flashing_warning = true;
		free(stuff);
	}
	else if ( desktop->object->is_core()  )
	{
		desktop->warning = "TRINIT CORE";
		desktop->critical_warning = true;
		desktop->flashing_warning = true;
	}
	else if ( !configured_use_runes )
	{
		if ( 0 <= xtime_sec && xtime_sec < 15 )
			desktop->warning = "Press F1 for basic usage help";
		if ( 20 <= xtime_sec && xtime_sec < 30 )
			desktop->warning = "Runes have not been configured";
		if ( 45 <= xtime_sec && xtime_sec < 60 )
			desktop->warning = "Note: Runes have not been configured";
		if ( 80 <= xtime_sec && xtime_sec < 100 )
			desktop->warning = "Warning: Runes have not been configured",
			desktop->critical_warning = true;
		if ( 110 <= xtime_sec && xtime_sec < 130 )
		{
			snprintf(desktop->warning_buffer, sizeof(desktop->warning_buffer),
				     "WARNING: UNDEPLOYED RUNES WILL BE DEPLOYED IN: %u",
				     130 - (unsigned int) xtime_sec);
			desktop->warning = desktop->warning_buffer;
			desktop->critical_warning = true;
			desktop->flashing_warning = true;
			desktop->rune_warning = true;
			desktop->angry = true;
		}
		if ( 130 <= xtime_sec )
		{
			use_runes = true;
			configured_use_runes = true;
			unlink("/etc/rune-disable");
			close(::open("/etc/rune-enable", O_WRONLY | O_CREAT, 0666));
		}
	}

	desktop->since_init = timespec_sub(now, desktop->init_time);
	bool init_boosted = timespec_lt(desktop->since_init, timespec_make(5, 0));
	float since_init = desktop->since_init.tv_sec * 1E0f + desktop->since_init.tv_nsec * 1E-9f;

	float rotate_speed = 2*M_PI / (desktop->angry ? 2 : 60);
	float color_speed = 2*M_PI / (desktop->angry ? 1 : 20);
	float text_speed = 2*M_PI / (desktop->angry ? 10 : 20);

	if ( desktop->object->is_core() )
	{
		rotate_speed *= 10;
		text_speed *= 5;
	}

	if ( desktop->object->is_unstable_core() )
	{
		rotate_speed *= 1.5;
		text_speed *= -1.0;
		color_speed *= 20;
	}

	if ( init_boosted )
	{
		rotate_speed *= 1.f + 100.0 * cosf(M_PI_2 * (since_init - 3.0) / 2.0);
		desktop->size_factor = sinf(M_PI_2 * (since_init - 3.0) / 2.0);
	}
	else
		desktop->size_factor = 1.0f;

	desktop->rotate_angle += rotate_speed * delta;
	desktop->color_angle += color_speed * delta;
	desktop->text_angle += text_speed * delta;
}

static int MainLoop(int argc, char* argv[], int kbfd)
{
	(void) argc;
	(void) argv;
	struct RenderInfo info;
	InitializeDesktop(&info.desktop);
	for ( info.frame = 0; true; info.frame++ )
	{
		HandleEvents(kbfd, &info.desktop);
		if ( !Render(&info) )
			return 1;
	}
	return 0;
}

static int CreateKeyboardConnection()
{
	int fd = open("/dev/tty", O_RDONLY | O_CLOEXEC);
	if ( fd < 0 )
		return -1;
	if ( settermmode(fd, TERMMODE_KBKEY | TERMMODE_UNICODE | TERMMODE_NONBLOCK) )
		return close(fd), -1;
	return fd;
}

int main(int argc, char* argv[])
{
	struct stat st;
	if ( stat("/etc/rune-enable", &st) == 0 )
		use_runes = true, configured_use_runes = true;
	else if ( stat("/etc/rune-disable", &st) == 0 )
		use_runes = false, configured_use_runes = true;

	int fontfd = open("/dev/vgafont", O_RDONLY);
	readall(fontfd, font, sizeof(font));
	close(fontfd);
	uint8_t rune_font[26][16] =
	{
		{ 0x00, 0x4a, 0x4a, 0x52, 0x64, 0x48, 0x70, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x00 },
		{ 0x00, 0x40, 0x40, 0x60, 0x60, 0x50, 0x50, 0x50, 0x48, 0x48, 0x48, 0x44, 0x44, 0x42, 0x42, 0x00 },
		{ 0x00, 0x40, 0x40, 0x40, 0x60, 0x50, 0x48, 0x44, 0x44, 0x48, 0x50, 0x60, 0x40, 0x40, 0x40, 0x00 },
		{ 0x00, 0x40, 0x60, 0x50, 0x48, 0x44, 0x60, 0x50, 0x48, 0x44, 0x40, 0x40, 0x40, 0x40, 0x40, 0x00 },
		{ 0x00, 0x60, 0x50, 0x48, 0x44, 0x48, 0x50, 0x60, 0x60, 0x50, 0x48, 0x44, 0x40, 0x40, 0x40, 0x00 },
		{ 0x00, 0x42, 0x44, 0x48, 0x50, 0x60, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x00 },
		{ 0x00, 0x10, 0x10, 0x10, 0x92, 0x54, 0x38, 0x10, 0x38, 0x54, 0x92, 0x10, 0x10, 0x10, 0x10, 0x00 },
		{ 0x00, 0x10, 0x10, 0x10, 0x90, 0x50, 0x30, 0x10, 0x18, 0x14, 0x12, 0x10, 0x10, 0x10, 0x10, 0x00 },
		{ 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00 },
		{ 0x00, 0x10, 0x10, 0x10, 0x10, 0x12, 0x14, 0x18, 0x10, 0x30, 0x50, 0x90, 0x10, 0x10, 0x10, 0x00 },
		{ 0x00, 0x40, 0x40, 0x40, 0x40, 0x40, 0x44, 0x4c, 0x54, 0x64, 0x44, 0x04, 0x04, 0x04, 0x04, 0x00 },
		{ 0x00, 0x10, 0x38, 0x54, 0x92, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00 },
		{ 0x00, 0x60, 0x50, 0x48, 0x44, 0x48, 0x50, 0x60, 0x60, 0x50, 0x48, 0x44, 0x48, 0x50, 0x60, 0x00 },
		{ 0x00, 0x92, 0x92, 0x54, 0x38, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00 },
		{ 0x00, 0x40, 0x60, 0x50, 0x48, 0x44, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x00 },
		{ 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x38, 0x54, 0x92, 0x92, 0x00 },
	};

	memcpy(font + 128 * 16, rune_font, sizeof(rune_font));

	int kbfd = CreateKeyboardConnection();
	if ( kbfd < 0 )
		error(1, 0, "couldn't create keyboard connection");

	int ret = MainLoop(argc, argv, kbfd);

	close(kbfd);

	return ret;
}
