code:
/proc/translate_legacy_chem_id(id)
	switch (id)
		if ("sacid")
			return "sulphuricacid"
		if ("facid")
			return "fluorosulfuricacid"
		if ("co2")
			return "carbondioxide"
		if ("mine_salve")
			return "minerssalve"
		else
			return ckey(id)

/obj/machinery/chem_dispenser
	name = "chem dispenser"
	desc = "Creates and dispenses chemicals."
	density = TRUE
	icon = 'icons/obj/chemical.dmi'
	icon_state = "dispenser"
	base_icon_state = "dispenser"
	use_power = IDLE_POWER_USE
	idle_power_usage = 40
	interaction_flags_machine = INTERACT_MACHINE_OPEN | INTERACT_MACHINE_ALLOW_SILICON | INTERACT_MACHINE_OFFLINE
	resistance_flags = FIRE_PROOF | ACID_PROOF
	circuit = /obj/item/circuitboard/machine/chem_dispenser
	processing_flags = NONE

	var/obj/item/stock_parts/cell/cell
	var/powerefficiency = 0.1
	var/amount = 30
	var/recharge_amount = 10
	var/recharge_counter = 0
	var/dispensed_temperature = DEFAULT_REAGENT_TEMPERATURE
	///If the UI has the pH meter shown
	var/show_ph = TRUE
	var/mutable_appearance/beaker_overlay
	var/working_state = "dispenser_working"
	var/nopower_state = "dispenser_nopower"
	var/has_panel_overlay = TRUE
	var/obj/item/reagent_containers/beaker = null
	//dispensable_reagents is copypasted in plumbing synthesizers. Please update accordingly. (I didn't make it global because that would limit custom chem dispensers)
	var/list/dispensable_reagents = list(
		/datum/reagent/aluminium,
		/datum/reagent/bromine,
		/datum/reagent/carbon,
		/datum/reagent/chlorine,
		/datum/reagent/copper,
		/datum/reagent/consumable/ethanol,
		/datum/reagent/fluorine,
		/datum/reagent/hydrogen,
		/datum/reagent/iodine,
		/datum/reagent/iron,
		/datum/reagent/lithium,
		/datum/reagent/mercury,
		/datum/reagent/nitrogen,
		/datum/reagent/oxygen,
		/datum/reagent/phosphorus,
		/datum/reagent/potassium,
		/datum/reagent/uranium/radium,
		/datum/reagent/silicon,
		/datum/reagent/sodium,
		/datum/reagent/stable_plasma,
		/datum/reagent/consumable/sugar,
		/datum/reagent/sulfur,
		/datum/reagent/toxin/acid,
		/datum/reagent/water,
		/datum/reagent/fuel
	)
	//these become available once the manipulator has been upgraded to tier 4 (femto)
	var/list/upgrade_reagents = list(
		/datum/reagent/acetone,
		/datum/reagent/ammonia,
		/datum/reagent/ash,
		/datum/reagent/diethylamine,
		/datum/reagent/fuel/oil,
		/datum/reagent/saltpetre
	)
	var/list/emagged_reagents = list(
		/datum/reagent/toxin/carpotoxin,
		/datum/reagent/medicine/mine_salve,
		/datum/reagent/medicine/morphine,
		/datum/reagent/drug/space_drugs,
		/datum/reagent/toxin
	)

	var/list/recording_recipe

	var/list/saved_recipes = list()

/obj/machinery/chem_dispenser/Initialize(mapload)
	. = ..()
	dispensable_reagents = sort_list(dispensable_reagents, /proc/cmp_reagents_asc)
	if(emagged_reagents)
		emagged_reagents = sort_list(emagged_reagents, /proc/cmp_reagents_asc)
	if(upgrade_reagents)
		upgrade_reagents = sort_list(upgrade_reagents, /proc/cmp_reagents_asc)
	if(is_operational)
		begin_processing()
	update_appearance()

/obj/machinery/chem_dispenser/Destroy()
	QDEL_NULL(beaker)
	QDEL_NULL(cell)
	return ..()

/obj/machinery/chem_dispenser/examine(mob/user)
	. = ..()
	if(panel_open)
		. += span_notice("[src]'s maintenance hatch is open!")
	if(in_range(user, src) || isobserver(user))
		. += "The status display reads:\n\
		Recharging [recharge_amount] power units per interval.\n\
		Power efficiency increased by [round((powerefficiency*1000)-100, 1)]%."

/obj/machinery/chem_dispenser/on_set_is_operational(old_value)
	if(old_value) //Turned off
		end_processing()
	else //Turned on
		begin_processing()

/obj/machinery/chem_dispenser/process(delta_time)
	if (recharge_counter >= 8)
		var/usedpower = cell.give(recharge_amount)
		if(usedpower)
			use_power(250*recharge_amount)
		recharge_counter = 0
		return
	recharge_counter += delta_time

/obj/machinery/chem_dispenser/proc/display_beaker()
	var/mutable_appearance/b_o = beaker_overlay || mutable_appearance(icon, "disp_beaker")
	b_o.pixel_y = -4
	b_o.pixel_x = -7
	return b_o

/obj/machinery/chem_dispenser/proc/work_animation()
	if(working_state)
		flick(working_state,src)

/obj/machinery/chem_dispenser/update_icon_state()
	icon_state = "[(nopower_state && !powered()) ? nopower_state : base_icon_state]"
	return ..()

/obj/machinery/chem_dispenser/update_overlays()
	. = ..()
	if(has_panel_overlay && panel_open)
		. += mutable_appearance(icon, "[base_icon_state]_panel-o")

	if(beaker)
		beaker_overlay = display_beaker()
		. += beaker_overlay

/obj/machinery/chem_dispenser/emag_act(mob/user)
	if(obj_flags & EMAGGED)
		to_chat(user, span_warning("[src] has no functional safeties to emag."))
		return
	to_chat(user, span_notice("You short out [src]'s safeties."))
	dispensable_reagents |= emagged_reagents//add the emagged reagents to the dispensable ones
	obj_flags |= EMAGGED

/obj/machinery/chem_dispenser/ex_act(severity, target)
	if(severity <= EXPLODE_LIGHT)
		return FALSE
	return ..()

/obj/machinery/chem_dispenser/contents_explosion(severity, target)
	..()
	if(!beaker)
		return

	switch(severity)
		if(EXPLODE_DEVASTATE)
			SSexplosions.high_mov_atom += beaker
		if(EXPLODE_HEAVY)
			SSexplosions.med_mov_atom += beaker
		if(EXPLODE_LIGHT)
			SSexplosions.low_mov_atom += beaker

/obj/machinery/chem_dispenser/handle_atom_del(atom/A)
	..()
	if(A == beaker)
		beaker = null
		cut_overlays()

/obj/machinery/chem_dispenser/ui_interact(mob/user, datum/tgui/ui)
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "ChemDispenser", name)
		if(user.hallucinating())
			ui.set_autoupdate(FALSE) //to not ruin the immersion by constantly changing the fake chemicals
		ui.open()

/obj/machinery/chem_dispenser/ui_data(mob/user)
	var/data = list()
	data["amount"] = amount
	data["energy"] = cell.charge ? cell.charge * powerefficiency : "0" //To prevent NaN in the UI.
	data["maxEnergy"] = cell.maxcharge * powerefficiency
	data["isBeakerLoaded"] = beaker ? 1 : 0
	data["showpH"] = show_ph

	var/beakerContents[0]
	var/beakerCurrentVolume = 0
	if(beaker && beaker.reagents && beaker.reagents.reagent_list.len)
		for(var/datum/reagent/R in beaker.reagents.reagent_list)
			beakerContents.Add(list(list("name" = R.name, "volume" = round(R.volume, 0.01), "pH" = R.ph, "purity" = R.purity))) // list in a list because Byond merges the first list...
			beakerCurrentVolume += R.volume
	data["beakerContents"] = beakerContents

	if (beaker)
		data["beakerCurrentVolume"] = round(beakerCurrentVolume, 0.01)
		data["beakerMaxVolume"] = beaker.volume
		data["beakerTransferAmounts"] = beaker.possible_transfer_amounts
		data["beakerCurrentpH"] = round(beaker.reagents.ph, 0.01)
	else
		data["beakerCurrentVolume"] = null
		data["beakerMaxVolume"] = null
		data["beakerTransferAmounts"] = null
		data["beakerCurrentpH"] = null

	var/chemicals[0]
	var/is_hallucinating = FALSE
	if(user.hallucinating())
		is_hallucinating = TRUE
	for(var/re in dispensable_reagents)
		var/datum/reagent/temp = GLOB.chemical_reagents_list[re]
		if(temp)
			var/chemname = temp.name
			if(is_hallucinating && prob(5))
				chemname = "[pick_list_replacements("hallucination.json", "chemicals")]"
			chemicals.Add(list(list("title" = chemname, "id" = ckey(temp.name), "pH" = temp.ph, "pHCol" = convert_ph_to_readable_color(temp.ph))))
	data["chemicals"] = chemicals
	data["recipes"] = saved_recipes

	data["recordingRecipe"] = recording_recipe
	data["recipeReagents"] = list()
	if(beaker?.reagents.ui_reaction_id)
		var/datum/chemical_reaction/reaction = get_chemical_reaction(beaker.reagents.ui_reaction_id)
		for(var/_reagent in reaction.required_reagents)
			var/datum/reagent/reagent = find_reagent_object_from_type(_reagent)
			data["recipeReagents"] += ckey(reagent.name)
	return data

/obj/machinery/chem_dispenser/ui_act(action, params)
	. = ..()
	if(.)
		return
	switch(action)
		if("amount")
			if(!is_operational || QDELETED(beaker))
				return
			var/target = text2num(params["target"])
			if(target in beaker.possible_transfer_amounts)
				amount = target
				work_animation()
				. = TRUE
		if("dispense")
			if(!is_operational || QDELETED(cell))
				return
			var/reagent_name = params["reagent"]
			if(!recording_recipe)
				var/reagent = GLOB.name2reagent[reagent_name]
				if(beaker && dispensable_reagents.Find(reagent))

					var/datum/reagents/holder = beaker.reagents
					var/to_dispense = max(0, min(amount, holder.maximum_volume - holder.total_volume))
					if(!cell?.use(to_dispense / powerefficiency))
						say("Not enough energy to complete operation!")
						return
					holder.add_reagent(reagent, to_dispense, reagtemp = dispensed_temperature)

					work_animation()
			else
				recording_recipe[reagent_name] += amount
			. = TRUE
		if("remove")
			if(!is_operational || recording_recipe)
				return
			var/amount = text2num(params["amount"])
			if(beaker && (amount in beaker.possible_transfer_amounts))
				beaker.reagents.remove_all(amount)
				work_animation()
				. = TRUE
		if("eject")
			replace_beaker(usr)
			. = TRUE
		if("dispense_recipe")
			if(!is_operational || QDELETED(cell))
				return

			var/list/chemicals_to_dispense = saved_recipes[params["recipe"]]
			if(!LAZYLEN(chemicals_to_dispense))
				return
			for(var/key in chemicals_to_dispense)
				var/reagent = GLOB.name2reagent[translate_legacy_chem_id(key)]
				var/dispense_amount = chemicals_to_dispense[key]
				if(!dispensable_reagents.Find(reagent))
					return
				if(!recording_recipe)
					if(!beaker)
						return

					var/datum/reagents/holder = beaker.reagents
					var/to_dispense = max(0, min(dispense_amount, holder.maximum_volume - holder.total_volume))
					if(!to_dispense)
						continue
					if(!cell?.use(to_dispense / powerefficiency))
						say("Not enough energy to complete operation!")
						return
					holder.add_reagent(reagent, to_dispense, reagtemp = dispensed_temperature)
					work_animation()
				else
					recording_recipe[key] += dispense_amount
			. = TRUE
		if("clear_recipes")
			if(!is_operational)
				return
			var/yesno = tgui_alert(usr, "Clear all recipes?",, list("Yes","No"))
			if(yesno == "Yes")
				saved_recipes = list()
			. = TRUE
		if("record_recipe")
			if(!is_operational)
				return
			recording_recipe = list()
			. = TRUE
		if("save_recording")
			if(!is_operational)
				return
			var/name = stripped_input(usr,"Name","What do you want to name this recipe?", "Recipe", MAX_NAME_LEN)
			if(!usr.canUseTopic(src, !issilicon(usr)))
				return
			if(saved_recipes[name] && tgui_alert(usr, "\"[name]\" already exists, do you want to overwrite it?",, list("Yes", "No")) == "No")
				return
			if(name && recording_recipe)
				for(var/reagent in recording_recipe)
					var/reagent_id = GLOB.name2reagent[translate_legacy_chem_id(reagent)]
					if(!dispensable_reagents.Find(reagent_id))
						visible_message(span_warning("[src] buzzes."), span_hear("You hear a faint buzz."))
						to_chat(usr, "[src] cannot find [reagent]!")
						playsound(src, 'sound/machines/buzz-two.ogg', 50, TRUE)
						return
				saved_recipes[name] = recording_recipe
				recording_recipe = null
				. = TRUE
		if("cancel_recording")
			if(!is_operational)
				return
			recording_recipe = null
			. = TRUE
		if("reaction_lookup")
			if(beaker)
				beaker.reagents.ui_interact(usr)

/obj/machinery/chem_dispenser/attackby(obj/item/I, mob/living/user, params)
	//SEPTIC EDIT BEGIN
	var/list/modifiers = params2list(params)
	//SEPTIC EDIT END
	if(default_unfasten_wrench(user, I))
		return
	if(default_deconstruction_screwdriver(user, icon_state, icon_state, I))
		update_appearance()
		return
	if(default_deconstruction_crowbar(I))
		return
	if(istype(I, /obj/item/reagent_containers) && !(I.item_flags & ABSTRACT) && I.is_open_container())
		var/obj/item/reagent_containers/B = I
		. = TRUE //no afterattack
		if(!user.transferItemToLoc(B, src))
			return
		replace_beaker(user, B)
		to_chat(user, span_notice("You add [B] to [src]."))
		updateUsrDialog()
	/* SEPTIC EDIT REMOVAL
	else if(!user.combat_mode && !istype(I, /obj/item/card/emag))
	*/
	//SEPTIC EDIT BEGIN
	else if(IS_HELP_INTENT(user, modifiers) && !istype(I, /obj/item/card/emag))
	//SEPTIC EDIT END
		to_chat(user, span_warning("You can't load [I] into [src]!"))
		return ..()
	else
		return ..()

