if SERVER then return end --prevents it from running on the server

local path_to_loaded_file = nil
local has_load_completed_array = {
	["clear_components_complete"] = false,
	["clear_IO_label_complete"] = false,
	["clear_IO_position_complete"] = false,
	["add_components_complete"] = false,
	["add_wires_complete"] = false,
	["add_labels_complete"] = false,
	["rename_labels_complete"] = false,
	["change_input_output_labels_complete"] = false,
	["update_values_in_components_complete"] = false,
	["resize_labels_complete"] = false,
	["move_input_output_nodes_complete"] = false
}



function blue_prints.parseXML(xmlString)

	-- Define HTML entities for backwards compatibility
    local html_entities = {
        ['&quot;'] = '"',
        ['&amp;'] = '&',
        ['&lt;'] = '<',
        ['&gt;'] = '>',
        ['&nbsp;'] = ' ',
        ['&iexcl;'] = '¡',
        ['&cent;'] = '¢',
        ['&pound;'] = '£',
        ['&curren;'] = '¤',
        ['&yen;'] = '¥',
        ['&brvbar;'] = '¦',
        ['&sect;'] = '§',
        ['&uml;'] = '¨',
        ['&copy;'] = '©',
        ['&ordf;'] = 'ª',
        ['&laquo;'] = '«',
        ['&not;'] = '¬',
        ['&shy;'] = '­',
        ['&reg;'] = '®',
        ['&macr;'] = '¯',
        ['&deg;'] = '°',
        ['&plusmn;'] = '±',
        ['&sup2;'] = '²',
        ['&sup3;'] = '³',
        ['&acute;'] = '´',
        ['&micro;'] = 'µ',
        ['&para;'] = '¶',
        ['&middot;'] = '·',
        ['&cedil;'] = '¸',
        ['&sup1;'] = '¹',
        ['&ordm;'] = 'º',
        ['&raquo;'] = '»',
        ['&frac14;'] = '¼',
        ['&frac12;'] = '½',
        ['&frac34;'] = '¾',
        ['&iquest;'] = '¿',
        ['&#xA;'] = '\n',
        ['&#xD;'] = '\n'
    }

	local function contains_html_entities(str)
        return str and str:find('&[^;]+;')
    end

    local function decode_html_entities(str)
        if not str or not contains_html_entities(str) then
            return str
        end
        return (str:gsub('(&[^;]+;)', function(entity)
            return html_entities[entity] or entity
        end))
    end

    -- Function to decode a string that might be in either format
    local function decode_string(str)
		if not str then return "" end
        
        -- First try new format
        local delimited_content = str:match("<<<STRINGSTART>>>(.-)<<<STRINGEND>>>")
        if delimited_content then
            -- Unescape any escaped delimiters
            return delimited_content:gsub("\\<<<STRINGSTART>>>", "<<<STRINGSTART>>>")
                                  :gsub("\\<<<STRINGEND>>>", "<<<STRINGEND>>>")
        end
        
        -- If not in new format, try old format with HTML entities
        return decode_html_entities(str)
    end

    local inputs = {}
    local outputs = {}
    local components = {}
    local wires = {}
    local labels = {}
    local inputNodePos = {}
    local outputNodePos = {}

    -- Parse InputNode
    local inputNode = xmlString:match("<InputNode[^>]+>")
    if inputNode then
        local posX, posY = inputNode:match('pos="([%d%.%-]+),([%d%.%-]+)"')
        inputNodePos = { x = tonumber(posX), y = tonumber(posY) }
    end

    -- Parse OutputNode
    local outputNode = xmlString:match("<OutputNode[^>]+>")
    if outputNode then
        local posX, posY = outputNode:match('pos="([%d%.%-]+),([%d%.%-]+)"')
        outputNodePos = { x = tonumber(posX), y = tonumber(posY) }
    end

    -- Parse input and output labels (if any)
    for name, value in xmlString:gmatch('<ConnectionLabelOverride name="([^"]+)" value="([^"]+)"') do
        if name:match("^signal_in") then
            inputs[name] = decode_string(value)
        elseif name:match("^signal_out") then
            outputs[name] = decode_string(value)
        end
    end
    
    -- Parse Components
    for component in xmlString:gmatch('<Component.-/>') do

        local id = component:match('id="(%d+)"')
        local positionX, positionY = component:match('position="([%-%d%.]+),([%-%d%.]+)"')
        local usedResource = component:match('usedresource="([^"]+)"')

		--try both formats for backwards compatibility

		local item = component:match('item=<<<STRINGSTART>>>([^<]+)<<<STRINGEND>>>')
		if not item or item == "" then
			item = component:match('%sitem="([^"]+)"')
		end
		
		local class = component:match('Class=<<<STRINGSTART>>>([^<]+)<<<STRINGEND>>>')
		if not class or class == "" then
			class = component:match('%sClass="([^"]+)"')
		end

        local componentData = {
            id = id,
            position = { x = tonumber(positionX), y = tonumber(positionY) },
            usedResource = usedResource,
            item = item,
            class = {
                name = class,
                attributes = {}
            }
        }

        -- Parse attributes in both new and old formats
        -- First try new format
        for attr, value in component:gmatch('(%w+)=<<<STRINGSTART>>>(.-)<<<STRINGEND>>>') do
            if attr ~= "id" and attr ~= "position" and attr ~= "backingitemid" and
                attr ~= "usedresource" and attr ~= "item" and attr ~= "Class" then
                componentData.class.attributes[attr] = decode_string(value)
            end
        end

        -- If no attributes found in new format, try old format
        if next(componentData.class.attributes) == nil then
            for attr, value in component:gmatch('(%w+)="(.-)"') do
                if attr ~= "id" and attr ~= "position" and attr ~= "backingitemid" and
                    attr ~= "usedresource" and attr ~= "item" and attr ~= "Class" then
                    componentData.class.attributes[attr] = decode_html_entities(value)
                end
            end
        end

        table.insert(components, componentData)
    end

    -- Parse Wires
    wires = {}
    for wire in xmlString:gmatch("<Wire.-</Wire>") do
        local wireData = {
            id = wire:match('id="(%d+)"'),
            prefab = wire:match('prefab="([^"]+)"')
        }

        local fromName, fromTarget = wire:match('<From name="([^"]+)" target="([^"]*)"')
        local toName, toTarget = wire:match('<To name="([^"]+)" target="([^"]*)"')

        wireData.from = { name = fromName, target = fromTarget ~= "" and fromTarget or nil }
        wireData.to = { name = toName, target = toTarget ~= "" and toTarget or nil }

        table.insert(wires, wireData)
    end

    -- Parse Labels with correct pattern matching
    for label in xmlString:gmatch('<Label.-/>') do
        -- Extract all attributes using individual pattern matches
        local id = label:match('id="(%d+)"')
        local color = label:match('color="([^"]+)"')
        local posX, posY = label:match('position="([%d%.%-]+),([%d%.%-]+)"')
        local sizeW, sizeH = label:match('size="([%d%.%-]+),([%d%.%-]+)"')

        -- Use decode_string for both header and body
        local header = decode_string(label:match('header="(.-)"'))
        local body = decode_string(label:match('body="(.-)"'))

        table.insert(labels, {
            id = id,
            color = color,
            position = { x = tonumber(posX), y = tonumber(posY) },
            size = { width = tonumber(sizeW), height = tonumber(sizeH) },
            header = header,
            body = body
        })
    end

    blue_prints.add_advertisement_label(components, labels, inputNodePos, outputNodePos) --if there is no ad, add one

	--[[
	-- Add this right before the return statement in blue_prints.parseXML:
	print("Parsed Components:")
	for i, component in ipairs(components) do
		print(string.format("Component %d:", i))
		print(string.format("  ID: %s", component.id))
		print(string.format("  Position: (%.2f, %.2f)", component.position.x, component.position.y))
		print(string.format("  Item: %s", component.item))
		print(string.format("  Class: %s", component.class.name))
		print("  Attributes:")
		for attr, value in pairs(component.class.attributes) do
			print(string.format("    %s: %s", attr, value))
		end
		print("----------")
	end
	--]]

    return inputs, outputs, components, wires, labels, inputNodePos, outputNodePos
