--!strict
--[=[
	@class TiePropertyImplementation
]=]

local require = require(script.Parent.loader).load(script)

local AttributeUtils = require("AttributeUtils")
local AttributeValue = require("AttributeValue")
local BaseObject = require("BaseObject")
local Maid = require("Maid")
local TiePropertyImplementationUtils = require("TiePropertyImplementationUtils")
local TieUtils = require("TieUtils")
local ValueBaseUtils = require("ValueBaseUtils")
local ValueObject = require("ValueObject")

local TiePropertyImplementation = setmetatable({}, BaseObject)
TiePropertyImplementation.ClassName = "TiePropertyImplementation"
TiePropertyImplementation.__index = TiePropertyImplementation

export type TiePropertyImplementation =
	typeof(setmetatable(
		{} :: {
			_folder: Instance,
			_memberDefinition: any,
		},
		{} :: typeof({ __index = TiePropertyImplementation })
	))
	& BaseObject.BaseObject

function TiePropertyImplementation.new(memberDefinition, folder: Instance, initialValue, _actualSelf)
	local self = setmetatable(BaseObject.new() :: any, TiePropertyImplementation)

	self._memberDefinition = assert(memberDefinition, "No memberDefinition")
	self._folder = assert(folder, "No folder")

	self._maid:GiveTask(function()
		local memberName = self._memberDefinition:GetMemberName()
		local currentInstance = self._folder:FindFirstChild(memberName)
		self._folder:SetAttribute(memberName, nil)

		if not currentInstance then
			return
		end

		-- Clean up what we've created.
		currentInstance:Destroy()
	end)

	self:SetImplementation(initialValue)

	-- Since "actualSelf" can be quite large, we clean up our stuff aggressively for GC.
	self._maid:GiveTask(function()
		self._maid:DoCleaning()

		for key, _ in pairs(self) do
			rawset(self, key, nil)
		end
	end)

	return self
end

function TiePropertyImplementation.SetImplementation(self: TiePropertyImplementation, implementation)
	self._maid._current = nil :: Maid.Maid?

	local maid = Maid.new()

	-- override with default value on nil case
	if implementation == nil then
		implementation = self._memberDefinition:GetDefaultValue()
	end

	self:_updateImplementation(maid, implementation)

	self._maid._current = maid
end

function TiePropertyImplementation._updateImplementation(
	self: TiePropertyImplementation,
	maid: Maid.Maid,
	implementation
)
	if ValueObject.isValueObject(implementation) then
		local checkType = implementation:GetCheckType()

		if checkType and AttributeUtils.isValidAttributeType(checkType) and checkType ~= "nil" then
			self:_removeClassIfNeeded()

			local attributeValue = AttributeValue.new(self._folder, self._memberDefinition:GetMemberName())
			self:_syncMember(maid, attributeValue, implementation)
			return
		end
	end

	if type(implementation) == "table" and implementation.Changed then
		local copy: BindableFunction = self:_changeToClassIfNeeded("BindableFunction") :: any
		copy.OnInvoke = function()
			return TieUtils.encode(implementation)
		end
		copy.Parent = self._folder
		return
	end

	if typeof(implementation) == "Instance" and implementation:IsA("ValueBase") then
		local resultingType = ValueBaseUtils.getValueBaseType(implementation.ClassName :: any)
		if resultingType and AttributeUtils.isValidAttributeType(resultingType) and resultingType ~= "nil" then
			self:_removeClassIfNeeded()

			local attributeValue = AttributeValue.new(self._folder, self._memberDefinition:GetMemberName())
			self:_syncMember(maid, attributeValue, implementation)
		else
			local copy = self:_changeToClassIfNeeded(implementation.ClassName)
			self:_syncMember(maid, copy, implementation)
			copy.Parent = self._folder
		end
		return
	end

	if AttributeUtils.isValidAttributeType(typeof(implementation)) and implementation ~= nil then
		self:_removeClassIfNeeded()
		self._folder:SetAttribute(self._memberDefinition:GetMemberName(), implementation)
		return
	end

	local className = ValueBaseUtils.getClassNameFromType(typeof(implementation))
	if not className then
		local memberName = self._memberDefinition:GetMemberName()
		error(
			string.format(
				"[TiePropertyImplementation] - Bad implementation value type %q, cannot set %s",
				typeof(implementation),
				memberName
			)
		)
	end

	local copy = self:_changeToClassIfNeeded(className);
	(copy :: any).Value = implementation
	copy.Parent = self._folder
end

function TiePropertyImplementation._changeToClassIfNeeded(self: TiePropertyImplementation, className: string): Instance
	return TiePropertyImplementationUtils.changeToClassIfNeeded(self._memberDefinition, self._folder, className)
end

function TiePropertyImplementation._removeClassIfNeeded(self: TiePropertyImplementation)
	local implementation = self._folder:FindFirstChild(self._memberDefinition:GetMemberName())
	if implementation then
		implementation:Destroy()
	end
end

function TiePropertyImplementation._syncMember(
	_self: TiePropertyImplementation,
	maid: Maid.Maid,
	copy: any,
	implementation
)
	copy.Value = implementation.Value

	maid:GiveTask(implementation.Changed:Connect(function()
		copy.Value = implementation.Value
	end))

	maid:GiveTask(copy.Changed:Connect(function()
		implementation.Value = copy.Value
	end))
end

return TiePropertyImplementation
