import dlib;

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

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;
debug bool g_frame_step = false;
debug bool g_frame_continue = false;

struct EditorCtx
{
	Arena           arena;
	PlatformWindow* window;
	Panel*          base_panel;
	EditState       state;
	u8[128]         input_buf;
	u32             icount;
	u64             editor_id_incr;
	Timer           timer;
	CmdPalette      cmd;
	u8[][]          file_names;
	u64             panel_id;

	Editor*         focused_editor;
}

struct CmdPalette
{
	Arena        arena;
	Arena        cmd_arena;
	u8[]         buffer;
	u32          icount;
	Command[]    commands;
	u8[][]       opt_strs;
	i64          selected;
	Command      current;
	Parameter[]  params;
}

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

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

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;
}

struct Command
{
	u8[]    name;
	CmdType type;
}

struct Parameter
{
	u8[] value;
	bool visible;
}

enum CmdType
{
	None,
	OpenFile,
	SaveFile,
	CreateFile,
	VSplit,
	HSplit,
}

alias CT = CmdType;

enum EditState
{
	NormalMode,
	InputMode,
	CmdOpen,
	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 bool      g_input_mode = false;
__gshared EditorCtx g_ed_ctx;

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

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

	g_delta = DeltaTime(&g_ed_ctx.timer);

	debug if(g_frame_step)
	{
		g_delta = 0.01;
	}


	//HandleInputs(ctx, inputs);

	debug if(g_frame_step && !g_frame_continue) return;
	debug g_frame_continue = false;

	g_input_mode = g_ed_ctx.state == ES.InputMode;

	BeginUI(inputs);

	UICtx* ui_ctx = GetCtx();

	debug
	{
		ui_ctx.dbg = false;

		for(InputEvent* ev = inputs.first; ev; ev = ev.next)
		{
			if(ev.key == Input.g && ev.md & MD.LeftCtrl)
			{
				ui_ctx.dbg = true;
				break;
			}
		}
	}

	UIItem* container    = Container(A2D.Y, UIS2());
	Push!("parent")(container);

	UIItem* ed_container = Container(A2D.X, UISY(ST.Percentage, 0.98));
	
	Push!("parent")(ed_container);
	EditorView(g_ed_ctx.base_panel);
	Pop!("parent");

	assert(ui_ctx.parent.top.value == container);

	StatusBar(&g_ed_ctx);

	if(g_ed_ctx.state == ES.CmdOpen)
	{
		CommandPalette(&g_ed_ctx.cmd);
	}

	EndUI();
}

void
InitEditorCtx(PlatformWindow* window)
{
	InitUICtx(window);

	EditorCtx* ctx = &g_ed_ctx;
	
	ctx.window        = window;
	ctx.arena         = CreateArena(MB(2));
	ctx.cmd.arena     = CreateArena(MB(1));
	ctx.cmd.cmd_arena = CreateArena(MB(1));
	ctx.cmd.buffer    = Alloc!(u8)(1024);
	ctx.base_panel    = CreatePanel(ctx, CreateEditor(ctx));
	ctx.timer         = CreateTimer();

	FocusEditor(ctx.base_panel.ed);

	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!(u8[])(&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!(u8)(&ctx.arena, CastStr!(u8)(e.name[start .. $]));
			}
		}
		catch(Exception e)
		{
			Logf("failed to open directory for filenames");
		}
	}
}

void
FocusEditor(Editor* ed)
{
	assert(ed);
	g_ed_ctx.focused_editor = ed;
	Logf("editor %s", ed.editor_id);
}

Panel*
CreatePanel(EditorCtx* ctx, Editor* ed = null)
{
	Panel* p = Alloc!(Panel)(&ctx.arena);

	p.layout_axis = A2D.Y;
	p.ed          = ed;
	p.id          = ctx.panel_id++;
	p.text_size   = 14;
	p.parent = p.first = p.last = p.next = p.prev = g_NIL_PANEL;

	return p;
}

bool
EditModeActive()
{
	return g_input_mode;
}

char[]
ToAbsolutePath(u8[] 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, u8[] file_name)
{
	import core.stdc.stdio;

	file_name = file_name.length == 0 ? 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, u8[] 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");
		}
	}
}

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

void
AddEditor(EditorCtx* ctx, Panel* target, Axis2D axis)
{
	if(CheckNil(g_NIL_PANEL, target.parent) || target.parent.layout_axis != axis)
	{
		Panel* first  = CreatePanel(ctx, target.ed);
		Panel* second = CreatePanel(ctx, CreateEditor(ctx));
	
		target.layout_axis = axis;
		target.ed          = null;

		DLLPush(target, first,  g_NIL_PANEL);
		DLLPush(target, second, g_NIL_PANEL);

		first.parent = second.parent = target;

		FocusEditor(second.ed);
	}
	else if(target.parent.layout_axis == axis)
	{
		Panel* panel = CreatePanel(ctx, CreateEditor(ctx));

		DLLPush(target.parent, target, panel);
		panel.parent = target.parent;

		UIItem* ed_item = GetEditorItem(panel.ed);
		if(!Nil(ed_item.parent))
		{
			u64 count;
			for(UIItem* c = ed_item.parent.first; !Nil(c); c = c.next)
			{
				count += 1;
			}

			f32 pct = 1.0/cast(f32)count;
			for(UIItem* c = ed_item.parent.first; !Nil(c); c = c.next)
			{
				c.resize_pct = pct;
			}
		}

		FocusEditor(panel.ed);
	}
}