end




function blue_prints.check_inventory_for_requirements(components)
	--print("Game mode: ", Game.IsSubEditor)

	local missing_components = {}

	if Game.IsSubEditor then return missing_components end --if in sub editor, you dont have to worry about inventory

	for _, component in ipairs(components) do
		missing_components[component.item] = (missing_components[component.item] or 0) + 1
	end
	local character = Character.Controlled

	local fpgacircuit_count = 0

	--count inventory items already stored inside the box
	if blue_prints.most_recent_circuitbox ~= nil then
		local items_already_in_box = blue_prints.get_components_currently_in_circuitbox(blue_prints
		.most_recent_circuitbox)

		for resource, count in pairs(items_already_in_box) do
			local identifier = resource
			if identifier == "fpgacircuit" then
				fpgacircuit_count = fpgacircuit_count + count
			elseif missing_components[identifier] and missing_components[identifier] > 0 then
				missing_components[identifier] = missing_components[identifier] - count
			end
		end
	end



	-- First pass: count regular items and fpgacircuits
	for inventory_item in character.Inventory.AllItems do
		local identifier = tostring(inventory_item.Prefab.Identifier)
		if identifier == "fpgacircuit" then
			fpgacircuit_count = fpgacircuit_count + 1
		elseif missing_components[identifier] and missing_components[identifier] > 0 then
			missing_components[identifier] = missing_components[identifier] - 1
		end
	end

	-- Second pass: use fpgacircuits as wildcards only for remaining missing items
	for identifier, count in pairs(missing_components) do
		if count > 0 and fpgacircuit_count > 0 then
			local used_fpga = math.min(count, fpgacircuit_count)
			missing_components[identifier] = count - used_fpga
			fpgacircuit_count = fpgacircuit_count - used_fpga
		end
	end

	-- Remove components that are not missing
	for identifier, count in pairs(missing_components) do
		if count <= 0 then
			missing_components[identifier] = nil
		end
	end

	return missing_components
end

