local class = {}

local fusionUtils = require("@packages/fusionUtils")
local onDestroy = fusionUtils.onDestroy

local input = require("@components/propertyFields/input")

local scaleUtils = require("@src/lib/scaleUtils")

function class.is(instance: Instance)
	return instance:IsA("Attachment") or instance:IsA("Part") or instance:IsA("Model") or instance:IsA("Folder")
end

class.properties = {
	{
		name = "Name",
		groupIdentifier = "stringInput",
		render = input,
	},
	{
		name = "Scale",
		groupIdentifier = "scaleInput",
		render = function(props)
			local inputField = input(props)

			onDestroy(inputField, {
				props.Instance:GetAttributeChangedSignal("_vfxEditorScale"):Connect(function()
					props.Value:set(props.Instance:GetAttribute("_vfxEditorScale") or 1)
				end),
			})

			return inputField
		end,
		get = function(instance)
			return instance:GetAttribute("_vfxEditorScale") or 1
		end,
		set = function(instance: Instance, value)
			-- this number should be at least 0.001
			local number = tonumber(value)

			if not number then
				return
			end

			if number < 0.001 then
				number = 0.001
			end

			local oldScale = instance:GetAttribute("_vfxEditorScale") or 1
			-- scale our instance relative to the old scale
			scaleUtils.scaleInstance(instance, number / oldScale)
			instance:SetAttribute("_vfxEditorScale", number)
		end,
	},
}

return class


local class = {}

local fusionUtils = require("@packages/fusionUtils")
local onDestroy = fusionUtils.onDestroy

local checkbox = require("@components/propertyFields/checkbox")
local input = require("@components/propertyFields/input")
local slider = require("@components/propertyFields/slider")
local range = require("@components/propertyFields/range")
local enumDropdown = require("@components/propertyFields/enumDropdown")
local colorSequence = require("@components/propertyFields/colorSequence")
local numberSequence = require("@components/propertyFields/numberSequence")
local texturePicker = require("@components/propertyFields/texturePicker")

local scaleUtils = require("@src/lib/scaleUtils")
local settingsManager = require("@src/lib/settingsManager")

local MAX_SIZE = settingsManager.getReactiveWithFallback("max_size", 10)
local MAX_SQUASH = settingsManager.getReactiveWithFallback("max_squash", 3)

function class.is(instance: Instance)
	return instance:IsA("ParticleEmitter")
end

function class.emit(instance: ParticleEmitter)
	local emitStart = tick()
	local emitAmount = instance:GetAttribute("EmitCount") or 20
	local emitDelay = instance:GetAttribute("EmitDelay") or 0
	instance:SetAttribute("_vfxEditorStartEmit", emitStart)

	task.delay(emitDelay, function()
		if instance:GetAttribute("_vfxEditorStartEmit") ~= emitStart then
			return
		end

		instance:Emit(emitAmount)
		instance:SetAttribute("_vfxEditorStartEmit", nil)
	end)
end

class.properties = {
	{
		name = "Name",
		groupIdentifier = "stringInput",
		render = input,
	},
	{
		name = "Scale",
		groupIdentifier = "scaleInput",
		render = function(props)
			local inputField = input(props)

			onDestroy(inputField, {
				props.Instance:GetAttributeChangedSignal("_vfxEditorScale"):Connect(function()
					props.Value:set(props.Instance:GetAttribute("_vfxEditorScale") or 1)
				end),
			})

			return inputField
		end,
		get = function(instance)
			return instance:GetAttribute("_vfxEditorScale") or 1
		end,
		set = function(instance: ParticleEmitter, value)
			-- this number should be at least 0.001
			local number = tonumber(value)

			if not number then
				return false
			end

			if number < 0.001 then
				number = 0.001
			end

			local oldScale = instance:GetAttribute("_vfxEditorScale") or 1
			-- scale our instance relative to the old scale
			scaleUtils.scaleInstance(instance, number / oldScale)
			instance:SetAttribute("_vfxEditorScale", number)
			return
		end,
	},
	{
		name = "Emit",
		properties = {
			{
				name = "Amount",
				groupIdentifier = "numberInput",
				linkedAttribute = "EmitCount",
				render = input,
				set = function(instance, value)
					if not tonumber(value) then
						return false
					end

					instance:SetAttribute("EmitCount", value)
					return
				end,
				get = function(instance)
					if instance:GetAttribute("EmitCount") then
						return instance:GetAttribute("EmitCount")
					end

					return instance:GetAttribute("EmitCount") or 20
				end,
			},
			{
				name = "Delay",
				groupIdentifier = "numberInput",
				linkedAttribute = "EmitDelay",
				render = input,
				set = function(instance, value)
					if not tonumber(value) then
						return false
					end

					instance:SetAttribute("EmitDelay", value)
					return
				end,
				get = function(instance)
					if instance:GetAttribute("EmitDelay") then
						return instance:GetAttribute("EmitDelay")
					end

					return instance:GetAttribute("EmitDelay") or 0
				end,
			},
		},
	},
	{
		name = "Appearance",
		properties = {
			{
				name = "Brightness",
				groupIdentifier = "slider",
				render = function(props)
					return slider({
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						Min = 0,
						Max = 10,
						Step = 0.05,
						Instance = props.Instance,
						PropertyName = props.PropertyName,
					})
				end,
			},
			{
				name = "Color",
				groupIdentifier = "colorSequence",
				render = colorSequence,
			},
			{
				name = "LightEmission",
				groupIdentifier = "slider",
				render = function(props)
					return slider({
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						Min = 0,
						Max = 1,
						Step = 0.01,
						Instance = props.Instance,
						PropertyName = props.PropertyName,
					})
				end,
			},
			{
				name = "LightInfluence",
				groupIdentifier = "slider",
				render = function(props)
					return slider({
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						Min = 0,
						Max = 1,
						Step = 0.01,
						Instance = props.Instance,
						PropertyName = props.PropertyName,
					})
				end,
			},
			{
				name = "Orientation",
				groupIdentifier = "orientationenumDropdown",
				render = enumDropdown,
			},
			{
				name = "Size",
				groupIdentifier = "sizeNumberSequence",
				render = function(props, useColor)
					return numberSequence({
						Instance = props.Instance,
						PropertyName = props.PropertyName,
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						SupportsEnvelope = true,
						Min = 0,
						Max = MAX_SIZE:get(),
					}, useColor)
				end,
				clipboardGet = function(props)
					return props.Instance:GetAttribute("_vfxEditorGraph" .. props.PropertyName:split(" ")[1])
				end,
				clipboardSet = function(props, value)
					props.Instance:SetAttribute("_vfxEditorGraph" .. props.PropertyName:split(" ")[1], value)
				end,
			},
			{
				name = "Squash",
				groupIdentifier = "squashNumberSequence",
				render = function(props, useColor)
					return numberSequence({
						Instance = props.Instance,
						PropertyName = props.PropertyName,
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						SupportsEnvelope = true,
						Min = -3,
						Max = MAX_SQUASH:get(),
					}, useColor)
				end,
				clipboardGet = function(props)
					return props.Instance:GetAttribute("_vfxEditorGraph" .. props.PropertyName:split(" ")[1])
				end,
				clipboardSet = function(props, value)
					props.Instance:SetAttribute("_vfxEditorGraph" .. props.PropertyName:split(" ")[1], value)
				end,
			},
			{
				name = "Texture",
				groupIdentifier = "texturePicker",
				render = texturePicker,
				get = function(instance: ParticleEmitter)
					return `{instance.Texture},{instance.FlipbookLayout.Name}`
				end,
				set = function(instance: ParticleEmitter, value)
					local split = value:split(",")

					if #split ~= 2 and Enum.ParticleFlipbookLayout[split[2]] then
						return false
					end

					instance.Texture = split[1]
					instance.FlipbookLayout =
						Enum.ParticleFlipbookLayout[split[2] :: any] :: Enum.ParticleFlipbookLayout

					if instance.FlipbookFramerate == NumberRange.new(1) then
						instance.FlipbookFramerate = NumberRange.new(30)
					end

					return
				end,
			},
			{
				name = "Transparency",
				groupIdentifier = "transparencyNumberSequence",
				render = function(props, useColor)
					return numberSequence({
						Instance = props.Instance,
						PropertyName = props.PropertyName,
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						SupportsEnvelope = true,
						Min = 0,
						Max = 1,
					}, useColor)
				end,
				clipboardGet = function(props)
					return props.Instance:GetAttribute("_vfxEditorGraph" .. props.PropertyName:split(" ")[1])
				end,
				clipboardSet = function(props, value)
					props.Instance:SetAttribute("_vfxEditorGraph" .. props.PropertyName:split(" ")[1], value)
				end,
			},
			{
				name = "ZOffset",
				groupIdentifier = "slider",
				render = function(props)
					return slider({
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						Min = 0,
						Max = 20,
						Step = 0.1,
						Instance = props.Instance,
						PropertyName = props.PropertyName,
					})
				end,
			},
		},
	},
	{
		name = "Emission",
		properties = {
			{
				name = "EmissionDirection",
				groupIdentifier = "emissionDirectionenumDropdown",
				render = enumDropdown,
			},
			{
				name = "Enabled",
				groupIdentifier = "checkbox",
				render = checkbox,
			},
			{
				name = "Lifetime",
				groupIdentifier = "range",
				render = function(props)
					return range({
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						Min = 0,
						Max = 5,
						Step = 0.1,
						Instance = props.Instance,
						PropertyName = props.PropertyName,
					})
				end,
			},
			{
				name = "Rate",
				groupIdentifier = "slider",
				render = function(props)
					return slider({
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						Min = 0,
						Max = 100,
						Step = 1,
						Instance = props.Instance,
						PropertyName = props.PropertyName,
					})
				end,
			},
			{
				name = "Rotation",
				groupIdentifier = "range",
				render = function(props)
					return range({
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						Min = -180,
						Max = 180,
						Step = 1,
						Instance = props.Instance,
						PropertyName = props.PropertyName,
					})
				end,
			},
			{
				name = "RotSpeed",
				groupIdentifier = "range",
				render = function(props)
					return range({
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						Min = -360,
						Max = 360,
						Step = 1,
						Instance = props.Instance,
						PropertyName = props.PropertyName,
					})
				end,
			},
			{
				name = "Speed",
				groupIdentifier = "range",
				render = function(props)
					return range({
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						Min = 0,
						Max = 100,
						Step = 1,
						Instance = props.Instance,
						PropertyName = props.PropertyName,
					})
				end,
			},
			{
				name = "SpreadAngle",
				groupIdentifier = "vector2Input",
				render = input,
				set = function(instance, value)
					if typeof(value) == "Vector2" then
						if instance.SpreadAngle.X ~= value.X or instance.SpreadAngle.Y ~= value.Y then
							instance.SpreadAngle = value
						end

						return false
					end

					local x, y = value:match("([^,]+),([^,]+)")

					if not x or not y then
						return false
					end

					instance.SpreadAngle = Vector2.new(tonumber(x), tonumber(y))
					return
				end,
			},
		},
	},
	{
		name = "EmitterShape",
		properties = {
			{
				name = "Shape",
				groupIdentifier = "shapeenumDropdown",
				render = enumDropdown,
			},
			{
				name = "ShapeInOut",
				groupIdentifier = "shapeInOutenumDropdown",
				render = enumDropdown,
			},
			{
				name = "ShapePartial",
				groupIdentifier = "slider",
				render = function(props, _, currentProperties)
					if currentProperties:get(false).Shape:get() == Enum.ParticleEmitterShape.Box then
						return nil
					end

					return slider({
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						Min = 0,
						Max = 1,
						Step = 0.01,
						Instance = props.Instance,
						PropertyName = props.PropertyName,
					})
				end,
			},
			{
				name = "ShapeStyle",
				groupIdentifier = "shapeStyleenumDropdown",
				render = enumDropdown,
			},
		},
	},
	{
		name = "Motion",
		properties = {
			{
				name = "Acceleration",
				groupIdentifier = "vector3Input",
				render = input,
				set = function(instance, value)
					if typeof(value) == "Vector3" then
						if
							instance.Acceleration.X ~= value.X
							or instance.Acceleration.Y ~= value.Y
							or instance.Acceleration.Z ~= value.Z
						then
							instance.Acceleration = value
						end

						return false
					end

					local x, y, z = value:match("([^,]+),([^,]+),([^,]+)")

					if not x or not y or not z then
						return false
					end

					instance.Acceleration = Vector3.new(tonumber(x), tonumber(y), tonumber(z))
					return
				end,
			},
		},
	},
	{
		name = "Particles",
		properties = {
			{
				name = "Drag",
				groupIdentifier = "slider",
				render = function(props)
					return slider({
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						Min = 0,
						Max = 5,
						Step = 0.025,
						Instance = props.Instance,
						PropertyName = props.PropertyName,
					})
				end,
			},
			{
				name = "LockedToPart",
				groupIdentifier = "checkbox",
				render = checkbox,
			},
			{
				name = "TimeScale",
				groupIdentifier = "slider",
				render = function(props)
					return slider({
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						Min = 0,
						Max = 1,
						Step = 0.02,
						Instance = props.Instance,
						PropertyName = props.PropertyName,
					})
				end,
			},
			{
				name = "VelocityInheritance",
				groupIdentifier = "slider",
				render = function(props)
					return slider({
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						Min = 0,
						Max = 1,
						Step = 0.02,
						Instance = props.Instance,
						PropertyName = props.PropertyName,
					})
				end,
			},
			{
				name = "WindAffectsDrag",
				groupIdentifier = "checkbox",
				render = checkbox,
			},
		},
	},
}

