/*
 * Copyright (c) 2013, 2014, 2016, 2024 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.
 *
 */

#if defined(__sortix__)
#include <sys/keycodes.h>
#endif

#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <signal.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <wchar.h>

#include "command.h"
#include "editor.h"
#include "input.h"
#include "modal.h"

struct terminal_sequence
{
	const char* sequence;
	int kbkey;
	bool control;
	bool shift;
};

// TODO: The terminal doesn't deliver shift-pageup and shift-pagedown events.
// TODO: The terminal doesn't deliver shift-home and shift-end events.

struct terminal_sequence terminal_sequences[] =
{
	{ "\e[1;2A", KBKEY_UP, false, true },
	{ "\e[1;2B", KBKEY_DOWN, false, true },
	{ "\e[1;2C", KBKEY_RIGHT, false, true },
	{ "\e[1;2D", KBKEY_LEFT, false, true },
	{ "\e[1;2F", KBKEY_END, false, true },
	{ "\e[1;2H", KBKEY_HOME, false, true },
	{ "\e[1;2~", KBKEY_HOME, false, true },
	{ "\e[1;5A", KBKEY_UP, true, false },
	{ "\e[1;5B", KBKEY_DOWN, true, false },
	{ "\e[1;5C", KBKEY_RIGHT, true, false },
	{ "\e[1;5D", KBKEY_LEFT, true, false },
	{ "\e[1;5F", KBKEY_END, true, false },
	{ "\e[1;5H", KBKEY_HOME, true, false },
	{ "\e[1;5~", KBKEY_HOME, true, false },
	{ "\e[1;6A", KBKEY_UP, true, true },
	{ "\e[1;6B", KBKEY_DOWN, true, true },
	{ "\e[1;6C", KBKEY_RIGHT, true, true },
	{ "\e[1;6D", KBKEY_LEFT, true, true },
	{ "\e[1;6F", KBKEY_END, true, true },
	{ "\e[1;6H", KBKEY_HOME, true, true },
	{ "\e[1;6~", KBKEY_HOME, true, true },
	{ "\e[1~", KBKEY_HOME, false, false },
	{ "\e[3;2~", KBKEY_DELETE, false, true },
	{ "\e[3;5~", KBKEY_DELETE, true, false },
	{ "\e[3;6~", KBKEY_DELETE, true, true },
	{ "\e[3~", KBKEY_DELETE, false, false },
	{ "\e[4;2~", KBKEY_END, false, true },
	{ "\e[4;5~", KBKEY_END, true, false },
	{ "\e[4;6~", KBKEY_END, true, true },
	{ "\e[4~", KBKEY_END, false, false },
	{ "\e[5;2~", KBKEY_PGUP, false, true },
	{ "\e[5;5~", KBKEY_PGUP, true, false },
	{ "\e[5;6~", KBKEY_PGUP, true, true },
	{ "\e[5~", KBKEY_PGUP, false, false },
	{ "\e[6;2~", KBKEY_PGDOWN, false, true },
	{ "\e[6;5~", KBKEY_PGDOWN, true, false },
	{ "\e[6;6~", KBKEY_PGDOWN, true, true },
	{ "\e[6~", KBKEY_PGDOWN, false, false },
	{ "\e[A", KBKEY_UP, false, false },
	{ "\e[B", KBKEY_DOWN, false, false },
	{ "\e[C", KBKEY_RIGHT, false, false },
	{ "\e[D", KBKEY_LEFT, false, false },
	{ "\e[F", KBKEY_END, false, false },
	{ "\e[H", KBKEY_HOME, false, false },
	{ "\e:", KBKEY_ESC, false, false },
	{ "\eOF", KBKEY_END, false, false },
	{ "\eOH", KBKEY_HOME, false, false },
	{ "\x7F", KBKEY_BKSPC, false, false },
};

void editor_codepoint(struct editor* editor, uint32_t codepoint)
{
	wchar_t c = (wchar_t) codepoint;

	if ( c == L'\b' || c == 127 /* delete */ )
		return;

	if ( editor->mode == MODE_EDIT )
		editor_type_character(editor, c);
	else
		editor_modal_character(editor, c);
}

