import dlib;
import dlib.util : HTPush = Push;
import dlib.math : Clamp;

import vulkan;
import vulkan : RendererGetExtent = GetExtent;
import buffer;
import parsing;
import editor;

import std.stdio;
import std.typecons : Tuple, tuple;
import std.math.traits : isNaN;
import std.math.rounding : ceil, floor;
import std.math.exponential : pow;
import std.math.remainder : fmod;
import std.algorithm.comparison : clamp;
import std.format : sformat;
import core.stdc.string : memset;
import core.stdc.math : fabsf, cbrtf;
import std.conv;
import core.stdc.stdio : sprintf;

/****** IMPORTANT NOTES *********

	- Display string should be separate from key string
	- If key is 0 then its transient and will be discarded next frame
	- Events should be created then processed instead of directly from inputs
  - Little mode ui thing in the corner thats colored and it slides the colour out (left to right) when mode is changed

*********************************/

enum Vec4 BORDER_COL     = SRGBVec4(0.254, 0.254, 0.266, 1.0);
enum Vec4 BORDER_HL_COL  = SRGBVec4(0.035, 0.549, 0.824, 1.0);
enum Vec4 TEXT_COL       = SRGBVec4(1.0);
enum Vec4 TEXT_HL_COL    = SRGBVec4(0.0, 0.0, 0.0, 1.0);
enum Vec4 BG_COL         = Vec4(0.18, 0.18, 0.18, 1.0);
enum Vec4 BG_HL_COL      = Vec4(0.160, 0.533, 0.803, 1.0);
enum Vec4 HL_BORDER_COL  = Vec4(0.172, 0.643, 0.988, 1.0);
enum Vec4 CMD_COL        = Vec4(0.22, 0.22, 0.22, 1.0);
enum Vec4 CMD_BORDER_COL = Vec4(0.48, 0.48, 0.48, 1.0);
enum Vec4 CMD_INPUT_COL  = Vec4(0.130, 0.420, 0.640, 1.0);
enum Vec4 GREY           = Vec4(0.80, 0.80, 0.80, 1.0);
enum Vec4 WHITE          = HexCol(0xFFFFFF);
enum Vec4 BLUE           = HexCol(0x4EA9FF);
enum Vec4 RED            = HexCol(0xFF3268);
enum Vec4 YELLOW         = HexCol(0xF7C443);
enum Vec4 OPT_COL        = Vec4(0.22, 0.22, 0.22, 1.0);
enum Vec4 OPT_ALT_COL    = Vec4(0.31, 0.31, 0.31, 1.0);

const u64  VERTEX_MAX_COUNT = 10000;
const Vec2 CLICK_BUFFER     = Vec2(3.0, 3.0);
const u32  FONT_SIZES       = 24;
const u32  ATLAS_DIMENSION  = 512;

// TODO: add setting
const f32 SCROLL_SPEED       = 48.0;
const f32 LINE_COUNT_PADDING = 4.0;
const f32 CORNER_RADIUS      = 8.0;

//u8[] FONT_BYTES     = Arr!(u8)(import("pc-9800.ttf"));
//u8[] FONT_BYTES     = Arr!(u8)(import("NuberNextCondensed-DemiBold.otf"));
u8[] FONT_BYTES          = cast(u8[])import("jetbrains-mono/JetBrainsMono-Regular.ttf");
u8[] VERTEX_BYTES        = cast(u8[])import("gui.vert.spv");
u8[] FRAGMENT_BYTES      = cast(u8[])import("gui.frag.spv");

const SEP_FLAGS   = UIF.Drag|UIF.Priority|UIF.TargetLeniency;
const PANEL_FLAGS = UIF.Click|UIF.Drag;

const UI_COUNT = 5000;

__gshared Style SEP_STYLE = {
	col:    Vec4(0.0, 0.0, 0.0, 1.0),
	hl_col: Vec4(0.2, 0.2, 0.2, 1.0),
};

__gshared Style SCROLLBAR_STYLE = {
	col:    GREY,
	hl_col: WHITE,
	corner_radius: Vec4(4.0f),
	edge_softness: 1.0,
};

__gshared Style CURSOR_STYLE = {
	col:    Vec4(1.0),
	hl_col: Vec4(1.0),
};

__gshared Style PANEL_STYLE = {
	col:              BG_COL,
	hl_col:           BG_COL,
	border_col:       BORDER_COL,
	border_hl_col:    BORDER_COL,
	corner_radius:    Vec4(CORNER_RADIUS),
	border_thickness: 1.0,
	edge_softness:    1.0,
};

__gshared Style CMD_STYLE = {
	col:              BG_COL,
	hl_col:           BG_HL_COL,
	border_col:       CMD_BORDER_COL,
	border_hl_col:    BORDER_HL_COL,
	corner_radius:    Vec4(CORNER_RADIUS),
	border_thickness: 1.5,
	edge_softness:    1.0,
};

enum Style CMD_OPT_STYLE_0 = {
	col:    OPT_COL,
	hl_col: BG_HL_COL,
};

enum Style CMD_OPT_STYLE_1 = {
	col:    OPT_ALT_COL,
	hl_col: BG_HL_COL,
};

__gshared Style[2] CMD_OPT_STYLES = [
	CMD_OPT_STYLE_0,
	CMD_OPT_STYLE_1,
];

enum Axis2D
{
	X,
	Y,
	Max,
} alias A2D = Axis2D;

enum UIEvent
{
	None        = 0,
	Click       = 1<<0,
	DragRelease = 1<<1,
	Drag        = 1<<2,
	DragStart   = 1<<3,
	Press       = 1<<4,
	Scroll      = 1<<5,
	AnimatePos  = 1<<6,
} alias UIE = UIEvent;

struct UIInput
{
	UIEvent  type;
	UIInput* next, prev;
	Modifier md;
	union
	{
		struct
		{
			Input  key;
			string text;
		};
		IVec2 rel;
		IVec2 pos;
		i32   scroll;
	}
}

enum UISyntaxHighlight
{
	None,
	D,
	Max,
} alias UISH = UISyntaxHighlight;

enum ItemStateFlags
{
	None      = 0,
	Hot       = 1<<0,
	Ready     = 1<<1,
	Active    = 1<<2,
	SetHot    = 1<<3,
	SetReady  = 1<<4,
	SetActive = 1<<5,
} alias ISF = ItemStateFlags;

struct PushConst
{
	Mat4 projection;
	Vec2 resolution;
	Vec2 aem_range;
	u32  atlas_index;
	f32  threshold_em;
	f32  antialias_per_em;
	f32  distance_range;
}

enum FO = FRAME_OVERLAP;
enum FS = FONT_SIZES;

struct FontSet
{
	FontAtlas  atlas;
	u32        size;
	u32        index;
	bool       texture_loaded;
	bool[FO]   descriptor_written;

	alias atlas this;
}

struct Style
{
	Vec4 col;
	Vec4 hl_col;
	Vec4 border_col;
	Vec4 border_hl_col;
	Vec4 corner_radius;
	f32  border_thickness = 0.0;
	f32  edge_softness    = 0.0;
}

enum UIFlags : u64
{
	None,
	Click           = 1<<0,
	Drag            = 1<<1,
	Priority        = 1<<2,
	TargetLeniency  = 1<<3,
} alias UIF = UIFlags;

struct UIItem
{
	UIKey    key;
	UIFlags  flags;
	Rect     rect;
	Vec2     size;
	Vec2     scroll_offset;
	Vec2     scroll_target;
	Vec2     scroll_size;
	Vec2     target_pos;
	Axis2D   axis;
	u64      last_frame;
	f32      ready_t;  // Item visible
	f32      hot_t;    // Item to be interacted with (e.g. hover)
	f32      active_t; // Item active (retained focus)

	UIItem*  draw_next; // Stacks
	UIItem*  free_next; 

	alias rect this;
}

struct UIBuffer
{
	MappedBuffer!(Vertex) m_vtx;
	MappedBuffer!(u32)    m_idx;
	Vertex[]              vtx;
	u32[]                 idx;
	u32                   count;
	u32                   vtx_offset;
}

struct VPos
{
	Vec2 start;
	Vec2 end;
}

struct Vertex
{
	Vec4    cols;
	Vec4    corner_radius; // x = top left, y = top right, z = bottom left, w = bottom right
	union
	{
		VPos[2] pos;
		struct
		{
			Vec2 dst_start;
			Vec2 dst_end;
			Vec2 src_start;
			Vec2 src_end;
		};
	};
	f32 border_thickness;
	f32 edge_softness;
	f32 raised;
	u32 has_texture;
}

struct UIPanel
{
	UIPanel* first, last, next, prev, parent;
	UIPanel* free_next;
	UIItem*  item;
	f32      pct;
	u32      id;
	Editor*  ed;
	bool     move_text_with_cursor;

	alias item this;
}

struct Window
{
	Window* next, prev;
	string  label;
	u32     x, y, w, h;
	Editor* ed;
}

enum SettingType
{
	Value,
	Slider,
	Dropdown,
	Toggle,
}

Attribute[10] attributes = [
	{ binding: 0, location: 0,  format: FMT.RGBA_F32, offset: Vertex.cols.offsetof               },
	{ binding: 0, location: 1,  format: FMT.RGBA_F32, offset: Vertex.corner_radius.offsetof      },
	{ binding: 0, location: 2,  format: FMT.RG_F32,   offset: Vertex.dst_start.offsetof          },
	{ binding: 0, location: 3,  format: FMT.RG_F32,   offset: Vertex.dst_end.offsetof            },
	{ binding: 0, location: 4,  format: FMT.RG_F32,   offset: Vertex.src_start.offsetof          },
	{ binding: 0, location: 5,  format: FMT.RG_F32,   offset: Vertex.src_end.offsetof            },
	{ binding: 0, location: 6,  format: FMT.R_F32,    offset: Vertex.border_thickness.offsetof   },
	{ binding: 0, location: 7,  format: FMT.R_F32,    offset: Vertex.edge_softness.offsetof      },
	{ binding: 0, location: 8,  format: FMT.R_F32,    offset: Vertex.raised.offsetof             },
	{ binding: 0, location: 9,  format: FMT.R_U32,    offset: Vertex.has_texture.offsetof        },
];

