code:
/**
 * # Clock Component
 *
 * Fires every tick of the circuit timer SS
 */
/obj/item/circuit_component/clock
	display_name = "Clock"
	desc = "A component that repeatedly fires."

	/// Whether the clock is on or not
	var/datum/port/input/on

	/// The signal from this clock component
	var/datum/port/output/signal

/obj/item/circuit_component/clock/get_ui_notices()
	. = ..()
	. += create_ui_notice("Clock Interval: [DisplayTimeText(COMP_CLOCK_DELAY)]", "orange", "clock")

/obj/item/circuit_component/clock/populate_ports()
	on = add_input_port("On", PORT_TYPE_NUMBER)

	signal = add_output_port("Signal", PORT_TYPE_SIGNAL)

/obj/item/circuit_component/clock/input_received(datum/port/input/port)

	if(on.value)
		start_process()
	else
		stop_process()

/obj/item/circuit_component/clock/Destroy()
	stop_process()
	return ..()

/obj/item/circuit_component/clock/process(delta_time)
	signal.set_output(COMPONENT_SIGNAL)

/**
 * Adds the component to the SSclock_component process list
 *
 * Starts ticking to send signals between periods of time
 */
/obj/item/circuit_component/clock/proc/start_process()
	START_PROCESSING(SSclock_component, src)

/**
 * Removes the component to the SSclock_component process list
 *
 * Signals stop getting sent.
 */
/obj/item/circuit_component/clock/proc/stop_process()
	STOP_PROCESSING(SSclock_component, src)


code:
/// The minimum delay value that the delay component can have.
#define COMP_DELAY_MIN_VALUE 0.1

/**
 * # Delay Component
 *
 * Delays a signal by a specified duration.
 */
/obj/item/circuit_component/delay
	display_name = "Delay"
	desc = "A component that delays a signal by a specified duration. Timer gets reset when triggered again."

	/// Amount to delay by
	var/datum/port/input/delay_amount
	/// Input signal to fire the delay
	var/datum/port/input/trigger
	/// Interrupts the delay before it fires
	var/datum/port/input/interrupt

	var/timer_id = TIMER_ID_NULL

	/// The output of the signal
	var/datum/port/output/output

/obj/item/circuit_component/delay/populate_ports()
	delay_amount = add_input_port("Delay", PORT_TYPE_NUMBER, trigger = null)
	trigger = add_input_port("Trigger", PORT_TYPE_SIGNAL, trigger = .proc/trigger_delay)
	interrupt = add_input_port("Interrupt", PORT_TYPE_SIGNAL, trigger = .proc/interrupt_timer)

	output = add_output_port("Result", PORT_TYPE_SIGNAL)

/obj/item/circuit_component/delay/proc/trigger_delay(datum/port/input/port)
	CIRCUIT_TRIGGER
	var/delay = delay_amount.value
	if(delay > COMP_DELAY_MIN_VALUE)
		// Convert delay into deciseconds
		timer_id = addtimer(CALLBACK(output, /datum/port/output.proc/set_output, trigger.value), delay*10, TIMER_UNIQUE|TIMER_STOPPABLE|TIMER_OVERRIDE)
	else
		if(timer_id != TIMER_ID_NULL)
			deltimer(timer_id)
			timer_id = TIMER_ID_NULL
		output.set_output(trigger.value)

/obj/item/circuit_component/delay/proc/interrupt_timer(datum/port/input/port)
	CIRCUIT_TRIGGER
	if(timer_id != TIMER_ID_NULL)
		deltimer(timer_id)
		timer_id = TIMER_ID_NULL

#undef COMP_DELAY_MIN_VALUE

repo_name: AnalWerewolf/sundown

license: mit


code:
/**
 * # Router Component
 *
 * Writes one of multiple inputs to one of multiple outputs.
 */
/obj/item/circuit_component/router
	display_name = "Router"
	desc = "Copies the input chosen by \"Input Selector\" to the output chosen by \"Output Selector\"."
	circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL

	var/datum/port/input/option/router_options

	/// Which ports to connect.
	var/datum/port/input/input_selector
	var/datum/port/input/output_selector

	/// How many ports to have.
	var/input_port_amount = 4
	var/output_port_amount = 4

	/// Current type of the ports
	var/current_type

	/// The ports to route.
	var/list/datum/port/input/ins
	var/list/datum/port/output/outs

/obj/item/circuit_component/router/populate_options()
	router_options = add_option_port("Router Options", GLOB.wiremod_basic_types)

/obj/item/circuit_component/router/populate_ports()
	current_type = router_options.value
	if(input_port_amount > 1)
		input_selector = add_input_port("Input Selector", PORT_TYPE_NUMBER, default = 1)
	if(output_port_amount > 1)
		output_selector = add_input_port("Output Selector", PORT_TYPE_NUMBER, default = 1)
	ins = list()
	for(var/port_id in 1 to input_port_amount)
		ins += add_input_port(input_port_amount > 1 ? "Input [port_id]" : "Input", current_type)
	outs = list()
	for(var/port_id in 1 to output_port_amount)
		outs += add_output_port(output_port_amount > 1 ? "Output [port_id]" : "Output", current_type)

/obj/item/circuit_component/router/Destroy()
	input_selector = null
	output_selector = null
	ins.Cut()
	ins = null
	outs.Cut()
	outs = null
	return ..()

// If I is in range, L[I]. If I is out of range, wrap around.
#define WRAPACCESS(L, I) L[(((I||1)-1)%length(L)+length(L))%length(L)+1]
/obj/item/circuit_component/router/pre_input_received(datum/port/input/port)
	var/current_option = router_options.value
	if(current_type != current_option)
		current_type = current_option
		for(var/datum/port/input/input as anything in ins)
			input.set_datatype(current_type)
		for(var/datum/port/output/output as anything in outs)
			output.set_datatype(current_type)

/obj/item/circuit_component/router/input_received(datum/port/input/port)
	var/datum/port/input/input = WRAPACCESS(ins, input_selector ? input_selector.value : 1)
	var/datum/port/output/output = WRAPACCESS(outs, output_selector ? output_selector.value : 1)
	output.set_output(input.value)

/obj/item/circuit_component/router/multiplexer
	display_name = "Multiplexer"
	desc = "Copies the input chosen by \"Input Selector\" to the output."
	output_port_amount = 1


code:
#define COMP_TIMEPIECE_TWENTYFOUR_HOUR "24-Hour"
#define COMP_TIMEPIECE_TWELVE_HOUR "12-Hour"
#define COMP_TIMEPIECE_SECONDS "Seconds"
#define COMP_TIMEPIECE_MINUTES "Minutes"
#define COMP_TIMEPIECE_HOURS "Hours"

/**
 * # Timepiece Component
 *
 * returns the current station time.
 */
/obj/item/circuit_component/timepiece
	display_name = "Timepiece"
	desc = "A component that outputs the current station time. The text output port is used for time formats while the numerical output port is used for units of time."
	circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL

	/// The time format of the text output
	var/datum/port/input/option/format
	/// The unit of time for the numerical output
	var/datum/port/input/option/time_unit
	/// The output for 24/12 hour formats
	var/datum/port/output/text_output
	/// seconds, minutes, hours.
	var/datum/port/output/num_output

/obj/item/circuit_component/timepiece/populate_ports()
	text_output = add_output_port("Time Format", PORT_TYPE_STRING)
	num_output = add_output_port("Unit of Time", PORT_TYPE_NUMBER)

/obj/item/circuit_component/timepiece/populate_options()
	var/static/format_options = list(
		COMP_TIMEPIECE_TWENTYFOUR_HOUR, // Station time is expressed in 24-h in the status tab. So this is the default.
		COMP_TIMEPIECE_TWELVE_HOUR,
	)
	format = add_option_port("Time Format", format_options)
	var/static/unit_options = list(
		COMP_TIMEPIECE_HOURS,
		COMP_TIMEPIECE_MINUTES,
		COMP_TIMEPIECE_SECONDS,
	)
	time_unit = add_option_port("Unit of Time", unit_options)

/obj/item/circuit_component/timepiece/input_received(datum/port/input/port)
	var/time

	switch(format.value)
		if(COMP_TIMEPIECE_TWENTYFOUR_HOUR)
			time = station_time_timestamp()
		if(COMP_TIMEPIECE_TWELVE_HOUR)
			time = time_to_twelve_hour(station_time())

	text_output.set_output(time)

	switch(time_unit.value)
		if(COMP_TIMEPIECE_HOURS)
			time = round(station_time() / (1 HOURS))
		if(COMP_TIMEPIECE_MINUTES)
			time = round(station_time() / (1 MINUTES))
		if(COMP_TIMEPIECE_SECONDS)
			time = round(station_time() / (1 SECONDS))

	num_output.set_output(time)

#undef COMP_TIMEPIECE_TWENTYFOUR_HOUR
#undef COMP_TIMEPIECE_TWELVE_HOUR
#undef COMP_TIMEPIECE_SECONDS
#undef COMP_TIMEPIECE_MINUTES
#undef COMP_TIMEPIECE_HOURS


code:
/**
 * # Typecast Component
 *
 * A component that casts a value to a type if it matches or outputs null.
 */
/obj/item/circuit_component/typecast
	display_name = "Typecast"
	desc = "A component that casts a value to a type if it matches or outputs null."
	circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL

	var/datum/port/input/option/typecast_options

	var/datum/port/input/input_value

	var/datum/port/output/output_value

	var/current_type

/obj/item/circuit_component/typecast/populate_ports()
	current_type = typecast_options.value
	input_value = add_input_port("Input", PORT_TYPE_ANY)
	output_value = add_output_port("Output", current_type)

/obj/item/circuit_component/typecast/populate_options()
	var/static/list/component_options = list(
		PORT_TYPE_STRING,
		PORT_TYPE_NUMBER,
		PORT_COMPOSITE_TYPE_LIST,
		PORT_TYPE_ATOM,
	)
	typecast_options = add_option_port("Typecast Options", component_options)

/obj/item/circuit_component/typecast/pre_input_received(datum/port/input/port)
	var/current_option = typecast_options.value
	if(current_type != current_option)
		current_type = current_option
		output_value.set_datatype(current_type)

/obj/item/circuit_component/typecast/input_received(datum/port/input/port)
	var/current_option = typecast_options.value
	var/value = input_value.value
	var/value_to_set = null
	switch(current_option)
		if(PORT_TYPE_STRING)
			if(istext(value))
				value_to_set = value
		if(PORT_TYPE_NUMBER)
			if(isnum(value))
				value_to_set = value
		if(PORT_COMPOSITE_TYPE_LIST)
			if(islist(value))
				value_to_set = value
		if(PORT_TYPE_ATOM)
			if(isatom(value))
				value_to_set = value

	output_value.set_output(value_to_set)


code:
#define COMP_TYPECHECK_MOB "organism"
#define COMP_TYPECHECK_HUMAN "humanoid"

/**
 * # Typecheck Component
 *
 * Checks the type of a value
 */
/obj/item/circuit_component/compare/typecheck
	display_name = "Typecheck"
	desc = "A component that checks the type of its input."

	input_port_amount = 1
	var/datum/port/input/option/typecheck_options

/obj/item/circuit_component/compare/typecheck/populate_options()
	var/static/component_options = list(
		PORT_TYPE_STRING,
		PORT_TYPE_NUMBER,
		PORT_COMPOSITE_TYPE_LIST,
		PORT_TYPE_ATOM,
		COMP_TYPECHECK_MOB,
		COMP_TYPECHECK_HUMAN,
	)
	typecheck_options = add_option_port("Typecheck Options", component_options)

/obj/item/circuit_component/compare/typecheck/do_comparisons(list/ports)
	if(!length(ports))
		return
	. = FALSE

	// We're only comparing the first port/value. There shouldn't be any more.
	var/datum/port/input/input_port = ports[1]
	var/input_val = input_port.value
	switch(typecheck_options.value)
		if(PORT_TYPE_STRING)
			return istext(input_val)
		if(PORT_TYPE_NUMBER)
			return isnum(input_val)
		if(PORT_COMPOSITE_TYPE_LIST)
			return islist(input_val)
		if(PORT_TYPE_ATOM)
			return isatom(input_val)
		if(COMP_TYPECHECK_MOB)
			return ismob(input_val)
		if(COMP_TYPECHECK_HUMAN)
			return ishuman(input_val)

#undef COMP_TYPECHECK_MOB
#undef COMP_TYPECHECK_HUMAN


code:
/**
 * # Getter Component
 *
 * Gets the current value from a variable.
 */
/obj/item/circuit_component/variable/getter
	display_name = "Variable Getter"
	desc = "A component that gets a variable globally on the circuit."

	/// The value of the variable
	var/datum/port/output/value

	circuit_size = 0

/obj/item/circuit_component/variable/getter/populate_ports()
	value = add_output_port("Value", PORT_TYPE_ANY)

/obj/item/circuit_component/variable/getter/pre_input_received(datum/port/input/port)
	. = ..()
	if(current_variable)
		value.set_datatype(current_variable.datatype)
		value.set_value(current_variable.value)