function blue_prints.add_component_to_circuitbox(component, use_fpga)
	if blue_prints.most_recent_circuitbox == nil then
		print("No circuitbox detected")
		return
	end


	--due to refactoring I no longer need this, but will keep it for historical compatability with older blueprints
	if component.item == "oscillatorcomponent" then component.item = "oscillator" end --these components are named strangely and break convention
	if component.item == "concatenationcomponent" then component.item = "concatcomponent" end
	if component.item == "exponentiationcomponent" then component.item = "powcomponent" end
	if component.item == "regexfind" then component.item = "regexcomponent" end
	if component.item == "signalcheck" then component.item = "signalcheckcomponent" end
	if component.item == "squareroot" then component.item = "squarerootcomponent" end


	local item_to_add = use_fpga and ItemPrefab.GetItemPrefab("fpgacircuit") or ItemPrefab.GetItemPrefab(component.item)
	local component_position = Vector2(component.position.x, component.position.y)
	--print(tostring(component))
	--print(item_to_add)
	--print(blue_prints.most_recent_circuitbox)
	--print(blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox"))
	--print(item_to_add)
	--print(component.name)
	--print(component_position)
	--print(component.item)

	if item_to_add == nil then
        print("Error: Invalid component data")
        return false
    end

	blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox").AddComponent(item_to_add, component_position)
	--print(string.format("Added component %s at position (%.2f, %.2f)", use_fpga and "fpgacircuit" or component.item, component_position.x, component_position.y))
end

function blue_prints.add_all_components_to_circuitbox(components, index, inventory_status)
	if blue_prints.most_recent_circuitbox == nil then
		print("No circuitbox detected")
		return
	end

	index = index or 1 -- Start with the first component if no index is provided
	inventory_status = inventory_status or blue_prints.check_inventory_for_requirements(components)

	if index <= #components then
		local component = components[index]
		local use_fpga = inventory_status[component.item] and inventory_status[component.item] > 0

		blue_prints.add_component_to_circuitbox(component, use_fpga)

		if use_fpga then
			inventory_status[component.item] = inventory_status[component.item] - 1
			if inventory_status[component.item] == 0 then
				inventory_status[component.item] = nil
			end
		end

		-- Schedule the next addition with a delay
		Timer.Wait(function() blue_prints.add_all_components_to_circuitbox(components, index + 1, inventory_status) end,
			blue_prints.time_delay_between_loops)
	else
		-- If all components are added, print a message
		--print("All components added.")
		has_load_completed_array["add_components_complete"] = true
	end
end

function blue_prints.add_wires_to_circuitbox_recursive(wires, index)
	if blue_prints.most_recent_circuitbox == nil then
		print("no circuitbox detected")
		return
	end

	local first_connection = nil
	local second_connection = nil

	local components = blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox").Components
	local input_output_nodes = blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox").InputOutputNodes
	local input_connection_node = blue_prints.getNthValue(input_output_nodes, 1)
	local input_connections = input_connection_node.Connectors
	local output_connection_node = blue_prints.getNthValue(input_output_nodes, 2)
	local output_connections = output_connection_node.Connectors

	if index > #wires then
		--print("All wires added.")
		has_load_completed_array["add_wires_complete"] = true
		return
	end

	local wire = wires[index]

	--select wire color
	local wire_prefab = ItemPrefab.GetItemPrefab(tostring(wire.prefab))
	LuaUserData.MakeMethodAccessible(Descriptors["Barotrauma.CircuitBoxUI"], "SelectWire")
	local circuitbox_ui = blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox").UI.SelectWire(nil,
		wire_prefab)

	for component_key, component_value in pairs(components) do
		local connectors = component_value.Connectors

		for i = 0, connectors.length - 1 do
			if tostring(component_value.ID) == tostring(wire.from.target) and tostring(connectors[i].name) == tostring(wire.from.name) then
				first_connection = connectors[i]
			end

			if tostring(component_value.ID) == tostring(wire.to.target) and tostring(connectors[i].name) == tostring(wire.to.name) then
				second_connection = connectors[i]
			end
		end
	end


	if wire.from.target == nil then
		local input_to_target = string.match(tostring(wire.from.name), "%d+")
		first_connection = input_connections[tonumber(input_to_target) - 1]
	end

	if wire.to.target == nil then
		local input_to_target = string.match(tostring(wire.to.name), "%d+")
		second_connection = output_connections[tonumber(input_to_target) - 1]
	end

	if first_connection and second_connection then
		blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox").AddWire(first_connection, second_connection)
	end

	-- Recur to the next wire
	Timer.Wait(function() blue_prints.add_wires_to_circuitbox_recursive(wires, index + 1) end,
		blue_prints.time_delay_between_loops)
end

-- Check if labels have been reset
local function check_labels_changed(input_dict, output_dict)
	local input_output_nodes = blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox").InputOutputNodes
	local input_connection_node = blue_prints.getNthValue(input_output_nodes, 1)
	local output_connection_node = blue_prints.getNthValue(input_output_nodes, 2)

	-- Check if any actual inputs/outputs differ from expected
	for key, value in pairs(input_connection_node.ConnectionLabelOverrides) do
		if value ~= (input_dict[key] or "") then
			return false
		end
	end

	for key, value in pairs(output_connection_node.ConnectionLabelOverrides) do
		if value ~= (output_dict[key] or "") then
			return false
		end
	end

	return true
end


function blue_prints.change_input_output_labels(input_dict, output_dict, recursion_count)
	if blue_prints.most_recent_circuitbox == nil then
		print("no circuitbox detected")
		return
	end

	local input_output_nodes = blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox").InputOutputNodes
	local input_connection_node = blue_prints.getNthValue(input_output_nodes, 1)
	local output_connection_node = blue_prints.getNthValue(input_output_nodes, 2)

	blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox").SetConnectionLabelOverrides(
	input_connection_node, input_dict)
	blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox").SetConnectionLabelOverrides(
	output_connection_node, output_dict)

	if check_labels_changed(input_dict, output_dict) then
		--This function gets called twice, once to clear the box, one to change the IO
		if has_load_completed_array["clear_IO_label_complete"] == false then
			has_load_completed_array["clear_IO_label_complete"] = true
		else 
			has_load_completed_array["change_input_output_labels_complete"] = true
		end
	else
		if recursion_count < 5 then
			Timer.Wait(function() blue_prints.change_input_output_labels(input_dict, output_dict, recursion_count + 1) end,
			blue_prints.time_delay_between_loops)
		else
			print("Error: Failed to change input/output labels")
		end
	end