union Rect
{
	Vec2[2] v;
	struct { Vec2 p0, p1; alias p0 pos; };
}

alias UIHash = u64;

alias UIPair = KVPair!(UIHash, UIItem*);

struct UIKey
{
	string text, hash_text;
	u64    hash;

	bool opEquals(UIKey key)           => this.hash == key.hash;
	bool opCast(T)() if(is(T == bool)) => this.hash != 0;
}

/*
 "atlas": {
    "type": "mtsdf",
    "distanceRange": 2,
    "distanceRangeMiddle": 0,
    "size": 32.5,
    "width": 216,
    "height": 216,
    "yOrigin": "bottom"
  },
  "metrics": {
    "emSize": 1,
    "lineHeight": 1.3200000000000001,
    "ascender": 1.02,
    "descender": -0.29999999999999999,
    "underlineY": -0.17999999999999999,
    "underlineThickness": 0.050000000000000003
  },
  "glyphs": [
    {
      "unicode": 32,
      "advance": 0.59999999999999998
    },
    {
      "unicode": 33,
      "advance": 0.59999999999999998,
      "planeBounds": {
        "left": 0.19230769230769232,
        "bottom": -0.046153846153846149,
        "right": 0.40769230769230769,
        "top": 0.78461538461538449
      },
      "atlasBounds": {
        "left": 19.5,
        "bottom": 151.5,
        "right": 26.5,
        "top": 178.5
      }
    },

*/

enum bool KeyType(T)        = (StringType!(T) || is(T == UIKey) || is(T == const(UIKey)));
enum bool ItemAndKeyType(T) = (KeyType!(T)    || is(T == UIItem*));

void
InitUI(Ctx* ctx)
{
	version(ENABLE_RENDERER)
	{
		version(linux)
		{
			PlatformHandles handles = {
				display: ctx.window.display,
				window:  ctx.window.window,
			};
		}

		version(Windows)
		{

		}
	}

	Arena arena = CreateArena(MB(4));
	
	UIBuffer[FRAME_OVERLAP] buffers;

	ctx.items         = CreateHashTable!(UIHash, UIItem*)(12);
	ctx.arena         = arena;
	ctx.temp_arena    = CreateArena(MB(1));
	ctx.font          = OpenFont(cast(u8[])FONT_BYTES);
	ctx.tab_width     = 2;
	ctx.syntax_colors = [DEFAULT_COLORS, SYNTAX_COLORS];

	assert(ctx.font);

	version(ENABLE_RENDERER)
	{
		VulkanBuildInfo build_info = {
			platform_handles: handles,
			application_name: "ScratchPad",
			engine_name:      "ScratchPad",
		};

		InitRenderer(build_info, MB(16), MB(8));

		DescLayoutBinding[2] layout_bindings = [
			{ binding: 0, descriptorType: DT.Image,   descriptorCount: FONT_SIZES, stageFlags: SS.All },
			{ binding: 1, descriptorType: DT.Sampler, descriptorCount: 1,          stageFlags: SS.All },
		];

		ctx.desc_set_layout = CreateDescSetLayout(layout_bindings);
		ctx.pipeline_layout = CreatePipelineLayout(ctx.desc_set_layout, PushConst.sizeof);

		for(u64 i = 0; i < FRAME_OVERLAP; i += 1)
		{
			ctx.buffers[i].m_vtx = CreateMappedBuffer!(Vertex)(BT.Vertex, VERTEX_MAX_COUNT);
			ctx.buffers[i].m_idx = CreateMappedBuffer!(u32)(BT.Index, 6);
			ctx.buffers[i].vtx   = ctx.buffers[i].m_vtx.data;
			ctx.buffers[i].idx   = ctx.buffers[i].m_idx.data;
			ctx.buffers[i].idx[0 .. $] = [0, 1, 2, 2, 1, 3];
			ctx.desc_sets[i]     = AllocDescSet(ctx.desc_set_layout);
			ctx.str_arenas[i]    = CreateArena(MB(1));

			ctx.slug_vertex_buffers[i]   = CreateMappedBuffer!(SlugVertex)(BT.Vertex, 1000);
			ctx.slug_index_buffers[i]    = CreateMappedBuffer!(u32)(BT.Index, 1000);
			ctx.slug_buffers[i].vertices = ctx.slug_vertex_buffers[i].data;
			ctx.slug_buffers[i].indices  = ctx.slug_index_buffers[i].data;
		}

		GfxPipelineInfo ui_info = {
			vertex_shader:     cast(u8[])VERTEX_BYTES,
			frag_shader:       cast(u8[])FRAGMENT_BYTES,
			input_rate:        IR.Instance,
			input_rate_stride: Vertex.sizeof,
			layout:            ctx.pipeline_layout,
			vertex_attributes: attributes,
			src_color:         BF.SrcAlpha,
			dst_color:         BF.OneMinusSrcAlpha,
			color_op:          BO.Add,
			src_alpha:         BF.One,
			dst_alpha:         BF.Zero,
			alpha_op:          BO.Add,
		};

		CreateGraphicsPipeline(&ctx.pipeline, &ui_info);

		for(u32 i = 0; i < ctx.font_descs.length; i += 1)
		{
			CreateDescriptor(&ctx.font_descs[i], DescInfo(
				type:    DT.Image,
				format:  FMT.RGBA_UNORM,
				usage:   IU.Texture,
				w:       ATLAS_DIMENSION,
				h:       ATLAS_DIMENSION,
				ch:      4,
				binding: 0,
				index:   i,
			));

			ctx.font_sets[i].index = i;
		}

		CreateDescriptor(&ctx.sampler, DescInfo(
			type:              DT.Sampler,
			min_filter:        Filter.Nearest,
			mag_filter:        Filter.Nearest,
			enable_anisotropy: true,
			mipmap_mode:       MipmapMode.Linear,
		 	binding:           1,
		));

		u8[512*512*4] white_tex = 255;
		for(u64 i = 0; i < ctx.font_descs.length; i += 1)
		{
			Transfer!(true)(&ctx.font_descs[i], white_tex);
		}

		for(u64 i = 0; i < FRAME_OVERLAP; i += 1)
		{
			Write(ctx.desc_sets[i], ctx.font_descs);
			Write(ctx.desc_sets[i], &ctx.sampler);
		}	

		SetClearColors([0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 0.0, 0.0]);

		FontSet* atlas = GetFontSet(14);

		LoadFontSet();
	}
}

T*
GetOrAlloc(string next_member = "free_next", T)(Arena* arena, T** top, T* nil, bool* allocated)
{
	import std.format : format;

	enum top_next    = format("(*top).%s", next_member);
	enum result_next = format("result.%s", next_member);

	T* result = nil;
	if(!CheckNil(nil, *top))
	{
		result = *top;
		*top   = mixin(top_next);
	}
	else
	{
		result     = Alloc!(T)(arena);
		*allocated = true;
	}

	mixin(result_next) = nil;

	return result;
}

void
PushFree(string next_member = "free_next", T)(T** top, T* node)
{
	import std.format : format;

	enum node_next = format("node.%s", next_member);

	mixin(node_next) = *top;
	*top             = node;
}

f32
GetLineHeight(u32 size)
{
	return GetFontSet(size).line_height;
}

FontSet*
GetFontSet(u32 size)
{
	Ctx* ctx = GetCtx();

	FontSet* fs = null;
	for(u64 i = 0; i < ctx.font_sets_used; i += 1)
	{
		if(size == ctx.font_sets[i].size)
		{
			fs = &ctx.font_sets[i];
			break;
		}
	}
	
	if(!fs)
	{
		fs = &ctx.font_sets[ctx.font_sets_used++];
		ctx.fonts_to_load = true;

		assert(ctx.font);

		fs.atlas              = CreateAtlas(&ctx.arena, ctx.font, size, UVec2(ATLAS_DIMENSION));
		fs.size               = size;
		fs.texture_loaded     = false;
		fs.descriptor_written = false;
	}

	return fs;
}

void
LoadFontSet()
{
	Ctx* ctx = GetCtx();
	u64  fi  = ctx.f_idx;

	for(u64 i = 0; i < ctx.font_sets_used; i += 1)
	{
		FontSet* fs = ctx.font_sets.ptr + i;
		if(!fs.texture_loaded)
		{
			Transfer!(true)(&ctx.font_descs[i], fs.atlas.data);
			fs.texture_loaded = true;
		}

		if(!fs.descriptor_written[fi])
		{
			Write(ctx.desc_sets[fi], ctx.font_descs);
			fs.descriptor_written[fi] = true;
		}
	}

	ctx.fonts_to_load[fi] = false;
}

void
Scissor(Ctx* ctx, Rect rect, Style* style)
{
	DrawUI(ctx);

	f32 b = style.border_thickness + style.border_thickness*style.edge_softness;

	Vec2 res = ctx.pc.resolution;

	i32 x = cast(i32)clamp(floor(rect.p0.x+b),     0.0, res.x);
	i32 y = cast(i32)clamp(floor(rect.p0.y+b),     0.0, res.y);
	i32 w = cast(i32)clamp(floor(rect.p1.x-b) - x, 0.0, res.x);
	i32 h = cast(i32)clamp(floor(rect.p1.y-b) - y, 0.0, res.y);

	SetScissor(x, y, w, h);
}

void
EndScissor(Ctx* ctx)
{
	DrawUI(ctx);
	ResetScissor();
}

