local this = {};

local error_handler;

local sdk = sdk;
local tostring = tostring;
local pairs = pairs;
local ipairs = ipairs;
local tonumber = tonumber;
local require = require;
local pcall = pcall;
local table = table;
local string = string;
local Vector3f = Vector3f;
local d2d = d2d;
local math = math;
local json = json;
local log = log;
local fs = fs;
local next = next;
local type = type;
local setmetatable = setmetatable;
local getmetatable = getmetatable;
local assert = assert;
local select = select;
local coroutine = coroutine;
local utf8 = utf8;
local re = re;
local imgui = imgui;
local draw = draw;
local Vector2f = Vector2f;
local reframework = reframework;
local os = os;
local ValueType = ValueType;
local package = package;

local table_tostring;
local deep_copy;
local merge;
local is_empty;
local unicode_map;
local unicode_relative_position;
local unicode_chars;
local epsilon = 0.000001;

this.table = {};
this.type = {};
this.number = {};
this.string = {};
this.vec2 = {};
this.vec3 = {};
this.vec4 = {};
this.math = {};
this.unicode = {};
this.sdk = {};

this.constants = {};
this.constants.uninitialized_int = -420;
this.constants.uninitialized_string = "Uninitialized";
this.constants.epsilon = epsilon;

function this.table.tostring(table_)
	if type(table_) == "number" or type(table_) == "boolean" or type(table_) == "string" then
		return tostring(table_);
	end

	if is_empty(table_) then
		return "{}"; 
	end

	local cache = {};
	local stack = {};
	local output = {};
    local depth = 1;
    local output_str = "{\n";

    while true do
        local size = 0;
        for k,v in pairs(table_) do
            size = size + 1;
        end

        local cur_index = 1;
        for k,v in pairs(table_) do
            if cache[table_] == nil or cur_index >= cache[table_] then

                if string.find(output_str, "}", output_str:len()) then
                    output_str = output_str .. ",\n";
                elseif not string.find(output_str, "\n", output_str:len()) then
                    output_str = output_str .. "\n";
                end

                -- This is necessary for working with HUGE tables otherwise we run out of memory using concat on huge strings
                table.insert(output,output_str);
                output_str = "";

                local key;
                if type(k) == "number" or type(k) == "boolean" then
                    key = "[" .. tostring(k) .. "]";
                else
                    key = "['" .. tostring(k) .. "']";
                end

                if type(v) == "number" or type(v) == "boolean" then
                    output_str = output_str .. string.rep('\t', depth) .. key .. " = "..tostring(v);
                elseif type(v) == "table" then
                    output_str = output_str .. string.rep('\t', depth) .. key .. " = {\n";
                    table.insert(stack, table_);
                    table.insert(stack, v);
                    cache[table_] = cur_index + 1;
                    break;
                else
                    output_str = output_str .. string.rep('\t', depth) .. key .. " = '" .. tostring(v) .. "'";
                end

                if cur_index == size then
                    output_str = output_str .. "\n" .. string.rep('\t', depth - 1) .. "}";
                else
                    output_str = output_str .. ",";
                end
            else
                -- close the table
                if cur_index == size then
                    output_str = output_str .. "\n" .. string.rep('\t', depth - 1) .. "}";
                end
            end

            cur_index = cur_index + 1;
        end

        if size == 0 then
            output_str = output_str .. "\n" .. string.rep('\t', depth - 1) .. "}";
        end

        if #stack > 0 then
            table_ = stack[#stack];
            stack[#stack] = nil;
            depth = cache[table_] == nil and depth + 1 or depth - 1;
        else
            break;
        end
    end

    -- This is necessary for working with HUGE tables otherwise we run out of memory using concat on huge strings
    table.insert(output, output_str);
    output_str = table.concat(output);

    return output_str;
end

function this.table.tostringln(table_)
	return "\n" .. table_tostring(table_);
end

function this.table.is_empty(table_)
	return next(table_) == nil;
end

function this.table.deep_copy(original, copies)
	copies = copies or {};
	local original_type = type(original);
	local copy;
	if original_type == "table" then
		if copies[original] then
			copy = copies[original];
		else
			copy = {};
			copies[original] = copy;
			for original_key, original_value in next, original, nil do
				copy[deep_copy(original_key, copies)] = deep_copy(original_value,copies);
			end
			setmetatable(copy, deep_copy(getmetatable(original), copies));
		end
	else -- number, string, boolean, etc
		copy = original;
	end
	return copy;
end

function this.table.find_index(table_, value, nullable)
	for i = 1, #table_ do
		if table_[i] == value then
			return i;
		end
	end

	if not nullable then
		return 1;
	end

	return nil;
end

function this.table.merge(...)
	local tables_to_merge = { ... };
	assert(#tables_to_merge > 1, "There should be at least two tables to merge them");

	for key, table_ in ipairs(tables_to_merge) do
		assert(type(table_) == "table", string.format("Expected a table as function parameter %d", key));
	end

	local result = deep_copy(tables_to_merge[1]);

	for i = 2, #tables_to_merge do
		local from = tables_to_merge[i];
		for key, value in pairs(from) do
			if type(value) == "table" then
				result[key] = result[key] or {};
				assert(type(result[key]) == "table", string.format("Expected a table: '%s'", key));
				result[key] = merge(result[key], value);
			else
				result[key] = value;
			end
		end
	end

	return result;
end

function this.type.is_boolean(value)
	return value == true or value == false;
end

function this.type.is_REField(value)
	return 	value.get_return_type == nil;
end

function this.type.is_REMethodDefinition(value)
	return 	value.get_return_type ~= nil;
end

function this.number.is_NaN(value)
	return tostring(value) == tostring(0/0);
end

function this.number.round(value)
	return math.floor(value + 0.5);
end

function this.number.is_odd(value)
	return value % 2 ~= 0;
end

function this.number.is_even(value)
	return value % 2 == 0;
end

function this.number.is_equal(value1, value2)
	if math.abs(value1 - value2) < epsilon then
		return true;
	end

	return false;
end

function this.string.trim(str)
	return str:match("^%s*(.-)%s*$");
end

function this.string.starts_with(str, pattern)
	return str:find("^" .. pattern) ~= nil;
end

function this.vec2.tostring(vector2f)
	return string.format("<%f, %f>", vector2f.x, vector2f.y);
end

function this.vec2.random(distance)
	distance = distance or 1;
	local radians = math.random() * math.pi * 2;
	return Vector2f.new(
		distance * math.cos(radians),
		distance * math.sin(radians)
	);
end

function this.vec3.tostring(vector3f)
	return string.format("<%f, %f, %f>", vector3f.x, vector3f.y, vector3f.z);
end

function this.vec4.tostring(vector4f)
	return string.format("<%f, %f, %f, %f>", vector4f.x, vector4f.y, vector4f.z, vector4f.w);
end

--- When called without arguments, returns a pseudo-random float with uniform distribution in the range [0,1). When called with two floats min and max, math.random returns a pseudo-random float with uniform distribution in the range [min, max). The call .random(max) is equivalent to .random(1, max)
---@param min number
---@param max number
---@return number
function this.math.random(min, max)
	if min == nil and max == nil then
		return math.random();
	end

	if max == nil then
		return max * math.random();
	end

	return min + (max - min) * math.random();
end

function this.math.sign(value)
	return (value >= 0 and 1) or -1;
end
function this.math.round(value, bracket)
	bracket = bracket or 1;
	return math.floor(value / bracket + this.math.sign(value) * 0.5) * bracket;
end

-- https://github.com/blitmap/lua-utf8-simple/blob/master/utf8_simple.lua

-- ABNF from RFC 3629
--
-- UTF8-octets = *( UTF8-char )
-- UTF8-char = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4
-- UTF8-1 = %x00-7F
-- UTF8-2 = %xC2-DF UTF8-tail
-- UTF8-3 = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) /
-- %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail )
-- UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) /
-- %xF4 %x80-8F 2( UTF8-tail )
-- UTF8-tail = %x80-BF