code:
/**
 * # Setter Component
 *
 * Stores the current input when triggered into a variable.
 */
/obj/item/circuit_component/variable/setter
	display_name = "Variable Setter"
	desc = "A component that sets a variable globally on the circuit."

	circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL

	/// The input to store
	var/datum/port/input/input_port

	circuit_size = 0

/obj/item/circuit_component/variable/setter/populate_ports()
	input_port = add_input_port("Input", PORT_TYPE_ANY)
	trigger_input = add_input_port("Store", PORT_TYPE_SIGNAL)

/obj/item/circuit_component/variable/setter/pre_input_received(datum/port/input/port)
	. = ..()
	if(port == variable_name)
		input_port.set_datatype(current_variable.datatype)

/obj/item/circuit_component/variable/setter/input_received(datum/port/input/port)
	if(!current_variable)
		return
	current_variable.set_value(input_port.value)


code:
/// An admin verb to view all circuits, plus useful information
/datum/admins/proc/view_all_circuits()
	set category = "Admin.Game"
	set name = "View All Circuits"

	var/static/datum/circuit_admin_panel/circuit_admin_panel = new
	circuit_admin_panel.ui_interact(usr)

/datum/circuit_admin_panel

/datum/circuit_admin_panel/ui_static_data(mob/user)
	var/list/data = list()
	data["circuits"] = list()

	for (var/obj/item/integrated_circuit/circuit as anything in GLOB.integrated_circuits)
		var/datum/mind/inserter = circuit.inserter_mind?.resolve()

		data["circuits"] += list(list(
			"ref" = REF(circuit),
			"name" = "[circuit.name] in [loc_name(circuit)]",
			"creator" = circuit.get_creator(),
			"has_inserter" = !isnull(inserter),
		))

	return data

/datum/circuit_admin_panel/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
	. = ..()
	if (.)
		return .

	if (!istext(params["circuit"]))
		return FALSE

	var/obj/item/integrated_circuit/circuit = locate(params["circuit"])
	if (!istype(circuit))
		to_chat(usr, span_warning("That circuit no longer exists."))
		return FALSE

	switch (action)
		if ("duplicate_circuit")
			if (alert(usr, "This will spawn the new circuit at where you are, are you sure?", "Confirm", "Yes", "No") != "Yes")
				return FALSE

			var/list/errors = list()

			var/obj/item/integrated_circuit/loaded/new_circuit = new(usr.drop_location())
			new_circuit.load_circuit_data(circuit.convert_to_json(), errors)

			if (length(errors))
				to_chat(usr, span_warning("Somehow, duplicating the circuit failed:"))
				for (var/error in errors)
					to_chat(usr, span_warning(error))
		if ("follow_circuit")
			usr.client?.admin_follow(circuit)
		if ("save_circuit")
			circuit.attempt_save_to(usr.client)
		if ("vv_circuit")
			usr.client?.debug_variables(circuit)
		if ("open_circuit")
			circuit.ui_interact(usr)
		if ("open_player_panel")
			var/datum/mind/inserter = circuit.inserter_mind?.resolve()
			usr.client?.holder?.show_player_panel(inserter?.current)

	return TRUE

/datum/circuit_admin_panel/ui_state(mob/user)
	return GLOB.admin_state

/datum/circuit_admin_panel/ui_interact(mob/user, datum/tgui/ui)
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "CircuitAdminPanel")
		ui.open()


code:
/**
 * # Integrated Circuit Component
 *
 * A component that performs a function when given an input
 *
 * Can be attached to an integrated circuitboard, where it can then
 * be connected between other components to provide an output or to receive
 * an input. This is the base type of all components
 */
/obj/item/circuit_component
	name = COMPONENT_DEFAULT_NAME
	icon = 'icons/obj/module.dmi'
	icon_state = "component"
	inhand_icon_state = "electronic"

	/// The name of the component shown on the UI
	var/display_name = "Generic"

	/// The colour this circuit component appears in the UI
	var/ui_color = "blue"

	/// The integrated_circuit that this component is attached to.
	var/obj/item/integrated_circuit/parent

	/// A list that contains the outpurt ports on this component
	/// Used to connect between the ports
	var/list/datum/port/output/output_ports = list()

	/// A list that contains the components the input ports on this component
	/// Used to connect between the ports
	var/list/datum/port/input/input_ports = list()

	/// Generic trigger input for triggering this component
	var/datum/port/input/trigger_input
	var/datum/port/output/trigger_output

	/// The flags of the circuit to control basic generalised behaviour.
	var/circuit_flags = NONE

	/// Used to determine the x position of the component within the UI
	var/rel_x = 0
	/// Used to determine the y position of the component within the UI
	var/rel_y = 0

	/// The power usage whenever this component receives an input
	var/power_usage_per_input = 1

	/// Whether the component is removable or not. Only affects user UI
	var/removable = TRUE

	/// Defines which shells support this component. Only used as an informational guide, does not restrict placing these components in circuits.
	var/required_shells = null

	/// Determines the amount of space this circuit occupies in an integrated circuit.
	var/circuit_size = 1

	/// The UI buttons of this circuit component. An assoc list that has this format: "button_icon" = "action_name"
	var/ui_buttons = null

/// Called when the option ports should be set up
/obj/item/circuit_component/proc/populate_options()
	return

/// Called when the rest of the ports should be set up
/obj/item/circuit_component/proc/populate_ports()
	return

/// Extension of add_input_port. Simplifies the code to make an option port to reduce boilerplate
/obj/item/circuit_component/proc/add_option_port(name, list/list_to_use, order = 0, trigger = .proc/input_received)
	return add_input_port(name, PORT_TYPE_OPTION, order = order, trigger = trigger, port_type = /datum/port/input/option, extra_args = list("possible_options" = list_to_use))

/obj/item/circuit_component/Initialize(mapload)
	. = ..()
	if(name == COMPONENT_DEFAULT_NAME)
		name = "[lowertext(display_name)] [COMPONENT_DEFAULT_NAME]"
	populate_options()
	populate_ports()
	if((circuit_flags & CIRCUIT_FLAG_INPUT_SIGNAL) && !trigger_input)
		trigger_input = add_input_port("Trigger", PORT_TYPE_SIGNAL, order = 2)
	if((circuit_flags & CIRCUIT_FLAG_OUTPUT_SIGNAL) && !trigger_output)
		trigger_output = add_output_port("Triggered", PORT_TYPE_SIGNAL, order = 2)
	if(circuit_flags & CIRCUIT_FLAG_INSTANT)
		ui_color = "orange"
	if(circuit_flags & CIRCUIT_FLAG_REFUSE_MODULE)
		desc += " Incompatible with module components."

/obj/item/circuit_component/Destroy()
	if(parent)
		// Prevents a Destroy() recursion
		var/obj/item/integrated_circuit/old_parent = parent
		parent = null
		old_parent.remove_component(src)

	trigger_input = null
	trigger_output = null

	QDEL_LIST(output_ports)
	QDEL_LIST(input_ports)
	return ..()

/obj/item/circuit_component/examine(mob/user)
	. = ..()
	if(circuit_flags & CIRCUIT_FLAG_REFUSE_MODULE)
		. += span_notice("It's incompatible with module components.")

/**
 * Called when a shell is registered from the component/the component is added to a circuit.
 *
 * Register all signals here on the shell.
 * Arguments:
 * * shell - Shell being registered
 */
/obj/item/circuit_component/proc/register_shell(atom/movable/shell)
	return

/**
 * Called when a shell is unregistered from the component/the component is removed from a circuit.
 *
 * Unregister all signals here on the shell.
 * Arguments:
 * * shell - Shell being unregistered
 */
/obj/item/circuit_component/proc/unregister_shell(atom/movable/shell)
	return

/**
 * Disconnects a component from other components
 *
 * Disconnects both the input and output ports of the component
 */
/obj/item/circuit_component/proc/disconnect()
	for(var/datum/port/output/port_to_disconnect as anything in output_ports)
		port_to_disconnect.disconnect_all()

	for(var/datum/port/input/port_to_disconnect as anything in input_ports)
		port_to_disconnect.disconnect_all()

/**
 * Adds an input port and returns it
 *
 * Arguments:
 * * name - The name of the input port
 * * type - The datatype it handles
 * * trigger - Whether this input port triggers an update on the component when updated.
 */
/obj/item/circuit_component/proc/add_input_port(name, type, order = 1, trigger = .proc/input_received, default = null, port_type = /datum/port/input, extra_args = null)
	var/list/arguments = list(src)
	arguments += args
	if(extra_args)
		arguments += extra_args
	var/datum/port/input/input_port = new port_type(arglist(arguments))
	input_ports += input_port
	sortTim(input_ports, /proc/cmp_port_order_asc)
	if(parent)
		SStgui.update_uis(parent)
	return input_port

/**
 * Removes an input port and deletes it. This will not cleanup any references made by derivatives of the circuit component
 *
 * Arguments:
 * * input_port - The input port to remove.
 */
/obj/item/circuit_component/proc/remove_input_port(datum/port/input/input_port)
	input_ports -= input_port
	qdel(input_port)
	if(parent)
		SStgui.update_uis(parent)

/**
 * Adds an output port and returns it
 *
 * Arguments:
 * * name - The name of the output port
 * * type - The datatype it handles.
 */
/obj/item/circuit_component/proc/add_output_port(name, type, order = 1)
	var/list/arguments = list(src)
	arguments += args
	var/datum/port/output/output_port = new(arglist(arguments))
	output_ports += output_port
	sortTim(output_ports, /proc/cmp_port_order_asc)
	return output_port

/**
 * Removes an output port and deletes it. This will not cleanup any references made by derivatives of the circuit component
 *
 * Arguments:
 * * output_port - The output port to remove.
 */
/obj/item/circuit_component/proc/remove_output_port(datum/port/output/output_port)
	output_ports -= output_port
	qdel(output_port)
	if(parent)
		SStgui.update_uis(parent)

/**
 * Called whenever an input is received from one of the ports.
 *
 * Return value indicates whether the trigger was successful or not.
 * Arguments:
 * * port - Can be null. The port that sent the input
 * * return_values - Only defined if the component is receiving an input due to instant execution. Contains the values to be returned once execution has stopped.
 */
/obj/item/circuit_component/proc/trigger_component(datum/port/input/port, list/return_values)
	SHOULD_NOT_SLEEP(TRUE)
	pre_input_received(port)
	if(!should_receive_input(port))
		return FALSE

	var/result
	if(port)
		var/proc_to_call = port.trigger
		if(!proc_to_call)
			return FALSE
		result = call(src, proc_to_call)(port, return_values)
	else
		result = input_received(null, return_values)

	if(result)
		return FALSE

	if(circuit_flags & CIRCUIT_FLAG_OUTPUT_SIGNAL)
		trigger_output.set_output(COMPONENT_SIGNAL)
	return TRUE

/obj/item/circuit_component/proc/set_circuit_size(new_size)
	if(parent)
		parent.current_size -= circuit_size

	circuit_size = new_size

	if(parent)
		parent.current_size += circuit_size

/**
 * Called whether this circuit component should receive an input.
 * If this returns false, the proc that is supposed to be triggered will not be called and an output signal will not be sent.
 * This is to only return false if flow of execution should be stopped because something bad has happened (e.g. no power)
 * Returning no value in input_received() is not an issue because it means flow of execution will continue even if the component failed to execute properly.
 *
 * Return value indicates whether or not
 * Arguments:
 * * port - Can be null. The port that sent the input
 */
/obj/item/circuit_component/proc/should_receive_input(datum/port/input/port)
	SHOULD_NOT_SLEEP(TRUE)
	if(!parent?.on)
		return FALSE

	if(!parent.admin_only)
		if(circuit_flags & CIRCUIT_FLAG_ADMIN)
			message_admins("[display_name] tried to execute on [parent.get_creator_admin()] that has admin_only set to 0")
			return FALSE

		var/obj/item/stock_parts/cell/cell = parent.get_cell()
		if(!cell?.use(power_usage_per_input))
			return FALSE

	if((!port || port.trigger == .proc/input_received) && (circuit_flags & CIRCUIT_FLAG_INPUT_SIGNAL) && !COMPONENT_TRIGGERED_BY(trigger_input, port))
		return FALSE

	return TRUE

/// Called before input_received and should_receive_input. Used to perform behaviour that shouldn't care whether the input should be received or not.
/obj/item/circuit_component/proc/pre_input_received(datum/port/input/port)
	SHOULD_NOT_SLEEP(TRUE)
	return

/**
 * Called from trigger_component. General component behaviour should go in this proc. This is the default proc that is called if no trigger proc is specified.
 *
 * Return value indicates that the circuit should not send an output signal.
 * Arguments:
 * * port - Can be null. The port that sent the input
 * * return_values - Only defined if the component is receiving an input due to instant execution. Contains the values to be returned once execution has stopped.
 */
/obj/item/circuit_component/proc/input_received(datum/port/input/port, list/return_values)
	SHOULD_NOT_SLEEP(TRUE)
	return

