import dlib;
import dlib.alloc : Reset;
import core.stdc.stdio : EOF;
import parsing;
import parsing : SetBuffers;
import std.format : sformat;
import std.math.algebraic : abs;
import editor;

struct FlatBuffer
{
	Tokenizer   tk;
	Arena       arena;
	Arena       ls_arena;
	LineBuffers linebufs;
	BufferInfo  info;

	string	    file_name;
	u8[]        data;
	u64         length;

	Line[]      lines;
	i64         pos;
	i64         col_pos;
	SelectMode  sel_mode;

	bool        dirty;

	alias info this;
}

struct BufferInfo
{
	u64     line_count;
	I64Vec2 selection;
	IVec2   position;
}

enum SelectMode
{
	None,
	Normal,
	Line,
}

alias SM = SelectMode;

struct Line
{
	u64 pos;
	u64 length;
	u32 line;
	u16 level;
}

struct LineBuffers
{
	Arena       arena;
	LineBuffer* first;
	LineBuffer* last;
}

struct LineBuffer
{
	u8[]        text;
	TS[]        style;
	LineBuffer* next;
}

enum WalkDir
{
	None     = 0,
	Backward = -1,
	Forward  = +1,
}

const bool[128] SYM_TOKENS = [
	'`': true,
	'"': true,
	'\'': true,
	'[': true,
	']': true,
	'~': true,
	'!': true,
	'@': true,
	'#': true,
	'$': true,
	'%': true,
	'^': true,
	'&': true,
	'*': true,
	'(': true,
	')': true,
	'-': true,
	'_': true,
	'=': true,
	'+': true,
	'|': true,
	'}': true,
	'{': true,
	',': true,
	'.': true,
	'?': true,
	'>': true,
	'<': true,
	':': true,
	'/': true,
	'\\': true,
	';': true,
];

const LineBuffer g_nil_line_buf;
LineBuffer* g_NIL_LINE_BUF;

const u64 SPACING   = 2;
const u64 BUF_START = 1;

FlatBuffer
CreateFlatBuffer(u8[] data, string file_name)
{
	FlatBuffer fb = {
		arena: CreateArena(MB(1)),
		ls_arena: CreateArena(KB(512)),
	};

	g_NIL_LINE_BUF = cast(LineBuffer*)&g_nil_line_buf;

	Init(&fb, data, file_name);
	fb.tk = CreateTokenizer(&fb);
	Fix(&fb);

	return fb;
}

void
Init(FlatBuffer* fb, u8[] data, string file_name)
{
	u64 capacity = data.length > 0 ? RoundUp(cast(u64)(data.length)*2, KB(4)) : KB(4);

	u8[] buf = Alloc!(u8)(capacity);

	fb.file_name  = file_name;
	fb.line_count = CountLF(data);
	fb.length     = data.length;
	fb.selection  = -1;
	fb.sel_mode   = SM.None;
	fb.linebufs   = CreateLineBuffers(MB(1));
	fb.pos        = 0;

	SetBuffers(fb, buf);

	MemCpy(fb.data.ptr, data.ptr, data.length);
}

void
SetBuffers(FlatBuffer* fb, u8[] data)
{
	fb.data = data;
}

void
Change(FlatBuffer* fb, u8[] data, string file_name)
{
	Free(fb.data);
	Reset(&fb.linebufs.arena);

	Init(fb, data, file_name);
	ResetTokenizer(&fb.tk, fb.data.length);
	Fix(fb);
}

u64
CountLF(u8[] data)
{
	u64 lf_count = 1;
	for(u64 i = 0; i < data.length; i += 1)
	{
		lf_count += u64(data.ptr[i] == '\n');
	}

	return lf_count;
}

pragma(inline) bool
ScanLineRightBrace(u8* data, u64 start, u64 len)
{
	bool result;
	for(u64 i = start; i < len; i += 1)
	{
		if(data[i] == '\t' || data[i] == ' ') continue;

		if(data[i] == '}' || data[i] == ']' || data[i] == ')')
		{
			result = true;
			break;
		}

		break;
	}

	return result;
}

