import dlib;
import dlib.util : Str;

import vulkan;
import std.format : sformat;
import buffer;
import ui : Nil;
import ui;
import parsing;

import std.algorithm.comparison : clamp;
import std.format;
import std.stdio;
import std.exception;
import std.file;
import std.string;
import core.stdc.stdio;

f32 g_delta = 0.0;

struct RenderCtx
{
	PlatformWindow* window;
	Descriptor[FS]  font_descs;
	Descriptor      default_tex;
	Descriptor      sampler;
	Pipeline        pipeline;
	Pipeline        slug_pipeline;
	DescSetLayout   desc_set_layout;
	DescSet[FO]     desc_sets;
	PipelineLayout  pipeline_layout;
	PushConst       pc;
}

struct Ctx
{
	RenderCtx                   rd_ctx;

	HashTable!(UIHash, UIItem*) items;
	UIItem*                     free_items;
	UIItem*                     last_item;

	Arena                       arena;
	Arena                       temp_arena;
	Arena[FO]                   str_arenas;  

	Inputs*                     inputs;
	u64                         frame;
	u64                         f_idx;

	Notification*               active_notifications;
	Notification*               free_notifications;

	LinkedList!(UIInput)        events;
	IVec2                       mouse_pos;

	SlugFont                    slug_font;
	FontFace                    font;
	FontSet[FS]                 font_sets;
	u32                         font_sets_used;
	bool[FO]                    fonts_to_load;
	bool                        prev_has_tex;
	u32                         prev_atlas_index;

	UIBuffer[FO]                buffers;
	
	SlugBuffer[FO]                slug_buffers;
	MappedBuffer!(SlugVertex)[FO] slug_vertex_buffers;
	MappedBuffer!(u32)[FO]        slug_index_buffers;

	u32                         tab_width;
	Vec4[TS.max][UISH.max]      syntax_colors;

	UIKey                       drag_key;
	UIKey                       hover_key;
	UIKey                       focus_key;

	UIPanel*                    base_panel;
	UIPanel*                    focused_panel;
	UIPanel*                    free_panels;

	u64                         last_hover_frame;

	f32                         pos_rate;
	f32                         scroll_rate;
	f32                         animation_rate;
	f32                         fade_rate;

	ConfigValue[CO.max]         config_values;

	EditState                   state;
	u8[128]                     input_buf;
	u32                         icount;
	u64                         editor_id_incr;
	Timer                       timer;
	CmdPalette                  cmd;
	string[]                    file_names;
	u64                         panel_id;

	debug bool                  dbg;

	alias rd_ctx this;
}

struct Notification
{
	Notification* next;
	string        message;
	f32           anim_progress = 0.0;
	f32           elapsed       = 0.0;
}

struct TextInputBuffer
{
	u8[] data;
	u64  length;
}

struct CmdPalette
{
	TextInputBuffer          text_input;
	i64                      selected;
	Command[CMD_LIST.length] cmd_arr;
	Command[]                cmds;

	Arena                    exec_cmd_arena;
	TextInputBuffer          exec_cmd_input;
	i64                      exec_cmd_selected;
	string[]                 exec_cmd_opts;
}

struct Editor
{
	Arena       arena;
	u64         editor_id;
	
	FlatBuffer  buf;
	Tokenizer   tk;

	I64Vec2     cursor_pos;
	Vec2        select_start;
	Vec2        select_end;
	i64         line_offset;
	i64         line_height;

	Editor*     free_next;

	alias buf this;
}

struct ChangeStacks
{
	Arena         arena;
	u64           current_pos;
	u8[]          current_str;
	u64           current_len;
	EditorChange* undos;
	EditorChange* redos;
}

struct EditorChange
{
	u8[]          str;
	u64           pos;
	EditorChange* next;
}

alias CmdFn = bool function(Ctx* ctx); // return true when completed

struct Command
{
	string  name;
	string  desc;
	string  cmd;
	CmdType type;
	union
	{
		CmdFn     fn;
		ConfigOpt opt;
	}
}

union ConfigValue
{
	f32  _f32;
	u64  _u64;
	Vec4 _vec4;
}

enum ValueType
{
	F32,
	U64,
	Vec4,
	Max,
} alias VT = ValueType;