/// Called when this component is about to be added to an integrated_circuit.
/obj/item/circuit_component/proc/add_to(obj/item/integrated_circuit/added_to)
	return TRUE

/// Called when this component is removed from an integrated_circuit.
/obj/item/circuit_component/proc/removed_from(obj/item/integrated_circuit/removed_from)
	return

/**
 * Gets the UI notices to be displayed on the CircuitInfo panel.
 *
 * Returns a list of buttons in the following format
 * list(
 *   "icon" = ICON(string)
 *   "content" = CONTENT(string)
 *   "color" = COLOR(string, not a hex)
 * )
 */
/obj/item/circuit_component/proc/get_ui_notices()
	. = list()

	if(circuit_flags & CIRCUIT_FLAG_INSTANT)
		. += create_ui_notice("Instant Execution", "red", "tachometer-alt")

	if(!removable)
		. += create_ui_notice("Unremovable", "red", "lock")

	if(length(required_shells))
		. += create_ui_notice("Supported Shells:", "green", "notes-medical")
		for(var/atom/movable/shell as anything in required_shells)
			. += create_ui_notice(initial(shell.name), "green", "plus-square")

	if(length(input_ports))
		. += create_ui_notice("Power Usage Per Input: [power_usage_per_input]", "orange", "bolt")

/**
 * Called when a special button is pressed on this component in the UI.
 *
 * Arguments:
 * * user - Interacting mob
 * * action - A string for which action is being performed. No parameters passed because it's only a button press.
 */
/obj/item/circuit_component/proc/ui_perform_action(mob/user, action)
	return

/**
 * Creates a UI notice entry to be used in get_ui_notices()
 *
 * Returns a list that can then be added to the return list in get_ui_notices()
 */
/obj/item/circuit_component/proc/create_ui_notice(content, color, icon)
	SHOULD_BE_PURE(TRUE)
	SHOULD_NOT_OVERRIDE(TRUE)
	return list(list(
		"icon" = icon,
		"content" = content,
		"color" = color,
	))

/obj/item/circuit_component/ui_host(mob/user)
	if(parent)
		return parent.ui_host()
	return ..()

/**
 * Creates a table UI notice entry to be used in get_ui_notices()
 *
 * Returns a list that can then be added to the return list in get_ui_notices()
 * Used by components to list their available columns. Recommended to use at the end of get_ui_notices()
 */
/obj/item/circuit_component/proc/create_table_notices(list/entries)
	SHOULD_BE_PURE(TRUE)
	SHOULD_NOT_OVERRIDE(TRUE)
	. = list()
	. += create_ui_notice("Available Columns:", "grey", "question-circle")

	for(var/entry in entries)
		. += create_ui_notice("Column Name: '[entry]'", "grey", "columns")

/**
 * Called when a circuit component is added to an object with a USB port.
 *
 * Arguments:
 * * shell - The object that USB cables can connect to
 */
/obj/item/circuit_component/proc/register_usb_parent(atom/movable/shell)
	return

/**
 * Called when a circuit component is removed from an object with a USB port.
 *
 * Arguments:
 * * shell - The object that USB cables can connect to
 */
/obj/item/circuit_component/proc/unregister_usb_parent(atom/movable/shell)
	return


code:
/// Component printer, creates components for integrated circuits.
/obj/machinery/component_printer
	name = "component printer"
	desc = "Produces components for the creation of integrated circuits."
	icon = 'icons/obj/wiremod_fab.dmi'
	icon_state = "fab-idle"
	circuit = /obj/item/circuitboard/machine/component_printer

	/// The internal material bus
	var/datum/component/remote_materials/materials

	density = TRUE

	/// The techweb the printer will get researched designs from
	var/datum/techweb/techweb

/obj/machinery/component_printer/Initialize(mapload)
	. = ..()

	techweb = SSresearch.science_tech

	materials = AddComponent( \
		/datum/component/remote_materials, \
		"component_printer", \
		mapload, \
		mat_container_flags = BREAKDOWN_FLAGS_LATHE, \
	)

/obj/machinery/component_printer/ui_interact(mob/user, datum/tgui/ui)
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "ComponentPrinter", name)
		ui.open()

/obj/machinery/component_printer/ui_assets(mob/user)
	return list(
		get_asset_datum(/datum/asset/spritesheet/sheetmaterials)
	)

/obj/machinery/component_printer/ui_act(action, list/params)
	. = ..()
	if (.)
		return

	switch (action)
		if ("print")
			var/design_id = params["designId"]
			if (!techweb.researched_designs[design_id])
				return TRUE

			var/datum/design/design = SSresearch.techweb_design_by_id(design_id)
			if (!(design.build_type & COMPONENT_PRINTER))
				return TRUE

			if (materials.on_hold())
				say("Mineral access is on hold, please contact the quartermaster.")
				return TRUE

			if (!materials.mat_container?.has_materials(design.materials))
				say("Not enough materials.")
				return TRUE

			balloon_alert_to_viewers("printed [design.name]")
			materials.mat_container?.use_materials(design.materials)
			materials.silo_log(src, "printed", -1, design.name, design.materials)
			var/atom/printed_design = new design.build_path(drop_location())
			printed_design.pixel_x = printed_design.base_pixel_x + rand(-5, 5)
			printed_design.pixel_y = printed_design.base_pixel_y + rand(-5, 5)
		if ("remove_mat")
			var/datum/material/material = locate(params["ref"])
			var/amount = text2num(params["amount"])

			if (!amount)
				return TRUE

			// SAFETY: eject_sheets checks for valid mats
			materials.eject_sheets(material, amount)

	return TRUE

/obj/machinery/component_printer/ui_data(mob/user)
	var/list/data = list()
	data["materials"] = materials.mat_container.ui_data()
	return data

/obj/machinery/component_printer/ui_static_data(mob/user)
	var/list/data = list()

	var/list/designs = list()

	// for (var/datum/design/component/component_design_type as anything in subtypesof(/datum/design/component))
	for (var/researched_design_id in techweb.researched_designs)
		var/datum/design/design = SSresearch.techweb_design_by_id(researched_design_id)
		if (!(design.build_type & COMPONENT_PRINTER))
			continue

		designs[researched_design_id] = list(
			"name" = design.name,
			"description" = design.desc,
			"materials" = get_material_cost_data(design.materials),
			"categories" = design.category,
		)

	data["designs"] = designs

	return data

/obj/machinery/component_printer/crowbar_act(mob/living/user, obj/item/tool)
	if(..())
		return TRUE
	return default_deconstruction_crowbar(tool)

/obj/machinery/component_printer/screwdriver_act(mob/living/user, obj/item/tool)
	if(..())
		return TRUE
	return default_deconstruction_screwdriver(user, "fab-o", "fab-idle", tool)

/obj/machinery/component_printer/proc/get_material_cost_data(list/materials)
	var/list/data = list()

	for (var/datum/material/material_type as anything in materials)
		data[initial(material_type.name)] = materials[material_type]

	return data

/obj/item/circuitboard/machine/component_printer
	name = "\improper Component Printer (Machine Board)"
	greyscale_colors = CIRCUIT_COLOR_SCIENCE
	build_path = /obj/machinery/component_printer
	req_components = list(
		/obj/item/stock_parts/matter_bin = 2,
		/obj/item/stock_parts/manipulator = 2,
		/obj/item/reagent_containers/glass/beaker = 2,
	)

/obj/machinery/debug_component_printer
	name = "debug component printer"
	desc = "Produces components for the creation of integrated circuits."
	icon = 'icons/obj/wiremod_fab.dmi'
	icon_state = "fab-idle"

	/// All of the possible circuit designs stored by this debug printer
	var/list/all_circuit_designs

	density = TRUE

/obj/machinery/debug_component_printer/Initialize(mapload)
	. = ..()
	all_circuit_designs = list()

	for(var/id in SSresearch.techweb_designs)
		var/datum/design/design = SSresearch.techweb_design_by_id(id)
		if((design.build_type & COMPONENT_PRINTER) && design.build_path)
			all_circuit_designs[design.build_path] = list(
				"name" = design.name,
				"description" = design.desc,
				"materials" = design.materials,
				"categories" = design.category
			)

	for(var/obj/item/circuit_component/component as anything in subtypesof(/obj/item/circuit_component))
		var/categories = list("Inaccessible")
		if(initial(component.circuit_flags) & CIRCUIT_FLAG_ADMIN)
			categories = list("Admin")
		if(!(component in all_circuit_designs))
			all_circuit_designs[component] = list(
				"name" = initial(component.display_name),
				"description" = initial(component.desc),
				"materials" = list(),
				"categories" = categories,
			)

/obj/machinery/debug_component_printer/ui_interact(mob/user, datum/tgui/ui)
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "ComponentPrinter", name)
		ui.open()

/obj/machinery/debug_component_printer/ui_assets(mob/user)
	return list(
		get_asset_datum(/datum/asset/spritesheet/sheetmaterials)
	)

/obj/machinery/debug_component_printer/ui_act(action, list/params)
	. = ..()
	if (.)
		return

	switch (action)
		if ("print")
			var/build_path = text2path(params["designId"])
			if (!build_path)
				return TRUE

			var/list/design = all_circuit_designs[build_path]
			if(!design)
				return TRUE

			balloon_alert_to_viewers("printed [design["name"]]")
			var/atom/printed_design = new build_path(drop_location())
			printed_design.pixel_x = printed_design.base_pixel_x + rand(-5, 5)
			printed_design.pixel_y = printed_design.base_pixel_y + rand(-5, 5)

	return TRUE

/obj/machinery/debug_component_printer/ui_static_data(mob/user)
	var/list/data = list()

	data["materials"] = list()
	data["designs"] = all_circuit_designs

	return data

/// Module duplicator, allows you to save and recreate module components.
/obj/machinery/module_duplicator
	name = "module duplicator"
	desc = "Allows you to duplicate module components so that you don't have to recreate them. Scan a module component over this machine to add it as an entry."
	icon = 'icons/obj/wiremod_fab.dmi'
	icon_state = "module-fab-idle"
	circuit = /obj/item/circuitboard/machine/module_duplicator

	/// The internal material bus
	var/datum/component/remote_materials/materials

	density = TRUE

	var/list/scanned_designs = list()

	var/cost_per_component = 1000

/obj/machinery/module_duplicator/Initialize(mapload)
	. = ..()

	materials = AddComponent( \
		/datum/component/remote_materials, \
		"module_duplicator", \
		mapload, \
		mat_container_flags = BREAKDOWN_FLAGS_LATHE, \
	)

/obj/machinery/module_duplicator/ui_interact(mob/user, datum/tgui/ui)
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "ComponentPrinter", name)
		ui.open()

/obj/machinery/module_duplicator/ui_assets(mob/user)
	return list(
		get_asset_datum(/datum/asset/spritesheet/sheetmaterials)
	)

/obj/machinery/module_duplicator/ui_act(action, list/params)
	. = ..()
	if (.)
		return

	switch (action)
		if ("print")
			var/design_id = text2num(params["designId"])

			if (design_id < 1 || design_id > length(scanned_designs))
				return TRUE

			var/list/design = scanned_designs[design_id]

			if (materials.on_hold())
				say("Mineral access is on hold, please contact the quartermaster.")
				return TRUE

			if (!materials.mat_container?.has_materials(design["materials"]))
				say("Not enough materials.")
				return TRUE

			balloon_alert_to_viewers("printed [design["name"]]")
			materials.mat_container?.use_materials(design["materials"])
			materials.silo_log(src, "printed", -1, design["name"], design["materials"])
			print_module(design)
		if ("remove_mat")
			var/datum/material/material = locate(params["ref"])
			var/amount = text2num(params["amount"])

			if (!amount)
				return TRUE

			// SAFETY: eject_sheets checks for valid mats
			materials.eject_sheets(material, amount)

	return TRUE

/obj/machinery/module_duplicator/proc/print_module(list/design)
	flick("module-fab-print", src)
	addtimer(CALLBACK(src, .proc/finish_module_print, design), 1.6 SECONDS)

/obj/machinery/module_duplicator/proc/finish_module_print(list/design)
	var/obj/item/circuit_component/module/module = new(drop_location())
	module.load_data_from_list(design["dupe_data"])
	module.pixel_x = module.base_pixel_x + rand(-5, 5)
	module.pixel_y = module.base_pixel_y + rand(-5, 5)

