import dlib;

import buffer;
import ui : Nil;
import ui;
import parsing;
import editor;
import std.format : sformat;
import std.math.rounding : ceil, floor;
import std.math.exponential : pow;
import core.stdc.math : fabsf;
import std.conv;
import core.stdc.stdio : sprintf;

const Vec4[4] DEFAULT_COL = Vec4(0.13, 0.13, 0.13, 1.0);

const Vec4[4] CMD_PALETTE_COL = Vec4(0.21, 0.21, 0.21, 1.0);

const Vec4[4] DEFAULT_BORDER_COL = Vec4(0.254, 0.254, 0.266, 1.0);

const Vec4[4] HL_BORDER_COL = Vec4(0.035, 0.549, 0.824, 1.0);

const Vec4[4] LC_COLOR = Vec4(0.12, 0.12, 0.12, 1.0);

const Vec4[4] LC_HL_COLOR = Vec4(0.012, 0.176, 0.29, 1.0);

const Vec4[4] CMD_PALETTE_INPUT_HL = Vec4(0.24, 0.45, 0.81, 1.0);

const Vec4[4] CMD_INPUT_BORDER_COL = Vec4(0.003, 0.48, 0.68, 1.0);

const Vec4[4] CMD_INPUT_BORDER_HL = Vec4(0.05, 0.56, 0.76, 1.0);

const Vec4[4] CMD_PALETTE_INPUT_COL = [
	Vec4(0.14, 0.14, 0.14, 1.0),
	Vec4(0.14, 0.14, 0.14, 1.0),
	Vec4(0.17, 0.17, 0.17, 1.0),
	Vec4(0.17, 0.17, 0.17, 1.0),
];

const UIPanel g_ui_nil_panel;
UIPanel* g_UI_NIL_PANEL;

UIPanel g_root_panel;

WidgetCtx g_widget_ctx;

struct WidgetCtx
{
	UIPanel* parent;
	UIPanel* focused_panel;
}

struct UIPanel
{
	AnimState anim;
	u8[]      id;
	Editor*   ed;

	UIPanel*  parent;
	UIPanel*  next;
	UIPanel*  prev;
	UIPanel*  first;
	UIPanel*  last;

	UIPanel*  list_next;

	Axis2D    axis;
	SizeType  size_type;
	f32       pct;
	Vec4      color;

	i64       prev_offset;
	i64       start_row;
	i64       end_row;
}

struct AnimState
{
	f32 start_value;
	f32 end_value;
	f32 start_time;
	f32 remaining_time;
}

struct TextPart
{
	UIItem*   item;
	TextPart* next;
	u32       count;
}

UIItem*
Root()
{
	Vec2 d = RootSize();

	Push!("color")(DEFAULT_COL);
	Push!("layout_axis")(A2D.Y);

	UIItem* root = Get("##root_item");
	BuildItem(root, UISize(ST.Pixels, d.x), UISize(ST.Pixels, d.y), UIF.DrawBackground);

	g_root_panel.pct = 1.0;
	g_root_panel.axis = A2D.Y;
	g_root_panel.color = Vec4(Vec3(0.0), 1.0);
	g_root_panel.list_next = g_root_panel.parent = g_root_panel.next = g_root_panel.prev = g_root_panel.first = g_root_panel.last = g_UI_NIL_PANEL;

	g_widget_ctx.parent = &g_root_panel;

	return root;
}