enum ConfigInputType
{
	Text,
	Slider,
	Dropdown,
	Max,
} alias CIT = ConfigInputType;

struct ConfigInfo
{
	ValueType       value_type;
	ConfigInputType input_type;
}

enum ConfigOpt
{
	CornerRadius,
	TabWidth,
	ScrollSpeed,
	EditorPadding,
	Max,
} alias CO = ConfigOpt;

enum CmdType
{
	None,
	Config,
	Callback,
	Hotkey,
} alias CT = CmdType;

enum EditState
{
	NormalMode,
	InputMode,
	CmdPalette,
	RunCmd,
	SetPanelFocus, // if moving left/right move up parent tree until one is found with a2d.x, same thing for up/down a2d.y
} alias ES = EditState;

__gshared       Ctx           g_ctx;
__gshared const Editor        g_nil_ed;
__gshared       Editor*       g_NIL_ED;
__gshared const UIItem        g_ui_nil_item;
__gshared       UIItem*       g_UI_NIL;
__gshared const UIInput       g_ui_nil_input;
__gshared       UIInput*      g_UI_NIL_INPUT;
__gshared const UIPanel       g_nil_panel;
__gshared       UIPanel*      g_NIL_PANEL;
__gshared const Notification  g_nil_notif;
__gshared       Notification* g_NIL_NOTIF;

const Command NO_CMD = {
	name: [],
	type: CT.None,
};

Command[21] CMD_LIST = [
	{
		name: "Open",
		cmd:  "open",
		desc: "Open a file in the focused editor view.",
		type: CT.Callback,
		fn:   &OpenFileWindow,
	},
	{
		name: "Save",
		cmd:  "save",
		desc: "Save the current file in the focused editor view.",
		type: CT.Callback,
		fn:   &SaveFileWindow,
	},
	{
		name: "New File",
		cmd:  "new file",
		desc: "Create a new file with a specified name and location.",
		type: CT.Callback,
		fn:   &NotImplementedCallback,
	},
	{
		name: "Vertical Split",
		cmd:  "vsplit",
		desc: "Split the current editor view vertically.",
		type: CT.Callback,
		fn:   &SplitVertically,
	},
	{
		name: "Horizontal Split",
		cmd:  "hsplit",
		desc: "Split the current editor view horizontally.",
		type: CT.Callback,
		fn:   &SplitHorizontally,
	},
	{
		name: "Close Panel",
		cmd:  "close",
		desc: "Close the currently highlighted panel, if it's the last panel it will have the file closed instead.",
		type: CT.Callback,
		fn:   &ClosePanelCmd,
	},
	{
		name: "Test Option 1",
		cmd:  "testopt1",
		desc: "This is a test option",
		type: CT.Callback,
		fn:   &NotImplementedCallback,
	},
	{
		name: "Test Option 2",
		cmd:  "testopt2",
		desc: "This is a test option",
		type: CT.Callback,
		fn:   &NotImplementedCallback,
	},
	{
		name: "Test Option 3",
		cmd:  "testopt3",
		desc: "This is a test option",
		type: CT.Callback,
		fn:   &NotImplementedCallback,
	},
	{
		name: "Test Option 4",
		cmd:  "testopt4",
		desc: "This is a test option",
		type: CT.Callback,
		fn:   &NotImplementedCallback,
	},
	{
		name: "Test Option 5",
		cmd:  "testopt5",
		desc: "This is a test option",
		type: CT.Callback,
		fn:   &NotImplementedCallback,
	},
	{
		name: "Test Option 6",
		cmd:  "testopt6",
		desc: "This is a test option",
		type: CT.Callback,
		fn:   &NotImplementedCallback,
	},
	{
		name: "Test Option 7",
		cmd:  "testopt7",
		desc: "This is a test option",
		type: CT.Callback,
		fn:   &NotImplementedCallback,
	},
	{
		name: "Test Option 8",
		cmd:  "testopt8",
		desc: "This is a test option",
		type: CT.Callback,
		fn:   &NotImplementedCallback,
	},
	{
		name: "Test Option 9",
		cmd:  "testopt9",
		desc: "This is a test option",
		type: CT.Callback,
		fn:   &NotImplementedCallback,
	},
	{
		name: "Test Option 10",
		cmd:  "testopt10",
		desc: "This is a test option",
		type: CT.Callback,
		fn:   &NotImplementedCallback,
	},
	{
		name: "Test Option 11",
		cmd:  "testopt11",
		desc: "This is a test option",
		type: CT.Callback,
		fn:   &NotImplementedCallback,
	},
	{
		name: "Test Option 12",
		cmd:  "testopt12",
		desc: "This is a test option",
		type: CT.Callback,
		fn:   &NotImplementedCallback,
	},
	{
		name: "Test Option 13",
		cmd:  "testopt13",
		desc: "This is a test option",
		type: CT.Callback,
		fn:   &NotImplementedCallback,
	},
	{
		name: "Test Option 14",
		cmd:  "testopt14",
		desc: "This is a test option",
		type: CT.Callback,
		fn:   &NotImplementedCallback,
	},
	{
		name: "Test Option 15",
		cmd:  "testopt15",
		desc: "This is a test option",
		type: CT.Callback,
		fn:   &NotImplementedCallback,
	},
];

