-- Current UI layout
local CurrentLayout;

function ChangeLayout(Layout)
	-- Sets the UI to the given layout

	-- Make sure the new layout isn't already set
	if CurrentLayout == Layout then
		return;
	end;

	-- Set this as the current layout
	CurrentLayout = Layout;

	-- Reset the UI
	for _, ElementName in pairs(UIElements) do
		local Element = UI[ElementName];
		Element.Visible = false;
	end;

	-- Keep track of the total vertical extents of all items
	local Sum = 0;

	-- Go through each layout element
	for ItemIndex, ItemName in ipairs(Layout) do

		local Item = UI[ItemName];

		-- Make the item visible
		Item.Visible = true;

		-- Position this item underneath the past items
		Item.Position = UDim2.new(0, 0, 0, 20) + UDim2.new(
			Item.Position.X.Scale,
			Item.Position.X.Offset,
			0,
			Sum + 10
		);

		-- Update the sum of item heights
		Sum = Sum + 10 + Item.AbsoluteSize.Y;

	end;

	-- Resize the container to fit the new layout
	UI.Size = UDim2.new(0, 200, 0, 30 + Sum);

end;

function UpdateUI()
	-- Updates information on the UI

	-- Make sure the UI's on
	if not UI then
		return;
	end;

	-- Get the textures in the selection
	local Textures = GetTextures(TextureTool.Type, TextureTool.Face);

	-- References to UI elements
	local ImageIdInput = UI.ImageIDOption.TextBox;
	local TransparencyInput = UI.TransparencyOption.Input.TextBox;

	-----------------------
	-- Update the UI layout
	-----------------------

	-- Get the plural version of the current texture type
	local PluralTextureType = TextureTool.Type .. 's';

	-- Figure out the necessary UI layout
	if #Selection.Items == 0 then
		ChangeLayout(Layouts.EmptySelection);
		return;

	-- When the selection has no textures
	elseif #Textures == 0 then
		ChangeLayout(Layouts.NoTextures);
		return;

	-- When only some selected items have textures
	elseif #Selection.Items ~= #Textures then
		ChangeLayout(Layouts['Some' .. PluralTextureType]);

	-- When all selected items have textures
	elseif #Selection.Items == #Textures then
		ChangeLayout(Layouts['All' .. PluralTextureType]);
	end;

	------------------------
	-- Update UI information
	------------------------

	-- Get the common properties
	local ImageId = Support.IdentifyCommonProperty(Textures, 'Texture');
	local Transparency = Support.IdentifyCommonProperty(Textures, 'Transparency');

	-- Update the common inputs
	UpdateDataInputs {
		[ImageIdInput] = ImageId and ParseAssetId(ImageId) or ImageId or '*';
		[TransparencyInput] = Transparency and Support.Round(Transparency, 3) or '*';
	};

	-- Update texture-specific information on UI
	if TextureTool.Type == 'Texture' then

		-- Get texture-specific UI elements
		local RepeatXInput = UI.RepeatOption.XInput.TextBox;
		local RepeatYInput = UI.RepeatOption.YInput.TextBox;

		-- Get texture-specific common properties
		local RepeatX = Support.IdentifyCommonProperty(Textures, 'StudsPerTileU');
		local RepeatY = Support.IdentifyCommonProperty(Textures, 'StudsPerTileV');

		-- Update inputs
		UpdateDataInputs {
			[RepeatXInput] = RepeatX and Support.Round(RepeatX, 3) or '*';
			[RepeatYInput] = RepeatY and Support.Round(RepeatY, 3) or '*';
		};

	end;

end;

function UpdateDataInputs(Data)
	-- Updates the data in the given TextBoxes when the user isn't typing in them

	-- Go through the inputs and data
	for Input, UpdatedValue in pairs(Data) do

		-- Makwe sure the user isn't typing into the input
		if not Input:IsFocused() then

			-- Set the input's value
			Input.Text = tostring(UpdatedValue);

		end;

	end;

end;

function ParseAssetId(Input)
	-- Returns the intended asset ID for the given input

	-- Get the ID number from the input
	local Id = tonumber(Input)
		or tonumber(Input:lower():match('%?id=([0-9]+)'))
		or tonumber(Input:match('/([0-9]+)/'))
		or tonumber(Input:lower():match('rbxassetid://([0-9]+)'));

	-- Return the ID
	return Id;
end;

function SetFace(Face)

	-- Update the tool option
	TextureTool.Face = Face;

	-- Update the UI
	FaceDropdown.SetOption(Face and Face.Name or '*');

end;

function SetTextureType(TextureType)

	-- Update the tool option
	TextureTool.Type = TextureType;

	-- Update the UI
	Core.ToggleSwitch(TextureType, UI.ModeOption);
	UI.AddButton.Button.Text = 'ADD ' .. TextureType:upper();
	UI.RemoveButton.Button.Text = 'REMOVE ' .. TextureType:upper();

end;