UIItem*
Panel(UIPanel* panel, UIFlags flags = UIF.None)
{
	Push!("color")(DEFAULT_COL);
	Push!("layout_axis")(panel.axis);

	UIItem* item       = Get(panel.id);
	UIItem* separator  = g_UI_NIL;

	UIPanel* parent_pn = panel.parent;

	UIItem* next       = !Nil(panel.next) ? Get(panel.next.id) : g_UI_NIL;
	UIItem* prev       = !Nil(panel.prev) ? Get(panel.prev.id) : g_UI_NIL;
	UIItem* parent     = !Nil(parent_pn)  ? Get(parent_pn.id)  : PeekParent();

	f32 x_pct = 1.0, y_pct = 1.0;
	if(!Nil(parent_pn))
	{
		x_pct = parent_pn.axis == A2D.X ? panel.pct : 1.0;
		y_pct = parent_pn.axis == A2D.Y ? panel.pct : 1.0;
	}

	if(item.signal & UIS.Clicked)
	{
		SetFocusedPanel(panel);
	}

	f32 adj_x = 0.0, adj_y = 0.0;
	if(!Nil(prev) || !Nil(next))
	{
		A2D p_axis      = parent_pn.axis;
		UIS prev_signal = p_axis == A2D.X ? UIS.BorderX0 : UIS.BorderY0;
		UIS next_signal = p_axis == A2D.X ? UIS.BorderX1 : UIS.BorderY1;

		if(p_axis == A2D.X)
		{
			flags |= !Nil(prev) ? UIF.BorderX0 : UIF.None;
			flags |= !Nil(next) ? UIF.BorderX1 : UIF.None;
		}
		else
		{
			flags |= !Nil(prev) ? UIF.BorderY0 : UIF.None;
			flags |= !Nil(next) ? UIF.BorderY1 : UIF.None;
		}

		Signal(item);

		f32 p_start = parent.rect.vec0.v[p_axis];
		f32 p_end   = parent.rect.vec1.v[p_axis];
		f32 pos     = item.dragged.v[p_axis];
		if(!Nil(prev) && item.signal & UIS.DraggedBorder && item.signal & prev_signal && pos != 0.0)
		{
			f32 pct = Remap(pos, 0.0, p_start-p_end, 0.0, 1.0);
			if(CheckPanelBounds(panel.pct - pct) && CheckPanelBounds(panel.prev.pct + pct))
			{
				panel.pct      -= pct;
				panel.prev.pct += pct;
			}
		}

		else if(!Nil(next) && item.signal & UIS.DraggedBorder && item.signal & next_signal && pos != 0.0)
		{
			f32 pct = Remap(pos, 0.0, p_start-p_end, 0.0, 1.0);
			if(CheckPanelBounds(panel.next.pct - pct) && CheckPanelBounds(panel.pct + pct))
			{
				panel.next.pct -= pct;
				panel.pct      += pct;
			}
		}
	}

	BuildItem(item, UISize(ST.Percentage, x_pct), UISize(ST.Percentage, y_pct), UIF.DrawBackground|UIF.DragBorder|flags);

	PushParent(item);

	Pop!("color", "layout_axis");

	return item;
}

UIItem*
LineCounter(u8[] parent_id, FlatBuffer* buf, f32 offset, i64 start_row, i64 end_row, bool focused)
{
	f32 padding = 4.0;

	Push!("padding")(Vec2(padding, 0.0));
	Push!("color")(focused ? LC_HL_COLOR : LC_COLOR);

	u8[] id = ScratchName(parent_id, "linec");
	u8[] max_row_text;
	u8[][] line_counts = ScratchAlloc!(u8[])(end_row-start_row);

	u64 width = u64(end_row.toChars().length);

	if(line_counts.length > 0)
	{
		for(u64 i = 0; start_row+i < end_row; i += 1)
		{
			line_counts[i] = ScratchName(width, parent_id, start_row+i);
		}

		max_row_text = ScratchAlloc!(u8)(width);
		max_row_text[] = cast(u8)'0';
	}
	
	UISize s_x = UISize(ST.Pixels, CalcTextWidth(max_row_text) + (padding*2.0));
	UISize s_y = UISize(ST.Percentage, 1.0);

	UIItem* item = Container(ScratchName(parent_id, "linec"), s_x, s_y,	A2D.Y, UIF.DrawBackground);
	{
		UIItem* inner = Container(ScratchName(parent_id, "lcinner"), s_x, s_y, A2D.Y, UIF.None);
		{
			for(u64 i = 0; i < line_counts.length; i += 1)
			{
				if(i == 0)
				{
					Push!("offset")(Vec2(0.0, offset));
					UIItem* line = Text(line_counts[i]);
					Pop!("offset");
				}
				else
				{
					UIItem* line = Text(line_counts[i]);
				}
			}
		}
		EndContainer();
	}
	EndContainer();

	Pop!("padding", "color");

	return item;
}