-- 0xxxxxxx                            | 007F   (127)
-- 110xxxxx	10xxxxxx                   | 07FF   (2047)
-- 1110xxxx	10xxxxxx 10xxxxxx          | FFFF   (65535)
-- 11110xxx	10xxxxxx 10xxxxxx 10xxxxxx | 10FFFF (1114111)

local pattern = "[%z\1-\127\194-\244][\128-\191]*";

-- helper function
function this.unicode.relative_position(position, length)
	if position < 0 then
		position = length + position + 1;
	end
	return position;
end

-- THE MEAT

-- maps f over s's utf8 characters f can accept args: (visual_index, utf8_character, byte_index)
function this.unicode.map(s, f, no_subs)
	local i = 0;

	if no_subs then
		for b, e in s:gmatch("()" .. pattern .. "()") do
			i = i + 1;
			local c = e - b;
			f(i, c, b);
		end
	else
		for b, c in s:gmatch("()(" .. pattern .. ")") do
			i = i + 1;
			f(i, c, b);
		end
	end
end

-- THE REST

-- returns the number of characters in a UTF-8 string
function this.unicode.len(s)
	-- count the number of non-continuing bytes
	return select(2, s:gsub('[^\128-\193]', ''));
end

-- replace all utf8 chars with mapping
function this.unicode.replace(s, map)
	return s:gsub(pattern, map);
