/*
 * Copyright (c) 2019-2020, yzrh <yzrh@noema.org>
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>

#ifdef __linux__

#include <sys/types.h>
#include <sys/stat.h>
#include <poll.h>

#endif /* __linux__ */

#include <SDL2/SDL.h>
#include <SDL2/SDL_ttf.h>

#ifdef SOUND

#include <SDL2/SDL_mixer.h>

#endif /* SOUND */

#include "version.h"
#include "extern.h"
#include "screen.h"

#ifdef GPIO

#include "gpio.h"

#endif /* GPIO */

static pthread_mutex_t mutex;
static pthread_cond_t cond;

static SDL_Renderer *renderer;
static TTF_Font *font_20;
static TTF_Font *font_24;

/*
 * Default to keyboard,
 * GPIO inputs are mapped to controller keys
 */
static Snake_Device device = SNAKE_KEYBOARD;
static int selection = 0;

static Snake_Text text_title;
static Snake_Text text_help_top;
static Snake_Text text_help_0;
static Snake_Text text_help_1;
static Snake_Text text_help_2;
static Snake_Text text_help_3;
static Snake_Text text_help_4;
static Snake_Text text_help_5;
static Snake_Text text_help_6;
static Snake_Text text_help_7;

static Snake_Text text_score;

static Snake_Text text_prompt_start;
static Snake_Text text_prompt_resume;

static Snake_Text text_prompt_0;
static Snake_Text text_prompt_1;
static Snake_Text text_prompt_2;

static bool start = 1;
static bool end = 0;
static bool begin = 0;

static bool god = 0;
static bool reset = 0;
static bool wait = 0;
static bool multiplayer = 0;
static bool flappy = 0;

static struct timespec delay = {
	.tv_sec = 0,
	.tv_nsec = TICK_TIME_INIT
};

static int score;
static int result;
static int distance;
static int speed;

static Snake_Input input = SNAKE_EMPTY;
static Snake_Input head[2][2];

static Snake_Pos *snake0 = NULL;
static Snake_Pos *snake1 = NULL;

static int fruit[2];

static Snake_Pos *gap = NULL;

