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

struct FlatBuffer
{
	Tokenizer   tk;
	Arena       arena;
	Arena       ls_arena;
	u8[]	      file_name;
	u8[]        data;
	LineStart[] line_starts;
	u64         length;
	u64         lf_count;

	i64         buf_pos;
	i64         offset;
	i64         rows;

	I64Vec2     sel;
	SelectMode  sel_mode;

	bool        dirty;
}

enum SelectMode
{
	None,
	Normal,
	Line,
}

alias SM = SelectMode;

struct LineStart
{
	u64 pos;
	u16 level;
}

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

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,
];

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

	u8[] buf = Alloc!(u8)(capacity);
	buf[0 .. data.length] = data[0 .. data.length];

	FlatBuffer fb = {
		arena: CreateArena(MB(1)),
		ls_arena: CreateArena(KB(512)),
		data: buf,
		length: cast(u64)data.length,
		lf_count: CountLF(data),
		sel: -1,
	};

	fb.tk = CreateTokenizer(&fb);

	Fix(&fb);

	return fb;
}

void
Change(FlatBuffer* fb, u8[] data)
{
	FreeArray(fb.data);

	u64 cap = data.length > 0 ? RoundUp(cast(u64)(data.length) * 2, KB(4)) : KB(4);

	fb.data = Alloc!(u8)(cap);
	fb.data[0 .. data.length] = data[0 .. $];
	fb.length = data.length;
	fb.lf_count = CountLF(data);

	ResetTokenizer(fb);

	Fix(fb);
}

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

	return lf_count;
}

void
Fix(FlatBuffer* fb) 
{
	with(fb)
	{
		Reset(&ls_arena);
		line_starts = AllocArray!(LineStart)(&ls_arena, lf_count+1);
		line_starts[0].pos = 0;

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

			if(data[i] == '}' || data[i] == ')' || data[i] == ']')
			{
				if(ignore > 0)
				{
					ignore -= 1;
				}
				else if(pending_level == 1)
				{
					pending_level = 0;
				}
				else if(level > 0)
				{
					level -= 1;
				}
			}

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

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

				ls_idx += 1;
			}
		}

		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)
	{
		fb.tk.buffer = ReallocArray!(TokenStyle)(fb.tk.buffer, fb.data.length*2);
		fb.data = ReallocArray!(u8)(fb.data, fb.data.length*2);
	}

	u64 prev_lf = fb.lf_count;
	for(u64 i = 0; i < length; i += 1)
	{
		fb.buf_pos += 1;
		fb.lf_count += 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.buf_pos += 1)
			{
				insert[length] = '\t';
			}

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

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

	u64 temp_len = fb.length-pos;

	u8[] temp = AllocCopySlice!(u8)(&fb.arena, fb.data, 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;

	Fix(fb);

	Reset(&fb.arena);

	UpdateOffset(fb);
}

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.sel = fb.buf_pos;
	}
	else if(mode == SM.Line)
	{
		fb.sel = CurrentLine(fb);
	}
}

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

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

pragma(inline) u64
CurrentLine(FlatBuffer* fb)
{
	return LineFromPos(fb, fb.buf_pos);
}

pragma(inline) u64
CurrentCol(FlatBuffer* fb)
{
	return LinePos(fb, fb.buf_pos);
}

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

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

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

		line = i;
	}

	return line;
}

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

pragma(inline) u64
LineLength(FlatBuffer* fb, u64 line)
{
	u64 len = 0;
	if(line < fb.line_starts.length)
	{
		u64 start = fb.line_starts[line].pos;
		u64 end = fb.line_starts.length-1 > line ? fb.line_starts[line+1].pos : fb.length;
		len = end-start;
	}

	return len;
}

void
GetLines(FlatBuffer* fb, LineBuffers* linebufs, u64 length)
{
	GetLines(fb, linebufs, fb.offset, length);
}

pragma(inline) void
SliceLineBuffer(FlatBuffer* fb, LineBuffers* lbufs, LineBuffer* lbuf, u64 start, u64 len)
{
	lbuf.text  = fb.data[start .. start+len];
	lbuf.style = !Nil(fb.tk.first) ? fb.tk.buffer[start .. start+len] : [];

	lbufs.count += 1;
}

