--!native
--!optimize 2
local Tokenizer = require("@self/Tokenizer")
local Types = require("@self/Types")
local models = require("@self/models")
local decoders = require("@self/decoders")
local pre_tokenizers = require("@self/pre_tokenizers")
local processors = require("@self/processors")
local normalizers = require("@self/normalizers")
local trainers = require("@self/trainers")

return {
	Tokenizer = Tokenizer,
	Types = Types,
	models = models,
	decoders = decoders,
	pre_tokenizers = pre_tokenizers,
	processors = processors,
	normalizers = normalizers,
	trainers = trainers,
}


--!native
--!optimize 2
local BPE = {}
BPE.__index = BPE

function BPE.new(vocab: { [string]: number }?, merges: { { string } }?, unk_token: string?, suffix: string?)
	local self = setmetatable({}, BPE)
	self.vocab = vocab or {}
	self.merges = merges or {}
	self.unk_token = unk_token or "[UNK]"
	self.suffix = suffix or "</w>"
	self.ids_to_tokens = {}
	for k, v in pairs(self.vocab) do
		self.ids_to_tokens[v] = k
	end
	self.merge_ranks = {}
	if merges then
		for i, pair in ipairs(merges) do
			local key = `{pair[1]} {pair[2]}`
			self.merge_ranks[key] = i
		end
	end
	self.ignore_merges = false
	return self
end

function BPE:set_ignore_merges(ignore: boolean)
	self.ignore_merges = ignore
end

function BPE:tokenize(sequences: { string })
	local tokens = {}
	for _, word in ipairs(sequences) do
		local word_tokens = self:_tokenize_word(word)
		for _, t in ipairs(word_tokens) do
			table.insert(tokens, t)
		end
	end
	return tokens
end

function BPE:_tokenize_word(word: string)
	local splits = {}
	local len = #word
	if len == 0 then
		return {}
	end
	for i = 1, len - 1 do
		table.insert(splits, string.sub(word, i, i))
	end
	table.insert(splits, string.sub(word, len, len) .. self.suffix)
	if self.ignore_merges then
		for i = 1, #splits do
			if not self.vocab[splits[i]] then
				splits[i] = self.unk_token
			end
		end
		return splits
	end
	while #splits > 1 do
		local min_rank = math.huge
		local best_pair_idx = -1
		for i = 1, #splits - 1 do
			local pair = `{splits[i]} {splits[i + 1]}`
			local rank = self.merge_ranks[pair]
			if rank and rank < min_rank then
				min_rank = rank
				best_pair_idx = i
			end
		end
		if best_pair_idx == -1 then
			break
		end
		local p1 = splits[best_pair_idx]
		local p2 = splits[best_pair_idx + 1]
		table.remove(splits, best_pair_idx + 1)
		splits[best_pair_idx] = `{p1}{p2}`
	end
	for i = 1, #splits do
		if not self.vocab[splits[i]] then
			splits[i] = self.unk_token
		end
	end
	return splits
end

function BPE:token_to_id(token: string)
	return self.vocab[token] or self.vocab[self.unk_token]
end

function BPE:id_to_token(id: number)
	return self.ids_to_tokens[id]
end

function BPE:get_vocab()
	return self.vocab
end

function BPE:get_vocab_size()
	local count = 0
	for _ in self.vocab do
		count += 1
	end
	return count
end

function BPE:dump()
	return {
		type = "BPE",
		vocab = self.vocab,
		merges = self.merges,
		unk_token = self.unk_token,
		suffix = self.suffix,
		ignore_merges = self.ignore_merges,
	}
end

function BPE.from_dump(data)
	local self = BPE.new(data.vocab, data.merges, data.unk_token, data.suffix)
	if data.ignore_merges then
		self:set_ignore_merges(data.ignore_merges)
	end
	return self
end

return BPE


--!optimize 2
local NFKC = {}
NFKC.__index = NFKC

function NFKC.new()
	local self = setmetatable({}, NFKC)
	return self
end

function NFKC:normalize(text: string)
	text = string.gsub(text, "\194\160", " ")
	return text
end

function NFKC:dump()
	return {
		type = "NFKC",
	}
end

NFKC.from_dump = NFKC.new

return NFKC


--!optimize 2
local Sequence = {}
Sequence.__index = Sequence

local Types = require("../Types")

function Sequence.new(normalizers: { Types.Normalizer })
	local self = setmetatable({}, Sequence)
	self.normalizers = normalizers or {}
	return self
end

function Sequence:normalize(text: string)
	for _, n in ipairs(self.normalizers) do
		text = n:normalize(text)
	end
	return text
end

Sequence.from_dump = Sequence.new

return Sequence


local Sequence = require("@self/Sequence")
local NFKC = require("@self/NFKC")

return {
	Sequence = Sequence,
	NFKC = NFKC,
}


--!native
--!optimize 2
local ByteLevel = {}
ByteLevel.__index = ByteLevel

function ByteLevel.new(add_prefix_space: boolean?, use_regex: boolean?)
	local self = setmetatable({}, ByteLevel)
	self.add_prefix_space = add_prefix_space == nil and true or add_prefix_space
	self.use_regex = use_regex == nil and true or use_regex
	return self
end