static void*
handler_rendering(void *args)
{
	const SDL_Color fg = {
		COLOUR_FOREGROUND_R,
		COLOUR_FOREGROUND_G,
		COLOUR_FOREGROUND_B
	};
	const SDL_Color bg = {
		COLOUR_BACKGROUND_R,
		COLOUR_BACKGROUND_G,
		COLOUR_BACKGROUND_B
	};
	const SDL_Color bg_s = {
		COLOUR_BACKGROUND_SHADE_R,
		COLOUR_BACKGROUND_SHADE_G,
		COLOUR_BACKGROUND_SHADE_B
	};

	char result_str[23];
	char score_str[11];

	Snake_Text text_result;

	Snake_Text text_welcome;
	Snake_Text text_clear;
	Snake_Text text_gameover;
	Snake_Text text_pause;

	while (!end) {
		draw_clear(renderer);
		draw_colour(renderer, SNAKE_FOREGROUND);

		draw_wall(renderer);

		draw_text(renderer,
			font_20,
			SCREEN_HEIGHT + 4 * SCREEN_UNIT_X,
			SCREEN_UNIT_Y,
			"Snake",
			1,
			0,
			fg,
			bg,
			&text_title);
		draw_text(renderer,
			font_20,
			SCREEN_HEIGHT + 4 * SCREEN_UNIT_X,
			2 * SCREEN_UNIT_Y,
			"Help",
			1,
			0,
			fg,
			bg,
			&text_help_top);
		if (god)
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT + SCREEN_UNIT_X / 4,
				2.5 * SCREEN_UNIT_Y,
				"0. Don't forget to cheat",
				0,
				0,
				fg,
				bg,
				&text_help_0);
		if (device == SNAKE_KEYBOARD) {
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT + SCREEN_UNIT_X / 4,
				3 * SCREEN_UNIT_Y,
				"1. Classic: use arrow keys to move",
				0,
				0,
				fg,
				bg,
				&text_help_1);
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT + SCREEN_UNIT_X / 4,
				3.5 * SCREEN_UNIT_Y,
				"2. Multiplayer: use s, e, d, f keys",
				0,
				0,
				fg,
				bg,
				&text_help_2);
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT + SCREEN_UNIT_X / 4,
				4 * SCREEN_UNIT_Y,
				"3. Flappy: SPACE key to jump",
				0,
				0,
				fg,
				bg,
				&text_help_3);
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT + SCREEN_UNIT_X / 4,
				4.5 * SCREEN_UNIT_Y,
				"4. ESCAPE key to pause",
				0,
				0,
				fg,
				bg,
				&text_help_4);
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT + SCREEN_UNIT_X / 4,
				5 * SCREEN_UNIT_Y,
				"5. r key to reset",
				0,
				0,
				fg,
				bg,
				&text_help_5);
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT + SCREEN_UNIT_X / 4,
				5.5 * SCREEN_UNIT_Y,
				"6. m key to toggle music",
				0,
				0,
				fg,
				bg,
				&text_help_6);
		} else if (device == SNAKE_TOUCHSCREEN) {
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT + SCREEN_UNIT_X / 4,
				3 * SCREEN_UNIT_Y,
				"1. Classic: swipe to move",
				0,
				0,
				fg,
				bg,
				&text_help_1);
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT + SCREEN_UNIT_X / 4,
				3.5 * SCREEN_UNIT_Y,
				"2. Multiplayer: use different side",
				0,
				0,
				fg,
				bg,
				&text_help_2);
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT + SCREEN_UNIT_X / 4,
				4 * SCREEN_UNIT_Y,
				"3. Flappy: tap to jump",
				0,
				0,
				fg,
				bg,
				&text_help_3);
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT + SCREEN_UNIT_X / 4,
				4.5 * SCREEN_UNIT_Y,
				"4. Tap here to pause",
				0,
				1,
				fg,
				bg,
				&text_help_4);
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT + SCREEN_UNIT_X / 4,
				5 * SCREEN_UNIT_Y,
				"5. Tap here to reset",
				0,
				1,
				fg,
				bg,
				&text_help_5);
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT + SCREEN_UNIT_X / 4,
				5.5 * SCREEN_UNIT_Y,
				"6. Tap here to toggle music",
				0,
				1,
				fg,
				bg,
				&text_help_6);
		} else if (device == SNAKE_CONTROLLER) {
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT + SCREEN_UNIT_X / 4,
				3 * SCREEN_UNIT_Y,
				"1. Classic: use D-pad to move",
				0,
				0,
				fg,
				bg,
				&text_help_1);
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT + SCREEN_UNIT_X / 4,
				3.5 * SCREEN_UNIT_Y,
				"2. Multiplayer: second controller",
				0,
				0,
				fg,
				bg,
				&text_help_2);
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT + SCREEN_UNIT_X / 4,
				4 * SCREEN_UNIT_Y,
				"3. Flappy: Press A to jump",
				0,
				0,
				fg,
				bg,
				&text_help_3);
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT + SCREEN_UNIT_X / 4,
				4.5 * SCREEN_UNIT_Y,
				"4. Press BACK to pause",
				0,
				0,
				fg,
				bg,
				&text_help_4);
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT + SCREEN_UNIT_X / 4,
				5 * SCREEN_UNIT_Y,
				"5. Press X to reset",
				0,
				0,
				fg,
				bg,
				&text_help_5);
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT + SCREEN_UNIT_X / 4,
				5.5 * SCREEN_UNIT_Y,
				"6. Press Y to toggle music",
				0,
				0,
				fg,
				bg,
				&text_help_6);
		}
		draw_text(renderer,
			font_20,
			SCREEN_HEIGHT + SCREEN_UNIT_X / 4,
			6 * SCREEN_UNIT_Y,
			"7. Have fun",
			0,
			0,
			fg,
			bg,
			&text_help_7);

		snprintf(score_str, 11, "Score: %d", score);

		draw_text(renderer,
			font_20,
			SCREEN_HEIGHT + 4 * SCREEN_UNIT_X,
			7 * SCREEN_UNIT_Y,
			score_str,
			1,
			0,
			fg,
			bg,
			&text_score);

		draw_snake(renderer, &snake0);

		if (flappy) {
			draw_pipe(renderer, &gap);
		} else if (multiplayer)
			draw_snake(renderer, &snake1);
		else if (!start)
			draw_fruit(renderer, fruit);

		if (start || reset) {
			if (multiplayer) {
				if (result == 3)
					snprintf(result_str, 23, "Draw");
				else if (result > 0)
					snprintf(result_str, 23,
						"%s is the winner",
						result == 1 ?
						"Player 2" : "Player 1");
				else
					snprintf(result_str, 23, "Reset");
				text_result.cache = 0;

				draw_text(renderer,
					font_24,
					SCREEN_HEIGHT / 2,
					SCREEN_HEIGHT / 2 -
					0.75 * SCREEN_UNIT_Y,
					result_str,
					1,
					0,
					fg,
					bg_s,
					&text_result);
			}
			if (start)
				draw_text(renderer,
					font_24,
					SCREEN_HEIGHT / 2,
					SCREEN_HEIGHT / 2,
					"Welcome",
					1,
					0,
					fg,
					bg_s,
					&text_welcome);
			else if (!flappy && !multiplayer &&
				score == SCREEN_BLOCK / 2)
				draw_text(renderer,
					font_24,
					SCREEN_HEIGHT / 2,
					SCREEN_HEIGHT / 2,
					"Why don't you have legs?",
					1,
					0,
					fg,
					bg_s,
					&text_clear);
			else
				draw_text(renderer,
					font_24,
					SCREEN_HEIGHT / 2,
					SCREEN_HEIGHT / 2,
					"Game over!",
					1,
					0,
					fg,
					bg_s,
					&text_gameover);
			if (device == SNAKE_KEYBOARD)
				draw_text(renderer,
					font_20,
					SCREEN_HEIGHT / 2,
					SCREEN_HEIGHT / 2 +
					0.75 * SCREEN_UNIT_Y,
					"Press RETURN key for selection",
					1,
					0,
					fg,
					bg_s,
					&text_prompt_start);
			else if (device == SNAKE_TOUCHSCREEN)
				draw_text(renderer,
					font_20,
					SCREEN_HEIGHT / 2,
					SCREEN_HEIGHT / 2 +
					0.75 * SCREEN_UNIT_Y,
					"Tap the option to select",
					1,
					0,
					fg,
					bg_s,
					&text_prompt_start);
			else if (device == SNAKE_CONTROLLER)
				draw_text(renderer,
					font_20,
					SCREEN_HEIGHT / 2,
					SCREEN_HEIGHT / 2 +
					0.75 * SCREEN_UNIT_Y,
					"Press START to select",
					1,
					0,
					fg,
					bg_s,
					&text_prompt_start);
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT / 2,
				SCREEN_HEIGHT / 2 + 1.5 * SCREEN_UNIT_Y,
				"Classic",
				1,
				device == SNAKE_TOUCHSCREEN ||
				selection == 0 ? 1 : 0,
				fg,
				bg_s,
				&text_prompt_0);
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT / 2,
				SCREEN_HEIGHT / 2 + 2.25 * SCREEN_UNIT_Y,
				"Multiplayer",
				1,
				device == SNAKE_TOUCHSCREEN ||
				selection == 1 ? 1 : 0,
				fg,
				bg_s,
				&text_prompt_1);
			draw_text(renderer,
				font_20,
				SCREEN_HEIGHT / 2,
				SCREEN_HEIGHT / 2 + 3 * SCREEN_UNIT_Y,
				"Flappy",
				1,
				device == SNAKE_TOUCHSCREEN ||
				selection == 2 ? 1 : 0,
				fg,
				bg_s,
				&text_prompt_2);
		} else if (wait) {
			draw_text(renderer,
				font_24,
				SCREEN_HEIGHT / 2,
				SCREEN_HEIGHT / 2,
				"Paused",
				1,
				0,
				fg,
				bg_s,
				&text_pause);
			if (device == SNAKE_KEYBOARD)
				draw_text(renderer,
					font_20,
					SCREEN_HEIGHT / 2,
					SCREEN_HEIGHT / 2 +
					0.75 * SCREEN_UNIT_Y,
					"Press ESCAPE key to resume",
					1,
					0,
					fg,
					bg_s,
					&text_prompt_resume);
			else if (device == SNAKE_TOUCHSCREEN)
				draw_text(renderer,
					font_20,
					SCREEN_HEIGHT / 2,
					SCREEN_HEIGHT / 2 +
					0.75 * SCREEN_UNIT_Y,
					"Tap here to resume",
					1,
					1,
					fg,
					bg_s,
					&text_prompt_resume);
			else if (device == SNAKE_CONTROLLER)
				draw_text(renderer,
					font_20,
					SCREEN_HEIGHT / 2,
					SCREEN_HEIGHT / 2 +
					0.75 * SCREEN_UNIT_Y,
					"Press START to resume",
					1,
					0,
					fg,
					bg_s,
					&text_prompt_resume);
		}

		SDL_RenderPresent(renderer);
	}
	return 0;
}