void
Fix(FlatBuffer* fb) 
{
	with(fb)
	{
		Reset(&ls_arena);
		
		lines = Alloc!(Line)(&ls_arena, line_count);
		lines[0].pos = 0;

		Line* prev = null;
		u16  ignore, pending_level, level;
		u64  ls_idx = 1;
		for(u64 i = 0; i < length; i += 1)
		{
			if(data[i] == '{' || data[i] == '(' || data[i] == '[')
			{
				if(pending_level > 0)
				{
					ignore += 1;
				}
				else
				{
					pending_level = 1;
				}
			}

			else if(data[i] == '}' || data[i] == ')' || data[i] == ']')
			{
				if(ignore > 0)
				{
					ignore -= 1;
				}
				else if(pending_level == 1)
				{
					pending_level = 0;
				}
				else if(level > 0)
				{
					if(prev)
					{
						prev.level = cast(u16)Max(prev.level-u16(1), 0);
					}
					level -= 1;
				}
			}

			if(data[i] == '\n')
			{
				level += pending_level;
				pending_level = 0;

				lines[ls_idx].pos   = i+1;
				lines[ls_idx].level = level;

				prev = &lines[ls_idx];

				ls_idx += 1;
			}
		}

		for(u64 i = 0; i < line_count-1; i += 1)
		{
			lines[i].length = lines[i+1].pos - lines[i+0].pos;
		}

		lines[line_count-1].length = length - lines[line_count-1].pos;

		dirty = false;

		TokenizeD(fb);
	}
}

LineBuffers
CreateLineBuffers(u64 arena_size)
{
	return LineBuffers(arena: CreateArena(arena_size));
}

void
Insert(FlatBuffer* fb, u8[] insert, u64 length, u64 pos)
{
	if(fb.length + length > fb.data.length-SPACING)
	{
		SetBuffers(&fb.tk, Realloc(fb.tk.tokens, fb.data.length*2));
		SetBuffers(fb, Realloc(fb.data, fb.data.length*2));
	}

	u64 new_lines;
	for(u64 i = 0; i < length; i += 1)
	{
		fb.pos    += 1;
		new_lines += u64(insert.ptr[i] == '\n');
	}

	if(length == 1)
	{
		u8 new_ch;
		switch(insert[0])
		{
			case '[': new_ch = ']'; break;
			case '(': new_ch = ')'; break;
			case '{': new_ch = '}'; break;
			case '\'', '"', '`', '\n': new_ch = insert[0]; break;
			default: break;
		}

		if(new_ch == '\n' && pos > 0)
		{
			u8 l = fb.data[pos-1];
			u8 r = fb.data[pos];
			bool expand = (l == '(' && r == ')') || (l == '{' && r == '}') || (l == '[' && r == ']');

			u32 level = LevelFromPos(fb, pos) + u32(expand);
			u64 limit = level + length;
			for(; length < limit; length += 1, fb.pos += 1)
			{
				insert[length] = '\t';
			}

			if(expand)
			{
				insert[length++] = '\n';
				new_lines       += 1;

				limit = level-1+length;
				for(; length < limit; length += 1)
				{
					insert[length] = '\t';
				}
			}
		}
		else if(new_ch != '\n' && new_ch > 0)
		{
			length   += 1;
			insert[1] = new_ch;
		}
	}

	u64 temp_len = fb.length-pos;

	u8[] temp = Alloc!(u8)(&fb.arena, fb.data[pos .. pos+temp_len]);

	fb.data[pos .. pos+length] = insert[0 .. length];
	pos += length;
	fb.data[pos .. pos+temp_len] = temp[0 .. temp_len];

	fb.length += length;

	fb.line_count += new_lines;

	Fix(fb);

	Reset(&fb.arena);
}

void
ToggleSelection(FlatBuffer* fb, SelectMode mode)
{
	if(fb.sel_mode == SM.None)
	{
		StartSelection(fb, mode);
	}
	else
	{
		EndSelection(fb);
	}
}

void
StartSelection(FlatBuffer* fb, SelectMode mode)
{
	fb.sel_mode = mode;

	if(mode == SM.Normal)
	{
		fb.selection = fb.pos;
	}
	else if(mode == SM.Line)
	{
		fb.selection = CurrentLine(fb);
	}
}

void
EndSelection(FlatBuffer* fb)
{
	fb.selection = -1;
	fb.sel_mode  = SM.None;
}

void
Insert(FlatBuffer* fb, u8[] insert, u64 length)
{
	Insert(fb, insert, length, fb.pos);
}

pragma(inline) i64
CurrentLine(FlatBuffer* fb)
{
	return LineFromPos(fb, fb.pos);
}

pragma(inline) i64
CurrentCol(FlatBuffer* fb)
{
	return LinePos(fb, fb.pos);
}