void
Panel(Ctx* ctx, UIPanel* panel)
{
	panel.item      = MakeItem!(PANEL_FLAGS)("###panel_%s", panel.id);
	Axis2D p_axis   = Nil(panel.parent) ? A2D.X       : panel.parent.axis;
	Vec2   p_size   = Nil(panel.parent) ? GetExtent() : panel.parent.size;
	Vec2   p_bounds = Nil(panel.parent) ? GetExtent() : panel.parent.p1;

	const f32 SEP_SIZE = 3.0f;

	panel.pos = 0.0;
	if(!Nil(panel.prev))
	{
		panel.p0           = panel.prev.p0;
		panel.p0.v[p_axis] = panel.prev.p1.v[p_axis]+SEP_SIZE;
	}
	else if(!Nil(panel.parent))
	{
		panel.pos = panel.parent.rect.pos;
	}

	panel.size            = p_size;
	panel.size.v[p_axis] *= panel.pct;
	panel.p1              = Clamp(panel.pos+panel.size, Vec2(0.0), p_bounds);

	Axis2D axis = panel.axis;
	if(panel.ed || Nil(panel.first))
	{
		assert(Nil(panel.first) && Nil(panel.last), "Editor panel must have no children");

		if(panel.ed)
		{
			FontSet* fs = GetFontSet(10);

			// Inputs
			if(ctx.state == ES.NormalMode || ctx.state == ES.InputMode)
			{
				bool hovered = ctx.hover_key == panel.key;
				bool focused = ctx.focus_key == panel.key && ctx.focused_panel == panel;
				panel.move_text_with_cursor  |= HandleInputs(panel, &ctx.events, hovered, focused);
			}
			else
			{
				panel.move_text_with_cursor = false;
			}

			panel.scroll_target.y = cast(f32)(panel.ed.line_offset)*fs.line_height;

			f32 max_px    = panel.ed.buf.line_count*fs.line_height;
			f32 offset    = -(panel.item.scroll_offset.y%fs.line_height);
			u64 start_ln  = cast(u64)(floor(panel.scroll_offset.y/fs.line_height));
			const f32 PAD = 4.0f;

			// Line counter

			u64 lc_chars = panel.ed.buf.line_count.toChars().length;
			f32 lc_width = PAD*2.0 + cast(f32)(lc_chars)*fs.max_advance;

			Style style = PANEL_STYLE;
			style.corner_radius.yw = 0.0;

			Rect[2] rects = Split(panel.rect, lc_width, A2D.X);

			Rect line_count_rect = rects[0];
			Rect text_view_rect  = rects[1];

			DrawRect(ctx, line_count_rect, &style);
			Scissor(ctx, line_count_rect, &style);

			Rect text_rect = Offset(Pad(line_count_rect, PAD), 0.0, offset);

			f32 panel_height     = Size!(A2D.Y)(line_count_rect);
			panel.ed.line_height = cast(i64)ceil(panel_height/fs.line_height);
			u64 max_ln           = Min(panel.ed.line_height + start_ln, panel.ed.buf.line_count);

			for(u64 i = start_ln; i < max_ln; i += 1)
			{
				DrawText(ctx, EdScratchf("%s", i+1), Vec4(1.0), fs, text_rect, TA.Right);
				text_rect.p0.y += fs.line_height;
			}

			EndScissor(ctx);
			DrawBorder(ctx, line_count_rect, &style);

			// Editor

			style.corner_radius = Vec4(0.0, CORNER_RADIUS, 0.0, CORNER_RADIUS);
			
			DrawRect(ctx, text_view_rect, &style);
			Scissor(ctx, text_view_rect, &style);

			text_rect = Pad(text_view_rect, PAD);

			I64Vec2     cursor_coords = VecPos(&panel.ed.buf);
			LineBuffer* current_lbuf  = GetLine(&panel.ed.buf, cursor_coords.y);
			UIItem*     cursor        = MakeItem("###cursor_%s", panel.id);
			if(ctx.focused_panel == panel)
			{
				i64 y_pos = cursor_coords.y - panel.ed.line_offset;

				cursor.size.x = Active(ES.InputMode) ? 2.0 : fs.max_advance;
				cursor.size.y = fs.line_height;

				cursor.target_pos.y = text_rect.p0.y + (y_pos*fs.line_height);
				cursor.target_pos.x = text_rect.p0.x + CalcTextWidth(current_lbuf.text[0 .. cursor_coords.x], fs);

				AnimatePos(cursor);

				if(cursor.p0 == cursor.target_pos)
				{
					panel.move_text_with_cursor = false;
				}

				DrawRect(ctx, cursor.rect, &CURSOR_STYLE);
			}

			text_rect.p0.y += offset;

			u64 i = start_ln;

			if(ctx.focused_panel == panel)
			{
				if(Active(ES.InputMode) && panel.move_text_with_cursor)
				{
					for(LineBuffer* lbuf = GetLine(&panel.ed.buf, i); i < max_ln && !CheckNil(g_NIL_LINE_BUF, lbuf); lbuf = GetLine(&panel.ed.buf, ++i))
					{
						if(i == cursor_coords.y)
						{
							string       str = Str(lbuf.text[0 .. cursor_coords.x]);
							TokenStyle[] tks = lbuf.style[0 .. cursor_coords.x];

							DrawText(ctx, str, tks, fs, text_rect, TA.Left);

							Rect split_rect = text_rect;
							split_rect.p0.x = cursor.p0.x;

							str = Str(lbuf.text[cursor_coords.x .. $]);
							tks = lbuf.style[cursor_coords.x .. $];

							DrawText(ctx, str, tks, fs, split_rect, TA.Left);
						}
						else
						{
							DrawText(ctx, lbuf.text, lbuf.style, fs, text_rect, TA.Left);
						}

						text_rect.p0.y += fs.line_height;
					}
				}
				else if(!Active(ES.InputMode))
				{
					DrawLines!(true)(ctx, panel, start_ln, max_ln, text_rect, fs, cursor_coords);
				}
				else 
				{
					DrawLines(ctx, panel, start_ln, max_ln, text_rect, fs);
				}
			} 
			else 
			{
				DrawLines(ctx, panel, start_ln, max_ln, text_rect, fs);
			}

			ResetBuffer(&panel.ed.buf);

			EndScissor(ctx);
			DrawBorder(ctx, text_view_rect, &style);
		}
		else
		{
			DrawRect(ctx, panel.rect, &PANEL_STYLE);
			DrawBorder(ctx, panel.rect, &PANEL_STYLE);
		}
	}
	else for(UIPanel* p = panel.first; !Nil(p); p = p.next)
	{
		Panel(ctx, p);
		if(!Nil(p.next))
		{
			UIItem* sep = MakeItem!(SEP_FLAGS)("###sep_%s_%s", p.id, p.next.id);

			sep.size         = panel.size;
			sep.size.v[axis] = SEP_SIZE;

			sep.p0         = p.p0;
			sep.p0.v[axis] = p.p1.v[panel.axis];
			sep.p1         = sep.p0+sep.size;
	
			DrawRect(ctx, sep.rect, &SEP_STYLE);

			IVec2 drag;
			if(Dragged(ctx, sep, &drag))
			{
				f32 mov_pct = Remap(drag.v[axis], 0.0, p_size.v[axis], 0.0, 1.0);

				if(p.pct - mov_pct >= 0.05 && p.next.pct + mov_pct >= 0.05)
				{
					p.pct      -= mov_pct;
					p.pct       = clamp(p.pct, 0.0, 1.0);
					p.next.pct += mov_pct;
					p.next.pct  = clamp(p.next.pct, 0.0, 1.0);
				}
			}
		}
	}
}

void
DrawLines(bool hl = false)(Ctx* ctx, UIPanel* panel, u64 start_line, u64 end_line, Rect text_rect, FontSet* fs, I64Vec2 cursor_coords = I64Vec2(-1))
{
	u64 i = start_line;
	for(LineBuffer* lbuf = GetLine(&panel.ed.buf, i); i < end_line && !CheckNil(g_NIL_LINE_BUF, lbuf); lbuf = GetLine(&panel.ed.buf, ++i))
	{
		static if(hl)
		{
			DrawText(ctx, lbuf.text, lbuf.style, fs, text_rect, TA.Left, cursor_coords.y == i ? cursor_coords.x : -1);
		}
		else
		{
			DrawText(ctx, lbuf.text, lbuf.style, fs, text_rect, TA.Left);
		}
		text_rect.p0.y += fs.line_height;
	}
}

struct SearchWindowCtx(T)
{
	T[]              options;
	TextInputBuffer* text_input;
	i64*             selected;
	i64              clicked;
	bool             scroll_to_selected;
}