static void*
handler_logic(void *args)
{
	struct timespec clock_start;
	struct timespec clock_end;
	double clock_pass = 0;
	int pipe_dim[6];
	int pipe_pos = 0;

	clock_gettime(CLOCK_MONOTONIC, &clock_start);

	for (;;) {
		pthread_mutex_lock(&mutex);
		while (start || reset || wait) {
			if (!wait) {
				clock_pass = 0;
				pipe_pos = 0;
			}
			pthread_cond_wait(&cond, &mutex);
			clock_gettime(CLOCK_MONOTONIC, &clock_start);
		}
		pthread_mutex_unlock(&mutex);

		if (end)
			return 0;

		if (flappy) {
			pthread_mutex_lock(&mutex);
			while (!begin)
				pthread_cond_wait(&cond, &mutex);
			pthread_mutex_unlock(&mutex);

			game_pipe_bound(&gap, pipe_dim);

			if (pipe_pos - SCREEN_UNIT > pipe_dim[0]) {
				score++;
				text_score.cache = 0;
			}
			pipe_pos = pipe_dim[0];

			if (!god && (collision(
				(int[]) {snake0->next->x, snake0->next->y,
				2 * SCREEN_UNIT, SCREEN_UNIT},
				(int[]) {pipe_dim[0], pipe_dim[2],
				pipe_dim[1], pipe_dim[3]}) ||
				collision(
				(int[]) {snake0->next->x, snake0->next->y,
				2 * SCREEN_UNIT, SCREEN_UNIT},
				(int[]) {pipe_dim[0], pipe_dim[4],
				pipe_dim[1], pipe_dim[5]}) ||
				snake0->y < 0 ||
				snake0->y + SCREEN_UNIT > SCREEN_HEIGHT)) {
				pthread_mutex_lock(&mutex);
				reset = 1;
				pthread_mutex_unlock(&mutex);
			} else {
				game_pipe_move(&gap);

				game_pipe_pos(&gap, &result);
				if (result == 3) {
					game_pipe_del(&gap);
					game_pipe_add(&gap);
				} else if (result == 2) {
					game_pipe_add(&gap);
				} else if (result == 1) {
					game_pipe_del(&gap);
				}
			}
		} else if (!multiplayer) {
			game_snake_move(&snake0, NULL,
				head[0], !god, &result);

			if (result == 0 || god) {
				head[1][0] = head[0][0];

				if (collision_block(fruit,
					(int[]) {snake0->x, snake0->y})) {
					score++;
					text_score.cache = 0;
					game_snake_grow(&snake0);

					if (score == SCREEN_BLOCK / 2) {
						pthread_mutex_lock(&mutex);
						reset = 1;
						pthread_mutex_unlock(&mutex);
					}

					game_fruit(fruit, &snake0);

					if (delay.tv_nsec - TICK_TIME_DEC >=
						TICK_TIME_MIN)
						delay.tv_nsec -= TICK_TIME_DEC;
				}
			} else {
				pthread_mutex_lock(&mutex);
				reset = 1;
				pthread_mutex_unlock(&mutex);
			}
		} else {
			game_snake_move(&snake0, &snake1,
				head[0], !god, &result);

			if (result == 0 || god) {
				head[1][0] = head[0][0];
				head[1][1] = head[0][1];

				clock_gettime(CLOCK_MONOTONIC, &clock_end);
				clock_pass += (clock_end.tv_sec -
					clock_start.tv_sec) +
					(clock_end.tv_nsec -
					clock_start.tv_nsec) * 0.000000001;
				clock_gettime(CLOCK_MONOTONIC, &clock_start);

				score = clock_pass;
				text_score.cache = 0;
				game_snake_grow(&snake0);
				game_snake_grow(&snake1);
			} else {
				pthread_mutex_lock(&mutex);
				reset = 1;
				pthread_mutex_unlock(&mutex);
			}
		}

		nanosleep(&delay, NULL);
	}
}

