// @TODO:
// - split
// - Windows installation
#include "base.h"
no_warn_start
#if _WIN32
#include <SDL.h>
#else
#include <SDL2/SDL.h>
#endif
no_warn_end
#include <GL/gl.h>
#include <locale.h>
#include <wctype.h>
#if _WIN32
#include <shellapi.h>
#endif

#include "unicode.h"
#if DEBUG
#include "text.h"
#else
#include "text.c"
#endif
#include "util.c"
#define MATH_GL
#include "math.c"
#if _WIN32
#include "filesystem-win.c"
#elif __unix__
#include "filesystem-posix.c"
#else
#error "Unrecognized operating system."
#endif

#include "command.h"
#include "colors.h"
#include "ted.h"
#include "time.c"
#include "string32.c"
#include "arr.c"
#include "buffer.c"
#include "ted.c"
#include "ui.c"
#include "node.c"
#include "menu.c"
#include "command.c"
#include "config.c"

static void die(char const *fmt, ...) {
	char buf[256] = {0};
	
	va_list args;
	va_start(args, fmt);
	vsnprintf(buf, sizeof buf - 1, fmt, args);
	va_end(args);

	// show a message box, and if that fails, print it
	if (SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", buf, NULL) < 0) {
		debug_println("%s\n", buf);
	}

	exit(EXIT_FAILURE);
}

static Rect error_box_rect(Ted *ted) {
	Font *font = ted->font;
	Settings const *settings = &ted->settings;
	float padding = settings->padding;
	float window_width = ted->window_width, window_height = ted->window_height;
	float char_height = text_font_char_height(font);
	return rect_centered(V2(window_width * 0.5f, window_height * 0.9f),
			V2(menu_get_width(ted), 3 * char_height + 2 * padding));
}