void
SearchInputWindow(T)(string hash_text, SearchWindowCtx!(T)* window_ctx, bool ready_cond) if(is(T == Command) || is(T == string))
{
	Ctx* ctx    = GetCtx();
	Vec2 extent = GetExtent();
	f32  x      = extent.x * 0.1;
	f32  y      = extent.y * 0.2;
	f32  w      = extent.x * 0.8;
	f32  h      = extent.y * 0.6;

	T[] options = window_ctx.options;
	TextInputBuffer* text_input = window_ctx.text_input;
	i64* selected = window_ctx.selected;
	bool scroll_to_selected = window_ctx.scroll_to_selected;

	UIItem* window_item = MakeItem(hash_text);
	SetReady(window_item, ready_cond);
	if(window_item.ready_t > 0.0)
	{
		const f32 PAD = 2.0f;

		FontSet* fs = GetFontSet(14);
		static if(is(T == Command))
		{
			FontSet* sub_fs = GetFontSet(12);
		}

		window_item.p0   = Vec2(x,   y);
		window_item.p1   = Vec2(x+w, y+h);
		window_item.size = Vec2(w,   h);

		Style style = CMD_STYLE;

		Rect[2] rects = Split(window_item.rect, fs.line_height+PAD*4.0, A2D.Y);

		Rect text_input_rect = rects[0];
		Rect opt_window_rect = rects[1];

		style.corner_radius.zw = 0.0;

		DrawRect(ctx, text_input_rect, &style, 0.0, window_item.ready_t);
		DrawBorder(ctx, text_input_rect, &style, 0.0, window_item.ready_t);

		text_input_rect = Offset(text_input_rect, PAD*4.0, PAD*2.0);

		string input_str = text_input.length ? Str(text_input.data[0 .. text_input.length]) : "Type to search...";
		Vec4   input_col = text_input.length ? WHITE : GREY;
		DrawText(ctx, input_str, input_col, fs, text_input_rect, TA.Left, -1, window_item.ready_t);

		style.corner_radius = Vec4(0.0, 0.0, CORNER_RADIUS, CORNER_RADIUS);

		Rect border_rect = opt_window_rect;
		Rect inner_rect  = Pad(opt_window_rect, style.border_thickness);

		DrawRect(ctx, border_rect, &style, 0.0, window_item.ready_t);
		Scissor(ctx, inner_rect, &CMD_STYLE);

		f32 full_opt_height = fs.line_height+PAD*3.0f;
		static if(is(T == Command))
		{
			full_opt_height += sub_fs.line_height;
		}

		f32 opt_section_height = Size!(A2D.Y)(inner_rect);
		i64 start_opt_index    = cast(i64)floor(window_item.scroll_offset.y/full_opt_height);
		f32 opt_start_offset   = window_item.scroll_offset.y%full_opt_height;

		if(scroll_to_selected)
		{
			f32 opt_start_pos = (*selected) * full_opt_height;
			f32 opt_end_pos   = opt_start_pos + full_opt_height;

			if(window_item.scroll_target.y > opt_start_pos)
			{
				window_item.scroll_target.y = opt_start_pos;
			}
			else if(window_item.scroll_target.y+opt_section_height < opt_end_pos)
			{
				window_item.scroll_target.y = opt_end_pos-opt_section_height;
			}
		}

		Rect opt_rect  = inner_rect;
		opt_rect.p0.y -= opt_start_offset;

		UIItem* selected_opt_item = g_UI_NIL;
		if(start_opt_index < options.length) foreach(i; start_opt_index .. options.length)
		{
			Rect[2] opt_rects = Split(opt_rect, full_opt_height, A2D.Y);
			if(opt_rects[0].p0.y == opt_rects[0].p1.y)
			{
				break;
			}

			UIItem* opt_item = MakeItem!(UIF.Click|UIF.Priority)("###opt_%s", i);

			f32 opt_height = Size!(A2D.Y)(opt_rects[0]);
			opt_item.size  = Vec2(window_item.size.x, opt_height);
			opt_item.rect  = opt_rects[0];

			bool hovered = Hovered!(true)(opt_item, selected, i) || *selected == i;
			SetHot(opt_item, hovered);

			if(hovered && Clicked(ctx, opt_item))
			{
				window_ctx.clicked = *selected;
			}

			DrawRect(ctx, opt_rects[0], &CMD_OPT_STYLES[i%2], opt_item.hot_t, window_item.ready_t);

			if(opt_rects[0].p1.y - opt_rects[0].p0.y > PAD)
			{
				opt_rects[0].p0.y += PAD;
				opt_rects[0].p0.x += PAD*2.0f;

				static if(is(T == Command))
				{
					DrawText(ctx, options[i].name, WHITE, fs, opt_rects[0], TA.Left, -1, window_item.ready_t);
					opt_rects[0].p0.y += fs.line_height+PAD;
					DrawText(ctx, options[i].desc, GREY, sub_fs, opt_rects[0], TA.Left, -1, window_item.ready_t);
				}
				static if(is(T == string))
				{
					DrawText(ctx, options[i], WHITE, fs, opt_rects[0], TA.Left, -1, window_item.ready_t);
				}
			}

			opt_rect = opt_rects[1];
		}

		f32 total_cmd_height  = options.length*full_opt_height;
		f32 scrollable_height = clamp(total_cmd_height-opt_section_height, 0.0, f32.max);

		window_item.scroll_target.y = clamp(window_item.scroll_target.y, 0.0, scrollable_height);

		if(options.length && total_cmd_height > opt_section_height)
		{
			UIItem* scrollbar_item = MakeItem!(UIF.Drag|UIF.Priority|UIF.TargetLeniency)("%s_scroll", hash_text);

			f32 scrollbar_pct     = opt_section_height/total_cmd_height;
			f32 scrollbar_height  = opt_section_height*scrollbar_pct;
			f32 offset_pct        = Remap(window_item.scroll_target.y, 0.0, scrollable_height, 0.0, 1.0);
			f32 scrollable_range  = opt_section_height-scrollbar_height;
			f32 scrollbar_offset  = offset_pct*scrollable_range;
			f32 inner_width       = Size!(A2D.X)(inner_rect);

			Rect[2] scrollbar_rects = Split(inner_rect, inner_width-10.0f, A2D.X);

			scrollbar_item.rect       = scrollbar_rects[1];
			scrollbar_item.rect.p0.y += scrollbar_offset; 
			scrollbar_item.rect.p1.y  = scrollbar_item.rect.p0.y+scrollbar_height;
			scrollbar_item.rect.p1.x -= 2.0f;

			i32 scrolled;
			for(UIInput* i = ctx.events.first; !Nil(i) && ready_cond; i = i.next)
			{
				if(i.type == UIE.Scroll)
				{
					scrolled += i.scroll;
					DLLRemove(&ctx.events, i, g_UI_NIL_INPUT);
				}
			}

			IVec2 drag;
			if(Dragged(ctx, scrollbar_item, &drag) || scrolled != 0)
			{
				drag.y -= scrolled*40;

				f32 mov_pct = Remap(drag.y, 0.0, scrollable_range, 0.0, 1.0);
				offset_pct  -= mov_pct;
				offset_pct = clamp(offset_pct, 0.0, 1.0);
				window_item.scroll_target.y = scrollable_height * offset_pct;
			}

			DrawRect(ctx, scrollbar_item.rect, &SCROLLBAR_STYLE, 0.0f, window_item.ready_t);
		}

		EndScissor(ctx);

		DrawBorder(ctx, border_rect, &style, 0.0, window_item.ready_t);
	}
	else
	{
		window_item.scroll_target.y = 0.0;
	}
}