return class


local class = {}

local fusionUtils = require("@packages/fusionUtils")
local onDestroy = fusionUtils.onDestroy

local checkbox = require("@components/propertyFields/checkbox")
local input = require("@components/propertyFields/input")
local slider = require("@components/propertyFields/slider")
local object = require("@components/propertyFields/object")
local enumDropdown = require("@components/propertyFields/enumDropdown")
local colorSequence = require("@components/propertyFields/colorSequence")
local numberSequence = require("@components/propertyFields/numberSequence")
local texturePicker = require("@components/propertyFields/texturePicker")

local scaleUtils = require("@src/lib/scaleUtils")

function class.is(instance: Instance)
	return instance:IsA("Trail")
end

class.properties = {
	{
		name = "Name",
		groupIdentifier = "stringInput",
		render = input,
	},
	{
		name = "Scale",
		groupIdentifier = "scaleInput",
		render = function(props)
			local inputField = input(props)

			onDestroy(inputField, {
				props.Instance:GetAttributeChangedSignal("_vfxEditorScale"):Connect(function()
					props.Value:set(props.Instance:GetAttribute("_vfxEditorScale") or 1)
				end),
			})

			return inputField
		end,
		get = function(instance)
			return instance:GetAttribute("_vfxEditorScale") or 1
		end,
		set = function(instance: ParticleEmitter, value)
			-- this number should be at least 0.001
			local number = tonumber(value)

			if not number then
				return false
			end

			if number < 0.001 then
				number = 0.001
			end

			local oldScale = instance:GetAttribute("_vfxEditorScale") or 1
			-- scale our instance relative to the old scale
			scaleUtils.scaleInstance(instance, number / oldScale)
			instance:SetAttribute("_vfxEditorScale", number)
			return
		end,
	},
	{
		name = "Appearance",
		properties = {
			{
				name = "Color",
				groupIdentifier = "colorSequence",
				render = colorSequence,
			},
			{
				name = "FaceCamera",
				groupIdentifier = "checkbox",
				render = checkbox,
			},
			{
				name = "LightEmission",
				groupIdentifier = "slider",
				render = function(props)
					return slider({
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						Min = 0,
						Max = 1,
						Step = 0.01,
						Instance = props.Instance,
						PropertyName = props.PropertyName,
					})
				end,
			},
			{
				name = "LightInfluence",
				groupIdentifier = "slider",
				render = function(props)
					return slider({
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						Min = 0,
						Max = 1,
						Step = 0.01,
						Instance = props.Instance,
						PropertyName = props.PropertyName,
					})
				end,
			},
			{
				name = "Texture",
				groupIdentifier = "texturePicker",
				render = texturePicker,
				get = function(instance: ParticleEmitter)
					return `{instance.Texture},None`
				end,
				set = function(instance: ParticleEmitter, value)
					local split = value:split(",")

					if #split ~= 2 then
						return false
					end

					instance.Texture = split[1]
					return
				end,
			},
			{
				name = "TextureLength",
				groupIdentifier = "slider",
				render = function(props)
					return slider({
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						Min = 0.1,
						Max = 5,
						Step = 0.01,
						Instance = props.Instance,
						PropertyName = props.PropertyName,
					})
				end,
			},
			{
				name = "TextureMode",
				groupIdentifier = "textureModeenumDropdown",
				render = enumDropdown,
			},
			{
				name = "Transparency",
				groupIdentifier = "transparencyNumberSequence",
				render = function(props, useColor)
					return numberSequence({
						Instance = props.Instance,
						PropertyName = props.PropertyName,
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						SupportsEnvelope = false,
						Min = 0,
						Max = 1,
					}, useColor)
				end,
				clipboardGet = function(props)
					return props.Instance:GetAttribute("_vfxEditorGraph" .. props.PropertyName:split(" ")[1])
				end,
				clipboardSet = function(props, value)
					props.Instance:SetAttribute("_vfxEditorGraph" .. props.PropertyName:split(" ")[1], value)
				end,
			},
		},
	},
	{
		name = "Data",
		properties = {
			{
				name = "Attachment0",
				groupIdentifier = "object",
				render = function(props, useColor)
					return object({
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						Filter = function(instance)
							return instance:IsA("Attachment")
						end,
						Events = props.Events,
					}, useColor)
				end,
			},
			{
				name = "Attachment1",
				groupIdentifier = "object",
				render = function(props, useColor)
					return object({
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						Filter = function(instance)
							return instance:IsA("Attachment")
						end,
						Events = props.Events,
					}, useColor)
				end,
			},
		},
	},
	{
		name = "Emission",
		properties = {
			{
				name = "Enabled",
				groupIdentifier = "checkbox",
				render = checkbox,
			},
			{
				name = "Lifetime",
				groupIdentifier = "slider",
				render = function(props)
					return slider({
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						Min = 0,
						Max = 5,
						Step = 0.1,
						Instance = props.Instance,
						PropertyName = props.PropertyName,
					})
				end,
			},
			{
				name = "MaxLength",
				groupIdentifier = "numberInput",
				render = input,
				set = function(instance, value)
					if not tonumber(value) then
						return false
					end

					instance.MaxLength = tonumber(value)
					return
				end,
			},
			{
				name = "MinLength",
				groupIdentifier = "numberInput",
				render = input,
				set = function(instance, value)
					if not tonumber(value) then
						return false
					end

					instance.MinLength = tonumber(value)
					return
				end,
			},
			{
				name = "WidthScale",
				groupIdentifier = "slider",
				render = function(props, useColor)
					return numberSequence({
						Instance = props.Instance,
						PropertyName = props.PropertyName,
						Value = props.Value,
						LayoutOrder = props.LayoutOrder,
						SupportsEnvelope = false,
						Min = 0,
						Max = 1,
					}, useColor)
				end,
				clipboardGet = function(props)
					return props.Instance:GetAttribute("_vfxEditorGraph" .. props.PropertyName:split(" ")[1])
				end,
				clipboardSet = function(props, value)
					props.Instance:SetAttribute("_vfxEditorGraph" .. props.PropertyName:split(" ")[1], value)
				end,
			},
		},
	},
}

return class


local classDataHandler = {}

local fusion = require("@packages/fusion")

local theme = require("@src/theme")

local classMap: {
	[string]: {
		is: (instance: Instance) -> boolean,
		properties: {
			[number]: property | category,
		},
		emit: (instance: Instance) -> nil,
	},
} =
	nil :: any

local function setup()
	local classes = script.classes:GetChildren()
	classMap = {}

	for _, class in classes do
		classMap[class.Name] = require(class)
	end
end

function classDataHandler.getProperties(instance: Instance): ({ [number]: category }?, string?)
	if not classMap then
		setup()
	end

	for idx, class in classMap do
		if class.is(instance) then
			return class.properties, idx
		end
	end

	return nil, nil
end

function classDataHandler.emit(instance: Instance)
	if not classMap then
		setup()
	end

	for _, class in classMap do
		if class.is(instance) and class.emit then
			class.emit(instance)
			break
		end
	end
end

export type property = {
	name: string,
	groupIdentifier: string?,
	render: (
		{
			Instance: Instance,
			PropertyName: string,
			Value: fusion.Value<any>,
			Events: { [string]: BindableEvent },
		},
		useColor: theme.useColorFunction,
		properties: processedProperties
	) -> nil,
	get: (instance: Instance) -> any,
	set: (instance: Instance, value: any) -> nil,
	clipboardGet: ({
		Instance: Instance,
		PropertyName: string,
		Value: fusion.Value<any>,
	}) -> nil,
	clipboardSet: (
		{
			Instance: Instance,
			PropertyName: string,
			Value: fusion.Value<any>,
		},
		value: any
	) -> nil,
}

export type category = {
	name: string,
	properties: { [number]: property },
}

export type processedProperties = {
	[string]: fusion.Value<any>,
}

return classDataHandler


local ChangeHistoryService = game:GetService("ChangeHistoryService")

return function(identifier: string, callback: () -> any): any
	local recording = ChangeHistoryService:TryBeginRecording(identifier)

	-- we want to still run the callback even if we can't record the changes
	local success, result, _check = pcall(callback)

	if _check then
		warn(
			"More than 1 value was returned from a callback inside historyHandler. This is not allowed; refactor the callback to use a single return value."
		)
	end

	if recording then
		if success then
			ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
		else
			ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
			warn(`Failed to record changes for {identifier}: {result}`)
		end
	end

	return result
end


local scaleUtils = {}

function scaleUtils.scaleNumber(number: number, scale: number): number
	return number * scale
end

function scaleUtils.scaleNumberSequence(numberSequence: NumberSequence, scale: number): NumberSequence
	local newKeypoints = {}

	for _, keypoint in numberSequence.Keypoints do
		table.insert(
			newKeypoints,
			NumberSequenceKeypoint.new(
				keypoint.Time,
				scaleUtils.scaleNumber(keypoint.Value, scale),
				scaleUtils.scaleNumber(keypoint.Envelope, scale)
			)
		)
	end

	return NumberSequence.new(newKeypoints)
end

function scaleUtils.scaleInstance(instance: Instance | ParticleEmitter | Beam | Trail, scale: number): nil
	if instance:IsA("ParticleEmitter") then
		instance.Size = scaleUtils.scaleNumberSequence(instance.Size, scale)
	elseif instance:IsA("Beam") then
		instance.Width0 = scaleUtils.scaleNumber(instance.Width0, scale)
		instance.Width1 = scaleUtils.scaleNumber(instance.Width1, scale)
	elseif instance:IsA("Trail") then
		instance.WidthScale = scaleUtils.scaleNumberSequence(instance.WidthScale, scale)
	else
		for _, child in instance:GetChildren() do
			scaleUtils.scaleInstance(child, scale)
		end
	end

	return nil
end

return scaleUtils


-- primarily exists to wrap around Get/SetSetting so we don't have to get a new plugin each time
-- and also turns its values into reactive values
local settingsManager = {}

local fusion = require("@packages/fusion")
local Value = fusion.Value

local plugin = script:FindFirstAncestorWhichIsA("Plugin")

if __DEV__ then
	local PluginDebugService = game:GetService("PluginDebugService")
	plugin = PluginDebugService:FindFirstChild("plugin") :: Plugin?
end

local SETTING_PREFIX = "vfxEditor_"

settingsManager._localStorage = {}

function settingsManager.get<T>(key: string): T?
	local value = plugin:GetSetting(SETTING_PREFIX .. key)

	if settingsManager._localStorage[key] == nil and value ~= nil then
		settingsManager._localStorage[key] = Value(value)
	end

	return value
end

function settingsManager.getReactive<T>(key: string): fusion.Value<T>?
	local value = settingsManager.get(key)

	if value == nil and settingsManager._localStorage[key] == nil then
		return nil
	end

	return settingsManager._localStorage[key]
end

function settingsManager.set(key: string, value: any)
	plugin:SetSetting(SETTING_PREFIX .. key, value)

	if settingsManager._localStorage[key] then
		settingsManager._localStorage[key]:set(value, true)
	else
		settingsManager._localStorage[key] = Value(value)
	end
end

function settingsManager.getReactiveWithFallback(key: string, default: any)
	local value = settingsManager.getReactive(key)

	if value == nil then
		settingsManager.set(key, default)
		return settingsManager.getReactive(key)
	end

	return value
end

return settingsManager


local fusionComponents = require("@packages/fusionComponents")
local storyBase = fusionComponents.utility.storyBase

local fusion = require("@packages/fusion")
local Children = fusion.Children
local New = fusion.New
local Clean = fusion.cleanup

local Value = fusion.Value

local theme = require("@src/theme")
local historyHandler = require("@src/lib/historyHandler")

local generateInstance = require("./generateInstance")

local app = require("@src/components/app")

