local g=game:GetService("\082\101\112\108\105\099\097\116\101\100\083\116\111\114\097\103\101")local r=game:GetService("\080\108\097\121\101\114\115")local J=game:GetService("\084\101\120\116\083\101\114\118\105\099\101")local Q=g.AvatarEditor local O=Q.RemoteEvent local p=require(Q.Shared.Util.Promise)local H=require(Q.Shared.Util.Maid)local d=require(Q.Shared.Util.Signal)local D=require(Q.Shared.Util.TableUtil)local V=require(Q.Shared.QueryMap)local b=require(Q.Shared.Colors)local W=require(Q.Shared.Settings)local x=require(Q.Shared.GetProductInfo)local I=require(Q.Shared.UserCanUse)local y={}local c={}local i={}i.HumanoidDescriptionChanged=d.new()i.CostumeAdded=d.new()i.CostumeRemoved=d.new()i.CostumeUpdated=d.new()i.SettingChanged=d.new()i.PermissionFailed=d.new()local function a(g,r)return(p.new(function(Q,O)local p,H=pcall(function()return J:FilterStringAsync(g,r.UserId)end)if p then Q(H)else O(H)end end)):andThen(function(g)return p.new(function(J,Q)local O,p=pcall(function()return g:GetNonChatStringForUserAsync(r.UserId)end)if O then J(p)else Q(p)end end)end)end local function E(g)local r=g.Character local J=r and r:FindFirstChildWhichIsA("\072\117\109\097\110\111\105\100")if J and J.Health>0 then return J end end local function U(g)return p.new(function(J,Q)local O,p=pcall(function()return r:GetHumanoidDescriptionFromUserId(g)end)if O then J(p)else Q(p)end end)end local function X(g,r)return p.new(function(J,Q)local O,p=pcall(function()g:ApplyDescription(r)end)if O then J(O)else Q(p)end end)end local function K(g,r)i.HumanoidDescriptionChanged:Fire(g,r)local J=E(g)if not J then return p.reject()end return X(J,r)end local function o(g,...)O:FireClient(g,...)end local function S(g,r)assert(typeof(r)=="\110\117\109\098\101\114")local J=E(g)if not J then return end local Q=J:GetAppliedDescription()if not Q then return end;((x(r)):andThen(function(r)local O=V.GetOptionName("\065\115\115\101\116\084\121\112\101",r.AssetTypeId)if not O then return end if O=="\072\097\116"then O="\072\097\116\065\099\099\101\115\115\111\114\121"elseif O=="\083\104\111\117\108\100\101\114\065\099\099\101\115\115\111\114\121"then O="\083\104\111\117\108\100\101\114\115\065\099\099\101\115\115\111\114\121"end if string.find(O,"\065\099\099\101\115\115\111\114\121")then local g=string.split(Q[O],"\044")local p=table.find(g,tostring(r.AssetId))if p then table.remove(g,p)elseif#J:GetAccessories()<W.MAX_ACCESSORIES then table.insert(g,tostring(r.AssetId))else end Q[O]=table.concat(g,"\044")else if Q[O]==0 or Q[O]~=r.AssetId then Q[O]=r.AssetId else Q[O]=O=="\083\104\105\114\116"and 1 or O=="\080\097\110\116\115"and 2 or 0 end end;(K(g,Q)):andThenCall(o,g,"\097\099\099\101\115\115\111\114\121")end)):catch(warn)end local function F(g,r)assert(typeof(r)=="\110\117\109\098\101\114")local J=E(g)if not J then return end local Q=J:GetAppliedDescription()if not Q then return end r=math.clamp(r,1,75)local O=b[r]if O then Q.HeadColor=O Q.LeftArmColor=O Q.LeftLegColor=O Q.RightArmColor=O Q.RightLegColor=O Q.TorsoColor=O end;((K(g,Q)):doneCall(o,g,"\116\111\110\101")):catch(warn)end local function s(g)local r=E(g)if not r then return end;(((U(g.UserId)):andThen(function(r)return K(g,r)end)):doneCall(o,g,"\114\101\115\101\116")):catch(warn)end local function T(g,r,J)assert(typeof(r)=="\115\116\114\105\110\103")assert(typeof(J)=="\110\117\109\098\101\114")local Q=E(g)if not Q then return end local O=Q:GetAppliedDescription()if not O then return end if r=="\072\101\105\103\104\116\083\099\097\108\101"then J=math.clamp(J,.9,1.05)elseif r=="\087\105\100\116\104\083\099\097\108\101"then J=math.clamp(J,.7,1)elseif r=="\072\101\097\100\083\099\097\108\101"then J=math.clamp(J,.95,1)elseif r=="\080\114\111\112\111\114\116\105\111\110\083\099\097\108\101"then J=math.clamp(J,0,1)elseif r=="\066\111\100\121\084\121\112\101\083\099\097\108\101"then J=math.clamp(J,0,1)else return end;(((p.new(function(g,Q)local p,H=pcall(function()O[r]=J end)if p then g(p)else Q(H)end end)):andThenCall(K,g,O)):doneCall(o,g,"\115\099\097\108\101",r,J)):catch(warn)end local function R(g,r)assert(typeof(r)=="\115\116\114\105\110\103")if r==""then return end r=string.gsub(r,"\091\037\112\037\115\093\043","")local J=y[g]if not J then return end if#J.Costumes>=W.MAX_COSTUME then return end local Q=E(g)if not Q then return end local O=Q:GetAppliedDescription()if not O then return end local p=i:ToAppearanceInfo(O);((a(r,g)):andThen(function(r)p.Name=r local Q=i:ToHumanoidDescription(p)Q.Parent=J.Folder table.insert(J.Costumes,p)local H=#J.Costumes J.Map[O]=H i.CostumeAdded:Fire(g,Q,p,H)end)):catch(warn)end local function w(g,r)assert(typeof(r)=="\073\110\115\116\097\110\099\101"and r:IsA("\072\117\109\097\110\111\105\100\068\101\115\099\114\105\112\116\105\111\110"))local J=y[g]if not J then return end local Q=E(g)if not Q then return end if r:IsDescendantOf(J.Folder)then local Q=J.Map[r]J.Map[r]=nil r:Destroy()i.CostumeRemoved:Fire(g,r,J.Costumes[Q],Q)end end


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local Text = game:GetService("TextService")

local AvatarEditor = ReplicatedStorage.AvatarEditor
local remoteEvent = AvatarEditor.RemoteEvent
local Promise = require(AvatarEditor.Shared.Util.Promise)
local Maid = require(AvatarEditor.Shared.Util.Maid)
local Signal = require(AvatarEditor.Shared.Util.Signal)
local TableUtil = require(AvatarEditor.Shared.Util.TableUtil)
local QueryMap = require(AvatarEditor.Shared.QueryMap)
local Colors = require(AvatarEditor.Shared.Colors)
local Settings = require(AvatarEditor.Shared.Settings)
local GetProductInfo = require(AvatarEditor.Shared.GetProductInfo)
local UserCanUse = require(AvatarEditor.Shared.UserCanUse)

local outfitCache = {}
local settingsCache = {}

local module = {}
module.HumanoidDescriptionChanged = Signal.new()
module.CostumeAdded = Signal.new()
module.CostumeRemoved = Signal.new()
module.CostumeUpdated = Signal.new() -- TODO update outfit
module.SettingChanged = Signal.new()
module.PermissionFailed = Signal.new()


local function filterText(text, player)
	return Promise.new(function(resolve, reject)
		local success, result = pcall(function()
			return Text:FilterStringAsync(text, player.UserId)
		end)
		if success then
			resolve(result)
		else
			reject(result)
		end
	end)
	:andThen(function(textFilterResult)
		return Promise.new(function(resolve, reject)
			local success, result = pcall(function()
				-- hopefully this appeases the roblox gods
				return textFilterResult:GetNonChatStringForUserAsync(player.UserId)
			end)
			if success then
				resolve(result)
			else
				reject(result)
			end
		end)
	end)
end


local function getHumanoid(player)
	local character = player.Character
	local humanoid = character and character:FindFirstChildWhichIsA("Humanoid")
	if humanoid and humanoid.Health > 0 then
		return humanoid
	end
end


local function getHumanoidDescriptionFromUserId(userId)
	return Promise.new(function(resolve, reject)
		local success, result = pcall(function()
			return Players:GetHumanoidDescriptionFromUserId(userId)
		end)
		if success then
			resolve(result)
		else
			reject(result)
		end
	end)
end


local function applyDescriptionToHumanoid(humanoid, description)
	return Promise.new(function(resolve, reject)
		local success, result = pcall(function()
			humanoid:ApplyDescription(description)
		end)
		if success then
			resolve(success)
		else
			reject(result)
		end
	end)
end


local function applyDescriptionToPlayer(player, description)
	module.HumanoidDescriptionChanged:Fire(player, description)
	
	local humanoid = getHumanoid(player)
	if not humanoid then
		return Promise.reject()
	end
	
	return applyDescriptionToHumanoid(humanoid, description)
end


local function descriptionUpdated(player, ...)
	remoteEvent:FireClient(player, ...)
end


local function accessory(player, id)
	assert(typeof(id) == "number")
	
	local humanoid = getHumanoid(player)
	if not humanoid then
		return
	end

	local description = humanoid:GetAppliedDescription()
	if not description then
		return
	end
	
	GetProductInfo(id)
	:andThen(function(productInfo)
		local assetType = QueryMap.GetOptionName("AssetType", productInfo.AssetTypeId)
		if not assetType then
			return
		end
		
		-- really Roblox...??
		if assetType == "Hat" then
			assetType = "HatAccessory"
		elseif assetType == "ShoulderAccessory" then
			assetType = "ShouldersAccessory"
		end
		
		if string.find(assetType, "Accessory") then
			local accessories = string.split(description[assetType], ",")
			local index = table.find(accessories, tostring(productInfo.AssetId))
			if index then
				table.remove(accessories, index)
			elseif #humanoid:GetAccessories() < Settings.MAX_ACCESSORIES then
				table.insert(accessories, tostring(productInfo.AssetId))
			else
				-- TODO should probably notify the client
			end
			description[assetType] = table.concat(accessories, ",")
		else
			if description[assetType] == 0 or (description[assetType] ~= productInfo.AssetId) then
				description[assetType] = productInfo.AssetId
			else
				-- https://devforum.roblox.com/t/default-shirt-and-pants-to-be-applied-if-the-requested-ones-are-unavailable-moderated/381818
				-- force load default clothes. hacky
				-- hopefully there will never be a shirt or pants with id = 1 or 2
				description[assetType] = (assetType == "Shirt") and 1 or (assetType == "Pants") and 2 or 0
			end
		end
		
		applyDescriptionToPlayer(player, description)
		:andThenCall(descriptionUpdated, player, "accessory")
	end)
	:catch(warn)
end

local function tone(player, index)
	assert(typeof(index) == "number")

	local humanoid = getHumanoid(player)
	if not humanoid then
		return
	end

	local description = humanoid:GetAppliedDescription()
	if not description then
		return
	end

	index = math.clamp(index, 1, 75)

	local color = Colors[index]
	if color then
		description.HeadColor = color
		description.LeftArmColor = color
		description.LeftLegColor = color
		description.RightArmColor = color
		description.RightLegColor = color
		description.TorsoColor = color
	end

	applyDescriptionToPlayer(player, description)
	:doneCall(descriptionUpdated, player, "tone")
	:catch(warn)
end

local function reset(player)
	local humanoid = getHumanoid(player)
	if not humanoid then
		return
	end
	
	getHumanoidDescriptionFromUserId(player.UserId)
	:andThen(function(description)
		return applyDescriptionToPlayer(player, description)
	end)
	:doneCall(descriptionUpdated, player, "reset")
	:catch(warn)
end

local function scale(player, scaleName, value)
	assert(typeof(scaleName) == "string")
	assert(typeof(value) == "number")

	local humanoid = getHumanoid(player)
	if not humanoid then
		return
	end

	local description = humanoid:GetAppliedDescription()
	if not description then
		return
	end

	if scaleName == "HeightScale" then
		value = math.clamp(value, 0.9, 1.05)
	elseif scaleName == "WidthScale" then
		value = math.clamp(value, 0.7, 1)
	elseif scaleName == "HeadScale" then
		value = math.clamp(value, 0.95, 1)
	elseif scaleName == "ProportionScale" then
		value = math.clamp(value, 0, 1)
	elseif scaleName == "BodyTypeScale" then
		value = math.clamp(value, 0, 1)
	else
		return
	end

	Promise.new(function(resolve, reject)
		local success, result = pcall(function()
			description[scaleName] = value
		end)
		if success then
			resolve(success)
		else
			reject(result)
		end
	end)
	:andThenCall(applyDescriptionToPlayer, player, description)
	:doneCall(descriptionUpdated, player, "scale", scaleName, value)
	:catch(warn)
end


local function create(player, name)
	assert(typeof(name) == "string")
	
	if name == "" then
		return
	end
	name = string.gsub(name, "[%p%s]+", "")
	
	local cache = outfitCache[player]
	if not cache then
		return
	end
	
	if #cache.Costumes >= Settings.MAX_COSTUME then
		return
	end
	
	local humanoid = getHumanoid(player)
	if not humanoid then
		return
	end

	local description = humanoid:GetAppliedDescription()
	if not description then
		return
	end
	
	local info = module:ToAppearanceInfo(description)
	filterText(name, player)
	:andThen(function(result)
		info.Name = result
		
		-- converting it back to a HumanoidDescription for the client to use the filtered name
		local descriptionCopy = module:ToHumanoidDescription(info)
		descriptionCopy.Parent = cache.Folder
		table.insert(cache.Costumes, info)
		local index = #cache.Costumes
		cache.Map[description] = index
		
		module.CostumeAdded:Fire(player, descriptionCopy, info, index)
	end)
	:catch(warn)
end


local function delete(player, description)
	assert(typeof(description) == "Instance" and description:IsA("HumanoidDescription"))
	
	local cache = outfitCache[player]
	if not cache then
		return
	end

	local humanoid = getHumanoid(player)
	if not humanoid then
		return
	end
	
	if description:IsDescendantOf(cache.Folder) then
		local index = cache.Map[description]
		cache.Map[description] = nil
		description:Destroy()
		module.CostumeRemoved:Fire(player, description, cache.Costumes[index], index)
	end
end