/obj/machinery/module_duplicator/attackby(obj/item/weapon, mob/user, params)
	if(!istype(weapon, /obj/item/circuit_component/module))
		return ..()

	var/obj/item/circuit_component/module/module = weapon
	if(module.circuit_flags & CIRCUIT_FLAG_UNDUPEABLE)
		balloon_alert(user, "module cannot be saved!")
		return ..()

	if(module.display_name == initial(module.display_name))
		balloon_alert(user, "module needs a name!")
		return ..()

	for(var/list/component_data as anything in scanned_designs)
		if(component_data["name"] == module.display_name)
			balloon_alert(user, "module name already exists!")
			return ..()

	var/total_cost = 0
	for(var/obj/item/circuit_component/component as anything in module.internal_circuit.attached_components)
		if(component.circuit_flags & CIRCUIT_FLAG_UNDUPEABLE)
			balloon_alert(user, "module contains prohibited components!")
			return ..()

		total_cost += cost_per_component

	var/list/data = list()

	data["dupe_data"] = list()
	module.save_data_to_list(data["dupe_data"])

	data["name"] = module.display_name
	data["desc"] = "A module that has been loaded in by [user]."
	data["materials"] = list(/datum/material/glass = total_cost)

	flick("module-fab-scan", src)
	addtimer(CALLBACK(src, .proc/finish_module_scan, user, data), 1.4 SECONDS)

/obj/machinery/module_duplicator/proc/finish_module_scan(mob/user, data)
	scanned_designs += list(data)

	balloon_alert(user, "module has been saved.")
	playsound(src, 'sound/machines/ping.ogg', 50)

/obj/machinery/module_duplicator/ui_data(mob/user)
	var/list/data = list()
	data["materials"] = materials.mat_container.ui_data()
	return data

/obj/machinery/module_duplicator/ui_static_data(mob/user)
	var/list/data = list()

	var/list/designs = list()

	var/index = 1
	for (var/list/design as anything in scanned_designs)
		designs["[index]"] = list(
			"name" = design["name"],
			"description" = design["desc"],
			"materials" = get_material_cost_data(design["materials"]),
			"categories" = list("Circuitry"),
		)
		index++

	data["designs"] = designs

	return data

/obj/machinery/module_duplicator/crowbar_act(mob/living/user, obj/item/tool)
	if(..())
		return TRUE
	return default_deconstruction_crowbar(tool)

/obj/machinery/module_duplicator/screwdriver_act(mob/living/user, obj/item/tool)
	if(..())
		return TRUE
	return default_deconstruction_screwdriver(user, "module-fab-o", "module-fab-idle", tool)

/obj/machinery/module_duplicator/proc/get_material_cost_data(list/materials)
	var/list/data = list()

	for (var/datum/material/material_type as anything in materials)
		data[initial(material_type.name)] = materials[material_type]

	return data

/obj/item/circuitboard/machine/module_duplicator
	name = "\improper Module Duplicator (Machine Board)"
	greyscale_colors = CIRCUIT_COLOR_SCIENCE
	build_path = /obj/machinery/module_duplicator
	req_components = list(
		/obj/item/stock_parts/matter_bin = 2,
		/obj/item/stock_parts/manipulator = 2,
		/obj/item/reagent_containers/glass/beaker = 2,
	)


code:
// An assoc list of all the possible datatypes.
GLOBAL_LIST_INIT_TYPED(circuit_datatypes, /datum/circuit_datatype, generate_circuit_datatypes())

/proc/generate_circuit_datatypes()
	var/list/datatypes_by_key = list()
	for(var/datum/circuit_datatype/type as anything in subtypesof(/datum/circuit_datatype))
		if(!initial(type.datatype) || initial(type.abstract))
			continue
		datatypes_by_key[initial(type.datatype)] = new type()
	return datatypes_by_key

/**
 * A circuit datatype. Used to determine the datatype of a port and also handle any additional behaviour.
 */
/datum/circuit_datatype
	/// The key. Used to identify the datatype. Should be a define.
	var/datatype

	/// The color of the port in the UI. Doesn't work with hex colours.
	var/color = "blue"

	/// The flags of the circuit datatype
	var/datatype_flags = 0

	/// The datatypes that this datatype can receive from.
	var/list/can_receive_from = list()

	/// Whether this datatype should be loaded into the global circuit_datatypes list.
	var/abstract = FALSE

/**
 * Returns the value to be set for the port
 *
 * Used for implicit conversions between outputs and inputs (e.g. number -> string)
 * and applying/removing signals on inputs
 */
/datum/circuit_datatype/proc/convert_value(datum/port/port, value_to_convert, force = FALSE)
	return value_to_convert

/**
 * Determines if a datatype is compatible with another port of a different type.
 * Note: This is ALWAYS called on the input port, never on the output port.
 * Inputs need to care about what types they're receiving, output ports don't have to care.
 *
 * Arguments:
 * * datatype_to_check - The datatype to check
 */
/datum/circuit_datatype/proc/can_receive_from_datatype(datatype_to_check)
	return datatype == datatype_to_check || (datatype_to_check in can_receive_from)

/**
 * Called when the datatype is given to a port.
 *
 * Arguments:
 * * gained_port - The gained port.
 */
/datum/circuit_datatype/proc/on_gain(datum/port/gained_port)
	return

/**
 * Called when the datatype is removed from a port.
 *
 * Arguments:
 * * lost_port - The removed port.
 */
/datum/circuit_datatype/proc/on_loss(datum/port/lost_port)
	return

/**
 * Determines if a port is compatible with this datatype.
 * This WILL throw a runtime if it returns false. This is for sanity checking and it should not return false
 * unless under extraordinary circumstances or people fail to write proper code.
 *
 * Arguments:
 * * port - The port to check if it is compatible.
 */
/datum/circuit_datatype/proc/is_compatible(datum/port/port)
	return TRUE

/**
 * The data to send to the UI attached to the port. Received by the type in FUNDAMENTAL_PORT_TYPES
 *
 * Arguments:
 * * port - The port sending the data.
 */
/datum/circuit_datatype/proc/datatype_ui_data(datum/port/port)
	return

/**
 * When an input is manually set by a player. This is where extra sanitizing can happen. Will still call convert_value()
 *
 * Arguments:
 * * port - The port sending the data.
 * *
 */
/datum/circuit_datatype/proc/handle_manual_input(datum/port/input/port, mob/user, user_input)
	return user_input

/**
 * Used by composite datatypes. Returns all the datatypes that build this datatype up.
 */
/datum/circuit_datatype/proc/get_datatypes()
	return list()


code:
#define LOG_ERROR(list, error) if(list) { list.Add(error) }

// Determines if a port can have a predefined input value if it is of this type.
GLOBAL_LIST_INIT(circuit_dupe_whitelisted_types, list(
	PORT_TYPE_NUMBER,
	PORT_TYPE_STRING,
	PORT_TYPE_ANY,
	PORT_TYPE_OPTION,
))

/// Loads a circuit based on json data at a location. Can also load usb connections, such as arrest consoles.
/obj/item/integrated_circuit/proc/load_circuit_data(json_data, list/errors)
	var/list/general_data = json_decode(json_data)

	if(!general_data)
		LOG_ERROR(errors, "Invalid json format!")
		return

	if(general_data["display_name"])
		set_display_name(general_data["display_name"])

	var/list/variable_data = general_data["variables"]
	for(var/list/variable as anything in variable_data)
		var/variable_name = variable["name"]
		circuit_variables[variable_name] = new /datum/circuit_variable(variable_name, variable["datatype"])

	admin_only = general_data["admin_only"]

	var/list/circuit_data = general_data["components"]
	var/list/identifiers_to_circuit = list()
	for(var/identifier in circuit_data)
		var/list/component_data = circuit_data[identifier]
		var/type = text2path(component_data["type"])
		if(!ispath(type, /obj/item/circuit_component))
			LOG_ERROR(errors, "Invalid path for circuit component, expected [/obj/item/circuit_component], got [type]")
			continue
		var/obj/item/circuit_component/component = load_component(type)
		identifiers_to_circuit[identifier] = component
		component.load_data_from_list(component_data)

		var/list/input_ports_data = component_data["input_ports_stored_data"]
		for(var/port_name in input_ports_data)
			var/datum/port/input/port
			var/list/port_data = input_ports_data[port_name]
			for(var/datum/port/input/port_to_check as anything in component.input_ports)
				if(port_to_check.name == port_name)
					port = port_to_check
					break

			if(!port)
				LOG_ERROR(errors, "Port '[port_name]' not found on [component.type] when trying to set it to a value of [port_data["stored_data"]]!")
				continue

			port.set_input(port_data["stored_data"])

	var/list/external_objects = general_data["external_objects"]
	for(var/identifier in external_objects)
		var/list/object_data = external_objects[identifier]
		var/type = text2path(object_data["type"])
		if(!ispath(type))
			LOG_ERROR(errors, "Invalid path for external object, expected a path, got [type]")
			continue
		var/atom/movable/object = new type(drop_location())
		var/list/connected_components = list()
		for(var/component_id in object_data["connected_components"])
			var/obj/item/circuit_component/component = identifiers_to_circuit[component_id]
			if(!component)
				continue
			connected_components += component
		SEND_SIGNAL(object, COMSIG_MOVABLE_CIRCUIT_LOADED, src, connected_components)

	for(var/identifier in identifiers_to_circuit)
		var/obj/item/circuit_component/component = identifiers_to_circuit[identifier]
		var/list/component_data = circuit_data[identifier]

		var/list/connections = component_data["connections"]
		for(var/port_name in connections)
			var/datum/port/input/port
			var/list/connection_data = connections[port_name]
			for(var/datum/port/input/port_to_check as anything in component.input_ports)
				if(port_to_check.name == port_name)
					port = port_to_check
					break

			if(!port)
				LOG_ERROR(errors, "Port [port_name] not found for [component.type].")
				continue

			if(connection_data["stored_data"])
				if(!(port.datatype in GLOB.circuit_dupe_whitelisted_types))
					continue
				port.set_input(connection_data["stored_data"])
				continue

			// The || list(connected_data) is for backwards compatibility with when inputs could only be connected to up to one output.
			for(var/list/output_data in (connection_data["connected_ports"] || list(connection_data)))
				var/obj/item/circuit_component/connected_component = identifiers_to_circuit[output_data["component_id"]]
				if(!connected_component)
					LOG_ERROR(errors, "No connected component found for [component.type] for port [connection_data["port_name"]]. (connected component identifier: [connection_data["component_id"]])")
					continue

				var/datum/port/output/output_port
				var/output_port_name = output_data["port_name"]
				for(var/datum/port/output/port_to_check as anything in connected_component.output_ports)
					if(port_to_check.name == output_port_name)
						output_port = port_to_check
						break

				if(!output_port)
					LOG_ERROR(errors, "No output port found for [component.type] for port [output_port_name] on component [connected_component.type]")
					continue

				port.connect(output_port)

	SEND_SIGNAL(src, COMSIG_CIRCUIT_POST_LOAD)

#undef LOG_ERROR

/// Converts a circuit into json.
/obj/item/integrated_circuit/proc/convert_to_json()
	var/list/circuit_to_identifiers = list()
	var/list/identifiers = list()
	var/list/external_objects = list() // Objects that are connected to a component. These objects will be linked to the components.
	for(var/obj/item/circuit_component/component as anything in attached_components)
		var/identifier = "[component.type][length(identifiers)]"
		identifiers += identifier
		circuit_to_identifiers[component] = identifier
		var/list/objects = list()
		SEND_SIGNAL(component, COMSIG_CIRCUIT_COMPONENT_SAVE, objects)

		for(var/atom/movable/object as anything in objects)
			if(object in external_objects)
				external_objects[object] += identifier
				continue
			external_objects[object] = list(identifier)

	var/list/circuit_data = list()
	for(var/obj/item/circuit_component/component as anything in circuit_to_identifiers)
		var/identifier = circuit_to_identifiers[component]
		var/list/component_data = list()

		component_data["type"] = component.type

		var/list/connections = list()
		var/list/input_ports_stored_data = list()
		for(var/datum/port/input/input as anything in component.input_ports)
			var/list/connection_data = list()
			if(!length(input.connected_ports))
				if(isnull(input.value) || !(input.datatype in GLOB.circuit_dupe_whitelisted_types))
					continue
				connection_data["stored_data"] = input.value
				input_ports_stored_data[input.name] = connection_data
				continue
			connection_data["connected_ports"] = list()
			for(var/datum/port/output/output as anything in input.connected_ports)
				connection_data["connected_ports"] += list(list(
					"component_id" = circuit_to_identifiers[output.connected_component],
					"port_name" = output.name,
				))
			connections[input.name] = connection_data
		component_data["connections"] = connections
		component_data["input_ports_stored_data"] = input_ports_stored_data

		component.save_data_to_list(component_data)
		circuit_data[identifier] = component_data

	var/external_objects_key = list()
	for(var/atom/movable/object as anything in external_objects)
		var/list/new_data = list()
		new_data["type"] = object.type
		new_data["connected_components"] = external_objects[object]
		external_objects_key["[object.type][length(external_objects_key)]"] = new_data

	var/list/general_data = list()
	general_data["components"] = circuit_data
	general_data["external_objects"] = external_objects_key
	general_data["display_name"] = display_name
	general_data["admin_only"] = admin_only

	var/list/variables = list()
	for(var/variable_identifier in circuit_variables)
		var/list/new_data = list()
		var/datum/circuit_variable/variable = circuit_variables[variable_identifier]
		new_data["name"] = variable.name
		new_data["datatype"] = variable.datatype
		variables += list(new_data)
	general_data["variables"] = variables

	SEND_SIGNAL(src, COMSIG_CIRCUIT_PRE_SAVE_TO_JSON, general_data)

	return json_encode(general_data)