static void*
handler_gravity(void *args)
{
	const struct timespec delay = {
		.tv_sec = 0,
		.tv_nsec = 1000000000 / SCREEN_UNIT
	};

	for (;;) {
		pthread_mutex_lock(&mutex);
		while (!flappy || !begin || start || reset || wait)
			pthread_cond_wait(&cond, &mutex);
		pthread_mutex_unlock(&mutex);

		if (end)
			return 0;

		if (distance > 0) {
			game_snake_fall(&snake0, 0,
				distance > speed ? speed : distance);
			speed++;

			pthread_mutex_lock(&mutex);
			distance -= distance > speed ? speed : distance;
			if (distance == 0)
				speed = 1;
			pthread_mutex_unlock(&mutex);
		} else {
			game_snake_fall(&snake0, 1, speed);
			speed++;
		}

		nanosleep(&delay, NULL);
	}
}

#ifdef GPIO

#ifdef __FreeBSD__

static void*
handler_gpio(void *args)
{
	const struct timespec delay = {
		.tv_sec = 0,
		.tv_nsec = 50000000
	};

	SDL_Event event;
	event.type = SDL_CONTROLLERBUTTONUP;

	while (!end) {
		if (gpio_read(GPIO_PIN_IN0) == 0) {
			event.cbutton.button =
				SDL_CONTROLLER_BUTTON_DPAD_LEFT;
			event.cbutton.which = 0;
			SDL_PushEvent(&event);
		} else if (gpio_read(GPIO_PIN_IN1) == 0) {
			event.cbutton.button =
				SDL_CONTROLLER_BUTTON_DPAD_UP;
			event.cbutton.which = 0;
			SDL_PushEvent(&event);
		} else if (gpio_read(GPIO_PIN_IN2) == 0) {
			event.cbutton.button =
				SDL_CONTROLLER_BUTTON_DPAD_DOWN;
			event.cbutton.which = 0;
			SDL_PushEvent(&event);
		} else if (gpio_read(GPIO_PIN_IN3) == 0) {
			event.cbutton.button =
				SDL_CONTROLLER_BUTTON_DPAD_RIGHT;
			event.cbutton.which = 0;
			SDL_PushEvent(&event);
		} else if (gpio_read(GPIO_PIN_IN4) == 0) {
			event.cbutton.button =
				SDL_CONTROLLER_BUTTON_BACK;
			event.cbutton.which = 0;
			SDL_PushEvent(&event);
		} else if (gpio_read(GPIO_PIN_IN5) == 0) {
			event.cbutton.button =
				SDL_CONTROLLER_BUTTON_GUIDE;
			event.cbutton.which = 0;
			SDL_PushEvent(&event);
		} else if (gpio_read(GPIO_PIN_IN6) == 0) {
			event.cbutton.button =
				SDL_CONTROLLER_BUTTON_START;
			event.cbutton.which = 0;
			SDL_PushEvent(&event);
		} else if (gpio_read(GPIO_PIN_IN7) == 0) {
			event.cbutton.button =
				SDL_CONTROLLER_BUTTON_A;
			event.cbutton.which = 0;
			SDL_PushEvent(&event);
		} else if (gpio_read(GPIO_PIN_IN8) == 0) {
			event.cbutton.button =
				SDL_CONTROLLER_BUTTON_B;
			event.cbutton.which = 0;
			SDL_PushEvent(&event);
		} else if (gpio_read(GPIO_PIN_IN9) == 0) {
			event.cbutton.button =
				SDL_CONTROLLER_BUTTON_X;
			event.cbutton.which = 0;
			SDL_PushEvent(&event);
		} else if (gpio_read(GPIO_PIN_IN10) == 0) {
			event.cbutton.button =
				SDL_CONTROLLER_BUTTON_Y;
			event.cbutton.which = 0;
			SDL_PushEvent(&event);
		} else if (gpio_read(GPIO_PIN_IN11) == 0) {
			event.cbutton.button =
				SDL_CONTROLLER_BUTTON_LEFTSHOULDER;
			event.cbutton.which = 0;
			SDL_PushEvent(&event);
		} else if (gpio_read(GPIO_PIN_IN12) == 0) {
			event.cbutton.button =
				SDL_CONTROLLER_BUTTON_RIGHTSHOULDER;
			event.cbutton.which = 0;
			SDL_PushEvent(&event);
		} else if (gpio_read(GPIO_PIN_IN13) == 0) {
			event.cbutton.button =
				SDL_CONTROLLER_BUTTON_LEFTSTICK;
			event.cbutton.which = 0;
			SDL_PushEvent(&event);
		} else if (gpio_read(GPIO_PIN_IN14) == 0) {
			event.cbutton.button =
				SDL_CONTROLLER_BUTTON_RIGHTSTICK;
			event.cbutton.which = 0;
			SDL_PushEvent(&event);
		} else if (gpio_read(GPIO_PIN_IN15) == 0) {
			event.cbutton.button =
				SDL_CONTROLLER_BUTTON_DPAD_LEFT;
			event.cbutton.which = 1;
			SDL_PushEvent(&event);
		} else if (gpio_read(GPIO_PIN_IN16) == 0) {
			event.cbutton.button =
				SDL_CONTROLLER_BUTTON_DPAD_UP;
			event.cbutton.which = 1;
			SDL_PushEvent(&event);
		} else if (gpio_read(GPIO_PIN_IN17) == 0) {
			event.cbutton.button =
				SDL_CONTROLLER_BUTTON_DPAD_DOWN;
			event.cbutton.which = 1;
			SDL_PushEvent(&event);
		} else if (gpio_read(GPIO_PIN_IN18) == 0) {
			event.cbutton.button =
				SDL_CONTROLLER_BUTTON_DPAD_RIGHT;
			event.cbutton.which = 1;
			SDL_PushEvent(&event);
		}

		nanosleep(&delay, NULL);
	}
	return 0;
}

#elif __linux__