bool
ClosePanelCmd(Ctx* ctx)
{
	Clear(ctx.focused_panel.ed);
	if(ctx.focused_panel != ctx.base_panel)
	{
		DLLRemove(ctx.focused_panel.parent, ctx.focused_panel, g_NIL_PANEL);

		u64 child_count;
		UIPanel* removed = ctx.focused_panel;
		UIPanel* parent  = removed.parent;
		if(!Nil(parent.first) && parent.first == parent.last)
		{
			UIPanel* only_child = parent.first;

			parent.ed     = only_child.ed;
			only_child.ed = null;

			DLLRemove(parent, only_child, g_NIL_PANEL);
			PushFree(&ctx.free_panels, only_child);

			Focus(parent);
		}
		else
		{
			for(UIPanel* child = parent.first; !Nil(child); child = child.next)
			{
				child_count += 1;
			}

			f32 add_pct = ctx.focused_panel.pct/child_count;
			for(UIPanel* child = parent.first; !Nil(child); child = child.next)
			{
				child.pct += add_pct;
			}

			UIPanel* focus = ctx.base_panel;
			for(UIPanel* panel = ctx.base_panel; true; panel = panel.first)
			{
				if(!CheckNil(g_NIL_ED, panel.ed))
				{
					Focus(panel);
					break;
				}
			}
		}

		PushFree(&ctx.free_panels, removed);
	}

	return true;
}

bool
SplitVertically(Ctx* ctx)
{
	AddPanel(ctx.focused_panel, A2D.X);
	return true;
}

bool
SplitHorizontally(Ctx* ctx)
{
	AddPanel(ctx.focused_panel, A2D.Y);
	return true;
}


bool
NotImplementedCallback(Ctx* ctx)
{
	Logf("Not yet implemented!");
	return true;
}

bool
CmdModeActive()
{
	return g_ctx.state == ES.CmdPalette;
}

bool
Active(EditState state)
{
	return g_ctx.state == state;
}

UIPanel*
MakePanel(UIPanel* parent = g_NIL_PANEL)
{
	UIPanel* p = Alloc!(UIPanel);

	static u32 id;

	p.id     = id++;
	p.item   = MakeItem("###panel_%s", p.id);

	return p;
}

void
Cycle(Inputs* inputs)
{
	ResetScratch(MB(4));

	g_delta      = DeltaTime(&g_ctx.timer);

	BeginUI(inputs);

	Ctx* ctx = GetCtx();

	Panel(ctx, ctx.base_panel);

	if(ctx.state == ES.RunCmd)
	{
		if(ctx.cmd.cmds[ctx.cmd.selected].fn(ctx))
		{
			ctx.state = ES.NormalMode;
		}
	}

	CommandPalette(ctx);

	EndUI();
}

void
Focus(UIPanel* panel)
{
	g_ctx.focused_panel = panel;
	g_ctx.focus_key     = panel.key;
}