void editor_type_kbkey(struct editor* editor, int kbkey)
{
	if ( kbkey < 0 )
		return;

	if ( kbkey == KBKEY_ESC )
	{
		editor_type_command(editor);
		return;
	}

	if ( editor->control && editor->shift )
	{
		switch ( kbkey )
		{
		case KBKEY_LEFT: editor_type_control_select_left(editor); break;
		case KBKEY_RIGHT: editor_type_control_select_right(editor); break;
		case KBKEY_UP: editor_type_control_select_up(editor); break;
		case KBKEY_DOWN: editor_type_control_select_down(editor); break;
		}
	}
	else if ( editor->control && !editor->shift )
	{
		switch ( kbkey )
		{
		case KBKEY_LEFT: editor_type_control_left(editor); break;
		case KBKEY_RIGHT: editor_type_control_right(editor); break;
		case KBKEY_UP: editor_type_control_up(editor); break;
		case KBKEY_DOWN: editor_type_control_down(editor); break;
		}
	}
	else if ( !editor->control && editor->shift )
	{
		switch ( kbkey )
		{
		case KBKEY_LEFT: editor_type_select_left(editor); break;
		case KBKEY_RIGHT: editor_type_select_right(editor); break;
		case KBKEY_UP: editor_type_select_up(editor); break;
		case KBKEY_DOWN: editor_type_select_down(editor); break;
		case KBKEY_HOME: editor_type_select_home(editor); break;
		case KBKEY_END: editor_type_select_end(editor); break;
		case KBKEY_PGUP: editor_type_select_page_up(editor); break;
		case KBKEY_PGDOWN: editor_type_select_page_down(editor); break;
		case KBKEY_BKSPC: editor_type_backspace(editor); break;
		case KBKEY_DELETE: editor_type_delete(editor); break;
		}
	}
	else if ( !editor->control && !editor->shift )
	{
		switch ( kbkey )
		{
		case KBKEY_LEFT: editor_type_left(editor); break;
		case KBKEY_RIGHT: editor_type_right(editor); break;
		case KBKEY_UP: editor_type_up(editor); break;
		case KBKEY_DOWN: editor_type_down(editor); break;
		case KBKEY_HOME: editor_type_home(editor); break;
		case KBKEY_END: editor_type_end(editor); break;
		case KBKEY_PGUP: editor_type_page_up(editor); break;
		case KBKEY_PGDOWN: editor_type_page_down(editor); break;
		case KBKEY_BKSPC: editor_type_backspace(editor); break;
		case KBKEY_DELETE: editor_type_delete(editor); break;
		}
	}
}

void editor_modal_kbkey(struct editor* editor, int kbkey)
{
	if ( editor->control )
		return;

	if ( kbkey < 0 )
		return;

	switch ( kbkey )
	{
	case KBKEY_LEFT: editor_modal_left(editor); break;
	case KBKEY_RIGHT: editor_modal_right(editor); break;
	case KBKEY_HOME: editor_modal_home(editor); break;
	case KBKEY_END: editor_modal_end(editor); break;
	case KBKEY_BKSPC: editor_modal_backspace(editor); break;
	case KBKEY_DELETE: editor_modal_delete(editor); break;
	case KBKEY_ESC: editor_type_edit(editor); break;
	}
}

void editor_kbkey(struct editor* editor, int kbkey)
{
	if ( editor->mode == MODE_EDIT )
		editor_type_kbkey(editor, kbkey);
	else
		editor_modal_kbkey(editor, kbkey);
}

void editor_emulate_kbkey(struct editor* editor,
                          int kbkey,
                          bool control,
                          bool shift)
{
	editor->control = control;
	editor->lshift = shift;
	editor->rshift = false;
	editor->shift = shift;

	editor_kbkey(editor, kbkey);
	editor_kbkey(editor, -kbkey);

	editor->control = false;
	editor->lshift = false;
	editor->rshift = false;
	editor->shift = false;
}

void editor_emulate_control_letter(struct editor* editor, uint32_t c)
{
#if !defined(__sortix__)
	if ( c == 'Z' )
	{
		raise(SIGSTOP);
	}
#endif

	editor->control = true;
	editor_codepoint(editor, c);
	editor->control = false;
}

void editor_input_begin(struct editor_input* editor_input)
{
	memset(editor_input, 0, sizeof(*editor_input));

	tcgetattr(0, &editor_input->saved_termios);
	struct termios tcattr;
	memcpy(&tcattr, &editor_input->saved_termios, sizeof(struct termios));
	tcattr.c_lflag &= ~(ECHO | ICANON | ISIG | IEXTEN);
	tcattr.c_iflag |= ICRNL;
	tcattr.c_cc[VMIN] = 1;
	tcattr.c_cc[VTIME] = 0;
	tcsetattr(0, TCSADRAIN, &tcattr);
	if ( getenv("TERM") && strcmp(getenv("TERM"), "sortix") != 0 )
	{
		printf("\e[?1049h");
		fflush(stdout);
	}
}