void
GetLines(FlatBuffer* fb, LineBuffers* linebufs, u64 start_line, u64 length)
{
	assert(linebufs != null, "GetLines failure: linebufs is null");

	Reset(&linebufs.arena);
	linebufs.first = Alloc!(LineBuffer)(&linebufs.arena);
	linebufs.count = 0;

	u64 total_lines = fb.line_starts.length;
	start_line = Min(start_line, total_lines);
	u64 end_line = Min(start_line + length, total_lines);

	if(fb.length == 0)
	{
		linebufs.first.text = AllocArray!(u8)(&linebufs.arena, 1);
	}
	else if(start_line == end_line) with(fb)
	{
		SliceLineBuffer(fb, linebufs, linebufs.first, line_starts[start_line].pos, LineLength(fb, start_line));
	}
	else with(linebufs)
	{
		LineBuffer* current = first;
		for(u64 i = start_line; i < end_line; i += 1)
		{
			u64 start = fb.line_starts[i].pos;
			u64 len = LineLength(fb, i);

			if(len > 0)
			{
				SliceLineBuffer(fb, linebufs, current, start, len);
				current.next = Alloc!(LineBuffer)(&arena);
				current = current.next;
			}

			if(i == fb.lf_count && fb.data[fb.length-1] == '\n')
			{
				current.text = AllocArray!(u8)(&arena, 1);
				count += 1;

				current.next = Alloc!(LineBuffer)(&arena);
			}
		}
	}
}

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

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

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

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

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

void
MoveDown(FlatBuffer* fb, u64 col)
{
	u64 line = CurrentLine(fb);
	if(line+1 < fb.line_starts.length)
	{
		line += 1;
		fb.buf_pos = fb.line_starts[line].pos;
		u64 end = fb.line_starts.length > line+1 ? fb.line_starts[line+1].pos-1 : fb.length;
		fb.buf_pos = Min(fb.buf_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.buf_pos != 0 && up) || (fb.buf_pos != fb.length && !up))
	{
		for(u64 i = fb.buf_pos + step; true; i += step)
		{
			static if(!up)
			{
				if(i == fb.length-1)
				{
					fb.buf_pos = fb.length;
					break;
				}

				ch = fb.data[i];
			}

			static if(up)
			{
				if(i == 0) 
				{
					fb.buf_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.buf_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.buf_pos < fb.length-1) || (!forward && fb.buf_pos > 0))
	{
		ChType type = GetChType(fb.data[fb.buf_pos]);
		if(type != GetChType(fb.data[fb.buf_pos+step]))
		{
			fb.buf_pos += step;
		}
		else for(u64 i = fb.buf_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.buf_pos = i;
				fb.buf_pos += u64(GetChType(next) == type);

				break;
			}

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

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

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

			fb.buf_pos += step;
		}
	}
}

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

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

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

		u8 ch = fb.data[i];

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

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

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

	}

	assert(fb.buf_pos >= 0 && fb.buf_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);
				} break;
			case Down:
				{
					MoveToEmptyLine!(false)(fb);
				} break;
			case Left:
				{
					if(fb.buf_pos > 0)
					{
						MoveToWordEdge!(false)(fb);
					}
				} break;
			case Right:
				{
					if(fb.buf_pos < fb.length)
					{
						MoveToWordEdge!(true)(fb);
					}
				} break;
			case Home: fb.buf_pos = 0;         taken = true; break;
			case End:  fb.buf_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.buf_pos > 0)
					{
						MoveToNextWord!(false)(fb);
					}
				} break;
			case Right:
				{
					if(fb.buf_pos < fb.length)
					{
						MoveToNextWord!(true)(fb);
					}
				} 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.buf_pos > 0 && fb.data[fb.buf_pos-1] != '\n')
					{
						fb.buf_pos -= 1;
						taken = true;
					}
				} break;
			case Right:
				{
					if(fb.buf_pos < fb.length && fb.data[fb.buf_pos] != '\n')
					{
						fb.buf_pos += 1;
						taken = true;
					}
				} break;
			case Home: MoveToSOL(fb); taken = true; break;
			case End:  MoveToEOL(fb); taken = true; break;
			default: break;
		}
	}

	UpdateOffset(fb);
	UpdateSelection(fb);

	Logf("selection %s", fb.sel.v);

	return taken;
}

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

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

void
UpdateOffset(FlatBuffer* fb)
{
	if(fb.rows > 0) with(fb)
	{
		i64 screen_pos = CurrentLine(fb) - offset;
		i64 start = 0;
		i64 end = rows-2;
		if(offset > 0 && screen_pos < start)
		{
			offset += screen_pos;
		}
		else if(screen_pos > end)
		{
			offset += screen_pos - end;
		}
	}

	assert(fb.offset <= fb.lf_count);
}

void
Backspace(FlatBuffer* fb)
{
	if(fb.buf_pos-1 >= 0)
	{
		fb.buf_pos -= 1;
		Delete(fb, 1, fb.buf_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.lf_count -= 1;
		}
	}

	u8[] temp;
	if(end != fb.length)
	{
		temp = AllocArray!(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);

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