end

function blue_prints.add_advertisement_label(components, labels, inputNodePos, outputNodePos)
	if blue_prints.most_recent_circuitbox == nil then
		print("no circuitbox detected")
		return
	end

	--just add an extra label to labels table thats to the right of the rightmost component or label
	--dont do this if there is already an advertisement label
	advertisement_header = "Blueprints"

	for _, label in ipairs(labels) do --if there is already an advertisement, dont add another
		if label.header and label.header == advertisement_header then
			return
		end
	end


	local furthestRight = blue_prints.getFurthestRightElement(components, labels, inputNodePos, outputNodePos)

	--print("Furthest right element type:", furthestRight.type)
	--print("Furthest right x-coordinate:", furthestRight.x)
	--print("Furthest right y-coordinate:", furthestRight.y)


	local function addManualLabel(id, color, posX, posY, sizeW, sizeH, header, body)
		table.insert(labels, {
			id = id,
			color = color,
			position = { x = tonumber(posX), y = tonumber(posY) },
			size = { width = tonumber(sizeW), height = tonumber(sizeH) },
			header = header,
			body = body
		})
	end

	addManualLabel(#labels, "#0082FF", furthestRight.x + 384, furthestRight.y, 512, 256, advertisement_header,
		'Circuit made with Blueprints. \n \n Get it now on the steam workshop!')
end

function blue_prints.add_labels_to_circuitbox_recursive(labels, index)
	if blue_prints.most_recent_circuitbox == nil then
		print("no circuitbox detected")
		return
	end

	-- Base case: if we've processed all labels, return
	if index > #labels then
		has_load_completed_array["add_labels_complete"] = true
		return
	end

	-- Process the current label
	local label = labels[index]
	local x_offset_for_resizing = label.position.x - (label.size.width / 2) + 128
	local y_offset_for_resizing = label.position.y + (label.size.height / 2) - 128

	local label_position = Vector2(x_offset_for_resizing, y_offset_for_resizing)
	blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox").AddLabel(label_position)

	-- Recursive call to process the next label
	Timer.Wait(function() blue_prints.add_labels_to_circuitbox_recursive(labels, index + 1) end,
		blue_prints.time_delay_between_loops)
end

function blue_prints.rename_all_labels_in_circuitbox(labels)
	if blue_prints.most_recent_circuitbox == nil then
		print("no circuitbox detected")
		return
	end

	local label_nodes = blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox").Labels

	for i, label in ipairs(labels) do
		--print(string.format("  Label %d:", i))
		--print(string.format("    ID: %s", label.id))
		--print(string.format("    Color: %s", label.color))
		--print(string.format("    Position: (%.2f, %.2f)", label.position.x, label.position.y))
		--print(string.format("    Size: %.2f x %.2f", label.size.width, label.size.height))
		--print(string.format("    Header: %s", label.header))
		--print(string.format("    Body: %s", label.body))

		if label.header == nil then label.header = "" end
		if label.body == nil then label.body = "" end

		local label_node = blue_prints.getNthValue(label_nodes, i)
		local label_header = blue_prints.net_limited_string_type(tostring(label.header))
		local label_body = blue_prints.net_limited_string_type(tostring(label.body))
		local label_color = blue_prints.hexToRGBA(label.color)

		blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox").RenameLabel(label_node, label_color,
			label_header, label_body)
	end

	--print("All labels added.")
	has_load_completed_array["rename_labels_complete"] = true
end

local function resize_label(label_node, direction, resize_vector)
	if blue_prints.most_recent_circuitbox == nil then
		print("no circuitbox detected")
		return
	end

	blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox").ResizeNode(label_node, direction, resize_vector)
end


function blue_prints.resize_labels(labels_from_blueprint)
	if blue_prints.most_recent_circuitbox == nil then
		print("no circuitbox detected")
		return
	end

	local label_nodes_in_box = blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox").Labels

	for i, label_in_blueprint in ipairs(labels_from_blueprint) do
		local label_node = blue_prints.getNthValue(label_nodes_in_box, i)

		local amount_to_expand_x = label_in_blueprint.size.width - label_node.size.X
		amount_to_expand_x = amount_to_expand_x
		local amount_to_expand_y = label_in_blueprint.size.height - label_node.size.Y

		local expansion_vector = Vector2(amount_to_expand_x, -amount_to_expand_y)


		blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox").ResizeNode(label_node, 2, expansion_vector) --2 is expand right

		--I dont know why, but if you dont have enough delay here, the labels will not resize properly
		--the second resize will somehow overwrite the other axis with its non-resized version.
		--ie if you were to resize a 256x256 into a 512x512, it would become a 256x512.
		local resize_delay = (blue_prints.time_delay_between_loops * 8)

		Timer.Wait(function() resize_label(label_node, 1, expansion_vector) end, resize_delay) --the commands override each other if sent too fast. 1 is expand down.
	end

	has_load_completed_array["resize_labels_complete"] = true