end

-- reverse a utf8 string
function this.unicode.reverse(s)
	-- reverse the individual greater-than-single-byte characters
	s = s:gsub(pattern, function (c)
		return #c > 1 and c:reverse()
	end);

	return s:reverse();
end

-- strip non-ascii characters from a utf8 string
function this.unicode.strip(s)
	return s:gsub(pattern, function(c)
		return #c > 1 and '';
	end);
end

-- generator for the above -- to iterate over all utf8 chars
function this.unicode.chars(s, no_subs)
	return coroutine.wrap(function()
		return unicode_map(s, coroutine.yield, no_subs);
	end);
end

-- like string.sub() but i, j are utf8 strings
-- a utf8-safe string.sub()
function this.unicode.sub(str, i, j)
	if coroutine == nil then
		return str;
	end

	local l = utf8.len(str);

	i = unicode_relative_position(i, l);
	j = j and unicode_relative_position(j, l) or l;

	if i < 1 then
		i = 1;
	end

	if j > l then
		j = l;
	end

	if i > j then
		return "";
	end

	local diff = j - i;
	local iterator = unicode_chars(str, true);

	-- advance up to i
	for _ = 1, i - 1 do
		iterator();
	end

	local c, b = select(2, iterator());

	-- i and j are the same, single-charaacter sub
	if diff == 0 then
		return string.sub(str, b, b + c - 1);
	end

	i = b;

	-- advance up to j
	for _ = 1, diff - 1 do
		iterator();
	end

	c, b = select(2, iterator());

	return string.sub(str, i, b + c - 1);
end

function this.sdk.generate_enum(type_def)
	if not type_def then
		return {};
	end

	local fields = type_def:get_fields();
	local enum = {};

	for i, field in ipairs(fields) do
		if field:is_static() then
			local name = field:get_name();
			local raw_value = field:get_data(nil);

			local enum_entry = {
				name = name,
				value = raw_value;
			};

			table.insert(enum, enum_entry);
		end
	end

	return enum;
end

function this.sdk.generate_enum_by_typename(type_name)
	local type_def = sdk.find_type_definition(type_name);
	if not type_def then
		return {};
	end;

	return this.sdk.generate_enum(type_def);
end

function this.init_dependencies()
	error_handler = require("MHR_Overlay.Misc.error_handler");
end

function this.init_module()
end

table_tostring = this.table.tostring;
deep_copy = this.table.deep_copy;
merge = this.table.merge;
is_empty = this.table.is_empty;
unicode_map = this.unicode.map;
unicode_relative_position = this.unicode.relative_position;
unicode_chars = this.unicode.chars;

return this;