void
InitCtx(PlatformWindow* window)
{
	Ctx* ctx = &g_ctx;

	g_NIL_ED       = cast(Editor*)&g_nil_ed;
	g_UI_NIL       = cast(UIItem*)&g_ui_nil_item;
	g_UI_NIL_INPUT = cast(UIInput*)&g_ui_nil_input;
	g_NIL_PANEL    = cast(UIPanel*)&g_nil_panel; 

	ctx.window                  = window;
	ctx.arena                   = CreateArena(MB(2));
	ctx.cmd.text_input.data     = Alloc!(u8)(1024);
	ctx.cmd.exec_cmd_input.data = Alloc!(u8)(1024);
	ctx.cmd.exec_cmd_arena      = CreateArena(MB(1));
	ctx.timer                   = CreateTimer();
	ctx.free_panels             = g_NIL_PANEL;

	InitUI(ctx);
	
	ctx.base_panel = CreatePanel();
	Focus(ctx.base_panel);

	if(getcwd() != "/")
	{
		// TODO: replace this with something nogc/nothrow
		try
		{
			u64 count = 0;
			foreach(DirEntry e; dirEntries(".", SpanMode.breadth))
			{
				if(indexOf(e.name, ".git") != -1 || e.isDir) continue;

				u64 start = indexOf(e.name, "./") == 0 ? 2 : 0;
				count += 1;
			}

			ctx.file_names = Alloc!(string)(&ctx.arena, count);

			count = 0;
			foreach(DirEntry e; dirEntries(".", SpanMode.breadth))
			{
				if(indexOf(e.name, ".git") != -1 || e.isDir) continue;

				u64 start = indexOf(e.name, "./") == 0 ? 2 : 0;
				ctx.file_names[count++] = Alloc(&ctx.arena, e.name);
			}
		}
		catch(Exception e)
		{
			Logf("failed to open directory for filenames");
		}
	}
}

Ctx*
GetCtx()
{
	return &g_ctx;
}

char[]
ToAbsolutePath(string file_name)
{
	import core.stdc.string : strlen;

	char[1024] name_buf = '\0';
	char[1024] wd_buf = '\0';
	version(linux)
	{
		import core.sys.posix.unistd;
		getcwd(wd_buf.ptr, wd_buf.length);
	}

	version(Windows)
	{
		import core.sys.windows.direct;
		_getcwd(wd_buf.ptr, wd_buf.length);
	}

	char[] wd = wd_buf[0 .. strlen(wd_buf.ptr)];

	version(linux) 
	{
		if(file_name[0] != '/')
		{
			name_buf.sformat("%s/%s", wd, cast(char[])file_name);
		}
		else
		{
			name_buf.sformat("%s", cast(char[])file_name);
		}
	}

	version(Windows)
	{
		name_buf.sformat("%s/%s", wd, cast(char[])file_name);
	}

	char[] path_buf = ScratchAlloc!(char)(strlen(name_buf.ptr)+1);
	path_buf[0 .. $] = name_buf[0 .. path_buf.length];

	return path_buf;
}

void
SaveFile(Editor* ed, string file_name)
{
	import core.stdc.stdio;

	file_name = file_name.length == 0 ? Str(ed.buf.file_name) : file_name;

	if(file_name.length > 0)
	{
		char[] file_path = ToAbsolutePath(file_name);
		auto f = fopen(cast(char*)file_path.ptr, "wb");
		if(f != null)
		{
			u64 tab_count;
			for(u64 i = 0; i < ed.buf.length; i += 1)
			{
				if(ed.buf.data[i] == '\t')
				{
					tab_count += 1;
				}
			}

			u64  tab_width = GetCtx().tab_width;
			u64  buf_size  = ed.buf.length + ((tab_width-1) * tab_count);
			u8[] temp_buf  = ScratchAlloc!(u8)(buf_size);

			u64 buf_pos;
			for(u64 i = 0; i < ed.buf.length; i += 1)
			{
				if(ed.buf.data[i] == '\t')
				{
					for(u64 j = 0; j < tab_width; j += 1)
					{
						temp_buf[buf_pos++] = ' ';
					}
				}
				else
				{
					temp_buf[buf_pos++] = ed.buf.data[i];
				}
			}

			fwrite(temp_buf.ptr, 1, buf_size, f);
			fflush(f);
			fclose(f);
		}
	}
}