return {
	summary = "A full representation of the plugin",
	controls = {
		Height = 550,
		Width = 350,
	},
	story = function(prnt, props)
		if props.controls.Height == "" or props.controls.Width == "" then
			props.controls.Height = 550
			props.controls.Width = 350
		end

		return storyBase(function(parent)
			local backgroundColor = theme.global.isDark:get() and Color3.fromRGB(46, 46, 46)
				or Color3.fromRGB(255, 255, 255)

			local instances = Value({
				historyHandler("Create story instance", generateInstance),
			})

			local background = New("Frame")({
				BackgroundColor3 = backgroundColor,
				Parent = parent,
				Size = UDim2.new(1, 0, 1, 0),
				[Children] = {
					app({
						Items = instances,
					}),
				},
			})

			background.Parent = parent

			return function()
				background:Destroy()
				Clean(instances:get())
			end
		end)(prnt, props)
	end,
}


local fusionComponents = require("@packages/fusionComponents")
local storyBase = fusionComponents.utility.storyBase

local fusion = require("@packages/fusion")
local Children = fusion.Children
local New = fusion.New

local Value = fusion.Value

local appTopbar = require("@src/components/appTopbar")

return {
	summary = "The app topbar, filled with dummy info",
	controls = {
		Height = 32,
		Width = 350,
	},
	story = storyBase(function(parent)
		local part = Instance.new("Part")
		part.Parent = game:GetService("CoreGui")

		local function cloneWithParent(instance)
			local clone = instance:Clone()
			clone.Parent = instance.Parent
			return clone
		end

		local instances = Value({
			cloneWithParent(part),
			cloneWithParent(part),
			cloneWithParent(part),
			cloneWithParent(part),
		})

		local content = New("Frame")({
			BackgroundTransparency = 1,
			Parent = parent,
			Size = UDim2.new(1, 0, 1, 0),
			[Children] = {
				appTopbar({
					Items = instances,
					SelectedInstance = Value(instances:get()[1]),
					SharedContextMenuState = Value(false),
				}),
			},
		})

		content.Parent = parent

		return function()
			content:Destroy()

			for _, instance in instances:get() do
				instance:Destroy()
			end

			part:Destroy()
		end
	end),
}


local fusion = require("@packages/fusion")

local Value = fusion.Value

local theme = require("@src/theme")
local historyHandler = require("@src/lib/historyHandler")

local generateInstance = require("./generateInstance")

local app = require("@src/components/app")

return {
	summary = "A full representation of the plugin",
	controls = {
		Height = 550,
		Width = 350,
	},
	story = function(prnt)
		local generatedInstance = historyHandler("Create story instance", generateInstance)

		local instances = Value({
			generatedInstance,
		})

		local component = app({
			Items = instances,
		})

		component.Parent = prnt

		return function()
			component:Destroy()
			generatedInstance:Destroy()
			theme.destroy()
		end
	end,
}


local fusionComponents = require("@packages/fusionComponents")
local storyBase = fusionComponents.utility.storyBase

local fusion = require("@packages/fusion")
local Value = fusion.Value

local listOption = require("@components/listOption")
local checkbox = require("@components/propertyFields/checkbox")

local instance = Instance.new("Part")

local PROPERTY_NAME = "Anchored"

return {
	summary = "Checkbox property field",
	story = storyBase(function(parent)
		local component = listOption({
			Size = UDim2.new(0, 256, 0, 48),
			Position = UDim2.new(0, 0, 0, 0),
			OptionName = PROPERTY_NAME,
			Content = {
				checkbox({
					Instance = instance,
					PropertyName = PROPERTY_NAME,
					Value = Value(instance[PROPERTY_NAME]),
					LayoutOrder = 1,
				}),
			},
		})

		component.Parent = parent

		return function()
			component:Destroy()
		end
	end),
}


local fusionComponents = require("@packages/fusionComponents")
local storyBase = fusionComponents.utility.storyBase

local fusion = require("@packages/fusion")
local Value = fusion.Value

local listOption = require("@components/listOption")
local colorSequence = require("@components/propertyFields/colorSequence")

local theme = require("@src/theme")

local PROPERTY_NAME = "Color"

return {
	summary = "Color sequence property field",
	story = storyBase(function(parent)
		local instance = Instance.new("ParticleEmitter")
		instance.Color = ColorSequence.new({
			ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 0, 0)),
			ColorSequenceKeypoint.new(0.25, Color3.fromRGB(255, 255, 0)),
			ColorSequenceKeypoint.new(0.5, Color3.fromRGB(0, 255, 0)),
			ColorSequenceKeypoint.new(0.75, Color3.fromRGB(0, 255, 255)),
			ColorSequenceKeypoint.new(1, Color3.fromRGB(0, 0, 255)),
		})

		instance.Parent = workspace

		local component = listOption({
			Size = UDim2.new(0, 256, 0, 48),
			Position = UDim2.new(0, 0, 0, 0),
			OptionName = PROPERTY_NAME,
			Content = {
				colorSequence({
					Instance = instance,
					PropertyName = PROPERTY_NAME,
					Value = Value(instance[PROPERTY_NAME]),
					LayoutOrder = 1,
				}, theme:get("InstanceTreeItem", "gray", "default", "Base")),
			},
		})

		component.Parent = parent

		return function()
			component:Destroy()
			instance:Destroy()
		end
	end),
}


local fusionComponents = require("@packages/fusionComponents")
local storyBase = fusionComponents.utility.storyBase

local fusion = require("@packages/fusion")
local Children = fusion.Children
local New = fusion.New

local Value = fusion.Value

local colorSequenceEditor = require("@components/propertyFields/colorSequence/editor")

local theme = require("@src/theme")

local PROPERTY_NAME = "Color"

return {
	summary = "Color sequence widget",
	story = storyBase(function(parent)
		local instance = Instance.new("ParticleEmitter")
		instance.Color = ColorSequence.new({
			ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 0, 0)),
			ColorSequenceKeypoint.new(0.25, Color3.fromRGB(255, 255, 0)),
			ColorSequenceKeypoint.new(0.5, Color3.fromRGB(0, 255, 0)),
			ColorSequenceKeypoint.new(0.75, Color3.fromRGB(0, 255, 255)),
			ColorSequenceKeypoint.new(1, Color3.fromRGB(0, 0, 255)),
		})

		instance.Parent = workspace

		local component = New("Frame")({
			Name = "ColorSequenceWidget",
			BackgroundTransparency = 1,
			Size = UDim2.new(0, 400, 0, 275),
			[Children] = {
				colorSequenceEditor({
					Value = Value(instance[PROPERTY_NAME]),
					useColor = theme:get("InstanceTreeItem", "gray", "default", "Base"),
				}),
			},
		})

		component.Parent = parent

		return function()
			component:Destroy()
			instance:Destroy()
		end
	end),
}


local fusionComponents = require("@packages/fusionComponents")
local storyBase = fusionComponents.utility.storyBase

local fusion = require("@packages/fusion")
local Value = fusion.Value

local listOption = require("@components/listOption")
local dropdown = require("@components/propertyFields/enumDropdown")

local instance = Instance.new("Part")

local PROPERTY_NAME = "Shape"

return {
	summary = "Dropdown property field",
	story = storyBase(function(parent)
		local component = listOption({
			Size = UDim2.new(0, 256, 0, 48),
			Position = UDim2.new(0, 0, 0, 0),
			OptionName = PROPERTY_NAME,
			Content = {
				dropdown({
					Instance = instance,
					PropertyName = PROPERTY_NAME,
					Value = Value(instance[PROPERTY_NAME]),
					LayoutOrder = 1,
				}),
			},
		})

		component.Parent = parent

		return function()
			component:Destroy()
		end
	end),
}


local fusionComponents = require("@packages/fusionComponents")
local storyBase = fusionComponents.utility.storyBase

local fusion = require("@packages/fusion")
local Value = fusion.Value

local listOption = require("@components/listOption")
local input = require("@components/propertyFields/input")

local instance = Instance.new("Part")

local PROPERTY_NAME = "Name"

return {
	summary = "Input property field",
	story = storyBase(function(parent)
		local component = listOption({
			Size = UDim2.new(0, 256, 0, 48),
			Position = UDim2.new(0, 0, 0, 0),
			OptionName = PROPERTY_NAME,
			Content = {
				input({
					Instance = instance,
					PropertyName = PROPERTY_NAME,
					Value = Value(instance[PROPERTY_NAME]),
					LayoutOrder = 1,
				}),
			},
		})

		component.Parent = parent

		return function()
			component:Destroy()
		end
	end),
}


local fusionComponents = require("@packages/fusionComponents")
local storyBase = fusionComponents.utility.storyBase

local fusion = require("@packages/fusion")
local Value = fusion.Value

local listOption = require("@components/listOption")
local numberSequence = require("@components/propertyFields/numberSequence")

local theme = require("@src/theme")

local PROPERTY_NAME = "Transparency"

return {
	summary = "Number sequence property field",
	story = storyBase(function(parent)
		local instance = Instance.new("ParticleEmitter")
		instance.Transparency = NumberSequence.new(0, 1)

		instance.Parent = workspace

		local component = listOption({
			Size = UDim2.new(0, 256, 0, 48),
			Position = UDim2.new(0, 0, 0, 0),
			OptionName = PROPERTY_NAME,
			Content = {
				numberSequence({
					Instance = instance,
					PropertyName = PROPERTY_NAME,
					Value = Value(instance[PROPERTY_NAME]),
					LayoutOrder = 1,
					Max = 1,
					Min = 0,
					SupportsEnvelope = true,
				}, theme:get("InstanceTreeItem", "gray", "default", "Base")),
			},
		})

		component.Parent = parent

		return function()
			component:Destroy()
			instance:Destroy()
		end
	end),
}


local fusionComponents = require("@packages/fusionComponents")
local storyBase = fusionComponents.utility.storyBase

local fusion = require("@packages/fusion")
local Value = fusion.Value

local numberSequenceWidget = require("@components/propertyFields/numberSequence/editor")

local theme = require("@src/theme")

local generateInstance = require("@src/stories/generateInstance")

local PROPERTY_NAME = "Transparency"

return {
	summary = "Number sequence property field",
	story = storyBase(function(parent)
		local instance = generateInstance().Particles.Drops

		local component = numberSequenceWidget({
			Value = Value(instance[PROPERTY_NAME]),
			useColor = theme:get("InstanceTreeItem", "gray", "default", "Base"),
			Instance = instance,
			PropertyName = PROPERTY_NAME,
			Max = 1,
			Min = 0,
			SupportsEnvelope = true,
		})

		component.Parent = parent

		return function()
			component:Destroy()
			instance.Parent.Parent:Destroy()
		end
	end),
}


local fusionComponents = require("@packages/fusionComponents")
local storyBase = fusionComponents.utility.storyBase

local fusion = require("@packages/fusion")
local Value = fusion.Value

local listOption = require("@components/listOption")
local slider = require("@components/propertyFields/slider")

local instance = Instance.new("Part")

local PROPERTY_NAME = "Transparency"

return {
	summary = "Slider property field",
	story = storyBase(function(parent)
		local component = listOption({
			Size = UDim2.new(0, 256, 0, 48),
			Position = UDim2.new(0, 0, 0, 0),
			OptionName = PROPERTY_NAME,
			Content = {
				slider({
					Instance = instance,
					PropertyName = PROPERTY_NAME,
					Value = Value(instance[PROPERTY_NAME]),
					LayoutOrder = 1,
					Min = 0,
					Max = 1,
					Step = 0.02,
				}),
			},
		})

		component.Parent = parent

		return function()
			component:Destroy()
		end
	end),
}


local fusionComponents = require("@packages/fusionComponents")
local storyBase = fusionComponents.utility.storyBase

local fusion = require("@packages/fusion")
local Value = fusion.Value

local listOption = require("@components/listOption")
local texturePicker = require("@components/propertyFields/texturePicker")

local PROPERTY_NAME = "Texture"

return {
	summary = "Texture property field",
	story = storyBase(function(parent)
		local instance = Instance.new("ParticleEmitter")
		instance.Texture = "rbxassetid://0"

		instance.Parent = workspace

		local component = listOption({
			Size = UDim2.new(0, 256, 0, 48),
			Position = UDim2.new(0, 0, 0, 0),
			OptionName = PROPERTY_NAME,
			Content = {
				texturePicker({
					Instance = instance,
					PropertyName = PROPERTY_NAME,
					Value = Value(instance[PROPERTY_NAME]),
					LayoutOrder = 1,
				}),
			},
		})

		component.Parent = parent

		return function()
			component:Destroy()
			instance:Destroy()
		end
	end),
}


local fusionComponents = require("@packages/fusionComponents")
local storyBase = fusionComponents.utility.storyBase

local fusion = require("@packages/fusion")
local Value = fusion.Value

local texturePickerWidget = require("@components/propertyFields/texturePicker/editor")

local generateInstance = require("@src/stories/generateInstance")
local theme = require("@src/theme")

local PROPERTY_NAME = "Texture"

return {
	summary = "Number sequence property field",
	story = storyBase(function(parent)
		local instance = generateInstance().Particles.Drops

		local component = texturePickerWidget({
			Value = Value(instance[PROPERTY_NAME]),
			Instance = instance,
			Path = Value(""),
			useColor = theme:get("InstanceTreeItem", "gray", "default", "Base"),
			IsSelectingImage = false,
		})

		component.Parent = parent

		return function()
			component:Destroy()
			instance.Parent.Parent:Destroy()
		end
	end),
}