static void*
handler_gpio(void *args)
{
	struct pollfd pfd[19];
	char path[29];

	for (int i = 0; i < 19; i++)
		pfd[i].events = POLLPRI | POLLERR;

	snprintf(path, 29, "/sys/class/gpio/gpio%d/value", GPIO_PIN_IN0);
	pfd[0].fd = open(path, O_RDONLY);
	snprintf(path, 29, "/sys/class/gpio/gpio%d/value", GPIO_PIN_IN1);
	pfd[1].fd = open(path, O_RDONLY);
	snprintf(path, 29, "/sys/class/gpio/gpio%d/value", GPIO_PIN_IN2);
	pfd[2].fd = open(path, O_RDONLY);
	snprintf(path, 29, "/sys/class/gpio/gpio%d/value", GPIO_PIN_IN3);
	pfd[3].fd = open(path, O_RDONLY);
	snprintf(path, 29, "/sys/class/gpio/gpio%d/value", GPIO_PIN_IN4);
	pfd[4].fd = open(path, O_RDONLY);
	snprintf(path, 29, "/sys/class/gpio/gpio%d/value", GPIO_PIN_IN5);
	pfd[5].fd = open(path, O_RDONLY);
	snprintf(path, 29, "/sys/class/gpio/gpio%d/value", GPIO_PIN_IN6);
	pfd[6].fd = open(path, O_RDONLY);
	snprintf(path, 29, "/sys/class/gpio/gpio%d/value", GPIO_PIN_IN7);
	pfd[7].fd = open(path, O_RDONLY);
	snprintf(path, 29, "/sys/class/gpio/gpio%d/value", GPIO_PIN_IN8);
	pfd[8].fd = open(path, O_RDONLY);
	snprintf(path, 29, "/sys/class/gpio/gpio%d/value", GPIO_PIN_IN9);
	pfd[9].fd = open(path, O_RDONLY);
	snprintf(path, 29, "/sys/class/gpio/gpio%d/value", GPIO_PIN_IN10);
	pfd[10].fd = open(path, O_RDONLY);
	snprintf(path, 29, "/sys/class/gpio/gpio%d/value", GPIO_PIN_IN11);
	pfd[11].fd = open(path, O_RDONLY);
	snprintf(path, 29, "/sys/class/gpio/gpio%d/value", GPIO_PIN_IN12);
	pfd[12].fd = open(path, O_RDONLY);
	snprintf(path, 29, "/sys/class/gpio/gpio%d/value", GPIO_PIN_IN13);
	pfd[13].fd = open(path, O_RDONLY);
	snprintf(path, 29, "/sys/class/gpio/gpio%d/value", GPIO_PIN_IN14);
	pfd[14].fd = open(path, O_RDONLY);
	snprintf(path, 29, "/sys/class/gpio/gpio%d/value", GPIO_PIN_IN15);
	pfd[15].fd = open(path, O_RDONLY);
	snprintf(path, 29, "/sys/class/gpio/gpio%d/value", GPIO_PIN_IN16);
	pfd[16].fd = open(path, O_RDONLY);
	snprintf(path, 29, "/sys/class/gpio/gpio%d/value", GPIO_PIN_IN17);
	pfd[17].fd = open(path, O_RDONLY);
	snprintf(path, 29, "/sys/class/gpio/gpio%d/value", GPIO_PIN_IN18);
	pfd[18].fd = open(path, O_RDONLY);

	SDL_Event event;
	event.type = SDL_CONTROLLERBUTTONUP;

	char val[2];

	while (!end) {
		poll(pfd, 19, -1);

		for (int i = 0; i < 19; i++) {
			if (pfd[i].revents & (POLLPRI | POLLERR)) {
				pfd[i].revents &= ~(POLLPRI | POLLERR);
				read(pfd[i].fd, val, 2);
				lseek(pfd[i].fd, 0, SEEK_SET);

				if (atoi(val) != 0)
					continue;

				input_gpio(i % 15, &event.cbutton.button);

				if (i < 15)
					event.cbutton.which = 0;
				else
					event.cbutton.which = 1;

				SDL_PushEvent(&event);
				break;
			}
		}
	}
	return 0;
}

#endif /* __FreeBSD__ */

#endif /* GPIO */