void
OpenFile(Editor* ed, string file_name)
{
	import core.stdc.stdio;
	import std.file;
	import std.conv;

	if(file_name.length > 0)
	{
		char[] file_path = ToAbsolutePath(file_name);
		auto f = fopen(file_path.ptr, "rb");
		if(f != null)
		{
			fseek(f, 0, SEEK_END);
			i64 len = ftell(f);
			fseek(f, 0, SEEK_SET);

			if(len > 0)
			{
				u8[] buf = ScratchAlloc!(u8)(len);
				fread(buf.ptr, u8.sizeof, len, f);
				Change(&ed.buf, buf, file_name);
				ed.buf.file_name = file_name;
			}

			fclose(f);
		}
		else
		{
			perror("[Error] Unable to open file");
		}
	}
}

string
Str(TextInputBuffer text_buffer)
{
	return Str(text_buffer.data[0 .. text_buffer.length]);
}

Editor*
CreateEditor()
{
	Editor* ed   = Alloc!(Editor)(&g_ctx.arena);
	ed.arena     = CreateArena(MB(4));
	ed.buf       = CreateFlatBuffer([], []);
	ed.editor_id = g_ctx.editor_id_incr++;
	
	return ed;
}

pragma(inline) void
InsertInputToBuf(Ctx* ctx, Editor* ed)
{
	if(ctx.icount > 0)
	{
		Insert(&ed.buf, ctx.input_buf, ctx.icount);
		ctx.icount = 0;
	}
}

void
ResetCtx(Editor* ed)
{
	InsertInputToBuf(&g_ctx, ed);
	ResetCtx();
}

void
ResetCmd(Ctx* ctx)
{
	ctx.cmd.text_input.length     = 0;
	ctx.cmd.selected              = 0;
	ctx.cmd.cmds                  = ctx.cmd.cmd_arr[0 .. $];
	ctx.cmd.exec_cmd_input.length = 0;
 	ctx.cmd.exec_cmd_selected     = 0;
	ctx.cmd.exec_cmd_opts         = [];
	ctx.cmd.cmds                  = CMD_LIST[0 .. $];

	Reset(&ctx.cmd.exec_cmd_arena);
}

void
ResetCtx()
{
	g_ctx.state                 = ES.NormalMode;
	g_ctx.cmd.text_input.length = 0;
	g_ctx.cmd.selected          = 0;
}

bool
Shift(Modifier md)
{
	return cast(bool)(md & (MD.LeftShift | MD.RightShift));
}

bool
MovePanelFocus(A2D axis, bool prev)(UIPanel* panel)
{
	bool result = false;

	if(!Nil(panel))
	{
		UIPanel* target = prev ? panel.prev : panel.next;
		if(panel.parent.axis == axis && !Nil(target) && target.ed != null)
		{
			//SetFocusedPanel(target);
			result = true;
		}
		else for(UIPanel* p = panel.parent; !Nil(p); p = p.parent)
		{
			if(p.parent.axis == axis)
			{
				for(UIPanel* t = prev ? p.prev : p.next; !Nil(t); t = prev ? t.prev : t.next)
				{
					UIPanel* f = t.first;
					if(!Nil(f) || f.ed == null)
					{
						while(f.ed == null)
						{
							if(Nil(f.first))
							{
								break;
							}

							f = f.first;
						}
					}

					if(!Nil(f) && f.ed != null)
					{
						//SetFocusedPanel(f);
						result = true;
						break;
					}
				}
			}
		}
	}

	return result;
}