UIItem*
Container(u8[] text, UISize size_x, UISize size_y, Axis2D axis, UIF flags = UIF.None)
{
	Push!("layout_axis")(axis);

	UIItem* item = Get(text);
	BuildItem(item, size_x, size_y, flags);
	PushParent(item);

	Pop!("layout_axis");

	return item;
}

void
EndContainer()
{
	PopParent();
}

UIItem*
Text(u8[] text)
{
	UIItem* item = Get(text);
	f32 width = CalcTextWidth(item.key.text);
	BuildItem(item, UISize(ST.Pixels, width), UISize(ST.Pixels, 16.0), UIF.DrawText);
	return item;
}

void
SetPanelScroll(UIPanel* panel, i64 rows, f32 text_size)
{
	i64 low  = panel.prev_offset < panel.ed.buf.offset ? panel.prev_offset : panel.ed.buf.offset;
	i64 high = panel.prev_offset < panel.ed.buf.offset ? panel.ed.buf.offset + rows : panel.prev_offset + rows;

	panel.start_row = low;
	panel.end_row = high;

	f32 start = panel.anim.start_value;
	f32 end = panel.anim.end_value;

	f32 offset = (panel.prev_offset-panel.ed.buf.offset) * text_size;
	if(offset > 0.0)
	{
		end = -offset;
	}
	else
	{
		start = offset;
	}

	with(panel.anim)
	{
		start_value = start;
		end_value = end;

		start_time = 0.1;
		remaining_time = remaining_time > 0.05 ? 0.04 : 0.1;

		panel.prev_offset = panel.ed.buf.offset;
	}
}

f32
EaseOutExp(f32 v)
{
	return fabsf(1.0 - v) <= 0.00009 ? 1.0 : 1.0 - pow(2, -10 * v);
}

void
EditorView(UIPanel* panel)
{
	bool focused = panel == GetFocusedPanel();

	UICtx*  ctx  = GetCtx();
	Editor* ed   = panel.ed;

	Push!("border_thickness")(2.0);
	Push!("border_color")(focused ? HL_BORDER_COL : DEFAULT_BORDER_COL);

	UIItem* item = Panel(panel, UIF.Clickable|UIF.DeferredBorder);
	{
		Pop!("border_thickness", "border_color");

		Container(ScratchName(panel.id, "cntr"), UISize(ST.Percentage, 1.0), UISize(ST.Percentage, 1.0), A2D.X);
		{
			f32 text_size = 16.0;
			f32 height = 0.0;

			i64 rows = cast(i64)ceil(item.size.y/text_size);
			if(ed.buf.rows != rows)
			{
				ed.buf.rows = rows;
			}

			if(panel.prev_offset != ed.buf.offset)
			{
				SetPanelScroll(panel, rows, text_size);
			}

			f32 offset = 0.0;
			i64 line_offset = ed.buf.offset;
			i64 line_rows = rows;
			if(panel.anim.remaining_time > 0.0) with(panel.anim)
			{
				remaining_time -= g_delta;
				if(remaining_time < 0.0)
				{
					remaining_time = 0.0;
				}

				offset = Remap(remaining_time, 0.0, start_time, 0.0, 1.0);
				offset = Remap(EaseOutExp(offset), 0.0, 1.0, start_value, end_value);

				line_offset = panel.start_row;
				line_rows = panel.end_row-panel.start_row;
				GetLines(&ed.buf, &ed.linebufs, panel.start_row, panel.end_row-panel.start_row);

				if(remaining_time == 0.0)
				{
					start_value = end_value = start_time = 0.0;
					panel.start_row = panel.end_row = -1;
				}
			}
			else
			{
				GetLines(&ed.buf, &ed.linebufs, rows);
			}

			LineCounter(panel.id, &panel.ed.buf, offset, line_offset, line_offset+line_rows, focused);

			Container(ScratchName(panel.id, "lines"), UISize(ST.Percentage, 1.0), UISize(ST.Percentage, 1.0), A2D.Y);
			{
				I64Vec2 sel   = ed.buf.sel;
				if(sel.x > sel.y)
				{
					i64 t = sel.y;
					sel.y = sel.x;
					sel.x = t;
				}

				U64Vec2 pos   = VecPos(&ed.buf);
				SelectMode sm = ed.buf.sel_mode;
				u64 i         = 0;
				for(LineBuffer* buf = ed.linebufs.first; buf != null; buf = buf.next, i += 1)
				{
					if(buf == ed.linebufs.first)
					{
						Push!("offset")(Vec2(0.0, offset));
					}

					if(buf.text.length > 0)
					{
						i64 line_no = i+line_offset;
						if(pos.y == line_no && focused)
						{
							Push!("highlighted_char")(pos.x);
						}

						if(sm == SM.Line && line_no >= sel.x && line_no <= sel.y)
						{
						}

						TextPart* tp = WrappedTextLine(buf.text, panel.id, text_size, line_no, buf.style);
						height += (text_size * tp.count);
						if(TextClicked(tp))
						{
							SetFocusedPanel(panel);
						}
					}
					Push!("highlighted_char")(-1);

					if(buf == ed.linebufs.first)
					{
						Pop!("offset");
					}
				}
			}
			EndContainer();
		}
		EndContainer();
	}
	EndPanel();

	if(item.signal & UIS.Clicked)
	{
		SetFocusedPanel(panel);
	}
}