/obj/item/integrated_circuit/proc/load_component(type)
	var/obj/item/circuit_component/component = new type(src)
	add_component(component)
	return component

/// Saves data to a list. Shouldn't be used unless you are quite literally saving the data of a component to a list. Input value is the list to save the data to
/obj/item/circuit_component/proc/save_data_to_list(list/component_data)
	component_data["rel_x"] = rel_x
	component_data["rel_y"] = rel_y

/// Loads data from a list
/obj/item/circuit_component/proc/load_data_from_list(list/component_data)
	rel_x = component_data["rel_x"]
	rel_y = component_data["rel_y"]

/client/proc/load_circuit()
	set name = "Load Circuit"
	set category = "Admin.Fun"

	if(!check_rights(R_VAREDIT))
		return

	var/list/errors = list()

	var/option = alert(usr, "Load by file or direct input?", "Load by file or string", "File", "Direct Input")
	var/txt
	switch(option)
		if("File")
			txt = file2text(input(usr, "Input File") as file|null)
		if("Direct Input")
			txt = input(usr, "Input JSON", "Input JSON") as text|null

	if(!txt)
		return

	var/obj/item/integrated_circuit/loaded/circuit = new(mob.drop_location())
	circuit.load_circuit_data(txt, errors)

	if(length(errors))
		to_chat(src, span_warning("The following errors were found whilst compiling the circuit data:"))
		for(var/error in errors)
			to_chat(src, span_warning(error))


code:
/// A list of all integrated circuits
GLOBAL_LIST_EMPTY_TYPED(integrated_circuits, /obj/item/integrated_circuit)

/**
 * # Integrated Circuitboard
 *
 * A circuitboard that holds components that work together
 *
 * Has a limited amount of power.
 */
/obj/item/integrated_circuit
	name = "integrated circuit"
	desc = "By inserting components and a cell into this, wiring them up, and putting them into a shell, anyone can pretend to be a programmer."
	icon = 'icons/obj/module.dmi'
	icon_state = "integrated_circuit"
	inhand_icon_state = "electronic"

	/// The name that appears on the shell.
	var/display_name = ""

	/// The max length of the name.
	var/label_max_length = 24

	/// The power of the integrated circuit
	var/obj/item/stock_parts/cell/cell

	/// The shell that this circuitboard is attached to. Used by components.
	var/atom/movable/shell

	/// The attached components
	var/list/obj/item/circuit_component/attached_components = list()

	/// Whether the integrated circuit is on or not. Handled by the shell.
	var/on = FALSE

	/// Whether the integrated circuit is locked or not. Handled by the shell.
	var/locked = FALSE

	/// Whether the integrated circuit is admin only. Disables power usage and allows admin circuits to be attached, at the cost of making it inaccessible to regular users.
	var/admin_only = FALSE

	/// The ID that is authorized to unlock/lock the shell so that the circuit can/cannot be removed.
	var/datum/weakref/owner_id

	/// The current examined component. Used in IntegratedCircuit UI
	var/datum/weakref/examined_component

	/// Set by the shell. Holds the reference to the owner who inserted the component into the shell.
	var/datum/weakref/inserter_mind

	/// Variables stored on this integrated circuit. with a `variable_name = value` structure
	var/list/datum/circuit_variable/circuit_variables = list()

	/// The maximum amount of setters and getters a circuit can have
	var/max_setters_and_getters = 30

	/// The current setter and getter count the circuit has.
	var/setter_and_getter_count = 0

	/// X position of the examined_component
	var/examined_rel_x = 0

	/// Y position of the examined component
	var/examined_rel_y = 0

	/// The X position of the screen. Used for adding components
	var/screen_x = 0

	/// The Y position of the screen. Used for adding components.
	var/screen_y = 0

	/// The current size of the circuit.
	var/current_size = 0

/obj/item/integrated_circuit/Initialize(mapload)
	. = ..()

	GLOB.integrated_circuits += src

	RegisterSignal(src, COMSIG_ATOM_USB_CABLE_TRY_ATTACH, .proc/on_atom_usb_cable_try_attach)

/obj/item/integrated_circuit/loaded/Initialize(mapload)
	. = ..()
	set_cell(new /obj/item/stock_parts/cell/high(src))

/obj/item/integrated_circuit/Destroy()
	for(var/obj/item/circuit_component/to_delete in attached_components)
		remove_component(to_delete)
		qdel(to_delete)
	QDEL_LIST_ASSOC_VAL(circuit_variables)
	attached_components.Cut()
	shell = null
	examined_component = null
	owner_id = null
	QDEL_NULL(cell)
	GLOB.integrated_circuits -= src
	return ..()

/obj/item/integrated_circuit/examine(mob/user)
	. = ..()
	if(cell)
		. += span_notice("The charge meter reads [cell ? round(cell.percent(), 1) : 0]%.")
	else
		. += span_notice("There is no power cell installed.")

/**
 * Sets the cell of the integrated circuit.
 *
 * Arguments:
 * * cell_to_set - The new cell of the circuit. Can be null.
 **/
/obj/item/integrated_circuit/proc/set_cell(obj/item/stock_parts/cell_to_set)
	SEND_SIGNAL(src, COMSIG_CIRCUIT_SET_CELL, cell_to_set)
	cell = cell_to_set

/**
 * Sets the locked status of the integrated circuit.
 *
 * Arguments:
 * * new_value - A boolean that determines if the circuit is locked or not.
 **/
/obj/item/integrated_circuit/proc/set_locked(new_value)
	SEND_SIGNAL(src, COMSIG_CIRCUIT_SET_LOCKED, new_value)
	locked = new_value

/obj/item/integrated_circuit/attackby(obj/item/I, mob/living/user, params)
	. = ..()
	if(istype(I, /obj/item/circuit_component))
		add_component_manually(I, user)
		return

	if(istype(I, /obj/item/stock_parts/cell))
		if(cell)
			balloon_alert(user, "there already is a cell inside!")
			return
		if(!user.transferItemToLoc(I, src))
			return
		set_cell(I)
		I.add_fingerprint(user)
		user.visible_message(span_notice("[user] inserts a power cell into [src]."), span_notice("You insert the power cell into [src]."))
		return

	if(istype(I, /obj/item/card/id))
		balloon_alert(user, "owner id set for [I]")
		owner_id = WEAKREF(I)
		return

	if(I.tool_behaviour == TOOL_SCREWDRIVER)
		if(!cell)
			return
		I.play_tool_sound(src)
		user.visible_message(span_notice("[user] unscrews the power cell from [src]."), span_notice("You unscrew the power cell from [src]."))
		cell.forceMove(drop_location())
		set_cell(null)
		return

/**
 * Registers an movable atom as a shell
 *
 * No functionality is done here. This is so that input components
 * can properly register any signals on the shell.
 * Arguments:
 * * new_shell - The new shell to register.
 */
/obj/item/integrated_circuit/proc/set_shell(atom/movable/new_shell)
	remove_current_shell()
	set_on(TRUE)
	SEND_SIGNAL(src, COMSIG_CIRCUIT_SET_SHELL, new_shell)
	shell = new_shell
	RegisterSignal(shell, COMSIG_PARENT_QDELETING, .proc/remove_current_shell)
	for(var/obj/item/circuit_component/attached_component as anything in attached_components)
		attached_component.register_shell(shell)
		// Their input ports may be updated with user values, but the outputs haven't updated
		// because on is FALSE
		attached_component.trigger_component()

/**
 * Unregisters the current shell attached to this circuit.
 */
/obj/item/integrated_circuit/proc/remove_current_shell()
	SIGNAL_HANDLER
	if(!shell)
		return
	shell.name = initial(shell.name)
	for(var/obj/item/circuit_component/attached_component as anything in attached_components)
		attached_component.unregister_shell(shell)
	UnregisterSignal(shell, COMSIG_PARENT_QDELETING)
	shell = null
	set_on(FALSE)
	SEND_SIGNAL(src, COMSIG_CIRCUIT_SHELL_REMOVED)

/**
 * Sets the on status of the integrated circuit.
 *
 * Arguments:
 * * new_value - A boolean that determines if the circuit is on or not.
 **/
/obj/item/integrated_circuit/proc/set_on(new_value)
	SEND_SIGNAL(src, COMSIG_CIRCUIT_SET_ON, new_value)
	on = new_value

/**
 * Adds a component to the circuitboard
 *
 * Once the component is added, the ports can be attached to other components
 */
/obj/item/integrated_circuit/proc/add_component(obj/item/circuit_component/to_add, mob/living/user)
	if(to_add.parent)
		return

	if(SEND_SIGNAL(src, COMSIG_CIRCUIT_ADD_COMPONENT, to_add, user) & COMPONENT_CANCEL_ADD_COMPONENT)
		return

	if(!to_add.add_to(src))
		return

	var/success = FALSE
	if(user)
		success = user.transferItemToLoc(to_add, src)
	else
		success = to_add.forceMove(src)

	if(!success)
		return

	to_add.rel_x = rand(COMPONENT_MIN_RANDOM_POS, COMPONENT_MAX_RANDOM_POS) - screen_x
	to_add.rel_y = rand(COMPONENT_MIN_RANDOM_POS, COMPONENT_MAX_RANDOM_POS) - screen_y
	to_add.parent = src
	attached_components += to_add
	current_size += to_add.circuit_size
	RegisterSignal(to_add, COMSIG_MOVABLE_MOVED, .proc/component_move_handler)
	SStgui.update_uis(src)

	if(shell)
		to_add.register_shell(shell)
	return TRUE

/**
 * Adds a component to the circuitboard through a manual action.
 */
/obj/item/integrated_circuit/proc/add_component_manually(obj/item/circuit_component/to_add, mob/living/user)
	if (SEND_SIGNAL(src, COMSIG_CIRCUIT_ADD_COMPONENT_MANUALLY, to_add, user) & COMPONENT_CANCEL_ADD_COMPONENT)
		return

	return add_component(to_add, user)

/obj/item/integrated_circuit/proc/component_move_handler(obj/item/circuit_component/source)
	SIGNAL_HANDLER
	if(source.loc != src)
		remove_component(source)

/**
 * Removes a component to the circuitboard
 *
 * This removes all connects between the ports
 */
/obj/item/integrated_circuit/proc/remove_component(obj/item/circuit_component/to_remove)
	if(shell)
		to_remove.unregister_shell(shell)

	UnregisterSignal(to_remove, COMSIG_MOVABLE_MOVED)
	current_size -= to_remove.circuit_size
	attached_components -= to_remove
	to_remove.disconnect()
	to_remove.parent = null
	SEND_SIGNAL(to_remove, COMSIG_CIRCUIT_COMPONENT_REMOVED, src)
	SStgui.update_uis(src)
	to_remove.removed_from(src)

/obj/item/integrated_circuit/get_cell()
	return cell

/obj/item/integrated_circuit/ui_assets(mob/user)
	return list(
		get_asset_datum(/datum/asset/simple/circuit_assets)
	)

/obj/item/integrated_circuit/ui_static_data(mob/user)
	. = list()
	.["global_basic_types"] = GLOB.wiremod_basic_types
	.["screen_x"] = screen_x
	.["screen_y"] = screen_y

/obj/item/integrated_circuit/ui_data(mob/user)
	. = list()
	.["components"] = list()
	for(var/obj/item/circuit_component/component as anything in attached_components)
		if (component.circuit_flags & CIRCUIT_FLAG_HIDDEN)
			.["components"] += null
			continue

		var/list/component_data = list()
		component_data["input_ports"] = list()
		for(var/datum/port/input/port as anything in component.input_ports)
			var/current_data = port.value
			if(isatom(current_data)) // Prevent passing the name of the atom.
				current_data = null
			var/list/connected_to = list()
			for(var/datum/port/output/output as anything in port.connected_ports)
				connected_to += REF(output)
			component_data["input_ports"] += list(list(
				"name" = port.name,
				"type" = port.datatype,
				"ref" = REF(port), // The ref is the identifier to work out what it is connected to
				"connected_to" = connected_to,
				"color" = port.color,
				"current_data" = current_data,
				"datatype_data" = port.datatype_ui_data(user)
			))
		component_data["output_ports"] = list()
		for(var/datum/port/output/port as anything in component.output_ports)
			component_data["output_ports"] += list(list(
				"name" = port.name,
				"type" = port.datatype,
				"ref" = REF(port),
				"color" = port.color,
				"datatype_data" = port.datatype_ui_data(user)
			))

		component_data["name"] = component.display_name
		component_data["x"] = component.rel_x
		component_data["y"] = component.rel_y
		component_data["removable"] = component.removable
		component_data["color"] = component.ui_color
		component_data["ui_buttons"] = component.ui_buttons
		.["components"] += list(component_data)

	.["variables"] = list()
	for(var/variable_name in circuit_variables)
		var/datum/circuit_variable/variable = circuit_variables[variable_name]
		var/list/variable_data = list()
		variable_data["name"] = variable.name
		variable_data["datatype"] = variable.datatype
		variable_data["color"] = variable.color
		.["variables"] += list(variable_data)

	.["display_name"] = display_name

	var/obj/item/circuit_component/examined
	if(examined_component)
		examined = examined_component.resolve()

	.["examined_name"] = examined?.display_name
	.["examined_desc"] = examined?.desc
	.["examined_notices"] = examined?.get_ui_notices()
	.["examined_rel_x"] = examined_rel_x
	.["examined_rel_y"] = examined_rel_y

	.["is_admin"] = check_rights_for(user.client, R_VAREDIT)