/obj/machinery/chem_dispenser/get_cell()
	return cell

/obj/machinery/chem_dispenser/emp_act(severity)
	. = ..()
	if(. & EMP_PROTECT_SELF)
		return
	var/list/datum/reagents/R = list()
	var/total = min(rand(7,15), FLOOR(cell.charge*powerefficiency, 1))
	var/datum/reagents/Q = new(total*10)
	if(beaker?.reagents)
		R += beaker.reagents
	for(var/i in 1 to total)
		Q.add_reagent(pick(dispensable_reagents), 10, reagtemp = dispensed_temperature)
	R += Q
	chem_splash(get_turf(src), 3, R)
	if(beaker?.reagents)
		beaker.reagents.remove_all()
	cell.use(total/powerefficiency)
	cell.emp_act(severity)
	work_animation()
	visible_message(span_danger("[src] malfunctions, spraying chemicals everywhere!"))

/obj/machinery/chem_dispenser/RefreshParts()
	recharge_amount = initial(recharge_amount)
	var/newpowereff = 0.0666666
	for(var/obj/item/stock_parts/cell/P in component_parts)
		cell = P
	for(var/obj/item/stock_parts/matter_bin/M in component_parts)
		newpowereff += 0.0166666666*M.rating
	for(var/obj/item/stock_parts/capacitor/C in component_parts)
		recharge_amount *= C.rating
	for(var/obj/item/stock_parts/manipulator/M in component_parts)
		if (M.rating > 3)
			dispensable_reagents |= upgrade_reagents
	powerefficiency = round(newpowereff, 0.01)

/obj/machinery/chem_dispenser/proc/replace_beaker(mob/living/user, obj/item/reagent_containers/new_beaker)
	if(!user)
		return FALSE
	if(beaker)
		try_put_in_hand(beaker, user)
		beaker = null
	if(new_beaker)
		beaker = new_beaker
	update_appearance()
	return TRUE

/obj/machinery/chem_dispenser/on_deconstruction()
	cell = null
	if(beaker)
		beaker.forceMove(drop_location())
		beaker = null
	return ..()