UIItem*
GetEditorItem(Editor* ed)
{
	return Get("###ed_%s", ed.editor_id);
}

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

void
ResetCtx(EditorCtx* ctx, Editor* ed)
{
	InsertInputToBuf(ctx, ed);

	ctx.state        = ES.NormalMode;
	ctx.cmd.icount   = 0;
	ctx.cmd.commands = [];
	ctx.cmd.current  = cast(Command)NO_CMD;
	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;
}

void
HandleInputs(Panel* p, LinkedList!(UIInput)* inputs)
{
	Editor*    ed  = p.ed;
	EditorCtx* ctx = &g_ed_ctx;
	u8[] cb_text;
	FlatBuffer* fb = &ed.buf;

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

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

		if(key == Input.Escape)
		{
			ResetCtx(ctx, ed);
			taken = true;
		}
		else if(ctx.state == ES.InputMode)
		{
			taken = HandleInputMode(ctx, p, node);
		}
		else if(ctx.state == ES.CmdOpen)
		{
			taken = HandleCmdMode(ctx, p, node);
		}
		else if(ctx.state == ES.SetPanelFocus)
		{
			switch(key) with(Input)
			{
				case Escape:
					{
						ResetCtx(ctx, ed);
						taken = true;
					} break;
				default: break;
			}
		}
		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.CmdOpen;
							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))
						{
							// Copy
							taken = true;
						}
					} break;
				case w:
					{
						if(Ctrl(md))
						{
							ctx.state = ES.SetPanelFocus;
						}
					} break;
				debug case d:
					{
						g_ui_ctx.dbg = !g_ui_ctx.dbg;
					} break;
				debug case g:
					{
						g_frame_step = !g_frame_step;
					} break;
				debug case s:
					{
						g_frame_continue = true;
					} break;
				default: taken = Move(fb, key, md); break;
			}
		}

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

	InsertInputToBuf(ctx, ed);

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

void
MoveCursor(Editor* ed, UIInput* ev)
{
	switch(ev.key) with(Input)
	{
		case Up, Down, Left, Right: Move(&ed.buf, ev.key, ev.md); break;
		default: break;
	}
}