/obj/item/integrated_circuit/ui_host(mob/user)
	if(shell)
		return shell
	return ..()

/obj/item/integrated_circuit/can_interact(mob/user)
	if(locked)
		return FALSE
	return ..()

/obj/item/integrated_circuit/ui_status(mob/user)
	. = ..()

	if (isobserver(user))
		. = max(., UI_UPDATE)

	// Extra protection because ui_state will not close the UI if they already have the ui open,
	// as ui_state is only set during
	if(admin_only)
		if(!check_rights_for(user.client, R_VAREDIT))
			return UI_CLOSE
		else
			return UI_INTERACTIVE

/obj/item/integrated_circuit/ui_state(mob/user)
	if(!shell)
		return GLOB.hands_state
	return GLOB.physical_obscured_state

/obj/item/integrated_circuit/ui_interact(mob/user, datum/tgui/ui)
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "IntegratedCircuit", name)
		ui.open()
		ui.set_autoupdate(FALSE)

#define WITHIN_RANGE(id, table) (id >= 1 && id <= length(table))

/obj/item/integrated_circuit/ui_act(action, list/params, datum/tgui/ui)
	. = ..()
	if(.)
		return

	switch(action)
		if("add_connection")
			var/input_component_id = text2num(params["input_component_id"])
			var/output_component_id = text2num(params["output_component_id"])
			var/input_port_id = text2num(params["input_port_id"])
			var/output_port_id = text2num(params["output_port_id"])
			if(!WITHIN_RANGE(input_component_id, attached_components) || !WITHIN_RANGE(output_component_id, attached_components))
				return
			var/obj/item/circuit_component/input_component = attached_components[input_component_id]
			var/obj/item/circuit_component/output_component = attached_components[output_component_id]

			if(!WITHIN_RANGE(input_port_id, input_component.input_ports) || !WITHIN_RANGE(output_port_id, output_component.output_ports))
				return
			var/datum/port/input/input_port = input_component.input_ports[input_port_id]
			var/datum/port/output/output_port = output_component.output_ports[output_port_id]

			if(!input_port.can_receive_from_datatype(output_port.datatype))
				return
			input_port.connect(output_port)
			. = TRUE
		if("remove_connection")
			var/component_id = text2num(params["component_id"])
			var/is_input = params["is_input"]
			var/port_id = text2num(params["port_id"])

			if(!WITHIN_RANGE(component_id, attached_components))
				return
			var/obj/item/circuit_component/component = attached_components[component_id]

			var/list/port_table
			if(is_input)
				port_table = component.input_ports
			else
				port_table = component.output_ports

			if(!WITHIN_RANGE(port_id, port_table))
				return

			var/datum/port/port = port_table[port_id]
			port.disconnect_all()
			. = TRUE
		if("detach_component")
			var/component_id = text2num(params["component_id"])
			if(!WITHIN_RANGE(component_id, attached_components))
				return
			var/obj/item/circuit_component/component = attached_components[component_id]
			if(!component.removable)
				return
			component.disconnect()
			remove_component(component)
			if(component.loc == src)
				usr.put_in_hands(component)
			. = TRUE
		if("set_component_coordinates")
			var/component_id = text2num(params["component_id"])
			if(!WITHIN_RANGE(component_id, attached_components))
				return
			var/obj/item/circuit_component/component = attached_components[component_id]
			component.rel_x = min(max(-COMPONENT_MAX_POS, text2num(params["rel_x"])), COMPONENT_MAX_POS)
			component.rel_y = min(max(-COMPONENT_MAX_POS, text2num(params["rel_y"])), COMPONENT_MAX_POS)
			. = TRUE
		if("set_component_input")
			var/component_id = text2num(params["component_id"])
			var/port_id = text2num(params["port_id"])
			if(!WITHIN_RANGE(component_id, attached_components))
				return
			var/obj/item/circuit_component/component = attached_components[component_id]
			if(!WITHIN_RANGE(port_id, component.input_ports))
				return
			var/datum/port/input/port = component.input_ports[port_id]

			if(params["set_null"])
				port.set_input(null)
				return TRUE

			if(params["marked_atom"])
				if(!(port.datatype_handler.datatype_flags & DATATYPE_FLAG_ALLOW_ATOM_INPUT))
					return
				var/obj/item/multitool/circuit/marker = usr.get_active_held_item()
				// Let's admins upload marked datums to an entity port.
				if(!istype(marker))
					var/client/user = usr.client
					if(!check_rights_for(user, R_VAREDIT))
						return TRUE
					var/atom/marked_atom = user.holder.marked_datum
					if(!marked_atom)
						return TRUE
					port.set_input(marked_atom)
					balloon_alert(usr, "updated [port.name]'s value to marked object.")
					return TRUE
				if(!marker.marked_atom)
					port.set_input(null)
					marker.say("Cleared port ('[port.name]')'s value.")
					return TRUE
				marker.say("Updated port ('[port.name]')'s value to the marked entity.")
				port.set_input(marker.marked_atom)
				return TRUE

			var/user_input = port.handle_manual_input(usr, params["input"])
			if(isnull(user_input))
				return TRUE
			port.set_input(user_input)
			. = TRUE
		if("get_component_value")
			var/component_id = text2num(params["component_id"])
			var/port_id = text2num(params["port_id"])
			if(!WITHIN_RANGE(component_id, attached_components))
				return
			var/obj/item/circuit_component/component = attached_components[component_id]
			if(!WITHIN_RANGE(port_id, component.output_ports))
				return

			var/datum/port/output/port = component.output_ports[port_id]
			var/value = port.value
			if(isatom(value))
				value = PORT_TYPE_ATOM
			else if(isnull(value))
				value = "null"
			var/string_form = copytext("[value]", 1, PORT_MAX_STRING_DISPLAY)
			if(length(string_form) >= PORT_MAX_STRING_DISPLAY-1)
				string_form += "..."
			balloon_alert(usr, "[port.name] value: [string_form]")
			. = TRUE
		if("set_display_name")
			var/new_name = params["display_name"]

			if(new_name)
				set_display_name(strip_html(params["display_name"], label_max_length))
			else
				set_display_name("")

			if(shell)
				if(display_name != "")
					shell.name = "[initial(shell.name)] ([display_name])"
				else
					shell.name = initial(shell.name)

			. = TRUE
		if("set_examined_component")
			var/component_id = text2num(params["component_id"])
			if(!WITHIN_RANGE(component_id, attached_components))
				return
			examined_component = WEAKREF(attached_components[component_id])
			examined_rel_x = text2num(params["x"])
			examined_rel_y = text2num(params["y"])
			. = TRUE
		if("remove_examined_component")
			examined_component = null
			. = TRUE
		if("save_circuit")
			return attempt_save_to(usr.client)
		if("add_variable")
			var/variable_identifier = trim(copytext(params["variable_name"], 1, PORT_MAX_NAME_LENGTH))
			if(variable_identifier in circuit_variables)
				return TRUE
			if(variable_identifier == "")
				return TRUE
			var/variable_datatype = params["variable_datatype"]
			if(!(variable_datatype in GLOB.wiremod_basic_types))
				return
			if(params["is_list"])
				variable_datatype = PORT_TYPE_LIST(variable_datatype)
			circuit_variables[variable_identifier] = new /datum/circuit_variable(variable_identifier, variable_datatype)
			. = TRUE
		if("remove_variable")
			var/variable_identifier = params["variable_name"]
			if(!(variable_identifier in circuit_variables))
				return
			var/datum/circuit_variable/variable = circuit_variables[variable_identifier]
			if(!variable)
				return
			circuit_variables -= variable_identifier
			qdel(variable)
			. = TRUE
		if("add_setter_or_getter")
			if(setter_and_getter_count >= max_setters_and_getters)
				balloon_alert(usr, "setter and getter count at maximum capacity")
				return
			var/designated_type = /obj/item/circuit_component/variable/getter
			if(params["is_setter"])
				designated_type = /obj/item/circuit_component/variable/setter
			var/obj/item/circuit_component/variable/component = new designated_type(src)
			if(!add_component(component, usr))
				qdel(component)
				return
			component.variable_name.set_input(params["variable"])
			component.rel_x = text2num(params["rel_x"])
			component.rel_y = text2num(params["rel_y"])
			RegisterSignal(component, COMSIG_CIRCUIT_COMPONENT_REMOVED, .proc/clear_setter_or_getter)
			setter_and_getter_count++
			return TRUE
		if("move_screen")
			screen_x = text2num(params["screen_x"])
			screen_y = text2num(params["screen_y"])
		if("perform_action")
			var/component_id = text2num(params["component_id"])
			if(!WITHIN_RANGE(component_id, attached_components))
				return
			var/obj/item/circuit_component/component = attached_components[component_id]
			component.ui_perform_action(ui.user, params["action_name"])

#undef WITHIN_RANGE

/obj/item/integrated_circuit/proc/clear_setter_or_getter(datum/source)
	SIGNAL_HANDLER
	// This'll also be called in the Destroy() override of /obj/item/circuit_component
	if(!QDELING(source))
		qdel(source)
	setter_and_getter_count--

/obj/item/integrated_circuit/proc/on_atom_usb_cable_try_attach(datum/source, obj/item/usb_cable/usb_cable, mob/user)
	SIGNAL_HANDLER
	usb_cable.balloon_alert(user, "circuit needs to be in a compatible shell")
	return COMSIG_CANCEL_USB_CABLE_ATTACK

/// Sets the display name that appears on the shell.
/obj/item/integrated_circuit/proc/set_display_name(new_name)
	display_name = new_name

/**
 * Returns the creator of the integrated circuit. Used in admin messages and other related things.
 */
/obj/item/integrated_circuit/proc/get_creator_admin()
	return get_creator(include_link = TRUE)

/**
 * Returns the creator of the integrated circuit. Used in admin logs and other related things.
 */
/obj/item/integrated_circuit/proc/get_creator(include_link = FALSE)
	var/datum/mind/inserter
	if(inserter_mind)
		inserter = inserter_mind.resolve()

	var/obj/item/card/id/id_card
	if(owner_id)
		id_card = owner_id.resolve()

	return "[src] (Shell: [shell || "*null*"], Inserter: [key_name(inserter, include_link)], Owner ID: [id_card?.name || "*null*"])"

/// Attempts to save a circuit to a given client
/obj/item/integrated_circuit/proc/attempt_save_to(client/saver)
	if(!check_rights_for(saver, R_VAREDIT))
		return FALSE
	var/temp_file = file("data/CircuitDownloadTempFile")
	fdel(temp_file)
	WRITE_FILE(temp_file, convert_to_json())
	DIRECT_OUTPUT(saver, ftp(temp_file, "[display_name || "circuit"].json"))
	return TRUE


code:
/obj/item/multitool/circuit
	name = "circuit multitool"
	desc = "A circuit multitool. Used to mark entities which can then be uploaded to components by pressing the upload button on a port. \
	Acts as a normal multitool otherwise. Use in hand to clear marked entity so that you can mark another entity."
	icon_state = "multitool_circuit"

	/// The marked atom of this multitool
	var/atom/marked_atom

/obj/item/multitool/circuit/Destroy()
	marked_atom = null
	return ..()

/obj/item/multitool/circuit/examine(mob/user)
	. = ..()
	. += span_notice("It has [marked_atom? "a" : "no"] marked entity registered.")

/obj/item/multitool/circuit/attack_self(mob/user, modifiers)
	. = ..()
	if(.)
		return
	if(!marked_atom)
		return

	say("Cleared marked targets.")
	clear_marked_atom()
	return TRUE

/obj/item/multitool/circuit/melee_attack_chain(mob/user, atom/target, params)
	var/is_right_clicking = LAZYACCESS(params2list(params), RIGHT_CLICK)

	if(marked_atom || !user.Adjacent(target) || is_right_clicking)
		return ..()

	say("Marked [target].")
	marked_atom = target
	RegisterSignal(marked_atom, COMSIG_PARENT_QDELETING, .proc/cleanup_marked_atom)
	update_icon()
	flick("multitool_circuit_flick", src)
	playsound(src.loc, 'sound/misc/compiler-stage2.ogg', 30, TRUE)
	return TRUE

/obj/item/multitool/circuit/update_overlays()
	. = ..()
	cut_overlays()
	if(marked_atom)
		. += "marked_overlay"