bool
SaveFileWindow(Ctx* ctx)
{
	bool exit;
	Editor* ed = ctx.focused_panel.ed;

	static bool init = false;
	if(!init)
	{
		ctx.cmd.exec_cmd_input.data[0 .. ed.file_name.length] = Arr!(u8)(ed.file_name[0 .. $]);
		ctx.cmd.exec_cmd_input.length = ed.file_name.length;
		init = true;
	}

	const PAD = 4.0f;

	Vec2 extent = GetExtent();

	// TODO: fix: This breaks when setting GetFontSet input to not 12, probably incorrect draw call boundaries (again)
	FontSet* fs = GetFontSet(12);
	
	f32 x = extent.x * 0.1;
	f32 w = extent.x * 0.8;

	f32 h = fs.line_height + PAD*2.0;
	f32 y = extent.y*0.5 - h;

	f32 inner_w = w - PAD*2.0;
	f32 inner_h = h - PAD*2.0;
	f32 inner_x = x + PAD;
	f32 inner_y = y + PAD;

	Rect window_rect = Rect(p0: Vec2(x, y), p1: Vec2(x+w, y+h));
	Rect inner_rect  = Rect(p0: Vec2(inner_x, inner_y), p1: Vec2(inner_x+inner_w, inner_y+inner_h));

	DrawRect(ctx, window_rect, &CMD_STYLE);
	DrawBorder(ctx, window_rect, &CMD_STYLE);

	string label = "Save As: ";
	DrawText(ctx, label, WHITE, fs, inner_rect, TA.Left, -1);

	inner_rect.p0.x += CalcTextWidth(label, fs);

	DrawRect(ctx, inner_rect, &CMD_STYLE);

	Rect text_rect  = inner_rect;
	text_rect.p0.x += PAD;
	DrawText(ctx, Str(ctx.cmd.exec_cmd_input), WHITE, fs, text_rect, TA.Left, -1);

	DrawBorder(ctx, inner_rect, &CMD_STYLE);

	for(UIInput* ev = ctx.events.first; ev && ctx.state == ES.RunCmd; ev = ev.next)
	{
		bool taken;
		if(ev.type == UIE.Press)
		{
			switch(ev.key) with(Input)
			{
				case Enter:
					{
						SaveFile(ed, Str(ctx.cmd.exec_cmd_input));
					} goto case;
				case Escape:
					{
						exit = taken = true;
					} break;
				default: taken = HandleTextInput(&ctx.cmd.exec_cmd_input, ev); break;
			}
		}

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

	if(exit)
	{
		init = false;
	}

	return exit;
}

bool
OpenFileWindow(Ctx* ctx)
{
	bool exit;
	CmdPalette* cmd = &ctx.cmd;

	if(!cmd.exec_cmd_opts.length && !cmd.exec_cmd_input.length)
	{
		cmd.exec_cmd_opts = ctx.file_names[0 .. $];
	}

	bool enter_hit, buffer_changed, selection_changed;
	if(ctx.state == ES.RunCmd)
	{
		HandleCmdMode(ctx, &enter_hit, &buffer_changed, &selection_changed);
		if(buffer_changed)
		{
			if(cmd.exec_cmd_input.length)
			{
				Reset(&cmd.exec_cmd_arena);
				cmd.exec_cmd_opts = Alloc!(string)(ctx.file_names.length);

				u64 option_count;
				string buffer_text = Str(cmd.exec_cmd_input);
				foreach(i; 0 .. ctx.file_names.length)
				{
					if(StrContains!(false)(ctx.file_names[i], buffer_text))
					{
						cmd.exec_cmd_opts[option_count++] = ctx.file_names[i];
					}
				}

				cmd.exec_cmd_opts = cmd.exec_cmd_opts[0 .. option_count];
			}
			else
			{
				cmd.exec_cmd_opts = ctx.file_names[0 .. $];
			}
		}

		if(buffer_changed || selection_changed)
		{
			cmd.exec_cmd_selected = clamp(cmd.exec_cmd_selected, 0, cmd.exec_cmd_opts.length ? cmd.exec_cmd_opts.length-1 : 0);
		}

		if(cmd.exec_cmd_opts.length)
		{
			string file_name = cmd.exec_cmd_opts[cmd.exec_cmd_selected];
			if(enter_hit && file_name.length)
			{
				OpenFile(ctx.focused_panel.ed, file_name);
				exit = true;
			}
		}
	}

	SearchWindowCtx!(string) window_ctx = {
		options:            cmd.exec_cmd_opts,
		text_input:         &cmd.exec_cmd_input,
		selected:           &cmd.exec_cmd_selected,
		scroll_to_selected: selection_changed,
		clicked:            -1,
	};

	i64 prev_selection = cmd.exec_cmd_selected;
	SearchInputWindow("###open_file", &window_ctx, ctx.state == ES.RunCmd);

	if(window_ctx.clicked != -1)
	{
		OpenFile(ctx.focused_panel.ed, cmd.exec_cmd_opts[window_ctx.clicked]);
		exit = true;
	}

	return exit;
}

void
CommandPalette(Ctx* ctx)
{
	CmdPalette* cmd = &ctx.cmd;

	bool enter_hit, buffer_changed, selection_changed;
	if(ctx.state == ES.CmdPalette)
	{
		HandleCmdMode(ctx, &enter_hit, &buffer_changed, &selection_changed);

		if(buffer_changed)
		{
			if(cmd.text_input.length)
			{
				u64 list_count;

				string buffer_text = Str(cmd.text_input);
				static foreach(i; 0 .. CMD_LIST.length)
				{
					if(StrContains!(false)(CMD_LIST[i].cmd, buffer_text) || StrContains!(false)(CMD_LIST[i].name, buffer_text))
					{
						cmd.cmd_arr[list_count++] = CMD_LIST[i];
					}
				}

				cmd.cmds = cmd.cmd_arr[0 .. list_count];
			}
			else
			{
				cmd.cmds = CMD_LIST[0 .. $];
			}
		}

		if(buffer_changed || selection_changed)
		{
			cmd.selected = clamp(cmd.selected, 0, cmd.cmds.length ? cmd.cmds.length-1 : 0);
		}
	}

	SearchWindowCtx!(Command) window_ctx = {
		options:            cmd.cmds,
		text_input:         &cmd.text_input,
		selected:           &cmd.selected,
		scroll_to_selected: selection_changed,
		clicked:            -1,
	};

	SearchInputWindow("###cmd_palette", &window_ctx, ctx.state == ES.CmdPalette);
}

void
AddPanel(UIPanel* target, Axis2D axis)
{
	if(Nil(target.parent) || target.parent.axis != axis)
	{
		UIPanel* first  = CreatePanel(target.ed);
		UIPanel* second = CreatePanel();

		target.axis = axis;
		target.ed   = null;

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

		first.parent = second.parent = target;
		first.pct    = second.pct    = 0.5;

		Logf("focus 1");
		Focus(second);
	}
	else
	{
		UIPanel* panel = CreatePanel();

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

		u32 count;
		for(UIPanel* s = target.parent.first; !Nil(s); s = s.next)
		{
			count += 1;
		}

		f32 reduce  = 1.0/count;
		f32 new_pct = 0.0;
		for(UIPanel* s = target.parent.first; !Nil(s); s = s.next)
		{
			f32 item_reduce = s.pct * reduce;
			new_pct += item_reduce;
			s.pct   -= item_reduce;
		}

		panel.pct = new_pct;

		Logf("focus 2");
		Focus(panel);
	}
}

Rect[2]
Split(bool pct = false)(Rect rect, f32 value, Axis2D axis)
{
	Rect[2] result = rect;

	static if(pct)
	{
		f32 size = (rect.p1.v[axis]-rect.p0.v[axis])*value;

		result[0].p1.v[axis]  = result[0].p0.v[axis]+size;
		result[1].p0.v[axis] += size;
	}
	else
	{
		f32 size = Min(value, rect.p1.v[axis]-rect.p0.v[axis]);

		result[0].p1.v[axis]  = result[0].p0.v[axis]+size;
		result[1].p0.v[axis] += size;
	}

	static foreach(i; 0 .. result.length)
	{
		if(result[i].p1.v[axis] < result[i].p0.v[axis])
		{
			result[i].p0.v[axis] = result[i].p1.v[axis];
		}
	}

	return result;
}

Vec2
ConvRectArgs(Args...)(Args args)
{
	static assert(Args.length == 1 || Args.length == 2, "px or px_x and px_y must be provided");
	static foreach(i; 0 .. Args.length)
	{
		static assert(is(Args[i]: f32));
	}

	static if(Args.length == 1)
	{
		Vec2 px_vec = Vec2(args[0]);
	}
	else
	{
		Vec2 px_vec = Vec2(args[0], args[1]);
	}

	return px_vec;
}

f32
Size(Axis2D axis)(Rect rect)
{
	return rect.p1.v[axis] - rect.p0.v[axis];
}

Rect
Offset(Args...)(Rect rect, Args args)
{
	Vec2 px = ConvRectArgs!(Args)(args);
	
	px.x = Min(px.x, (rect.p1.x-rect.p0.x));
	px.y = Min(px.y, (rect.p1.y-rect.p0.y));

	rect.p0 += px;

	return rect;
}

Rect
Pad(Args...)(Rect rect, Args args)
{
	Vec2 px = ConvRectArgs!(Args)(args);

	px.x = Min(px.x, (rect.p1.x-rect.p0.x)*0.5);
	px.y = Min(px.y, (rect.p1.y-rect.p0.y)*0.5);

	rect.p0 += Vec2(px.x, px.y);
	rect.p1 -= Vec2(px.x, px.y);

	return rect;
}

UIPanel*
CreatePanel(Editor* ed = null)
{
	static u32 panel_id;

	UIPanel* p = g_NIL_PANEL;
	if(ed)
	{
		p    = Alloc!(UIPanel)(&g_ctx.arena);
		p.id = panel_id++;
		p.ed = ed;
	}
	else
	{
		bool allocated;
		p = GetOrAlloc(&g_ctx.arena, &g_ctx.free_panels, g_NIL_PANEL, &allocated);
		if(allocated)
		{
			p.id   = panel_id++;
		}
		if(!p.ed)
		{
			p.ed = CreateEditor();
		}
	}

	p.item  = MakeItem!(PANEL_FLAGS)("###panel_%s", p.id);
	p.pct   = 1.0;
	p.first = p.last = p.next = p.prev = p.parent = p.free_next = g_NIL_PANEL;

	return p;
}

static string
AssignItem(T, alias p)()
{
	import std.format : format;

	enum string id = __traits(identifier, p);
	string result  = "";
	static if(is(T == UIItem*))
	{
		result = id;
	}
	else static if(is(T == UIKey) || StringType!(T))
	{
		result = format("Get(%s)", id);
	}
	else static assert(false, "Unknown type for AssignItem");

	return result;
}

static string 
AssignKey(T, alias p)()
{
	import std.format : format;

	enum string id = __traits(identifier, p);
	string result  = "";
	static if(is(T == UIItem*))
	{
		result = format("%s.key", id);
	}
	else static if(is(T == UIKey) || is(T == const(UIKey)))
	{
		result = id;
	}
	else static if(StringType!(T))
	{
		result = format("MakeKey(%s)", id);
	}
	else static assert(false, "Unknown type for AssignKey");

	return result;
}

bool
Ready(T)(T param) if(ItemAndKeyType!(T))
{
	UIItem* item = mixin(AssignItem!(T, param));
	return item.ready_t > 0.0001;
}

bool
Hovered(bool current_frame, T, U)(T param, U* ptr, U index)
{
	bool result = Hovered!(current_frame)(param);
	if(result)
	{
		*ptr = index;
	}

	return result;
}

bool
Hovered(bool current_frame, T)(T param)
{
	Ctx* ctx = GetCtx();
	bool result;

	UIKey key = mixin(AssignKey!(T, param));

	if(!ZeroKey(ctx.hover_key))
	{
		static if(current_frame)
		{
			result = key.hash == ctx.hover_key.hash && ctx.last_hover_frame == ctx.frame;
		}
		else
		{
			result = key.hash == ctx.hover_key_hash;
		}
	}

	return result;
}

void
PushUIEvent(Ctx* ctx, UIInput input)
{
	UIInput* ev = Alloc!(UIInput)(&ctx.temp_arena);
	*ev         = input;

	DLLPush(&ctx.events, ev, null);
}

void
DrawUI(Ctx* ctx, u32 atlas_index)
{
	if(ctx.pc.atlas_index != atlas_index) 
	{
		DrawUI(ctx);
		ctx.pc.atlas_index = atlas_index;
		PushConstants(ctx.pipeline, &ctx.pc);
	}
}

void
DrawSlug(Ctx* ctx)
{
	version(ENABLE_RENDERER)
	{
		// TODO: fix this
		SlugBuffer* buffer = ctx.slug_buffers.ptr + ctx.f_idx;

		auto vertex_buffer = ctx.slug_vertex_buffers.ptr + ctx.f_idx;
		auto index_buffer  = ctx.slug_index_buffers.ptr  + ctx.f_idx;

		BindBuffers(index_buffer, vertex_buffer);
		DrawIndexed(buffer.quad_offset*6, 1, 0, 0, 0);
	}
}

void
DrawUI(Ctx* ctx)
{
	version(ENABLE_RENDERER)
	{
		UIBuffer* b = ctx.buffers.ptr + ctx.f_idx;
		if(b.vtx_offset != b.count)
		{
			u32 count = b.count - b.vtx_offset;
			BindBuffers(&b.m_idx, &b.m_vtx);
			DrawIndexed(6, count, 0, 0, b.vtx_offset);
			b.vtx_offset += count;
		}
	}
}

void
BeginUI(Inputs* inputs)
{
	Ctx* ctx = GetCtx();
	Arena* a = &ctx.temp_arena;

	Reset(a);

	// Convert Inputs
	ctx.events.first = ctx.events.last = null;

	bool mouse_moved;
	static f32  mouse_down_elapsed = 0.0;
	static bool dragging;
	static i32  final_y;
	static bool mouse_down;
	for(InputEvent* i = inputs.first; i; i = i.next)
	{
		switch(i.key) 
		{
			case Input.LeftClick:
				{
					mouse_down = i.pressed;
					if(mouse_down)
					{
						PushUIEvent(ctx, UIInput(UIE.Click));
					}
					else
					{
						if(dragging)
						{
							PushUIEvent(ctx, UIInput(UIE.DragRelease));
							final_y = 0;
						}
						mouse_down_elapsed = 0.0;
						dragging           = false;
					}
				} break;
			case Input.MouseMotion:
				{
					mouse_moved   = true;
					ctx.mouse_pos = IVec2(i.x, i.y);

					if(!dragging && mouse_down_elapsed > 0.06f)
					{
						PushUIEvent(ctx, UIInput(type: UIE.DragStart, pos: IVec2(i.x, i.y)));
						dragging  = true;
					}

					if(dragging)
					{
						final_y += i.rel_y;
						PushUIEvent(ctx, UIInput(type: UIE.Drag, rel: IVec2(i.rel_x, i.rel_y)));
					}
				} break;
			case Input.ScrollUp, Input.ScrollDown:
				{
					if(i.pressed)
					{
						PushUIEvent(ctx, UIInput(type: UIE.Scroll, md: i.md, scroll: i.key == Input.ScrollUp ? -1 : 1));
					}
				} break;
			default:
				{
					if(i.pressed)
					{
						PushUIEvent(ctx, UIInput(type: UIE.Press, md: i.md, key: i.key, text: i.pressed ? i.text : []));
					}
				} break;
		}
	}

	if(mouse_down)
	{
		mouse_down_elapsed += g_delta;
	}

	u64 next_frame = ctx.frame+1;

	// Check current mouse target
	if(mouse_moved && !Nil(ctx.last_item))
	{
		UIKey hovered = FindHoverKey!(true)(ctx);

		if(ZeroKey(hovered))
		{
			hovered = FindHoverKey!(false)(ctx);
		}

		if(!ZeroKey(hovered))
		{
			ctx.last_hover_frame = next_frame;
			ctx.hover_key        = hovered;
		}
		else
		{
			ctx.hover_key = ZeroKey();
		}
	}

	// Clean up items
	KVPair!(UIHash, UIItem*)*[] items = GetAllNodes(&ctx.temp_arena, &ctx.items);
	foreach(i; 0 .. items.length)
	{
		UIItem* item = items[i].value;
		if(item.last_frame != ctx.frame)
		{
			Delete(&ctx.items, items[i].key);
			memset(item, 0, UIItem.sizeof);
			PushFree(&ctx.free_items, item);
		}
	}

	// Ctx state
	ctx.frame     = next_frame;
	ctx.f_idx     = ctx.frame%FRAME_OVERLAP;
	ctx.inputs    = inputs;
	ctx.last_item = g_UI_NIL;

	Reset(&ctx.str_arenas[ctx.f_idx]);

	assert(!mouse_moved || ZeroKey(ctx.hover_key) || (ctx.frame == ctx.last_hover_frame));

	ctx.animation_rate = 1.0 - pow(2.0, (-30.0f * g_delta));
	ctx.fade_rate      = 1.0 - pow(2.0, (-50.0f * g_delta));
	ctx.scroll_rate    = 1.0 - pow(2.0, (-60.0f * g_delta));
	ctx.pos_rate       = 1.0 - pow(2.0, (-90.0f * g_delta));

	version(ENABLE_RENDERER)
	{
		BeginFrame();

		if(ctx.fonts_to_load[ctx.f_idx])
		{
			LoadFontSet();
		}

		BeginRendering();

		SetStencilTest(false);

		PushConstants(ctx.pipeline, &ctx.pc);
		Bind(ctx.pipeline, ctx.desc_sets[ctx.f_idx]);
	}

	UIBuffer*   buffer      = ctx.buffers.ptr + ctx.f_idx;
	SlugBuffer* slug_buffer = ctx.slug_buffers.ptr + ctx.f_idx;

	memset(buffer.vtx.ptr,           0, Vertex.sizeof     * buffer.count);
	memset(slug_buffer.vertices.ptr, 0, SlugVertex.sizeof * slug_buffer.quad_offset);
	
	buffer.count            = 0;
	buffer.vtx_offset       = 0;
	slug_buffer.quad_offset = 0;
}

UIKey
FindHoverKey(bool priority)(Ctx* ctx)
{
	UIKey hovered;

	for(UIItem* item = ctx.last_item; !Nil(item); item = item.draw_next)
	{
		if(item.flags & (UIF.Click|UIF.Drag))
		{
			Rect rect = item.flags & UIF.TargetLeniency ? Rect(p0: item.p0-10.0f, p1: item.p1+10.0f) : item.rect;

			static if(priority)
			{
				bool in_bounds = item.flags & UIF.Priority && InBounds(ctx.mouse_pos, &rect);
			}
			else
			{
				bool in_bounds = InBounds(ctx.mouse_pos, &rect);
			}

			if(in_bounds)
			{
				hovered = item.key;
				break;
			}
		}
	}

	return hovered;
}

void
EndUI()
{
	Ctx* ctx = GetCtx();

	if(ctx.dbg)
	{
		const PAD = 4.0f;
		static FontSet* fs;
		if(!fs)
		{
			fs = GetFontSet(12);
		}

		string debug_hover_key = Scratchf("Hover Key: %s", ctx.hover_key.hash_text.length ? ctx.hover_key.hash_text : "None");

		Vec2 ext = GetExtent();
		f32 text_width = CalcTextWidth(debug_hover_key, fs);

		Rect debug_rect;

		debug_rect.p0.x = ext.x - text_width - PAD*2.0f;
		debug_rect.p0.y = ext.y - fs.line_height - PAD*2.0f;
		debug_rect.p1   = ext;

		DrawText(ctx, debug_hover_key, WHITE, fs, debug_rect, TA.Left);
	}

	version(ENABLE_RENDERER)
	{
		DrawUI(ctx);
		FinishRendering();
		SubmitAndPresent();
	}

	if(!ZeroKey(ctx.drag_key))
	{
		for(UIInput* i = ctx.events.first; !Nil(i); i = i.next)
		{
			if(i.type == UIE.DragRelease)
			{
				ctx.drag_key = ZeroKey();
				break;
			}
		}
	}
}

void
SetNil(Args...)(Args args)
{
	static foreach(i; 0 .. Args.length)
	{
		static if(is(typeof(Args[i]) == UIItem**))
		{
			*args[i] = g_UI_NIL;
		}
		else
		{
			*args[i] = null;
		}
	}
}

void
Clamp(Ctx* ctx, Vertex* v)
{
	v.dst_start = Clamp(v.dst_start, Vec2(0.0), cast(Vec2)ctx.pc.resolution);
	v.dst_end   = Clamp(v.dst_end,   Vec2(0.0), cast(Vec2)ctx.pc.resolution);
}

bool
Focused(T)(T param) if(ItemAndKeyType!(T))
{
	UIItem* item = mixin(AssignItem(T, param));
	return g_ctx.focus_item == item;
}

void
UpdateState(f32* v, bool cond)
{
	*v += g_ctx.animation_rate * (cast(f32)(cond) - *v);
	f32 diff = fabsf((*v)-1.0);
	if(cond && diff < 0.00009)
	{
		*v = 1.0;
	}
	else if(!cond && diff > 0.99998)
	{
		*v = 0.0;
	}
}

void
UpdateState(ISF flags)(Ctx* ctx, UIItem* item)
{
	static if(flags & ISF.Hot)  	UpdateState(&item.hot_t, cast(bool)(flags & ISF.SetHot) || Hovered!(true)(item));
	static if(flags & ISF.Ready)  UpdateState(&item.ready_t, cast(bool)(flags & ISF.SetReady));
	static if(flags & ISF.Active)	UpdateState(&item.active_t, cast(bool)(flags & ISF.SetActive) || Focused(item));
}

pragma(inline) void
AnimateHot(/* Vec4*[] */ Args...)(f32 hot, Vec4 hl_col, Args cols)
{
	if(hot != 0.0)
	{
		static foreach(i; 0 .. Args.length)
		{
			*(cols[i]) = Mix(*(cols[i]), hl_col, hot);
		}
	}
}

pragma(inline) void
AnimateReady(Args...)(f32 ready, Args cols)
{
	if(ready != 1.0)
	{
		static foreach(i; 0 .. Args.length)
		{
			static if(is(Args[i] == f32*))
			{
				*(cols[i]) *= ready;
			}
			else static if(is(Args[i] == Vec4*))
			{
				cols[i].a *= ready;
			}
		}
	}
}

void
DrawDropShadow(Ctx* ctx, Rect rect, f32 ready_t = 1.0)
{
	Vec2 size = Vec2(rect.p1.x-rect.p0.x, rect.p1.y-rect.p0.y);

	f32 px    = clamp(((size.x+size.y)/2.0)*0.004, 1.0, f32.max);
	f32 alpha = 0.1;
	AnimateReady(ready_t, &alpha);

	Vertex* v       = GetVertex(ctx);
	v.dst_start     = rect.p0 - Vec2(px);
	v.dst_end       = rect.p1 + Vec2(px);
	v.cols          = Vec4(0.0, 0.0, 0.0, alpha);
	v.corner_radius = 0.8f;
	v.edge_softness = 8.0f;

	Clamp(ctx, v);
}

void
DrawBorder(Ctx* ctx, Rect rect, Style* style, f32 hot_t = 0.0, f32 ready_t = 1.0)
{
	Vec4 col = style.border_col;
	AnimateHot(hot_t, style.border_hl_col, &col);
	AnimateReady(ready_t, &col);

	Vertex* v          = GetVertex(ctx);
	v.dst_start        = rect.p0;
	v.dst_end          = rect.p1;
	v.cols             = col;
	v.corner_radius    = style.corner_radius;
	v.border_thickness = style.border_thickness;
	v.edge_softness    = style.edge_softness;

	Clamp(ctx, v);
}

void
DrawRect(Ctx* ctx, Rect rect, Style* style, f32 hot_t = 0.0f, f32 ready_t = 1.0f)
{
	Vec4 col = style.col;
	AnimateHot(hot_t, style.hl_col, &col);
	AnimateReady(ready_t, &col);

	bool bordered = style.border_thickness > 0.0009f;

	Vertex* v       = GetVertex(ctx);
	v.dst_start     = rect.p0 + style.border_thickness;
	v.dst_end       = rect.p1 - style.border_thickness;
	v.cols          = col;
	v.corner_radius = style.corner_radius * (bordered ? 0.5f : 1.0f);
	v.edge_softness = style.edge_softness;

	Clamp(ctx, v);
}

enum TextAlign
{
	Left,
	Center,
	Right,
} alias TA = TextAlign;

void
DrawText(T, U, V)(Ctx* ctx, T text_param, U col_param, V text_size_param, Rect rect, TextAlign text_align, i64 hl_char = -1, f32 ready = 1.0) 
{
	static if(is(V == FontSet*))
	{
		FontSet* fs = text_size_param;
	}
	else if(is(V == u32))
	{
		FontSet* fs = GetFontSet(text_size_param);
	}
	else static assert(false, "invalid input parameter, V must be FontSet* or u32");

	string text = Str(text_param);

	DrawUI(ctx, fs.index);

	f32 x = rect.p0.x;
	if(text_align & TA.Right)
	{
		x = rect.p1.x - CalcTextWidth(text, fs);
	}
	else if(text_align & TA.Center)
	{
		x = ((rect.p1.x-rect.p0.x - CalcTextWidth(text, fs)) / 2.0);
	}

	if(rect.p0.x > rect.p1.x)
	{
		Logf("assert failed %f %f", rect.p0.x, rect.p1.x);
		assert(false);
	}
	x = clamp(x, rect.p0.x, rect.p1.x);

	static if(is(U == TokenStyle[]))
	{
		TokenStyle[] tks = col_param;
		foreach(j; 0 .. text.length)
		{
			u8     ch  = text[j];
			Glyph* g   = ch < fs.glyphs.length ? fs.glyphs.ptr + ch : null;
			Vec4   col = SYNTAX_COLORS[tks[j]];
			if(hl_char == j)
			{
				col = InvertCol(col);
			}

			AnimateReady(ready, &col);
			DrawGlyph(rect, g, &x, rect.p0.y, fs.line_height, fs.size, col);

			if(x > rect.p1.x) break;
		}
	}
	else static if(is(U == Vec4))
	{
		Vec4 col = col_param;
		AnimateReady(ready, &col);

		foreach(j; 0 .. text.length)
		{
			u8     ch = text[j];
			Glyph* g  = ch < fs.glyphs.length ? fs.glyphs.ptr + ch : null;
			DrawGlyph(rect, g, &x, rect.p0.y, fs.line_height, fs.size, hl_char == j ? InvertCol(col) : col);

			if(x > rect.p1.x) break;
		}
	}
	else static assert(false, "invalid input parameter, U must be TokenStyle[] or Vec4");
}

pragma(inline) Vec4
InvertCol(Vec4 col)
{
	return Vec4(1.0-col.r, 1.0-col.g, 1.0-col.b, col.a);
}

void
BeginScissor(Ctx* ctx, UIItem* item, bool scissor_x = true, bool scissor_y = true)
{
	DrawUI(ctx);

	Vec2 res = ctx.pc.resolution;

	i32 x = cast(i32)clamp(scissor_x ? floor(item.rect.p0.x)     : 0,     0.0, res.x);
	i32 y = cast(i32)clamp(scissor_y ? floor(item.rect.p0.y)     : 0,     0.0, res.y);
	i32 w = cast(i32)clamp(scissor_x ? floor(item.rect.p1.x) - x : res.x, 0.0, res.x);
	i32 h = cast(i32)clamp(scissor_y ? floor(item.rect.p1.y) - y : res.y, 0.0, res.y);

	SetScissor(x, y, w, h);

	ResetScissor();
}

Vec2
GetExtent()
{
	version(ENABLE_RENDERER) return Vec2(RendererGetExtent()); else return Vec2(1280, 720);
}

template 
StackIDs(string stack, string ctx = "ctx")
{
	import std.string : replace;
	struct Identifiers { string stack, stack_top_node; }

	enum Identifiers StackIDs = {
		stack:           replace(ctx~".@",     "@", stack),
		stack_top_node:  replace(ctx~".@_top", "@", stack),
	};
}

T*
Recurse(bool pre = true, T)(T* node, T* nil)
{
	T* child = pre ? node.first : node.last;

	T* result = nil;
	if(!Nil(child))
	{
		result = child;
	}
	else for(T* p = node; !Nil(p); p = p.parent)
	{
		T* sibling = pre ? p.next : p.prev;
		if(!Nil(sibling))
		{
			result = sibling;
			break;
		}
	}

	return result;
}

Vec2
RootSize()
{
	return cast(Vec2)GetExtent();
}

UIKey
MakeKey(Args...)(string str, Args args)
{
	char[] key = sformat(ScratchAlloc!(char)(cast(u64)(str.length)), str, args);
	return MakeKey(key);	
}

UIKey
MakeKey(T)(T str) if(StringType!T)
{
	static if(is(T: string))
	{
		string id = str;
	}
	else
	{
		string id = Str(str);
	}

	UIKey key;

	if(str.length)
	{
		bool hash_only = false;
		i64 pos = 0;
		u32 hash_count = 0;
		for(i64 i = id.length-1; i >= 0; i -= 1)
		{
			if(i == 0 && id[i] == '#')
			{
				hash_only = true;
			}

			if(hash_count == 2)
			{
				if(id[i] == '#')
				{
					hash_count += 1;
					pos = i;
				}

				break;
			}

			if(id[i] == '#')
			{
				pos = i;
				hash_count += 1;
			}
		}

		if(hash_count < 2 || hash_only)
		{
			key.text      = hash_only ? "" : id;
			key.hash_text = hash_only ? id : "";
			key.hash      = Hash(id);
		}
		else if(hash_count == 2 || hash_count == 3)
		{
			key.text      = id[0 .. pos];
			key.hash_text = id[pos .. $];
			key.hash      = hash_count == 2 ? Hash(id) : Hash(id[pos+hash_count .. $]);
		}
	}

	return key;
}

static Vec4
HexCol(u64 col)
{
	return Vec4(
		cast(f32)((col >> 16) & 0xFF) / 255.0,
		cast(f32)((col >> 8 ) & 0xFF) / 255.0,
		cast(f32)((col >> 0 ) & 0xFF) / 255.0,
		1.0
	);
}

static UIKey
ZeroKey()
{
	return UIKey();
}

bool
ZeroKey(T)(T param) if(is(T == UIItem*) || is(T == UIKey))
{
	UIKey key = mixin(AssignKey!(T, param));
	return key.hash == 0 && key.hash_text.length == 0;
}

UIItem*
NewItem(Ctx* ctx)
{
	bool allocated;
	return GetOrAlloc(&ctx.arena, &ctx.free_items, g_UI_NIL, &allocated);
}

UIItem*
MakeItem(UIFlags flags = UIF.None, Args...)(string str, Args args)
{
	u64 len = cast(u64)(str.length*1.5);
	static foreach(i; 0 .. Args.length)
	{
		static if(StringType!(Args[i]))
		{
			len += args[i].length;
		}
	}

	char[] key = sformat(Alloc!(char)(&g_ctx.str_arenas[g_ctx.f_idx], len), str, args);
	return MakeItem(key, flags);
}

pragma(inline) UIItem*
MakeItem(T)(T k, UIFlags flags = UIF.None) if(KeyType!(T))
{
	UIKey   key = mixin(AssignKey!(T, k));
	Ctx*    ctx = GetCtx();
	UIItem* item;

	Result!(UIItem*) res = ctx.items[key.hash];
	if(res.ok)
	{
		item = res.value;
	}
	else
	{
		item = NewItem(ctx);
		HTPush(&ctx.items, key.hash, item);
	}

	if(item.last_frame != ctx.frame)
	{
		item.flags     = flags;
		item.draw_next = ctx.last_item;
		ctx.last_item  = item;

		if(item.scroll_offset != item.scroll_target)
		{
			static foreach(axis; A2D.min .. A2D.max)
			{
				if(fabsf(item.scroll_offset.v[axis] - item.scroll_target.v[axis]) > 0.0009f)
				{
					f32 v = ctx.scroll_rate * (item.scroll_target.v[axis] - item.scroll_offset.v[axis]);
					item.scroll_offset.v[axis] += v;

					if(fabsf(item.scroll_offset.v[axis] - item.scroll_target.v[axis]) < 2.0f)
					{
						item.scroll_offset.v[axis] = item.scroll_target.v[axis];
					}
				}
			}
		}
	}

	item.key        = key;
	item.last_frame = ctx.frame;

	return item;
}

void
AnimatePos(UIItem* item)
{
	if(item.p0 != item.target_pos)
	{
		item.p0 += g_ctx.pos_rate * (item.target_pos - item.p0);
		
		static foreach(axis; A2D.min .. A2D.max)
		{
			if(fabsf(item.p0.v[axis] - item.target_pos.v[axis]) < 2.0f)
			{
				item.p0.v[axis] = item.target_pos.v[axis];
			}
		}
	}

	item.p1 = item.p0+item.size;
}

void
SetReady(UIItem* item, bool ready)
{
	item.ready_t += g_ctx.fade_rate * (cast(f32)(ready) - item.ready_t);

	if(ready && item.ready_t > 0.9998)
	{
		item.ready_t = 1.0;
	}
	else if(!ready && item.ready_t < 0.0009)
	{
		item.ready_t = 0.0;
	}
}

void
SetHot(UIItem* item, bool hot)
{
	item.hot_t += g_ctx.animation_rate * (cast(f32)(hot) - item.hot_t);

	if(hot && item.hot_t > 0.9998)
	{
		item.hot_t = 1.0;
	}
	else if(!hot && item.hot_t < 0.0009)
	{
		item.hot_t = 0.0;
	}
}

f32
CalcTextWidth(T, U)(T text, U param) if((is(T == string) || StringType!T) && (is(U: u32) || is(U == FontSet*) ))
{
	static if(is(T == string))
	{
		string str = text;
	}
	else
	{
		string str = Str(text);
	}

	static if(is(U: u32))
	{
		FontSet* fs = GetFontSet(param);
	}
	else
	{
		FontSet* fs = param;
	}

	u32 tab_width = g_ctx.tab_width;

	f32 width = 0.0;
	for(u64 i = 0; i < str.length; i += 1)
	{
		width += GlyphWidth(fs.glyphs.ptr + str.ptr[i], fs);
	}

	return width;
}

pragma(inline) f32
GlyphWidth(Glyph* g, FontSet* fs)
{
	return g.ch == '\t' ? (fs.glyphs[' '].advance*cast(f32)(g_ctx.tab_width)) : g.advance;
}

pragma(inline) void
DrawGlyph(Rect rect, Glyph* glyph, f32* x_pos, f32 y, f32 line_height, u32 size, Vec4 col = Vec4(1.0))
{
	if(glyph)
	{
		Ctx* ctx    = GetCtx();
		Vertex* v   = null;
		f32 advance = glyph.advance;

		if(glyph.ch == ' ' || glyph.ch == '\n')
		{
			*x_pos += advance;
		}
		else if(glyph.ch == '\t')
		{
			*x_pos += advance*ctx.tab_width;
		}
		else
		{
			f32 w = glyph.plane.right-glyph.plane.left;
			f32 h = glyph.plane.bottom-glyph.plane.top;


			v = GetVertex(ctx);

			f32 y_pos = y + glyph.plane.top;

			// Need to change how the quads are calculated, i had changed it so they were all same height and same top/bottom placement as it was easier to draw rasterized glyphs that way but doesn't work when the SDFs are not generated in a similar way
			v.dst_start   = Vec2(*x_pos+glyph.plane.left,   y_pos);
			v.dst_end     = Vec2(*x_pos+glyph.plane.left+w, y_pos+h);
			v.cols        = col;
			v.src_start   = Vec2(glyph.atlas.left,  glyph.atlas.top);    
			v.src_end     = Vec2(glyph.atlas.right, glyph.atlas.bottom);
			v.has_texture = true;

			f32 end_x   = *x_pos + advance;
			f32 width   = end_x - *x_pos;
			f32 bound_x = rect.p1.x;
			if(end_x > bound_x)
			{
				f32 cull_pct = (end_x - bound_x) / width;
				v.dst_end.x -= (v.dst_end.x - v.dst_start.x) * cull_pct;
				v.src_end.x -= (v.src_end.x - v.src_start.x) * cull_pct;
			}

			f32 end_y  = y + line_height;
			f32 height = end_y - y;
			if(end_y > rect.p1.y)
			{
				f32 cull_pct = (end_y - rect.p1.y) / height;
				v.dst_end.y -= (v.dst_end.y - v.dst_start.y) * cull_pct;
				v.src_end.y -= (v.src_end.y - v.src_start.y) * cull_pct;
			}

			static foreach(axis; A2D.min .. A2D.max)
			{
				static foreach(i; 0 .. v.pos.length)
				{
					v.pos[i].end[axis] = v.pos[i].start.v[axis] > v.pos[i].end.v[axis] ? v.pos[i].start.v[axis] : v.pos[i].end.v[axis]; 
				}
			}

			*x_pos += advance;
		}
	}
}

pragma(inline) Vertex*
GetVertex(Ctx* ctx)
{
	assert(ctx.buffers[ctx.f_idx].count < VERTEX_MAX_COUNT);
	return ctx.buffers[ctx.f_idx].vtx.ptr + ctx.buffers[ctx.f_idx].count++;
}


bool
Nil(T)(T item)
{
	     static if(is(T == UIItem*))  return item == null || item == g_UI_NIL;
	else static if(is(T == UIInput*)) return item == null || item == g_UI_NIL_INPUT;
	else static if(is(T == UIPanel*)) return item == null || item == g_NIL_PANEL;
	else static                       assert(false, "Invalid nil type checked");
}

bool
Dragged(Ctx* ctx, UIItem* item, IVec2* dragged)
{
	if(item.key == ctx.drag_key || (!ctx.drag_key && item.key == ctx.hover_key))
	{
		for(UIInput* i = ctx.events.first; !Nil(i); i = i.next)
		{
			bool taken;

			if(i.type == UIE.DragStart)
			{
				ctx.drag_key = item.key;
				taken        = true;
			}
			else if(i.type == UIE.Drag && ctx.drag_key == item.key)
			{
				*dragged += i.rel;
				taken     = true;
			}

			if(taken)
			{
				DLLRemove(&ctx.events, i, g_UI_NIL_INPUT);
			}
		}
	}

	return *dragged != IVec2(0);
}

bool
Clicked(Ctx* ctx, UIItem* item)
{
	bool result;

	if(ctx.hover_key == item.key)
	{
		for(UIInput* i = ctx.events.first; !Nil(i); i = i.next)
		{
			if(i.type == UIE.Click)
			{
				result = true;
				DLLRemove(&ctx.events, i, g_UI_NIL_INPUT);
				break;
			}
		}
	}

	return result;
}

pragma(inline) bool
InBounds(T)(T pos, Rect* rect)
{
	return pos.x >= rect.p0.x && pos.x <= rect.p1.x && pos.y >= rect.p0.y && pos.y <= rect.p1.y;
}

static f32 
SRGB(f32 v)
{
	return v < 0.0031308 ? v * 12.92 : pow(v, 0.41666) * 1.055 - 0.055;
}

static Vec4
LinearToSRGB(Vec4 v)
{
	return Vec4(SRGB(v.v[0]), SRGB(v.v[1]), SRGB(v.v[2]), v.v[3]);
}

static Vec4
SRGBVec4(f32 v)
{
	return LinearToSRGB(Vec4(v));
}

static Vec4
SRGBVec4(f32 r, f32 g, f32 b, f32 a)
{
	return LinearToSRGB(Vec4(r, g, b, a));
}

static Vec4
SRGBToOklab(Vec4 v)
{
	f32 l = 0.4122214708f * v.r + 0.5363325363f * v.g + 0.0514459929f * v.b;
	f32 m = 0.2119034982f * v.r + 0.6806995451f * v.g + 0.1073969566f * v.b;
	f32 s = 0.0883024619f * v.r + 0.2817188376f * v.g + 0.6299787005f * v.b;

	f32 l_ = cbrtf(l);
	f32 m_ = cbrtf(m);
	f32 s_ = cbrtf(s);

	return Vec4(
		0.2104542553f*l_ + 0.7936177850f*m_ - 0.0040720468f*s_,
		1.9779984951f*l_ - 2.4285922050f*m_ + 0.4505937099f*s_,
		0.0259040371f*l_ + 0.7827717662f*m_ - 0.8086757660f*s_,
		v.a
	);
}

static Vec4
OklabToSRGB(Vec4 v)
{
	f32 l_ = v.r + 0.3963377774f * v.g + 0.2158037573f * v.b;
	f32 m_ = v.r - 0.1055613458f * v.g - 0.0638541728f * v.b;
	f32 s_ = v.r - 0.0894841775f * v.g - 1.2914855480f * v.b;

	float l = l_*l_*l_;
	float m = m_*m_*m_;
	float s = s_*s_*s_;

	return Vec4(
		+4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s,
		-1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s,
		-0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s,
		v.a
	);
}

string
EdScratchf(Args...)(string fmt, Args args)
{
	char[] buf = Alloc!(char)(&g_ctx.str_arenas[g_ctx.f_idx], fmt.length < 16 ? 32 : 128);
	return Str(sformat(buf, fmt, args));
}

unittest
{
	{ // UI Key
		u8[] str = cast(u8[])r"Test String##123";
		UIKey key = MakeKey(str);

		assert(key.text == cast(u8[])r"Test String");
		assert(key.hash == Hash(str));

		u8[] str1 = cast(u8[])r"Test String###123123";
		key = MakeKey(str1);

		assert(key.text == cast(u8[])r"Test String");
		assert(key.hash == Hash(r"123123"));
	}
}