pragma(inline) i64
LinePos(FlatBuffer* fb, u64 pos)
{
	u64 line = LineFromPos(fb, pos);
	return pos - fb.lines[line].pos;
}

pragma(inline) I64Vec2
VecPos(FlatBuffer* fb)
{
	return I64Vec2(CurrentCol(fb), CurrentLine(fb));
}

pragma(inline) i64
LineFromPos(FlatBuffer* fb, u64 pos)
{
	i64 line = 0;
	for(i64 i = 0; i < fb.line_count; i += 1)
	{
		if(fb.lines[i].pos > pos)
		{
			break;
		}

		line = i;
	}

	return line;
}

pragma(inline) u16
LevelFromPos(FlatBuffer* fb, u64 pos)
{
	u64 line = LineFromPos(fb, pos);
	return fb.lines[line].level;
}

pragma(inline) u64
LineLength(FlatBuffer* fb, u64 line)
{
	return fb.lines[line].length;
}

LineBuffer*
GetLine(FlatBuffer* fb, u64 line)
{
	LineBuffer* lbuf = g_NIL_LINE_BUF;

	if(line < fb.lines.length)
	{
		lbuf = SliceLineBuffer(fb, &fb.lines[line]);
	}

	return lbuf;
}

pragma(inline) LineBuffer*
SliceLineBuffer(FlatBuffer* fb, Line* ls)
{
	LineBuffer* lbuf = Alloc!(LineBuffer)(&fb.linebufs.arena);

	lbuf.text    = fb.data[ls.pos .. ls.pos+ls.length];
	lbuf.style   = fb.tk.tokens[ls.pos .. ls.pos+ls.length];

	return lbuf;
}

void
Clear(Editor* ed)
{
	Change(&ed.buf, [], "");
	ed.line_offset = 0;
}

pragma(inline) void
ResetBuffer(FlatBuffer* fb)
{
	Reset(&fb.linebufs.arena);
}

void
MoveToEOL(FlatBuffer* fb)
{
	if(fb.data[fb.pos] != '\n') 
	{
		for(u64 i = 0; i < fb.lines.length; i += 1)
		{
			if(fb.pos < fb.lines[i].pos)
			{
				fb.pos = 0 < i ? fb.lines[i].pos-1 : fb.length;
				break;
			}
		}
	}
}

void
MoveToSOL(FlatBuffer* fb)
{
	if(fb.pos != 0 && fb.data[fb.pos-1] != '\n')
	{
		for(u64 i = 0; i < fb.lines.length; i += 1)
		{
			if(fb.pos < fb.lines[i].pos)
			{
				fb.pos = i > 0 ? fb.lines[i-1].pos : 0;
				break;
			}
		}
	}
}

void
MoveUp(FlatBuffer* fb)
{
	MoveUp(fb, LinePos(fb, fb.pos));
}

void
MoveUp(FlatBuffer* fb, u64 col)
{
	u64 line = CurrentLine(fb);
	if(line > 0)
	{
		u64 end = fb.lines[line].pos-1;
		line -= 1;
		col = Max(col, fb.col_pos);
		fb.pos = Min(fb.lines[line].pos+col, end);
	}
}

void
MoveDown(FlatBuffer* fb)
{
	MoveDown(fb, LinePos(fb, fb.pos));
}

void
MoveDown(FlatBuffer* fb, u64 col)
{
	u64 line = CurrentLine(fb);
	if(line+1 < fb.lines.length)
	{
		line += 1;
		fb.pos = fb.lines[line].pos;
		u64 end = fb.lines.length > line+1 ? fb.lines[line+1].pos-1 : fb.length;
		col = Max(col, fb.col_pos);
		fb.pos = Min(fb.pos+col, end);
	}
}

void
MoveToEmptyLine(bool up)(FlatBuffer* fb)
{
	static if(!up) u64 lf_pos;
	bool started;
	u64  step = up ? -1 : 1;
	
	u8 ch = 0;
	if((fb.pos != 0 && up) || (fb.pos != fb.length && !up))
	{
		for(u64 i = fb.pos + step; true; i += step)
		{
			static if(!up)
			{
				if(i == fb.length-1)
				{
					fb.pos = fb.length;
					break;
				}

				ch = fb.data[i];
			}

			static if(up)
			{
				if(i == 0) 
				{
					fb.pos = 0;
					break;
				}

				ch = fb.data[i-1];
			}

			if(started && !CheckWhiteSpace(fb.data[i]))
			{
				started = false;
			}

			if(!started && ch == '\n')
			{
				started = true;
				static if(!up) lf_pos = i;
				continue;
			}

			if(started && ch == '\n')
			{
				fb.pos = i;
				MoveToSOL(fb);
				break;
			}
		}
	}
}