bool
TextClicked(TextPart* text_part)
{
	bool result = false;

	alias result res;

	for(TextPart* tp = text_part; !Nil(tp.item); tp = tp.next)
	{
		if(tp.item.signal & UIS.Clicked)
		{
			res = true;
			break;
		}
	}

	return res;
}

TextPart*
WrappedTextLine(u8[] text, u8[] parent_id, f32 text_size, u64 line_no, TS[] style = [])
{
	Push!("color")(Vec4(1.0));
	Push!("text_size")(text_size);

	UIItem* parent = PeekParent();
	TextPart* part = ScratchAlloc!(TextPart)();
	part.item = g_UI_NIL;

	TextPart* tp = part;
	Node!(TextBuffer)* lines = MakeMultiline(text, parent.size.x, parent_id, line_no, style);
	for(Node!(TextBuffer)* line = lines; line != null; line = line.next, tp = tp.next)
	{
		part.count += 1;

		tp.item = Get(line.value.text);
		tp.next = ScratchAlloc!(TextPart)();
		tp.next.item = g_UI_NIL;

		tp.item.token_styles = line.value.style;

		Signal(tp.item);

		if(tp.item.signal & UIS.Clicked)
		{
			ClickedCharIndex(tp.item);
		}

		BuildItem(tp.item, UISize(ST.Percentage, 1.0), UISize(ST.Pixels, text_size), UIF.DrawText|UIF.Clickable|UIF.Draggable);
	}

	Pop!("color");

	return part;
}

UIItem*
TextInput(u8[] text, u8[] hash, f32 x, f32 y, f32 w_pct, f32 h, f32 text_size, Vec4[4] bg_cols, Vec4[4] border_col)
{
	UIItem* input = Get(ScratchName(text, hash));

	Push!("padding")(Vec2(6.0, (h-(text_size))*0.5)-2.0);

	Push!("offset")(Vec2(x, y));
	Push!("border_color")(border_col);
	Push!("color")(bg_cols);

	Container(ScratchName(hash, "_cntr"), UISize(ST.Percentage, w_pct), UISize(ST.Pixels, h), A2D.Y, UIF.DrawBackground|UIF.DrawBorder);

	Pop!("color", "offset", "border_color");

	Push!("color")(Vec4(1.0));
	Push!("text_size")(text_size);

	BuildItem(input, UISize(ST.Percentage, 1.0), UISize(ST.Pixels, text_size), UIF.DrawText);

	EndContainer();

	Pop!("color", "padding");

	return input;
}