function ByteLevel:pre_tokenize(text: string)
	local original_text = text
	if self.add_prefix_space and not text:match("^%s") then
		text = ` {text}`
	end
	local offset_shift = if self.add_prefix_space and text ~= original_text then -1 else 0
	local splits = {}
	local offsets = {}
	local current_token = ""
	local last_type = nil
	local token_start = 1
	for i = 1, #text do
		local char = string.sub(text, i, i)
		local char_type = "other"
		if char:match("%s") then
			char_type = "space"
		elseif char:match("%a") then
			char_type = "letter"
		elseif char:match("%d") then
			char_type = "number"
		elseif char:match("%p") then
			char_type = "punct"
		end
		local split = false
		if last_type == nil then
		elseif last_type == "space" and char_type ~= "space" then
			split = false
		elseif last_type ~= "space" and char_type == "space" then
			split = true
		elseif last_type ~= char_type then
			split = true
		end
		if split then
			table.insert(splits, current_token)
			local s = math.max(1, token_start + offset_shift)
			local e = math.max(0, (i - 1) + offset_shift)
			if e < s then
				table.insert(offsets, { 0, 0 })
			else
				table.insert(offsets, { s, e })
			end
			current_token = ""
			token_start = i
		end
		current_token = `{current_token}{char}`
		last_type = char_type
	end
	if #current_token > 0 then
		table.insert(splits, current_token)
		local s = math.max(1, token_start + offset_shift)
		local e = math.max(0, #text + offset_shift)
		if e < s then
			table.insert(offsets, { 0, 0 })
		else
			table.insert(offsets, { s, e })
		end
	end
	return splits, offsets
end

function ByteLevel:alphabet()
	local chars = {}
	for i = 0, 255 do
		table.insert(chars, string.char(i))
	end
	return chars
end

return ByteLevel


--!native
--!optimize 2
local Sequence = {}
Sequence.__index = Sequence

local Types = require("../Types")

function Sequence.new(pre_tokenizers: { Types.PreTokenizer })
	local self = setmetatable({}, Sequence)
	self.pre_tokenizers = pre_tokenizers or {}
	return self
end

function Sequence:pre_tokenize(text: string)
	local current_tokens = { text }
	local current_offsets = { { 1, #text } }
	for _, pt in ipairs(self.pre_tokenizers) do
		local new_tokens = {}
		local new_offsets = {}
		for i, token in ipairs(current_tokens) do
			local sub_tokens, sub_offsets = pt:pre_tokenize(token)
			for j, sub_token in ipairs(sub_tokens) do
				table.insert(new_tokens, sub_token)
				local start_off, end_off = sub_offsets[j][1], sub_offsets[j][2]
				if start_off == 0 and end_off == 0 then
					table.insert(new_offsets, { 0, 0 })
				else
					local base_offset = current_offsets[i][1]
					table.insert(new_offsets, { base_offset + start_off - 1, base_offset + end_off - 1 })
				end
			end
		end
		current_tokens = new_tokens
		current_offsets = new_offsets
	end
	return current_tokens, current_offsets
end

return Sequence


--!native
--!optimize 2
local Split = {}
Split.__index = Split

type Behavior = "Removed" | "Isolated" | "MergedWithPrevious" | "MergedWithNext"

function Split.new(pattern: string, behavior: Behavior, invert: boolean?)
	local self = setmetatable({}, Split)
	self.pattern = pattern
	self.behavior = behavior or "Removed"
	self.invert = invert or false
	return self
end

function Split:pre_tokenize(text: string)
	local splits = {}
	local offsets = {}

	if self.invert then
		for s, e in string.gmatch(text, "()" .. self.pattern .. "()") do
			table.insert(splits, string.sub(text, s, e - 1))
			table.insert(offsets, { s, e - 1 })
		end
		return splits, offsets
	end

	local last_pos = 1
	for s, e in string.gmatch(text, "()" .. self.pattern .. "()") do
		local pre_segment = string.sub(text, last_pos, s - 1)
		local separator = string.sub(text, s, e - 1)

		if self.behavior == "Removed" then
			if #pre_segment > 0 then
				table.insert(splits, pre_segment)
				table.insert(offsets, { last_pos, s - 1 })
			end
			last_pos = e
		elseif self.behavior == "Isolated" then
			if #pre_segment > 0 then
				table.insert(splits, pre_segment)
				table.insert(offsets, { last_pos, s - 1 })
			end
			table.insert(splits, separator)
			table.insert(offsets, { s, e - 1 })
			last_pos = e
		elseif self.behavior == "MergedWithPrevious" then
			if #pre_segment > 0 then
				table.insert(splits, pre_segment .. separator)
				table.insert(offsets, { last_pos, e - 1 })
			else
				table.insert(splits, separator)
				table.insert(offsets, { s, e - 1 })
			end
			last_pos = e
		elseif self.behavior == "MergedWithNext" then
			if #pre_segment > 0 then
				table.insert(splits, pre_segment)
				table.insert(offsets, { last_pos, s - 1 })
			end
			last_pos = s
		end
	end
	if last_pos <= #text then
		table.insert(splits, string.sub(text, last_pos))
		table.insert(offsets, { last_pos, #text })
	end
	return splits, offsets
end

return Split


--!native
--!optimize 2
local ByteLevel = {}
ByteLevel.__index = ByteLevel

local Types = require("../Types")

function ByteLevel.new(trim_offsets: boolean?)
	local self = setmetatable({}, ByteLevel)
	self.trim_offsets = trim_offsets == nil and true or trim_offsets
	return self
end

function ByteLevel:process(encoding: Types.Encoding, pair_encoding: Types.Encoding?): Types.Encoding
	if pair_encoding then
		local new_ids = {}
		local new_tokens = {}
		local new_type_ids = {}
		local new_attention_mask = {}
		local new_special_tokens_mask = {}
		local new_offsets = {}
		local function add(enc, type_id)
			for i = 1, #enc.ids do
				table.insert(new_ids, enc.ids[i])
				table.insert(new_tokens, enc.tokens[i])
				table.insert(new_type_ids, type_id)
				table.insert(new_attention_mask, enc.attention_mask[i])
				table.insert(new_special_tokens_mask, enc.special_tokens_mask[i])
				table.insert(new_offsets, enc.offsets[i])
			end
		end
		add(encoding, 0)
		add(pair_encoding, 1)
		return {
			ids = new_ids,
			tokens = new_tokens,
			offsets = new_offsets,
			attention_mask = new_attention_mask,
			special_tokens_mask = new_special_tokens_mask,
			type_ids = new_type_ids,
		}
	end
	return encoding
end

return ByteLevel


--!native
--!optimize 2
local Types = require("../Types")

local TemplateProcessing = {}
TemplateProcessing.__index = TemplateProcessing

export type Template = { string | { id: string, type_id: number } }

function TemplateProcessing.new(single: Template, pair: Template, special_tokens: { [string]: number })
	local self = setmetatable({}, TemplateProcessing)
	self.single = single
	self.pair = pair
	self.special_tokens = special_tokens
	return self
end

function TemplateProcessing:process(encoding: Types.Encoding, pair_encoding: Types.Encoding?): Types.Encoding
	local template = if pair_encoding then self.pair else self.single

	local new_ids = {}
	local new_tokens = {}
	local new_type_ids = {}
	local new_attention_mask = {}
	local new_special_tokens_mask = {}
	local new_offsets = {}

	local function add_seq(enc: Types.Encoding, type_id: number)
		for i = 1, #enc.ids do
			table.insert(new_ids, enc.ids[i])
			table.insert(new_tokens, enc.tokens[i])
			table.insert(new_type_ids, type_id)
			table.insert(new_attention_mask, 1)
			table.insert(new_special_tokens_mask, 0)
			table.insert(new_offsets, enc.offsets[i])
		end
	end
	local function add_special(token: string, type_id: number)
		local id = self.special_tokens[token]
		if not id then
			warn("Special token not found in map: " .. token)
			id = 0
		end
		table.insert(new_ids, id)
		table.insert(new_tokens, token)
		table.insert(new_type_ids, type_id)
		table.insert(new_attention_mask, 1)
		table.insert(new_special_tokens_mask, 1)
		table.insert(new_offsets, { 0, 0 })
	end
	for _, item in ipairs(template) do
		if type(item) == "string" then
			if item == "$A" then
				add_seq(encoding, 0)
			elseif item == "$B" then
				if pair_encoding then
					add_seq(pair_encoding, 1)
				end
			else
				add_special(item, 0)
			end
		elseif type(item) == "table" then
			add_special(item.id, item.type_id)
		end
	end
	return {
		ids = new_ids,
		tokens = new_tokens,
		offsets = new_offsets,
		attention_mask = new_attention_mask,
		special_tokens_mask = new_special_tokens_mask,
		type_ids = new_type_ids,
	}
end

return TemplateProcessing


local TemplateProcessing = require("@self/TemplateProcessing")
local ByteLevel = require("@self/ByteLevel")

return {
	TemplateProcessing = TemplateProcessing,
	ByteLevel = ByteLevel,
}


--!native
--!optimize 2
local BpeTrainer = {}
BpeTrainer.__index = BpeTrainer

local Types = require("../Types")
local BPE = require("../models/BPE")

type BpeTrainerOptions = {
	vocab_size: number?,
	min_frequency: number?,
	special_tokens: { string }?,
	show_progress: boolean?,
	initial_alphabet: { string }?,
	limit_alphabet: number?,
	continuing_subword_prefix: string?,
	end_of_word_suffix: string?,
	pre_tokenizer: Types.PreTokenizer?,
}

function BpeTrainer.new(options: BpeTrainerOptions)
	local self = setmetatable({}, BpeTrainer)
	self.vocab_size = options.vocab_size or 30000
	self.min_frequency = options.min_frequency or 0
	self.special_tokens = options.special_tokens or {}
	self.show_progress = options.show_progress or false
	self.initial_alphabet = options.initial_alphabet or {}
	self.limit_alphabet = options.limit_alphabet
	self.continuing_subword_prefix = options.continuing_subword_prefix
	self.end_of_word_suffix = options.end_of_word_suffix or "</w>"
	self.pre_tokenizer = options.pre_tokenizer
	return self
end

function BpeTrainer:train(iterator: () -> string?): (Types.Model, { string })
	local word_counts: { [string]: number } = {}
	local alphabet: { [string]: number } = {}
	if self.show_progress then
		print("Counting words...")
	end
	for text in iterator do
		local words = {}
		if self.pre_tokenizer then
			local pre_tokenized, _ = self.pre_tokenizer:pre_tokenize(text)
			words = pre_tokenized
		else
			for word in string.gmatch(text, "%S+") do
				table.insert(words, word)
			end
		end
		for _, word in ipairs(words) do
			word_counts[word] = (word_counts[word] or 0) + 1
			for i = 1, #word do
				local char = string.sub(word, i, i)
				alphabet[char] = (alphabet[char] or 0) + 1
			end
		end
	end
	local vocab: { [string]: number } = {}
	local merges: { { string } } = {}
	local next_id = 0
	for _, token in ipairs(self.special_tokens) do
		if not vocab[token] then
			vocab[token] = next_id
			next_id += 1
		end
	end
	for _, char in ipairs(self.initial_alphabet) do
		if not alphabet[char] then
			alphabet[char] = 0
		end
	end
	local sorted_alphabet = {}
	for char in pairs(alphabet) do
		table.insert(sorted_alphabet, char)
	end
	table.sort(sorted_alphabet)
	for _, char in ipairs(sorted_alphabet) do
		if not vocab[char] then
			if next_id >= self.vocab_size then
				break
			end
			vocab[char] = next_id
			next_id += 1
		end
	end
	local splits: { { string } } = {}
	local counts: { number } = {}
	for word, count in pairs(word_counts) do
		local split = {}
		for i = 1, #word do
			table.insert(split, string.sub(word, i, i))
		end
		if self.end_of_word_suffix then
			split[#split] ..= self.end_of_word_suffix
		end
		table.insert(splits, split)
		table.insert(counts, count)
	end
	local initial_symbols = {}
	for _, split in ipairs(splits) do
		for _, symbol in ipairs(split) do
			if not vocab[symbol] then
				initial_symbols[symbol] = true
			end
		end
	end
	local sorted_symbols = {}
	for s in pairs(initial_symbols) do
		table.insert(sorted_symbols, s)
	end
	table.sort(sorted_symbols)
	for _, s in ipairs(sorted_symbols) do
		if next_id < self.vocab_size then
			vocab[s] = next_id
			next_id += 1
		end
	end
	if self.show_progress then
		print("Training BPE...")
	end
	local pair_counts: { [string]: number } = {}
	for i, split in ipairs(splits) do
		local count = counts[i]
		if #split >= 2 then
			for j = 1, #split - 1 do
				local pair = `{split[j]}\0{split[j + 1]}`
				pair_counts[pair] = (pair_counts[pair] or 0) + count
			end
		end
	end
	while next_id < self.vocab_size do
		local best_pair = nil
		local max_freq = -1
		for pair, freq in pairs(pair_counts) do
			if freq > max_freq then
				max_freq = freq
				best_pair = pair
			elseif freq == max_freq then
				if best_pair and pair < best_pair then
					best_pair = pair
				end
			end
		end
		if not best_pair or max_freq < self.min_frequency then
			break
		end
		local p1, p2 = best_pair:match("^(.*)%z(.*)$")
		local merged_token = `{p1}{p2}`
		vocab[merged_token] = next_id
		next_id += 1
		table.insert(merges, { p1, p2 })
		pair_counts[best_pair] = nil
		for i, split in ipairs(splits) do
			if #split >= 2 then
				local new_split = {}
				local j = 1
				local changed = false
				while j <= #split do
					if j < #split and split[j] == p1 and split[j + 1] == p2 then
						local count = counts[i]
						if j > 1 then
							local left = new_split[#new_split]
							local pair_left = `{left}\0{p1}`
							if pair_left ~= best_pair then
								pair_counts[pair_left] = (pair_counts[pair_left] or 0) - count
								if pair_counts[pair_left] <= 0 then
									pair_counts[pair_left] = nil
								end
							end
							local new_pair_left = `{left}\0{merged_token}`
							pair_counts[new_pair_left] = (pair_counts[new_pair_left] or 0) + count
						end
						if j + 1 < #split then
							local right = split[j + 2]
							local pair_right = `{p2}\0{right}`
							if pair_right ~= best_pair then
								pair_counts[pair_right] = (pair_counts[pair_right] or 0) - count
								if pair_counts[pair_right] <= 0 then
									pair_counts[pair_right] = nil
								end
							end
							local new_pair_right = `{merged_token}\0{right}`
							pair_counts[new_pair_right] = (pair_counts[new_pair_right] or 0) + count
						end
						table.insert(new_split, merged_token)
						j += 2
						changed = true
					else
						table.insert(new_split, split[j])
						j += 1
					end
				end
				if changed then
					splits[i] = new_split
				end
			end
		end
		if self.show_progress and next_id % 100 == 0 then
			print("Vocab size: " .. next_id)
		end
	end
	return BPE.new(vocab, merges, nil, self.end_of_word_suffix), {}
end

return BpeTrainer


local BpeTrainer = require("@self/BpeTrainer")

return {
	BpeTrainer = BpeTrainer,
}


--!native
--!optimize 2
local Tape = require("../autograd/Tape")

local Gradcheck = {}

function Gradcheck.check(fn: (...number) -> (number, number), inputs: { any }, eps: number?, tol: number?, max_check: number?)
	eps = eps or 1e-4
	tol = tol or 1e-2
	local out, grad = fn(table.unpack(inputs))
	Tape.backwardFrom(out, grad)
	for idx, t in ipairs(inputs) do
		if type(t) == "table" and t._storage then
			for i = 1, math.min(#t._storage, max_check or 50) do
				local orig = t._storage[i]
				t._storage[i] = orig + eps
				local out1, _ = fn(table.unpack(inputs))
				t._storage[i] = orig - eps
				local out2, _ = fn(table.unpack(inputs))
				t._storage[i] = orig
				local num = (out1 - out2) / (2 * (eps :: number))
				local ana = t._grad and t._grad._storage[i] or 0
				local rel = math.abs(num - ana) / math.max(1e-6, math.abs(num) + math.abs(ana))
				if rel > tol then
					print(
						string.format(
							"Gradcheck fail on input %d idx %d: num=%.6g ana=%.6g rel=%.3g",
							idx,
							i,
							num,
							ana,
							rel
						)
					)
					return false
				end
			end
		end
	end
	return true
end

return Gradcheck


--!native
--!optimize 2
local RunService = game:GetService("RunService")

local JPSPlus = require("./JPSPlus")

local WIDTH, HEIGHT = 384, 384
local OBSTACLE_DENSITY = 0.25
local SEED = 12345

local NUM_QUERIES = 5000
local WARMUP = 200

local DIAG_POLICY = JPSPlus.grid.DiagonalPolicy.NoSqueeze

local YIELD_EVERY = 100

local function percentile(sorted, p)
	local n = #sorted
	if n == 0 then return 0 end
	local idx = math.floor(p * n)
	if idx < 1 then idx = 1 end
	if idx > n then idx = n end
	return sorted[idx]
end

local function summarize(times)
	table.sort(times)
	local n = #times
	local total = 0
	local maxv = 0
	for i = 1, n do
		local t = times[i]
		total += t
		if t > maxv then maxv = t end
	end
	return {
		n = n,
		avg = total / n,
		p50 = percentile(times, 0.50),
		p95 = percentile(times, 0.95),
		p99 = percentile(times, 0.99),
		max = maxv,
	}
end

local function fmtSeconds(s)
	return string.format("%.6f", s)
end

local function printSummary(label, summary, found, total)
	print(("[%s] n=%d found=%d/%d avg=%ss p50=%ss p95=%ss p99=%ss max=%ss")
		:format(
			label,
			summary.n,
			found, total,
			fmtSeconds(summary.avg),
			fmtSeconds(summary.p50),
			fmtSeconds(summary.p95),
			fmtSeconds(summary.p99),
			fmtSeconds(summary.max)
		)
	)
end

local rng = Random.new(SEED)
local grid = JPSPlus.grid.new(WIDTH, HEIGHT, function(x, y)
	return rng:NextNumber() < OBSTACLE_DENSITY
end)

local walkable = {}
walkable.reserve = WIDTH * HEIGHT

for y = 1, HEIGHT do
	for x = 1, WIDTH do
		local id = grid:id(x, y)
		if grid:isWalkableId(id) then
			walkable[#walkable + 1] = { x = x, y = y }
		end
	end
end

assert(#walkable > 0, "No walkable cells; reduce obstacle density.")

task.wait(5)

local memBefore = gcinfo()
local t0 = os.clock()

local db = JPSPlus.preprocess.build(grid, {
	DiagonalPolicy = DIAG_POLICY,
})

local t1 = os.clock()
local memAfter = gcinfo()

print(("Preprocess: time=%ss  memDelta=%.1f KB  grid=%dx%d  walkable=%d")
	:format(fmtSeconds(t1 - t0), (memAfter - memBefore), WIDTH, HEIGHT, #walkable))

local planner = JPSPlus.planner.new(db)

local pairs = table.create(NUM_QUERIES)
for i = 1, NUM_QUERIES do
    local a = walkable[rng:NextInteger(1, #walkable)]
    local b = walkable[rng:NextInteger(1, #walkable)]
    pairs[i] = { ax = a.x, ay = a.y, bx = b.x, by = b.y }
end

for i = 1, WARMUP do
    local q = pairs[i]
    planner:findPath(q.ax, q.ay, q.bx, q.by, { Expand = false })
end

do
    local timesA = table.create(NUM_QUERIES)
    local foundA = 0

    local startA = os.clock()

    for i = 1, NUM_QUERIES do
        local q = pairs[i]
        local t = os.clock()
        local path = planner:findPath(q.ax, q.ay, q.bx, q.by, { Expand = false })
        timesA[i] = os.clock() - t
        if path then foundA += 1 end

        if (i % YIELD_EVERY) == 0 then
            task.wait()
        end
    end

    local totalA = os.clock() - startA

    local summaryA = summarize(timesA)
    printSummary("JPS+ Expand=false", summaryA, foundA, NUM_QUERIES)
    print(("Total wall time (Expand=false): %ss  QPS=%.1f")
        :format(fmtSeconds(totalA), NUM_QUERIES / totalA))
end

do
    local timesB = table.create(NUM_QUERIES)
    local foundB = 0

    local startB = os.clock()

    for i = 1, NUM_QUERIES do
        local q = pairs[i]
        local t = os.clock()
        local path = planner:findPath(q.ax, q.ay, q.bx, q.by, { Expand = true })
        timesB[i] = os.clock() - t
        if path then foundB += 1 end

        if (i % YIELD_EVERY) == 0 and RunService:IsStudio() then
            task.wait()
        end
    end

    local totalB = os.clock() - startB

    local summaryB = summarize(timesB)
    printSummary("JPS+ Expand=true", summaryB, foundB, NUM_QUERIES)
    print(("Total wall time (Expand=true): %ss  QPS=%.1f")
        :format(fmtSeconds(totalB), NUM_QUERIES / totalB))
end

do
    local timesD = table.create(NUM_QUERIES)
    local foundD = 0

    local startD = os.clock()

    for i = 1, NUM_QUERIES do
        local q = pairs[i]
        local t = os.clock()
        local path = planner:findPath(q.ax, q.ay, q.bx, q.by, { NoReconstruct = true })
        timesD[i] = os.clock() - t
        if path then foundD += 1 end

        if (i % YIELD_EVERY) == 0 and RunService:IsStudio() then
            task.wait()
        end
    end

    local totalD = os.clock() - startD

    local summaryD = summarize(timesD)
    printSummary("JPS+ NoReconstruct=true", summaryD, foundD, NUM_QUERIES)
    print(("Total wall time (NoReconstruct=true): %ss  QPS=%.1f")
        :format(fmtSeconds(totalD), NUM_QUERIES / totalD))
end

do
    local timesC = table.create(NUM_QUERIES)
    local foundC = 0

    local startC = os.clock()

    for i = 1, NUM_QUERIES do
        local q = pairs[i]
        local t = os.clock()
        local path = planner:findPath(q.ax, q.ay, q.bx, q.by, { Expand = true })
        if path then
            path = JPSPlus.smoothing.simplify(grid, path)
            foundC += 1
        end
        timesC[i] = os.clock() - t

        if (i % YIELD_EVERY) == 0 and RunService:IsStudio() then
            task.wait()
        end
    end

    local totalC = os.clock() - startC

    local summaryC = summarize(timesC)
    printSummary("JPS+ Expand=true+Smooth", summaryC, foundC, NUM_QUERIES)
    print(("Total wall time (Expand+Smooth): %ss  QPS=%.1f")
        :format(fmtSeconds(totalC), NUM_QUERIES / totalC))
end


local WorldGrid = {}
WorldGrid.__index = WorldGrid

function WorldGrid.new(origin: Vector3, cellSize: number, width: number, height: number)
	return setmetatable({
		Origin = origin,
		CellSize = cellSize,
		Width = width,
		Height = height,
	}, WorldGrid)
end

function WorldGrid:worldToCell(pos: Vector3)
	local p = pos - self.Origin
	local x = math.floor(p.X / self.CellSize) + 1
	local y = math.floor(p.Z / self.CellSize) + 1

	if x < 1 or x > self.Width or y < 1 or y > self.Height then
		return nil
	end
	return x, y
end

function WorldGrid:cellToWorldCenter(x: number, y: number)
	return self.Origin + Vector3.new((x - 0.5) * self.CellSize, 0, (y - 0.5) * self.CellSize)
end

return WorldGrid


--!optimize 2
--!native
local PhysicsService = game:GetService("PhysicsService")
local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")

local JPSPlus = require("./JPSPlus")
local WorldGrid = require("./WorldGrid")

local npcModel1 = workspace:WaitForChild("Agent 1")
local npcModel2 = workspace:WaitForChild("Agent 2")

PhysicsService:RegisterCollisionGroup("Agent")
PhysicsService:CollisionGroupSetCollidable("Agent", "Agent", false)

local origin = Vector3.new(-128, 0, -128)
local cellSize = 2
local width, height = 128, 128

local adapter = WorldGrid.new(origin, cellSize, width, height)

local overlap = OverlapParams.new()
overlap.FilterType = Enum.RaycastFilterType.Include
overlap.FilterDescendantsInstances = CollectionService:GetTagged("NavBlock")

local queryBoxSize = Vector3.new(cellSize * 0.9, 10, cellSize * 0.9)

local grid = JPSPlus.grid.new(width, height, function(x, y)
	local center = adapter:cellToWorldCenter(x, y) + Vector3.new(0, 5, 0)
	local parts = workspace:GetPartBoundsInBox(CFrame.new(center), queryBoxSize, overlap)
	return (#parts > 0)
end)

local db = JPSPlus.preprocess.build(grid, {
	DiagonalPolicy = JPSPlus.grid.DiagonalPolicy.NoSqueeze,
})

local planner = JPSPlus.planner.new(db)

local groundParams = RaycastParams.new()
groundParams.FilterType = Enum.RaycastFilterType.Include
groundParams.FilterDescendantsInstances = CollectionService:GetTagged("NavGround")
groundParams.IgnoreWater = true

local function snapToGround(p: Vector3, fallbackY: number)
	local hit = workspace:Raycast(p + Vector3.new(0, 200, 0), Vector3.new(0, -600, 0), groundParams)
	if hit then
		return Vector3.new(p.X, hit.Position.Y, p.Z)
	end
	return Vector3.new(p.X, fallbackY, p.Z)
end

local function makePathVisualizer(folderName: string, idx: number)
	local folder = workspace:FindFirstChild(folderName)
	if not folder then
		folder = Instance.new("Folder")
		folder.Name = folderName
		folder.Parent = workspace
	end

	local randomColor = Color3.fromRGB(math.random(0, 255), math.random(0, 255), math.random(0, 255))
	local pool: {BasePart} = {}

	local function getPart(i: number): BasePart
		local part = pool[i]
		if part then return part end

		part = Instance.new("Part")
		part.Name = `{i}-{idx}`
		part.Size = Vector3.new(0.5, 0.5, 0.5)
		part.Anchored = true
		part.CanCollide = false
		part.Shape = Enum.PartType.Ball
		part.Material = Enum.Material.Neon
		part.Color = randomColor
		part.Transparency = 0.5
		part.Parent = folder

		pool[i] = part
		return part
	end

	local function render(points: {Vector3})
		for i, p in ipairs(points) do
			local part = getPart(i)
			part.Position = p
			part.Transparency = 0.5
		end

		for i = #points + 1, #pool do
			local part = pool[i]
			if part then
				part.Transparency = 1
			end
		end
	end

	local function clear()
		for i = 1, #pool do
			local part = pool[i]
			if part then
				part.Transparency = 1
			end
		end
	end

	return {
		Render = render,
		Clear = clear,
		Folder = folder,
	}
end

local function findWorldPath(startPos: Vector3, goalPos: Vector3, yHint: number): {Vector3}?
	local sx, sy = adapter:worldToCell(startPos)
	local gx, gy = adapter:worldToCell(goalPos)
	if not sx or not gx then
		return nil
	end

	if sx < 1 or sx > width or sy < 1 or sy > height then return nil end
	if gx < 1 or gx > width or gy < 1 or gy > height then return nil end

	local cells = planner:findPath(sx, sy, gx, gy, { Expand = true })
	if not cells then return nil end

	--cells = JPSPlus.smoothing.simplify(grid, cells) -- this is optional

	local points = table.create(#cells)
	for i = 1, #cells do
		local c = cells[i]
		local p = adapter:cellToWorldCenter(c.x, c.y)
		points[i] = snapToGround(p, yHint)
	end

	return points
end

local function chaseNPC(npc: Model, visual, getTargetPos: () -> Vector3?)
	for _, obj in npc:GetDescendants() do
		if not obj:IsA("BasePart") then continue end
		obj.CollisionGroup = "Agent"
	end
	local humanoid = npc:WaitForChild("Humanoid") :: Humanoid
	local root = npc:WaitForChild("HumanoidRootPart") :: BasePart

	local replanHz = 10 -- 10 replans/sec is usually enough
	local replanInterval = 1 / replanHz
	local moveUpdateHz = 20 -- how often to re-issue MoveTo
	local moveUpdateInterval = 1 / moveUpdateHz
	local waypointReach = 2 -- studs

	local currentPath: {Vector3}? = nil
	local wpIndex = 1
	local pathVersion = 0

	local computing = false
	local lastReplanTime = 0
	local failedReplans = 0

	task.spawn(function()
		local lastMoveToTime = 0
		while npc.Parent do
			task.wait()

			local targetPos = getTargetPos()
			if not targetPos then
				currentPath = nil
				continue
			end

			local path = currentPath
			if not path or wpIndex > #path then
				continue
			end

			while path and wpIndex <= #path and (root.Position - path[wpIndex]).Magnitude <= waypointReach do
				wpIndex += 1
			end
			if not path or wpIndex > #path then
				continue
			end

			if (os.clock() - lastMoveToTime) >= moveUpdateInterval then
				humanoid:MoveTo(path[wpIndex])
				lastMoveToTime = os.clock()
			end
		end
	end)

	while npc.Parent do
		local now = os.clock()
		local targetPos = getTargetPos()
		if not targetPos then
			task.wait(moveUpdateInterval)
			continue
		end

		local shouldReplan = false
		if (now - lastReplanTime) >= replanInterval then
			shouldReplan = true
		end

		if shouldReplan and not computing then
			computing = true
			lastReplanTime = now

			local myVersion = pathVersion + 1
			pathVersion = myVersion

			task.spawn(function()
				local path = findWorldPath(root.Position, targetPos, root.Position.Y)
				computing = false

				if myVersion ~= pathVersion then
					return
				end

				if path and #path > 0 then
					failedReplans = 0
					currentPath = path
					wpIndex = 1
					visual.Render(path)
				else
					failedReplans += 1
					if failedReplans >= 20 then -- ~2 seconds at 10Hz
						visual.Clear()
					end
				end
			end)
		end
		task.wait(0.01)
	end
end

local visual = makePathVisualizer("Visual", 0)
local visual2 = makePathVisualizer("Visual", 1000)

task.spawn(chaseNPC, npcModel1, visual, function()
	local plr = Players:GetPlayers()[1]
	local char = plr and plr.Character
	local hrp = char and char:FindFirstChild("HumanoidRootPart")
	return hrp and hrp.Position
end)
task.spawn(chaseNPC, npcModel2, visual2, function()
	return npcModel1 and npcModel1.PrimaryPart.Position
end)


--!optimize 2
local Dirs = {}

@native
local function bit(d) return bit32.lshift(1, d-1) end

Dirs.E = 1
Dirs.W = 2
Dirs.N = 3
Dirs.S = 4
Dirs.NE = 5
Dirs.NW = 6
Dirs.SE = 7
Dirs.SW = 8

Dirs.dx = { [1]= 1, [2]=-1, [3]= 0, [4]= 0, [5]= 1, [6]=-1, [7]= 1, [8]=-1 }
Dirs.dy = { [1]= 0, [2]= 0, [3]=-1, [4]= 1, [5]=-1, [6]=-1, [7]= 1, [8]= 1 }
Dirs.isDiag = { [1]=false,[2]=false,[3]=false,[4]=false,[5]=true,[6]=true,[7]=true,[8]=true }

Dirs.compA = { [5]=Dirs.E, [6]=Dirs.W, [7]=Dirs.E, [8]=Dirs.W }
Dirs.compB = { [5]=Dirs.N, [6]=Dirs.N, [7]=Dirs.S, [8]=Dirs.S }

Dirs.NAT_MASK = {
	[0]= bit(1)+bit(2)+bit(3)+bit(4)+bit(5)+bit(6)+bit(7)+bit(8),
	[Dirs.E] = bit(Dirs.E) + bit(Dirs.NE) + bit(Dirs.SE),
	[Dirs.W] = bit(Dirs.W) + bit(Dirs.NW) + bit(Dirs.SW),
	[Dirs.N] = bit(Dirs.N) + bit(Dirs.NE) + bit(Dirs.NW),
	[Dirs.S] = bit(Dirs.S) + bit(Dirs.SE) + bit(Dirs.SW),
	[Dirs.NE] = bit(Dirs.NE) + bit(Dirs.E) + bit(Dirs.N),
	[Dirs.NW] = bit(Dirs.NW) + bit(Dirs.W) + bit(Dirs.N),
	[Dirs.SE] = bit(Dirs.SE) + bit(Dirs.E) + bit(Dirs.S),
	[Dirs.SW] = bit(Dirs.SW) + bit(Dirs.W) + bit(Dirs.S),
}

-- d1 = (-dx, dy), d2 = (dx, -dy)
Dirs.diagForced1 = {
	[Dirs.NE] = Dirs.NW,
	[Dirs.NW] = Dirs.NE,
	[Dirs.SE] = Dirs.SW,
	[Dirs.SW] = Dirs.SE,
}
Dirs.diagForced2 = {
	[Dirs.NE] = Dirs.SE,
	[Dirs.NW] = Dirs.SW,
	[Dirs.SE] = Dirs.NE,
	[Dirs.SW] = Dirs.NW,
}

return Dirs


--!optimize 2
local Grid = {}
Grid.__index = Grid

Grid.DiagonalPolicy = {
	Always = "Always", -- diagonal allowed if destination is free
	NoSqueeze = "NoSqueeze", -- (default) diagonal allowed if dest free AND (at least one orthogonal adjacent is free)
	NoCorner = "NoCorner", -- diagonal allowed if dest free AND (both orthogonal adjacents are free)
}

function Grid.new(width, height, isBlockedFn)
	assert(width > 0 and height > 0)
	local stride = width + 2
	local totalW = width + 2
	local totalH = height + 2
	local total = totalW * totalH

	local blocked = table.create(total, true)
	-- fill interior
	for y = 1, height do
		local iy = y + 1
		for x = 1, width do
			local ix = x + 1
			local id = (iy - 1) * stride + ix
			if isBlockedFn then
				blocked[id] = isBlockedFn(x, y) and true or false
			else
				blocked[id] = false
			end
		end
	end

	return setmetatable({
		Width = width,
		Height = height,
		Stride = stride,
		Total = total,
		Blocked = blocked, -- includes padded border
	}, Grid)
end

function Grid:id(x, y)
	-- external (1..W,1..H) -> internal id
	local ix = x + 1
	local iy = y + 1
	return (iy - 1) * self.Stride + ix
end

function Grid:xy(id)
	-- internal id -> external (x,y)
	local stride = self.Stride
	local iy = ((id - 1) // stride) + 1
	local ix = (id - 1) - (iy - 1) * stride + 1
	return ix - 1, iy - 1
end

function Grid:isWalkableId(id)
	return not self.Blocked[id]
end

function Grid:setBlocked(x, y, value)
	local id = self:id(x, y)
	self.Blocked[id] = value and true or false
end

return Grid


--@EternityDev
-- Jump Point Search Plus (JPS+)
local JPSPlus = {}

JPSPlus.dirs = require("@self/dirs")
JPSPlus.grid = require("@self/grid")
JPSPlus.preprocess = require("@self/preprocess")
JPSPlus.planner = require("@self/planner")
JPSPlus.smoothing = require("@self/smoothing")

return JPSPlus


--!native
--!optimize 2
local Dirs = require("./dirs")
local PQ = require("./priorityqueue")

local Planner = {}
Planner.__index = Planner

local function octile(ix, iy, gx, gy, D, D2)
	local dx = math.abs(ix - gx)
	local dy = math.abs(iy - gy)
	local mn = (dx < dy) and dx or dy
	local mx = (dx > dy) and dx or dy
	return D2 * mn + D * (mx - mn)
end

local function sign(a)
	return (a > 0 and 1) or (a < 0 and -1) or 0
end

local function alignedSteps(ix, iy, gx, gy, dx, dy)
	local rx = gx - ix
	local ry = gy - iy

	if dx == 0 then
		if rx ~= 0 then return nil end
		if ry == 0 then return 0 end
		if (ry > 0 and dy > 0) or (ry < 0 and dy < 0) then
			return math.abs(ry)
		end
		return nil
	elseif dy == 0 then
		if ry ~= 0 then return nil end
		if rx == 0 then return 0 end
		if (rx > 0 and dx > 0) or (rx < 0 and dx < 0) then
			return math.abs(rx)
		end
		return nil
	end
	if math.abs(rx) ~= math.abs(ry) then return nil end
	if rx == 0 then return 0 end
	if ((rx > 0 and dx > 0) or (rx < 0 and dx < 0)) and ((ry > 0 and dy > 0) or (ry < 0 and dy < 0)) then
		return math.abs(rx)
	end
	return nil
end

function Planner.new(db)
	local total = db.grid.Total
	return setmetatable({
		db = db,
		stamp = 0,

		seen = table.create(total, 0),
		closed = table.create(total, 0),
		g = table.create(total, 0),
		parent = table.create(total, 0),
		inDir = table.create(total, 0),

		ix = table.create(total, 0),
		iy = table.create(total, 0),

		open = PQ.new(total),
	}, Planner)
end

local function successorMask(db, id, inDir)
	local grid = db.grid
	local blocked = grid.Blocked
	local stride = grid.Stride

	local mask = Dirs.NAT_MASK[inDir] or Dirs.NAT_MASK[0]
	if inDir == 0 then return mask end

	local function bit(d) return bit32.lshift(1, d - 1) end

	local function isForced(nId, diagId)
		return (not blocked[nId]) and blocked[diagId]
	end

	if inDir == Dirs.E then
		if isForced(id - stride, id - stride - 1) then
			mask = bit32.bor(mask, bit(Dirs.N), bit(Dirs.NE))
		end
		if isForced(id + stride, id + stride - 1) then
			mask = bit32.bor(mask, bit(Dirs.S), bit(Dirs.SE))
		end
	elseif inDir == Dirs.W then
		if isForced(id - stride, id - stride + 1) then
			mask = bit32.bor(mask, bit(Dirs.N), bit(Dirs.NW))
		end
		if isForced(id + stride, id + stride + 1) then
			mask = bit32.bor(mask, bit(Dirs.S), bit(Dirs.SW))
		end
	elseif inDir == Dirs.N then
		if isForced(id - 1, id - 1 + stride) then
			mask = bit32.bor(mask, bit(Dirs.W), bit(Dirs.NW))
		end
		if isForced(id + 1, id + 1 + stride) then
			mask = bit32.bor(mask, bit(Dirs.E), bit(Dirs.NE))
		end
	elseif inDir == Dirs.S then
		if isForced(id - 1, id - 1 - stride) then
			mask = bit32.bor(mask, bit(Dirs.W), bit(Dirs.SW))
		end
		if isForced(id + 1, id + 1 - stride) then
			mask = bit32.bor(mask, bit(Dirs.E), bit(Dirs.SE))
		end
	else
		local d1 = Dirs.diagForced1[inDir]
		local d2 = Dirs.diagForced2[inDir]

		local dx = Dirs.dx[inDir]
		local dy = Dirs.dy[inDir]

		if blocked[id - dx] then
			mask = bit32.bor(mask, bit(d1))
		end
		if blocked[id - dy * stride] then
			mask = bit32.bor(mask, bit(d2))
		end
	end

	return mask
end

local function expandJumpIdsToCells(grid, ids)
	local out = {}
	if #ids == 0 then return out end

	local x0, y0 = grid:xy(ids[1])
	table.insert(out, { x = x0, y = y0 })

	for i = 2, #ids do
		local x1, y1 = grid:xy(ids[i - 1])
		local x2, y2 = grid:xy(ids[i])

		local sx = sign(x2 - x1)
		local sy = sign(y2 - y1)
		local steps = math.max(math.abs(x2 - x1), math.abs(y2 - y1))

		for s = 1, steps do
			table.insert(out, { x = x1 + sx * s, y = y1 + sy * s })
		end
	end

	return out
end

function Planner:findPath(sx, sy, gx, gy, options)
	options = options or {}
	local D = options.StraightCost or 1.0
	local D2 = options.DiagonalCost or math.sqrt(2)
	local expand = options.Expand or false
	local maxIter = options.MaxIterations or 2e6

	local noReconstruct = options.NoReconstruct or false
	local returnIds = options.ReturnIds or false

	local db = self.db
	local grid = db.grid
	local blocked = grid.Blocked

	local start = grid:id(sx, sy)
	local goal = grid:id(gx, gy)
	if blocked[start] or blocked[goal] then
		return nil
	end
	if start == goal then
		if noReconstruct then return true end
		if returnIds then return { start } end
		return { { x = sx, y = sy } }
	end

	local six, siy = sx + 1, sy + 1
	local gix, giy = gx + 1, gy + 1

	self.stamp += 1
	local stamp = self.stamp

	local seen = self.seen
	local closed = self.closed
	local gScore = self.g
	local parent = self.parent
	local inDirArr = self.inDir
	local ixArr = self.ix
	local iyArr = self.iy

	local open = self.open
	open.n = 0

	-- buffers + offsets
	local offB = db.offB
	local clearBuf = db.clearBuf
	local jumpBuf = db.jumpBuf
	local step = db.step

	local readu16 = buffer.readu16

	seen[start] = stamp
	closed[start] = 0
	gScore[start] = 0
	parent[start] = 0
	inDirArr[start] = 0
	ixArr[start] = six
	iyArr[start] = siy

	open:push(start, octile(six, siy, gix, giy, D, D2), 0)

	local iter = 0

	while true do
		iter += 1
		if iter > maxIter then return nil end

		local current, currentG = open:pop()
		if not current then return nil end

		if seen[current] ~= stamp or currentG ~= gScore[current] then
			continue
		end
		if closed[current] == stamp then
			continue
		end
		closed[current] = stamp

		if current == goal then
			break
		end

		local cix, ciy = ixArr[current], iyArr[current]
		local inDir = inDirArr[current]
		local mask = successorMask(db, current, inDir)

		local curByte = (current - 1) * 2

		local bit = 1
		for dir = 1, 8 do
			if bit32.band(mask, bit) ~= 0 then
				local baseB = offB[dir]
				local c = readu16(clearBuf, baseB + curByte)
				if c > 0 then
					local dx, dy = Dirs.dx[dir], Dirs.dy[dir]

					local goalSteps = alignedSteps(cix, ciy, gix, giy, dx, dy)
					local stepsTaken = nil
					local nid = nil

					if goalSteps and goalSteps > 0 and goalSteps <= c then
						stepsTaken = goalSteps
						nid = goal
					else
						local j = readu16(jumpBuf, baseB + curByte)
						local bestDist = (j > 0) and j or (c + 1)
						if Dirs.isDiag[dir] then
							local rx = gix - cix -- relative X to goal
							local ry = giy - ciy -- relative Y to goal
							if (rx > 0 and dx > 0) or (rx < 0 and dx < 0) then
								local distToRow = math.abs(rx)
								if distToRow < bestDist then
									bestDist = distToRow
								end
							end
							if (ry > 0 and dy > 0) or (ry < 0 and dy < 0) then
								local distToCol = math.abs(ry)
								if distToCol < bestDist then
									bestDist = distToCol
								end
							end
						end
						if bestDist <= c then
							stepsTaken = bestDist
							nid = current + bestDist * step[dir]
						end
					end

					if nid then
						local addG = (Dirs.isDiag[dir] and (stepsTaken * D2) or (stepsTaken * D))
						local ng = gScore[current] + addG

						if seen[nid] ~= stamp or ng < gScore[nid] then
							seen[nid] = stamp
							gScore[nid] = ng
							parent[nid] = current
							inDirArr[nid] = dir

							local nix, niy
							if nid == goal then
								nix, niy = gix, giy
							else
								nix = cix + dx * stepsTaken
								niy = ciy + dy * stepsTaken
							end
							ixArr[nid] = nix
							iyArr[nid] = niy

							local h = octile(nix, niy, gix, giy, D, D2)
							open:push(nid, ng + h, ng)
						end
					end
				end
			end
			bit = bit32.lshift(bit, 1)
		end
	end

	if noReconstruct then
		return true
	end

	-- Reconstruct ids
	local ids = {}
	local node = goal
	while node ~= 0 do
		table.insert(ids, node)
		node = parent[node]
	end
	for i = 1, (#ids // 2) do
		ids[i], ids[#ids - i + 1] = ids[#ids - i + 1], ids[i]
	end

	if returnIds then
		return ids
	end

	if expand then
		return expandJumpIdsToCells(grid, ids)
	end
	local out = table.create(#ids)
	for i = 1, #ids do
		local x, y = grid:xy(ids[i])
		out[i] = { x = x, y = y }
	end
	return out
end

return Planner


--!native
--!optimize 2
local Dirs = require("./dirs")
local Grid = require("./grid")

local Preprocess = {}

local function makeOffsets(total)
	local off = table.create(8)
	local offB = table.create(8)
	for d = 1, 8 do
		local o = (d - 1) * total
		off[d] = o
		offB[d] = o * 2 -- bytes for u16
	end
	return off, offB
end

function Preprocess.build(grid, options)
	options = options or {}
	local diagPolicy = options.DiagonalPolicy or Grid.DiagonalPolicy.NoSqueeze

	local blocked = grid.Blocked
	local stride = grid.Stride
	local total = grid.Total
	local w, h = grid.Width, grid.Height

	local off, offB = makeOffsets(total)

	local clearBuf = buffer.create(8 * total * 2)
	local jumpBuf = buffer.create(8 * total * 2)

	local readu16 = buffer.readu16
	local writeu16 = buffer.writeu16

	local step = table.create(8, 0)
	for d = 1, 8 do
		step[d] = Dirs.dx[d] + Dirs.dy[d] * stride
	end

	local function canDiagFrom(id, dx, dy)
		local dest = id + dx + dy * stride
		if blocked[dest] then return false end
		if diagPolicy == Grid.DiagonalPolicy.Always then
			return true
		end
		local a = id + dx
		local b = id + dy * stride
		local fa = not blocked[a]
		local fb = not blocked[b]
		if diagPolicy == Grid.DiagonalPolicy.NoCorner then
			return fa and fb
		end
		-- NoSqueeze
		return fa or fb
	end

	local function r(buf, baseBytes, id)
		return readu16(buf, baseBytes + (id - 1) * 2)
	end
	local function w16(buf, baseBytes, id, v)
		writeu16(buf, baseBytes + (id - 1) * 2, v)
	end

	-- E
	do
		local base = offB[Dirs.E]
		for iy = 2, h + 1 do
			for ix = w + 1, 2, -1 do
				local id = (iy - 1) * stride + ix
				if blocked[id] then
					w16(clearBuf, base, id, 0)
				else
					local nid = id + 1
					if not blocked[nid] then
						w16(clearBuf, base, id, 1 + r(clearBuf, base, nid))
					else
						w16(clearBuf, base, id, 0)
					end
				end
			end
		end
	end

	-- W
	do
		local base = offB[Dirs.W]
		for iy = 2, h + 1 do
			for ix = 2, w + 1 do
				local id = (iy - 1) * stride + ix
				if blocked[id] then
					w16(clearBuf, base, id, 0)
				else
					local nid = id - 1
					if not blocked[nid] then
						w16(clearBuf, base, id, 1 + r(clearBuf, base, nid))
					else
						w16(clearBuf, base, id, 0)
					end
				end
			end
		end
	end

	-- N
	do
		local base = offB[Dirs.N]
		for iy = 2, h + 1 do
			for ix = 2, w + 1 do
				local id = (iy - 1) * stride + ix
				if blocked[id] then
					w16(clearBuf, base, id, 0)
				else
					local nid = id - stride
					if not blocked[nid] then
						w16(clearBuf, base, id, 1 + r(clearBuf, base, nid))
					else
						w16(clearBuf, base, id, 0)
					end
				end
			end
		end
	end

	-- S
	do
		local base = offB[Dirs.S]
		for iy = h + 1, 2, -1 do
			for ix = 2, w + 1 do
				local id = (iy - 1) * stride + ix
				if blocked[id] then
					w16(clearBuf, base, id, 0)
				else
					local nid = id + stride
					if not blocked[nid] then
						w16(clearBuf, base, id, 1 + r(clearBuf, base, nid))
					else
						w16(clearBuf, base, id, 0)
					end
				end
			end
		end
	end

	-- Diagonal clear
	local function diagClear(dir, ixStart, ixEnd, ixStep, iyStart, iyEnd, iyStep)
		local base = offB[dir]
		local dx, dy = Dirs.dx[dir], Dirs.dy[dir]
		local st = dx + dy * stride
		for iy = iyStart, iyEnd, iyStep do
			for ix = ixStart, ixEnd, ixStep do
				local id = (iy - 1) * stride + ix
				if blocked[id] then
					w16(clearBuf, base, id, 0)
				else
					if canDiagFrom(id, dx, dy) then
						local nid = id + st
						w16(clearBuf, base, id, 1 + r(clearBuf, base, nid))
					else
						w16(clearBuf, base, id, 0)
					end
				end
			end
		end
	end

	diagClear(Dirs.NE, w + 1, 2, -1, 2, h + 1, 1)
	diagClear(Dirs.NW, 2, w + 1, 1,  2, h + 1, 1)
	diagClear(Dirs.SE, w + 1, 2, -1, h + 1, 2, -1)
	diagClear(Dirs.SW, 2, w + 1, 1,  h + 1, 2, -1)

	local function forcedStraightAt(cId, dir)
		if dir == Dirs.E then
			return (blocked[cId - stride] and canDiagFrom(cId, 1, -1)) or (blocked[cId + stride] and canDiagFrom(cId, 1, 1))
		elseif dir == Dirs.W then
			return (blocked[cId - stride] and canDiagFrom(cId, -1, -1)) or (blocked[cId + stride] and canDiagFrom(cId, -1, 1))
		elseif dir == Dirs.N then
			return (blocked[cId - 1] and canDiagFrom(cId, -1, -1)) or (blocked[cId + 1] and canDiagFrom(cId, 1, -1))
		end
		-- S
		return (blocked[cId - 1] and canDiagFrom(cId, -1, 1)) or (blocked[cId + 1] and canDiagFrom(cId, 1, 1))
	end

	local function straightJump(dir, ixStart, ixEnd, ixStep, iyStart, iyEnd, iyStep)
		local base = offB[dir]
		local st = step[dir]
		for iy = iyStart, iyEnd, iyStep do
			for ix = ixStart, ixEnd, ixStep do
				local id = (iy - 1) * stride + ix
				if r(clearBuf, base, id) == 0 then
					w16(jumpBuf, base, id, 0)
				else
					local nid = id + st
					if forcedStraightAt(nid, dir) then
						w16(jumpBuf, base, id, 1)
					else
						local nxt = r(jumpBuf, base, nid)
						w16(jumpBuf, base, id, (nxt > 0) and (1 + nxt) or 0)
					end
				end
			end
		end
	end

	straightJump(Dirs.E, w + 1, 2, -1, 2, h + 1, 1)
	straightJump(Dirs.W, 2, w + 1, 1,  2, h + 1, 1)
	straightJump(Dirs.N, 2, w + 1, 1,  2, h + 1, 1)
	straightJump(Dirs.S, 2, w + 1, 1,  h + 1, 2, -1)

	local function forcedDiagAt(cId, dx, dy)
		if blocked[cId - dx] and canDiagFrom(cId, -dx, dy) then
			return true
		end
		if blocked[cId - dy * stride] and canDiagFrom(cId, dx, -dy) then
			return true
		end
		return false
	end

	local function isDiagJumpPoint(cId, dir)
		local dx, dy = Dirs.dx[dir], Dirs.dy[dir]
		if forcedDiagAt(cId, dx, dy) then
			return true
		end
		local a = Dirs.compA[dir]
		local b = Dirs.compB[dir]
		return r(jumpBuf, offB[a], cId) > 0 or r(jumpBuf, offB[b], cId) > 0
	end

	local function diagJump(dir, ixStart, ixEnd, ixStep, iyStart, iyEnd, iyStep)
		local base = offB[dir]
		local st = step[dir]
		for iy = iyStart, iyEnd, iyStep do
			for ix = ixStart, ixEnd, ixStep do
				local id = (iy - 1) * stride + ix
				if r(clearBuf, base, id) == 0 then
					w16(jumpBuf, base, id, 0)
				else
					local nid = id + st
					if isDiagJumpPoint(nid, dir) then
						w16(jumpBuf, base, id, 1)
					else
						local nxt = r(jumpBuf, base, nid)
						w16(jumpBuf, base, id, (nxt > 0) and (1 + nxt) or 0)
					end
				end
			end
		end
	end

	diagJump(Dirs.NE, w + 1, 2, -1, 2, h + 1, 1)
	diagJump(Dirs.NW, 2, w + 1, 1,  2, h + 1, 1)
	diagJump(Dirs.SE, w + 1, 2, -1, h + 1, 2, -1)
	diagJump(Dirs.SW, 2, w + 1, 1,  h + 1, 2, -1)

	return {
		grid = grid,
		DiagonalPolicy = diagPolicy,
		step = step,
		off = off,
		offB = offB, -- byte offsets per dir

		clearBuf = clearBuf,
		jumpBuf = jumpBuf,
	}
end

return Preprocess


--!native
--!optimize 2
local PQ = {}
PQ.__index = PQ

function PQ.new(capacity)
	capacity = capacity or 256
	return setmetatable({
		n = 0,
		id = table.create(capacity),
		f  = table.create(capacity),
		g  = table.create(capacity),
	}, PQ)
end

function PQ:push(nodeId, fScore, gScore)
	local n = self.n + 1
	self.n = n

	local id, f, g = self.id, self.f, self.g
	id[n] = nodeId
	f[n] = fScore
	g[n] = gScore

	local i = n
	while i > 1 do
		local p = i // 2
		local pf, cf = f[p], f[i]
		if pf <= cf then break end

		id[p], id[i] = id[i], id[p]
		f[p], f[i] = cf, pf
		g[p], g[i] = g[i], g[p]
		i = p
	end
end

function PQ:pop()
	local n = self.n
	if n == 0 then return nil end

	local id, f, g = self.id, self.f, self.g
	local topId = id[1]
	local topG = g[1]

	if n == 1 then
		self.n = 0
		return topId, topG
	end

	id[1] = id[n]
	f[1] = f[n]
	g[1] = g[n]
	n -= 1
	self.n = n

	local i = 1
	while true do
		local l = i + i -- i * 2
		if l > n then break end

		local r = l + 1
		local s = l
		local sf = f[l]

		if r <= n then
			local rf = f[r]
			if rf < sf then
				s = r
				sf = rf
			end
		end

		local cf = f[i]
		if cf <= sf then break end

		id[i], id[s] = id[s], id[i]
		f[i], f[s] = sf, cf
		g[i], g[s] = g[s], g[i]
		i = s
	end

	return topId, topG
end

function PQ:peek()
	if self.n == 0 then return nil end
	return self.id[1], self.f[1], self.g[1]
end

function PQ:isEmpty()
	return self.n == 0
end

return PQ


--!native
--!optimize 2
local Smoothing = {}

local function hasLOS(blocked, stride, x0, y0, x1, y1)
	local dx = x1 - x0
	local dy = y1 - y0
	local adx = math.abs(dx)
	local ady = math.abs(dy)
	local sx = (dx > 0 and 1) or (dx < 0 and -1) or 0
	local sy = (dy > 0 and 1) or (dy < 0 and -1) or 0

	local ix, iy = x0 + 1, y0 + 1
	local id = (iy - 1) * stride + ix

	local stepX = sx
	local stepY = sy * stride

	local err = adx - ady

	local targetIx = x1 + 1
	local targetIy = y1 + 1

	while true do
		if blocked[id] then
			return false
		end
		if ix == targetIx and iy == targetIy then
			return true
		end

		local e2 = err + err
		local moveX = e2 > -ady
		local moveY = e2 < adx

		if moveX and moveY then
			if blocked[id + stepX] and blocked[id + stepY] then
				return false
			end
		end

		if moveX then
			err -= ady
			ix += sx
			id += stepX
		end
		if moveY then
			err += adx
			iy += sy
			id += stepY
		end
	end
end

function Smoothing.simplify(grid, path)
	if not path or #path <= 2 then return path end

	local blocked = grid.Blocked
	local stride = grid.Stride
	local n = #path

	local out = { path[1] }
	local outN = 1
	local i = 1

	while i < n do
		-- binary search could be faster for long paths, but greedy from end is usually good tho
		local best = i + 1
		local pi = path[i]
		local pix, piy = pi.x, pi.y

		for j = n, i + 2, -1 do
			local pj = path[j]
			if hasLOS(blocked, stride, pix, piy, pj.x, pj.y) then
				best = j
				break
			end
		end

		outN += 1
		out[outN] = path[best]
		i = best
	end

	return out
end

return Smoothing


--!strict
--!optimize 2
local promise = require(script.Parent.Parent.promise)
local janitor = require(script.Parent.Parent.janitor)

export type Promise<T...> = promise.TypedPromise<T...>
export type Janitor = janitor.Janitor

export type task = {
	type: "call" | "wait" | "promise" | "andThen" | "catch" | "finally" | "track",
	payload: any,
}

export type status = "Pending" | "Running" | "Resolved" | "Rejected" | "Destroyed"

export type TaskChain = {
	call: (self: TaskChain, func: (...any) -> ...any, ...any) -> TaskChain,
	wait: (self: TaskChain, duration: number) -> TaskChain,

	promise: (self: TaskChain, promise: Promise) -> TaskChain,
	andThen: (self: TaskChain, onSuccess: ((...any) -> ...any)?, onFailure: ((any) -> any)?) -> TaskChain,
	track: <T>(self: TaskChain, item: T, cleanupMethod: string | boolean | nil) -> TaskChain,

	catch: (self: TaskChain, errorHandler: (err: any) -> any) -> TaskChain,
	finally: (self: TaskChain, handler: () -> ()) -> TaskChain,
	destroy: (self: TaskChain) -> (),
	remove: (self: TaskChain) -> (),

	await: (self: TaskChain) -> any?,
	fork: (self: TaskChain) -> TaskChain,

	_tasks: { task },
	_janitor: Janitor,
	_promise: Promise,
	_status: status,
	_isProcessing: boolean,
}

return {
	promise = promise,
	janitor = janitor,
}


--!optimize 2
local TaskChain = {}
TaskChain.__index = TaskChain

local types = require(script.Types)

--]
local function _processNextTask(self: types.TaskChain)
	if self._status == "Destroyed" or not self._isProcessing then
		return
	end

	if #self._tasks == 0 then
		self._isProcessing = false
		return
	end

	local taskDescriptor = table.remove(self._tasks, 1)

	self._promise = self._promise:andThen(function(...)
		if self._status == "Destroyed" then
			return types.promise
				.new(function(_, _, onCancel)
					onCancel(function()
						return
					end)
				end)
				:cancel()
		end

		local results = { ... }
		local taskType = taskDescriptor.type
		local payload = taskDescriptor.payload
		if taskType == "call" then
			return payload.func(table.unpack(results), table.unpack(payload.args))
		elseif taskType == "wait" then
			local waitPromise = types.promise.delay(payload.duration)
			self._janitor:Add(waitPromise, "cancel")
			return waitPromise:andThen(function()
				return table.unpack(results)
			end)
		elseif taskType == "promise" then
			self._janitor:Add(payload.promise, "cancel")
			return payload.promise
		elseif taskType == "andThen" then
			local p = types.promise.resolve(table.unpack(results))
			return p:andThen(payload.onSuccess, payload.onFailure)
		elseif taskType == "track" then
			self._janitor:Add(payload.item, payload.cleanupMethod)
			return table.unpack(results)
		elseif taskType == "catch" or taskType == "finally" then
			return table.unpack(results)
		end
	end)
	_processNextTask(self)
end

--]
function TaskChain.new(): types.TaskChain
	local self = setmetatable({
		_tasks = {},
		_janitor = types.janitor.new(),
		_status = "Pending",
		_isProcessing = false,
	}, TaskChain)

	self._promise = types.promise.resolve()
	self._janitor:Add(self._promise, "cancel")

	return self
end

--]
function TaskChain:call(func: (...any) -> ...any, ...): types.TaskChain
	table.insert(self._tasks, {
		type = "call",
		payload = { func = func, args = { ... } },
	})
	if not self._isProcessing then
		self._isProcessing = true
		self._status = "Running"
		_processNextTask(self)
	end
	return self
end

--]
function TaskChain:wait(duration: number?): types.TaskChain
	table.insert(self._tasks, {
		type = "wait",
		payload = { duration = (duration or 0.016666) },
	})
	if not self._isProcessing then
		self._isProcessing = true
		self._status = "Running"
		_processNextTask(self)
	end
	return self
end

--]
function TaskChain:promise(promise: types.Promise): types.TaskChain
	table.insert(self._tasks, {
		type = "promise",
		payload = { promise = promise },
	})
	if not self._isProcessing then
		self._isProcessing = true
		self._status = "Running"
		_processNextTask(self)
	end
	return self
end

--]
function TaskChain:andThen(onSuccess, onFailure): types.TaskChain
	table.insert(self._tasks, {
		type = "andThen",
		payload = { onSuccess = onSuccess, onFailure = onFailure },
	})
	if not self._isProcessing then
		self._isProcessing = true
		self._status = "Running"
		_processNextTask(self)
	end
	return self
end

--]
function TaskChain:track(item, cleanupMethod: string): types.TaskChain
	table.insert(self._tasks, {
		type = "track",
		payload = { item = item, cleanupMethod = cleanupMethod },
	})
	if not self._isProcessing then
		self._isProcessing = true
		self._status = "Running"
		_processNextTask(self)
	end
	return self
end

--]
function TaskChain:catch(errorHandler): types.TaskChain
	self._promise = self._promise:catch(errorHandler)
	self._promise:catch(function()
		if self._status ~= "Destroyed" then
			self._status = "Rejected"
		end
	end)
	return self
end

--]
function TaskChain:finally(handler): types.TaskChain
	self._promise = self._promise:finally(handler)
	self._promise:andThen(function()
		if self._status == "Running" then
			self._status = "Resolved"
		end
	end)
	return self
end

--]
function TaskChain:await(): any?
	local value = select(2, self._promise:await())
	self:destroy()
	return value
end

--]
function TaskChain:fork(): types.TaskChain
	local newChain = TaskChain.new()
	newChain._promise = self._promise
	return newChain
end

--]
function TaskChain:destroy()
	if self._status == "Destroyed" then
		return
	end

	self._status = "Destroyed"
	self._isProcessing = false
	self._tasks = {}

	self._janitor:Destroy()
	table.clear(self)
	setmetatable(self, nil)
end

TaskChain.remove = TaskChain.destroy

return table.freeze(TaskChain)


--!native
local Int = {}
Int.__index = Int

export type Int = typeof(setmetatable({
	sign = 0 :: number, -- -1, 0, +1
	limbs = {} :: { number }, -- little-endian uint32 limbs (base 2^32)
}, Int))

local function trimLimbs(limbs: { number })
	while #limbs > 0 and limbs[#limbs] == 0 do
		table.remove(limbs)
	end
end

local function cloneArray(t: { number }): { number }
	local out = table.create(#t)
	for i = 1, #t do
		out[i] = t[i]
	end
	return out
end

local function assertEndian(endian: string)
	if endian ~= "le" and endian ~= "be" then
		error(("Invalid endian '%s'. Use 'le' or 'be'."):format(endian))
	end
end

local function assertBytes(bytes: { number })
	if type(bytes) ~= "table" then
		error("bytes must be an array/table of integers 0..255")
	end
	for i = 1, #bytes do
		local b = bytes[i]
		if type(b) ~= "number" or b % 1 ~= 0 or b < 0 or b > 255 then
			error(("bytes[%d] is not a valid byte (0..255 integer). Got: %s"):format(i, b))
		end
	end
end

local function toLEBytes(endian: string, bytes: { number }): { number }
	if endian == "le" then
		return cloneArray(bytes)
	end
	local out = table.create(#bytes)
	for i = 1, #bytes do
		out[i] = bytes[#bytes - i + 1]
	end
	return out
end

local function fromLEBytes(endian: string, leBytes: { number }): { number }
	if endian == "le" then
		return leBytes
	end
	local out = table.create(#leBytes)
	for i = 1, #leBytes do
		out[i] = leBytes[#leBytes - i + 1]
	end
	return out
end

local function twosComplementToMagnitudeLE(leBytes: { number }): { number }
	local n = #leBytes
	local out = table.create(n)
	for i = 1, n do
		out[i] = 255 - leBytes[i]
	end
	-- +1
	local carry = 1
	for i = 1, n do
		local v = out[i] + carry
		if v >= 256 then
			out[i] = v - 256
			carry = 1
		else
			out[i] = v
			carry = 0
			break
		end
	end
	return out
end

local function magnitudeToTwosComplementLE(magLE: { number }, length: number): { number }
	local out = table.create(length, 0)
	for i = 1, length do
		out[i] = (i <= #magLE) and magLE[i] or 0
	end
	for i = 1, length do
		out[i] = 255 - out[i]
	end
	-- +1
	local carry = 1
	for i = 1, length do
		local v = out[i] + carry
		if v >= 256 then
			out[i] = v - 256
			carry = 1
		else
			out[i] = v
			carry = 0
			break
		end
	end
	return out
end

local function limbsFromLEBytes(leBytes: { number }): { number }
	local limbs = {}
	local limbIndex = 1
	local i = 1
	while i <= #leBytes do
		local b0 = leBytes[i] or 0
		local b1 = leBytes[i + 1] or 0
		local b2 = leBytes[i + 2] or 0
		local b3 = leBytes[i + 3] or 0

		local limb = b0 + b1 * 256 + b2 * 65536 + b3 * 16777216
		limbs[limbIndex] = limb

		limbIndex += 1
		i += 4
	end
	trimLimbs(limbs)
	return limbs
end

local function leBytesFromLimbs(limbs: { number }): { number }
	if #limbs == 0 then
		return { 0 }
	end
	local out = table.create(#limbs * 4)
	local k = 1
	for i = 1, #limbs do
		local limb = limbs[i]
		for _ = 1, 4 do
			out[k] = limb % 256
			limb = math.floor(limb / 256)
			k += 1
		end
	end
	while #out > 1 and out[#out] == 0 do
		table.remove(out)
	end
	return out
end

return function(InfiniteMath): typeof(Int)
	function Int.is(value: any): boolean
		return typeof(value) == "table" and getmetatable(value) == Int
	end

	function Int.zero(): Int
		return setmetatable({ sign = 0, limbs = {} }, Int)
	end

	function Int.FromRawBytes(endian: string, bytes: { number }, signed: boolean?): Int
		assertEndian(endian)
		assertBytes(bytes)

		if #bytes == 0 then
			return Int.zero()
		end

		local le = toLEBytes(endian, bytes)
		local sign = 1

		if signed then
			local msb = le[#le]
			if msb >= 128 then
				sign = -1
				le = twosComplementToMagnitudeLE(le)
			end
		end

		local limbs = limbsFromLEBytes(le)
		if #limbs == 0 then
			return Int.zero()
		end

		return setmetatable({
			sign = sign,
			limbs = limbs,
		}, Int)
	end

	function Int:ToRawBytes(endian: string, minBytes: number?, signed: boolean?): { number }
		assertEndian(endian)

		if minBytes ~= nil then
			if type(minBytes) ~= "number" or minBytes % 1 ~= 0 or minBytes < 0 then
				error("minBytes must be a non-negative integer")
			end
		end

		if signed ~= true and self.sign < 0 then
			error("ToRawBytes(unsigned) cannot serialize a negative Int. Pass signed=true.")
		end

		local magLE = leBytesFromLimbs(self.limbs)

		if signed then
			if self.sign >= 0 then
				-- ensure sign bit 0
				if magLE[#magLE] >= 128 then
					magLE[#magLE + 1] = 0
				end
				if minBytes ~= nil then
					while #magLE < minBytes do
						magLE[#magLE + 1] = 0
					end
				end
				return fromLEBytes(endian, magLE)
			end
			local length = minBytes or #magLE
			if length < #magLE then
				error("minBytes too small to represent this value")
			end
			while #magLE < length do
				magLE[#magLE + 1] = 0
			end
			local twosLE = magnitudeToTwosComplementLE(magLE, length)
			return fromLEBytes(endian, twosLE)
		end
		if minBytes ~= nil then
			if minBytes < #magLE then
				error("minBytes too small to represent this value")
			end
			while #magLE < minBytes do
				magLE[#magLE + 1] = 0
			end
		end
		return fromLEBytes(endian, magLE)
	end

	function Int:IterRawBytes(endian: string, minBytes: number?, signed: boolean?)
		return ipairs(self:ToRawBytes(endian, minBytes, signed))
	end

	function Int:ToNumber()
		if self.sign == 0 or #self.limbs == 0 then
			return InfiniteMath.new(0)
		end

		local n: number = #self.limbs
		local msw: number = self.limbs[n]
		if msw == 0 then
			return InfiniteMath.new(0)
		end

		-- log10(value) ~= ((n-1)*32 + log2(msw)) * log10(2)
		local LOG10_2 = math.log(2) / math.log(10)
		local log2 = (n - 1) * 32 + (math.log(msw) / math.log(2))
		local log10 = log2 * LOG10_2

		local exp10 = math.floor(log10)
		local frac = log10 - exp10
		local mant = 10 ^ frac -- [1,10)

		return InfiniteMath.new({ mant * self.sign, exp10 })
	end

	function Int:ScientificNotation(abbrev: boolean?, secondAbbrev: boolean?): string
		return self:ToNumber():ScientificNotation(abbrev, secondAbbrev)
	end

	function Int:__tostring()
		return self:ScientificNotation()
	end

	return Int
end


--!native
-- forked by @EternityDev
-- originally created by @KdudeDev

--[=[
	@class InfiniteMath

	InfiniteMath is module that allows you to surpass the limit of -10^308 to 10^308, with a new limit of -10^^308 to 10^^308 (1 with 10^308 zeros)

	InfiniteMath has many of the functions from the math library, as well as metamethods for arithmatic and comparison.

	Datastores and OrderedDataStores are supported, with special functions for OrderedDataStores.
]=]

--[=[
	An InfiniteMath constructed number that has a limit of -10^^308 to 10^^308.

	@class Number
]=]

--[=[
	@prop DECIMALPOINTS number
	@within InfiniteMath

	How many decimal points are displayed. 1 = 1.0, 2 = 1.00, 3 = 1.000, etc.
]=]

--[=[
	@prop AALENGTHMAX number
	@within InfiniteMath

	The maximum length for suffixes generated for aaNotation conversions. Exceeding this length results in a scientific number instead.
]=]

local InfiniteMath = {
	DECIMALPOINTS = 2,
	AALENGTHMAX = 3,
}

local Number = {}
Number.__index = Number

export type Number = typeof(setmetatable({
	first = 0 :: number,
	second = 0 :: number
}, Number))

--[[ Private variables ]]--

local THRESHOLD = 16
local LEADERBOARDPRECISION, LEADERBOARDPOINT = 10000, 5
local NaN = 0/0
local POW10 = table.create(617)
local SMALL_POW10 = table.create(2*THRESHOLD + 1)

-- pre-compute powers with offset
for i = -308, 308 do
	POW10[i + 309] = 10 ^ i
end
for d = -THRESHOLD, THRESHOLD do
	SMALL_POW10[d + THRESHOLD + 1] = 10 ^ d
end

local suffixes = require("@self/Values/Suffixes")
local full_names = require("@self/Values/FullNames")
local int = require("@self/Int")

--[[ Private functions ]]--
local function fastNew(first: number, second: number): Number
	return setmetatable({ first = first, second = second }, Number)
end

local function pow10(e: number): number
	-- e must be integer here
	return POW10[e + 309] or (10 ^ e)
end

local function smallPow10(e: number): number
	-- e must be integer here
	return SMALL_POW10[e + THRESHOLD + 1] or (10 ^ e)
end

local function isInt(x: number): boolean
	return x % 1 == 0
end

local function normalize(first: number, second: number): (number, number)
	if first == 0 then
		return 0, 0
	end
	local sign = if first < 0 then -1 else 1
	local m = math.abs(first)

	-- m is always a finite double here. cuz log10(m) is in [-308..308]
	local k = math.floor(math.log10(m))
	if k ~= 0 then
		m /= pow10(k)
		second += k
	end
	if m >= 10 then
		m /= 10
		second += 1
	elseif m < 1 then
		m *= 10
		second -= 1
	end
	return m * sign, second
end

local function normalize2(first: number, second: number): (number, number)
	if first == 0 then
		return 0, 0
	end

	local sign = if first < 0 then -1 else 1
	local m = math.abs(first)

	if m >= 10 then
		m /= 10
		second += 1
		if m >= 10 then
			m /= 10
			second += 1
		end
	elseif m < 1 then
		m *= 10
		second -= 1
		if m < 1 then
			m *= 10
			second -= 1
		end
	end

	if m >= 10 or m < 1 then
		return normalize(sign * m, second)
	end

	return sign * m, second
end

local function convert(number: number): (number, number)
	if type(number) ~= "number" then
		error('Type is not a "number".')
	end

	if number ~= number then
		error("NaN is not supported.")
	end
	if number == math.huge or number == -math.huge then
		error('INF is not supported as a number input. Use "1,1000" or "1e+1000".')
	end

	if number == 0 then
		return 0, 0
	end

	local sign = if number < 0 then -1 else 1
	number = math.abs(number)

	-- exponent guess
	local e = math.floor(math.log10(number))
	-- scale into ~[1,10)
	local m = number / (10 ^ e)

	if m >= 10 then
		m /= 10
		e += 1
	elseif m < 1 then
		m *= 10
		e -= 1
	end
	return m * sign, e
end

local function coerce(x: any): Number
	if type(x) == "table" and getmetatable(x) == Number then
		return x
	elseif type(x) == "number" then
		local f, e = convert(x)
		return fastNew(f, e)
	elseif type(x) == "string" then
		return InfiniteMath.new(x)
	end
	local f = x.first or x[1]
	local e = x.second or x[2]
	f, e = normalize(f, e)
	return fastNew(f, e)
end

local function isNumberObject(x: any): boolean
	return type(x) == "table" and getmetatable(x) == Number
end

local function isFieldNaN(v: any): boolean
	return v == "nan" or (type(v) == "number" and v ~= v)
end

local function isFieldInf(v: any): boolean
	return v == "inf" or v == "-inf"
		or (type(v) == "number" and (v == math.huge or v == -math.huge))
end

local function checkNumber(a): Number
	if type(a) == "table" and getmetatable(a) == Number then
		return a
	end
	return coerce(a)
end

local function cmp(a: Number, b: Number): number
	local f1, e1 = a.first, a.second
	local f2, e2 = b.first, b.second

	local s1, s2 = math.sign(f1), math.sign(f2)
	if s1 ~= s2 then
		return (s1 < s2) and -1 or 1
	end
	if s1 == 0 then return 0 end

	-- magnitude
	if e1 ~= e2 then
		if s1 > 0 then
			return (e1 < e2) and -1 or 1
		end
		return (e1 > e2) and -1 or 1 -- inverted for negatives
	end

	if f1 ~= f2 then
		return (f1 < f2) and -1 or 1
	end
	return 0
end

local function parseExpString(s: string): (number?, number?)
	local m, e = string.match(s, "^%s*([+-]?%d*%.?%d+)%s*,%s*([+-]?%d+)%s*$")
	if not m then
		m, e = string.match(s, "^%s*([+-]?%d*%.?%d+)%s*[eE]%s*([+-]?%d+)%s*$")
	end
	if not m then return nil end
	return tonumber(m), tonumber(e)
end

-- math metamethods:
function Number.__add(a, b)
	a, b = coerce(a), coerce(b)

	local first1, second1 = a.first, a.second
	local first2, second2 = b.first, b.second
	local difference = second1 - second2

	if math.abs(difference) > THRESHOLD then -- Check if difference in exponents is greater than threshold
		if math.max(second1, second2) == second1 then
			return fastNew(first1, second1)
		end
		return fastNew(first2, second2)
	end

	first2 *= smallPow10(-difference)
	first1, second1 = normalize(first1 + first2, second1)

	return fastNew(first1 , second1)
end

function Number.__sub(a, b)
	a, b = coerce(a), coerce(b)

	local first1, second1 = a.first, a.second
	local first2, second2 = b.first, b.second
	local difference = second1 - second2

	if math.abs(difference) > THRESHOLD then -- Check if difference in exponents is greater than threshold
		if math.max(second1, second2) == second1 then
			return fastNew(first1 ,second1)
		end
		return fastNew(first2, second2)
	end

	first2 *= smallPow10(-difference)
	first1, second1 = normalize(first1 - first2, second1)

	return fastNew(first1 , second1)
end

function Number.__mul(a, b)
	a = coerce(a); b = coerce(b)
	local rf, re = normalize2(a.first * b.first, a.second + b.second)
	return fastNew(rf, re)
end

function Number.__div(a, b)
	a = coerce(a); b = coerce(b)
	local rf, re = normalize2(a.first / b.first, a.second - b.second)
	return fastNew(rf, re)
end

function Number.__pow(a, power)
	a = coerce(a)

	if type(power) == "string" then
		power = tonumber(power)
	elseif type(power) == "table" and getmetatable(power) == Number then
		power = power:Reverse()
	end

	if power == math.huge or power ~= power or type(power) ~= "number" then
		error(`'{power}' is not a valid power. If power is 'inf' you must keep it below 10^308.`)
		return
	end

	if power == 0 then
		return fastNew(1, 0)
	end

	local f = a.first
	local e = a.second

	if f == 0 then
		if power < 0 then
			error("0 cannot be raised to a negative power")
		end
		return fastNew(0, 0)
	end

	local sign = 1
	if f < 0 then
		if not isInt(power) then
			return fastNew(NaN, NaN)
		end
		if (power % 2) ~= 0 then
			sign = -1
		end
		f = -f
	end

	-- log10(|a|) = log10(f) + e
	local y = power * (math.log10(f) + e)
	if y ~= y then
		return fastNew(NaN, NaN)
	end
	if y == math.huge or y == -math.huge then
		error("result exponent overflow")
	end

	local newE = math.floor(y)
	local newF = 10 ^ (y - newE) -- in [1,10)

	if newF >= 10 then
		newF /= 10
		newE += 1
	end

	return fastNew(newF * sign, newE)
end

function Number.__mod(a, b)
	a, b = coerce(a), coerce(b)
	local sign = InfiniteMath.sign(a)

	local divided

	if sign == 1 then
		divided = InfiniteMath.floor(a / b)
	else
		divided = InfiniteMath.round(a / b)
	end
	local nextNum = b * divided

	return a - nextNum
end

function Number.__eq(a, b)
	a, b = coerce(a), coerce(b)
	local c = cmp(a, b)
	return (c == c) and (c == 0)
end

function Number.__lt(a, b)
	a, b = coerce(a), coerce(b)
	local c = cmp(a, b)
	if c ~= c then
		return false
	end
	return c < 0
end

function Number.__le(a, b)
	a, b = coerce(a), coerce(b)
	local c = cmp(a, b)
	if c ~= c then
		return false
	end
	return c <= 0
end

function Number.__unm(a)
	a = coerce(a)
	return fastNew(-a.first, a.second) -- much faster than a * -1
end

function Number.__tostring(self)
	return self:GetSuffix(true)
end

function Number.__concat(self, value)
	return `{self}{value}`
end

--[[ Class methods ]]--

--[=[
	@within InfiniteMath

	Returns a new InfiniteMath constructed number

	You can use numbers `1`, correctly formatted strings `"1,0"`, tables `{1, 0}`, and other constructed numbers `InfiniteMath.new(1)`.

	```lua
		print(InfiniteMath.new(1)) -- 1
	```

	To create a number above 1e+308, we can use strings or tables.

	```lua
		print(InfiniteMath.new("1,1000")) -- 10DTL
		print(InfiniteMath.new({1, 1000})) -- 10DTL
	```

	@param val number | string | table | Number
	@return Number
]=]
function InfiniteMath.new(val : number | string | {} | Number) : Number
	local first, second

	if type(val) == "table" then
		if val.first ~= nil and val.second ~= nil then
			if getmetatable(val) ~= Number then
				return setmetatable(val, Number)
			end

			return val
		end

		first = val[1]
		second = val[2]
	elseif type(val) == "string" then
		local mantStr, expStr = parseExpString(val)
		if mantStr and expStr then
			first, second = normalize(mantStr, expStr)
		else
			local n = tonumber(val)
			if n == nil then
				error('string is not a valid number format. Use "1,0" or "1e+3" etc.')
			end
			if n == math.huge or n == -math.huge then
				error('String overflows to INF. Use explicit exponent like "1e+1000" (supported) or "1,1000".')
			end
			first, second = convert(n)
		end
	elseif type(val) == 'number' then
		if val ~= val then
			error("NaN is not allowed.")
		end
		if val == math.huge or val == -math.huge then
			error('INF number is not allowed. Please use "string" or "table" instead of "number" to go above INF (e.g. "1,1000" or "1e+1000").')
		end

		first, second = convert(val)
	else
		error(`'{type(val)}' is not a valid type. Please only use "number", "string", "table", or constructed numbers.`)
		return
	end

	return setmetatable({
		first = first,
		second = second
	}, Number)
end

--[=[
	@within Number

	Sets the value of the constructed number without creating a new Number metatable. If you want to set the value of a constructed number it is better to use this than setting it to a new `.new()`.

	`SetValue` takes a new `first` and `second` to set the value to, if you want to set the value to `100` you must pass `1, 3`.

	```lua
		local Number = InfiniteMath.new(1000)
		print(Number) -- 1000
		Number:SetValue(5, 4)
		print(Number) -- 5000
	```

	@method SetValue
	@param newFirst number
	@param newSecond number
]=]
function Number:SetValue(newFirst: number, newSecond: number)
	if type(newFirst) ~= "number" or type(newSecond) ~= "number" then
		error('Both parameters of SetValue must be "number"')
	end

	newFirst, newSecond = normalize(newFirst, newSecond)

	self.first = newFirst
	self.second = newSecond
end

--[=[
	@within Number

	Attempts to return the constructed number converted into a regular number. If the constructed number is above 1e+308 it will instead return INF.

	```lua
		print(InfiniteMath.new("1, 3"):Reverse()) -- 1000
	```

	@method Reverse
	@return number
]=]
function Number:Reverse(): number
	local first, second = normalize(self.first, self.second)

	return first * 10^second
end

--[=[
	@within Number

	Returns a string with the number and a suffix at the end, these suffixes will go up to 1e+12000. After, it will default to returning scientific notation.

	By default, it will return an abbreviated suffix (1K). Using true will use the default behavior. Using false will return the full suffix (1 Thousand).

	```lua
		print(InfiniteMath.new(1000):GetSuffix()) -- 1K
		print(InfiniteMath.new(1000):GetSuffix(true)) -- 1K
		print(InfiniteMath.new(1000):GetSuffix(false)) -- 1 Thousand
	```

	@method GetSuffix
	@param abbreviation boolean | nil
	@return string
]=]
function Number:GetSuffix(abbreviation: boolean?): string
	if type(abbreviation) ~= "boolean" then
		abbreviation = true
	end

	local first, second = normalize(self.first, self.second)

	if second < 3 then
		local result = `{self:Reverse()}`
		local Length = 2
		if math.sign(first) == -1 then Length = 3 end

		if math.abs(first) >= 1 then
			if InfiniteMath.DECIMALPOINTS > 0 then
				result = result:sub(1, second + Length + InfiniteMath.DECIMALPOINTS)
				local decimal = string.split(result, ".")[2]
				if decimal == nil then return result end

				if tonumber(decimal) == 0 then
					result = string.split(result, ".")[1]
				elseif decimal == string.rep(9 :: any, InfiniteMath.DECIMALPOINTS) then
					result = tonumber(string.split(result, ".")[1]) + 1
				end
			else
				result = string.split(result, ".")[1]
			end
		else
			if math.abs(first) <= .01 then return "0" end
			result = result:sub(1, second + Length + InfiniteMath.DECIMALPOINTS)
			local decimal = result:split(".")[2]

			if decimal == string.rep(9 :: any, InfiniteMath.DECIMALPOINTS) then
				result = tonumber(string.split(result, ".")[1]) + 1
			end
		end

		return result
	end

	local secondRemainder = second % 3
	first *= smallPow10(secondRemainder)

	local suffixIndex = math.floor(second/3)
	local dp = InfiniteMath.DECIMALPOINTS
	local scale = pow10(dp)
	local str = math.floor(first * scale) / scale -- The * 10 / 10 controls decimal precision, more zeros = more decimals

	local suffix = if abbreviation then suffixes[suffixIndex] else full_names[suffixIndex]
	if not abbreviation and suffix ~= nil then
		suffix = " " .. suffix
	end

	if suffixIndex > 0 then
		if suffix ~= nil then
			str ..= suffix
		else
			str = self:ScientificNotation(abbreviation)
		end
	end

	return str
end

--[=[
	@within Number

	Returns a string with the number formatted in scientific notation.

	```lua
		print(InfiniteMath.new(1000):ScientificNotation()) -- 1e+3
	```

	When a number reaches `1e+1000000` you can choose an abbreviation mode for the amount of zeros in the scientific notation. By default, it will use GetSuffix on the exponent `1e+1M`, but you can also choose to have it use scientific notation `1e+1e+6`.

	```lua
		print(InfiniteMath.new("1, 1e+6"):ScientificNotation()) -- 1e+1M
		print(InfiniteMath.new("1, 1e+6"):ScientificNotation(true)) -- 1e+1M
		print(InfiniteMath.new("1, 1e+6"):ScientificNotation(false)) -- 1e+1e+6
	```

	You can also use nil and false to stop the functionality and instead just display `1e+1000000`.

	```lua
		print(InfiniteMath.new("1, 1e+6"):ScientificNotation(nil, false)) -- 1e+1000000
	```

	@method ScientificNotation
	@param abbreviation boolean | nil
	@param secondAbbreviation boolean | nil
	@return string
]=]
function Number:ScientificNotation(abbreviation: boolean?, secondAbbreviation: boolean?): string
	local first, second = normalize(self.first, self.second)

	local dp = InfiniteMath.DECIMALPOINTS
	local scale = pow10(dp)
	local str = math.floor(first * scale) / scale -- The * 10 / 10 controls decimal precision, more zeros = more decimals

	if secondAbbreviation == false then
		second = tostring(second)
	elseif second > 1e+6 then
		second = fastNew(second, 0)
		if abbreviation ~= false then
			second = second:GetSuffix(true)
		else
			second = second:ScientificNotation(nil, false)
		end
	end

	return `{str}e+{second}`
end

--[=[
	@within Number

	Returns a string with the number formatted in logarithmic notation.

	```lua
		print(InfiniteMath.new(1000):ScientificNotation()) -- e3.0
	```

	@method LogarithmNotation
	@return string
]=]
function Number:LogarithmNotation(): string
	local first, second = normalize(self.first, self.second)
	first, second = `{first}`, `{second}`

	local suffixIndex = math.floor(second/3)

	if suffixIndex == 0 then
		local secondRemainder = second % 3
		first *= 10^secondRemainder

		return math.floor(first * 10^InfiniteMath.DECIMALPOINTS)/10^InfiniteMath.DECIMALPOINTS
	end

	local log = `{math.log10(first :: any)}`

	if string.match(log, "%.") then
		log = string.split(log, ".")
		log = string.sub(log[2], 1, 3)
	end

	return `e{second}.{log}`
end

local Alphabet = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"}

--[=[
	@within Number

	Returns a string with the number formatted in double letter notation. Use the AALENGTHMAX property to adjust the max letters possible.

	```lua
		print(InfiniteMath.new(1e+15):aaNotation()) -- 1a
	```

	@method aaNotation
	@return string
]=]
function Number:aaNotation(): string
	local first, second = normalize(self.first, self.second)

	local secondRemainder = second % 3
	first *= smallPow10(secondRemainder)
	local suffixIndex = math.floor(second / 3) - 4

	if suffixIndex <= 0 then
		return self:GetSuffix()
	end

	local suffix = ""
	while suffixIndex > 0 do
		local remainder = suffixIndex % #Alphabet
		remainder = if remainder == 0 then #Alphabet else remainder
		suffix = `{Alphabet[remainder]}{suffix}`
		suffixIndex = math.floor((suffixIndex - remainder) / #Alphabet)
	end

	if #suffix > InfiniteMath.AALENGTHMAX then
		return self:ScientificNotation()
	end

	local dp = InfiniteMath.DECIMALPOINTS
	local scale = pow10(dp)
	local str = math.floor(first * scale) / scale
	return `{str}{suffix}`
end

--[=[
	@within Number

	Returns a number that you can use for OrderedDataStores in order to create global leaderboards that support InfiniteMath constructed numbers.

	```lua
		print(InfiniteMath.new(1000):ConvertForLeaderboards()) -- 31000
	```

	@method ConvertForLeaderboards
	@return number
]=]
function Number:ConvertForLeaderboards(): number
	local first, second = normalize(self.first, self.second)
	first, second = `{first}`, `{second}`

	first = string.gsub(first, "%.", "")

	return math.floor(tonumber(`{second}.{first:sub(1, LEADERBOARDPOINT)}`) * LEADERBOARDPRECISION)
end

--[=[
	@within InfiniteMath

	Returns a constructed number, and should be given a number created by `Number:ConvertForLeaderboards`. This is what you will display on global leaderboards using OrderedDataStores.

	```lua
		local ValueFromStore = InfiniteMath.new(1000):ConvertForLeaderboards()
		print(InfiniteMath:ConvertFromLeaderboards(ValueFromStore)) -- 1K
	```

	@method ConvertFromLeaderboards
	@param GivenNumber number
	@return Number
]=]
function InfiniteMath:ConvertFromLeaderboards(GivenNumber: number): Number
	--return 0
	local num = GivenNumber
	if num == 0 then
		return fastNew(0, 0)
	end

	num /= LEADERBOARDPRECISION
	num = `{num}`

	local numbers = string.split('.')
	local second, first = numbers[1], numbers[2]
	first, second = `{first}`, `{second}`

	local firstFirst = string.sub(first, 1, 1)
	local firstSecond = string.sub(first, 2)

	first = `{firstFirst}.{firstSecond}`

	return fastNew(tonumber(first), tonumber(second))
end

--[[ Math methods ]]--

--[=[
	@within InfiniteMath

	Rounds a number down to the nearest integer

	@param Num number | string | table | Number
	@return Number
]=]
function InfiniteMath.floor(Num): Number
	Num = checkNumber(Num)

	local _first, second = normalize(Num.first, Num.second)
	if second >= 300 then return Num end -- Rounding after 1e+300 would be pointless, so don't do it.

	return InfiniteMath.new(math.floor(Num:Reverse()))
end

--[=[
	@within InfiniteMath

	Rounds a number to the nearest integer

	@param Num number | string | table | Number
	@return Number
]=]
function InfiniteMath.round(Num): Number
	Num = checkNumber(Num)

	local _, second = normalize(Num.first, Num.second)
	if second >= 300 then return Num end -- Rounding after 1e+300 would be pointless, so don't do it.

	local r = Num:Reverse() -- finite here due to second < 300
	local rounded = if r >= 0 then math.floor(r + 0.5) else math.ceil(r - 0.5)

	return fastNew(rounded, 0)
end

--[=[
	@within InfiniteMath

	Rounds a number up to the nearest integer

	@param Num number | string | table | Number
	@return Number
]=]
function InfiniteMath.ceil(Num): Number
	Num = checkNumber(Num)

	local _first, second = normalize(Num.first, Num.second)
	if second >= 300 then return Num end -- Rounding after 1e+300 would be pointless, so don't do it.

	return fastNew(math.ceil(Num:Reverse()), 0)
end

--[=[
	@within InfiniteMath

	Returns the absolute value (distance from 0)

	@param Num number | string | table | Number
	@return Number
]=]
function InfiniteMath.abs(Num): Number
	Num = checkNumber(Num)
	local first, second = normalize(Num.first, Num.second)

	return fastNew(math.abs(first), second)
end

--[=[
	@within InfiniteMath

	Clamps a number between a minimum and maximum value

	@param Num number | string | table | Number
	@param Min number | string | table | Number
	@param Max number | string | table | Number
	@return Number
]=]
function InfiniteMath.clamp<T, A, B>(Num: T, Min: A, Max: B): T
	Num = checkNumber(Num)
	local first, second = normalize(Num.first, Num.second)
	Num = fastNew(first, second)

	if Min ~= nil then
		Min = checkNumber(Min)
		local firstMin, secondMin = normalize(Min.first, Min.second)
		Min = fastNew(firstMin, secondMin)
	else
		Min = fastNew(0, 0)
	end

	if Max ~= nil then
		Max = checkNumber(Max)
		local firstMax, secondMax = normalize(Max.first, Max.second)
		Max = fastNew(firstMax, secondMax)
	else
		Max = fastNew(1, 1e+308)
	end

	Num = if Num < Min then Min elseif Num > Max then Max else Num

	return Num
end

--[=[
	@within InfiniteMath

	Returns the smallest number among the given arguments

	@param ... number | string | table | Number
	@return Number
]=]
function InfiniteMath.min(...): Number
	local Numbers = {...}
	if Numbers[1] == nil then
		error("InfiniteMath.min requires at least 1 argument.")
	end

	for Index, Num in Numbers do
		Numbers[Index] = checkNumber(Num)
	end

	local Min = Numbers[1]

	for _, Num in Numbers do
		if Num < Min then
			Min = Num
		end
	end

	return Min
end

--[=[
	@within InfiniteMath

	Returns the largest number among the given arguments

	@param ... number | string | table | Number
	@return Number
]=]
function InfiniteMath.max(...): Number
	local Numbers = {...}
	if Numbers[1] == nil then
		error("InfiniteMath.max requires at least 1 argument.")
	end

	for Index, Num in Numbers do
		Numbers[Index] = checkNumber(Num)
	end

	local Max = Numbers[1]

	for _, Num in Numbers do
		if Num > Max then
			Max = Num
		end
	end

	return Max
end

--[=[
	@within InfiniteMath

	Returns the sign of the number. Negative numbers return -1, positive numbers return 1, 0 returns 0.

	@param Num number | string | table | Number
	@return number
]=]
function InfiniteMath.sign(Num): number
	Num = checkNumber(Num)
	local first = normalize(Num.first, Num.second)
	first = tonumber(first)

	return if first > 0 then 1 elseif first < 0 then -1 else 0
end

--[=[
	@within InfiniteMath

	Returns the square root of a number

	@param Num number | string | table | Number
	@return Number
]=]

function InfiniteMath.sqrt(Num): Number
	return Num^.5
end

--[=[
	@within InfiniteMath

	Returns the remainder of the division of a by b that rounds the quotient towards zero.

	@param a number | string | table | Number
	@param b number | string | table | Number
	@return Number
]=]
function InfiniteMath.fmod(a, b): Number
	a, b = checkNumber(a), checkNumber(b)
	local q = a / b
	local qi = (InfiniteMath.sign(q) >= 0) and InfiniteMath.floor(q) or InfiniteMath.ceil(q)
	return a - b * qi
end

--[=[
	@within InfiniteMath

	Returns both the integral part of n and the fractional part (if there is one).

	@param Num number | string | table | Number
	@return Number
]=]
function InfiniteMath.modf(Num): (Number, number?)
	Num = checkNumber(Num)
	local sign = InfiniteMath.sign(Num)
	Num *= sign

	local first, second = normalize(Num.first, Num.second)
	local firstSplit = tostring(first):split(".")

	if firstSplit[2] ~= nil then
		first = string.sub(firstSplit[2], 1, second)
		first = firstSplit[1].."."..first

		local power = if second == 0 then 2 else second
		local decimal

		if second > 0 then
			local a = string.sub(firstSplit[2], power + 1)
			decimal = a / 10^#a
		else
			decimal = firstSplit[2] / 10^#firstSplit[2]
		end

		return fastNew(first, second) * sign, decimal
	end

	return Num
end

--[=[
	@within InfiniteMath

	Returns the logarithm of x with the given base. Default base is constant e (2.7182818)

	@param Num number | string | table | Number
	@param Base number
	@return Number
]=]
function InfiniteMath.log(Num, Base): Number
	Num = checkNumber(Num)

	if InfiniteMath.sign(Num) == -1 then
		return NaN  -- log of a negative number is always nan
	end

	if Base == nil then
		Base = 2.7182818
	end

	local f, e = normalize(Num.first, Num.second)

	-- log_base(Num) = log_base(f) + e * log_base(10)
	local term1 = InfiniteMath.new(e) * math.log(10, Base)
	local term2 = InfiniteMath.new(math.log(f, Base))
	return term1 + term2
end

--[=[
	@within InfiniteMath

	Returns the base-10 logarithm of x.

	@param Num number | string | table | Number
	@return Number
]=]
function InfiniteMath.log10(Num): Number
	return InfiniteMath.log(Num, 10)
end

--[=[
	@within InfiniteMath

	Checks if a value is nil (or an invalid constructed number).

	This is meant as a "safe check" for external inputs / user data.

	Returns true if:
	- `x == nil`, OR
	- `x` is a constructed `Number` but has missing fields (`first` or `second` is nil).

	@param x any
	@return boolean
]=]
function InfiniteMath.isNil(x: any): boolean
	if x == nil then
		return true
	end
	if isNumberObject(x) then
		return x.first == nil or x.second == nil
	end
	return false
end

--[=[
	@within InfiniteMath

	Checks if a value is NaN.

	Supported inputs:
	- `number`: true if `x ~= x`
	- constructed `Number`: true if either internal field is NaN-like
	- `string`: true if it contains "nan"
	- anything else: false

	This function does NOT throw.

	@param x any
	@return boolean
]=]
function InfiniteMath.isNaN(x: any): boolean
	if x == nil then
		return false
	end
	if type(x) == "number" then
		return x ~= x
	end
	if isNumberObject(x) then
		return isFieldNaN(x.first) or isFieldNaN(x.second)
	end
	if type(x) == "string" then
		return x == "nan"
	end
	return false
end

--[=[
	@within InfiniteMath

	Checks if a value is positive or negative infinity.

	Supported inputs:
	- `number`: true if `x == math.huge` or `x == -math.huge`
	- constructed `Number`: true if either internal field is inf-like
	- `string`: true if it equals "inf", "-inf"
	- anything else: false

	This function does NOT throw.

	@param x any
	@return boolean
]=]
function InfiniteMath.isInf(x: any): boolean
	if x == nil then
		return false
	end
	if type(x) == "number" then
		return x == math.huge or x == -math.huge
	end
	if isNumberObject(x) then
		return isFieldInf(x.first) or isFieldInf(x.second)
	end
	if type(x) == "string" then
		return x == "inf" or x == "-inf"
	end
	return false
end

--[=[
	@within InfiniteMath

	Checks if a value represents exactly zero.

	Note:
	- For constructed `Number`s, it normalizes using `fixNumber`, so values like `{0, 1000}` are treated as zero.

	Supported inputs:
	- `number`
	- constructed `Number`
	- `string` (tries `tonumber`, and also supports `"0,123"` style)

	This function does NOT throw.

	@param x any
	@return boolean
]=]
function InfiniteMath.isZero(x: any): boolean
	if x == nil then
		return false
	end
	if type(x) == "number" then
		return x == 0
	end
	if isNumberObject(x) then
		local f, e = normalize(x.first, x.second)
		return f == 0 and e == 0
	end
	if type(x) == "string" then
		local n = tonumber(x)
		if n ~= nil then
			return n == 0
		end
		-- "0,1234" style
		local a = x:match("^%s*([+-]?%d*%.?%d+)%s*,")
		return a ~= nil and tonumber(a) == 0
	end
	return false
end

InfiniteMath.Int = int(InfiniteMath)

return InfiniteMath


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")

local Test = require(ServerScriptService.TestRunner.Test)

-- Run tests
return Test.run({ ReplicatedStorage.Modules })