end

function blue_prints.update_values_in_components(components_from_blueprint)
	if blue_prints.most_recent_circuitbox == nil then
		print("no circuitbox detected")
		return
	end

	local components_in_box = blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox").Components

	for index, component_in_box in ipairs(components_in_box) do
		local component_to_copy = components_from_blueprint[index]

		local component_class_to_change = component_in_box.Item.GetComponentString(component_to_copy.class.name)
		if component_class_to_change then
			for attr, value in pairs(component_to_copy.class.attributes) do
				--print("  " .. attr .. ":", value)


				--print(component_class_to_change)
				--print(tostring(component_class_to_change[attr]))

				local success = false
				local result


				--print(attr)
				local is_actually_editable = false
				local my_editables = component_in_box.Item.GetInGameEditableProperties(false)
				for tuple in my_editables do
					--print(tuple.Item2.name, ' = ', tuple.Item2.GetValue(tuple.Item1))
					if tostring(tuple.Item2.name) == tostring(attr) then
						is_actually_editable = true
						break
					end
				end


				if is_actually_editable then --only attempt load if its actually editable
					-- First attempt (string version)
					if not success then
						success, result = pcall(function()
							component_class_to_change[attr] = value
							return "String operation successful"
						end)
					end

					-- Second attempt (number version)
					if not success then
						success, result = pcall(function()
							component_class_to_change[attr] = tonumber(value)
							return "Number operation successful"
						end)
					end

					-- Third attempt (bool version)
					if not success then
						success, result = pcall(function()
							component_class_to_change[attr] = blue_prints.string_to_bool(value)
							return "Boolean operation successful"
						end)
					end



					--oscillator is a special case because it uses enums
					if component_to_copy.class.name == "OscillatorComponent" then
						if tostring(attr) == "OutputType" then
							if value == "Pulse" then component_class_to_change.OutputType = component_class_to_change
								.WaveType.Pulse end
							if value == "Sawtooth" then component_class_to_change.OutputType = component_class_to_change
								.WaveType.Sawtooth end
							if value == "Sine" then component_class_to_change.OutputType = component_class_to_change
								.WaveType.Sine end
							if value == "Square" then component_class_to_change.OutputType = component_class_to_change
								.WaveType.Square end
							if value == "Triangle" then component_class_to_change.OutputType = component_class_to_change
								.WaveType.Triangle end
						end
						if tostring(attr) == "Frequency" then
							component_class_to_change.Frequency = tonumber(value)
						end
						success = true
					end


					if success then
						--print("Operation succeeded:", result)
					else
						print("All operations failed. Last error: ", result)
						print("Component type that failed: ", component_class_to_change.name)
					end


					--print("Game mode: ", Game.GameSession.GameMode.Name)
					if Game.GameSession ~= nil then        --a nil check for the sub editor
						local gameModeName = tostring(Game.GameSession.GameMode.Name)
						if not Game.GameSession.GameMode.IsSinglePlayer then --these dont exist in single player so will cause a crash if not avoided.
							local property = component_class_to_change.SerializableProperties[Identifier(attr)]
							Networking.CreateEntityEvent(component_in_box.Item,
								Item.ChangePropertyEventData(property, component_class_to_change))
						end
					end
				end
			end
		end
	end

	--print("All values updated inside components.")
	has_load_completed_array["update_values_in_components_complete"] = true
end


-- Check if input/output nodes have moved to correct positions
local function check_input_output_positions(expected_input_pos, expected_output_pos)
	local input_output_nodes = blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox").InputOutputNodes
	local input_connection_node = blue_prints.getNthValue(input_output_nodes, 1)
	local output_connection_node = blue_prints.getNthValue(input_output_nodes, 2)

	local input_pos_str = ""
	local output_pos_str = ""
	local actual_input_pos_str = ""
	local actual_output_pos_str = ""

	--find the strings for the expected and actual positions
	if input_connection_node and input_connection_node.Position then
		input_pos_str = type(expected_input_pos) == "table" and string.format("{X:%d Y:%d}", expected_input_pos.x, expected_input_pos.y) or tostring(expected_input_pos)
		output_pos_str = type(expected_output_pos) == "table" and string.format("{X:%d Y:%d}", expected_output_pos.x, expected_output_pos.y) or tostring(expected_output_pos)
		actual_input_pos_str = tostring(input_connection_node.Position)
		actual_output_pos_str = tostring(output_connection_node.Position)
	end

	if actual_input_pos_str == input_pos_str and actual_output_pos_str == output_pos_str then
		--print("Input/output nodes moved to correct positions")
		return true
	else
		--print("Input/output nodes not in correct positions")
		--print("Expected input position: ", input_pos_str)
		--print("Actual input position: ", actual_input_pos_str) 
		--print("Expected output position: ", output_pos_str)
		--print("Actual output position: ", actual_output_pos_str)
	end
	return false
end