bool
HandleInputs(UIPanel* p, LinkedList!(UIInput)* inputs, bool hovered, bool focused)
{
	Editor*     ed  = p.ed;
	Ctx*        ctx = &g_ctx;
	FlatBuffer* fb  = &ed.buf;
	u64 initial_len = p.ed.buf.length;
	u8[] cb_text;

	for(auto node = inputs.first; node != null; node = node.next)
	{
		bool taken = false;

		Input    key = node.key;
		Modifier md  = node.md;

		switch(node.type)
		{
			case UIE.Click:
				{
					if(hovered)
					{
						Focus(p);
						taken = true;
					}
					else
					{
						// TODO: set cursor pos
					}
				} break;
			case UIE.Drag:
				{
					// TODO: did drag start on this panel and if so modify selection
				} break;
			case UIE.DragRelease:
				{
					// TODO: if dragged end selection
				} break;
			case UIE.DragStart:
				{
					if(hovered)
					{
						// TODO: Selection
					}
				} break;
			case UIE.Press:
				{
					if(focused)
					{
						if(key == Input.Escape)
						{
							ResetCtx(ed);
							taken = true;
						}
						else if(ctx.state == ES.InputMode)
						{
							taken = HandleInputMode(ctx, p, node);
						}
						else
						{
							switch(key) with(Input)
							{
								case a, i:
									{
										if(key == a && Shift(md) && fb)
										{	
											MoveToEOL(fb);
										}
										else if(key == a)
										{
											Move(fb, Right, MD.None);
										}
										else if(key == i && Shift(md))
										{
											MoveToSOL(fb);
										}

										ctx.state = ES.InputMode;
										taken = true;
									} break;
								case Semicolon:
									{
										if(Shift(node.md))
										{
											ctx.state                 = ES.CmdPalette;
											ResetCmd(ctx);
											
											taken = true;
										}
									} break;
								case v:
									{
										if(Ctrl(md))
										{
											cb_text = ClipboardText(ctx.window);
										}
										if(Shift(md))
										{
											ToggleSelection(fb, SM.Line);
										}
										else
										{
											ToggleSelection(fb, SM.Normal);
										}
									} break;
								case c:
									{
										if(Ctrl(md))
										{
											// TODO: Copy
											taken = true;
										}
									} break;
								case g:
									{
										debug 
										{
											if(Shift(md))
											{
												ctx.dbg = !ctx.dbg;
											}
										}
									} break;
								default: 
									{
										taken = MoveCursor(ed, node);
									} break;
							}
						}
					}
				} break;
			case UIE.Scroll:
				{
					if(hovered)
					{
						SetOffset(ed, node.scroll*10);
						taken = true;
					}
				} break;
			default: break;
		}

		if(taken)
		{
			DLLRemove(inputs, node, null);
		}
	}

	InsertInputToBuf(ctx, ed);

	if(cb_text.length > 0)
	{
		Insert(fb, cb_text, cb_text.length);
	}

	return initial_len != p.ed.buf.length;
}

void
SetOffset(Editor* ed, i64 adjust)
{
	i64 max_offset = ed.buf.line_count-ed.line_height;
	if(max_offset < 0)
	{
		max_offset = 0;
	}

	ed.line_offset = clamp(ed.line_offset+adjust, 0, max_offset);
}

bool
MoveCursor(Editor* ed, UIInput* ev)
{
	bool taken;

	switch(ev.key) with(Input)
	{
		case Up, Down, Left, Right: taken = Move(&ed.buf, ev.key, ev.md); break;
		default: break;
	}

	if(taken)
	{
		I64Vec2 pos = VecPos(&ed.buf);
		i64     min = ed.line_offset+2;
		i64     max = clamp(ed.line_offset+ed.line_height-3, 0, ed.buf.line_count);
		if(pos.y > max)
		{
			SetOffset(ed, pos.y-max);
		}
		else if(pos.y < min)
		{
			SetOffset(ed, -(min-pos.y));
		}
	}

	return taken;
}

pragma(inline) void
InsertChar(Ctx* ctx, Input input, bool modified)
{
	ctx.input_buf[ctx.icount++] = InputToChar(input, modified);
}

static string
CharCases()
{
	import std.traits;

	string result = "";
	foreach(input; EnumMembers!Input)
	{
		u8 ch = InputToChar(input);
		if(ch > 0)
		{
			result ~= format("case Input.%s: InsertChar(ctx, Input.%s, cast(bool)(ev.md & (MD.LeftShift | MD.RightShift))); taken = true; break;\n", input, input);
		}
	}
	return result;
}

bool
HandleInputMode(Ctx* ctx, UIPanel* p, UIInput* ev)
{
	bool taken;

	if(!taken) switch(ev.key)
	{
		mixin(CharCases());
		case Input.Backspace:
			{
				Backspace(&p.ed.buf);
				taken = true;
			} break;
		case Input.Escape:
			{
				ctx.state = ES.NormalMode;
				taken = true;
			} break;
		default: taken = MoveCursor(p.ed, ev); break;
	}

	return taken;
}