void editor_input_process_byte(struct editor_input* editor_input,
                               struct editor* editor)
{
	unsigned char uc;
	ssize_t amount_read = read(0, &uc, sizeof(uc));

	if ( amount_read != sizeof(uc) )
		return;

	// TODO: Work around https://gcc.gnu.org/bugzilla/show_bug.cgi?id=110091
	//       false positive warning on gcc 14.2.0 -O2.
	#if defined(__GNUC__) && 12 <= __GNUC__
	#pragma GCC diagnostic ignored "-Wdangling-pointer"
	#endif
	if ( editor_input->termseq_used < MAX_TERMSEQ_SIZE )
		editor_input->termseq[editor_input->termseq_used++] = (char) uc;

	size_t num_seqs = sizeof(terminal_sequences) / sizeof(terminal_sequences[0]);

	while ( editor_input->termseq_seen < editor_input->termseq_used )
	{
		size_t match = 0;
		size_t match_size = 0;
		bool full_match = false;
		bool partial_match = false;

		for ( size_t i = 0; i < num_seqs; i++ )
		{
			struct terminal_sequence* terminal_sequence = &terminal_sequences[i];
			const char* sequence = terminal_sequence->sequence;

			bool potential_partial_match = false;
			for ( size_t n = 0; n < editor_input->termseq_used; n++ )
			{
				if ( sequence[n] != editor_input->termseq[n] )
				{
					potential_partial_match = false;
					break;
				}

				if ( sequence[n+1] == '\0' )
				{
					potential_partial_match = false;
					full_match = true;
					match = i;
					match_size = n + 1;
					break;
				}

				potential_partial_match = true;
			}

			if ( potential_partial_match )
				partial_match = true;
		}

		if ( full_match )
		{
			editor_emulate_kbkey(editor,
			                     terminal_sequences[match].kbkey,
			                     terminal_sequences[match].control,
			                     terminal_sequences[match].shift);
			memmove(editor_input->termseq,
			        editor_input->termseq + match_size,
			        editor_input->termseq_used - match_size);
			editor_input->termseq_used -= match_size;
			editor_input->termseq_seen = 0;
			continue;
		}

		if ( partial_match )
		{
			editor_input->termseq_seen = editor_input->termseq_used;
			continue;
		}

		char input = editor_input->termseq[0];

		if ( 1 <= input && input <= 26 && input != '\t' && input != '\n' )
		{
			editor_emulate_control_letter(editor, L'A' + input - 1);
		}
		else
		{
			wchar_t wc;
			size_t amount = mbrtowc(&wc, &input, 1, &editor_input->ps);
			if ( amount == (size_t) -1 )
				memset(&editor_input->ps, 0, sizeof(editor_input->ps));
			if ( amount == (size_t)  1 )
				editor_codepoint(editor, (uint32_t) wc);
		}

		memmove(editor_input->termseq,
		        editor_input->termseq + 1,
		        editor_input->termseq_used - 1);
		editor_input->termseq_used--;
		editor_input->termseq_seen = 0;
	}
}

extern volatile sig_atomic_t had_sigwinch;

void editor_input_process(struct editor_input* editor_input,
                          struct editor* editor)
{
	sigset_t sigset;
	sigemptyset(&sigset);
	struct pollfd pfd = { .fd = 0, .events = POLLIN };
	struct timespec timeout = { .tv_sec = 0 };
	struct timespec* timeout_ptr = NULL;
	while ( !had_sigwinch && ppoll(&pfd, 1, timeout_ptr, &sigset) == 1 )
	{
		editor_input_process_byte(editor_input, editor);
		timeout_ptr = &timeout;
	}
	had_sigwinch = 0;
}

void editor_input_end(struct editor_input* editor_input)
{
	if ( getenv("TERM") && strcmp(getenv("TERM"), "sortix") != 0 )
	{
		printf("\e[?1049l");
		fflush(stdout);
	}
	tcsetattr(0, TCSADRAIN, &editor_input->saved_termios);
}

void editor_input_suspend(struct editor_input* editor_input)
{
	(void) editor_input;

#if !defined(__sortix__)
	struct termios current_termios;

	if ( getenv("TERM") && strcmp(getenv("TERM"), "sortix") != 0 )
	{
		printf("\e[?1049l");
		fflush(stdout);
	}

	tcgetattr(0, &current_termios);

	raise(SIGSTOP);

	tcsetattr(0, TCSADRAIN, &current_termios);

	if ( getenv("TERM") && strcmp(getenv("TERM"), "sortix") != 0 )
	{
		printf("\e[?1049h");
		fflush(stdout);
	}
#endif
}