function SetProperty(TextureType, Face, Property, Value)

	-- Make sure the given value is valid
	if not Value then
		return;
	end;

	-- Start a history record
	TrackChange();

	-- Go through each texture
	for _, Texture in pairs(GetTextures(TextureType, Face)) do

		-- Store the state of the texture before modification
		table.insert(HistoryRecord.Before, { Part = Texture.Parent, TextureType = TextureType, Face = Face, [Property] = Texture[Property] });

		-- Create the change request for this texture
		table.insert(HistoryRecord.After, { Part = Texture.Parent, TextureType = TextureType, Face = Face, [Property] = Value });

	end;

	-- Register the changes
	RegisterChange();

end;

function SetTextureId(TextureType, Face, AssetId)
	-- Sets the textures in the selection to the intended, given image asset

	-- Make sure the given asset ID is valid
	if not AssetId then
		return;
	end;

	-- Prepare the change request
	local Changes = {
		Texture = 'rbxassetid://' .. AssetId;
	};

	-- Attempt an image extraction on the given asset
	Core.Try(Core.SyncAPI.Invoke, Core.SyncAPI, 'ExtractImageFromDecal', AssetId)
		:Then(function (ExtractedImage)
			Changes.Texture = 'rbxassetid://' .. ExtractedImage;
		end);

	-- Start a history record
	TrackChange();

	-- Go through each texture
	for _, Texture in pairs(GetTextures(TextureType, Face)) do

		-- Create the history change requests for this texture
		local Before, After = { Part = Texture.Parent, TextureType = TextureType, Face = Face }, { Part = Texture.Parent, TextureType = TextureType, Face = Face };

		-- Gather change information to finish up the history change requests
		for Property, Value in pairs(Changes) do
			Before[Property] = Texture[Property];
			After[Property] = Value;
		end;

		-- Store the state of the texture before modification
		table.insert(HistoryRecord.Before, Before);

		-- Create the change request for this texture
		table.insert(HistoryRecord.After, After);

	end;

	-- Register the changes
	RegisterChange();

end;

function AddTextures(TextureType, Face)

	-- Prepare the change request for the server
	local Changes = {};

	-- Go through the selection
	for _, Part in pairs(Selection.Items) do

		-- Make sure this part doesn't already have a texture of the same type
		local HasTextures;
		for _, Child in pairs(Part:GetChildren()) do
			if Child.ClassName == TextureType and Child.Face == Face then
				HasTextures = true;
			end;
		end;

		-- Queue a texture to be created for this part, if not already existent
		if not HasTextures then
			table.insert(Changes, { Part = Part, TextureType = TextureType, Face = Face });
		end;

	end;

	-- Send the change request to the server
	local Textures = Core.SyncAPI:Invoke('CreateTextures', Changes);

	-- Put together the history record
	local HistoryRecord = {
		Textures = Textures;

		Unapply = function (HistoryRecord)
			-- Reverts this change

			-- Select changed parts
			Selection.Replace(Support.GetListMembers(HistoryRecord.Textures, 'Parent'));

			-- Remove the textures
			Core.SyncAPI:Invoke('Remove', HistoryRecord.Textures);

		end;

		Apply = function (HistoryRecord)
			-- Reapplies this change

			-- Restore the textures
			Core.SyncAPI:Invoke('UndoRemove', HistoryRecord.Textures);

			-- Select changed parts
			Selection.Replace(Support.GetListMembers(HistoryRecord.Textures, 'Parent'));

		end;

	};

	-- Register the history record
	Core.History.Add(HistoryRecord);

end;

function RemoveTextures(TextureType, Face)

	-- Get all the textures in the selection
	local Textures = GetTextures(TextureType, Face);

	-- Create the history record
	local HistoryRecord = {
		Textures = Textures;

		Unapply = function (HistoryRecord)
			-- Reverts this change

			-- Restore the textures
			Core.SyncAPI:Invoke('UndoRemove', HistoryRecord.Textures);

			-- Select changed parts
			Selection.Replace(Support.GetListMembers(HistoryRecord.Textures, 'Parent'));

		end;

		Apply = function (HistoryRecord)
			-- Reapplies this change

			-- Select changed parts
			Selection.Replace(Support.GetListMembers(HistoryRecord.Textures, 'Parent'));

			-- Remove the textures
			Core.SyncAPI:Invoke('Remove', HistoryRecord.Textures);

		end;

	};

	-- Send the removal request
	Core.SyncAPI:Invoke('Remove', Textures);

	-- Register the history record
	Core.History.Add(HistoryRecord);

end;

function TrackChange()

	-- Start the record
	HistoryRecord = {
		Before = {};
		After = {};

		Unapply = function (Record)
			-- Reverts this change

			-- Select the changed parts
			Selection.Replace(Support.GetListMembers(Record.Before, 'Part'));

			-- Send the change request
			Core.SyncAPI:Invoke('SyncTexture', Record.Before);

		end;

		Apply = function (Record)
			-- Applies this change

			-- Select the changed parts
			Selection.Replace(Support.GetListMembers(Record.After, 'Part'));

			-- Send the change request
			Core.SyncAPI:Invoke('SyncTexture', Record.After);

		end;

	};

end;

function RegisterChange()
	-- Finishes creating the history record and registers it

	-- Make sure there's an in-progress history record
	if not HistoryRecord then
		return;
	end;

	-- Send the change to the server
	Core.SyncAPI:Invoke('SyncTexture', HistoryRecord.After);

	-- Register the record and clear the staging
	Core.History.Add(HistoryRecord);
	HistoryRecord = nil;

end;