static string
TextLineCharCases()
{
	import std.traits;

	string result = "";
	foreach(input; EnumMembers!Input)
	{
		u8 ch = InputToChar(input);
		if(ch > 0 && ch != '\n' && ch != '\t' && ch != ' ')
		{
			if(ch == '\'' || ch == '\\')
			{
				result ~= format("case %s: result = '\\%s'; taken = true; break;\n", input, cast(char)ch);
			}
			else
			{
				result ~= format("case %s: result = '%s'; taken = true; break;\n", input, cast(char)ch);
			}
		}
	}

	return result;
}

pragma(inline) u8
Lower(u8 ch)
{
	if(ch >= 65 && ch <= 90)
	{
		ch += 32;
	}

	return ch;
}

bool
StrContains(bool begins_with, T, U)(T str_param, U match_param) if(StringType!(T) && StringType!(U))
{
	static if(!is(T == string))
	{
		string str = Str(str_param);
	}
	else
	{
		string str = str_param;
	}

	static if(!is(U == string))
	{
		string match = Str(match_param);
	}
	else
	{
		string match = match_param;
	}

	return StrContains!(begins_with)(str, match);
}

bool
StrContains(bool begins_with)(string str, string match)
{
	u64 count;
	for(u64 i = 0; i < str.length; i += 1)
	{
		static if(begins_with) if(i >= match.length)
		{
			break;
		}

		if(Lower(str[i]) == Lower(match[count]))
		{
			count += 1;
		}
		else
		{
			count = 0;
		}

		static if(!begins_with) if(count == match.length)
		{
			break;
		}
	}

	return count == match.length;
}

void
HandleCmdMode(Ctx* ctx, bool* enter_hit, bool* buffer_changed, bool* selection_changed)
{
	UIPanel*         panel       = ctx.focused_panel;
	Editor*          ed          = panel.ed;
	TextInputBuffer* text_buffer = ctx.state == ES.CmdPalette ? &ctx.cmd.text_input : &ctx.cmd.exec_cmd_input;
	i64*             selected    = ctx.state == ES.CmdPalette ? &ctx.cmd.selected   : &ctx.cmd.exec_cmd_selected;

	for(UIInput* ev = ctx.events.first; ev; ev = ev.next)
	{
		bool taken;

		if(ctx.state != ES.CmdPalette && ctx.state != ES.RunCmd)
		{
			break;
		}

		switch(ev.type) 
		{
			case UIE.Press:
				{
					switch(ev.key) with(Input)
					{
						case Enter:
							{
								if(ctx.state == ES.CmdPalette)
								{
									switch(ctx.cmd.cmds[ctx.cmd.selected].type) with(CmdType)
									{
										case Config:
											{
												// TODO: implement
											} break;
										case Hotkey:
											{
												// TODO: implement
											} break;
										case Callback:
											{
												assert(ctx.cmd.cmds[ctx.cmd.selected].fn, "Callback type doesn't have a function pointer set");
												ctx.state = ES.RunCmd;
												taken = true;
											} break;
										default: break;
									}
								}
								else
								{
									*enter_hit = true;
									taken = true;
								}
							} break;
						case Up, Down:
							{
								i32 incr  = ev.key == Up ? -1 : +1;
								*selected = (*selected)+incr;

								*selection_changed = true;
								taken = true;
							} break;
						case Escape:
							{
								ResetCtx();
								taken = true;
							} break;
						default: 
							{
								*buffer_changed = taken = HandleTextInput(text_buffer, ev);
							} break;
					}
				} break;
				default: break;
		}

		if(taken)
		{
			DLLRemove(&ctx.events, ev, null);
		}
	}
}

bool
HandleTextInput(TextInputBuffer* text_input, UIInput* ev)
{
	bool taken;

	switch(ev.key) with(Input)
	{
		case Backspace:
			{
				if(text_input.length)
				{
					text_input.length -= 1;
					taken = true;
				}
			} break;
		default: 
			{
				if(ev.text.length)
				{
					Check(text_input, ev.text.length);
					text_input.data[text_input.length .. text_input.length+ev.text.length] = cast(u8[])ev.text[0 .. $];
					text_input.length += ev.text.length;

					taken = true;
				}
			} break;
	}

	return taken;
}

pragma(inline) void
Check(TextInputBuffer* text_buffer, u64 length)
{
	if(text_buffer.length+length >= text_buffer.data.length)
	{
		text_buffer.data = Realloc!(u8)(text_buffer.data, text_buffer.data.length*2);
	}
}