int
main(void)
{
	printf("Snake " VERSION "." RELEASE "." PATCH EXTRA "\n"
		"Copyright (c) 2019-2020, yzrh <yzrh@noema.org>\n");

	if (SDL_Init(SDL_INIT_VIDEO |
		SDL_INIT_AUDIO |
		SDL_INIT_GAMECONTROLLER) != 0) {
		fprintf(stderr, "SDL initialisation failure: %s\n",
			SDL_GetError());
		return 1;
	}

	SDL_Window *window = SDL_CreateWindow(
		"Snake " VERSION "." RELEASE "." PATCH EXTRA,
		SDL_WINDOWPOS_UNDEFINED,
		SDL_WINDOWPOS_UNDEFINED,
		SCREEN_WIDTH,
		SCREEN_HEIGHT,
		SDL_WINDOW_OPENGL);

	if (window == NULL) {
		fprintf(stderr, "Window creation failure: %s\n",
			SDL_GetError());
		return 1;
	}

	renderer = SDL_CreateRenderer(
		window,
		-1,
		SDL_RENDERER_ACCELERATED |
		SDL_RENDERER_PRESENTVSYNC);

	if (renderer == NULL) {
		fprintf(stderr, "Renderer creation failure: %s\n",
			SDL_GetError());
		return 1;
	}

	if (TTF_Init() != 0) {
		fprintf(stderr, "TTF initialisation failure: %s\n",
			TTF_GetError());
		return 1;
	}

	font_20 = TTF_OpenFont("../contrib/font.ttf", 20);
	font_24 = TTF_OpenFont("../contrib/font.ttf", 24);

	if (font_20 == NULL) {
		font_20 = TTF_OpenFont("/usr/local/share/snake/font.ttf", 20);
		font_24 = TTF_OpenFont("/usr/local/share/snake/font.ttf", 24);
	}

	if (font_20 == NULL) {
		font_20 = TTF_OpenFont("/usr/share/snake/font.ttf", 20);
		font_24 = TTF_OpenFont("/usr/share/snake/font.ttf", 24);
	}

	if (font_20 == NULL) {
		fprintf(stderr, "%s\n", TTF_GetError());
		return 1;
	}

#ifdef SOUND

	if ((Mix_Init(MIX_INIT_FLAC) & MIX_INIT_FLAC) != MIX_INIT_FLAC)
		fprintf(stderr, "Mixer initialisation failure: %s\n",
			Mix_GetError());

	if (Mix_OpenAudio(48000, AUDIO_S16SYS, 2, 4096) == -1)
		fprintf(stderr, "%s\n", Mix_GetError());

	Mix_Music *music = Mix_LoadMUS("../contrib/music.flac");

	if (music == NULL)
		music = Mix_LoadMUS("/usr/local/share/snake/music.flac");

	if (music == NULL)
		music = Mix_LoadMUS("/usr/share/snake/music.flac");

	if (music == NULL)
		fprintf(stderr, "%s\n", Mix_GetError());

	if (Mix_PlayMusic(music, -1) == -1)
		fprintf(stderr, "Audio playback failure: %s\n",
			Mix_GetError());

#endif /* SOUND */

#ifdef GPIO

#ifdef __linux__

	gpio_export(GPIO_PIN_IN0);
	gpio_export(GPIO_PIN_IN1);
	gpio_export(GPIO_PIN_IN2);
	gpio_export(GPIO_PIN_IN3);
	gpio_export(GPIO_PIN_IN4);
	gpio_export(GPIO_PIN_IN5);
	gpio_export(GPIO_PIN_IN6);
	gpio_export(GPIO_PIN_IN7);
	gpio_export(GPIO_PIN_IN8);
	gpio_export(GPIO_PIN_IN9);
	gpio_export(GPIO_PIN_IN10);
	gpio_export(GPIO_PIN_IN11);
	gpio_export(GPIO_PIN_IN12);
	gpio_export(GPIO_PIN_IN13);
	gpio_export(GPIO_PIN_IN14);
	gpio_export(GPIO_PIN_IN15);
	gpio_export(GPIO_PIN_IN16);
	gpio_export(GPIO_PIN_IN17);
	gpio_export(GPIO_PIN_IN18);

	/* Wait for sysfs directory to show up */
	sleep(1);

#endif /* __linux__ */

	gpio_config(GPIO_PIN_IN0, GPIO_IN, GPIO_INT);
	gpio_config(GPIO_PIN_IN1, GPIO_IN, GPIO_INT);
	gpio_config(GPIO_PIN_IN2, GPIO_IN, GPIO_INT);
	gpio_config(GPIO_PIN_IN3, GPIO_IN, GPIO_INT);
	gpio_config(GPIO_PIN_IN4, GPIO_IN, GPIO_INT);
	gpio_config(GPIO_PIN_IN5, GPIO_IN, GPIO_INT);
	gpio_config(GPIO_PIN_IN6, GPIO_IN, GPIO_INT);
	gpio_config(GPIO_PIN_IN7, GPIO_IN, GPIO_INT);
	gpio_config(GPIO_PIN_IN8, GPIO_IN, GPIO_INT);
	gpio_config(GPIO_PIN_IN9, GPIO_IN, GPIO_INT);
	gpio_config(GPIO_PIN_IN10, GPIO_IN, GPIO_INT);
	gpio_config(GPIO_PIN_IN11, GPIO_IN, GPIO_INT);
	gpio_config(GPIO_PIN_IN12, GPIO_IN, GPIO_INT);
	gpio_config(GPIO_PIN_IN13, GPIO_IN, GPIO_INT);
	gpio_config(GPIO_PIN_IN14, GPIO_IN, GPIO_INT);
	gpio_config(GPIO_PIN_IN15, GPIO_IN, GPIO_INT);
	gpio_config(GPIO_PIN_IN16, GPIO_IN, GPIO_INT);
	gpio_config(GPIO_PIN_IN17, GPIO_IN, GPIO_INT);
	gpio_config(GPIO_PIN_IN18, GPIO_IN, GPIO_INT);

#endif /* GPIO */

	int seed;
	int random = open("/dev/random", O_RDONLY);
	if (read(random, &seed, sizeof(int)) > 0)
		srand(seed);
	close(random);

	SDL_JoystickID player = 0;
	SDL_Event event;

	pthread_mutex_init(&mutex, NULL);
	pthread_cond_init(&cond, NULL);
	pthread_t th_renderer;
	pthread_t th_logic;
	pthread_t th_gravity;

	#ifdef GPIO

	pthread_t th_gpio;

	#endif /* GPIO */

	pthread_create(&th_renderer, NULL, handler_rendering, NULL);
	pthread_create(&th_logic, NULL, handler_logic, NULL);
	pthread_create(&th_gravity, NULL, handler_gravity, NULL);

	#ifdef GPIO

	pthread_create(&th_gpio, NULL, handler_gpio, NULL);

	#endif /* GPIO */

	while (SDL_WaitEvent(&event)) {
		if (event.type == SDL_QUIT)
			break;

		switch (event.type) {
			case SDL_KEYUP:
				if (device != SNAKE_KEYBOARD) {
					device = SNAKE_KEYBOARD;
					text_help_1.cache = 0;
					text_help_2.cache = 0;
					text_help_3.cache = 0;
					text_help_4.cache = 0;
					text_help_5.cache = 0;
					text_help_6.cache = 0;
					text_prompt_start.cache = 0;
					text_prompt_resume.cache = 0;
				}
				input_keyboard(event.key.keysym.sym, &input);
				if (multiplayer &&
					(event.key.keysym.sym ==
					SDLK_s ||
					event.key.keysym.sym ==
					SDLK_e ||
					event.key.keysym.sym ==
					SDLK_d ||
					event.key.keysym.sym ==
					SDLK_f))
					player = 1;
				else
					player = 0;
				break;
			case SDL_FINGERUP:
				if (device != SNAKE_TOUCHSCREEN) {
					device = SNAKE_TOUCHSCREEN;
					text_help_1.cache = 0;
					text_help_2.cache = 0;
					text_help_3.cache = 0;
					text_help_4.cache = 0;
					text_help_5.cache = 0;
					text_help_6.cache = 0;
					text_prompt_start.cache = 0;
					text_prompt_resume.cache = 0;
				}
				if (start || reset || wait) {
					if (collision_bound(
						event.tfinger.x,
						event.tfinger.y,
						&text_help_5)) {
						input = SNAKE_RESET;
					} else if (collision_bound(
						event.tfinger.x,
						event.tfinger.y,
						&text_help_6)) {
						input = SNAKE_MUSIC;
					} else if (wait) {
						if (collision_bound(
							event.tfinger.x,
							event.tfinger.y,
							&text_prompt_resume))
							input = SNAKE_PAUSE;
					} else {
						if (collision_bound(
							event.tfinger.x,
							event.tfinger.y,
							&text_prompt_0)) {
							selection = 0;
							input = SNAKE_SELECT;
						} else if (collision_bound(
							event.tfinger.x,
							event.tfinger.y,
							&text_prompt_1)) {
							selection = 1;
							input = SNAKE_SELECT;
						} else if (collision_bound(
							event.tfinger.x,
							event.tfinger.y,
							&text_prompt_2)) {
							selection = 2;
							input = SNAKE_SELECT;
						}
					}
				} else if (collision_bound(
					event.tfinger.x,
					event.tfinger.y,
					&text_help_4)) {
					input = SNAKE_PAUSE;
				} else if (collision_bound(
					event.tfinger.x,
					event.tfinger.y,
					&text_help_5)) {
					input = SNAKE_RESET;
				} else if (collision_bound(
					event.tfinger.x,
					event.tfinger.y,
					&text_help_6)) {
					input = SNAKE_MUSIC;
				} else if (collision_bound(
					event.tfinger.x,
					event.tfinger.y,
					&text_help_top)) {
					input = SNAKE_GOD;
				} else if (collision_bound(
					event.tfinger.x,
					event.tfinger.y,
					&text_score)) {
					input = SNAKE_GROW;
				} else if (collision_bound(
					event.tfinger.x,
					event.tfinger.y,
					&text_title)) {
					input = SNAKE_TELEPORT;
				} else if (collision_bound(
					event.tfinger.x,
					event.tfinger.y,
					&text_help_1)) {
					input = SNAKE_TICK_DECREASE;
				} else if (collision_bound(
					event.tfinger.x,
					event.tfinger.y,
					&text_help_7)) {
					input = SNAKE_TICK_INCREASE;
				} else if (flappy) {
					input = SNAKE_JUMP;
				} else {
					input = SNAKE_EMPTY;
				}
				break;
			case SDL_FINGERMOTION:
				if (device != SNAKE_TOUCHSCREEN) {
					device = SNAKE_TOUCHSCREEN;
					text_help_1.cache = 0;
					text_help_2.cache = 0;
					text_help_3.cache = 0;
					text_help_4.cache = 0;
					text_help_5.cache = 0;
					text_help_6.cache = 0;
					text_prompt_start.cache = 0;
					text_prompt_resume.cache = 0;
				}
				if ((event.tfinger.dx *
					event.tfinger.dx) >
					(event.tfinger.dy *
					event.tfinger.dy)) {
					if (event.tfinger.dx > 1)
						input = SNAKE_RIGHT;
					else if (event.tfinger.dx < -1)
						input = SNAKE_LEFT;
				} else {
					if (event.tfinger.dy > 1)
						input = SNAKE_DOWN;
					else if (event.tfinger.dy < -1)
						input = SNAKE_UP;
				}
				if (multiplayer &&
					event.tfinger.x > SCREEN_HEIGHT / 2)
					player = 1;
				else
					player = 0;
				break;
			case SDL_CONTROLLERBUTTONUP:
				if (device != SNAKE_CONTROLLER) {
					device = SNAKE_CONTROLLER;
					text_help_1.cache = 0;
					text_help_2.cache = 0;
					text_help_3.cache = 0;
					text_help_4.cache = 0;
					text_help_5.cache = 0;
					text_help_6.cache = 0;
					text_prompt_start.cache = 0;
					text_prompt_resume.cache = 0;
				}
				input_controller(event.cbutton.button, &input);
				player = event.cbutton.which;
				break;
			default:
				break;
		}

		if (!start && !reset && !wait) {
			switch (input) {
				case SNAKE_LEFT:
					if (head[1][player] != SNAKE_RIGHT)
						head[0][player] = SNAKE_LEFT;
					break;
				case SNAKE_UP:
					if (head[1][player] != SNAKE_DOWN)
						head[0][player] = SNAKE_UP;
					break;
				case SNAKE_DOWN:
					if (head[1][player] != SNAKE_UP)
						head[0][player] = SNAKE_DOWN;
					break;
				case SNAKE_RIGHT:
					if (head[1][player] != SNAKE_LEFT)
						head[0][player] = SNAKE_RIGHT;
					break;
				case SNAKE_JUMP:
					if (!begin) {
						pthread_mutex_lock(&mutex);
						begin = 1;
						pthread_cond_broadcast(&cond);
						pthread_mutex_unlock(&mutex);
					}
					if (flappy) {
						pthread_mutex_lock(&mutex);
						distance += PIPE_GAP / 2 *
							SCREEN_UNIT;
						if (distance == 0)
							speed = 1;
						pthread_mutex_unlock(&mutex);
					}
					break;
				case SNAKE_RESET:
					pthread_mutex_lock(&mutex);
					reset = 1;
					pthread_mutex_unlock(&mutex);
					break;
				case SNAKE_GOD:
					god = 1;
					break;
				case SNAKE_GROW:
					if (god && !flappy && !multiplayer)
						game_snake_grow(&snake0);
					break;
				case SNAKE_TELEPORT:
					if (god) {
						snake0->x = snake0->y =
							SCREEN_HEIGHT / 2;
						if (flappy) {
							snake0->next->x =
								SCREEN_HEIGHT /
								2 - SCREEN_UNIT;
							snake0->next->y =
								SCREEN_HEIGHT /
								2;
						}
					}
					break;
				case SNAKE_MUSIC:
#ifdef SOUND
					if (Mix_PausedMusic())
						Mix_ResumeMusic();
					else
						Mix_PauseMusic();
#endif /* SOUND */
					break;
				case SNAKE_TICK_DECREASE:
					if (god && !flappy && delay.tv_nsec -
						TICK_TIME_DEC >= TICK_TIME_MIN)
						delay.tv_nsec -= TICK_TIME_DEC;
					break;
				case SNAKE_TICK_INCREASE:
					if (god && !flappy)
						delay.tv_nsec += TICK_TIME_DEC;
					break;
				case SNAKE_PAUSE:
					wait = 1;
					break;
				default:
					break;
			}
		} else {
			switch (input) {
				case SNAKE_RESET:
					wait = 0;

					pthread_mutex_lock(&mutex);
					reset = 1;
					pthread_mutex_unlock(&mutex);
					break;
				case SNAKE_MUSIC:
#ifdef SOUND
					if (Mix_PausedMusic())
						Mix_ResumeMusic();
					else
						Mix_PauseMusic();
#endif /* SOUND */
					break;
				case SNAKE_SELECT:
					if (wait)
						break;

					if (selection == 0) {
						flappy = 0;
						multiplayer = 0;
					} else if (selection == 1) {
						flappy = 0;
						multiplayer = 1;
					} else if (selection == 2) {
						multiplayer = 0;
						flappy = 1;
					}

					delay.tv_nsec = TICK_TIME_INIT;

					god = 0;

					head[0][0] = head[0][1] =
						SNAKE_UP;
					head[1][0] = head[1][1] =
						SNAKE_EMPTY;

					score = 0;
					text_score.cache = 0;

					if (flappy) {
						delay.tv_nsec = TICK_TIME_INIT /
							SCREEN_UNIT;

						game_snake_init(&snake0, 0);

						snake0->next->x =
							SCREEN_HEIGHT / 2 -
							SCREEN_UNIT;
						snake0->next->y =
							SCREEN_HEIGHT / 2;

						game_pipe_init(&gap);

						pthread_mutex_lock(&mutex);
						begin = 0;
						distance = 0;
						speed = 1;
						pthread_mutex_unlock(&mutex);
					} else if (!multiplayer) {
						game_snake_init(
							&snake0, 0);
						game_fruit(
							fruit,
							&snake0);
					} else {
						game_snake_init(
							&snake0,
							-4 *
							SCREEN_UNIT);
						game_snake_init(
							&snake1,
							4 *
							SCREEN_UNIT);
					}
					start = 0;

					pthread_mutex_lock(&mutex);
					reset = 0;
					pthread_cond_broadcast(&cond);
					pthread_mutex_unlock(&mutex);
					break;
				case SNAKE_PAUSE:
					wait = 0;

					pthread_mutex_lock(&mutex);
					pthread_cond_broadcast(&cond);
					pthread_mutex_unlock(&mutex);
					break;
				case SNAKE_UP:
					if ((start || reset) &&
						selection - 1 >= 0)
						selection--;
					break;
				case SNAKE_DOWN:
					if ((start || reset) &&
						selection + 1 <= 2)
						selection++;
					break;
				default:
					break;
			}
		}
		input = SNAKE_EMPTY;
	}

	start = 0;
	end = 1;

	wait = 0;
	flappy = 1;

	pthread_mutex_lock(&mutex);
	reset = 0;
	pthread_cond_broadcast(&cond);
	pthread_mutex_unlock(&mutex);

	#ifdef GPIO

	pthread_join(th_gpio, NULL);

	#endif /* GPIO */

	pthread_join(th_gravity, NULL);
	pthread_join(th_logic, NULL);
	pthread_join(th_renderer, NULL);
	pthread_cond_destroy(&cond);
	pthread_mutex_destroy(&mutex);

#ifdef GPIO

#ifdef __linux__

	gpio_unexport(GPIO_PIN_IN0);
	gpio_unexport(GPIO_PIN_IN1);
	gpio_unexport(GPIO_PIN_IN2);
	gpio_unexport(GPIO_PIN_IN3);
	gpio_unexport(GPIO_PIN_IN4);
	gpio_unexport(GPIO_PIN_IN5);
	gpio_unexport(GPIO_PIN_IN6);
	gpio_unexport(GPIO_PIN_IN7);
	gpio_unexport(GPIO_PIN_IN8);
	gpio_unexport(GPIO_PIN_IN9);
	gpio_unexport(GPIO_PIN_IN10);
	gpio_unexport(GPIO_PIN_IN11);
	gpio_unexport(GPIO_PIN_IN12);
	gpio_unexport(GPIO_PIN_IN13);
	gpio_unexport(GPIO_PIN_IN14);
	gpio_unexport(GPIO_PIN_IN15);
	gpio_unexport(GPIO_PIN_IN16);
	gpio_unexport(GPIO_PIN_IN17);
	gpio_unexport(GPIO_PIN_IN18);

#endif /* __linux__ */

#endif /* GPIO */

#ifdef SOUND
	Mix_Quit();
#endif /* SOUND */

	TTF_Quit();
	SDL_DestroyRenderer(renderer);
	SDL_DestroyWindow(window);
	SDL_Quit();
}