return function()
	local part = Instance.new("Part")
	part.TopSurface = Enum.SurfaceType.Smooth
	part.Anchored = true
	part.Transparency = 1
	part.CFrame = CFrame.new(0, 2, 0)
	part.Size = Vector3.new(1, 1, 1)
	part.CanCollide = false
	part.BrickColor = BrickColor.new("Institutional white")

	local attachment = Instance.new("Attachment")
	attachment.Parent = part

	local emitter = Instance.new("ParticleEmitter")
	emitter.Lifetime = NumberRange.new(0.25, 0.25)
	emitter.Color = ColorSequence.new({
		ColorSequenceKeypoint.new(0.00, Color3.new(1.00, 0.85, 0.45)),
		ColorSequenceKeypoint.new(1.00, Color3.new(1.00, 0.85, 0.45)),
	})
	emitter.LightEmission = 1
	emitter.Squash = NumberSequence.new({
		NumberSequenceKeypoint.new(0.00, 0.50, 0.25),
		NumberSequenceKeypoint.new(1.00, 0.00, 0.00),
	})
	emitter.Speed = NumberRange.new(0.00, 0.00)
	emitter.Texture = "rbxassetid://6865222957"
	emitter.Rotation = NumberRange.new(-36.00, 360.00)
	emitter.Rate = 25
	emitter.Transparency = NumberSequence.new({
		NumberSequenceKeypoint.new(0.00, 1.00, 0.00),
		NumberSequenceKeypoint.new(0.50, 0.00, 0.00),
		NumberSequenceKeypoint.new(1.00, 1.00, 0.00),
	})
	emitter.Size = NumberSequence.new({
		NumberSequenceKeypoint.new(0.00, 2.00, 0.00),
		NumberSequenceKeypoint.new(1.00, 0.00, 0.00),
	})
	emitter.Parent = attachment

	part.Parent = workspace
	part.Anchored = true
	return part
end


local tailwind = require("@packages/tailwind")

return function(themeName: string)
	return {
		Base = {
			TopbarBackground = themeName == "Dark" and tailwind.neutral[800] or tailwind.neutral[50],
			TreeBackground = {
				color = themeName == "Dark" and tailwind.neutral[800] or tailwind.neutral[50],
				transparency = 0,
				shadow = 4,
			},
			Text = {
				color = themeName == "Dark" and tailwind.neutral[100] or tailwind.neutral[900],
			},
			SecondaryText = {
				color = themeName == "Dark" and tailwind.neutral[400] or tailwind.neutral[600],
			},
			Stroke = themeName == "Dark" and tailwind.neutral[700] or tailwind.neutral[300],
		},
	}
end


local tailwind = require("@packages/tailwind")

return function(themeName: string)
	return {
		Base = {
			Text = themeName == "Dark" and tailwind.neutral[100] or tailwind.neutral[900],
		},
		Hover = {
			Background = themeName == "Dark" and tailwind.neutral[750] or tailwind.neutral[200],
			Text = themeName == "Dark" and tailwind.neutral[100] or tailwind.neutral[900],
		},
		Pressing = {
			Background = themeName == "Dark" and tailwind.neutral[700] or tailwind.neutral[300],
			Text = themeName == "Dark" and tailwind.neutral[100] or tailwind.neutral[900],
		},
		Selected = {
			Background = themeName == "Dark" and Color3.fromRGB(46, 46, 46) or Color3.fromRGB(255, 255, 255),
			Text = themeName == "Dark" and tailwind.neutral[100] or tailwind.neutral[900],
			Stroke = themeName == "Dark" and tailwind.neutral[700] or tailwind.neutral[300],
		},
	}
end


local tailwind = require("@packages/tailwind")

return function(themeName: string)
	return {
		Base = {
			Text = themeName == "Dark" and tailwind.neutral[100] or tailwind.neutral[900],
			SecondaryText = themeName == "Dark" and tailwind.neutral[500] or tailwind.neutral[500],
			ToggleButton = themeName == "Dark" and tailwind.neutral[400] or tailwind.neutral[500],
			Line = themeName == "Dark" and tailwind.neutral[750] or tailwind.neutral[200],
			Background = themeName == "Dark" and tailwind.neutral[700] or tailwind.neutral[200],
		},
	}
end


local tailwind = require("@packages/tailwind")

return function(themeName: string)
	return {
		Base = {
			Title = themeName == "Dark" and tailwind.neutral[100] or tailwind.neutral[900],
			Description = themeName == "Dark" and tailwind.neutral[200] or tailwind.neutral[700],
			Line = themeName == "Dark" and tailwind.neutral[750] or tailwind.neutral[200],
			Dot = themeName == "Dark" and tailwind.neutral[400] or tailwind.neutral[700],
			SelectedDot = themeName == "Dark" and tailwind.red[400] or tailwind.red[500],
		},
	}
end


local tailwind = require("@packages/tailwind")

return function(themeName: string)
	return {
		Base = {
			ScrollBar = themeName == "Dark" and tailwind.neutral[700] or tailwind.neutral[300],
			ScrollBarBackground = themeName == "Dark" and tailwind.neutral[785] or tailwind.neutral[50],
			Stroke = themeName == "Dark" and tailwind.neutral[700] or tailwind.neutral[300],
		},
	}
end


local tailwind = require("@packages/tailwind")

return function(themeName: string)
	return {
		Base = {
			Title = themeName == "Dark" and tailwind.neutral[100] or tailwind.neutral[900],
			Description = themeName == "Dark" and tailwind.neutral[200] or tailwind.neutral[700],
			Line = themeName == "Dark" and tailwind.neutral[750] or tailwind.neutral[200],
			SidebarBackground = {
				color = themeName == "Dark" and tailwind.neutral[800] or tailwind.white,
				transparency = 0,
				shadow = 2,
			},
			LighterBackground = {
				color = themeName == "Dark" and tailwind.neutral[825] or tailwind.white,
				transparency = 0,
				shadow = 2,
			},
		},
	}
end


local StudioService = game:GetService("StudioService")
local Studio = settings().Studio

local themeFramework = require("@packages/themeFramework")
local fusion = require("@packages/fusion")
local Clean = fusion.cleanup
local Computed = fusion.Computed
local Value = fusion.Value

export type color = themeFramework.color
export type colorTable = themeFramework.colorTable
export type useColorFunction = themeFramework.useColorFunction

local globals = {
	font = Font.fromEnum(Enum.Font.SourceSans),
	isDark = Value(false),
	background = Value(Color3.fromRGB(250, 250, 250)),
}

local theme = themeFramework.new(script.components, function(self, themeName)
	local isDark = themeName == "Dark"
	local background = isDark and Color3.fromRGB(46, 46, 46) or Color3.fromRGB(250, 250, 250) -- roblox default backgrounds

	globals.isDark:set(isDark)
	globals.background:set(background)

	self:setFallback(background)
end)

local currentTheme = Value(Studio.Theme.Name)

local connections = {
	Studio.ThemeChanged:Connect(function()
		if theme == nil then
			return
		end

		theme:load(theme:build(Studio.Theme.Name))
		currentTheme:set(Studio.Theme.Name)
	end),
	script.Destroying:Connect(function()
		theme.destroy()
	end),
}

theme:load(theme:build(Studio.Theme.Name))

local wasDestroyed = false

return setmetatable({
	global = globals,
	getClassIcon = function(className)
		return Computed(function()
			currentTheme:get() -- register dependency so it updates when the theme changes. the overhead isn't significant enough to explore other solutions
			return StudioService:GetClassIcon(className)
		end)
	end,
	destroy = function()
		if not wasDestroyed then
			wasDestroyed = true
		end

		Clean(connections)
		theme = nil
	end,
}, {
	__index = theme,
}) :: themeFramework.themeFramework & {
	global: {
		font: Font,
		isDark: fusion.Value<boolean>,
		background: fusion.Value<Color3>,
	},
	destroy: () -> (),
	getClassIcon: (
		className: string
	) -> fusion.Computed<{
		Image: string,
		ImageRectSize: Vector2,
		ImageRectOffset: Vector2,
	}>,
}


--[[
	A shout-out to @Mephistopheles for
	their implementation of the replication buffer.
]]

local Players = game:GetService('Players')
local ReplicatedStorage = game:GetService('ReplicatedStorage')
local RunService = game:GetService('RunService')
local BetterReplication = script.Parent.Parent

local localPlayer = Players.LocalPlayer

local GetRegistry = BetterReplication.Remotes.GetRegistry
local PositionCache = require(BetterReplication.Client.PositionCache)
local Config = require(BetterReplication.Config)
local RunServiceUtils = require(BetterReplication.Lib.Utils)
local Snapshots = require(BetterReplication.Lib.Snapshots)
local BufferUtils = require(BetterReplication.Lib.BufferUtils)

local FromClient = BetterReplication.Remotes.FromClient
local ToClient = BetterReplication.Remotes.ToClient
local RegisterIdentifier = BetterReplication.Remotes.RegisterIdentifier
local OutOfProximity = BetterReplication.Remotes.OutOfProximity

local runserviceConnection: RBXScriptConnection
local registeredIdentifiers = {} :: {[number]: Player}
local inProximity = {} :: {[Player]: boolean}
local renderCache = {} :: {[Player]: {
	renderAt: number,
	lastClockAt: number,
	lastClockDuration: number
}}

local readToClient = BufferUtils.readToClientSimplified
if Config.makeRagdollFriendly then
	readToClient = BufferUtils.readToClient
end

local function handleIdentifierRegistry(b: buffer)
	local data = BufferUtils.readRegisterIdentifier(b)

	local player: Player = Players:FindFirstChild(data.p)
	registeredIdentifiers[data.id] = player
end

-- associate identifer with player object and push the new cframe
local function handleReplication(b: buffer)
	local data = readToClient(b)

	local player = registeredIdentifiers[data.p]
	local renderCacheEntry = renderCache[player]

	local currentClock = data.t
	if currentClock > renderCacheEntry.lastClockAt then
		renderCache[player].lastClockDuration = os.clock()
		renderCache[player].lastClockAt = currentClock
	else
		return
	end

	if player and player.Character then
		if not renderCacheEntry.renderAt then
			renderCache[player].renderAt = currentClock - Config.interpolationDelay
		end
		inProximity[player] = true

		local snapshots = Snapshots.getSnapshotInstance(player)
		snapshots:pushAt(currentClock, data.c)
		PositionCache[player] = data.c
	end
end

local function handleProximities(b: buffer)
	local data = BufferUtils.readOutOfProximityArray(b)

	for _, identifier: number in data do
		local player = registeredIdentifiers[identifier]
		if player then
			inProximity[player] = false
		end
	end
end

local function interpolate(dt)
	local stagedPlayers = {}
	local stagedResults = {}

	for player, isIn in inProximity do
		if not isIn or player == localPlayer then
			continue
		end

		local renderCacheEntry = renderCache[player]

		local estimatedServerTime = renderCacheEntry.lastClockAt + (os.clock() - renderCacheEntry.lastClockDuration)

		local clientRenderAt = renderCacheEntry.renderAt
		clientRenderAt += dt

		local renderTimeError = Config.interpolationDelay - (estimatedServerTime - clientRenderAt)
		if math.abs(renderTimeError) > .1 then
			clientRenderAt = estimatedServerTime - Config.interpolationDelay
		elseif renderTimeError > .01 then
			clientRenderAt = math.max(estimatedServerTime - Config.interpolationDelay, clientRenderAt - .1 * dt)
		elseif renderTimeError < -.01 then
			clientRenderAt = math.min(estimatedServerTime - Config.interpolationDelay, clientRenderAt + .1 * dt)
		end

		renderCache[player].renderAt = clientRenderAt
		local snapshot = Snapshots.getSnapshotInstance(player)
		local res = snapshot:getAt(clientRenderAt)

		if res then
			table.insert(
				stagedPlayers,
				player.Character.HumanoidRootPart
			)
			table.insert(
				stagedResults,
				res
			)
		end
	end
	workspace:BulkMoveTo(stagedPlayers, stagedResults, Enum.BulkMoveMode.FireAllEvents)
end

local function setUp(player: Player)
	inProximity[player] = false
	renderCache[player] = {
		renderAt = nil,
		lastClockAt = 0,
		lastClockDuration = 0
	}
	Snapshots.registerPlayer(player)
end

local module = {}