pragma(inline) void
InsertChar(EditorCtx* 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(EditorCtx* ctx, Panel* p, UIInput* ev)
{
	bool taken = false;

	switch(ev.key)
	{
		mixin(CharCases());
		case Input.Backspace:
			{
				Backspace(&p.ed.buf);
			} break;
		case Input.Escape:
			{
				ctx.state = ES.NormalMode;
			} break;
		default: 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)(const u8[] str, u8[] match)
{
	return StrContains!(begins_with)(cast(u8[])str, match);
}

bool
StrContains(bool begins_with)(u8[] str, u8[] 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
GetCommands(CmdPalette* cmd)
{
	const Command[] cmd_list = [
		{
			name: CastStr!(u8)("open"),
			type: CT.OpenFile,
		},
		{
			name: CastStr!(u8)("save"),
			type: CT.SaveFile,
		},
		{
			name: CastStr!(u8)("create"),
			type: CT.CreateFile,
		},
		{
			name: CastStr!(u8)("vsplit"),
			type: CT.VSplit,
		},
		{
			name: CastStr!(u8)("hsplit"),
			type: CT.HSplit,
		},
	];

	Reset(&cmd.arena);
	cmd.commands = Alloc!(Command)(&cmd.arena, cmd_list.length);
	cmd.params = [];

	u8[] str = cmd.buffer[0 .. cmd.icount];

	u64 count = 0;
	if(str.length > 0)
	{
		for(u64 i = 0; i < cmd_list.length; i += 1)
		{
			if(StrContains!(true)(cmd_list[i].name, str))
			{
				cmd.commands[count] = cast(Command)cmd_list[i];
				count += 1;
			}
		}
	}

	if(count == 0 && cmd.icount == 0)
	{
		cmd.commands[] = cast(Command[])cmd_list[];
	}
	else
	{
		cmd.commands = cmd.commands[0 .. count];
	}

	cmd.opt_strs = Alloc!(u8[])(&cmd.arena, cmd.commands.length);
	for(u64 i = 0; i < cmd.commands.length; i += 1)
	{
		cmd.opt_strs[i] = cmd.commands[i].name;
	}
}

bool
HandleCmdMode(EditorCtx* ctx, Panel* panel, UIInput* ev)
{
	bool taken;
	CmdPalette* cmd = &ctx.cmd;
	Editor*      ed = panel.ed;

	u64 prev_count = cmd.icount;
	switch(ev.key) with(Input)
	{
		case Enter:
			{
				if(cmd.current.type == CT.None && cmd.commands.length > 0)
				{
					if(cmd.commands[cmd.selected].type == CT.OpenFile)
					{
						goto case Tab;
					}
					else
					{
						cmd.current = cmd.commands[cmd.selected];
					}
				}

				switch(cmd.current.type)
				{
					case CT.OpenFile:
						{
							if(cmd.selected >= 0 && cmd.selected < cmd.opt_strs.length)
							{
								OpenFile(ed, cmd.opt_strs[cmd.selected]);
							}
						} break;
					case CT.SaveFile:
						{
							SaveFile(ed, GetParam(cmd));
						} break;
					case CT.VSplit, CT.HSplit:
						{
							AddEditor(ctx, panel, cmd.current.type == CT.VSplit ? A2D.X : A2D.Y);
						} break;
					default: break;
				}

				ResetCtx(ctx, ed);
			} goto CmdInputEnd;
		case Backspace:
			{
				if(cmd.icount > 0 && cmd.buffer[--cmd.icount] == ' ')
				{
					cmd.current = cast(Command)NO_CMD;
				}
			} break;
		case Space:
			{
				Check(cmd, 1);
				cmd.buffer[cmd.icount++] = ' ';
			} goto case Tab;
		case Tab:
			{
				if(cmd.commands.length > 0)
				{
					cmd.current = cmd.commands[cmd.selected];
					cmd.buffer[0 .. cmd.current.name.length] = cmd.current.name[0 .. $];
					cmd.icount = cast(u32)cmd.current.name.length;
					cmd.buffer[cmd.icount++] = ' ';
				}
			} break;
		case Up, Down:
			{
				cmd.selected = clamp(cmd.selected+(ev.key == Up ? -1 : +1), 0, cmd.opt_strs.length-1);
			} break;
		default: 
			{
				if(ev.text.length)
				{
					cmd.buffer[cmd.icount .. cmd.icount+ev.text.length] = cast(u8[])ev.text[0 .. $];
					cmd.icount += ev.text.length;
				}
			} break;
	}

	if(cmd.current.type == CT.None && (cmd.commands.length == 0 || prev_count != cmd.icount))
	{
		GetCommands(cmd);
	}
	else if(prev_count != cmd.icount)
	{
		switch(cmd.current.type) with(CT)
		{
			case OpenFile:
				{
					PopulateParams(cmd, ctx.file_names);
				} break;
			case SaveFile:
				{
					u8[] param = GetParam(cmd);
				} break;
			default: break;
		}
	}

CmdInputEnd:

	return taken;
}

pragma(inline) void
Check(CmdPalette* cmd, u64 length)
{
	if(cmd.icount+length >= cmd.buffer.length)
	{
		cmd.buffer = Realloc!(u8)(cmd.buffer, cmd.buffer.length*2);
	}
}

u8[]
GetParam(CmdPalette* cmd)
{
	u8[] param = [];
	for(u64 i = cmd.current.name.length; i < cmd.icount; i += 1)
	{
		if(cmd.buffer[i] != ' ')
		{
			param = cmd.buffer[i .. cmd.icount];
			break;
		}
	}

	return param;
}

void
PopulateParams(CmdPalette* cmd, u8[][] strs)
{
	u8[] param = GetParam(cmd);
	if(cmd.params.length == 0 || !param.length)
	{
		cmd.params = Alloc!(Parameter)(&cmd.cmd_arena, strs.length);
		cmd.opt_strs = Alloc!(u8[])(&cmd.cmd_arena, strs.length);

		for(u64 i = 0; i < cmd.params.length; i += 1)
		{
			cmd.params[i].value   = strs[i];
			cmd.params[i].visible = true;
			cmd.opt_strs[i] = cmd.params[i].value;
		}
	}

	if(param.length)
	{
		cmd.opt_strs = Alloc!(u8[])(&cmd.cmd_arena, strs.length);

		u64 matches;
		for(u64 i = 0; i < cmd.params.length; i += 1)
		{
			bool contains = StrContains!(false)(cmd.params[i].value, param);
			if(contains)
			{
				cmd.opt_strs[matches] = cmd.params[i].value;
				matches += 1;
			}

			cmd.params[i].visible = contains;
		}

		cmd.opt_strs = cmd.opt_strs[0 .. matches];
	}

	if(cmd.selected >= cmd.opt_strs.length)
	{
		cmd.selected = Max(cmd.opt_strs.length, 0);
	}
}