#if _WIN32
INT WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    PSTR lpCmdLine, INT nCmdShow) {
	(void)hInstance; (void)hPrevInstance; (void)lpCmdLine; (void)nCmdShow;
	int argc = 0;
    LPWSTR* wide_argv = CommandLineToArgvW(GetCommandLineW(), &argc);
    char** argv = malloc(argc * sizeof *argv);
	if (!argv) {
		die("Out of memory.");
	}
    for (int i = 0; i < argc; i++) {
        LPWSTR wide_arg = wide_argv[i];
        int len = (int)wcslen(wide_arg);
        argv[i] = malloc(len + 1);
		if (!argv[i]) die("Out of memory.");
        for (int j = 0; j <= len; j++)
            argv[i][j] = (char)wide_arg[j];
    }
    LocalFree(wide_argv);
#else
int main(int argc, char **argv) {
#endif
	setlocale(LC_ALL, ""); // allow unicode

	// read command-line arguments
	char const *starting_filename = NULL;
	switch (argc) {
	case 0: case 1: break;
	case 2:
		starting_filename = argv[1];
		break;
	default:	
		fprintf(stderr, "Usage: %s [filename]\n", argv[0]);
		return EXIT_FAILURE;
	}

	SDL_SetHint(SDL_HINT_NO_SIGNAL_HANDLERS, "1"); // if this program is sent a SIGTERM/SIGINT, don't turn it into a quit event
	if (SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER) < 0)
		die("%s", SDL_GetError());

	Ted *ted = calloc(1, sizeof *ted);
	if (!ted) {
		die("Not enough memory available to run ted.");
	}

	{ // get local data directory
#if _WIN32
		wchar_t *appdata = NULL;
		KNOWNFOLDERID id = FOLDERID_LocalAppData;
		if (SHGetKnownFolderPath(&id, 0, NULL, &appdata) == S_OK) {
			strbuf_printf(ted->local_data_dir, "%ls" PATH_SEPARATOR_STR "ted", appdata);
			CoTaskMemFree(appdata);
		}
		id = FOLDERID_Profile;
		wchar_t *home = NULL;
		if (SHGetKnownFolderPath(&id, 0, NULL, &home) == S_OK) {
			strbuf_printf(ted->home, "%ls", home);
			CoTaskMemFree(home);
		}
		strbuf_printf(ted->global_data_dir, "C:\\Program Files\\ted");
#else
		char *home = getenv("HOME");
		strbuf_printf(ted->home, "%s", home);
		strbuf_printf(ted->local_data_dir, "%s/.local/share/ted", home);
		strbuf_printf(ted->global_data_dir, "/usr/share/ted");
#endif

	}

	FILE *log = NULL;
	{
		// open log file
		char log_filename[TED_PATH_MAX];
		strbuf_printf(log_filename, "%s/log.txt", ted->local_data_dir);
		log = fopen(log_filename, "w");
	}

	{ // get current working directory
		fs_get_cwd(ted->cwd, sizeof ted->cwd);
	}

	{ // check if this is the installed version of ted (as opposed to just running it from the directory with the source)
		char executable_path[TED_PATH_MAX] = {0};
		char const *cwd = ted->cwd;
	#if _WIN32
		if (GetModuleFileNameA(NULL, executable_path, sizeof executable_path) > 0) {
			char *last_backslash = strrchr(executable_path, '\\');
			if (last_backslash) {
				*last_backslash = '\0';
				ted->search_cwd = streq(cwd, executable_path);
			}
		}
	#else
		ssize_t len = readlink("/proc/self/exe", executable_path, sizeof executable_path - 1);
		if (len == -1) {
			// some posix systems don't have /proc/self/exe. oh well.
		} else {
			executable_path[len] = '\0';
			char *last_slash = strrchr(executable_path, '/');
			if (last_slash) {
				*last_slash = '\0';
				ted->search_cwd = streq(cwd, executable_path);
			}
		}
	#endif
	}
	#if TED_FORCE_SEARCH_CWD
	// override whether or not we are in the executable's directory
	// (for testing on Unix systems without /proc)
	ted->search_cwd = true;
	#endif

	Settings *settings = &ted->settings;

	{
		// read global configuration file first to establish defaults
		char global_config_filename[TED_PATH_MAX];
		strbuf_printf(global_config_filename, "%s" PATH_SEPARATOR_STR "ted.cfg", ted->global_data_dir);
		if (fs_file_exists(global_config_filename))
			config_read(ted, global_config_filename);
	}
	{
		// read local configuration file
		char config_filename[TED_PATH_MAX];
		if (ted_get_file(ted, "ted.cfg", config_filename, sizeof config_filename))
			config_read(ted, config_filename);
		else
			ted_seterr(ted, "Couldn't find config file (ted.cfg), not even the backup one that should have come with ted.");
	}
	if (ted_haserr(ted)) {
		die("Error reading config: %s", ted_geterr(ted));
	}

	SDL_Window *window = SDL_CreateWindow("ted", SDL_WINDOWPOS_UNDEFINED, 
		SDL_WINDOWPOS_UNDEFINED, 1280, 720, SDL_WINDOW_SHOWN|SDL_WINDOW_OPENGL|SDL_WINDOW_RESIZABLE);
	if (!window)
		die("%s", SDL_GetError());
		
	{ // set icon
		char icon_filename[TED_PATH_MAX];
		if (ted_get_file(ted, "assets/icon.bmp", icon_filename, sizeof icon_filename)) {
			SDL_Surface *icon = SDL_LoadBMP(icon_filename);
			SDL_SetWindowIcon(window, icon);
			SDL_FreeSurface(icon);
		} // if we can't find the icon file, it's no big deal
	}

	SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
	SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
	SDL_GLContext *glctx = SDL_GL_CreateContext(window);
	if (!glctx) {
		die("%s", SDL_GetError());
	}

	SDL_GL_SetSwapInterval(1); // vsync

	ted_load_fonts(ted);
	if (ted_haserr(ted))
		die("Error loading font: %s", ted_geterr(ted));
	{
		TextBuffer *lbuffer = &ted->line_buffer;
		line_buffer_create(lbuffer, ted);
		if (buffer_haserr(lbuffer))
			die("Error creating line buffer: %s", buffer_geterr(lbuffer));
	}
	
	
	{
		u16 buffer_index = (u16)ted_new_buffer(ted);
		assert(buffer_index == 0);
		TextBuffer *buffer = &ted->buffers[buffer_index];
		buffer_create(buffer, ted);
		ted->active_buffer = buffer;
		u16 node_index = (u16)ted_new_node(ted);
		assert(node_index == 0);
		Node *node = ted->active_node = &ted->nodes[node_index];
		ted->root = node;
		node->tabs = NULL;
		arr_add(node->tabs, 0);


		char path[TED_PATH_MAX];
		if (starting_filename) {
			// get full path to file
			if (!path_is_absolute(starting_filename))
				strbuf_printf(path, "%s%s%s", ted->cwd, ted->cwd[strlen(ted->cwd) - 1] == PATH_SEPARATOR ? "" : PATH_SEPARATOR_STR,
					starting_filename);
			else
				strbuf_printf(path, "%s", starting_filename);
		} else {
			strbuf_printf(path, "Untitled");
		}

		if (starting_filename && fs_file_exists(path)) {
			if (!buffer_load_file(buffer, path))
				ted_seterr(ted, "Couldn't load file: %s", buffer_geterr(buffer));
		} else {
			buffer_new_file(buffer, path);
			if (buffer_haserr(buffer))
				ted_seterr(ted, "Couldn't create file: %s", buffer_geterr(buffer));
		}
	}


	u32 *colors = settings->colors; (void)colors;

	Uint32 time_at_last_frame = SDL_GetTicks();

	while (!ted->quit) {
	#if DEBUG
		//printf("\033[H\033[2J");
	#endif


		SDL_Event event;
		Uint8 const *keyboard_state = SDL_GetKeyboardState(NULL);
		
		{ // get mouse position
			int mouse_x = 0, mouse_y = 0;
			SDL_GetMouseState(&mouse_x, &mouse_y);
			ted->mouse_pos = V2((float)mouse_x, (float)mouse_y);
		}
		bool ctrl_down = keyboard_state[SDL_SCANCODE_LCTRL] || keyboard_state[SDL_SCANCODE_RCTRL];
		bool shift_down = keyboard_state[SDL_SCANCODE_LSHIFT] || keyboard_state[SDL_SCANCODE_RSHIFT];
		bool alt_down = keyboard_state[SDL_SCANCODE_LALT] || keyboard_state[SDL_SCANCODE_RALT];

		memset(ted->nmouse_clicks, 0, sizeof ted->nmouse_clicks);
		ted->scroll_total_x = ted->scroll_total_y = 0;

		//printf("%p\n",(void *)ted->drag_buffer);

		float window_width = 0, window_height = 0;
		{
			int window_width_int = 0, window_height_int = 0;
			SDL_GetWindowSize(window, &window_width_int, &window_height_int);
			ted->window_width = (float)window_width_int;
			ted->window_height = (float)window_height_int;
		}
		window_width = ted->window_width, window_height = ted->window_height;

		while (SDL_PollEvent(&event)) {

			TextBuffer *buffer = ted->active_buffer;
			u32 key_modifier = (u32)ctrl_down << KEY_MODIFIER_CTRL_BIT
				| (u32)shift_down << KEY_MODIFIER_SHIFT_BIT
				| (u32)alt_down << KEY_MODIFIER_ALT_BIT;
			switch (event.type) {
			case SDL_QUIT:
				command_execute(ted, CMD_QUIT, 1);
				break;
			case SDL_MOUSEWHEEL: {
				// scroll with mouse wheel
				Sint32 dx = event.wheel.x, dy = -event.wheel.y;
				ted->scroll_total_x += dx;
				ted->scroll_total_y += dy;
				double scroll_speed = 2.5;
				if (ted->active_buffer)
					buffer_scroll(ted->active_buffer, dx * scroll_speed, dy * scroll_speed);
			} break;
			case SDL_MOUSEBUTTONDOWN: {
				Uint32 button = event.button.button;
				
				if (button == SDL_BUTTON_LEFT) {
					// shift+left click = right click
					if (shift_down) button = SDL_BUTTON_RIGHT;
					// ctrl+left click = middle click
					if (ctrl_down)  button = SDL_BUTTON_MIDDLE;
				}


				float x = (float)event.button.x, y = (float)event.button.y;
				if (button < arr_count(ted->nmouse_clicks) 
					&& ted->nmouse_clicks[button] < arr_count(ted->mouse_clicks[button])) {
					v2 pos = V2(x, y);
					bool add = true;
					if (*ted->error_shown) {
						if (rect_contains_point(error_box_rect(ted), pos)) {
							// clicked on error
							if (button == SDL_BUTTON_LEFT) {
								// dismiss error
								*ted->error_shown = '\0';
							}
							// don't let anyone else use this event
							add = false;
						}
					}
					if (add)
						ted->mouse_clicks[button][ted->nmouse_clicks[button]++] = pos;
				}
				switch (button) {
				case SDL_BUTTON_LEFT: {
					if (buffer) {
						BufferPos pos = {0};
						if (buffer_pixels_to_pos(buffer, V2(x, y), &pos)) {
							if (key_modifier == KEY_MODIFIER_SHIFT) {
								buffer_select_to_pos(buffer, pos);
							} else if (key_modifier == 0) {
								buffer_cursor_move_to_pos(buffer, pos);
								switch ((event.button.clicks - 1) % 3) {
								case 0: break; // single-click
								case 1: // double-click: select word
									buffer_select_word(buffer);
									break;
								case 2: // triple-click: select line
									buffer_select_line(buffer);
									break;
								}
							}
							ted->drag_buffer = buffer;
						}
					}
				} break;
				}
			} break;
			case SDL_MOUSEMOTION: {
				float x = (float)event.motion.x, y = (float)event.motion.y;
				if (ted->drag_buffer != ted->active_buffer)
					ted->drag_buffer = NULL;
				if (ted->drag_buffer) {
					BufferPos pos = {0};
					// drag to select
					// we don't check the return value here, because it's okay to drag off the screen.
					buffer_pixels_to_pos(ted->drag_buffer, V2(x, y), &pos);
					buffer_select_to_pos(ted->drag_buffer, pos);
				}
			} break;
			case SDL_KEYDOWN: {
				SDL_Scancode scancode = event.key.keysym.scancode;
				SDL_Keymod modifier = event.key.keysym.mod;
				u32 key_combo = (u32)scancode << 3 |
					(u32)((modifier & (KMOD_LCTRL|KMOD_RCTRL)) != 0) << KEY_MODIFIER_CTRL_BIT |
					(u32)((modifier & (KMOD_LSHIFT|KMOD_RSHIFT)) != 0) << KEY_MODIFIER_SHIFT_BIT |
					(u32)((modifier & (KMOD_LALT|KMOD_RALT)) != 0) << KEY_MODIFIER_ALT_BIT;
				if (key_combo < KEY_COMBO_COUNT) {
					KeyAction *action = &ted->key_actions[key_combo];
					if (action->command) {
						command_execute(ted, action->command, action->argument);
					}
				}
			} break;
			case SDL_TEXTINPUT: {
				char *text = event.text.text;
				if (buffer
					&& (key_modifier & ~KEY_MODIFIER_SHIFT) == 0) // unfortunately, some key combinations like ctrl+minus still register as a "-" text input event
					buffer_insert_utf8_at_cursor(buffer, text);
			} break;
			}
		}
		
		if (!(SDL_GetMouseState(NULL, NULL) & SDL_BUTTON_LMASK)) {
			// originally this was done on SDL_MOUSEBUTTONUP events but for some reason
			// I was getting a bunch of those even when I was holding down the mouse.
			// This makes it much smoother.
			ted->drag_buffer = NULL;
		}


		u32 key_modifier = (u32)ctrl_down << KEY_MODIFIER_CTRL_BIT
			| (u32)shift_down << KEY_MODIFIER_SHIFT_BIT
			| (u32)alt_down << KEY_MODIFIER_ALT_BIT;

		double frame_dt;
		{
			Uint32 time_this_frame = SDL_GetTicks();
			frame_dt = 0.001 * (time_this_frame - time_at_last_frame);
			time_at_last_frame = time_this_frame;
		}

		TextBuffer *active_buffer = ted->active_buffer;
		if (active_buffer && key_modifier == KEY_MODIFIER_ALT) {
			// alt + arrow keys to scroll
			double scroll_speed = 20.0;
			double scroll_amount_x = scroll_speed * frame_dt * 1.5; // characters are taller than they are wide
			double scroll_amount_y = scroll_speed * frame_dt;
			if (keyboard_state[SDL_SCANCODE_UP])
				buffer_scroll(active_buffer, 0, -scroll_amount_y);
			if (keyboard_state[SDL_SCANCODE_DOWN])
				buffer_scroll(active_buffer, 0, +scroll_amount_y);
			if (keyboard_state[SDL_SCANCODE_LEFT])
				buffer_scroll(active_buffer, -scroll_amount_x, 0);
			if (keyboard_state[SDL_SCANCODE_RIGHT])
				buffer_scroll(active_buffer, +scroll_amount_x, 0);
		}
		
		// update window dimensions
		{
			int window_width_int = 0, window_height_int = 0;
			SDL_GetWindowSize(window, &window_width_int, &window_height_int);
			ted->window_width = (float)window_width_int;
			ted->window_height = (float)window_height_int;
		}
		window_width = ted->window_width, window_height = ted->window_height;


		// set up GL
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
		glViewport(0, 0, (GLsizei)window_width, (GLsizei)window_height);
		glMatrixMode(GL_MODELVIEW);
		glLoadIdentity();
		// pixel coordinates; down is positive y
		glOrtho(0, window_width, window_height, 0, -1, +1);
		{ // clear (background)
			float bg_color[4];
			rgba_u32_to_floats(settings->colors[COLOR_BG], bg_color);
			glClearColor(bg_color[0], bg_color[1], bg_color[2], bg_color[3]);
		}
		glClear(GL_COLOR_BUFFER_BIT);
		
		Font *font = ted->font;

		if (ted->active_node) {
			float x1 = 25, y1 = 25, x2 = window_width-25, y2 = window_height-25;
			Node *node = ted->root;
			node_frame(ted, node, rect4(x1, y1, x2, y2));
		} else {
			gl_color_rgba(colors[COLOR_TEXT_SECONDARY]);
			text_render_anchored(font, "Press Ctrl+O to open a file or Ctrl+N to create a new one.",
				window_width * 0.5f, window_height * 0.5f, ANCHOR_MIDDLE);
		}

		Menu menu = ted->menu;
		if (menu) {
			menu_frame(ted, menu);
		}


		if (text_has_err()) {
			ted_seterr(ted, "Couldn't render text: %s", text_get_err());
		}
		for (u16 i = 0; i < TED_MAX_BUFFERS; ++i) {
			TextBuffer *buffer = &ted->buffers[i];
			if (buffer_haserr(buffer)) {
				ted_seterr_to_buferr(ted, buffer);
				buffer_clearerr(buffer);
			}
		}

		// check if there's a new error
		if (ted_haserr(ted)) {
			ted->error_time = time_get_seconds();
			str_cpy(ted->error_shown, sizeof ted->error_shown, ted->error);

			{ // output error to log file
				char tstr[256];
				time_t t = time(NULL);
				struct tm *tm = localtime(&t);
				strftime(tstr, sizeof tstr, "%Y-%m-%d %H:%M:%S", tm);
				if (log) {
					fprintf(log, "[ERROR %s] %s\n", tstr, ted->error);
					fflush(log);
				}
			}
			ted_clearerr(ted);
		}

		// error box
		if (*ted->error_shown) {
			double t = time_get_seconds();
			double time_passed = t - ted->error_time;
			if (time_passed > settings->error_display_time) {
				// stop showing error
				*ted->error_shown = '\0';
			} else {
				Rect r = error_box_rect(ted);
				float padding = settings->padding;

				glBegin(GL_QUADS);
				gl_color_rgba(colors[COLOR_ERROR_BG]);
				rect_render(r);
				gl_color_rgba(colors[COLOR_ERROR_BORDER]);
				rect_render_border(r, settings->border_thickness);
				glEnd();
				gl_color_rgba(colors[COLOR_ERROR_TEXT]);


				float text_x1 = rect_x1(r) + padding, text_x2 = rect_x2(r) - padding;
				float text_y1 = rect_y1(r) + padding;

				// (make sure text wraps)
				TextRenderState text_state = text_render_state_default;
				text_state.min_x = text_x1;
				text_state.max_x = text_x2;
				text_state.wrap = true;
				text_render_with_state(font, &text_state, ted->error_shown, text_x1, text_y1);
			}
		}


	#if DEBUG
		for (u16 i = 0; i < TED_MAX_BUFFERS; ++i)
			if (ted->buffers_used[i])
				buffer_check_valid(&ted->buffers[i]);
		buffer_check_valid(&ted->line_buffer);
	#endif

		SDL_GL_SwapWindow(window);
	}


	if (ted->menu)
		menu_close(ted); // free any memory used by the current menu

	if (log) fclose(log);

	SDL_GL_DeleteContext(glctx);
	SDL_DestroyWindow(window);
	SDL_Quit();
	for (u16 i = 0; i < TED_MAX_BUFFERS; ++i)
		if (ted->buffers_used[i])
			buffer_free(&ted->buffers[i]);
	for (u16 i = 0; i < TED_MAX_NODES; ++i)
		if (ted->nodes_used[i])
			node_free(&ted->nodes[i]);
	buffer_free(&ted->line_buffer);
	text_font_free(ted->font);
	text_font_free(ted->font_bold);
	free(ted);
#if _WIN32
	for (int i = 0; i < argc; ++i)
		free(argv[i]);
	free(argv);
#endif

	return 0;
}