pragma(inline) bool
IsLetter(u8 ch)
{
	return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');
}

pragma(inline) bool
IsNumber(u8 ch)
{
	return ch >= '0' && ch <= '9';
}

pragma(inline) bool
IsSymbol(u8 ch)
{
	return ch < 128 && SYM_TOKENS[ch];
}

enum ChType
{
	Letter,
	Number,
	Symbol,
	Whitespace,
}

bool 
CheckChMatch(u8 ch, ChType start_type)
{
	return (start_type == ChType.Letter && !IsLetter(ch)) || 
				 (start_type == ChType.Number && (!IsNumber(ch) && ch != '.')) || 
				 (start_type == ChType.Symbol && !IsSymbol(ch));
}

ChType
GetChType(u8 ch)
{
	ChType type = ChType.Letter;
	if(IsNumber(ch))
	{
		type = ChType.Number;
	}
	else if(IsSymbol(ch))
	{
		type = ChType.Symbol;
	}
	else if(CheckWhiteSpace(ch))
	{
		type = ChType.Whitespace;
	}

	return type;
}

void
MoveToWordEdge(bool forward)(FlatBuffer* fb)
{
	u64 step = forward ? 1 : -1;
	
	if((forward && fb.pos < fb.length-1) || (!forward && fb.pos > 0))
	{
		ChType type = GetChType(fb.data[fb.pos]);
		if(type != GetChType(fb.data[fb.pos+step]))
		{
			fb.pos += step;
		}
		else for(u64 i = fb.pos+step; true; i += step)
		{
			static if(forward)
			{
				u8 next = i < fb.length-1 ? fb.data[i+1] : 0;
			}
			else
			{
				u8 next = i > 1 ? fb.data[i-1] : 0; 
			}

			static if(forward) if(i == fb.length-2)
			{
				fb.pos = i;
				fb.pos += u64(GetChType(next) == type);

				break;
			}

			static if(!forward) if(i == 1)
			{
				fb.pos = i;
				fb.pos += GetChType(next) == type ? -1 : 0;
				break;
			}

			if(CheckChMatch(next, type))
			{
				fb.pos = i;
				break;
			}
		}

		while(CheckWhiteSpace(fb.data[fb.pos]))
		{
			bool exit = forward ? fb.pos == fb.length-1 : fb.pos == 0;
			if(exit) break;

			fb.pos += step;
		}
	}
}

void
MoveToNextWord(bool forward)(FlatBuffer* fb)
{
	ChType type = GetChType(fb.data[fb.pos]);

	bool hit_ws;
	u64 step = forward ? 1 : -1;
	for(u64 i = fb.pos; true; i += step) with(ChType)
	{
		static if(forward) if(i == fb.length-1)
		{
			fb.pos = i;
			break;
		}

		static if(!forward) if(i == 0)
		{
			fb.pos = i;
			break;
		}

		u8 ch = fb.data[i];

		if(CheckWhiteSpace(ch)) 
		{
			hit_ws = true;
			continue;
		}

		if(hit_ws)
		{
			fb.pos = i;
			break;
		}

		if(CheckChMatch(ch, type))
		{
			fb.pos = i;
			break;
		}

	}

	assert(fb.pos >= 0 && fb.pos <= fb.length);
}