local started = false
function module.start()
	if started then warn(script:GetFullName(),"| BetterReplication already started!") return end

	Players.PlayerAdded:Connect(function(player: Player)
		setUp(player)
	end)
	for _, player in Players:GetPlayers() do
		setUp(player)
	end
	Players.PlayerRemoving:Connect(function(player: Player)
		inProximity[player] = nil
		renderCache[player] = nil

		Snapshots.deregisterPlayer(player)

		for id, other in registeredIdentifiers do
			if other == player then
				registeredIdentifiers[id] = nil
				break
			end
		end
	end)

	RegisterIdentifier.OnClientEvent:Connect(handleIdentifierRegistry)
	registeredIdentifiers = GetRegistry:InvokeServer()

	OutOfProximity.OnClientEvent:Connect(handleProximities)
	ToClient.OnClientEvent:Connect(handleReplication)
	runserviceConnection = RunService.PreSimulation:Connect(interpolate)

	started = true
end

function module.toggle(v: boolean)
	if not started then
		error(script:GetFullName(),"| Start BetterReplication first using the .start() method!")
	end
	if v then
		if not runserviceConnection then
			runserviceConnection = RunService.PreSimulation:Connect(interpolate)
		else
			warn(script:GetFullName(),"| A BetterReplication connection is already active.")
		end
	else
		if runserviceConnection then
			runserviceConnection:Disconnect()
			runserviceConnection = nil
		else
			warn(script:GetFullName(),"| There is no BetterReplication connection active.")
		end
	end
end

function module.continue()
	module.toggle(true)
end

function module.pause()
	module.toggle(false)
end

return module


-- A client-sided utility that you can use to access non-interpolated (!!!) player positions

return {} :: {[Player]: CFrame}


local Players = game:GetService('Players')
local BetterReplication = script.Parent.Parent

local Config = require(BetterReplication.Config)
local BufferUtils = require(BetterReplication.Lib.BufferUtils)
local Utils = require(BetterReplication.Lib.Utils)

local FromClient = BetterReplication.Remotes.FromClient

local player = Players.LocalPlayer
local character: Model = nil
local rootpart: Part = nil

local lastCframe = CFrame.new(9999, 9999, 9999)
local angleThreshold = math.rad(0.5)
local positionThreshold = 0.01

local writeFromClient = BufferUtils.writeFromClientSimplified
if Config.makeRagdollFriendly then
	writeFromClient = BufferUtils.writeFromClient
end
local optimize = Config.optimizeInactivity

local function hasSignificantChange(cframe1, cframe2)
	local positionDelta = (cframe1.Position - cframe2.Position).Magnitude
	if positionDelta > positionThreshold then
		return true
	end

	local x1, y1, z1 = cframe1:ToOrientation()
	local x2, y2, z2 = cframe2:ToOrientation()
	local angleDelta = math.abs(x1 - x2) + math.abs(y1 - y2) + math.abs(z1 - z2)

	return angleDelta > angleThreshold
end

local function update()
	if not rootpart then return end
	local rootCframe = rootpart.CFrame

	if not optimize or hasSignificantChange(lastCframe, rootCframe) then
		lastCframe = rootCframe

		FromClient:FireServer(writeFromClient(os.clock(), rootCframe))
	end
end

local module = {}
function module.start()
	if not player.Character then
		error("No existing player.Character, can not start the replication handler.")
	end

	player.CharacterAdded:Connect(function(char)
		character = char

		rootpart = nil
		rootpart = char:WaitForChild("HumanoidRootPart")
	end)
	character = player.Character
	rootpart = character:WaitForChild("HumanoidRootPart")

	update()
	Utils.FrequencyPostSimulation(update, 20)
end

function module.setCharacter(char)
	character = char
	rootpart = char.HumanoidRootPart
end

return module


local Config = {
	-- not recommended to change these values at runtime

	-- from how many studs should players not be replicated with BetterReplication anymore?
	proximityThreshold = 100, -- default: 100

	-- ship the whole cframe orientation instead of only the yaw axis to make the humanoidrootpart interpolate accurately
	-- this setting is only relevant for when the humanoidrootpart doesnt just rotate around its yaw axis (f.e. ragdolls)
	makeRagdollFriendly = false, -- default: false

	-- stops the client from sending position data when they do not move
	optimizeInactivity = true, -- default: true

	-----------------------

	-- emulate packet loss to see how BetterReplication performs
	-- -1 = disabled, <0,1> = a rate (fe.e .5 = 50%)
	packetLossRate = -1,

	-- dont touch this part of the config
	-- unless you know what youre doing! :D
	tickRate = 1/20,
	interpolationDelay = 1/10
}

return Config


local BetterReplication = require(game.ReplicatedStorage.BetterReplication.Interface)

BetterReplication.start()
BetterReplication.bindSanityCheck(function(data)
	print(data.t, data.c)
	return true
end)


local BetterReplication = require(game.ReplicatedStorage.BetterReplication.Interface)
BetterReplication.startReplication()


local BetterReplication = require(game.ReplicatedStorage.BetterReplication.Interface)
BetterReplication.startInterpolater()


type from_client_packet = {
	t: number,
	c: CFrame
}
type to_client_packet = {
	t: number,
	p: number,
	c: CFrame
}
type register_identifier = {
	p: string,
	id: number
}

local writef16, readf16

do
	local EXPONENT_BIAS = 14

	local SIGN_OFFSET = 2 ^ 15
	local EXPONENT_OFFSET = 2 ^ 10
	local MANTISSA_OFFSET = 2 ^ 10

	local MANTISSA_RANGE = 2 ^ 11
	local SUBNORMAL_RANGE = MANTISSA_RANGE // 2

	-- https://en.wikipedia.org/wiki/Half-precision_floating-point_format#Precision_limitations
	local INTERVAL_BIAS = 10

	local FLOAT16_MAX = 0b1111111_111100000

	function writef16(b: buffer, offset: number, value: number)
		if value > FLOAT16_MAX then
			buffer.writeu16(b, offset, 0b0_11111_0000000000)
		elseif value < -FLOAT16_MAX then
			buffer.writeu16(b, offset, 0b1_11111_0000000000)
		elseif value ~= value then
			buffer.writeu16(b, offset, 0b1_11111_0000000001)
		elseif value == 0 then
			buffer.writeu16(b, offset, 0)
		else
			local absoluteValue = math.abs(value)

			local interval = 2 ^ ( math.floor(math.log(absoluteValue, 2)) - INTERVAL_BIAS )
			local representableValue = math.floor(absoluteValue / interval) * interval

			local fraction, unbiasedExponent = math.frexp(representableValue)
			local exponent = unbiasedExponent + EXPONENT_BIAS

			local mantissa = if exponent <= 0
				then fraction * SUBNORMAL_RANGE / 2 ^ math.abs(exponent)
				else fraction * MANTISSA_RANGE

			local floatRepresentation =
				  math.round(mantissa) % MANTISSA_OFFSET
				+ math.max(exponent, 0) * EXPONENT_OFFSET
				+ if value < 0 then SIGN_OFFSET else 0

			buffer.writeu16(b, offset, floatRepresentation)
		end
	end

	function readf16(b: buffer, offset: number): number
		local uintForm = buffer.readu16(b, offset)

		local isSigned = uintForm // SIGN_OFFSET == 1
		local signlessRepresentation = uintForm % SIGN_OFFSET

		if signlessRepresentation == 0b11111_0000000000 then
			return if isSigned then -math.huge else math.huge
		elseif signlessRepresentation == 0b11111_0000000001 then
			return 0 / 0
		elseif signlessRepresentation == 0b00000_0000000000 then
			return 0
		else
			local mantissa = signlessRepresentation % MANTISSA_OFFSET
			local exponent = signlessRepresentation // MANTISSA_OFFSET

			local fraction = if exponent == 0
				then mantissa / SUBNORMAL_RANGE
				else mantissa / MANTISSA_RANGE + 0.5

			local value = math.ldexp(fraction, exponent - EXPONENT_BIAS)
			return if isSigned then -value else value
		end
	end
end