function blue_prints.move_input_output_nodes(inputNodePos, outputNodePos, recursion_count)
	if blue_prints.most_recent_circuitbox == nil then
		print("no circuitbox detected")
		return
	end

	local input_output_nodes = blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox").InputOutputNodes
	local input_connection_node = blue_prints.getNthValue(input_output_nodes, 1)
	local output_connection_node = blue_prints.getNthValue(input_output_nodes, 2)



	local sacrificial_immutable_array_input = blue_prints.immutable_array_type.Create(input_connection_node)
	local input_connection_node_in_immutable_aray = sacrificial_immutable_array_input.Add(input_connection_node)


	local input_delta_x = inputNodePos.x - input_connection_node.Position.X
	local input_delta_y = inputNodePos.y - input_connection_node.Position.Y
	local move_input_vector = Vector2(input_delta_x, input_delta_y)

	blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox").MoveComponent(move_input_vector,
		input_connection_node_in_immutable_aray)



	local sacrificial_immutable_array_output = blue_prints.immutable_array_type.Create(output_connection_node)
	local output_connection_node_in_immutable_aray = sacrificial_immutable_array_output.Add(output_connection_node)

	local output_delta_x = outputNodePos.x - output_connection_node.Position.X
	local output_delta_y = outputNodePos.y - output_connection_node.Position.Y
	local move_output_vector = Vector2(output_delta_x, output_delta_y)

	blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox").MoveComponent(move_output_vector,
		output_connection_node_in_immutable_aray)

	--this is used by both clear circuitbox and to reposition later
	--clear IO label must be done first, otherwise the increased width will thrown the numbers off
	if check_input_output_positions(inputNodePos, outputNodePos) and has_load_completed_array["clear_IO_label_complete"] then
		if has_load_completed_array["clear_IO_position_complete"] == false then
			has_load_completed_array["clear_IO_position_complete"] = true
		else 

			has_load_completed_array["move_input_output_nodes_complete"] = true
		end
	else
		if recursion_count < 5 then
			local new_recursion_count = recursion_count + 1
			Timer.Wait(function() blue_prints.move_input_output_nodes(inputNodePos, outputNodePos, new_recursion_count) end,
				blue_prints.time_delay_between_loops)
		else
			print("Error: Failed to move input/output nodes")
		end
	end
end

function blue_prints.reset_io()
    if blue_prints.most_recent_circuitbox == nil then
        print("no circuitbox detected")
        return false
    end

    -- Stage 1: Reset the labels on the input output panels
	local empty_input = {
		signal_in1 = "", signal_in2 = "", signal_in3 = "", signal_in4 = "",
		signal_in5 = "", signal_in6 = "", signal_in7 = "", signal_in8 = ""
	}
	local empty_output = {
		signal_out1 = "", signal_out2 = "", signal_out3 = "", signal_out4 = "",
		signal_out5 = "", signal_out6 = "", signal_out7 = "", signal_out8 = ""
	}
	blue_prints.change_input_output_labels(empty_input, empty_output, 0)

    -- Stage 2: Move input/output nodes to default positions
	local move_input_vector = Vector2(-512, 0)
	local move_output_vector = Vector2(512, 0)
	local initial_recursion_count = 0
	blue_prints.move_input_output_nodes(move_input_vector, move_output_vector, initial_recursion_count)


end

function blue_prints.clear_circuitbox_recursive(batch_size, recursion_count)
	--this function removes all components, but does not change IO
    if blue_prints.most_recent_circuitbox == nil then
        print("no circuitbox detected")
        return
    end

    batch_size = batch_size or 10 -- Default batch size of 10
    local circuit_box = blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox")
    local something_cleared = false

    -- Clear components in batches
    local components = circuit_box.Components
    if #components > 0 then
        local batch = blue_prints.immutable_array_type.Create(blue_prints.getNthValue(components, 1))
        local count = 0
        
        for _, component in ipairs(components) do
            if count < batch_size then
                batch = batch.Add(component)
                count = count + 1
                something_cleared = true
            else
                break
            end
        end
        
        if count > 0 then
            circuit_box.RemoveComponents(batch)
        end
    end

    -- Clear labels in batches
    local labels = circuit_box.Labels
    if #labels > 0 and not something_cleared then
        local batch = blue_prints.immutable_array_type.Create(blue_prints.getNthValue(labels, 1))
        local count = 0
        
        for _, label in ipairs(labels) do
            if count < batch_size then
                batch = batch.Add(label)
                count = count + 1
                something_cleared = true
            else
                break
            end
        end
        
        if count > 0 then
            circuit_box.RemoveLabel(batch)
        end
    end

    -- Clear wires in batches
    local wires = circuit_box.Wires
    if #wires > 0 and not something_cleared then
        local batch = blue_prints.immutable_array_type.Create(blue_prints.getNthValue(wires, 1))
        local count = 0
        
        for _, wire in ipairs(wires) do
            if count < batch_size then
                batch = batch.Add(wire)
                count = count + 1
                something_cleared = true
            else
                break
            end
        end
        
        if count > 0 then
            circuit_box.RemoveWires(batch)
        end
    end

    -- If we cleared something, schedule next batch
    if something_cleared and recursion_count < 20 then
        Timer.Wait(function() 
            blue_prints.clear_circuitbox_recursive(batch_size, recursion_count + 1)
        end, blue_prints.time_delay_between_loops)
	elseif something_cleared and recursion_count >= 20 then
		print("Recursion limit reached. Clearing circuitbox failed.")
    else
        -- Everything is cleared
        has_load_completed_array["clear_components_complete"] = true
    end
end

-- Update the original clear_circuitbox function to use the recursive version
function blue_prints.clear_circuitbox()
	-- Reset inputs and outputs to default simultaneously to clearing components
	blue_prints.reset_io()

	--If you remove too fast it causes baro to have event system problems.
	--without this you get unpredictable results
	blue_prints.clear_circuitbox_recursive(blue_prints.component_batch_size, 0)
end
	
	

