--[[



]]

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