#include <cstdio>
#include <algorithm>
#include <ncurses.h>

#include <SDL.h>
#include <imgui.h>

#include "ImGuiInputHandler.h"
#include "KKeymap.h"
#include "Editor.h"
#include "PasteSplit.h"


static bool
map_key(const SDL_Keycode key,
        const SDL_Keymod mod,
        bool &k_prefix,
        bool &esc_meta,
        bool &k_ctrl_pending,
        Editor *ed,
        MappedInput &out,
        bool &suppress_textinput_once)
{
	// Ctrl handling
	const bool is_ctrl = (mod & KMOD_CTRL) != 0;
	const bool is_alt  = (mod & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0;

	// If previous key was ESC, interpret this as Meta via ESC keymap.
	// Prefer KEYDOWN when we can derive a printable ASCII; otherwise defer to TEXTINPUT.
	if (esc_meta) {
		int ascii_key = 0;
		if (key == SDLK_BACKSPACE) {
			// ESC BACKSPACE: map to DeleteWordPrev using ncurses KEY_BACKSPACE constant
			ascii_key = KEY_BACKSPACE;
		} else if (key >= SDLK_a && key <= SDLK_z) {
			ascii_key = static_cast<int>('a' + (key - SDLK_a));
		} else if (key == SDLK_COMMA) {
			ascii_key = '<';
		} else if (key == SDLK_PERIOD) {
			ascii_key = '>';
		} else if (key == SDLK_LESS) {
			ascii_key = '<';
		} else if (key == SDLK_GREATER) {
			ascii_key = '>';
		}
		if (ascii_key != 0) {
			esc_meta  = false; // consume if we can decide on KEYDOWN
			ascii_key = KLowerAscii(ascii_key);
			CommandId id;
			if (KLookupEscCommand(ascii_key, id)) {
				out = {true, id, "", 0};
				return true;
			}
			// Known printable but unmapped ESC sequence: report invalid
			out = {true, CommandId::UnknownEscCommand, "", 0};
			return true;
		}
		// No usable ASCII from KEYDOWN → keep esc_meta set and let TEXTINPUT handle it
		out.hasCommand = false;
		return true;
	}

	// Movement and basic keys
	// These keys exit k-prefix mode if active (user pressed C-k then a special key).
	switch (key) {
	case SDLK_LEFT:
		k_prefix = false;
		k_ctrl_pending = false;
		out            = {true, CommandId::MoveLeft, "", 0};
		return true;
	case SDLK_RIGHT:
		k_prefix = false;
		k_ctrl_pending = false;
		out            = {true, CommandId::MoveRight, "", 0};
		return true;
	case SDLK_UP:
		k_prefix = false;
		k_ctrl_pending = false;
		out            = {true, CommandId::MoveUp, "", 0};
		return true;
	case SDLK_DOWN:
		k_prefix = false;
		k_ctrl_pending = false;
		out            = {true, CommandId::MoveDown, "", 0};
		return true;
	case SDLK_HOME:
		k_prefix = false;
		k_ctrl_pending = false;
		out            = {true, CommandId::MoveHome, "", 0};
		return true;
	case SDLK_END:
		k_prefix = false;
		k_ctrl_pending = false;
		out            = {true, CommandId::MoveEnd, "", 0};
		return true;
	case SDLK_PAGEUP:
		k_prefix = false;
		k_ctrl_pending = false;
		out            = {true, CommandId::PageUp, "", 0};
		return true;
	case SDLK_PAGEDOWN:
		k_prefix = false;
		k_ctrl_pending = false;
		out            = {true, CommandId::PageDown, "", 0};
		return true;
	case SDLK_DELETE:
		k_prefix = false;
		k_ctrl_pending = false;
		out            = {true, CommandId::DeleteChar, "", 0};
		return true;
	case SDLK_BACKSPACE:
		k_prefix = false;
		k_ctrl_pending = false;
		out            = {true, CommandId::Backspace, "", 0};
		return true;
	case SDLK_TAB:
		// Insert a literal tab character when not interpreting a k-prefix suffix.
		// If k-prefix is active, let the k-prefix handler below consume the key
		// (so Tab doesn't leave k-prefix stuck).
		if (!k_prefix) {
			out = {true, CommandId::InsertText, std::string("\t"), 0};
			return true;
		}
		break; // fall through so k-prefix handler can process
	case SDLK_RETURN:
	case SDLK_KP_ENTER:
		k_prefix = false;
		k_ctrl_pending = false;
		if (mod & KMOD_SHIFT) {
			out = {true, CommandId::SmartNewline, "", 0};
		} else {
			out = {true, CommandId::Newline, "", 0};
		}
		return true;
	case SDLK_ESCAPE:
		k_prefix = false;
		k_ctrl_pending = false;
		esc_meta       = true; // next key will be treated as Meta
		out.hasCommand = false; // no immediate command for bare ESC in GUI
		return true;
	default:
		break;
	}

	// If we are in k-prefix, interpret the very next key via the C-k keymap immediately.
	if (k_prefix) {
		esc_meta = false;
		// Normalize to ASCII; preserve case for letters using Shift
		int ascii_key = 0;
		if (key >= SDLK_a && key <= SDLK_z) {
			ascii_key = static_cast<int>('a' + (key - SDLK_a));
			if (mod & KMOD_SHIFT)
				ascii_key = ascii_key - 'a' + 'A';
		} else if (key == SDLK_COMMA) {
			ascii_key = '<';
		} else if (key == SDLK_PERIOD) {
			ascii_key = '>';
		} else if (key == SDLK_LESS) {
			ascii_key = '<';
		} else if (key == SDLK_GREATER) {
			ascii_key = '>';
		} else if (key >= SDLK_SPACE && key <= SDLK_z) {
			ascii_key = static_cast<int>(key);
		}
		bool ctrl2 = (mod & KMOD_CTRL) != 0;
		// If user typed a literal 'C' (uppercase) or '^' as a control qualifier, keep k-prefix active
		// Do NOT treat lowercase 'c' as a qualifier; 'c' is a valid k-command (BufferClose).
		if (ascii_key == 'C' || ascii_key == '^') {
			k_ctrl_pending = true;
			// Keep waiting for the next suffix; show status and suppress ensuing TEXTINPUT
			if (ed)
				ed->SetStatus("C-k C _");
			suppress_textinput_once = true;
			out.hasCommand          = false;
			return true;
		}
		// Otherwise, consume the k-prefix now for the actual suffix
		k_prefix = false;
		if (ascii_key != 0) {
			int lower                  = KLowerAscii(ascii_key);
			bool ctrl_suffix_supported = (lower == 'd' || lower == 'x' || lower == 'q');
			bool pass_ctrl             = (ctrl2 || k_ctrl_pending) && ctrl_suffix_supported;
			k_ctrl_pending             = false;
			CommandId id;
			bool mapped = KLookupKCommand(ascii_key, pass_ctrl, id);
			// Diagnostics for u/U
			if (lower == 'u') {
				char disp = (ascii_key >= 0x20 && ascii_key <= 0x7e)
					            ? static_cast<char>(ascii_key)
					            : '?';
				std::fprintf(stderr,
				             "[kge] k-prefix suffix: sym=%d mods=0x%x ascii=%d '%c' ctrl2=%d pass_ctrl=%d mapped=%d id=%d\n",
				             static_cast<int>(key), static_cast<unsigned int>(mod), ascii_key, disp,
				             ctrl2 ? 1 : 0, pass_ctrl ? 1 : 0, mapped ? 1 : 0,
				             mapped ? static_cast<int>(id) : -1);
				std::fflush(stderr);
			}
			if (mapped) {
				out = {true, id, "", 0};
				if (ed)
					ed->SetStatus(""); // clear "C-k _" hint after suffix
				return true;
			}
			int shown = KLowerAscii(ascii_key);
			char c    = (shown >= 0x20 && shown <= 0x7e) ? static_cast<char>(shown) : '?';
			std::string arg(1, c);
			out = {true, CommandId::UnknownKCommand, arg, 0};
			if (ed)
				ed->SetStatus(""); // clear hint; handler will set unknown status
			return true;
		}
		// Non-printable/unmappable key as k-suffix (e.g., F-keys): report unknown and exit k-mode
		out = {true, CommandId::UnknownKCommand, std::string("?"), 0};
		if (ed)
			ed->SetStatus("");
		return true;
	}

	if (is_ctrl) {
		// Universal argument: C-u
		if (key == SDLK_u) {
			if (ed)
				ed->UArgStart();
			out.hasCommand = false;
			return true;
		}
		// Cancel universal arg on C-g as well (it maps to Refresh via ctrl map)
		if (key == SDLK_g) {
			if (ed)
				ed->UArgClear();
			// Also cancel any pending k-prefix qualifier
			k_ctrl_pending = false;
			k_prefix       = false; // treat as cancel of prefix
		}
		if (key == SDLK_k || key == SDLK_KP_EQUALS) {
			k_prefix = true;
			out      = {true, CommandId::KPrefix, "", 0};
			return true;
		}
		// Map other control chords via shared keymap
		if (key >= SDLK_a && key <= SDLK_z) {
			int ascii_key = static_cast<int>('a' + (key - SDLK_a));
			CommandId id;
			if (KLookupCtrlCommand(ascii_key, id)) {
				out = {true, id, "", 0};
				return true;
			}
		}
	}

	// Alt/Meta bindings (ESC f/b equivalent)
	if (is_alt) {
		int ascii_key = 0;
		if (key == SDLK_BACKSPACE) {
			// Alt BACKSPACE: map to DeleteWordPrev using ncurses KEY_BACKSPACE constant
			ascii_key = KEY_BACKSPACE;
		} else if (key >= SDLK_a && key <= SDLK_z) {
			ascii_key = static_cast<int>('a' + (key - SDLK_a));
		} else if (key == SDLK_COMMA) {
			ascii_key = '<';
		} else if (key == SDLK_PERIOD) {
			ascii_key = '>';
		} else if (key == SDLK_LESS) {
			ascii_key = '<';
		} else if (key == SDLK_GREATER) {
			ascii_key = '>';
		}
		if (ascii_key != 0) {
			CommandId id;
			if (KLookupEscCommand(ascii_key, id)) {
				out = {true, id, "", 0};
				return true;
			}
		}
	}

	// If collecting universal argument, allow digits on KEYDOWN path too
	if (ed && ed->UArg() != 0) {
		if ((key >= SDLK_0 && key <= SDLK_9) && !(mod & KMOD_SHIFT)) {
			int d = static_cast<int>(key - SDLK_0);
			ed->UArgDigit(d);
			out.hasCommand = false;
			// We consumed a digit on KEYDOWN; SDL will often also emit TEXTINPUT for it.
			// Request suppression of the very next TEXTINPUT to avoid double-counting.
			suppress_textinput_once = true;
			return true;
		}
	}

	// k_prefix handled earlier

	return false;
}


bool
ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e)
{
	MappedInput mi;
	bool produced = false;
	switch (e.type) {
	case SDL_MOUSEWHEEL: {
		// High-resolution trackpads can deliver fractional wheel deltas. Accumulate
		// precise values and emit one scroll step per whole unit.
		float dy = 0.0f;
#if SDL_VERSION_ATLEAST(2, 0, 18)
		dy = e.wheel.preciseY;
#else
		dy = static_cast<float>(e.wheel.y);
#endif
#ifdef SDL_MOUSEWHEEL_FLIPPED
		if (e.wheel.direction == SDL_MOUSEWHEEL_FLIPPED)
			dy = -dy;
#endif
		if (dy != 0.0f) {
			wheel_accum_y_  += dy;
			float abs_accum = wheel_accum_y_ >= 0.0f ? wheel_accum_y_ : -wheel_accum_y_;
			int steps       = static_cast<int>(abs_accum);
			if (steps > 0) {
				CommandId id = (wheel_accum_y_ > 0.0f) ? CommandId::ScrollUp : CommandId::ScrollDown;
				std::lock_guard<std::mutex> lk(mu_);
				for (int i = 0; i < steps; ++i) {
					q_.push(MappedInput{true, id, std::string(), 0});
				}
				// remove the whole steps, keep fractional remainder
				wheel_accum_y_ += (wheel_accum_y_ > 0.0f)
					                  ? -static_cast<float>(steps)
					                  : static_cast<float>(steps);
				return true; // consumed
			}
		}
		return false;
	}
	case SDL_KEYDOWN: {
		// Remember state before mapping; used for TEXTINPUT suppression heuristics
		const bool was_k_prefix = k_prefix_;
		const bool was_esc_meta = esc_meta_;
		SDL_Keymod mods         = SDL_Keymod(e.key.keysym.mod);
		const SDL_Keycode key   = e.key.keysym.sym;

		// New window: Cmd+N (macOS) or Ctrl+Shift+N (Linux/Windows)
		{
			const bool gui_n   = (mods & KMOD_GUI) && !(mods & KMOD_CTRL) && (key == SDLK_n);
			const bool ctrl_sn = (mods & KMOD_CTRL) && (mods & KMOD_SHIFT) && (key == SDLK_n);
			if (gui_n || ctrl_sn) {
				std::lock_guard<std::mutex> lk(mu_);
				q_.push(MappedInput{true, CommandId::NewWindow, std::string(), 0});
				suppress_text_input_once_ = true;
				return true;
			}
		}

		// Font zoom: Cmd+=/Cmd+-/Cmd+0 (macOS) or Ctrl+=/Ctrl+-/Ctrl+0
		if ((mods & (KMOD_CTRL | KMOD_GUI)) && !(mods & KMOD_SHIFT)) {
			bool is_zoom = true;
			CommandId zoom_cmd = CommandId::FontZoomIn;
			if (key == SDLK_EQUALS || key == SDLK_PLUS)
				zoom_cmd = CommandId::FontZoomIn;
			else if (key == SDLK_MINUS)
				zoom_cmd = CommandId::FontZoomOut;
			else if (key == SDLK_0)
				zoom_cmd = CommandId::FontZoomReset;
			else
				is_zoom = false;
			if (is_zoom) {
				std::lock_guard<std::mutex> lk(mu_);
				q_.push(MappedInput{true, zoom_cmd, std::string(), 0});
				suppress_text_input_once_ = true;
				return true;
			}
		}

		// Handle Paste: Ctrl+V (Windows/Linux) or Cmd+V (macOS)
		// Note: SDL defines letter keycodes in lowercase only (e.g., SDLK_v). Shift does not change keycode.
		if ((mods & (KMOD_CTRL | KMOD_GUI)) && (key == SDLK_v)) {
			char *clip = SDL_GetClipboardText();
			if (clip) {
				std::string text(clip);
				SDL_free(clip);
				// Turn line breaks (\n, \r\n, or bare \r) into Newline
				// commands and the rest into InsertText; InsertText itself
				// rejects any embedded '\r'/'\n'.
				std::lock_guard<std::mutex> lk(mu_);
				for (const auto &cmd : SplitPasteIntoCommands(text))
					q_.push(cmd);
				// Suppress the corresponding TEXTINPUT that may follow
				suppress_text_input_once_ = true;
				return true; // consumed
			}
		}

		{
			bool suppress_req = false;
			produced          = map_key(key, mods,
			                   k_prefix_, esc_meta_,
			                   k_ctrl_pending_,
			                   ed_,
			                   mi,
			                   suppress_req);
			if (suppress_req) {
				// Prevent the corresponding TEXTINPUT from delivering the same digit again
				suppress_text_input_once_ = true;
			}
		}

		// Note: Do NOT suppress SDL_TEXTINPUT after inserting a TAB. Most platforms
		// do not emit TEXTINPUT for Tab, and suppressing here would incorrectly
		// eat the next character typed if no TEXTINPUT follows the Tab press.

		// If we just consumed a universal-argument digit or '-' on KEYDOWN and emitted UArgStatus,
		// suppress the subsequent SDL_TEXTINPUT for this keystroke to avoid duplicating the digit in status.
		// Additional suppression handled above when KEYDOWN consumed a uarg digit

		// Suppress the immediate following SDL_TEXTINPUT when a printable KEYDOWN was used as a
		// k-prefix suffix or Meta (Alt/ESC) chord, regardless of whether a command was produced.
		const bool is_alt              = (mods & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0;
		const bool is_printable_letter = (key >= SDLK_SPACE && key <= SDLK_z);
		const bool is_non_text_key     =
			key == SDLK_TAB || key == SDLK_RETURN || key == SDLK_KP_ENTER ||
			key == SDLK_BACKSPACE || key == SDLK_DELETE || key == SDLK_ESCAPE ||
			key == SDLK_LEFT || key == SDLK_RIGHT || key == SDLK_UP || key == SDLK_DOWN ||
			key == SDLK_HOME || key == SDLK_END || key == SDLK_PAGEUP || key == SDLK_PAGEDOWN;

		bool should_suppress = false;
		if (!is_non_text_key) {
			// Any k-prefix suffix that is printable should suppress TEXTINPUT, even if no
			// command mapped (we report unknown via status instead of inserting text).
			if (was_k_prefix && is_printable_letter) {
				should_suppress = true;
			}
			// Alt/Meta + letter can also generate TEXTINPUT on some platforms
			const bool is_meta_symbol = (
				key == SDLK_COMMA || key == SDLK_PERIOD || key == SDLK_LESS || key ==
				SDLK_GREATER);
			if (is_alt && ((key >= SDLK_a && key <= SDLK_z) || is_meta_symbol)) {
				should_suppress = true;
			}
			// ESC-as-meta followed by printable
			if (was_esc_meta && (is_printable_letter || is_meta_symbol)) {
				should_suppress = true;
			}
		}
		if (should_suppress) {
			suppress_text_input_once_ = true;
		}
		break;
	}
	case SDL_TEXTINPUT: {
		// If the previous KEYDOWN requested suppression of this TEXTINPUT (e.g.,
		// we already handled a uarg digit/minus or a k-prefix printable), do it
		// immediately before any other handling to avoid duplicates.
		if (suppress_text_input_once_) {
			suppress_text_input_once_ = false; // consume suppression
			produced                  = true; // consumed event
			break;
		}

		// If editor universal argument is active, consume digit TEXTINPUT
		if (ed_ &&ed_


		
		->
		UArg() != 0
		)
		{
			const char *txt = e.text.text;
			if (txt && *txt) {
				unsigned char c0 = static_cast<unsigned char>(txt[0]);
				if (c0 >= '0' && c0 <= '9') {
					int d = c0 - '0';
					ed_->UArgDigit(d);
					produced = true; // consumed to update status
					break;
				}
			}
			// Non-digit ends collection; allow processing normally below
		}

		// If we are still in k-prefix and KEYDOWN path didn't handle the suffix,
		// use TEXTINPUT's actual character (handles Shifted letters on macOS) to map the k-command.
		if (k_prefix_) {
			k_prefix_       = false;
			esc_meta_       = false;
			const char *txt = e.text.text;
			if (txt && *txt) {
				unsigned char c0 = static_cast<unsigned char>(txt[0]);
				int ascii_key    = 0;
				if (c0 < 0x80) {
					ascii_key = static_cast<int>(c0);
				}
				if (ascii_key != 0) {
					// Qualifier via TEXTINPUT: uppercase 'C' or '^' only
					if (ascii_key == 'C' || ascii_key == '^') {
						k_ctrl_pending_ = true;
						if (ed_)
							ed_->SetStatus("C-k C _");
						// Keep k-prefix active; do not emit a command
						k_prefix_ = true;
						produced  = true;
						break;
					}
					// Map via k-prefix table; do not pass Ctrl for TEXTINPUT case
					CommandId id;
					bool pass_ctrl  = k_ctrl_pending_;
					k_ctrl_pending_ = false;
					bool mapped     = KLookupKCommand(ascii_key, pass_ctrl, id);
					// Diagnostics: log any k-prefix TEXTINPUT suffix mapping
					char disp = (ascii_key >= 0x20 && ascii_key <= 0x7e)
						            ? static_cast<char>(ascii_key)
						            : '?';
					std::fprintf(stderr,
					             "[kge] k-prefix TEXTINPUT suffix: ascii=%d '%c' mapped=%d id=%d\n",
					             ascii_key, disp, mapped ? 1 : 0,
					             mapped ? static_cast<int>(id) : -1);
					std::fflush(stderr);
					if (mapped) {
						mi = {true, id, "", 0};
						if (ed_)
							ed_->SetStatus(""); // clear "C-k _" hint after suffix
						produced = true;
						break; // handled; do not insert text
					} else {
						// Unknown k-command via TEXTINPUT path
						int shown = KLowerAscii(ascii_key);
						char c    = (shown >= 0x20 && shown <= 0x7e)
							         ? static_cast<char>(shown)
							         : '?';
						std::string arg(1, c);
						mi = {true, CommandId::UnknownKCommand, arg, 0};
						if (ed_)
							ed_->SetStatus("");
						produced = true;
						break;
					}
				}
			}
			// If no usable ASCII was found, still report an unknown k-command and exit k-mode
			mi = {true, CommandId::UnknownKCommand, std::string("?"), 0};
			if (ed_)
				ed_->SetStatus("");
			produced = true;
			break;
		}
		// (suppression is handled at the top of this case)
		// Handle ESC-as-meta fallback on TEXTINPUT: some platforms emit only TEXTINPUT
		// for the printable part after ESC. If esc_meta_ is set, translate first char.
		if (esc_meta_) {
			esc_meta_       = false; // consume meta prefix
			const char *txt = e.text.text;
			if (txt && *txt) {
				// Parse first UTF-8 codepoint (we care only about common ASCII cases)
				unsigned char c0 = static_cast<unsigned char>(txt[0]);
				// Map a few common symbols/letters used in our ESC map
				int ascii_key = 0;
				if (c0 < 0x80) {
					// ASCII path
					ascii_key = static_cast<int>(c0);
					ascii_key = KLowerAscii(ascii_key);
				} else {
					// Basic handling for macOS Option combos that might produce ≤/≥
					// Compare the UTF-8 prefix for these two symbols
					std::string s(txt);
					if (s.rfind("\xE2\x89\xA4", 0) == 0) {
						// U+2264 '≤'
						ascii_key = '<';
					} else if (s.rfind("\xE2\x89\xA5", 0) == 0) {
						// U+2265 '≥'
						ascii_key = '>';
					}
				}
				if (ascii_key != 0) {
					CommandId id;
					if (KLookupEscCommand(ascii_key, id)) {
						mi       = {true, id, "", 0};
						produced = true;
						break;
					}
				}
			}
			// If we get here, unmapped ESC sequence via TEXTINPUT: report invalid
			mi       = {true, CommandId::UnknownEscCommand, "", 0};
			produced = true;
			break;
		}
		if (!k_prefix_ && e.text.text[0] != '\0') {
			// Ensure InsertText never carries a newline; those must originate from KEYDOWN
			std::string text(e.text.text);
			// Strip any CR/LF that might slip through from certain platforms/IME behaviors
			text.erase(std::remove(text.begin(), text.end(), '\n'), text.end());
			text.erase(std::remove(text.begin(), text.end(), '\r'), text.end());
			if (!text.empty()) {
				mi.hasCommand = true;
				mi.id         = CommandId::InsertText;
				mi.arg        = std::move(text);
				mi.count      = 0;
				produced      = true;
			} else {
				// Nothing to insert after filtering; consume the event
				produced = true;
			}
		} else {
			produced = true; // consumed while k-prefix is active
		}
		break;
	}
	default:
		break;
	}

	if (produced && mi.hasCommand) {
		std::lock_guard<std::mutex> lk(mu_);
		q_.push(mi);
	}
	return produced;
}


bool
ImGuiInputHandler::Poll(MappedInput &out)
{
	std::lock_guard<std::mutex> lk(mu_);
	if (q_.empty())
		return false;
	out = q_.front();
	q_.pop();
	return true;
}