local function writei24(b: buffer, offset: number, value: number)
	local uint = (value + 0x1000000) % 0x1000000
	buffer.writeu8(b, offset, uint)
	buffer.writeu16(b, offset + 1, uint // 0x100)
end

local function readi24(b: buffer, offset: number): number
	local uint = buffer.readu8(b, offset) + buffer.readu16(b, offset + 1) * 0x100
	return (uint + 0x800000) % 0x1000000 - 0x800000
end

local function writeCFrame(b: buffer, cframe: CFrame): buffer
	local oldBlen = buffer.len(b)

	local newB = buffer.create(oldBlen + 15)
	buffer.copy(newB, 0, b)

	local x, y, z = cframe.X, cframe.Y, cframe.Z

	writei24(newB, oldBlen + 0, x*128)
	writei24(newB, oldBlen + 3, y*128)
	writei24(newB, oldBlen + 6, z*128)

	local axis, angle = cframe:ToAxisAngle()
	axis *= angle

	writef16(newB, oldBlen + 9, axis.X)
	writef16(newB, oldBlen + 11, axis.Y)
	writef16(newB, oldBlen + 13, axis.Z)

	return newB
end

local function readCFrame(b: buffer, cursor: number): CFrame
	local position = Vector3.new(
		readi24(b, cursor + 0)/128,
		readi24(b, cursor + 3)/128,
		readi24(b, cursor + 6)/128
	)

	local axisAngle = Vector3.new(
		readf16(b, cursor + 9),
		readf16(b, cursor + 11),
		readf16(b, cursor + 13)
	)
	local angle = axisAngle.Magnitude

	return if angle ~= 0
		then CFrame.fromAxisAngle(axisAngle, angle) + position
		else CFrame.new(position)
end

local function writeSimpleCFrame(b: buffer, cframe: CFrame): buffer
	local oldBlen = buffer.len(b)

	local newB = buffer.create(oldBlen + 11)
	buffer.copy(newB, 0, b)

	local x, y, z = cframe.X, cframe.Y, cframe.Z
	local _, ry, _ = cframe:ToEulerAnglesYXZ()

	writei24(newB, oldBlen + 0, x*128)
	writei24(newB, oldBlen + 3, y*128)
	writei24(newB, oldBlen + 6, z*128)
	writef16(newB, oldBlen + 9, ry)

	return newB
end

local function readSimpleCFrame(b: buffer, cursor: number): CFrame
	local x = readi24(b, cursor + 0)/128
	local y = readi24(b, cursor + 3)/128
	local z = readi24(b, cursor + 6)/128

	local ry = readf16(b, cursor + 9)

	return CFrame.new(x, y, z) * CFrame.Angles(0, ry, 0)
end

local function writeOutOfProximityArray(arr: {number}): buffer
	local b = buffer.create(#arr)

	for i = 0, #arr-1 do
		buffer.writeu8(b, i, arr[i+1])
	end
	return b
end

local function readOutOfProximityArray(b:  buffer): {number}
	local itrs = buffer.len(b)-1

	local arr = {}
	for i = 0, itrs do
		table.insert(
			arr,
			buffer.readu8(b, i)
		)
	end
	return arr
end

local function readFromClient(b: buffer): from_client_packet
	local t = buffer.readf32(b, 0)
	local c = readCFrame(b, 4)

	return {t = t, c = c}
end

local function readFromClientSimplified(b: buffer): from_client_packet
	local t = buffer.readf32(b, 0)
	local c = readSimpleCFrame(b, 4)

	return {t = t, c = c}
end

local function writeFromClientSimplified(t: number, cframe: CFrame): buffer
	local b = buffer.create(4)
	buffer.writef32(b, 0, t)

	return writeSimpleCFrame(b, cframe)
end

local function writeFromClient(t: number, cframe: CFrame): buffer
	local b = buffer.create(4)
	buffer.writef32(b, 0, t)

	return writeCFrame(b, cframe)
end

local function readToClientSimplified(b: buffer): to_client_packet
	local t = buffer.readf32(b, 0)
	local p = buffer.readu8(b, 4)
	local c = readSimpleCFrame(b, 5)

	return {t = t, p = p, c = c}
end

local function writeToClientSimplified(t: number, p: number, cframe: CFrame): buffer
	local b = buffer.create(4 + 1)
	buffer.writef32(b, 0, t)
	buffer.writeu8(b, 4, p)

	return writeSimpleCFrame(b, cframe)
end

local function readToClient(b: buffer): to_client_packet
	local t = buffer.readf32(b, 0)
	local p = buffer.readu8(b, 4)
	local c = readCFrame(b, 5)

	return {t = t, p = p, c = c}
end

local function writeToClient(t: number, p: number, cframe: CFrame): buffer
	local b = buffer.create(4 + 1)
	buffer.writef32(b, 0, t)
	buffer.writeu8(b, 4, p)

	return writeCFrame(b, cframe)
end

local function readRegisterIdentifier(b: buffer): register_identifier
	local id = buffer.readu8(b, 0)
	local p = buffer.readstring(b, 1, buffer.len(b) - 1)

	return {p = p, id = id}
end

local function writeRegisterIdentifier(p: string, id: number): buffer
	local pLen = string.len(p)
	local b = buffer.create(1 + pLen)

	buffer.writeu8(b, 0, id)
	buffer.writestring(b, 1, p)

	return b
end

return {
	writeToClient = writeToClient,
	writeToClientSimplified = writeToClientSimplified,
	readToClient = readToClient,
	readToClientSimplified = readToClientSimplified,

	writeFromClient = writeFromClient,
	writeFromClientSimplified = writeFromClientSimplified,
	readFromClient = readFromClient,
	readFromClientSimplified = readFromClientSimplified,

	readRegisterIdentifier = readRegisterIdentifier,
	writeRegisterIdentifier = writeRegisterIdentifier,

	writeOutOfProximityArray = writeOutOfProximityArray,
	readOutOfProximityArray = readOutOfProximityArray,
}


-- Implementation by Pogo. Used for the replication buffer implementation.
--!strict
local clock = os.clock

local Snapshot = {}
Snapshot.__index = Snapshot

type datetime = number
export type Snapshot = {
	-- fields
	_subject: Player?,
	_cache: {
		{
			o: CFrame,
			at: datetime
		}
	},

	-- methods
	pushAt: (self: Snapshot, at:number, o: CFrame) -> (),
	getAt: (self: Snapshot, at: number) -> CFrame,
	getBefore: (self: Snapshot, at: number) -> {o: CFrame, at: datetime},
	getAfter: (self: Snapshot, at: number) -> {o: CFrame, at: datetime},
	destroy: (self: Snapshot) -> (),
}

local maxLength = 10

local snapshotInstances = {}
function Snapshot.getSnapshotInstance(player: Player): Snapshot
	return snapshotInstances[player] or Snapshot.registerPlayer(player)
end

function Snapshot.registerPlayer(player: Player): Snapshot
	local snapshot = Snapshot.new(player)
	snapshotInstances[player] = snapshot

	return snapshot
end

function Snapshot.deregisterPlayer(player: Player)
	local snapshot = snapshotInstances[player]
	if snapshot then
		snapshotInstances[player] = nil
		snapshot:destroy()
	end
end

function Snapshot.new(subject: Player): Snapshot
	local self = setmetatable({}, Snapshot) :: Snapshot

	self._subject = subject
	self._cache = {}

	return self
end

function Snapshot.pushAt(self: Snapshot, at, o): ()
	for i = 1, 10 do
		if self._cache[i] then
			if self._cache[i].at < at then
				table.insert(self._cache, i, {["at"] = at, ["o"] = o})
				break
			end
		else
			self._cache[i] = {["at"] = at, ["o"] = o}
			break
		end
	end

	if #self._cache > 10 then
		table.remove(self._cache, 11)
	end
end

function Snapshot:getBefore(at)
	local closest, distance = nil, 1e9
	for _, snapshot in self._cache do
		local dist = math.abs(snapshot.at - at)
		if snapshot.at < at and dist < distance then
			closest = snapshot
			distance = dist
		end
	end
	return closest
end

function Snapshot:getAfter(at)
	local closest, distance = nil, 1e9
	for _, snapshot in self._cache do
		local dist = math.abs(snapshot.at - at)
		if snapshot.at > at and dist < distance then
			closest = snapshot
			distance = dist
		end
	end
	return closest
end

function Snapshot.getAt(self: Snapshot, at): CFrame?
	if #self._cache == 0 then return nil end
	if #self._cache == 1 then return self._cache[1].o end

	local from, to = self:getAfter(at), self:getBefore(at)
	if not (from and to) then
		to = self._cache[1]
		from = self:getAfter(to.at)
		if not (from and to) then
			return nil
		end
	end

	local a = (at - from.at) / (to.at - from.at)
	return from.o:Lerp(to.o, a)
end

function Snapshot.destroy(self: Snapshot): ()
	self._subject = nil
	table.clear(self._cache)

	setmetatable(self, nil)
	table.clear(self)
	table.freeze(self :: {})
end

return Snapshot


-- Thanks Tazmondo for this util suggestion
local RunService = game:GetService('RunService')

local Utils = {}

function Utils.FrequencyPostSimulation(callback: (number) -> (), frequency: number)
	local last = time()
	frequency = 1/frequency
	return RunService.PostSimulation:Connect(function()
		local dt = time() - last
		if dt < frequency then
			return
		end
		last = time()
		callback(dt)
	end)
end

function Utils.FrequencyHeartbeat(callback: (number) -> (), frequency: number)
	local last = time()
	frequency = 1/frequency
	return RunService.Heartbeat:Connect(function()
		local dt = time() - last
		if dt < frequency then
			return
		end
		last = time()
		callback(dt)
	end)
end

return Utils


-- humanoidAnimateR15Moods.lua

local Character = script.Parent
local Humanoid = Character:WaitForChild("Humanoid")
local pose = "Standing"

local userNoUpdateOnLoopSuccess, userNoUpdateOnLoopValue = pcall(function() return UserSettings():IsUserFeatureEnabled("UserNoUpdateOnLoop") end)
local userNoUpdateOnLoop = userNoUpdateOnLoopSuccess and userNoUpdateOnLoopValue

local userAnimateScaleRunSuccess, userAnimateScaleRunValue = pcall(function() return UserSettings():IsUserFeatureEnabled("UserAnimateScaleRun") end)
local userAnimateScaleRun = userAnimateScaleRunSuccess and userAnimateScaleRunValue

local function getRigScale()
	if userAnimateScaleRun then
		return Character:GetScale()
	else
		return 1
	end
end

local AnimationSpeedDampeningObject = script:FindFirstChild("ScaleDampeningPercent")
local HumanoidHipHeight = 2

local EMOTE_TRANSITION_TIME = 0.1

local currentAnim = ""
local currentAnimInstance = nil
local currentAnimTrack = nil
local currentAnimKeyframeHandler = nil
local currentAnimSpeed = 1.0

local runAnimTrack = nil
local runAnimKeyframeHandler = nil

local PreloadedAnims = {}

local animTable = {}
local animNames = {
	idle = 	{
				{ id = "http://www.roblox.com/asset/?id=507766666", weight = 1 },
				{ id = "http://www.roblox.com/asset/?id=507766951", weight = 1 },
				{ id = "http://www.roblox.com/asset/?id=507766388", weight = 9 }
			},
	walk = 	{
				{ id = "http://www.roblox.com/asset/?id=507777826", weight = 10 }
			},
	run = 	{
				{ id = "http://www.roblox.com/asset/?id=507767714", weight = 10 }
			},
	swim = 	{
				{ id = "http://www.roblox.com/asset/?id=507784897", weight = 10 }
			},
	swimidle = 	{
				{ id = "http://www.roblox.com/asset/?id=507785072", weight = 10 }
			},
	jump = 	{
				{ id = "http://www.roblox.com/asset/?id=507765000", weight = 10 }
			},
	fall = 	{
				{ id = "http://www.roblox.com/asset/?id=507767968", weight = 10 }
			},
	climb = {
				{ id = "http://www.roblox.com/asset/?id=507765644", weight = 10 }
			},
	sit = 	{
				{ id = "http://www.roblox.com/asset/?id=2506281703", weight = 10 }
			},
	toolnone = {
				{ id = "http://www.roblox.com/asset/?id=507768375", weight = 10 }
			},
	toolslash = {
				{ id = "http://www.roblox.com/asset/?id=522635514", weight = 10 }
			},
	toollunge = {
				{ id = "http://www.roblox.com/asset/?id=522638767", weight = 10 }
			},
	wave = {
				{ id = "http://www.roblox.com/asset/?id=507770239", weight = 10 }
			},
	point = {
				{ id = "http://www.roblox.com/asset/?id=507770453", weight = 10 }
			},
	dance = {
				{ id = "http://www.roblox.com/asset/?id=507771019", weight = 10 },
				{ id = "http://www.roblox.com/asset/?id=507771955", weight = 10 },
				{ id = "http://www.roblox.com/asset/?id=507772104", weight = 10 }
			},
	dance2 = {
				{ id = "http://www.roblox.com/asset/?id=507776043", weight = 10 },
				{ id = "http://www.roblox.com/asset/?id=507776720", weight = 10 },
				{ id = "http://www.roblox.com/asset/?id=507776879", weight = 10 }
			},
	dance3 = {
				{ id = "http://www.roblox.com/asset/?id=507777268", weight = 10 },
				{ id = "http://www.roblox.com/asset/?id=507777451", weight = 10 },
				{ id = "http://www.roblox.com/asset/?id=507777623", weight = 10 }
			},
	laugh = {
				{ id = "http://www.roblox.com/asset/?id=507770818", weight = 10 }
			},
	cheer = {
				{ id = "http://www.roblox.com/asset/?id=507770677", weight = 10 }
			},
}

-- Existance in this list signifies that it is an emote, the value indicates if it is a looping emote
local emoteNames = { wave = false, point = false, dance = true, dance2 = true, dance3 = true, laugh = false, cheer = false}

math.randomseed(tick())

function findExistingAnimationInSet(set, anim)
	if set == nil or anim == nil then
		return 0
	end

	for idx = 1, set.count, 1 do
		if set[idx].anim.AnimationId == anim.AnimationId then
			return idx
		end
	end

	return 0
end

function configureAnimationSet(name, fileList)
	if (animTable[name] ~= nil) then
		for _, connection in pairs(animTable[name].connections) do
			connection:disconnect()
		end
	end
	animTable[name] = {}
	animTable[name].count = 0
	animTable[name].totalWeight = 0
	animTable[name].connections = {}

	local allowCustomAnimations = true

	local success, msg = pcall(function() allowCustomAnimations = game:GetService("StarterPlayer").AllowCustomAnimations end)
	if not success then
		allowCustomAnimations = true
	end

	-- check for config values
	local config = script:FindFirstChild(name)
	if (allowCustomAnimations and config ~= nil) then
		table.insert(animTable[name].connections, config.ChildAdded:connect(function(child) configureAnimationSet(name, fileList) end))
		table.insert(animTable[name].connections, config.ChildRemoved:connect(function(child) configureAnimationSet(name, fileList) end))

		local idx = 0
		for _, childPart in pairs(config:GetChildren()) do
			if (childPart:IsA("Animation")) then
				local newWeight = 1
				local weightObject = childPart:FindFirstChild("Weight")
				if (weightObject ~= nil) then
					newWeight = weightObject.Value
				end
				animTable[name].count = animTable[name].count + 1
				idx = animTable[name].count
				animTable[name][idx] = {}
				animTable[name][idx].anim = childPart
				animTable[name][idx].weight = newWeight
				animTable[name].totalWeight = animTable[name].totalWeight + animTable[name][idx].weight
				table.insert(animTable[name].connections, childPart.Changed:connect(function(property) configureAnimationSet(name, fileList) end))
				table.insert(animTable[name].connections, childPart.ChildAdded:connect(function(property) configureAnimationSet(name, fileList) end))
				table.insert(animTable[name].connections, childPart.ChildRemoved:connect(function(property) configureAnimationSet(name, fileList) end))
			end
		end
	end

	-- fallback to defaults
	if (animTable[name].count <= 0) then
		for idx, anim in pairs(fileList) do
			animTable[name][idx] = {}
			animTable[name][idx].anim = Instance.new("Animation")
			animTable[name][idx].anim.Name = name
			animTable[name][idx].anim.AnimationId = anim.id
			animTable[name][idx].weight = anim.weight
			animTable[name].count = animTable[name].count + 1
			animTable[name].totalWeight = animTable[name].totalWeight + anim.weight
		end
	end

	-- preload anims
	for i, animType in pairs(animTable) do
		for idx = 1, animType.count, 1 do
			if PreloadedAnims[animType[idx].anim.AnimationId] == nil then
				Humanoid:LoadAnimation(animType[idx].anim)
				PreloadedAnims[animType[idx].anim.AnimationId] = true
			end
		end
	end
end

------------------------------------------------------------------------------------------------------------

function configureAnimationSetOld(name, fileList)
	if (animTable[name] ~= nil) then
		for _, connection in pairs(animTable[name].connections) do
			connection:disconnect()
		end
	end
	animTable[name] = {}
	animTable[name].count = 0
	animTable[name].totalWeight = 0
	animTable[name].connections = {}

	local allowCustomAnimations = true

	local success, msg = pcall(function() allowCustomAnimations = game:GetService("StarterPlayer").AllowCustomAnimations end)
	if not success then
		allowCustomAnimations = true
	end

	-- check for config values
	local config = script:FindFirstChild(name)
	if (allowCustomAnimations and config ~= nil) then
		table.insert(animTable[name].connections, config.ChildAdded:connect(function(child) configureAnimationSet(name, fileList) end))
		table.insert(animTable[name].connections, config.ChildRemoved:connect(function(child) configureAnimationSet(name, fileList) end))
		local idx = 1
		for _, childPart in pairs(config:GetChildren()) do
			if (childPart:IsA("Animation")) then
				table.insert(animTable[name].connections, childPart.Changed:connect(function(property) configureAnimationSet(name, fileList) end))
				animTable[name][idx] = {}
				animTable[name][idx].anim = childPart
				local weightObject = childPart:FindFirstChild("Weight")
				if (weightObject == nil) then
					animTable[name][idx].weight = 1
				else
					animTable[name][idx].weight = weightObject.Value
				end
				animTable[name].count = animTable[name].count + 1
				animTable[name].totalWeight = animTable[name].totalWeight + animTable[name][idx].weight
				idx = idx + 1
			end
		end
	end

	-- fallback to defaults
	if (animTable[name].count <= 0) then
		for idx, anim in pairs(fileList) do
			animTable[name][idx] = {}
			animTable[name][idx].anim = Instance.new("Animation")
			animTable[name][idx].anim.Name = name
			animTable[name][idx].anim.AnimationId = anim.id
			animTable[name][idx].weight = anim.weight
			animTable[name].count = animTable[name].count + 1
			animTable[name].totalWeight = animTable[name].totalWeight + anim.weight
			-- print(name .. " [" .. idx .. "] " .. anim.id .. " (" .. anim.weight .. ")")
		end
	end

	-- preload anims
	for i, animType in pairs(animTable) do
		for idx = 1, animType.count, 1 do
			Humanoid:LoadAnimation(animType[idx].anim)
		end
	end
end

-- Setup animation objects
function scriptChildModified(child)
	local fileList = animNames[child.Name]
	if (fileList ~= nil) then
		configureAnimationSet(child.Name, fileList)
	end
end

script.ChildAdded:connect(scriptChildModified)
script.ChildRemoved:connect(scriptChildModified)

-- Clear any existing animation tracks
-- Fixes issue with characters that are moved in and out of the Workspace accumulating tracks
local animator = if Humanoid then Humanoid:FindFirstChildOfClass("Animator") else nil
if animator then
	local animTracks = animator:GetPlayingAnimationTracks()
	for i,track in ipairs(animTracks) do
		track:Stop(0)
		track:Destroy()
	end
end

for name, fileList in pairs(animNames) do
	configureAnimationSet(name, fileList)
end

-- ANIMATION

-- declarations
local toolAnim = "None"
local toolAnimTime = 0

local jumpAnimTime = 0
local jumpAnimDuration = 0.31

local toolTransitionTime = 0.1
local fallTransitionTime = 0.2

local currentlyPlayingEmote = false

-- functions

function stopAllAnimations()
	local oldAnim = currentAnim

	-- return to idle if finishing an emote
	if (emoteNames[oldAnim] ~= nil and emoteNames[oldAnim] == false) then
		oldAnim = "idle"
	end

	if currentlyPlayingEmote then
		oldAnim = "idle"
		currentlyPlayingEmote = false
	end

	currentAnim = ""
	currentAnimInstance = nil
	if (currentAnimKeyframeHandler ~= nil) then
		currentAnimKeyframeHandler:disconnect()
	end

	if (currentAnimTrack ~= nil) then
		currentAnimTrack:Stop()
		currentAnimTrack:Destroy()
		currentAnimTrack = nil
	end

	-- clean up walk if there is one
	if (runAnimKeyframeHandler ~= nil) then
		runAnimKeyframeHandler:disconnect()
	end

	if (runAnimTrack ~= nil) then
		runAnimTrack:Stop()
		runAnimTrack:Destroy()
		runAnimTrack = nil
	end

	return oldAnim
end

function getHeightScale()
	if Humanoid then
		if not Humanoid.AutomaticScalingEnabled then
			-- When auto scaling is not enabled, the rig scale stands in for
			-- a computed scale.
			return getRigScale()
		end

		local scale = Humanoid.HipHeight / HumanoidHipHeight
		if AnimationSpeedDampeningObject == nil then
			AnimationSpeedDampeningObject = script:FindFirstChild("ScaleDampeningPercent")
		end
		if AnimationSpeedDampeningObject ~= nil then
			scale = 1 + (Humanoid.HipHeight - HumanoidHipHeight) * AnimationSpeedDampeningObject.Value / HumanoidHipHeight
		end
		return scale
	end
	return getRigScale()
end

local function rootMotionCompensation(speed)
	local speedScaled = speed * 1.25
	local heightScale = getHeightScale()
	local runSpeed = speedScaled / heightScale
	return runSpeed
end

local smallButNotZero = 0.0001
local function setRunSpeed(speed)
	local normalizedWalkSpeed = 0.5 -- established empirically using current `913402848` walk animation
	local normalizedRunSpeed  = 1
	local runSpeed = rootMotionCompensation(speed)

	local walkAnimationWeight = smallButNotZero
	local runAnimationWeight = smallButNotZero
	local timeWarp = 1

	if runSpeed <= normalizedWalkSpeed then
		walkAnimationWeight = 1
		timeWarp = runSpeed/normalizedWalkSpeed
	elseif runSpeed < normalizedRunSpeed then
		local fadeInRun = (runSpeed - normalizedWalkSpeed)/(normalizedRunSpeed - normalizedWalkSpeed)
		walkAnimationWeight = 1 - fadeInRun
		runAnimationWeight  = fadeInRun
	else
		timeWarp = runSpeed/normalizedRunSpeed
		runAnimationWeight = 1
	end
	currentAnimTrack:AdjustWeight(walkAnimationWeight)
	runAnimTrack:AdjustWeight(runAnimationWeight)
	currentAnimTrack:AdjustSpeed(timeWarp)
	runAnimTrack:AdjustSpeed(timeWarp)
end

function setAnimationSpeed(speed)
	if currentAnim == "walk" then
			setRunSpeed(speed)
	else
		if speed ~= currentAnimSpeed then
			currentAnimSpeed = speed
			currentAnimTrack:AdjustSpeed(currentAnimSpeed)
		end
	end
end

function keyFrameReachedFunc(frameName)
	if (frameName == "End") then
		if currentAnim == "walk" then
			if userNoUpdateOnLoop == true then
				if runAnimTrack.Looped ~= true then
					runAnimTrack.TimePosition = 0.0
				end
				if currentAnimTrack.Looped ~= true then
					currentAnimTrack.TimePosition = 0.0
				end
			else
				runAnimTrack.TimePosition = 0.0
				currentAnimTrack.TimePosition = 0.0
			end
		else
			local repeatAnim = currentAnim
			-- return to idle if finishing an emote
			if (emoteNames[repeatAnim] ~= nil and emoteNames[repeatAnim] == false) then
				repeatAnim = "idle"
			end

			if currentlyPlayingEmote then
				if currentAnimTrack.Looped then
					-- Allow the emote to loop
					return
				end

				repeatAnim = "idle"
				currentlyPlayingEmote = false
			end

			local animSpeed = currentAnimSpeed
			playAnimation(repeatAnim, 0.15, Humanoid)
			setAnimationSpeed(animSpeed)
		end
	end
end

function rollAnimation(animName)
	local roll = math.random(1, animTable[animName].totalWeight)
	local origRoll = roll
	local idx = 1
	while (roll > animTable[animName][idx].weight) do
		roll = roll - animTable[animName][idx].weight
		idx = idx + 1
	end
	return idx
end

local function switchToAnim(anim, animName, transitionTime, humanoid)
	-- switch animation
	if (anim ~= currentAnimInstance) then

		if (currentAnimTrack ~= nil) then
			currentAnimTrack:Stop(transitionTime)
			currentAnimTrack:Destroy()
		end

		if (runAnimTrack ~= nil) then
			runAnimTrack:Stop(transitionTime)
			runAnimTrack:Destroy()
			if userNoUpdateOnLoop == true then
				runAnimTrack = nil
			end
		end

		currentAnimSpeed = 1.0

		-- load it to the humanoid; get AnimationTrack
		currentAnimTrack = humanoid:LoadAnimation(anim)
		currentAnimTrack.Priority = Enum.AnimationPriority.Core

		-- play the animation
		currentAnimTrack:Play(transitionTime)
		currentAnim = animName
		currentAnimInstance = anim

		-- set up keyframe name triggers
		if (currentAnimKeyframeHandler ~= nil) then
			currentAnimKeyframeHandler:disconnect()
		end
		currentAnimKeyframeHandler = currentAnimTrack.KeyframeReached:connect(keyFrameReachedFunc)

		-- check to see if we need to blend a walk/run animation
		if animName == "walk" then
			local runAnimName = "run"
			local runIdx = rollAnimation(runAnimName)

			runAnimTrack = humanoid:LoadAnimation(animTable[runAnimName][runIdx].anim)
			runAnimTrack.Priority = Enum.AnimationPriority.Core
			runAnimTrack:Play(transitionTime)

			if (runAnimKeyframeHandler ~= nil) then
				runAnimKeyframeHandler:disconnect()
			end
			runAnimKeyframeHandler = runAnimTrack.KeyframeReached:connect(keyFrameReachedFunc)
		end
	end
end

function playAnimation(animName, transitionTime, humanoid)
	local idx = rollAnimation(animName)
	local anim = animTable[animName][idx].anim

	switchToAnim(anim, animName, transitionTime, humanoid)
	currentlyPlayingEmote = false
end

function playEmote(emoteAnim, transitionTime, humanoid)
	switchToAnim(emoteAnim, emoteAnim.Name, transitionTime, humanoid)
	currentlyPlayingEmote = true
end

-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------

local toolAnimName = ""
local toolAnimTrack = nil
local toolAnimInstance = nil
local currentToolAnimKeyframeHandler = nil

function toolKeyFrameReachedFunc(frameName)
	if (frameName == "End") then
		playToolAnimation(toolAnimName, 0.0, Humanoid)
	end
end

function playToolAnimation(animName, transitionTime, humanoid, priority)
		local idx = rollAnimation(animName)
		local anim = animTable[animName][idx].anim

		if (toolAnimInstance ~= anim) then

			if (toolAnimTrack ~= nil) then
				toolAnimTrack:Stop()
				toolAnimTrack:Destroy()
				transitionTime = 0
			end

			-- load it to the humanoid; get AnimationTrack
			toolAnimTrack = humanoid:LoadAnimation(anim)
			if priority then
				toolAnimTrack.Priority = priority
			end

			-- play the animation
			toolAnimTrack:Play(transitionTime)
			toolAnimName = animName
			toolAnimInstance = anim

			currentToolAnimKeyframeHandler = toolAnimTrack.KeyframeReached:connect(toolKeyFrameReachedFunc)
		end
end

function stopToolAnimations()
	local oldAnim = toolAnimName

	if (currentToolAnimKeyframeHandler ~= nil) then
		currentToolAnimKeyframeHandler:disconnect()
	end

	toolAnimName = ""
	toolAnimInstance = nil
	if (toolAnimTrack ~= nil) then
		toolAnimTrack:Stop()
		toolAnimTrack:Destroy()
		toolAnimTrack = nil
	end

	return oldAnim
end

-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------
-- STATE CHANGE HANDLERS

function onRunning(speed)
	local heightScale = if userAnimateScaleRun then getHeightScale() else 1

	local movedDuringEmote = currentlyPlayingEmote and Humanoid.MoveDirection == Vector3.new(0, 0, 0)
	local speedThreshold = movedDuringEmote and (Humanoid.WalkSpeed / heightScale) or 0.75
	if speed > speedThreshold * heightScale then
		local scale = 16.0
		playAnimation("walk", 0.2, Humanoid)
		setAnimationSpeed(speed / scale)
		pose = "Running"
	else
		if emoteNames[currentAnim] == nil and not currentlyPlayingEmote then
			playAnimation("idle", 0.2, Humanoid)
			pose = "Standing"
		end
	end
end

function onDied()
	pose = "Dead"
end

function onJumping()
	playAnimation("jump", 0.1, Humanoid)
	jumpAnimTime = jumpAnimDuration
	pose = "Jumping"
end

function onClimbing(speed)
	if userAnimateScaleRun then
		speed /= getHeightScale()
	end
	local scale = 5.0
	playAnimation("climb", 0.1, Humanoid)
	setAnimationSpeed(speed / scale)
	pose = "Climbing"
end

function onGettingUp()
	pose = "GettingUp"
end

function onFreeFall()
	if (jumpAnimTime <= 0) then
		playAnimation("fall", fallTransitionTime, Humanoid)
	end
	pose = "FreeFall"
end

function onFallingDown()
	pose = "FallingDown"
end

function onSeated()
	pose = "Seated"
end

function onPlatformStanding()
	pose = "PlatformStanding"
end

-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------

function onSwimming(speed)
	if userAnimateScaleRun then
		speed /= getHeightScale()
	end
	if speed > 1.00 then
		local scale = 10.0
		playAnimation("swim", 0.4, Humanoid)
		setAnimationSpeed(speed / scale)
		pose = "Swimming"
	else
		playAnimation("swimidle", 0.4, Humanoid)
		pose = "Standing"
	end
end

function animateTool()
	if (toolAnim == "None") then
		playToolAnimation("toolnone", toolTransitionTime, Humanoid, Enum.AnimationPriority.Idle)
		return
	end

	if (toolAnim == "Slash") then
		playToolAnimation("toolslash", 0, Humanoid, Enum.AnimationPriority.Action)
		return
	end

	if (toolAnim == "Lunge") then
		playToolAnimation("toollunge", 0, Humanoid, Enum.AnimationPriority.Action)
		return
	end
end

function getToolAnim(tool)
	for _, c in ipairs(tool:GetChildren()) do
		if c.Name == "toolanim" and c.className == "StringValue" then
			return c
		end
	end
	return nil
end

local lastTick = 0

function stepAnimate(currentTime)
	local amplitude = 1
	local frequency = 1
  	local deltaTime = currentTime - lastTick
  	lastTick = currentTime

	local climbFudge = 0
	local setAngles = false

  	if (jumpAnimTime > 0) then
  		jumpAnimTime = jumpAnimTime - deltaTime
  	end

	if (pose == "FreeFall" and jumpAnimTime <= 0) then
		playAnimation("fall", fallTransitionTime, Humanoid)
	elseif (pose == "Seated") then
		playAnimation("sit", 0.5, Humanoid)
		return
	elseif (pose == "Running") then
		playAnimation("walk", 0.2, Humanoid)
	elseif (pose == "Dead" or pose == "GettingUp" or pose == "FallingDown" or pose == "Seated" or pose == "PlatformStanding") then
		stopAllAnimations()
		amplitude = 0.1
		frequency = 1
		setAngles = true
	end

	-- Tool Animation handling
	local tool = Character:FindFirstChildOfClass("Tool")
	if tool and tool:FindFirstChild("Handle") then
		local animStringValueObject = getToolAnim(tool)

		if animStringValueObject then
			toolAnim = animStringValueObject.Value
			-- message recieved, delete StringValue
			animStringValueObject.Parent = nil
			toolAnimTime = currentTime + .3
		end

		if currentTime > toolAnimTime then
			toolAnimTime = 0
			toolAnim = "None"
		end

		animateTool()
	else
		stopToolAnimations()
		toolAnim = "None"
		toolAnimInstance = nil
		toolAnimTime = 0
	end
end

-- connect events
Humanoid.Died:connect(onDied)
Humanoid.Running:connect(onRunning)
Humanoid.Jumping:connect(onJumping)
Humanoid.Climbing:connect(onClimbing)
Humanoid.GettingUp:connect(onGettingUp)
Humanoid.FreeFalling:connect(onFreeFall)
Humanoid.FallingDown:connect(onFallingDown)
Humanoid.Seated:connect(onSeated)
Humanoid.PlatformStanding:connect(onPlatformStanding)
Humanoid.Swimming:connect(onSwimming)

-- setup emote chat hook
game:GetService("Players").LocalPlayer.Chatted:connect(function(msg)
	local emote = ""
	if (string.sub(msg, 1, 3) == "/e ") then
		emote = string.sub(msg, 4)
	elseif (string.sub(msg, 1, 7) == "/emote ") then
		emote = string.sub(msg, 8)
	end

	if (pose == "Standing" and emoteNames[emote] ~= nil) then
		playAnimation(emote, EMOTE_TRANSITION_TIME, Humanoid)
	end
end)

-- emote bindable hook
script:WaitForChild("PlayEmote").OnInvoke = function(emote)
	-- Only play emotes when idling
	if pose ~= "Standing" then
		return
	end

	if emoteNames[emote] ~= nil then
		-- Default emotes
		playAnimation(emote, EMOTE_TRANSITION_TIME, Humanoid)

		return true, currentAnimTrack
	elseif typeof(emote) == "Instance" and emote:IsA("Animation") then
		-- Non-default emotes
		playEmote(emote, EMOTE_TRANSITION_TIME, Humanoid)

		return true, currentAnimTrack
	end

	-- Return false to indicate that the emote could not be played
	return false
end

if Character.Parent ~= nil then
	-- initialize to idle
	playAnimation("idle", 0.1, Humanoid)
	pose = "Standing"
end

-- loop to handle timed state transitions and tool animations
while Character.Parent ~= nil do
	local _, currentGameTime = wait(0.1)
	stepAnimate(currentGameTime)
end


--!strict
local Players = game:GetService("Players")
local BetterReplication = script.Parent.Parent

local BufferUtils = require(BetterReplication.Lib.BufferUtils)
local GetRegistry = BetterReplication.Remotes.GetRegistry
local Utils = require(BetterReplication.Lib.Utils)
local Config = require(BetterReplication.Config)

local FromClient = BetterReplication.Remotes.FromClient
local ToClient = BetterReplication.Remotes.ToClient
local RegisterIdentifier = BetterReplication.Remotes.RegisterIdentifier
local OutOfProximity = BetterReplication.Remotes.OutOfProximity

local random = Random.new(tick())

type playerIdentifier = number

local UptodatePositions = {}
local positionTable = {} :: {[Player]: CFrame} -- table for getting latest position data
local identifiers = {} :: {[Player]: playerIdentifier}
local currentlyOutOfProximity = {} :: {[Player]: {Player}}
local warrantIteration = false
local enabled = true
local sanityFunc: (data: BufferUtils.from_client_packet) -> boolean = nil

local writeToClient = BufferUtils.writeToClientSimplified
local readFromClient = BufferUtils.readFromClientSimplified
if Config.makeRagdollFriendly then
	readFromClient = BufferUtils.readFromClient
	writeToClient = BufferUtils.writeToClient
end

-------------------

local function getIdentifier(player: Player)
	local potentialId = 1

	for i = 1, 255 do
		local taken = false
		for _, id in identifiers do
			if id == i then
				taken = true
				break
			end
		end

		if not taken then
			potentialId = i
			break
		end
	end
	identifiers[player] = potentialId

	RegisterIdentifier:FireAllClients(BufferUtils.writeRegisterIdentifier(player.Name, potentialId))
end
GetRegistry.OnServerInvoke = function()
	local t = {}
	for player, id in identifiers do
		t[id] = player
	end
	return t
end

Players.PlayerAdded:Connect(function(player: Player)
	getIdentifier(player)
	currentlyOutOfProximity[player] = {}

	--player.CharacterAdded:Connect(function(char: Model)
	--	local scr = script.UpdatePosition:Clone()
	--	scr.Parent = char
	--end)
end)
Players.PlayerRemoving:Connect(function(player: Player)
	positionTable[player] = nil
	identifiers[player] = nil
end)

--
FromClient.OnServerEvent:Connect(function(player, b: buffer)
	local data = readFromClient(b)

	if Config.packetLossRate < 0 and random:NextNumber() <= Config.packetLossRate then
		warn(script:GetFullName() ,"| Simulated packet loss for:", player)
		return
	end

	if sanityFunc then
		local res = sanityFunc(data)
		if not res then
			return
		end
	end

	positionTable[player] = data.c
	if not enabled then return end

	warrantIteration = true

	local outOfProximity = currentlyOutOfProximity[player]
	for _, receiver in Players:GetPlayers() do
		if receiver == player or table.find(outOfProximity, receiver) then
			continue
		end

		ToClient:FireClient(receiver, writeToClient(data.t, identifiers[player], data.c))
	end
end)

local function proximityClock(ht)
	if not warrantIteration then return end

	for _, receiver in Players:GetPlayers() do
		local updateMap = {}

		local newOutOfProximity = {}
		local proximityUpdate = false

		local receiverCframe = positionTable[receiver]
		if not receiverCframe then continue end

		for subject, cframe in positionTable do
			if subject == receiver then continue end

			local isInProximity = (receiverCframe.Position - cframe.Position).Magnitude <= Config.proximityThreshold
			local outOfProximityIndex = table.find(
				currentlyOutOfProximity[receiver],
				subject
			)

			if outOfProximityIndex then
				if isInProximity then
					table.remove(
						currentlyOutOfProximity[receiver],
						outOfProximityIndex
					)
				end
			elseif not outOfProximityIndex and not isInProximity then
				table.insert(
					currentlyOutOfProximity[receiver],
					subject
				)
				table.insert(
					newOutOfProximity,
					identifiers[subject]
				)

				proximityUpdate = true
			end
		end
		if proximityUpdate then
			OutOfProximity:FireClient(receiver, BufferUtils.writeOutOfProximityArray(newOutOfProximity))
		end
	end
	warrantIteration = false
end

-- start the replication handler
function UptodatePositions.start()
	Utils.FrequencyHeartbeat(proximityClock, 1/Config.tickRate)
end

function UptodatePositions.bindSanityCheck(func: (data: BufferUtils.from_client_packet) -> boolean)
	sanityFunc = func
end

function UptodatePositions.getCFrame(player: Player): CFrame
	print('POSITION CALL')
	return positionTable[player]
end

function UptodatePositions.toggle(v: boolean)
	enabled = v
end

return UptodatePositions


local ScriptEditorService = game:GetService("ScriptEditorService")

local Cleanup = {}
Cleanup.Functions = {}
Cleanup.List = {}

function Cleanup:Hook(Service: string | nil, Tag: string | nil)
	if Service and Tag then
		Cleanup.Functions[Service] = Tag
		return true
	end
	return Cleanup.List
end

function Cleanup:Clear()
	for Service, Tag in (Cleanup.Functions) do
		if Service == "ScriptAnalysisCallback" then
			ScriptEditorService:DeregisterScriptAnalysisCallback(Tag)
		end
	end

	table.clear(Cleanup.Functions)

	for _, Connection in (Cleanup.List) do
		if typeof(Connection) == "thread" then
			task.cancel(Connection)
		else
			Connection:Disconnect()
		end
	end

	table.clear(Cleanup.List)
end

return Cleanup


local LogService = game:GetService("LogService")

local Cleanup = require(script.Parent.Cleanup)
local Server = require(script.Parent.Server)
local Http = require(script.Parent.Http)

local GUID = Http:GUID()

local Output = {
	UploadInterval = 1/15;
	PendingLogs = false;
	Busy = false;
	Logs = {
		Uploaded = 0;
		Count = 0;
		Data = {};
	};
}

export type OutputLog = {
	["messageType"]: string;
	["stackTrace"]: StackTrace | nil;
	["timestamp"]: number;
	["message"]: string;
}

export type OutputLogs = {
	[string]: OutputLog;
}

export type StackTrace = {
	[number]: string
}

function Output:Upload(Logs: OutputLogs, Count: number)
	local Data = {
		["StartTime"] = Server.StartTime;
		["PlaceId"] = game.PlaceId;
		["GUID"] = GUID;
		["Logs"] = Logs
	}

	local HttpRequest = Http:Post(Data)

	if HttpRequest then
		if HttpRequest == "Ratelimited" then
			task.wait(Output.UploadInterval)
			Output:Upload(Logs, Count)
		else
			for i = Output.Logs.Uploaded, Count do
				Output.Logs.Data[i] = nil
			end
			Output.Logs.Uploaded = Count
		end
	end
end

function Output:Collect(Count: number): OutputLogs
	local Logs = {}

	for i = math.max(Output.Logs.Uploaded, 1), Count do
		Logs[tostring(i)] = Output.Logs.Data[i]
	end

	return Logs
end

function Output:Flush(Count: number)
	Server:UpdateStartTime()

	if not Output.Busy then
		Output.Busy = true

		local Logs = Output:Collect(Count)

		if Logs then
			Output:Upload(Logs, Count)
		end

		Output.Busy = false
	end
end

function Output:IsConnected(): boolean
	if Server.Disconnected then
		return false
	end

	if not Server.Connected then
		Server:AutoConnect(true)
	end

	return Server.Connected
end

function Output:Insert(Log: OutputLog)
	Output.Logs.Count += 1
	Output.Logs.Data[Output.Logs.Count] = Log

	if not Output.PendingLogs and Output:IsConnected() then
		Output.PendingLogs = true
	end
end

function Output:IsTrace(Message: string): boolean
	local StackTrace = false

	if string.sub(Message, 1, 8) == "Script '" and string.find(Message, "Line") then
		StackTrace = true
	end

	if Message == "Stack Begin" or Message == "Stack End" then
		StackTrace = true
	end

	return StackTrace
end

function Output:Load()
	Server:AutoConnect()

	Cleanup:Hook().OutputQueue = task.spawn(function()
		while task.wait(Output.UploadInterval) do
			if Output.PendingLogs and Output:IsConnected() then
				Output.PendingLogs = false

				Output:Flush(Output.Logs.Count)
			end
		end
	end)

	Cleanup:Hook().MessageOut = LogService.MessageOut:Connect(function(Message: string, Type: Enum.MessageType)
		local Log = {
			["timestamp"] = DateTime.now().UnixTimestampMillis;
			["messageType"] = Type.Name;
			["message"] = Message;
		}

		if Output:IsTrace(Message) then
			Log.messageType = "MessageTrace"
		end

		Output:Insert(Log)
	end)
end

return Output