/// Clears the current marked atom
/obj/item/multitool/circuit/proc/clear_marked_atom()
	if(!marked_atom)
		return
	UnregisterSignal(marked_atom, COMSIG_PARENT_QDELETING)
	marked_atom = null
	update_icon()

/obj/item/multitool/circuit/proc/cleanup_marked_atom(datum/source)
	SIGNAL_HANDLER
	if(source == marked_atom)
		clear_marked_atom()


code:
/**
 * # Component Port
 *
 * A port used by a component. Connects to other ports.
 */
/datum/port
	/// The component this port is attached to
	var/obj/item/circuit_component/connected_component

	/// Name of the port. Used when displaying the port.
	var/name

	/// The port type. Ports can only connect to each other if the type matches
	var/datatype

	/// The value that's currently in the port. It's of the above type.
	var/value

	/// The default port type. Stores the original datatype of the port set on Initialize.
	var/datum/circuit_datatype/datatype_handler

	/// The port color. If unset, appears as blue.
	var/color

	/// The weight of the port. Determines the
	var/order = 1

/datum/port/New(obj/item/circuit_component/to_connect, name, datatype, order = 1)
	if(!to_connect)
		qdel(src)
		return
	. = ..()
	connected_component = to_connect
	src.name = name
	src.order = order
	set_datatype(datatype)

/datum/port/Destroy(force)
	disconnect_all()
	connected_component = null
	datatype_handler = null
	return ..()

/**
 * Sets the port's value to value.
 * Casts to the port's datatype (e.g. number -> string), and assumes this can be done.
 */
/datum/port/proc/set_value(value, force = FALSE)
	if(isweakref(value))
		var/datum/weakref/reference_to_obj = value
		value = reference_to_obj.resolve()

	if(src.value != value || force)
		if(isatom(src.value))
			UnregisterSignal(src.value, COMSIG_PARENT_QDELETING)
		src.value = datatype_handler.convert_value(src, value, force)
		if(isatom(value))
			RegisterSignal(value, COMSIG_PARENT_QDELETING, .proc/null_value)
	SEND_SIGNAL(src, COMSIG_PORT_SET_VALUE, value)

/**
 * Updates the value of the input and calls input_received on the connected component
 */
/datum/port/input/proc/set_input(value, list/return_values)
	if(QDELETED(src)) //Pain
		return
	set_value(value)
	if(trigger)
		connected_component.trigger_component(src, return_values)

/datum/port/output/proc/set_output(value)
	set_value(value)

/**
 * Sets the datatype of the port.
 *
 * Arguments:
 * * new_type - The type this port is to be set to.
 */
/datum/port/proc/set_datatype(type_to_set)
	if(type_to_set == datatype)
		return

	if(datatype_handler)
		datatype_handler.on_loss(src)
	datatype_handler = null

	var/datum/circuit_datatype/handler = GLOB.circuit_datatypes[type_to_set]
	if(!handler || !handler.is_compatible(src))
		type_to_set = PORT_TYPE_ANY
		handler = GLOB.circuit_datatypes[type_to_set]
		// We can't leave this port without a type or else it'll just keep spewing out unnecessary and unneeded runtimes as well as leaving the circuit in a broken state.
		stack_trace("[src] port attempted to be set to an incompatible datatype! (target datatype to set: [type_to_set])")

	datatype = type_to_set
	datatype_handler = handler
	color = datatype_handler.color
	datatype_handler.on_gain(src)
	src.value = null
	SEND_SIGNAL(src, COMSIG_PORT_SET_TYPE, type_to_set)
	if(connected_component?.parent)
		SStgui.update_uis(connected_component.parent)

/datum/port/input/set_datatype(new_type)
	for(var/datum/port/output/output as anything in connected_ports)
		check_type(output)
	..()

/**
 * Returns the data from the datatype
 */
/datum/port/proc/datatype_ui_data()
	return datatype_handler.datatype_ui_data(src)

/**
 * # Output Port
 *
 * An output port that many input ports can connect to
 *
 * Sends a signal whenever the output value is changed
 */
/datum/port/output

/**
 * Disconnects a port from all other ports.
 *
 * Called by [/obj/item/circuit_component] whenever it is disconnected from
 * an integrated circuit
 */
/datum/port/proc/disconnect_all()
	value = null
	SEND_SIGNAL(src, COMSIG_PORT_DISCONNECT)

/datum/port/input/disconnect_all()
	..()
	for(var/datum/port/output/output as anything in connected_ports)
		disconnect(output)

/datum/port/input/proc/disconnect(datum/port/output/output)
	SIGNAL_HANDLER
	connected_ports -= output
	UnregisterSignal(output, COMSIG_PORT_SET_VALUE)
	UnregisterSignal(output, COMSIG_PORT_SET_TYPE)
	UnregisterSignal(output, COMSIG_PORT_DISCONNECT)

/// Do our part in setting all source references anywhere to null.
/datum/port/proc/on_value_qdeleting(datum/source)
	SIGNAL_HANDLER
	if(value == source)
		value = null
	else
		stack_trace("Impossible? [src] should only receive COMSIG_PARENT_QDELETING from an atom currently in the port, not [source].")

/**
 * # Input Port
 *
 * An input port remembers connected output ports.
 *
 * Registers the PORT_SET_VALUE signal on each connected port,
 * and keeps its value equal to the last such signal received.
 */
/datum/port/input
	/// The proc that this trigger will call on the connected component.
	var/trigger

	/// The ports this port is wired to.
	var/list/datum/port/output/connected_ports

/datum/port/input/New(obj/item/circuit_component/to_connect, name, datatype, order = 1, trigger = null, default = null)
	. = ..()
	set_value(default)
	if(trigger)
		src.trigger = trigger
	src.connected_ports = list()

/**
 * Connects an input port to an output port.
 *
 * Arguments:
 * * output - The output port to connect to.
 */
/datum/port/input/proc/connect(datum/port/output/output)
	if(output in connected_ports)
		return
	connected_ports += output
	RegisterSignal(output, COMSIG_PORT_SET_VALUE, .proc/receive_value)
	RegisterSignal(output, COMSIG_PORT_SET_TYPE, .proc/check_type)
	RegisterSignal(output, COMSIG_PORT_DISCONNECT, .proc/disconnect)
	// For signals, we don't update the input to prevent sending a signal when connecting ports.
	if(!(datatype_handler.datatype_flags & DATATYPE_FLAG_AVOID_VALUE_UPDATE))
		set_input(output.value)

/datum/port/input/set_datatype(new_type)
	. = ..()
	for(var/datum/port/output/port as anything in connected_ports)
		check_type(port)

/**
 * Determines if a datatype is compatible with another port of a different type.
 *
 * Arguments:
 * * other_datatype - The datatype to check
 */
/datum/port/input/proc/can_receive_from_datatype(datatype_to_check)
	return datatype_handler.can_receive_from_datatype(datatype_to_check)

/**
 * Determines if a datatype is compatible with another port of a different type.
 *
 * Arguments:
 * * other_datatype - The datatype to check
 */
/datum/port/input/proc/handle_manual_input(mob/user, manual_input)
	if(datatype_handler.datatype_flags & DATATYPE_FLAG_ALLOW_MANUAL_INPUT)
		return datatype_handler.handle_manual_input(src, user, manual_input)
	return null

/**
 * Mirror value updates from connected output ports after an input_receive_delay.
 */
/datum/port/input/proc/receive_value(datum/port/output/output, value)
	SIGNAL_HANDLER
	SScircuit_component.add_callback(src, CALLBACK(src, .proc/set_input, value))

/// Signal handler proc to null the input if an atom is deleted. An update is not sent because this was not set by anything.
/datum/port/proc/null_value(datum/source)
	SIGNAL_HANDLER
	if(value == source)
		value = null

/**
 * Handle type updates from connected output ports, breaking uncastable connections.
 */
/datum/port/input/proc/check_type(datum/port/output/output)
	SIGNAL_HANDLER
	if(!can_receive_from_datatype(output.datatype))
		disconnect(output)


code:
/// A cable that can connect integrated circuits to anything with a USB port, such as computers and machines.
/obj/item/usb_cable
	name = "usb cable"
	desc = "A cable that can connect integrated circuits to anything with a USB port, such as computers and machines."
	icon = 'icons/obj/wiremod.dmi'
	icon_state = "usb_cable"
	inhand_icon_state = "coil"
	base_icon_state = "coil"
	w_class = WEIGHT_CLASS_TINY
	custom_materials = list(/datum/material/iron = 75)

	/// The currently connected circuit
	var/obj/item/integrated_circuit/attached_circuit

/obj/item/usb_cable/Destroy()
	attached_circuit = null
	STOP_PROCESSING(SSobj, src)
	return ..()

/obj/item/usb_cable/Initialize(mapload)
	. = ..()
	RegisterSignal(src, COMSIG_MOVABLE_MOVED, .proc/on_moved)

/obj/item/usb_cable/examine(mob/user)
	. = ..()

	if (!isnull(attached_circuit))
		. += span_notice("It is attached to [attached_circuit.shell || attached_circuit].")

// Look, I'm not happy about this either, but moving an object doesn't call Moved if it's inside something else.
// There's good reason for this, but there's no element or similar yet to track it as far as I know.
// SSobj runs infrequently, this is only ran while there's an attached circuit, its performance cost is negligible.
/obj/item/usb_cable/process(delta_time)
	if (!check_in_range())
		return PROCESS_KILL

/obj/item/usb_cable/pre_attack(atom/target, mob/living/user, params)
	. = ..()
	if (.)
		return

	if (prob(1))
		balloon_alert(user, "wrong way, god damnit")
		return TRUE

	var/signal_result = SEND_SIGNAL(target, COMSIG_ATOM_USB_CABLE_TRY_ATTACH, src, user)

	var/last_attached_circuit = attached_circuit
	if (signal_result & COMSIG_USB_CABLE_CONNECTED_TO_CIRCUIT)
		if (isnull(attached_circuit))
			CRASH("Producers of COMSIG_USB_CABLE_CONNECTED_TO_CIRCUIT must set attached_circuit")
		balloon_alert(user, "connected to circuit\nconnect to a port")

		playsound(src, 'sound/machines/pda_button1.ogg', 20, TRUE)

		if (last_attached_circuit != attached_circuit)
			if (!isnull(last_attached_circuit))
				unregister_circuit_signals(last_attached_circuit)
			register_circuit_signals()

		START_PROCESSING(SSobj, src)

		return TRUE

	if (signal_result & COMSIG_USB_CABLE_ATTACHED)
		// Short messages are better to read
		var/connection_description = "port"
		if (istype(target, /obj/machinery/computer))
			connection_description = "computer"
		else if (ismachinery(target))
			connection_description = "machine"

		balloon_alert(user, "connected to [connection_description]")
		playsound(src, 'sound/items/screwdriver2.ogg', 20, TRUE)

		return TRUE

	if (signal_result & COMSIG_CANCEL_USB_CABLE_ATTACK)
		return TRUE

	return FALSE

/obj/item/usb_cable/suicide_act(mob/user)
	user.visible_message(span_suicide("[user] is wrapping [src] around [user.p_their()] neck! It looks like [user.p_theyre()] trying to commit suicide!"))
	return OXYLOSS

/obj/item/usb_cable/proc/register_circuit_signals()
	RegisterSignal(attached_circuit, COMSIG_MOVABLE_MOVED, .proc/on_moved)
	RegisterSignal(attached_circuit, COMSIG_PARENT_QDELETING, .proc/on_circuit_qdeling)
	RegisterSignal(attached_circuit.shell, COMSIG_MOVABLE_MOVED, .proc/on_moved)

/obj/item/usb_cable/proc/unregister_circuit_signals(obj/item/integrated_circuit/old_circuit)
	UnregisterSignal(attached_circuit, list(
		COMSIG_MOVABLE_MOVED,
		COMSIG_PARENT_QDELETING,
	))

	UnregisterSignal(attached_circuit.shell, COMSIG_MOVABLE_MOVED)

/obj/item/usb_cable/proc/on_moved()
	SIGNAL_HANDLER

	check_in_range()

/obj/item/usb_cable/proc/check_in_range()
	if (isnull(attached_circuit))
		STOP_PROCESSING(SSobj, src)
		return FALSE

	if (!IN_GIVEN_RANGE(attached_circuit, src, USB_CABLE_MAX_RANGE))
		balloon_alert_to_viewers("detached, too far away")
		unregister_circuit_signals(attached_circuit)
		attached_circuit = null
		STOP_PROCESSING(SSobj, src)
		return FALSE

	return TRUE

/obj/item/usb_cable/proc/on_circuit_qdeling()
	SIGNAL_HANDLER

	attached_circuit = null
	STOP_PROCESSING(SSobj, src)


code:
/**
 * A circuit variable that holds the name, the datatype and the colour of the variable (taken from the datatype).
 *
 * Used in integrated circuits for setter and getter circuit components.
 */