bool
Move(FlatBuffer* fb, Input key, Modifier md)
{
	bool taken;

	if(md & (MD.LeftShift | MD.RightShift))
	{
		switch(key) with(Input)
		{
			case Up:
				{
					MoveToEmptyLine!(true)(fb);
					taken = true;
				} break;
			case Down:
				{
					MoveToEmptyLine!(false)(fb);
					taken = true;
				} break;
			case Left:
				{
					if(fb.pos > 0)
					{
						MoveToWordEdge!(false)(fb);
						taken = true;
					}
				} break;
			case Right:
				{
					if(fb.pos < fb.length)
					{
						MoveToWordEdge!(true)(fb);
						taken = true;
					}
				} break;
			case Home: fb.pos = 0;         taken = true; break;
			case End:  fb.pos = fb.length; taken = true; break;
			default: break;
		}
	}
	else if(md & (MD.LeftCtrl | MD.RightCtrl))
	{
		switch(key) with(Input)
		{
			case Up:
				{
				} break;
			case Down:
				{
				} break;
			case Left:
				{
					if(fb.pos > 0)
					{
						MoveToNextWord!(false)(fb);
						taken = true;
					}
				} break;
			case Right:
				{
					if(fb.pos < fb.length)
					{
						MoveToNextWord!(true)(fb);
						taken = true;
					}
				} break;
			default: break;
		}
	}
	else
	{
		switch(key) with(Input)
		{
			case Down:
				{
					MoveDown(fb);
					taken = true;
				} break;
			case Up:
				{
					MoveUp(fb);
					taken = true;
				} break;
			case Left:
				{
					if(fb.pos > 0 && fb.data[fb.pos-1] != '\n')
					{
						fb.pos -= 1;
						taken = true;
					}
				} break;
			case Right:
				{
					if(fb.pos < fb.length && fb.data[fb.pos] != '\n')
					{
						fb.pos += 1;
						taken = true;
					}
				} break;
			case Home: MoveToSOL(fb); taken = true; break;
			case End:  MoveToEOL(fb); taken = true; break;
			default: break;
		}
	}

	if(key == Input.Left || key == Input.Right)
	{
		fb.col_pos = LinePos(fb, fb.pos);
	}

	UpdateSelection(fb);

	return taken;
}

void
UpdateSelection(FlatBuffer* fb)
{
	if(fb.sel_mode == SM.Normal)
	{
		fb.selection.y = fb.pos;
	}

	if(fb.sel_mode == SM.Line)
	{
		fb.selection.y = CurrentLine(fb);
	}
}

void
Backspace(FlatBuffer* fb)
{
	if(fb.pos-1 >= 0)
	{
		fb.pos -= 1;
		Delete(fb, 1, fb.pos);
	}
}

void
Delete(FlatBuffer* fb, u64 length, u64 pos)
{
	u64 end = pos+length;
	assert(end <= fb.length, "Delete failure: pos+length is not in range");

	for(u64 i = pos; i < fb.length && i < pos+length; i += 1)
	{
		if(fb.data[i] == '\n')
		{
			fb.line_count -= 1;
		}
	}

	u8[] temp;
	if(end != fb.length)
	{
		temp = Alloc!(u8)(&fb.arena, fb.length-end);
		temp[0 .. temp.length] = fb.data[end .. fb.length];
		fb.data[pos .. pos+temp.length] = temp[0 .. temp.length];
	}

	fb.length -= length;

	Fix(fb);
}

void
Replace(FlatBuffer* fb, u8[] insert, u64 length, u64 pos, u64 delete_length)
{
	Delete(fb, delete_length, pos);
	Insert(fb, insert, insert.length, pos);
}

unittest
{
	import std.stdio;

	u8[] StringToU8(string str)
	{
		return (cast(u8*)str.ptr)[0 .. str.length];
	}

	// FlatBuffer Insert/Delete/Replace
	{
		/*
		u8[] data = StringToU8("This is a test string for testing the FlatBuffer struct.");
		FlatBuffer buf = CreateFlatBuffer(data, "file");

		u8[] insert = StringToU8("This is a string inserted into the beginning of the buffer. ");
		Insert(&buf, insert, insert.length, 0);

		string result1 = "This is a string inserted into the beginning of the buffer. This is a test string for testing the FlatBuffer struct.";
		foreach(i, ch; result1)
		{	
			assert(buf.data[i] == ch, "FlatBuffer Insert failure: buffers do not match");
		}

		Delete(&buf, 17, 34);

		// 10, 21
		string result2 = "This is a string inserted into the buffer. This is a test string for testing the FlatBuffer struct.";
		foreach(i, ch; result2)
		{
			assert(buf.data[i] == ch, "FlatBuffer Delete failure: buffers do not match");
		}

		insert = StringToU8("replaced line inserted into the beginning of ");
		Replace(&buf, insert, insert.length, 10, 21);

		string result3 = "This is a replaced line inserted into the beginning of the buffer. This is a test string for testing the FlatBuffer struct.";
		foreach(i, ch; result3)
		{
			assert(buf.data[i] == ch, "FlatBuffer Replace failure: buffers do not match");
		}
		*/
	}
}