/obj/machinery/chem_dispenser/AltClick(mob/living/user)
	. = ..()
	if(!can_interact(user) || !user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
		return
	replace_beaker(user)

/obj/machinery/chem_dispenser/drinks/Initialize(mapload)
	. = ..()
	AddComponent(/datum/component/simple_rotation, ROTATION_ALTCLICK | ROTATION_CLOCKWISE)

/obj/machinery/chem_dispenser/drinks/setDir()
	var/old = dir
	. = ..()
	if(dir != old)
		update_appearance()  // the beaker needs to be re-positioned if we rotate

/obj/machinery/chem_dispenser/drinks/display_beaker()
	var/mutable_appearance/b_o = beaker_overlay || mutable_appearance(icon, "disp_beaker")
	switch(dir)
		if(NORTH)
			b_o.pixel_y = 7
			b_o.pixel_x = rand(-9, 9)
		if(EAST)
			b_o.pixel_x = 4
			b_o.pixel_y = rand(-5, 7)
		if(WEST)
			b_o.pixel_x = -5
			b_o.pixel_y = rand(-5, 7)
		else//SOUTH
			b_o.pixel_y = -7
			b_o.pixel_x = rand(-9, 9)
	return b_o

/obj/machinery/chem_dispenser/drinks
	name = "soda dispenser"
	desc = "Contains a large reservoir of soft drinks."
	icon = 'icons/obj/chemical.dmi'
	icon_state = "soda_dispenser"
	base_icon_state = "soda_dispenser"
	has_panel_overlay = FALSE
	dispensed_temperature = WATER_MATTERSTATE_CHANGE_TEMP // magical mystery temperature of 274.5, where ice does not melt, and water does not freeze
	amount = 10
	pixel_y = 6
	layer = WALL_OBJ_LAYER
	circuit = /obj/item/circuitboard/machine/chem_dispenser/drinks
	working_state = null
	nopower_state = null
	pass_flags = PASSTABLE
	show_ph = FALSE
	dispensable_reagents = list(
		/datum/reagent/water,
		/datum/reagent/consumable/ice,
		/datum/reagent/consumable/coffee,
		/datum/reagent/consumable/cream,
		/datum/reagent/consumable/tea,
		/datum/reagent/consumable/icetea,
		/datum/reagent/consumable/space_cola,
		/datum/reagent/consumable/spacemountainwind,
		/datum/reagent/consumable/dr_gibb,
		/datum/reagent/consumable/space_up,
		/datum/reagent/consumable/tonic,
		/datum/reagent/consumable/sodawater,
		/datum/reagent/consumable/lemon_lime,
		/datum/reagent/consumable/pwr_game,
		/datum/reagent/consumable/shamblers,
		/datum/reagent/consumable/sugar,
		/datum/reagent/consumable/pineapplejuice,
		/datum/reagent/consumable/orangejuice,
		/datum/reagent/consumable/grenadine,
		/datum/reagent/consumable/limejuice,
		/datum/reagent/consumable/tomatojuice,
		/datum/reagent/consumable/lemonjuice,
		/datum/reagent/consumable/menthol
	)
	upgrade_reagents = null
	emagged_reagents = list(
		/datum/reagent/consumable/ethanol/thirteenloko,
		/datum/reagent/consumable/ethanol/whiskey_cola,
		/datum/reagent/toxin/mindbreaker,
		/datum/reagent/toxin/staminatoxin
	)

/obj/machinery/chem_dispenser/drinks/fullupgrade //fully ugpraded stock parts, emagged
	desc = "Contains a large reservoir of soft drinks. This model has had its safeties shorted out."
	obj_flags = CAN_BE_HIT | EMAGGED
	flags_1 = NODECONSTRUCT_1
	circuit = /obj/item/circuitboard/machine/chem_dispenser/drinks/fullupgrade

/obj/machinery/chem_dispenser/drinks/fullupgrade/Initialize(mapload)
	. = ..()
	dispensable_reagents |= emagged_reagents //adds emagged reagents

/obj/machinery/chem_dispenser/drinks/beer
	name = "booze dispenser"
	desc = "Contains a large reservoir of the good stuff."
	icon = 'icons/obj/chemical.dmi'
	icon_state = "booze_dispenser"
	base_icon_state = "booze_dispenser"
	dispensed_temperature = WATER_MATTERSTATE_CHANGE_TEMP
	circuit = /obj/item/circuitboard/machine/chem_dispenser/drinks/beer
	dispensable_reagents = list(
		/datum/reagent/consumable/ethanol/beer,
		/datum/reagent/consumable/ethanol/beer/maltliquor,
		/datum/reagent/consumable/ethanol/kahlua,
		/datum/reagent/consumable/ethanol/whiskey,
		/datum/reagent/consumable/ethanol/wine,
		/datum/reagent/consumable/ethanol/vodka,
		/datum/reagent/consumable/ethanol/gin,
		/datum/reagent/consumable/ethanol/rum,
		/datum/reagent/consumable/ethanol/tequila,
		/datum/reagent/consumable/ethanol/vermouth,
		/datum/reagent/consumable/ethanol/cognac,
		/datum/reagent/consumable/ethanol/ale,
		/datum/reagent/consumable/ethanol/absinthe,
		/datum/reagent/consumable/ethanol/hcider,
		/datum/reagent/consumable/ethanol/creme_de_menthe,
		/datum/reagent/consumable/ethanol/creme_de_cacao,
		/datum/reagent/consumable/ethanol/creme_de_coconut,
		/datum/reagent/consumable/ethanol/triple_sec,
		/datum/reagent/consumable/ethanol/sake,
		/datum/reagent/consumable/ethanol/applejack
	)
	upgrade_reagents = null
	emagged_reagents = list(
		/datum/reagent/consumable/ethanol,
		/datum/reagent/iron,
		/datum/reagent/toxin/minttoxin,
		/datum/reagent/consumable/ethanol/atomicbomb,
		/datum/reagent/consumable/ethanol/fernet
	)

/obj/machinery/chem_dispenser/drinks/beer/fullupgrade //fully ugpraded stock parts, emagged
	desc = "Contains a large reservoir of the good stuff. This model has had its safeties shorted out."
	obj_flags = CAN_BE_HIT | EMAGGED
	flags_1 = NODECONSTRUCT_1
	circuit = /obj/item/circuitboard/machine/chem_dispenser/drinks/beer/fullupgrade

/obj/machinery/chem_dispenser/drinks/beer/fullupgrade/Initialize(mapload)
	. = ..()
	dispensable_reagents |= emagged_reagents //adds emagged reagents

/obj/machinery/chem_dispenser/mutagen
	name = "mutagen dispenser"
	desc = "Creates and dispenses mutagen."
	dispensable_reagents = list(/datum/reagent/toxin/mutagen)
	upgrade_reagents = null
	emagged_reagents = list(/datum/reagent/toxin/plasma)

/obj/machinery/chem_dispenser/mutagensaltpeter
	name = "botanical chemical dispenser"
	desc = "Creates and dispenses chemicals useful for botany."
	flags_1 = NODECONSTRUCT_1

	circuit = /obj/item/circuitboard/machine/chem_dispenser/mutagensaltpeter

	dispensable_reagents = list(
		/datum/reagent/toxin/mutagen,
		/datum/reagent/saltpetre,
		/datum/reagent/plantnutriment/eznutriment,
		/datum/reagent/plantnutriment/left4zednutriment,
		/datum/reagent/plantnutriment/robustharvestnutriment,
		/datum/reagent/water,
		/datum/reagent/toxin/plantbgone,
		/datum/reagent/toxin/plantbgone/weedkiller,
		/datum/reagent/toxin/pestkiller,
		/datum/reagent/medicine/cryoxadone,
		/datum/reagent/ammonia,
		/datum/reagent/ash,
		/datum/reagent/diethylamine)
	upgrade_reagents = null

/obj/machinery/chem_dispenser/fullupgrade //fully ugpraded stock parts, emagged
	desc = "Creates and dispenses chemicals. This model has had its safeties shorted out."
	obj_flags = CAN_BE_HIT | EMAGGED
	flags_1 = NODECONSTRUCT_1
	circuit = /obj/item/circuitboard/machine/chem_dispenser/fullupgrade

/obj/machinery/chem_dispenser/fullupgrade/Initialize(mapload)
	. = ..()
	dispensable_reagents |= emagged_reagents //adds emagged reagents

/obj/machinery/chem_dispenser/abductor
	name = "reagent synthesizer"
	desc = "Synthesizes a variety of reagents using proto-matter."
	icon = 'icons/obj/abductor.dmi'
	icon_state = "chem_dispenser"
	base_icon_state = "chem_dispenser"
	has_panel_overlay = FALSE
	circuit = /obj/item/circuitboard/machine/chem_dispenser/abductor
	working_state = null
	nopower_state = null
	dispensable_reagents = list(
		/datum/reagent/aluminium,
		/datum/reagent/bromine,
		/datum/reagent/carbon,
		/datum/reagent/chlorine,
		/datum/reagent/copper,
		/datum/reagent/consumable/ethanol,
		/datum/reagent/fluorine,
		/datum/reagent/hydrogen,
		/datum/reagent/iodine,
		/datum/reagent/iron,
		/datum/reagent/lithium,
		/datum/reagent/mercury,
		/datum/reagent/nitrogen,
		/datum/reagent/oxygen,
		/datum/reagent/phosphorus,
		/datum/reagent/potassium,
		/datum/reagent/uranium/radium,
		/datum/reagent/silicon,
		/datum/reagent/silver,
		/datum/reagent/sodium,
		/datum/reagent/stable_plasma,
		/datum/reagent/consumable/sugar,
		/datum/reagent/sulfur,
		/datum/reagent/toxin/acid,
		/datum/reagent/water,
		/datum/reagent/fuel,
		/datum/reagent/acetone,
		/datum/reagent/ammonia,
		/datum/reagent/ash,
		/datum/reagent/diethylamine,
		/datum/reagent/fuel/oil,
		/datum/reagent/saltpetre,
		/datum/reagent/medicine/mine_salve,
		/datum/reagent/medicine/morphine,
		/datum/reagent/drug/space_drugs,
		/datum/reagent/toxin,
		/datum/reagent/toxin/plasma,
		/datum/reagent/uranium
	)


code:
///Tutorial states
#define TUT_NO_BUFFER 50
#define TUT_START 1
#define TUT_HAS_REAGENTS 2
#define TUT_IS_ACTIVE 3
#define TUT_IS_REACTING 4
#define TUT_FAIL 4.5
#define TUT_COMPLETE 5
#define TUT_MISSING 10

/obj/machinery/chem_heater
	name = "reaction chamber" //Maybe this name is more accurate?
	density = TRUE
	icon = 'icons/obj/chemical.dmi'
	icon_state = "mixer0b"
	base_icon_state = "mixer"
	use_power = IDLE_POWER_USE
	idle_power_usage = 40
	resistance_flags = FIRE_PROOF | ACID_PROOF
	circuit = /obj/item/circuitboard/machine/chem_heater

	var/obj/item/reagent_containers/beaker = null
	var/target_temperature = 300
	var/heater_coefficient = 0.05
	var/on = FALSE
	var/dispense_volume = 1
	//The list of active clients using this heater, so that we can update the UI on a reaction_step. I assume there are multiple clients possible.
	var/list/ui_client_list
	///If the user has the tutorial enabled
	var/tutorial_active = FALSE
	///What state we're at in the tutorial
	var/tutorial_state = 0

/obj/machinery/chem_heater/Initialize(mapload)
	. = ..()
	create_reagents(200, NO_REACT)//Lets save some calculations here
	//TODO: comsig reaction_start and reaction_end to enable/disable the UI autoupdater - this doesn't work presently as there's a hard divide between instant and processed reactions

/obj/machinery/chem_heater/deconstruct(disassembled)
	. = ..()
	if(beaker && disassembled)
		UnregisterSignal(beaker.reagents, COMSIG_REAGENTS_REACTION_STEP)
		beaker.forceMove(drop_location())
		beaker = null

/obj/machinery/chem_heater/Destroy()
	if(beaker)
		UnregisterSignal(beaker.reagents, COMSIG_REAGENTS_REACTION_STEP)
		QDEL_NULL(beaker)
	return ..()

/obj/machinery/chem_heater/handle_atom_del(atom/A)
	. = ..()
	if(A == beaker)
		beaker = null
		update_appearance()

/obj/machinery/chem_heater/update_icon_state()
	icon_state = "[base_icon_state][beaker ? 1 : 0]b"
	return ..()

/obj/machinery/chem_heater/AltClick(mob/living/user)
	. = ..()
	if(!user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
		return
	replace_beaker(user)

/obj/machinery/chem_heater/proc/replace_beaker(mob/living/user, obj/item/reagent_containers/new_beaker)
	if(!user)
		return FALSE
	if(beaker)
		try_put_in_hand(beaker, user)
		UnregisterSignal(beaker.reagents, COMSIG_REAGENTS_REACTION_STEP)
		beaker = null
	if(new_beaker)
		beaker = new_beaker
		RegisterSignal(beaker.reagents, COMSIG_REAGENTS_REACTION_STEP, .proc/on_reaction_step)
	update_appearance()
	return TRUE

/obj/machinery/chem_heater/RefreshParts()
	heater_coefficient = 0.1
	for(var/obj/item/stock_parts/micro_laser/M in component_parts)
		heater_coefficient *= M.rating

/obj/machinery/chem_heater/examine(mob/user)
	. = ..()
	if(in_range(user, src) || isobserver(user))
		. += span_notice("The status display reads: Heating reagents at <b>[heater_coefficient*1000]%</b> speed.")

/obj/machinery/chem_heater/process(delta_time)
	..()
	//Tutorial logics
	if(tutorial_active)
		switch(tutorial_state)
			if(TUT_NO_BUFFER)
				if(reagents.has_reagent(/datum/reagent/reaction_agent/basic_buffer, 5) && reagents.has_reagent(/datum/reagent/reaction_agent/acidic_buffer, 5))
					tutorial_state = TUT_START

			if(TUT_START)
				if(!reagents.has_reagent(/datum/reagent/reaction_agent/basic_buffer, 5) || !reagents.has_reagent(/datum/reagent/reaction_agent/acidic_buffer, 5))
					tutorial_state = TUT_NO_BUFFER
					return
				if(beaker?.reagents.has_reagent(/datum/reagent/mercury, 10) || beaker?.reagents.has_reagent(/datum/reagent/chlorine, 10))
					tutorial_state = TUT_HAS_REAGENTS
			if(TUT_HAS_REAGENTS)
				if(!(beaker?.reagents.has_reagent(/datum/reagent/mercury, 9)) || !(beaker?.reagents.has_reagent(/datum/reagent/chlorine, 9)))
					tutorial_state = TUT_MISSING
					return
				if(beaker?.reagents.chem_temp > 374)//If they heated it up as asked
					tutorial_state = TUT_IS_ACTIVE
					target_temperature = 375
					beaker.reagents.chem_temp = 375

			if(TUT_IS_ACTIVE)
				if(!(beaker?.reagents.has_reagent(/datum/reagent/mercury)) || !(beaker?.reagents.has_reagent(/datum/reagent/chlorine))) //Slightly concerned that people might take ages to read and it'll react anyways
					tutorial_state = TUT_MISSING
					return
				if(length(beaker?.reagents.reaction_list) == 1)//Only fudge numbers for our intentful reaction
					beaker.reagents.chem_temp = 375

				if(target_temperature >= 390)
					tutorial_state = TUT_IS_REACTING

			if(TUT_IS_REACTING)
				if(!(beaker?.reagents.has_reagent(/datum/reagent/mercury)) || !(beaker?.reagents.has_reagent(/datum/reagent/chlorine)))
					tutorial_state = TUT_COMPLETE

			if(TUT_COMPLETE)
				if(beaker?.reagents.has_reagent(/datum/reagent/consumable/failed_reaction))
					tutorial_state = TUT_FAIL
					return
				if(!beaker?.reagents.has_reagent(/datum/reagent/medicine/calomel))
					tutorial_state = TUT_MISSING

	if(machine_stat & NOPOWER)
		return
	if(on)
		if(beaker?.reagents.total_volume)
			if(beaker.reagents.is_reacting)//on_reaction_step() handles this
				return
			//keep constant with the chemical acclimator please
			beaker.reagents.adjust_thermal_energy((target_temperature - beaker.reagents.chem_temp) * heater_coefficient * delta_time * SPECIFIC_HEAT_DEFAULT * beaker.reagents.total_volume)
			beaker.reagents.handle_reactions()

/obj/machinery/chem_heater/attackby(obj/item/I, mob/user, params)
	if(default_deconstruction_screwdriver(user, "mixer0b", "mixer0b", I))
		return

	if(default_deconstruction_crowbar(I))
		return

	if(istype(I, /obj/item/reagent_containers) && !(I.item_flags & ABSTRACT) && I.is_open_container())
		. = TRUE //no afterattack
		var/obj/item/reagent_containers/B = I
		if(!user.transferItemToLoc(B, src))
			return
		replace_beaker(user, B)
		to_chat(user, span_notice("You add [B] to [src]."))
		updateUsrDialog()
		update_appearance()
		return

	if(beaker)
		if(istype(I, /obj/item/reagent_containers/dropper))
			var/obj/item/reagent_containers/dropper/D = I
			D.afterattack(beaker, user, 1)
			return
		if(istype(I, /obj/item/reagent_containers/syringe))
			var/obj/item/reagent_containers/syringe/S = I
			S.afterattack(beaker, user, 1)
			return
	return ..()

/obj/machinery/chem_heater/on_deconstruction()
	replace_beaker()
	return ..()

///Forces a UI update every time a reaction step happens inside of the beaker it contains. This is so the UI is in sync with the reaction since it's important that the output matches the current conditions for pH adjustment and temperature.
/obj/machinery/chem_heater/proc/on_reaction_step(datum/reagents/holder, num_reactions, delta_time)
	SIGNAL_HANDLER
	if(on)
		holder.adjust_thermal_energy((target_temperature - beaker.reagents.chem_temp) * heater_coefficient * delta_time * SPECIFIC_HEAT_DEFAULT * beaker.reagents.total_volume * (rand(8,11) * 0.1))//Give it a little wiggle room since we're actively reacting
	for(var/ui_client in ui_client_list)
		var/datum/tgui/ui = ui_client
		if(!ui)
			stack_trace("Warning: UI in UI client list is missing in [src] (chem_heater)")
			remove_ui_client_list(ui)
			continue
		ui.send_update()

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

/obj/machinery/chem_heater/ui_close(mob/user)
	for(var/ui_client in ui_client_list)
		var/datum/tgui/ui = ui_client
		if(ui.user == user)
			remove_ui_client_list(ui)
	return ..()

/*
*This adds an open ui client to the list - so that it can be force updated from reaction mechanisms.
* After adding it to the list, it enables a signal incase the ui is deleted - which will call a method to remove it from the list
* This is mostly to ensure we don't have defunct ui instances stored from any condition.
*/
/obj/machinery/chem_heater/proc/add_ui_client_list(new_ui)
	LAZYADD(ui_client_list, new_ui)
	RegisterSignal(new_ui, COMSIG_PARENT_QDELETING, .proc/on_ui_deletion)

///This removes an open ui instance from the ui list and deregsiters the signal
/obj/machinery/chem_heater/proc/remove_ui_client_list(old_ui)
	UnregisterSignal(old_ui, COMSIG_PARENT_QDELETING)
	LAZYREMOVE(ui_client_list, old_ui)

///This catches a signal and uses it to delete the ui instance from the list
/obj/machinery/chem_heater/proc/on_ui_deletion(datum/tgui/source, force)
	SIGNAL_HANDLER
	remove_ui_client_list(source)

/obj/machinery/chem_heater/ui_assets()
	. = ..() || list()
	. += get_asset_datum(/datum/asset/simple/tutorial_advisors)

/obj/machinery/chem_heater/ui_data(mob/user)
	var/data = list()
	data["targetTemp"] = target_temperature
	data["isActive"] = on
	data["isBeakerLoaded"] = beaker ? 1 : 0

	data["currentTemp"] = beaker ? beaker.reagents.chem_temp : null
	data["beakerCurrentVolume"] = beaker ? round(beaker.reagents.total_volume, 0.01) : null
	data["beakerMaxVolume"] = beaker ? beaker.volume : null
	data["currentpH"] = beaker ? round(beaker.reagents.ph, 0.01)  : null
	var/upgrade_level = heater_coefficient*10
	data["upgradeLevel"] = upgrade_level

	var/list/beaker_contents = list()
	for(var/r in beaker?.reagents.reagent_list)
		var/datum/reagent/reagent = r
		beaker_contents.len++
		beaker_contents[length(beaker_contents)] = list("name" = reagent.name, "volume" = round(reagent.volume, 0.01))
	data["beakerContents"] = beaker_contents

	var/list/active_reactions = list()
	var/flashing = DISABLE_FLASHING //for use with alertAfter - since there is no alertBefore, I set the after to 0 if true, or to the max value if false
	for(var/_reaction in beaker?.reagents.reaction_list)
		var/datum/equilibrium/equilibrium = _reaction
		if(!length(beaker.reagents.reaction_list))//I'm not sure why when it explodes it causes the gui to fail (it's missing danger (?) )
			stack_trace("how is this happening??")
			continue
		if(!equilibrium.reaction.results)//Incase of no result reactions
			continue
		var/_reagent = equilibrium.reaction.results[1]
		var/datum/reagent/reagent = beaker?.reagents.get_reagent(_reagent) //Reactions are named after their primary products
		if(!reagent)
			continue
		var/overheat = FALSE
		var/danger = FALSE
		var/purity_alert = 2 //same as flashing
		if(reagent.purity < equilibrium.reaction.purity_min)
			purity_alert = ENABLE_FLASHING//Because 0 is seen as null
			danger = TRUE
		if(!(flashing == ENABLE_FLASHING))//So that the pH meter flashes for ANY reactions out of optimal
			if(equilibrium.reaction.optimal_ph_min > beaker?.reagents.ph || equilibrium.reaction.optimal_ph_max < beaker?.reagents.ph)
				flashing = ENABLE_FLASHING
		if(equilibrium.reaction.is_cold_recipe)
			if(equilibrium.reaction.overheat_temp > beaker?.reagents.chem_temp && equilibrium.reaction.overheat_temp != NO_OVERHEAT)
				danger = TRUE
				overheat = TRUE
		else
			if(equilibrium.reaction.overheat_temp < beaker?.reagents.chem_temp)
				danger = TRUE
				overheat = TRUE
		if(equilibrium.reaction.reaction_flags & REACTION_COMPETITIVE) //We have a compeitive reaction - concatenate the results for the different reactions
			for(var/entry in active_reactions)
				if(entry["name"] == reagent.name) //If we have multiple reaction methods for the same result - combine them
					entry["reactedVol"] = equilibrium.reacted_vol
					entry["targetVol"] = round(equilibrium.target_vol, 1)//Use the first result reagent to name the reaction detected
					entry["quality"] = (entry["quality"] + equilibrium.reaction_quality) /2
					continue
		active_reactions.len++
		active_reactions[length(active_reactions)] = list("name" = reagent.name, "danger" = danger, "purityAlert" = purity_alert, "quality" = equilibrium.reaction_quality, "overheat" = overheat, "inverse" = reagent.inverse_chem_val, "minPure" = equilibrium.reaction.purity_min, "reactedVol" = equilibrium.reacted_vol, "targetVol" = round(equilibrium.target_vol, 1))//Use the first result reagent to name the reaction detected
	data["activeReactions"] = active_reactions
	data["isFlashing"] = flashing

	data["acidicBufferVol"] = reagents.get_reagent_amount(/datum/reagent/reaction_agent/acidic_buffer)
	data["basicBufferVol"] = reagents.get_reagent_amount(/datum/reagent/reaction_agent/basic_buffer)
	data["dispenseVolume"] = dispense_volume

	data["tutorialMessage"] = null
	//Tutorial output
	if(tutorial_active)
		switch(tutorial_state)
			if(TUT_NO_BUFFER)//missing buffer
				data["tutorialMessage"] = {"It looks like you’re a little low on buffers, here’s how to make more:

Acidic buffer: 2 parts Sodium
			2 parts Hydrogen
			2 parts Ethanol
			2 parts Water

Basic buffer: 3 parts Ammonia
			2 parts Chlorine
			2 parts Hydrogen
			2 parts Oxygen

Heat either up to speed up the reaction.

When the reactions are done, refill your chamber by pressing the Draw all buttons, to the right of the respective volume indicators.

To continue with the tutorial, fill both of your acidic and alkaline volumes to at least 5u."}
			if(TUT_START)//Default start
				data["tutorialMessage"] = {"Hello and welcome to the exciting world of chemistry! This help option will teach you the basic of reactions by guiding you through a calomel reaction.

For the majority of reactions, the overheat temperature is 900K, and the pH range is 5-9, though it's always worth looking up the ranges as these are changing. Calomel is no different.

To continue the tutorial, insert a beaker with at least 10u mercury and 10u chlorine added."}
			if(TUT_HAS_REAGENTS) //10u Hg and Cl
				data["tutorialMessage"] = {"Good job! You'll see that at present this isn't reacting. That's because this reaction needs a minimum temperature of 375K.

For the most part the hotter your reaction is, the faster it will react when it’s past it’s minimum temperature. But be careful to not heat it too much! "If your reaction is slow, your temperature is too low"!

When you’re ready, set your temperature to 375K and heat up the beaker to that amount."}
			if(TUT_IS_ACTIVE) //heat 375K
				data["tutorialMessage"] = {"Great! You should see your reaction slowly progressing.

Notice the pH dial on the right; the sum pH should be slowly drifting towards the left on the dial. How pure your solution is at the end depends on how well you keep your reaction within the optimal pH range. The dial will flash if any of the present reactions are outside their optimal. "If you're getting sludge, give your pH a nudge"!

In a moment, we’ll increase the temperature so that our rate is faster. It’s up to you to keep your pH within the limits, so keep an eye on that dial, and get ready to add basic buffer using the injection button to the left of the volume indicator.

To continue set your target temperature to 390K."}
			if(TUT_IS_REACTING) //Heat 390K
				data["tutorialMessage"] = "Stay focused on the reaction! You can do it!"
			if(TUT_FAIL) //Sludge
				data["tutorialMessage"] = "Ah, unfortunately your purity was too low and the reaction fell apart into errant sludge. Don't worry, you can always try again! Be careful though, for some reactions, failing isn't nearly as forgiving."
			if(TUT_COMPLETE) //Complete
				var/datum/reagent/calo = beaker?.reagents.has_reagent(/datum/reagent/medicine/calomel)
				if(!calo)
					tutorial_state = TUT_COMPLETE
					return
				switch(calo.purity)
					if(-INFINITY to 0.25)
						data["tutorialMessage"] = "You did it! Congratulations! I can tell you that your final purity was [calo.purity]. That's pretty close to the fail purity of 0.15 - which can often make some reactions explode. This chem will invert into Toxic sludge when ingested by another person, and will not cause of calomel's normal effects. Sneaky, huh?"
					if(0.25 to 0.6)
						data["tutorialMessage"] = "You did it! Congratulations! I can tell you that your final purity was [calo.purity]. Normally, this reaction will resolve above 0.7 without intervention. Are you praticing impure reactions? The lower you go, the higher change you have of getting dangerous effects during a reaction. In some more dangerous reactions, you're riding a fine line between death and an inverse chem, don't forget you can always chill your reaction to give yourself more time to manage it!"
					if(0.6 to 0.75)
						data["tutorialMessage"] = "You did it! Congratulations! I can tell you that your final purity was [calo.purity]. Normally, this reaction will resolve above 0.7 without intervention. Did you maybe add too much basic buffer and go past 9? If you like - you're welcome to try again. Just double press the help button!"
					if(0.75 to 0.85)
						data["tutorialMessage"] = "You did it! Congratulations! I can tell you that your final purity was [calo.purity]. You got pretty close to optimal! Feel free to try again if you like by double pressing the help button."
					if(0.75 to 0.99)
						data["tutorialMessage"] = "You did it! Congratulations! I can tell you that your final purity was [calo.purity]. You got pretty close to optimal! Feel free to try again if you like by double pressing the help button, but this is a respectable purity."
					if(0.99 to 1)
						data["tutorialMessage"] = "You did it! Congratulations! I can tell you that your final purity was [calo.purity]. Your calomel is as pure as they come! You've mastered the basics of chemistry, but there's plenty more challenges on the horizon. Good luck!"
						user.client?.give_award(/datum/award/achievement/jobs/chemistry_tut, user)
				data["tutorialMessage"] += "\n\nDid you notice that your temperature increased past 390K while reacting too? That's because this reaction is exothermic (heat producing), so for some reactions you might have to adjust your target to compensate. Oh, and you can check your purity by researching and printing off a chemical analyzer at the medlathe (for now)!"
			if(TUT_MISSING) //Missing
				data["tutorialMessage"] = "Uh oh, something went wrong. Did you take the beaker out, heat it up too fast, or have other things in the beaker? Try restarting the tutorial by double pressing the help button."

	return data

/obj/machinery/chem_heater/ui_act(action, params)
	. = ..()
	if(.)
		return
	switch(action)
		if("power")
			on = !on
			. = TRUE
		if("temperature")
			var/target = params["target"]
			if(text2num(target) != null)
				target = text2num(target)
				. = TRUE
			if(.)
				target_temperature = clamp(target, 0, 1000)
		if("eject")
			//Eject doesn't turn it off, so you can preheat for beaker swapping
			replace_beaker(usr)
			. = TRUE
		if("acidBuffer")
			var/target = params["target"]
			if(text2num(target) != null)
				target = text2num(target)
				. = TRUE
			if(.)
				move_buffer("acid", target)
		if("basicBuffer")
			var/target = params["target"]
			if(text2num(target) != null)
				target = text2num(target) //Because the input is flipped
				. = TRUE
			if(.)
				move_buffer("basic", target)
		if("disp_vol")
			var/target = params["target"]
			if(text2num(target) != null)
				target = text2num(target) //Because the input is flipped
				. = TRUE
			if(.)
				dispense_volume = target
		if("help")
			tutorial_active = !tutorial_active
			if(tutorial_active)
				tutorial_state = 1
				return
			tutorial_state = 0
			//Refresh window size
			ui_close(usr)
			ui_interact(usr, null)

///Moves a type of buffer from the heater to the beaker, or vice versa
/obj/machinery/chem_heater/proc/move_buffer(buffer_type, volume)
	if(!beaker)
		say("No beaker found!")
		return
	if(buffer_type == "acid")
		if(volume < 0)
			var/datum/reagent/acid_reagent = beaker.reagents.get_reagent(/datum/reagent/reaction_agent/acidic_buffer)
			if(!acid_reagent)
				say("Unable to find acidic buffer in beaker to draw from! Please insert a beaker containing acidic buffer.")
				return
			var/datum/reagent/acid_reagent_heater = reagents.get_reagent(/datum/reagent/reaction_agent/acidic_buffer)
			var/cur_vol = 0
			if(acid_reagent_heater)
				cur_vol = acid_reagent_heater.volume
			volume = 100 - cur_vol
			beaker.reagents.trans_id_to(src, acid_reagent.type, volume)//negative because we're going backwards
			return
		//We must be positive here
		reagents.trans_id_to(beaker, /datum/reagent/reaction_agent/acidic_buffer, dispense_volume)
		return

	if(buffer_type == "basic")
		if(volume < 0)
			var/datum/reagent/basic_reagent = beaker.reagents.get_reagent(/datum/reagent/reaction_agent/basic_buffer)
			if(!basic_reagent)
				say("Unable to find basic buffer in beaker to draw from! Please insert a beaker containing basic buffer.")
				return
			var/datum/reagent/basic_reagent_heater = reagents.get_reagent(/datum/reagent/reaction_agent/basic_buffer)
			var/cur_vol = 0
			if(basic_reagent_heater)
				cur_vol = basic_reagent_heater.volume
			volume = 100 - cur_vol
			beaker.reagents.trans_id_to(src, basic_reagent.type, volume)//negative because we're going backwards
			return
		reagents.trans_id_to(beaker, /datum/reagent/reaction_agent/basic_buffer, dispense_volume)
		return

/obj/machinery/chem_heater/proc/get_purity_color(datum/equilibrium/equilibrium)
	var/_reagent = equilibrium.reaction.results[1]
	var/datum/reagent/reagent = equilibrium.holder.get_reagent(_reagent)
	switch(reagent.purity)
		if(1 to INFINITY)
			return "blue"
		if(0.8 to 1)
			return "green"
		if(reagent.inverse_chem_val to 0.8)
			return "olive"
		if(equilibrium.reaction.purity_min to reagent.inverse_chem_val)
			return "orange"
		if(-INFINITY to equilibrium.reaction.purity_min)
			return "red"

//Has a lot of buffer and is upgraded
/obj/machinery/chem_heater/debug
	name = "Debug Reaction Chamber"
	desc = "Now with even more buffers!"

/obj/machinery/chem_heater/debug/Initialize(mapload)
	. = ..()
	reagents.maximum_volume = 2000
	reagents.add_reagent(/datum/reagent/reaction_agent/basic_buffer, 1000)
	reagents.add_reagent(/datum/reagent/reaction_agent/acidic_buffer, 1000)
	heater_coefficient = 0.4 //hack way to upgrade

//map load types
/obj/machinery/chem_heater/withbuffer
	desc = "This Reaction Chamber comes with a bit of buffer to help get you started."

/obj/machinery/chem_heater/withbuffer/Initialize(mapload)
	. = ..()
	reagents.add_reagent(/datum/reagent/reaction_agent/basic_buffer, 20)
	reagents.add_reagent(/datum/reagent/reaction_agent/acidic_buffer, 20)

#undef TUT_NO_BUFFER
#undef TUT_START
#undef TUT_HAS_REAGENTS
#undef TUT_IS_ACTIVE
#undef TUT_IS_REACTING
#undef TUT_FAIL
#undef TUT_COMPLETE
#undef TUT_MISSING

repo_name: AnalWerewolf/sundown

license: mit


code:
#define BEAKER1 1
#define BEAKER2 2

/obj/machinery/chem_mass_spec
	name = "High-performance liquid chromatography machine"
	desc = {"This machine can separate reagents based on charge, meaning it can clean reagents of some of their impurities, unlike the Chem Master 3000.
By selecting a range in the mass spectrograph certain reagents will be transferred from one beaker to another, which will clean it of any impurities up to a certain amount.
This will not clean any inverted reagents. Inverted reagents will still be correctly detected and displayed on the scanner, however.
\nLeft click with a beaker to add it to the input slot, Right click with a beaker to add it to the output slot. Alt + left/right click can let you quickly remove the corresponding beaker."}
	density = TRUE
	layer = BELOW_OBJ_LAYER
	icon = 'icons/obj/chemical.dmi'
	icon_state = "HPLC"
	base_icon_state = "HPLC"
	use_power = IDLE_POWER_USE
	idle_power_usage = 20
	resistance_flags = FIRE_PROOF | ACID_PROOF
	circuit = /obj/item/circuitboard/machine/chem_mass_spec
	///If we're processing reagents or not
	var/processing_reagents = FALSE
	///Time we started processing + the delay
	var/delay_time = 0
	///How much time we've done so far
	var/progress_time = 0
	///Lower mass range - for mass selection of what will be processed
	var/lower_mass_range = 0
	///Upper_mass_range - for mass selection of what will be processed
	var/upper_mass_range = INFINITY
	///The log output to clarify how the thing works
	var/list/log = list()
	///Input reagents container
	var/obj/item/reagent_containers/beaker1
	///Output reagents container
	var/obj/item/reagent_containers/beaker2
	///multiplies the final time needed to process the chems depending on the laser stock part
	var/cms_coefficient = 1

/obj/machinery/chem_mass_spec/Initialize(mapload)
	. = ..()
	ADD_TRAIT(src, DO_NOT_SPLASH, src.type)
	if(mapload)
		beaker2 = new /obj/item/reagent_containers/glass/beaker/large(src)

/obj/machinery/chem_mass_spec/Destroy()
	QDEL_NULL(beaker1)
	QDEL_NULL(beaker2)
	return ..()

/obj/machinery/chem_mass_spec/RefreshParts()
	cms_coefficient = 1
	for(var/obj/item/stock_parts/micro_laser/laser in component_parts)
		cms_coefficient /= laser.rating

/obj/machinery/chem_mass_spec/deconstruct(disassembled)
	if(beaker1)
		beaker1.forceMove(drop_location())
		beaker1 = null
	if(beaker2)
		beaker2.forceMove(drop_location())
		beaker2 = null
	. = ..()

/obj/machinery/chem_mass_spec/update_overlays()
	. = ..()
	if(panel_open)
		. += mutable_appearance(icon, "[base_icon_state]_panel-o")

/*			beaker swapping/attack code			*/

/obj/machinery/chem_mass_spec/attackby(obj/item/item, mob/user, params)
	if(processing_reagents)
		to_chat(user, " The [src] is currently processing a batch!")
		return ..()

	if(default_deconstruction_screwdriver(user, icon_state, icon_state, item))
		update_appearance()
		return

	if(default_unfasten_wrench(user, item))
		return

	if(istype(item, /obj/item/reagent_containers) && !(item.item_flags & ABSTRACT) && item.is_open_container())
		var/obj/item/reagent_containers/beaker = item
		. = TRUE //no afterattack
		if(!user.transferItemToLoc(beaker, src))
			return
		replace_beaker(user, BEAKER1, beaker)
		to_chat(user, span_notice("You add [beaker] to [src]."))
		update_appearance()
		updateUsrDialog()
		return
	..()

/obj/machinery/chem_mass_spec/attackby_secondary(obj/item/item, mob/user, params)
	. = ..()

	if(processing_reagents)
		to_chat(user, " The [src] is currently processing a batch!")
		return

	if(default_deconstruction_crowbar(item))
		return

	if(istype(item, /obj/item/reagent_containers) && !(item.item_flags & ABSTRACT) && item.is_open_container())
		var/obj/item/reagent_containers/beaker = item
		if(!user.transferItemToLoc(beaker, src))
			return
		replace_beaker(user, BEAKER2, beaker)
		to_chat(user, span_notice("You add [beaker] to [src]."))
		updateUsrDialog()
		. = SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN

	update_appearance()

/obj/machinery/chem_mass_spec/AltClick(mob/living/user)
	. = ..()
	if(processing_reagents)
		to_chat(user, " The [src] is currently processing a batch!")
		return
	if(!can_interact(user) || !user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
		return ..()
	replace_beaker(user, BEAKER1)

/obj/machinery/chem_mass_spec/alt_click_secondary(mob/living/user)
	. = ..()
	if(processing_reagents)
		to_chat(user, " The [src] is currently processing a batch!")
		return
	if(!can_interact(user) || !user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
		return
	replace_beaker(user, BEAKER2)

///Gee how come you get two beakers?
/*
 * Similar to other replace beaker procs, except now there are two of them!
 * When passed a beaker along with a position define it will swap a beaker in that slot (if there is one) with the beaker the machine is bonked with
 *
 * arguments:
 * * user - The one bonking the machine
 * * target beaker - the define (BEAKER1/BEAKER2) of what position to replace
 * * new beaker - the new beaker to add/replace the slot with
 */
/obj/machinery/chem_mass_spec/proc/replace_beaker(mob/living/user, target_beaker, obj/item/reagent_containers/new_beaker)
	if(!user)
		return FALSE
	switch(target_beaker)
		if(BEAKER1)
			if(beaker1)
				try_put_in_hand(beaker1, user)
				beaker1 = null
			beaker1 = new_beaker
			lower_mass_range = calculate_smallest_mass()
			upper_mass_range = calculate_largest_mass()
		if(BEAKER2)
			if(beaker2)
				try_put_in_hand(beaker2, user)
				beaker2 = null
			beaker2 = new_beaker
	update_appearance()
	return TRUE

/*			Icon code			*/

/obj/machinery/chem_mass_spec/update_icon_state()
	if(powered())
		icon_state = "HPLC_on"
	else
		icon_state = "HPLC"
	return ..()

/obj/machinery/chem_mass_spec/update_overlays()
	. = ..()
	if(beaker1)
		. += "HPLC_beaker1"
	if(beaker2)
		. += "HPLC_beaker2"
	if(powered())
		if(processing_reagents)
			. += "HPLC_graph_active"
		else if (length(beaker1?.reagents.reagent_list))
			. += "HPLC_graph_idle"

/*			UI Code				*/

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

/obj/machinery/chem_mass_spec/ui_data(mob/user)
	var/data = list()
	data["graphLowerRange"] = 0
	data["lowerRange"] = lower_mass_range
	data["upperRange"] = upper_mass_range
	data["processing"] = processing_reagents
	data["log"] = log
	data["beaker1"] = beaker1 ? TRUE : FALSE
	data["beaker2"] = beaker2 ? TRUE : FALSE
	if(processing_reagents)
		data["eta"] = delay_time - progress_time
	else
		data["eta"] = estimate_time()

	var/beakerContents[0]
	if(beaker1 && beaker1.reagents)
		for(var/datum/reagent/reagent as anything in beaker1.reagents.reagent_list)
			var/in_range = TRUE
			if(reagent.inverse_chem_val > reagent.purity && reagent.inverse_chem)
				var/datum/reagent/inverse_reagent = GLOB.chemical_reagents_list[reagent.inverse_chem]
				if(inverse_reagent.mass < lower_mass_range || inverse_reagent.mass > upper_mass_range)
					in_range = FALSE
				beakerContents.Add(list(list("name" = inverse_reagent.name, "volume" = round(reagent.volume, 0.01), "mass" = inverse_reagent.mass, "purity" = 1-reagent.purity, "selected" = in_range, "color" = "#b60046", "type" = "Inverted")))
				data["peakHeight"] = max(data["peakHeight"], reagent.volume)
				continue
			if(reagent.mass < lower_mass_range || reagent.mass > upper_mass_range)
				in_range = FALSE
			///We want to be sure that the impure chem appears after the parent chem in the list so that it always overshadows pure reagents
			beakerContents.Add(list(list("name" = reagent.name, "volume" = round(reagent.volume * reagent.purity, 0.01), "mass" = reagent.mass, "purity" = reagent.purity, "selected" = in_range, "color" = "#3cf096", "type" = "Clean")))
			if(1 > reagent.purity && reagent.impure_chem)
				var/datum/reagent/impure_reagent = GLOB.chemical_reagents_list[reagent.impure_chem]
				beakerContents.Add(list(list("name" = impure_reagent.name, "volume" = round(reagent.volume * (1-reagent.purity), 0.01), "mass" = reagent.mass, "purity" = 1-reagent.purity, "selected" = in_range, "color" = "#fc9738", "type" = "Impurity")))
				data["peakHeight"] = max(data["peakHeight"], reagent.volume * (1-reagent.purity))
			data["peakHeight"] = max(data["peakHeight"], reagent.volume * reagent.purity)

		data["beaker1CurrentVolume"] = beaker1.reagents.total_volume
		data["beaker1MaxVolume"] = beaker1.reagents.maximum_volume
	data["beaker1Contents"] = beakerContents
	data["graphUpperRange"] = calculate_largest_mass()  //+10 because of the range on the peak

	beakerContents = list()
	if(beaker2 && beaker2.reagents)
		for(var/datum/reagent/reagent in beaker2.reagents.reagent_list)
			///Normal stuff
			beakerContents.Add(list(list("name" = reagent.name, "volume" = round(reagent.volume * reagent.purity, 0.01), "mass" = reagent.mass, "purity" = reagent.purity, "color" = "#3cf096", "type" = "Clean", log = log[reagent.type])))
			///Impure stuff
			if(1 > reagent.purity && reagent.impure_chem)
				var/datum/reagent/impure_reagent = GLOB.chemical_reagents_list[reagent.impure_chem]
				beakerContents.Add(list(list("name" = impure_reagent.name, "volume" = round(reagent.volume * (1-reagent.purity), 0.01), "mass" = reagent.mass, "purity" = 1-reagent.purity, "color" = "#fc9738", "type" = "Impurity")))
		data["beaker2CurrentVolume"] = beaker2.reagents.total_volume
		data["beaker2MaxVolume"] = beaker2.reagents.maximum_volume
	data["beaker2Contents"] = beakerContents

	return data

/obj/machinery/chem_mass_spec/ui_act(action, params)
	. = ..()
	if(.)
		return
	switch(action)
		if("activate")
			if(!beaker1 || !beaker2 || !is_operational)
				say("This [src] is missing an output beaker!")
				return
			if(processing_reagents)
				say("You shouldn't be seeing this message! Please report this bug to https://github.com/tgstation/tgstation/issues . Thank you!")
				stack_trace("Someone managed to break the HPLC and tried to get it to activate when it's already activated!")
				return
			processing_reagents = TRUE
			estimate_time()
			progress_time = 0
			update_appearance()
			begin_processing()
			. = TRUE
		if("leftSlider")
			if(!is_operational || processing_reagents)
				return
			var/current_center = (lower_mass_range + upper_mass_range)/2
			lower_mass_range = clamp(params["value"], calculate_smallest_mass(), current_center)
			. = TRUE
		if("rightSlider")
			if(!is_operational || processing_reagents)
				return
			var/current_center = (lower_mass_range + upper_mass_range)/2
			upper_mass_range = clamp(params["value"], current_center, calculate_largest_mass())
			. = TRUE
		if("centerSlider")
			if(!is_operational || processing_reagents)
				return
			var/current_center = (lower_mass_range + upper_mass_range)/2
			var/delta_center = current_center - params["value"]
			var/lowest = calculate_smallest_mass()
			var/highest = calculate_largest_mass()
			lower_mass_range = clamp(lower_mass_range - delta_center, lowest, highest)
			upper_mass_range = clamp(upper_mass_range - delta_center, lowest, highest)
			. = TRUE
		if("eject1")
			if(processing_reagents)
				return
			replace_beaker(usr, BEAKER1)
			. = TRUE
		if("eject2")
			if(processing_reagents)
				return
			replace_beaker(usr, BEAKER2)
			. = TRUE

/*				processing procs				*/

///Increments time if it's progressing - if it's past time then it purifies and stops processing
/obj/machinery/chem_mass_spec/process(delta_time)
	. = ..()
	if(!is_operational)
		return FALSE
	if(!processing_reagents)
		return TRUE
	if(progress_time >= delay_time)
		processing_reagents = FALSE
		progress_time = 0
		purify_reagents()
		end_processing()
		update_appearance()
		return TRUE
	progress_time += delta_time
	return FALSE

/*
 * Processing through the reagents in beaker 1
 * For all the reagents within the selected range - we will then purify them up to their initial purity (usually 75%). It will take away the relative reagent volume from the sum volume of the reagent however.
 * If there are any inverted reagents - then it will instead just create a new reagent of the inverted type. This doesn't really do anything other than change the name of it,
 * As it processes through the reagents, it saves what changes were applied to each reagent in a log var to show the results at the end
 */
/obj/machinery/chem_mass_spec/proc/purify_reagents()
	log = list()
	for(var/datum/reagent/reagent as anything in beaker1.reagents.reagent_list)
		//Inverse first
		var/volume = reagent.volume
		if(reagent.inverse_chem_val > reagent.purity && reagent.inverse_chem)
			var/datum/reagent/inverse_reagent = GLOB.chemical_reagents_list[reagent.inverse_chem]
			if(inverse_reagent.mass < lower_mass_range || inverse_reagent.mass > upper_mass_range)
				continue
			log += list(inverse_reagent.type = "Cannot purify inverted") //Might as well make it do something - just updates the reagent's name
			beaker2.reagents.add_reagent(reagent.inverse_chem, volume, reagtemp = beaker1.reagents.chem_temp, added_purity = 1-reagent.purity)
			beaker1.reagents.remove_reagent(reagent.type, volume)
			continue

		if(reagent.mass < lower_mass_range || reagent.mass > upper_mass_range)
			continue

		var/delta_purity = initial(reagent.purity) - reagent.purity
		if(delta_purity <= 0)//As pure as we can be - so lets not add more than we need
			log += list(reagent.type = "Can't purify over [initial(reagent.purity)*100]%")
			beaker2.reagents.add_reagent(reagent.type, volume, reagtemp = beaker1.reagents.chem_temp, added_purity = reagent.purity, added_ph = reagent.ph)
			beaker1.reagents.remove_reagent(reagent.type, volume)
			continue

		var/product_vol = reagent.volume * (1-delta_purity)
		beaker2.reagents.add_reagent(reagent.type, product_vol, reagtemp = beaker1.reagents.chem_temp, added_purity = initial(reagent.purity), added_ph = reagent.ph)
		beaker1.reagents.remove_reagent(reagent.type, reagent.volume)
		log += list(reagent.type = "Purified to [initial(reagent.purity)*100]%")

/*				Mass spec graph calcs		 	 */

///Returns the largest mass to the nearest 50 (rounded up)
/obj/machinery/chem_mass_spec/proc/calculate_largest_mass()
	if(!beaker1?.reagents)
		return 0
	var/max_mass = 0
	for(var/datum/reagent/reagent as anything in beaker1.reagents.reagent_list)
		if(reagent.inverse_chem_val > reagent.purity && reagent.inverse_chem)
			var/datum/reagent/inverse_reagent = GLOB.chemical_reagents_list[reagent.inverse_chem]
			max_mass = max(max_mass, inverse_reagent.mass)
			continue
		max_mass = max(max_mass, reagent.mass)
	return CEILING(max_mass, 50)

///Returns the smallest mass to the nearest 50 (rounded down)
/obj/machinery/chem_mass_spec/proc/calculate_smallest_mass()
	if(!beaker1?.reagents)
		return 0
	var/min_mass = 0
	for(var/datum/reagent/reagent as anything in beaker1.reagents.reagent_list)
		if(reagent.inverse_chem_val > reagent.purity && reagent.inverse_chem)
			var/datum/reagent/inverse_reagent = GLOB.chemical_reagents_list[reagent.inverse_chem]
			min_mass = min(min_mass, inverse_reagent.mass)
			continue
		min_mass = min(min_mass, reagent.mass)
	return FLOOR(min_mass, 50)

/*
 * Estimates how long the highlighted range will take to process
 * The time will increase based off the reagent's volume, mass and purity.
 * In most cases this is between 10 to 30s for a single reagent.
 * This is why having a higher mass for a reagent is a balancing tool.
 */
/obj/machinery/chem_mass_spec/proc/estimate_time()
	if(!beaker1?.reagents)
		return 0
	var/time = 0
	for(var/datum/reagent/reagent as anything in beaker1.reagents.reagent_list)
		if(reagent.inverse_chem_val > reagent.purity && reagent.inverse_chem)
			var/datum/reagent/inverse_reagent = GLOB.chemical_reagents_list[reagent.inverse_chem]
			if(inverse_reagent.mass < lower_mass_range || inverse_reagent.mass > upper_mass_range)
				continue
			time += (((inverse_reagent.mass * reagent.volume) + (inverse_reagent.mass * reagent.purity * 0.1)) * 0.003) + 10 ///Roughly 10 - 30s?
			continue
		if(reagent.mass < lower_mass_range || reagent.mass > upper_mass_range)
			continue
		var/inverse_purity = 1-reagent.purity
		time += (((reagent.mass * reagent.volume) + (reagent.mass * inverse_purity * 0.1)) * 0.0035) + 10 ///Roughly 10 - 30s?
	delay_time = (time * cms_coefficient)
	return delay_time


code:
/**
 * Machine that allows to identify and separate reagents in fitting container
 * as well as to create new containers with separated reagents in it.
 *
 * Contains logic for both ChemMaster and CondiMaster, switched by "condi".
 */
/obj/machinery/chem_master
	name = "ChemMaster 3000"
	desc = "Used to separate chemicals and distribute them in a variety of forms."
	density = TRUE
	layer = BELOW_OBJ_LAYER
	icon = 'icons/obj/chemical.dmi'
	icon_state = "mixer0"
	base_icon_state = "mixer"
	use_power = IDLE_POWER_USE
	idle_power_usage = 20
	resistance_flags = FIRE_PROOF | ACID_PROOF
	circuit = /obj/item/circuitboard/machine/chem_master

	/// Input reagents container
	var/obj/item/reagent_containers/beaker
	/// Pill bottle for newly created pills
	var/obj/item/storage/pill_bottle/bottle
	/// Whether separated reagents should be moved back to container or destroyed. 1 - move, 0 - destroy
	var/mode = 1
	/// Decides what UI to show. If TRUE shows UI of CondiMaster, if FALSE - ChemMaster
	var/condi = FALSE
	/// Currently selected pill style
	var/chosen_pill_style = 1
	/// Currently selected condiment bottle style
	var/chosen_condi_style = CONDIMASTER_STYLE_AUTO
	/// Current UI screen. On the moment of writing this comment there were two: 'home' - main screen, and 'analyze' - info about specific reagent
	var/screen = "home"
	/// Info to display on 'analyze' screen
	var/analyze_vars[0]
	/// List of available pill styles for UI
	var/list/pill_styles
	/// List of available condibottle styles for UI
	var/list/condi_styles

/obj/machinery/chem_master/Initialize(mapload)
	create_reagents(100)

	//Calculate the span tags and ids fo all the available pill icons
	var/datum/asset/spritesheet/simple/assets = get_asset_datum(/datum/asset/spritesheet/simple/pills)
	pill_styles = list()
	for (var/x in 1 to PILL_STYLE_COUNT)
		var/list/SL = list()
		SL["id"] = x
		SL["className"] = assets.icon_class_name("pill[x]")
		pill_styles += list(SL)

	condi_styles = strip_condi_styles_to_icons(get_condi_styles())

	. = ..()

/obj/machinery/chem_master/Destroy()
	QDEL_NULL(beaker)
	QDEL_NULL(bottle)
	return ..()

/obj/machinery/chem_master/RefreshParts()
	reagents.maximum_volume = 0
	for(var/obj/item/reagent_containers/glass/beaker/B in component_parts)
		reagents.maximum_volume += B.reagents.maximum_volume

/obj/machinery/chem_master/ex_act(severity, target)
	if(severity <= EXPLODE_LIGHT)
		return FALSE
	return ..()

/obj/machinery/chem_master/contents_explosion(severity, target)
	. = ..()
	switch(severity)
		if(EXPLODE_DEVASTATE)
			if(beaker)
				SSexplosions.high_mov_atom += beaker
			if(bottle)
				SSexplosions.high_mov_atom += bottle
		if(EXPLODE_HEAVY)
			if(beaker)
				SSexplosions.med_mov_atom += beaker
			if(bottle)
				SSexplosions.med_mov_atom += bottle
		if(EXPLODE_LIGHT)
			if(beaker)
				SSexplosions.low_mov_atom += beaker
			if(bottle)
				SSexplosions.low_mov_atom += bottle

/obj/machinery/chem_master/handle_atom_del(atom/A)
	..()
	if(A == beaker)
		beaker = null
		reagents.clear_reagents()
		update_appearance()
	else if(A == bottle)
		bottle = null

/obj/machinery/chem_master/update_icon_state()
	icon_state = "[base_icon_state][beaker ? 1 : 0]"
	return ..()

/obj/machinery/chem_master/update_overlays()
	. = ..()
	if(machine_stat & BROKEN)
		. += "waitlight"

/obj/machinery/chem_master/blob_act(obj/structure/blob/B)
	if (prob(50))
		qdel(src)

/obj/machinery/chem_master/attackby(obj/item/I, mob/user, params)
	if(default_deconstruction_screwdriver(user, "mixer0_nopower", "mixer0", I))
		return

	else if(default_deconstruction_crowbar(I))
		return

	if(default_unfasten_wrench(user, I))
		return
	if(istype(I, /obj/item/reagent_containers) && !(I.item_flags & ABSTRACT) && I.is_open_container())
		. = TRUE // no afterattack
		if(panel_open)
			to_chat(user, span_warning("You can't use the [src.name] while its panel is opened!"))
			return
		var/obj/item/reagent_containers/B = I
		. = TRUE // no afterattack
		if(!user.transferItemToLoc(B, src))
			return
		replace_beaker(user, B)
		to_chat(user, span_notice("You add [B] to [src]."))
		updateUsrDialog()
		update_appearance()
	else if(!condi && istype(I, /obj/item/storage/pill_bottle))
		if(bottle)
			to_chat(user, span_warning("A pill bottle is already loaded into [src]!"))
			return
		if(!user.transferItemToLoc(I, src))
			return
		bottle = I
		to_chat(user, span_notice("You add [I] into the dispenser slot."))
		updateUsrDialog()
	else
		return ..()

/obj/machinery/chem_master/AltClick(mob/living/user)
	. = ..()
	if(!can_interact(user) || !user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
		return
	replace_beaker(user)

/**
 * Handles process of moving input reagents containers in/from machine
 *
 * When called checks for previously inserted beaker and gives it to user.
 * Then, if new_beaker provided, places it into src.beaker.
 * Returns `boolean`. TRUE if user provided (ignoring whether threre was any beaker change) and FALSE if not.
 *
 * Arguments:
 * * user - Mob that initialized replacement, gets previously inserted beaker if there's any
 * * new_beaker - New beaker to insert. Optional
 */
/obj/machinery/chem_master/proc/replace_beaker(mob/living/user, obj/item/reagent_containers/new_beaker)
	if(!user)
		return FALSE
	if(beaker)
		try_put_in_hand(beaker, user)
		beaker = null
	if(new_beaker)
		beaker = new_beaker
	update_appearance()
	return TRUE

/obj/machinery/chem_master/on_deconstruction()
	replace_beaker()
	if(bottle)
		bottle.forceMove(drop_location())
		adjust_item_drop_location(bottle)
		bottle = null
	return ..()

/obj/machinery/chem_master/ui_assets(mob/user)
	return list(
		get_asset_datum(/datum/asset/spritesheet/simple/pills),
		get_asset_datum(/datum/asset/spritesheet/simple/condiments),
	)

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

/obj/machinery/chem_master/ui_data(mob/user)
	var/list/data = list()
	data["isBeakerLoaded"] = beaker ? 1 : 0
	data["beakerCurrentVolume"] = beaker ? round(beaker.reagents.total_volume, 0.01) : null
	data["beakerMaxVolume"] = beaker ? beaker.volume : null
	data["mode"] = mode
	data["condi"] = condi
	data["screen"] = screen
	data["analyzeVars"] = analyze_vars
	data["chosenPillStyle"] = chosen_pill_style
	data["chosenCondiStyle"] = chosen_condi_style
	data["autoCondiStyle"] = CONDIMASTER_STYLE_AUTO
	data["isPillBottleLoaded"] = bottle ? 1 : 0
	if(bottle)
		var/datum/component/storage/STRB = bottle.GetComponent(/datum/component/storage)
		data["pillBottleCurrentAmount"] = bottle.contents.len
		data["pillBottleMaxAmount"] = STRB.max_items

	var/beaker_contents[0]
	if(beaker)
		for(var/datum/reagent/R in beaker.reagents.reagent_list)
			beaker_contents.Add(list(list("name" = R.name, "id" = ckey(R.name), "volume" = round(R.volume, 0.01)))) // list in a list because Byond merges the first list...
	data["beakerContents"] = beaker_contents

	var/buffer_contents[0]
	if(reagents.total_volume)
		for(var/datum/reagent/N in reagents.reagent_list)
			buffer_contents.Add(list(list("name" = N.name, "id" = ckey(N.name), "volume" = round(N.volume, 0.01)))) // ^
	data["bufferContents"] = buffer_contents

	//Calculated at init time as it never changes
	data["pillStyles"] = pill_styles
	data["condiStyles"] = condi_styles
	return data

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

	if(action == "eject")
		replace_beaker(usr)
		return TRUE

	if(action == "ejectPillBottle")
		if(!bottle)
			return FALSE
		bottle.forceMove(drop_location())
		adjust_item_drop_location(bottle)
		bottle = null
		return TRUE

	if(action == "transfer")
		var/reagent = GLOB.name2reagent[params["id"]]
		var/amount = text2num(params["amount"])
		var/to_container = params["to"]
		// Custom amount
		if (amount == -1)
			amount = text2num(input(
				"Enter the amount you want to transfer:",
				name, ""))
		if (amount == null || amount <= 0)
			return FALSE
		if (to_container == "beaker" && !mode)
			reagents.remove_reagent(reagent, amount)
			return TRUE
		if (!beaker)
			return FALSE
		if (to_container == "buffer")
			var/datum/reagent/R = beaker.reagents.get_reagent(reagent)
			if(!check_reactions(R, beaker.reagents))
				return FALSE
			beaker.reagents.trans_id_to(src, reagent, amount)
			return TRUE
		if (to_container == "beaker" && mode)
			var/datum/reagent/R = reagents.get_reagent(reagent)
			if(!check_reactions(R, reagents))
				return FALSE
			reagents.trans_id_to(beaker, reagent, amount)
			return TRUE
		return FALSE

	if(action == "toggleMode")
		mode = !mode
		return TRUE

	if(action == "pillStyle")
		var/id = text2num(params["id"])
		chosen_pill_style = id
		return TRUE

	if(action == "condiStyle")
		chosen_condi_style = params["id"]
		return TRUE

	if(action == "create")
		if(reagents.total_volume == 0)
			return FALSE
		var/item_type = params["type"]
		// Get amount of items
		var/amount = text2num(params["amount"])
		if(amount == null)
			amount = text2num(input(usr,
				"Max 10. Buffer content will be split evenly.",
				"How many to make?", 1))
		amount = clamp(round(amount), 0, 10)
		if (amount <= 0)
			return FALSE
		// Get units per item
		var/vol_each = text2num(params["volume"])
		var/vol_each_text = params["volume"]
		var/vol_each_max = reagents.total_volume / amount
		var/list/style

		if (item_type == "pill")
			vol_each_max = min(50, vol_each_max)
		else if (item_type == "patch")
			vol_each_max = min(40, vol_each_max)
		else if (item_type == "bottle")
			vol_each_max = min(30, vol_each_max)
		else if (item_type == "condimentPack")
			vol_each_max = min(10, vol_each_max)
		else if (item_type == "condimentBottle")
			var/list/styles = get_condi_styles()
			if (chosen_condi_style == CONDIMASTER_STYLE_AUTO || !(chosen_condi_style in styles))
				style = guess_condi_style(reagents)
			else
				style = styles[chosen_condi_style]
			vol_each_max = min(50, vol_each_max)
		else
			return FALSE
		if(vol_each_text == "auto")
			vol_each = vol_each_max
		if(vol_each == null)
			vol_each = text2num(input(usr,
				"Maximum [vol_each_max] units per item.",
				"How many units to fill?",
				vol_each_max))
		vol_each = round(clamp(vol_each, 0, vol_each_max), 0.01)
		if(vol_each <= 0)
			return FALSE
		// Get item name
		var/name = params["name"]
		var/name_has_units = item_type == "pill" || item_type == "patch"
		if(!name)
			var/name_default
			if (style && style["name"] && !style["generate_name"])
				name_default = style["name"]
			else
				name_default = reagents.get_master_reagent_name()
			if (name_has_units)
				name_default += " ([vol_each]u)"
			name = stripped_input(usr,
				"Name:",
				"Give it a name!",
				name_default,
				MAX_NAME_LEN)
		if(!name || !reagents.total_volume || !src || QDELETED(src) || !usr.canUseTopic(src, !issilicon(usr)))
			return FALSE
		// Start filling
		if(item_type == "pill")
			var/obj/item/reagent_containers/pill/P
			var/target_loc = drop_location()
			var/drop_threshold = INFINITY
			if(bottle)
				var/datum/component/storage/STRB = bottle.GetComponent(
					/datum/component/storage)
				if(STRB)
					drop_threshold = STRB.max_items - bottle.contents.len
					target_loc = bottle
			for(var/i = 0; i < amount; i++)
				if(i < drop_threshold)
					P = new/obj/item/reagent_containers/pill(target_loc)
				else
					P = new/obj/item/reagent_containers/pill(drop_location())
				P.name = trim("[name] pill")
				if(chosen_pill_style == RANDOM_PILL_STYLE)
					P.icon_state ="pill[rand(1,21)]"
				else
					P.icon_state = "pill[chosen_pill_style]"
				if(P.icon_state == "pill4")
					P.desc = "A tablet or capsule, but not just any, a red one, one taken by the ones not scared of knowledge, freedom, uncertainty and the brutal truths of reality."
				adjust_item_drop_location(P)
				reagents.trans_to(P, vol_each, transfered_by = usr)
			return TRUE
		if(item_type == "patch")
			var/obj/item/reagent_containers/pill/patch/P
			for(var/i = 0; i < amount; i++)
				P = new/obj/item/reagent_containers/pill/patch(drop_location())
				P.name = trim("[name] patch")
				adjust_item_drop_location(P)
				reagents.trans_to(P, vol_each, transfered_by = usr)
			return TRUE
		if(item_type == "bottle")
			var/obj/item/reagent_containers/glass/bottle/P
			for(var/i = 0; i < amount; i++)
				P = new/obj/item/reagent_containers/glass/bottle(drop_location())
				P.name = trim("[name] bottle")
				adjust_item_drop_location(P)
				reagents.trans_to(P, vol_each, transfered_by = usr)
			return TRUE
		if(item_type == "condimentPack")
			var/obj/item/reagent_containers/food/condiment/pack/P
			for(var/i = 0; i < amount; i++)
				P = new/obj/item/reagent_containers/food/condiment/pack(drop_location())
				P.originalname = name
				P.name = trim("[name] pack")
				P.desc = "A small condiment pack. The label says it contains [name]."
				reagents.trans_to(P, vol_each, transfered_by = usr)
			return TRUE
		if(item_type == "condimentBottle")
			var/obj/item/reagent_containers/food/condiment/P
			for(var/i = 0; i < amount; i++)
				P = new/obj/item/reagent_containers/food/condiment(drop_location())
				if (style)
					apply_condi_style(P, style)
				P.renamedByPlayer = TRUE
				P.name = name
				reagents.trans_to(P, vol_each, transfered_by = usr)
			return TRUE
		return FALSE

	if(action == "analyze")
		var/datum/reagent/R = GLOB.name2reagent[params["id"]]
		if(R)
			var/state = "Unknown"
			if(initial(R.reagent_state) == 1)
				state = "Solid"
			else if(initial(R.reagent_state) == 2)
				state = "Liquid"
			else if(initial(R.reagent_state) == 3)
				state = "Gas"
			var/const/P = 3 //The number of seconds between life ticks
			var/T = initial(R.metabolization_rate) * (60 / P)
			analyze_vars = list("name" = initial(R.name), "state" = state, "color" = initial(R.color), "description" = initial(R.description), "metaRate" = T, "overD" = initial(R.overdose_threshold), "pH" = initial(R.ph))
			screen = "analyze"
			return TRUE

	if(action == "goScreen")
		screen = params["screen"]
		return TRUE

	return FALSE

/obj/machinery/chem_master/adjust_item_drop_location(atom/movable/AM) // Special version for chemmasters and condimasters
	if (AM == beaker)
		AM.pixel_x = AM.base_pixel_x - 8
		AM.pixel_y = AM.base_pixel_y + 8
		return null
	else if (AM == bottle)
		if (length(bottle.contents))
			AM.pixel_x = AM.base_pixel_x - 13
		else
			AM.pixel_x = AM.base_pixel_x - 7
		AM.pixel_y = AM.base_pixel_y - 8
		return null
	else
		var/md5 = md5(AM.name)
		for (var/i in 1 to 32)
			. += hex2num(md5[i])
		. = . % 9
		AM.pixel_x = AM.base_pixel_x + ((.%3)*6)
		AM.pixel_y = AM.base_pixel_y - 8 + (round( . / 3)*8)

/**
 * Translates styles data into UI compatible format
 *
 * Expects to receive list of availables condiment styles in its complete format, and transforms them in simplified form with enough data to get UI going.
 * Returns list(list("id" = <key>, "className" = <icon class>, "title" = <name and desc>),..).
 *
 * Arguments:
 * * styles - List of styles for condiment bottles in internal format: [/obj/machinery/chem_master/proc/get_condi_styles]
 */
/obj/machinery/chem_master/proc/strip_condi_styles_to_icons(list/styles)
	var/list/icons = list()
	for (var/s in styles)
		if (styles[s] && styles[s]["class_name"])
			var/list/icon = list()
			var/list/style = styles[s]
			icon["id"] = s
			icon["className"] = style["class_name"]
			icon["title"] = "[style["name"]]\n[style["desc"]]"
			icons += list(icon)

	return icons

/**
 * Defines and provides list of available condiment bottle styles
 *
 * Uses typelist() for styles storage after initialization.
 * For fallback style must provide style with key (const) CONDIMASTER_STYLE_FALLBACK
 * Returns list(
 * <key> = list(
 * "icon_state" = <bottle icon_state>,
 * "name" = <bottle name>,
 * "desc" = <bottle desc>,
 * ?"generate_name" = <if truthy, autogenerates default name from reagents instead of using "name">,
 * ?"icon_empty" = <icon_state when empty>,
 * ?"fill_icon_thresholds" = <list of thresholds for reagentfillings, no tresholds if not provided or falsy>,
 * ?"inhand_icon_state" = <inhand icon_state, falsy - no icon, not provided - whatever is initial (currently "beer")>,
 * ?"lefthand_file" = <file for inhand icon for left hand, ignored if "inhand_icon_state" not provided>,
 * ?"righthand_file" = <same as "lefthand_file" but for right hand>,
 * ),
 * ..
 * )
 *
 */
/obj/machinery/chem_master/proc/get_condi_styles()
	var/list/styles = typelist("condi_styles")
	if (!styles.len)
		//Possible_states has the reagent type as key and a list of, in order, the icon_state, the name and the desc as values. Was used in the condiment/on_reagent_change(changetype) to change names, descs and sprites.
		styles += list(
			CONDIMASTER_STYLE_FALLBACK = list("icon_state" = "emptycondiment", "icon_empty" = "", "name" = "condiment bottle", "desc" = "Just your average condiment bottle.", "fill_icon_thresholds" = list(0, 10, 25, 50, 75, 100), "generate_name" = TRUE),
			"enzyme" = list("icon_state" = "enzyme", "icon_empty" = "", "name" = "universal enzyme bottle", "desc" = "Used in cooking various dishes."),
			"flour" = list("icon_state" = "flour", "icon_empty" = "", "name" = "flour sack", "desc" = "A big bag of flour. Good for baking!"),
			"mayonnaise" = list("icon_state" = "mayonnaise", "icon_empty" = "", "name" = "mayonnaise jar", "desc" = "An oily condiment made from egg yolks."),
			"milk" = list("icon_state" = "milk", "icon_empty" = "", "name" = "space milk", "desc" = "It's milk. White and nutritious goodness!"),
			"blackpepper" = list("icon_state" = "peppermillsmall", "inhand_icon_state" = "", "icon_empty" = "emptyshaker", "name" = "pepper mill", "desc" = "Often used to flavor food or make people sneeze."),
			"rice" = list("icon_state" = "rice", "icon_empty" = "", "name" = "rice sack", "desc" = "A big bag of rice. Good for cooking!"),
			"sodiumchloride" = list("icon_state" = "saltshakersmall", "inhand_icon_state" = "", "icon_empty" = "emptyshaker", "name" = "salt shaker", "desc" = "Salt. From dead crew, presumably."),
			"soymilk" = list("icon_state" = "soymilk", "icon_empty" = "", "name" = "soy milk", "desc" = "It's soy milk. White and nutritious goodness!"),
			"soysauce" = list("icon_state" = "soysauce", "inhand_icon_state" = "", "icon_empty" = "", "name" = "soy sauce bottle", "desc" = "A salty soy-based flavoring."),
			"sugar" = list("icon_state" = "sugar", "icon_empty" = "", "name" = "sugar sack", "desc" = "Tasty spacey sugar!"),
			"ketchup" = list("icon_state" = "ketchup", "icon_empty" = "", "name" = "ketchup bottle", "desc" = "You feel more American already."),
			"capsaicin" = list("icon_state" = "hotsauce", "icon_empty" = "", "name" = "hotsauce bottle", "desc" = "You can almost TASTE the stomach ulcers!"),
			"frostoil" = list("icon_state" = "coldsauce", "icon_empty" = "", "name" = "coldsauce bottle", "desc" = "Leaves the tongue numb from its passage."),
			"cornoil" = list("icon_state" = "oliveoil", "icon_empty" = "", "name" = "corn oil bottle", "desc" = "A delicious oil used in cooking. Made from corn."),
			"bbqsauce" = list("icon_state" = "bbqsauce", "icon_empty" = "", "name" = "bbq sauce bottle", "desc" = "Hand wipes not included.")
		)
		var/list/carton_in_hand = list(
			"inhand_icon_state" = "carton",
			"lefthand_file" = 'icons/mob/inhands/equipment/kitchen_lefthand.dmi',
			"righthand_file" = 'icons/mob/inhands/equipment/kitchen_righthand.dmi'
		)
		for (var/style_reagent in list("flour", "milk", "rice", "soymilk", "sugar"))
			if (style_reagent in styles)
				styles[style_reagent] += carton_in_hand
		var/datum/asset/spritesheet/simple/assets = get_asset_datum(/datum/asset/spritesheet/simple/condiments)
		for (var/reagent in styles)
			styles[reagent]["class_name"] = assets.icon_class_name(reagent)
	return styles

/**
 * Provides condiment bottle style based on reagents.
 *
 * Gets style from available by key, using last part of main reagent type (eg. "rice" for /datum/reagent/consumable/rice) as key.
 * If not available returns fallback style, or null if no such thing.
 * Returns list that is one of condibottle styles from [/obj/machinery/chem_master/proc/get_condi_styles]
 */
/obj/machinery/chem_master/proc/guess_condi_style(datum/reagents/reagents)
	var/list/styles = get_condi_styles()
	if (reagents.reagent_list.len > 0)
		var/main_reagent = reagents.get_master_reagent_id()
		if (main_reagent)
			var/list/path = splittext("[main_reagent]", "/")
			main_reagent = path[path.len]
		if(main_reagent in styles)
			return styles[main_reagent]
	return styles[CONDIMASTER_STYLE_FALLBACK]

/**
 * Applies style to condiment bottle.
 *
 * Applies props provided in "style" assuming that "container" is freshly created with no styles applied before.
 * User specified name for bottle applied after this method during bottle creation,
 * so container.name overwritten here for consistency rather than with some purpose in mind.
 *
 * Arguments:
 * * container - condiment bottle that gets style applied to it
 * * style - assoc list, must probably one from [/obj/machinery/chem_master/proc/get_condi_styles]
 */
/obj/machinery/chem_master/proc/apply_condi_style(obj/item/reagent_containers/food/condiment/container, list/style)
	container.name = style["name"]
	container.desc = style["desc"]
	container.icon_state = style["icon_state"]
	container.icon_empty = style["icon_empty"]
	container.fill_icon_thresholds = style["fill_icon_thresholds"]
	if ("inhand_icon_state" in style)
		container.inhand_icon_state = style["inhand_icon_state"]
		if (style["lefthand_file"] || style["righthand_file"])
			container.lefthand_file = style["lefthand_file"]
			container.righthand_file = style["righthand_file"]

//Checks to see if the target reagent is being created (reacting) and if so prevents transfer
//Only prevents reactant from being moved so that people can still manlipulate input reagents
/obj/machinery/chem_master/proc/check_reactions(datum/reagent/reagent, datum/reagents/holder)
	if(!reagent)
		return FALSE
	var/canMove = TRUE
	for(var/e in holder.reaction_list)
		var/datum/equilibrium/E = e
		if(E.reaction.reaction_flags & REACTION_COMPETITIVE)
			continue
		for(var/result in E.reaction.required_reagents)
			var/datum/reagent/R = result
			if(R == reagent.type)
				canMove = FALSE
	if(!canMove)
		say("Cannot move arrested chemical reaction reagents!")
	return canMove

/**
 * Machine that allows to identify and separate reagents in fitting container
 * as well as to create new containers with separated reagents in it.
 *
 * All logic related to this is in [/obj/machinery/chem_master] and condimaster specific UI enabled by "condi = TRUE"
 */
/obj/machinery/chem_master/condimaster
	name = "CondiMaster 3000"
	desc = "Used to create condiments and other cooking supplies."
	condi = TRUE


code:
/*
* A debug chem tester that will process through all recipies automatically and try to react them.
* Highlights low purity reactions and and reactions that don't happen
*/
/obj/machinery/chem_recipe_debug
	name = "chemical reaction tester"
	density = TRUE
	icon = 'icons/obj/chemical.dmi'
	icon_state = "HPLC_debug"
	use_power = IDLE_POWER_USE
	idle_power_usage = 40
	resistance_flags = FIRE_PROOF | ACID_PROOF | INDESTRUCTIBLE
	///List of every reaction in the game kept locally for easy access
	var/list/cached_reactions = list()
	///What index in the cached_reactions we're in
	var/index = 1
	///If the machine is currently processing through the list
	var/processing = FALSE
	///Final output that highlights all of the reactions with inoptimal purity/voolume at base
	var/problem_string
	///Final output that highlights all of the reactions with inoptimal purity/voolume at base
	var/impure_string
	///The count of reactions that resolve between 1 - 0.9 purity
	var/minorImpurity
	///The count of reactions that resolve below 0.9 purity
	var/majorImpurity
	///If we failed to react this current chem so use a lower temp - all reactions only
	var/failed = 0
	///If we're forcing optimal conditions
	var/should_force_temp = FALSE
	var/should_force_ph = FALSE
	///Forced values
	var/force_temp = 300
	var/force_ph = 7
	///Multiplier of product
	var/vol_multiplier = 20
	///If we're reacting
	var/react = FALSE
	///Number of delta times taken to react
	var/react_time = 0
	///IF we're doing EVERY reaction
	var/process_all = FALSE
	///The name
	var/list/reaction_names = list()
	///If it's started
	var/reaction_stated = FALSE
	///If we spawn a beaker at the end of a reaction or not
	var/beaker_spawn = FALSE
	///If we force min temp on reaction setup
	var/min_temp = FALSE
	///The recipe we're editing
	var/datum/chemical_reaction/edit_recipe

///Create reagents datum
/obj/machinery/chem_recipe_debug/Initialize(mapload)
	. = ..()
	create_reagents(9000)//I want to make sure everything fits
	end_processing()

///Enable the machine
/obj/machinery/chem_recipe_debug/attackby(obj/item/I, mob/user, params)
	. = ..()
	ui_interact(usr)

///Enable the machine
/obj/machinery/chem_recipe_debug/AltClick(mob/living/user)
	. = ..()
	if(processing)
		say("currently processing reaction [index]: [cached_reactions[index]] of [cached_reactions.len]")
		return
	process_all = TRUE
	say("Starting processing")
	setup_reactions()
	begin_processing()

///Resets the index, and creates the cached_reaction list from all possible reactions
/obj/machinery/chem_recipe_debug/proc/setup_reactions()
	cached_reactions = list()
	if(process_all)
		for(var/reaction in GLOB.chemical_reactions_list_reactant_index)
			if(is_type_in_list(GLOB.chemical_reactions_list_reactant_index[reaction], cached_reactions))
				continue
			cached_reactions += GLOB.chemical_reactions_list_reactant_index[reaction]
	else
		cached_reactions = reaction_names
	reagents.clear_reagents()
	index = 1
	processing = TRUE

/*
* The main loop that sets up, creates and displays results from a reaction
* warning: this code is a hot mess
*/
/obj/machinery/chem_recipe_debug/process(delta_time)
	if(processing == FALSE)
		setup_reactions()
	if(should_force_ph)
		reagents.ph = force_ph
	if(should_force_temp)
		reagents.chem_temp = force_temp
	if(reagents.is_reacting == TRUE)
		react_time += delta_time
		return
	if(reaction_stated == TRUE)
		reaction_stated = FALSE
		relay_ended_reaction()
	if(index > cached_reactions.len)
		relay_all_reactions()
		return
	setup_reaction()
	reaction_stated = TRUE

/obj/machinery/chem_recipe_debug/proc/relay_all_reactions()
	say("Completed testing, missing reactions products (may have exploded) are:")
	say("[problem_string]", sanitize=FALSE)
	say("Problem with results are:")
	say("[impure_string]", sanitize=FALSE)
	say("Reactions with minor impurity: [minorImpurity], reactions with major impurity: [majorImpurity]")
	processing = FALSE
	problem_string = null
	impure_string = null
	minorImpurity = null
	majorImpurity = null
	end_processing()

/obj/machinery/chem_recipe_debug/proc/relay_ended_reaction()
	if(reagents.reagent_list)
		var/cached_purity
		say("Reaction completed for [cached_reactions[index]] final temperature = [reagents.chem_temp], ph = [reagents.ph], time taken = [react_time]s.")
		var/datum/chemical_reaction/reaction = cached_reactions[index]
		for(var/reagent_type in reaction.results)
			var/datum/reagent/reagent =  reagents.get_reagent(reagent_type)
			if(!reagent)
				say(span_warning("Unable to find product [reagent_type] in holder after reaction! reagents found are:"))
				for(var/other_reagent in reagents.reagent_list)
					say("[other_reagent]")
				var/obj/item/reagent_containers/glass/beaker/bluespace/beaker = new /obj/item/reagent_containers/glass/beaker/bluespace(loc)
				reagents.trans_to(beaker)
				beaker.name = "[cached_reactions[index]] failed"
				if(!failed)
					problem_string += "[cached_reactions[index]] [span_warning("Unable to find product [reagent_type] in holder after reaction! Trying alternative setup. index:[index]")]\n"
					failed++
					return
			say("Reaction has a product [reagent_type] [reagent.volume]u purity of [reagent.purity]")
			if(reagent.purity < 0.9)
				impure_string += "Reaction [cached_reactions[index]] has a product [reagent_type] [reagent.volume]u [span_boldwarning("purity of [reagent.purity]")] index:[index]\n"
				majorImpurity++
			else if (reagent.purity < 1)
				impure_string += "Reaction [cached_reactions[index]] has a product [reagent_type] [reagent.volume]u [span_warning("purity of [reagent.purity]")] index:[index]\n"
				minorImpurity++
			if(reagent.volume < reaction.results[reagent_type])
				impure_string += "Reaction [cached_reactions[index]] has a product [reagent_type] [span_warning("[reagent.volume]u")] purity of [reagent.purity] index:[index]\n"
			cached_purity = reagent.purity
		if(beaker_spawn && reagents.total_volume)
			var/obj/item/reagent_containers/glass/beaker/bluespace/beaker = new /obj/item/reagent_containers/glass/beaker/bluespace(loc)
			reagents.trans_to(beaker)
			beaker.name = "[cached_reactions[index]] purity: [cached_purity]"
		reagents.clear_reagents()
		reagents.chem_temp = 300
		index++
		failed = 0
	else
		say("No reagents left in beaker!")
		index++

/obj/machinery/chem_recipe_debug/proc/setup_reaction()
	react_time = 0
	if(!length(cached_reactions))
		return FALSE
	var/datum/chemical_reaction/reaction = cached_reactions[index]
	if(!reaction)
		say("Unable to find reaction on index: [index]")
	say("Using forced temperatures.")
	if(reaction.reaction_flags & REACTION_INSTANT)
		say("This reaction is instant")
	for(var/reagent_type in reaction.required_reagents)
		reagents.add_reagent(reagent_type, reaction.required_reagents[reagent_type]*vol_multiplier)
	for(var/catalyst_type in reaction.required_catalysts)
		reagents.add_reagent(catalyst_type, reaction.required_catalysts[catalyst_type])
	if(should_force_temp && !min_temp)
		say("Using forced temperatures.")
		reagents.chem_temp = force_temp ? force_temp : reaction.optimal_temp
	if(should_force_ph)
		say("Using forced pH.")
		reagents.ph = force_ph ? force_ph : (reaction.optimal_ph_max + reaction.optimal_ph_min)/2
	if(failed == 0 && !should_force_temp)
		reagents.chem_temp = reaction.optimal_temp
	if(failed == 1 && !should_force_temp)
		reagents.chem_temp = reaction.required_temp+25
		failed++
	if(min_temp)
		say("Overriding temperature to required temp.")
		reagents.chem_temp = reaction.is_cold_recipe ? reaction.required_temp - 1 : reaction.required_temp + 1
	say("Reacting [span_nicegreen("[cached_reactions[index]]")] starting pH: [reagents.ph] index [index] of [cached_reactions.len]")

/obj/machinery/chem_recipe_debug/ui_data(mob/user)
	var/data = list()
	data["targetTemp"] = force_temp
	data["targatpH"] = force_ph
	data["isActive"] = reagents.is_reacting
	data["forcepH"] = should_force_ph
	data["forceTemp"] = should_force_temp
	data["targetVol"] = vol_multiplier
	data["processAll"] = process_all
	data["currentTemp"] = reagents.chem_temp
	data["currentpH"] = round(reagents.ph, 0.01)
	data["processing"] = processing
	data["index"] = index
	data["endIndex"] = cached_reactions.len
	data["beakerSpawn"] = beaker_spawn
	data["minTemp"] = min_temp
	data["editRecipe"] = null

	var/list/beaker_contents = list()
	for(var/datum/reagent/reagent as anything in reagents.reagent_list)
		beaker_contents.len++
		beaker_contents[length(beaker_contents)] = list("name" = reagent.name, "volume" = round(reagent.volume, 0.01), "purity" = round(reagent.purity))
	data["chamberContents"] = beaker_contents

	var/list/queued_reactions = list()
	for(var/datum/chemical_reaction/reaction as anything in reaction_names)
		var/datum/reagent/reagent = find_reagent_object_from_type(reaction.results[1])
		queued_reactions.len++
		queued_reactions[length(queued_reactions)] = list("name" = reagent.name)
	data["queuedReactions"] = queued_reactions

	var/list/active_reactions = list()
	var/flashing = DISABLE_FLASHING //for use with alertAfter - since there is no alertBefore, I set the after to 0 if true, or to the max value if false
	for(var/datum/equilibrium/equilibrium as anything in reagents.reaction_list)
		if(!length(reagents.reaction_list))//I'm not sure why when it explodes it causes the gui to fail (it's missing danger (?) )
			stack_trace("Chem debug managed to find an equilibrium in a location where there should be none (skipping this entry and continuing). This is usually because of an ill timed explosion.")
			continue
		if(!equilibrium.reaction.results)//Incase of no result reactions
			continue
		var/datum/reagent/reagent = reagents.get_reagent(equilibrium.reaction.results[1]) //Reactions are named after their primary products
		if(!reagent)
			continue
		var/overheat = FALSE
		var/danger = FALSE
		var/purity_alert = 2 //same as flashing
		if(reagent.purity < equilibrium.reaction.purity_min)
			purity_alert = ENABLE_FLASHING//Because 0 is seen as null
			danger = TRUE
		if(flashing != ENABLE_FLASHING)//So that the pH meter flashes for ANY reactions out of optimal
			if(equilibrium.reaction.optimal_ph_min > reagents.ph || equilibrium.reaction.optimal_ph_max < reagents.ph)
				flashing = ENABLE_FLASHING
		if(equilibrium.reaction.is_cold_recipe)
			if(equilibrium.reaction.overheat_temp > reagents.chem_temp && equilibrium.reaction.overheat_temp != NO_OVERHEAT)
				danger = TRUE
				overheat = TRUE
		else
			if(equilibrium.reaction.overheat_temp < reagents.chem_temp)
				danger = TRUE
				overheat = TRUE
		if(equilibrium.reaction.reaction_flags & REACTION_COMPETITIVE) //We have a compeitive reaction - concatenate the results for the different reactions
			for(var/entry in active_reactions)
				if(entry["name"] == reagent.name) //If we have multiple reaction methods for the same result - combine them
					entry["reactedVol"] = equilibrium.reacted_vol
					entry["targetVol"] = round(equilibrium.target_vol, 1)//Use the first result reagent to name the reaction detected
					entry["quality"] = (entry["quality"] + equilibrium.reaction_quality) /2
					continue
		active_reactions.len++
		active_reactions[length(active_reactions)] = list("name" = reagent.name, "danger" = danger, "purityAlert" = purity_alert, "quality" = equilibrium.reaction_quality, "overheat" = overheat, "inverse" = reagent.inverse_chem_val, "minPure" = equilibrium.reaction.purity_min, "reactedVol" = equilibrium.reacted_vol, "targetVol" = round(equilibrium.target_vol, 1))//Use the first result reagent to name the reaction detected
	data["activeReactions"] = active_reactions
	data["isFlashing"] = flashing

	if(edit_recipe)
		data["editRecipeName"] = edit_recipe.type
		data["editRecipeCold"] = edit_recipe.is_cold_recipe
		data["editRecipe"] = list(
			list("name" = "required_temp" , "var" = edit_recipe.required_temp),
			list("name" = "optimal_temp" , "var" = edit_recipe.optimal_temp),
			list("name" = "overheat_temp" , "var" = edit_recipe.overheat_temp),
			list("name" = "optimal_ph_min" , "var" = edit_recipe.optimal_ph_min),
			list("name" = "optimal_ph_max" , "var" = edit_recipe.optimal_ph_max),
			list("name" = "determin_ph_range" , "var" = edit_recipe.determin_ph_range),
			list("name" = "temp_exponent_factor" , "var" = edit_recipe.temp_exponent_factor),
			list("name" = "ph_exponent_factor" , "var" = edit_recipe.ph_exponent_factor),
			list("name" = "thermic_constant" , "var" = edit_recipe.thermic_constant),
			list("name" = "H_ion_release" , "var" = edit_recipe.H_ion_release),
			list("name" = "rate_up_lim" , "var" = edit_recipe.rate_up_lim),
			list("name" = "purity_min" , "var" = edit_recipe.purity_min),
		)

	return data

/obj/machinery/chem_recipe_debug/ui_act(action, params)
	. = ..()
	if(.)
		return
	switch(action)
		if("power")
			return
		if("temperature")
			var/target = params["target"]
			if(text2num(target) != null)
				target = text2num(target)
				. = TRUE
			if(.)
				force_temp = clamp(target, 0, 1000)
		if("pH")
			var/target = params["target"]
			if(text2num(target) != null)
				target = text2num(target)
				. = TRUE
			if(.)
				force_ph = target
		if("forceTemp")
			should_force_temp = ! should_force_temp
			. = TRUE
		if("forcepH")
			should_force_ph = ! should_force_ph
			. = TRUE
		if("react")
			react = TRUE
			return TRUE
		if("all")
			process_all = !process_all
			return TRUE
		if("beakerSpawn")
			beaker_spawn = !beaker_spawn
			return TRUE
		if("setTargetList")
			var/text = stripped_input(usr,"List","Enter a list of Recipe product names separated by commas", "Recipe", MAX_MESSAGE_LEN)
			reaction_names = list()
			if(!text)
				say("Could not find reaction")
			var/list/names = splittext("[text]", ",")
			for(var/name in names)
				var/datum/reagent/reagent = find_reagent_object_from_type(get_chem_id(name))
				if(!reagent)
					say("Could not find [name]")
					continue
				var/datum/chemical_reaction/reaction = GLOB.chemical_reactions_list_product_index[reagent.type]
				if(!reaction)
					say("Could not find [name] reaction!")
					continue
				reaction_names += reaction
		if("vol")
			var/target = params["target"]
			if(text2num(target) != null)
				target = text2num(target)
				. = TRUE
			if(.)
				vol_multiplier = clamp(target, 1, 200)
		if("start")
			if(processing)
				say("currently processing reaction [index]: [cached_reactions[index]] of [cached_reactions.len]")
				return
			say("Starting processing")
			index = 1
			setup_reactions()
			begin_processing()
			return TRUE
		if("stop")
			relay_all_reactions()
		if("minTemp")
			min_temp = !min_temp
		if("setEdit")
			var/name = (input("Enter the name of any reagent", "Input") as text|null)
			reaction_names = list()
			if(!text)
				say("Could not find reaction")
			var/datum/reagent/reagent = find_reagent_object_from_type(get_chem_id(name))
			if(!reagent)
				say("Could not find [name]")
				return
			var/datum/chemical_reaction/reaction = GLOB.chemical_reactions_list_product_index[reagent.type]
			if(!reaction)
				say("Could not find [name] reaction!")
				return
			edit_recipe = reaction[1]
		if("updateVar")
			var/target = params["target"]
			edit_recipe.vars[params["type"]] = target
		if("export")
			var/export = {"[edit_recipe.type]
[edit_recipe.is_cold_recipe ? "is_cold_recipe = TRUE" : ""]
required_temp = [edit_recipe.required_temp]
optimal_temp = [edit_recipe.optimal_temp]
overheat_temp = [edit_recipe.overheat_temp]
optimal_ph_min = [edit_recipe.optimal_ph_min]
optimal_ph_max = [edit_recipe.optimal_ph_max]
determin_ph_range = [edit_recipe.determin_ph_range]
temp_exponent_factor = [edit_recipe.temp_exponent_factor]
ph_exponent_factor = [edit_recipe.ph_exponent_factor]
thermic_constant = [edit_recipe.thermic_constant]
H_ion_release = [edit_recipe.H_ion_release]
rate_up_lim = [edit_recipe.rate_up_lim]
purity_min = [edit_recipe.purity_min]"}
			say(export)
			text2file(export, "[GLOB.log_directory]/chem_parse.txt")

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

///Moves a type of buffer from the heater to the beaker,

/obj/machinery/chem_recipe_debug/ui_status(mob/user)
	return UI_INTERACTIVE

/obj/machinery/chem_recipe_debug/ui_state(mob/user)
	return GLOB.physical_state