/datum/circuit_variable
	/// The display name of the circuit variable
	var/name

	/// The datatype of the circuit variable. Used by the setter and getter circuit components
	var/datatype

	/// The datatype handler for the circuit variable.
	var/datum/circuit_datatype/datatype_handler

	/// The colour that appears in the UI. The value is set to the datatype's matching colour
	var/color

	/// The current value held by the variable.
	var/value

	/// The components that are currently listening. Triggers them when the value is updated.
	var/list/obj/item/circuit_component/listeners

/datum/circuit_variable/New(name, datatype)
	. = ..()
	src.name = name
	src.datatype = datatype

	src.datatype_handler = GLOB.circuit_datatypes[datatype]
	src.listeners = list()
	src.color = datatype_handler.color

/// Sets the value of the circuit component and triggers the appropriate listeners
/datum/circuit_variable/proc/set_value(new_value)
	value = new_value
	for(var/obj/item/circuit_component/component as anything in listeners)
		component.trigger_component()

/datum/circuit_variable/proc/on_listener_qdel(datum/listener)
	SIGNAL_HANDLER
	listeners -= listener

/// Adds a listener to receive inputs when the variable has a value that is set.
/datum/circuit_variable/proc/add_listener(obj/item/circuit_component/to_add)
	listeners += to_add
	RegisterSignal(to_add, COMSIG_PARENT_QDELETING, .proc/on_listener_qdel)

/// Removes a listener to receive inputs when the variable has a value that is set. Listener will usually clean themselves up
/datum/circuit_variable/proc/remove_listener(obj/item/circuit_component/to_remove)
	UnregisterSignal(to_remove, COMSIG_PARENT_QDELETING)
	listeners -= to_remove


code:
/datum/circuit_datatype/any
	datatype = PORT_TYPE_ANY
	color = "blue"
	datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT|DATATYPE_FLAG_ALLOW_ATOM_INPUT

/datum/circuit_datatype/any/can_receive_from_datatype(datatype_to_check)
	return TRUE

/datum/circuit_datatype/any/handle_manual_input(datum/port/input/port, mob/user, user_input)
	var/result = text2num(user_input)
	if(isnull(result))
		return user_input
	return result


code:
// This file is for types that do not have any special conversion behaviour

/datum/circuit_datatype/table
	datatype = PORT_TYPE_TABLE
	color = "grey"


code:
/datum/circuit_composite_template/assoc_list
	datatype = PORT_COMPOSITE_TYPE_ASSOC_LIST
	composite_datatype_path = /datum/circuit_datatype/composite_instance/assoc_list
	expected_types = 2

/datum/circuit_composite_template/assoc_list/generate_name(list/composite_datatypes)
	return "[composite_datatypes[1]], [composite_datatypes[2]] list"

/datum/circuit_datatype/composite_instance/assoc_list
	color = "white"
	datatype_flags = DATATYPE_FLAG_COMPOSITE

/datum/circuit_datatype/composite_instance/assoc_list/convert_value(datum/port/port, value_to_convert, force)
	var/datum/circuit_datatype/key_handler = GLOB.circuit_datatypes[composite_datatypes[1]]
	var/datum/circuit_datatype/value_handler = GLOB.circuit_datatypes[composite_datatypes[2]]

	var/list/converted_list = list()
	for(var/data in value_to_convert)
		converted_list[key_handler.convert_value(port, data)] = value_handler.convert_value(port, value_to_convert[data])
	return converted_list


code:
/// A template used to make composite datatypes for circuits.
/// Used so that we don't have to generate every single possible combination of types
/datum/circuit_composite_template
	/// The datatype this composite template is of.
	var/datatype
	/// The path to the composite type
	var/composite_datatype_path
	/// The amount of composite datatypes needed to generate a datatype from this template
	var/expected_types = 1

	/// Types to generate during initialization
	var/list/types_to_generate = list()

	var/list/datum/circuit_datatype/composite_instance/generated_types = list()

/**
 * Generates a name for the datatype using the unique list of composite datatypes.
 * This should always generate a constant value for the input given as it is used as
 * the unique identifier
 **/
/datum/circuit_composite_template/proc/generate_name(list/composite_datatypes)
	SHOULD_BE_PURE(TRUE)
	return "[datatype]<[composite_datatypes.Join(", ")]>"

/**
 * Generates a composite type using this template. Not recommended to use directly, use a define instead.
 *
 * Arguments:
 * * composite_datatypes - The list of composite datatypes to use to generate this type.
 **/
/datum/circuit_composite_template/proc/generate_composite_type(list/composite_datatypes)
	var/new_datatype = generate_name(composite_datatypes)

	if(!GLOB.circuit_datatypes)
		types_to_generate += list(composite_datatypes)
		return new_datatype

	if(expected_types != length(composite_datatypes))
		CRASH("Invalid amount of composite datatypes passed to [type]. Expected [expected_types], got [length(composite_datatypes)] arguments.")

	if(GLOB.circuit_datatypes[new_datatype])
		return new_datatype
	for(var/datatype_to_check in composite_datatypes)
		if(!GLOB.circuit_datatypes[datatype_to_check])
			CRASH("Attempted to form an invalid composite datatype using datatypes that don't exist! (got [datatype_to_check], expected a valid datatype)")
	var/datum/circuit_datatype/composite_instance/generated_type = new composite_datatype_path(new_datatype, datatype, composite_datatypes)
	GLOB.circuit_datatypes[new_datatype] = generated_type
	generated_types[new_datatype] = generated_type
	return new_datatype

/**
 * Used to generate composite types from anything that was generated
 * before global variables were all initialized.
 * Used for when composite datatypes are used in globally defined lists, before GLOB.circuit_datatypes is available.
 **/
/datum/circuit_composite_template/proc/Initialize()
	if(types_to_generate)
		for(var/list/data as anything in types_to_generate)
			generate_composite_type(data)

/// A composite instance generated by a template
/datum/circuit_datatype/composite_instance
	datatype_flags = DATATYPE_FLAG_COMPOSITE
	abstract = TRUE

	/// The base datatype, used for comparisons
	var/base_datatype

	/// The composite datatypes that make this datatype up
	var/list/composite_datatypes

	/// A list composed of the composite datatypes sent to the UI.
	var/list/composite_datatypes_style

/datum/circuit_datatype/composite_instance/New(datatype, base_datatype, list/composite_datatypes)
	. = ..()
	if(!datatype || !composite_datatypes)
		return

	src.datatype = datatype
	src.base_datatype = base_datatype
	src.composite_datatypes = composite_datatypes
	abstract = FALSE

	composite_datatypes_style += list()
	for(var/datatype_to_check in composite_datatypes)
		composite_datatypes_style += GLOB.circuit_datatypes[datatype_to_check].color

/datum/circuit_datatype/composite_instance/can_receive_from_datatype(datatype_to_check)
	. = ..()
	if(.)
		return

	var/datum/circuit_datatype/composite_instance/datatype_handler = SSwiremod_composite.get_composite_type(base_datatype, datatype_to_check)
	if(!datatype_handler || length(datatype_handler.composite_datatypes) != length(composite_datatypes))
		return FALSE

	for(var/index in 1 to length(composite_datatypes))
		if(!GLOB.circuit_datatypes[composite_datatypes[index]].can_receive_from_datatype(datatype_handler.composite_datatypes[index]))
			return FALSE
	return TRUE

/datum/circuit_datatype/composite_instance/datatype_ui_data(datum/port/port)
	var/list/ui_data = list()

	ui_data["composite_types"] = composite_datatypes_style
	return ui_data

/datum/circuit_datatype/composite_instance/get_datatypes()
	return composite_datatypes


code:
/datum/circuit_composite_template/list
	datatype = PORT_COMPOSITE_TYPE_LIST
	composite_datatype_path = /datum/circuit_datatype/composite_instance/list
	expected_types = 1

/datum/circuit_composite_template/list/generate_name(list/composite_datatypes)
	return "[composite_datatypes[1]] [datatype]"

/datum/circuit_datatype/composite_instance/list
	color = "white"
	datatype_flags = DATATYPE_FLAG_COMPOSITE

/datum/circuit_datatype/composite_instance/list/convert_value(datum/port/port, value_to_convert, force)
	var/datum/circuit_datatype/datatype_handler = GLOB.circuit_datatypes[composite_datatypes[1]]

	var/list/converted_list = list()
	for(var/data in value_to_convert)
		converted_list += list(datatype_handler.convert_value(port, data))
	return converted_list


code:
/datum/circuit_datatype/datum
	datatype = PORT_TYPE_DATUM
	color = "yellow"
	datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT|DATATYPE_FLAG_ALLOW_ATOM_INPUT
	can_receive_from = list(
		PORT_TYPE_ATOM,
	)

/datum/circuit_datatype/datum/convert_value(datum/port/port, value_to_convert)
	var/datum/object = value_to_convert
	if(QDELETED(object))
		return null
	return object


code:
/datum/circuit_datatype/entity
	datatype = PORT_TYPE_ATOM
	color = "purple"
	datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT|DATATYPE_FLAG_ALLOW_ATOM_INPUT

/datum/circuit_datatype/entity/convert_value(datum/port/port, value_to_convert)
	var/atom/object = value_to_convert
	if(QDELETED(object))
		return null
	return object


code:
/datum/circuit_datatype/number
	datatype = PORT_TYPE_NUMBER
	color = "green"
	datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT

/datum/circuit_datatype/number/handle_manual_input(datum/port/input/port, mob/user, user_input)
	return text2num(user_input)


code:
/datum/port/input/option
	var/list/possible_options

/datum/port/input/option/New(obj/item/circuit_component/to_connect, name, datatype, order = 1, trigger = null, default = null, possible_options)
	. = ..()
	src.possible_options = possible_options
	if(length(possible_options))
		set_value(possible_options[1])

/datum/circuit_datatype/option
	datatype = PORT_TYPE_OPTION
	color = "violet"
	datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT
	can_receive_from = list(
		PORT_TYPE_STRING,
	)

/datum/circuit_datatype/option/is_compatible(datum/port/gained_port)
	return istype(gained_port, /datum/port/input/option)

/datum/circuit_datatype/option/convert_value(datum/port/input/option/port, value_to_convert, force = FALSE)
	if(force)
		return value_to_convert

	if(!port.possible_options)
		return null

	if(value_to_convert in port.possible_options)
		return value_to_convert
	return port.possible_options[1]

/datum/circuit_datatype/option/datatype_ui_data(datum/port/input/option/port)
	return port.possible_options


code:
/datum/circuit_datatype/signal
	datatype = PORT_TYPE_SIGNAL
	color = "teal"
	datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT|DATATYPE_FLAG_AVOID_VALUE_UPDATE
	can_receive_from = list(
		PORT_TYPE_NUMBER,
		PORT_TYPE_INSTANT_SIGNAL,
		PORT_TYPE_RESPONSE_SIGNAL,
	)

/datum/circuit_datatype/signal/handle_manual_input(datum/port/input/port, mob/user, user_input)
	var/atom/parent = port.connected_component
	if(parent)
		parent.balloon_alert(user, "triggered [port.name]")
	return COMPONENT_SIGNAL

/datum/circuit_datatype/signal/instant_signal
	datatype = PORT_TYPE_INSTANT_SIGNAL

/datum/circuit_datatype/signal/instant_signal/is_compatible(datum/port/port)
	return istype(port, /datum/port/output)

/datum/circuit_datatype/signal/response_signal
	datatype = PORT_TYPE_RESPONSE_SIGNAL

/datum/circuit_datatype/signal/response_signal/is_compatible(datum/port/port)
	return istype(port, /datum/port/input)


code:
/datum/circuit_datatype/string
	datatype = PORT_TYPE_STRING
	color = "orange"
	datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT

/datum/circuit_datatype/string/can_receive_from_datatype(datatype_to_check)
	return TRUE

/datum/circuit_datatype/string/convert_value(datum/port/port, value_to_convert)
	if(isnull(value_to_convert))
		return

	// So that they can't easily get the name like this.
	if(isatom(value_to_convert))
		return PORT_TYPE_ATOM
	else
		return copytext("[value_to_convert]", 1, PORT_MAX_STRING_LENGTH)


code:
/**
 * # Hello World preset
 *
 * Says "Hello World" when triggered. Needs to be wired up and connected first.
 */
/obj/item/integrated_circuit/loaded/hello_world

/obj/item/integrated_circuit/loaded/hello_world/Initialize(mapload)
	. = ..()
	var/obj/item/circuit_component/speech/speech = new()
	add_component(speech)

	speech.message.set_input("Hello World")


code:
/**
 * # Speech Relay preset
 *
 * Acts like poly. Says whatever it hears.
 */
/obj/item/integrated_circuit/loaded/speech_relay

/obj/item/integrated_circuit/loaded/speech_relay/Initialize(mapload)
	. = ..()
	var/obj/item/circuit_component/hear/hear = new()
	add_component(hear)
	hear.rel_x = 100
	hear.rel_y = 200

	var/obj/item/circuit_component/speech/speech = new()
	add_component(speech)
	speech.rel_x = 400
	speech.rel_y = 200

	speech.message.connect(hear.message_port)