void
CommandPalette(CmdPalette* cmd)
{
	u8[]   text    = cmd.buffer[0 .. cmd.icount];
	u8[][] options = cmd.opt_strs;

	Vec2 size = RootSize();

	f32 x = size.x*0.15;
	f32 y = size.y*0.1;
	f32 w = size.x*0.7;
	f32 h = size.y*0.8;

	Push!("color")(CMD_PALETTE_COL);

	UIItem* window = Window(CastStr!(u8)("##cmd_palette"), x, y, w, 40.0*(options.length+1));

	Push!("corner_radius")(2.0);
	Push!("edge_softness")(0.1);
	Push!("border_thickness")(2.0);

	UIItem* text_box = TextInput(text, CastStr!(u8)("###cmd_palette_input"), 0.0, 0.0, 1.0, 40.0, 16.0, CMD_PALETTE_INPUT_COL, CMD_INPUT_BORDER_COL);

	for(u64 i = 0; i < options.length; i += 1)
	{
		TextInput(
				options[i], 
				ScratchName("###cmd%04s", 10, i), 
				0.0, 
				0.0, 
				1.0, 
				40.0, 
				16.0, 
				i == cmd.selected ? CMD_PALETTE_INPUT_HL : CMD_PALETTE_INPUT_COL,
				i == cmd.selected ? CMD_INPUT_BORDER_HL  : CMD_INPUT_BORDER_COL
			);
	}

	EndWindow();

	Pop!("border_thickness", "corner_radius", "edge_softness", "color");
}

UIItem*
Window(u8[] id, f32 x, f32 y, f32 w, f32 h)
{
	UIItem* item = Get(id);

	Push!("offset")(Vec2(x, y));

	BuildItem(item, UISize(ST.Pixels, w), UISize(ST.Pixels, h), UIF.Window|UIF.DrawBackground|UIF.DrawBorder);

	Pop!("offset");

	PushParent(item);

	return item;
}

void
EndWindow()
{
	PopParent();
}

void
EndPanel()
{
	PopParent();
}

void
DrawPanels(UIPanel* panel)
{
	if(!Nil(panel))
	{
		debug
		{
			import core.stdc.math : fabsf;

			if(!Nil(panel.first))
			{
				f32 total = 1.0;
				for(UIPanel* child = panel.first; !Nil(child); child = child.next)
				{
					total -= child.pct;
				}

				if(total > 0.00009)
				{
					Logf("%s failed with %f", cast(char[])panel.id, total);
					assert(false);
				}
			}
		}

		if(panel.ed)
		{
			EditorView(panel);
		}
		else
		{
			Panel(panel);
		}

		DrawPanels(panel.first);

		if(panel.ed == null)
		{
			EndPanel();
		}

		DrawPanels(panel.next);
	}
}

UIPanel*
Recurse(UIPanel* panel)
{
	UIPanel* result = g_UI_NIL_PANEL;
	if(!Nil(panel.first))
	{
		result = panel.first;
	}
	else if(!Nil(panel.next))
	{
		result = panel.next;
	}
	else for(UIPanel* p = panel.parent; !Nil(p); p = p.parent)
	{
		if(!Nil(p.next))
		{
			result = p.next;
			break;
		}
	}

	return result;
}

bool
CheckPanelBounds(f32 pct)
{
	return pct >= 0.0 && pct <= 1.0;
}

void
PushPanel(UIPanel* parent, UIPanel* panel)
{
	DLLPush(parent, panel, g_UI_NIL_PANEL);
	panel.parent = parent;
}

void
InsertPanel(UIPanel* parent, UIPanel* prev, UIPanel* panel)
{
	DLLInsert(parent, panel, prev, g_UI_NIL_PANEL);
	panel.parent = prev.parent;
}

void
InitWidgets()
{
	g_UI_NIL_PANEL = cast(UIPanel*)&g_ui_nil_panel;
	g_widget_ctx.parent = g_UI_NIL_PANEL;
}

void
SetFocusedPanel(UIPanel* panel)
{
	if(!CheckNil(g_UI_NIL_PANEL, panel))
	{
		g_widget_ctx.focused_panel = panel;
		SetFocus(Get(panel.id));
	}
}

UIPanel*
GetFocusedPanel()
{
	return Nil(g_widget_ctx.focused_panel) ? g_UI_NIL_PANEL : g_widget_ctx.focused_panel;
}

bool
Nil(UIPanel* panel)
{
	return panel == null || panel == g_UI_NIL_PANEL;
}