function blue_prints.construct_blueprint_steps(inputs, outputs, components, wires, labels, inputNodePos, outputNodePos)
    -- Reset all completion flags
    for key, _ in pairs(has_load_completed_array) do
        has_load_completed_array[key] = false
    end
    
    -- Track which steps have been started
    local steps_started = {}
    
	local largest_component_count = math.max(#components, #wires, #labels) --it resets every time, so you can just use largest

    -- Maximum time to wait for each step (in milliseconds)
    local TIMEOUT_PER_STEP = ((blue_prints.time_delay_between_loops * largest_component_count) / 1000) + 2  -- if wait longer than this, something has gone wrong
    local step_start_time = nil

	local function check_timeout()
		--if step_start_time then
		--	print(os.time() - step_start_time)
		--end

        if step_start_time and os.time() - step_start_time > TIMEOUT_PER_STEP then
            return true
        end
        return false
    end

	local function end_build_testing_and_cleanup()
		blue_prints.delayed_loading_complete_array_check()
        blue_prints.delayed_loading_complete_unit_test()
    end


    local function check_progress()

		-- Check for timeouts
		if check_timeout() then
			print("Step has timed out.")
			end_build_testing_and_cleanup()
			return
        end

        -- Step 1: Clear circuitbox must complete first
        if not has_load_completed_array["clear_components_complete"] or 
		not has_load_completed_array["clear_IO_position_complete"] or
		not has_load_completed_array["clear_IO_label_complete"] then
            if not steps_started["clear"] then
                steps_started["clear"] = true
                step_start_time = os.time()
                blue_prints.clear_circuitbox()
            end
            return Timer.Wait(check_progress, blue_prints.time_delay_between_loops)
		end

        -- After clear is complete, these steps can run in parallel:



        -- Parallel Step: Add components
        if not has_load_completed_array["add_components_complete"] then
            if not steps_started["components"] then
                steps_started["components"] = true
                step_start_time = os.time()
                blue_prints.add_all_components_to_circuitbox(components)
            end
        end

        -- Parallel Step: Add labels
        if not has_load_completed_array["add_labels_complete"] then
            if not steps_started["labels"] then
                steps_started["labels"] = true
                step_start_time = os.time()
                blue_prints.add_labels_to_circuitbox_recursive(labels, 1)
            end
        end

        -- Parallel Step: Change I/O labels
        if not has_load_completed_array["change_input_output_labels_complete"] then
            if not steps_started["io_labels"] then
                steps_started["io_labels"] = true
                step_start_time = os.time()
                blue_prints.change_input_output_labels(inputs, outputs)
            end
        end

        -- These steps need to wait for their dependencies:

        -- Add wires (needs components)
        if has_load_completed_array["add_components_complete"] and 
           not has_load_completed_array["add_wires_complete"] then
            if not steps_started["wires"] then
                steps_started["wires"] = true
				step_start_time = os.time()
                blue_prints.add_wires_to_circuitbox_recursive(wires, 1)
            end
        end

        -- Update component values (needs components)
        if has_load_completed_array["add_components_complete"] and 
           not has_load_completed_array["update_values_in_components_complete"] then
            if not steps_started["update_values"] then
                steps_started["update_values"] = true
				step_start_time = os.time()
                blue_prints.update_values_in_components(components)
            end
        end

        -- Rename labels (needs labels)
        if has_load_completed_array["add_labels_complete"] and 
           not has_load_completed_array["rename_labels_complete"] then
            if not steps_started["rename"] then
				step_start_time = os.time()
                steps_started["rename"] = true
                blue_prints.rename_all_labels_in_circuitbox(labels)
            end
        end

        -- Resize labels (needs rename labels)
        if has_load_completed_array["rename_labels_complete"] and 
           not has_load_completed_array["resize_labels_complete"] then
            if not steps_started["resize"] then
				step_start_time = os.time()
                steps_started["resize"] = true
                blue_prints.resize_labels(labels)
            end
        end

        -- Move I/O nodes (needs I/O labels)
        if has_load_completed_array["change_input_output_labels_complete"] and 
           not has_load_completed_array["move_input_output_nodes_complete"] then
            if not steps_started["move_nodes"] then
				step_start_time = os.time()
                steps_started["move_nodes"] = true
				local initial_recursion_count = 0
                blue_prints.move_input_output_nodes(inputNodePos, outputNodePos, initial_recursion_count)
            end
        end

        -- Continue checking progress
        if not (has_load_completed_array["move_input_output_nodes_complete"] and
                has_load_completed_array["resize_labels_complete"] and
                has_load_completed_array["update_values_in_components_complete"] and
                has_load_completed_array["add_wires_complete"])
				then
            return Timer.Wait(check_progress, blue_prints.time_delay_between_loops)
        end

        -- Final checks when everything is complete
		end_build_testing_and_cleanup()
    end

    -- Start the process
    check_progress()
end






function blue_prints.construct_blueprint(provided_path)
	if Character.Controlled == nil then
		print("you dont have a character")
		return
	end
	if blue_prints.most_recent_circuitbox == nil then
		print("no circuitbox detected")
		return
	end

	local is_locked = blue_prints.get_circuit_box_lock_status()
	if is_locked then
		print("Circuit box is locked. Wiring is not possible.")
		GUI.AddMessage('Wiring Locked', Color.Red)
		return
	end

	local isValid, message = blue_prints.validate_blueprint_file(provided_path)
	if isValid then
		--print("Blueprint file is valid")
	else
		print("Blueprint validation failed: " .. message)
		GUI.AddMessage('Blueprint File Invalid', Color.Red)
		return
	end

	if Game.Paused then --the load will fail if you attempt it while paused. This fixes that.
		print("Unpause the game to complete loading your circuit.")
		Timer.Wait(function() blue_prints.construct_blueprint(provided_path) end, 1000)
		return
	end

	-- Check if the filename already ends with .txt
	if not string.match(provided_path, "%.txt$") then
		-- Add .txt if it's not already present
		provided_path = provided_path .. ".txt"
	end

	local file_path = blue_prints.normalizePath(blue_prints.save_path .. "/" .. provided_path)
	path_to_loaded_file = file_path -- Keep track for unit testing

	-- Use our new read function
	local xmlContent = blue_prints.readFileContents(file_path)

	if xmlContent then
		-- Parse the XML content
		local inputs, outputs, components, wires, labels, inputNodePos, outputNodePos = blue_prints.parseXML(xmlContent)

		-- Check inventory for required components
		local missing_components = blue_prints.check_inventory_for_requirements(components)

		local all_needed_items_are_present = true
		for _, count in pairs(missing_components) do
			if count > 0 then
				all_needed_items_are_present = false
				break
			end
		end

		if all_needed_items_are_present then
			print("All required components are present! Now loading blueprint...")
			GUI.AddMessage('File Loading...', Color.White)

			-- Extract folder name if present, otherwise use root
			local folder = "[Root Directory]"
			local folder_path = provided_path:match("^(.-)/")
			if folder_path then
				folder = folder_path
			end

			--save the folder and filename
			local filename = provided_path:match("([^/\\]+)$") or provided_path  -- Get everything after last / or \
			blue_prints.most_recently_used_blueprint_name = filename:gsub("%.txt$", "") -- Remove .txt if present
			blue_prints.most_recent_folder = folder

			blue_prints.construct_blueprint_steps(inputs, outputs, components, wires, labels, inputNodePos,outputNodePos)

		else
			print("You are missing: ")
			GUI.AddMessage('Not Enough Components', Color.Red)
			for name, count in pairs(missing_components) do
				if count > 0 then
					print(name .. ": " .. count)
				end
			end
		end
	else
		print("file not found")
		print("saved designs:")
		blue_prints.print_all_saved_files()
	end
end

function blue_prints.delayed_loading_complete_array_check()
	local function allTrue(array)
		for key, value in pairs(array) do
			if value ~= true then
				return false -- If any value is not true, return false
			end
		end
		return true -- If the loop completes, all values are true
	end

	local result = allTrue(has_load_completed_array)
	if result then
		GUI.AddMessage('Load Complete', Color.White)
		print("Blueprint loading complete")
	else
		GUI.AddMessage('Loading Failed', Color.Red)

		local message_box = GUI.MessageBox('Load Failed',
		'Your circuit has failed to load. One of the load functions has failed to complete. Hit F3 for more details. \n \nTry loading it again. \n\nBarotrauma circuitbox code is sensitive to sending too many commands too fast. Try slowing things down with the delay slider (above the load button) and try again. This can help on laggy servers. \n \nIf the problem persists please report this bug on the steam workshop page. Include a download link to your saved blueprint file and a screenshot of your console text (you can see that by hitting F3)',
		{ 'OK' })

		local ok_button = message_box.Buttons[0] or message_box.Buttons[1]

		ok_button.OnClicked = function()
			message_box.Close()
		end

		for key, value in pairs(has_load_completed_array) do
			print(tostring(key) .. ": " .. tostring(value))
		end
	end
end

function blue_prints.delayed_loading_complete_unit_test()
	if blue_prints.unit_tests_enabled then
		xml_of_loaded_circuit = blue_prints.prepare_circuitbox_xml_for_saving()
		--run a unit test here to make sure it works
		local load_test_results = blue_prints.loading_complete_unit_test(path_to_loaded_file, xml_of_loaded_circuit)
		print("Passed loading unit test: " .. tostring(load_test_results))

		if load_test_results then
			--GUI.AddMessage('Load Complete!', Color.White)
		else
			GUI.AddMessage('Load Failed!', Color.Red)

			local message_box = GUI.MessageBox('Load Failed',
			'Your circuit has failed to load.\n\nYour blueprint file might be from an earlier version of Blueprints and nothing is actually wrong. Try saving it again (overwriting the original) to update your blueprint file to the latest version.\n\nIf you are certain your file is up to date try loading it again. Do not move or change anything during loading: the loaded circuit must match the blueprint file EXACTLY in order for you not to see this message.\n\nThis also means if the inputs change any values in any component the unit test will also fail. Same thing if you have some value that changes over time, like RGB or timing values from an oscillator. So the circuit might still be ok, it just does not match your save exactly.\n\nBarotrauma circuitbox code is sensitive to sending too many commands too fast. Try slowing things down with the delay slider (above the load button) and try again.\n\nIf none of these exceptions apply and the problem persists please report this bug on the steam workshop page or the discord. Include a download link to your saved blueprint file and a screenshot of your console text (you can see that by hitting F3)',
			{ 'OK' })
	
			local ok_button = message_box.Buttons[0] or message_box.Buttons[1]
	
			ok_button.OnClicked = function()
				message_box.Close()
			end
	
			for key, value in pairs(has_load_completed_array) do
				print(tostring(key) .. ": " .. tostring(value))
			end

		end
	end
end
