--By Mami
local flib_event = require("__flib__.event")
local floor = math.floor
local table_insert = table.insert


---@param map_data MapData
---@param station Station
---@param manifest Manifest
---@param sign int?
local function set_comb1(map_data, station, manifest, sign)
	local comb = station.entity_comb1
	if comb.valid then
		if manifest then
			local signals = {}
			for i, item in ipairs(manifest) do
				signals[i] = {index = i, signal = {type = item.type, name = item.name}, count = sign*item.count}
			end
			set_combinator_output(map_data, comb, signals)
		else
			set_combinator_output(map_data, comb, nil)
		end
	end
end

---@param map_data MapData
---@param train Train
local function on_failed_delivery(map_data, train)
	--NOTE: must change train status to STATUS_D or remove it from tracked trains after this call
	local is_p_delivery_made = train.status ~= STATUS_D_TO_P and train.status ~= STATUS_P
	if not is_p_delivery_made then
		local station = map_data.stations[train.p_station_id]
		remove_manifest(map_data, station, train.manifest, 1)
		if train.status == STATUS_P then
			set_comb1(map_data, station, nil)
			unset_wagon_combs(map_data, station)
		end
	end
	local is_r_delivery_made = train.status == STATUS_R_TO_D
	if not is_r_delivery_made then
		local station = map_data.stations[train.r_station_id]
		remove_manifest(map_data, station, train.manifest, -1)
		if train.status == STATUS_R then
			set_comb1(map_data, station, nil)
			unset_wagon_combs(map_data, station)
		end
	end
	train.r_station_id = 0
	train.p_station_id = 0
	train.manifest = nil
end


---@param map_data MapData
---@param depot_id uint
---@param train_id uint
local function add_available_train(map_data, depot_id, train_id)
	local depot = map_data.depots[depot_id]
	local train = map_data.trains[train_id]
	local comb = depot.entity_comb
	local network_name = get_comb_network_name(comb)
	if network_name then
		local network = map_data.available_trains[network_name]
		if not network then
			network = {}
			map_data.available_trains[network_name] = network
		end
		network[train_id] = depot_id
	end
	depot.available_train_id = train_id
	train.status = STATUS_D
	train.depot_id = depot_id
	train.depot_name = depot.entity_stop.backer_name
	train.network_name = network_name
	train.network_flag = mod_settings.network_flag
	train.priority = 0
	if network_name then
		local signals = comb.get_merged_signals(defines.circuit_connector_id.combinator_input)
		if signals then
			for k, v in pairs(signals) do
				local item_name = v.signal.name
				local item_count = v.count
				if item_name then
					if item_name == SIGNAL_PRIORITY then
						train.priority = item_count
					end
					if item_name == network_name then
						train.network_flag = item_count
					end
				end
			end
		end
	end
end
---@param map_data MapData
---@param train Train
---@param depot Depot
function remove_available_train(map_data, train, depot)
	---@type uint
	local train_id = depot.available_train_id
	if train.network_name then
		local network = map_data.available_trains[train.network_name]
		if network then
			network[train_id] = nil
			if next(network) == nil then
				map_data.available_trains[train.network_name] = nil
			end
		end
	end
	depot.available_train_id = nil
end


---@param map_data MapData
---@param stop LuaEntity
---@param comb LuaEntity
local function on_depot_built(map_data, stop, comb)
	local depot = {
		entity_stop = stop,
		entity_comb = comb,
		--available_train = nil,
	}
	map_data.depots[stop.unit_number] = depot
end

---@param map_data MapData
---@param depot Depot
local function on_depot_broken(map_data, depot)
	local train_id = depot.available_train_id
	if train_id then
		local train = map_data.trains[train_id]
		lock_train(train.entity)
		send_lost_train_alert(train.entity, depot.entity_stop.backer_name)
		remove_available_train(map_data, train, depot)
		map_data.trains[train_id] = nil
	end
	map_data.depots[depot.entity_stop.unit_number] = nil
end

---@param map_data MapData
---@param stop LuaEntity
---@param comb1 LuaEntity
---@param comb2 LuaEntity
local function on_station_built(map_data, stop, comb1, comb2)
	local station = {
		entity_stop = stop,
		entity_comb1 = comb1,
		entity_comb2 = comb2,
		wagon_combs = nil,
		deliveries_total = 0,
		last_delivery_tick = map_data.total_ticks,
		priority = 0,
		r_threshold = 0,
		locked_slots = 0,
		network_flag = 0,
		deliveries = {},
		accepted_layouts = {},
		layout_pattern = nil,
		p_count_or_r_threshold_per_item = {},
	}
	set_station_from_comb_state(station)
	local id = stop.unit_number--[[@as uint]]
	map_data.stations[id] = station
	map_data.warmup_station_ids[#map_data.warmup_station_ids + 1] = id

	update_station_if_auto(map_data, station, nil)
end
---@param map_data MapData
---@param station_id uint
---@param station Station
local function on_station_broken(map_data, station_id, station)
	if station.deliveries_total > 0 then
		--search for trains coming to the destroyed station
		for train_id, train in pairs(map_data.trains) do
			local is_r = train.r_station_id == station_id
			local is_p = train.p_station_id == station_id
			if is_p or is_r then
				local is_p_delivery_made = train.status ~= STATUS_D_TO_P and train.status ~= STATUS_P
				local is_r_delivery_made = train.status == STATUS_R_TO_D
				if (is_r and not is_r_delivery_made) or (is_p and not is_p_delivery_made) then
					--train is attempting delivery to a stop that was destroyed, stop it
					on_failed_delivery(map_data, train)
					if train.entity then
						remove_train(map_data, train, train_id)
						lock_train(train.entity)
						send_lost_train_alert(train.entity, train.depot_name)
					else
						train.se_awaiting_removal = train_id
					end
				end
			end
		end
	end
	map_data.stations[station_id] = nil
end

---@param map_data MapData
---@param stop LuaEntity
---@param comb_operation string
---@param comb_forbidden LuaEntity?
local function search_for_station_combinator(map_data, stop, comb_operation, comb_forbidden)
	local pos_x = stop.position.x
	local pos_y = stop.position.y
	local search_area = {
		{pos_x - 2, pos_y - 2},
		{pos_x + 2, pos_y + 2}
	}
	local entities = stop.surface.find_entities(search_area)
	for _, entity in pairs(entities) do
		if
		entity.valid and entity.name == COMBINATOR_NAME and
		entity ~= comb_forbidden and map_data.to_stop[entity.unit_number] == stop
		then
			local param = get_comb_params(entity)
			if param.operation == comb_operation then
				return entity
			end
		end
	end
end

---@param map_data MapData
---@param comb LuaEntity
local function on_combinator_built(map_data, comb)
	local pos_x = comb.position.x
	local pos_y = comb.position.y

	local search_area
	if comb.direction == defines.direction.north or comb.direction == defines.direction.south then
		search_area = {
			{pos_x - 1.5, pos_y - 2},
			{pos_x + 1.5, pos_y + 2}
		}
	else
		search_area = {
			{pos_x - 2, pos_y - 1.5},
			{pos_x + 2, pos_y + 1.5}
		}
	end
	local stop = nil
	local rail = nil
	local entities = comb.surface.find_entities(search_area)
	for _, cur_entity in pairs(entities) do
		if cur_entity.valid then
			if cur_entity.name == "train-stop" then
				--NOTE: if there are multiple stops we take the later one
				stop = cur_entity
			elseif cur_entity.type == "straight-rail" then
				rail = cur_entity
			end
		end
	end

	local out = comb.surface.create_entity({
		name = COMBINATOR_OUT_NAME,
		position = comb.position,
		force = comb.force
	})
	assert(out, "cybersyn: could not spawn combinator controller")
	comb.connect_neighbour({
		target_entity = out,
		source_circuit_id = defines.circuit_connector_id.combinator_output,
		wire = defines.wire_type.green,
	})
	comb.connect_neighbour({
		target_entity = out,
		source_circuit_id = defines.circuit_connector_id.combinator_output,
		wire = defines.wire_type.red,
	})

	local control = get_comb_control(comb)
	local params = control.parameters
	local op = params.operation

	if op == OPERATION_DEFAULT then
		op = OPERATION_PRIMARY_IO
		params.operation = op
		params.first_signal = NETWORK_SIGNAL_DEFAULT
		control.parameters = params
	elseif op ~= OPERATION_PRIMARY_IO and op ~= OPERATION_SECONDARY_IO and op ~= OPERATION_DEPOT and op ~= OPERATION_WAGON_MANIFEST then
		op = OPERATION_PRIMARY_IO
		params.operation = op
		control.parameters = params
	end

	map_data.to_comb[comb.unit_number] = comb
	map_data.to_comb_params[comb.unit_number] = params
	map_data.to_output[comb.unit_number] = out
	map_data.to_stop[comb.unit_number] = stop

	if op == OPERATION_WAGON_MANIFEST then
		if rail then
			update_station_from_rail(map_data, rail, nil, true)
		end
	elseif op == OPERATION_DEPOT then
		if stop then
			local station = map_data.stations[stop.unit_number]
			---@type Depot
			local depot = map_data.depots[stop.unit_number]
			if depot or station then
				--NOTE: repeated combinators are ignored
			else
				on_depot_built(map_data, stop, comb)
			end
		end
	elseif op == OPERATION_SECONDARY_IO then
		if stop then
			local station = map_data.stations[stop.unit_number]
			if station and not station.entity_comb2 then
				station.entity_comb2 = comb
			end
		end
	elseif op == OPERATION_PRIMARY_IO then
		if stop then
			local station = map_data.stations[stop.unit_number]
			if station then
				--NOTE: repeated combinators are ignored
			else
				local depot = map_data.depots[stop.unit_number]
				if depot then
					on_depot_broken(map_data, depot)
				end
				--no station or depot
				--add station

				local comb2 = search_for_station_combinator(map_data, stop, OPERATION_SECONDARY_IO, comb)

				on_station_built(map_data, stop, comb, comb2)
			end
		end
	end
end
---@param map_data MapData
---@param comb LuaEntity
---@param network_name string?
function on_combinator_network_updated(map_data, comb, network_name)
	local stop = map_data.to_stop[comb.unit_number]

	if stop and stop.valid then
		local station = map_data.stations[stop.unit_number]
		if station then
			if station.entity_comb1 == comb then
				station.network_name = network_name
			end
		else
			local depot_id = stop.unit_number
			local depot = map_data.depots[depot_id]
			if depot and depot.entity_comb == comb then
				local train_id = depot.available_train_id
				if train_id then
					local train = map_data.trains[train_id]
					remove_available_train(map_data, train, depot)
					add_available_train(map_data, depot_id, train_id)
				end
			end
		end
	end
end
---@param map_data MapData
---@param comb LuaEntity
function on_combinator_broken(map_data, comb)
	--NOTE: we do not check for wagon manifest combinators and update their stations, it is assumed they will be lazy deleted later
	---@type uint
	local comb_id = comb.unit_number
	local out = map_data.to_output[comb_id]
	local stop = map_data.to_stop[comb_id]

	if stop and stop.valid then
		local station = map_data.stations[stop.unit_number]
		if station then
			if station.entity_comb1 == comb then
				local comb1 = search_for_station_combinator(map_data, stop, OPERATION_PRIMARY_IO, comb)
				if comb1 then
					station.entity_comb1 = comb1
					set_station_from_comb_state(station)
					update_station_if_auto(map_data, station)
				else
					on_station_broken(map_data, stop.unit_number, station)
					local depot_comb = search_for_station_combinator(map_data, stop, OPERATION_DEPOT, comb)
					if depot_comb then
						on_depot_built(map_data, stop, depot_comb)
					end
				end
			elseif station.entity_comb2 == comb then
				station.entity_comb2 = search_for_station_combinator(map_data, stop, OPERATION_SECONDARY_IO, comb)
			end
		else
			local depot = map_data.depots[stop.unit_number]
			if depot and depot.entity_comb == comb then
				--NOTE: this will disrupt deliveries in progress that where dispatched from this station in a minor way
				local depot_comb = search_for_station_combinator(map_data, stop, OPERATION_DEPOT, comb)
				if depot_comb then
					depot.entity_comb = depot_comb
				else
					on_depot_broken(map_data, depot)
				end
			end
		end
	end

	if out and out.valid then
		out.destroy()
	end
	map_data.to_comb[comb_id] = nil
	map_data.to_output[comb_id] = nil
	map_data.to_stop[comb_id] = nil
	map_data.to_comb_params[comb_id] = nil
end

---@param map_data MapData
---@param comb LuaEntity
function combinator_update(map_data, comb)
	---@type uint
	local unit_number = comb.unit_number
	local control = get_comb_control(comb)
	local params = control.parameters
	local old_params = map_data.to_comb_params[unit_number]

	if params.operation ~= old_params.operation then
		if (old_params.operation == OPERATION_PRIMARY_IO) and (params.operation == OPERATION_PRIMARY_IO_ACTIVE or params.operation == OPERATION_PRIMARY_IO_FAILED_REQUEST) then
			--make sure only OPERATION_PRIMARY_IO gets stored on map_data.to_comb_params
			params.operation = OPERATION_PRIMARY_IO
		else
			--NOTE: This is rather dangerous, we may need to actually implement operation changing
			on_combinator_broken(map_data, comb)
			on_combinator_built(map_data, comb)
			return
		end
	end
	local new_signal = params.first_signal
	local old_signal = old_params.first_signal
	local new_network = new_signal and new_signal.name or nil
	local old_network = old_signal and old_signal.name or nil
	if new_network ~= old_network then
		on_combinator_network_updated(map_data, comb, new_network)
		map_data.to_comb_params[unit_number] = params
	end
	if params.second_constant ~= old_params.second_constant then
		local stop = global.to_stop[comb.unit_number]
		if stop then
			local station = global.stations[stop.unit_number]
			if station then
				local bits = params.second_constant
				local is_pr_state = floor(bits/2)%3
				station.is_p = is_pr_state == 0 or is_pr_state == 1
				station.is_r = is_pr_state == 0 or is_pr_state == 2
				local allows_all_trains = bits%2 == 1
				if station.allows_all_trains ~= allows_all_trains then
					station.allows_all_trains = allows_all_trains
					update_station_if_auto(map_data, station)
				end
			end
		end
		map_data.to_comb_params[unit_number] = params
	end
end

---@param map_data MapData
---@param stop LuaEntity
local function on_stop_built(map_data, stop)
	local pos_x = stop.position.x
	local pos_y = stop.position.y

	local search_area = {
		{pos_x - 2, pos_y - 2},
		{pos_x + 2, pos_y + 2}
	}
	local comb2 = nil
	local comb1 = nil
	local depot_comb = nil
	local entities = stop.surface.find_entities(search_area)
	for _, entity in pairs(entities) do
		if entity.valid and entity.name == COMBINATOR_NAME and map_data.to_stop[entity.unit_number] == nil then
			map_data.to_stop[entity.unit_number] = stop
			local param = get_comb_params(entity)
			local op = param.operation
			if op == OPERATION_PRIMARY_IO then
				comb1 = entity
			elseif op == OPERATION_SECONDARY_IO then
				comb2 = entity
			elseif op == OPERATION_DEPOT then
				depot_comb = entity
			end
		end
	end
	if comb1 then
		on_station_built(map_data, stop, comb1, comb2)
	elseif depot_comb then
		on_depot_built(map_data, stop, depot_comb)
	end
end
---@param map_data MapData
---@param stop LuaEntity
local function on_stop_broken(map_data, stop)
	local pos_x = stop.position.x
	local pos_y = stop.position.y

	local search_area = {
		{pos_x - 2, pos_y - 2},
		{pos_x + 2, pos_y + 2}
	}
	local entities = stop.surface.find_entities(search_area)
	for _, entity in pairs(entities) do
		if entity.valid and map_data.to_stop[entity.unit_number] == stop then
			map_data.to_stop[entity.unit_number] = nil
		end
	end

	local station = map_data.stations[stop.unit_number]
	if station then
		on_station_broken(map_data, stop.unit_number, station)
	else
		local depot = map_data.depots[stop.unit_number]
		if depot then
			on_depot_broken(map_data, depot)
		end
	end
end
---@param map_data MapData
---@param stop LuaEntity
---@param old_name string
local function on_station_rename(map_data, stop, old_name)
	--search for trains coming to the renamed station
	local station_id = stop.unit_number
	local station = map_data.stations[station_id]
	if station and station.deliveries_total > 0 then
		for train_id, train in pairs(map_data.trains) do
			local is_p = train.p_station_id == station_id
			local is_r = train.r_station_id == station_id
			if is_p or is_r then
				local is_p_delivery_made = train.status ~= STATUS_D_TO_P and train.status ~= STATUS_P
				local is_r_delivery_made = train.status == STATUS_R_TO_D
				if is_r and not is_r_delivery_made then
					local r_station = map_data.stations[train.r_station_id]
					if train.entity then
						rename_manifest_schedule(train.entity, r_station.entity_stop, old_name)
					elseif IS_SE_PRESENT then
						train.se_awaiting_rename = {r_station.entity_stop, old_name}
					end
				elseif is_p and not is_p_delivery_made then
					--train is attempting delivery to a stop that was renamed
					local p_station = map_data.stations[train.p_station_id]
					if train.entity then
						rename_manifest_schedule(train.entity, p_station.entity_stop, old_name)
					elseif IS_SE_PRESENT then
						train.se_awaiting_rename = {p_station.entity_stop, old_name}
					end
				end
			end
		end
	else
		local depot = map_data.depots[station_id]
		if depot and depot.available_train_id then
			local train = map_data.trains[depot.available_train_id]
			train.depot_name = stop.backer_name
		end
	end
end


---@param map_data MapData
local function find_and_add_all_stations_from_nothing(map_data)
	for _, surface in pairs(game.surfaces) do
		local entities = surface.find_entities_filtered({name = COMBINATOR_NAME})
		for k, comb in pairs(entities) do
			if comb.valid then
				on_combinator_built(map_data, comb)
			end
		end
	end
end

---@param map_data MapData
---@param depot_id uint
---@param train_entity LuaTrain
local function on_train_arrives_depot(map_data, depot_id, train_entity)
	local contents = train_entity.get_contents()
	local fluid_contents = train_entity.get_fluid_contents()
	local is_train_empty = next(contents) == nil and next(fluid_contents) == nil
	local train_id = train_entity.id
	local train = map_data.trains[train_id]
	if train then
		if train.manifest and train.status == STATUS_R_TO_D then
			--succeeded delivery
			train.p_station_id = 0
			train.r_station_id = 0
			train.manifest = nil
			add_available_train(map_data, depot_id, train_id)
		else
			if train.manifest then
				on_failed_delivery(map_data, train)
				send_unexpected_train_alert(train.entity)
			end
			add_available_train(map_data, depot_id, train_id)
		end
		if is_train_empty then
			set_depot_schedule(train_entity, train.depot_name)
		else
			--train still has cargo
			lock_train(train_entity)
			remove_train(map_data, train, train_id)
			send_nonempty_train_in_depot_alert(train_entity)
		end
	elseif is_train_empty then
		train = {
			status = STATUS_D,
			entity = train_entity,
			layout_id = 0,
			item_slot_capacity = 0,
			fluid_capacity = 0,
			p_station_id = 0,
			r_station_id = 0,
			last_manifest_tick = map_data.total_ticks,
			--manifest = nil,
		}
		update_train_layout(map_data, train)
		map_data.trains[train_id] = train
		add_available_train(map_data, depot_id, train_id)

		set_depot_schedule(train_entity, train.depot_name)
	else
		lock_train(train_entity)
		send_nonempty_train_in_depot_alert(train_entity)
	end
end
---@param map_data MapData
---@param stop LuaEntity
---@param train Train
local function on_train_arrives_buffer(map_data, stop, train)
	if train.manifest then
		---@type uint
		local station_id = stop.unit_number
		if train.status == STATUS_D_TO_P then
			if train.p_station_id == station_id then
				train.status = STATUS_P
				local station = map_data.stations[station_id]
				set_comb1(map_data, station, train.manifest, 1)
				set_p_wagon_combs(map_data, station, train)
			end
		elseif train.status == STATUS_P_TO_R then
			if train.r_station_id == station_id then
				train.status = STATUS_R
				local station = map_data.stations[station_id]
				set_comb1(map_data, station, train.manifest, -1)
				set_r_wagon_combs(map_data, station, train)
			end
		elseif train.status == STATUS_P and train.p_station_id == station_id then
			--this player intervention that is considered valid
		elseif (train.status == STATUS_R or train.status == STATUS_R_TO_D) and train.r_station_id == station_id then
			--this player intervention that is considered valid
		else
			on_failed_delivery(map_data, train)
			remove_train(map_data, train, train.entity.id)
			lock_train(train.entity)
			send_lost_train_alert(train.entity, train.depot_name)
		end
	else
		--train is lost somehow, probably from player intervention
		remove_train(map_data, train, train.entity.id)
	end
end
---@param map_data MapData
---@param train Train
local function on_train_leaves_station(map_data, train)
	if train.manifest then
		if train.status == STATUS_P then
			train.status = STATUS_P_TO_R
			local station = map_data.stations[train.p_station_id]
			remove_manifest(map_data, station, train.manifest, 1)
			set_comb1(map_data, station, nil)
			unset_wagon_combs(map_data, station)
			if train.has_filtered_wagon then
				train.has_filtered_wagon = false
				for carriage_i, carriage in ipairs(train.entity.carriages) do
					if carriage.type == "cargo-wagon" then
						local inv = carriage.get_inventory(defines.inventory.cargo_wagon)
						if inv and inv.is_filtered() then
							---@type uint
							for i = 1, #inv do
								inv.set_filter(i, nil)
							end
						end
					end
				end
			end
		elseif train.status == STATUS_R then
			train.status = STATUS_R_TO_D
			local station = map_data.stations[train.r_station_id]
			remove_manifest(map_data, station, train.manifest, -1)
			set_comb1(map_data, station, nil)
			unset_wagon_combs(map_data, station)
		end
	elseif train.status == STATUS_D then
		local depot = map_data.depots[train.depot_id]
		remove_available_train(map_data, train, depot)
	end
end


---@param map_data MapData
---@param train Train
local function on_train_broken(map_data, train)
	--NOTE: train.entity is only absent if the train is climbing a space elevator as of 0.5.0
	if train.manifest and train.entity then
		on_failed_delivery(map_data, train)
		remove_train(map_data, train, train.entity.id)
		if train.entity.valid then
			lock_train(train.entity)
		end
	end
end
---@param map_data MapData
---@param pre_train_id uint
---@param train_entity LuaEntity
local function on_train_modified(map_data, pre_train_id, train_entity)
	local train = map_data.trains[pre_train_id]
	--NOTE: train.entity is only absent if the train is climbing a space elevator as of 0.5.0
	if train and train.entity then
		if train.manifest then
			on_failed_delivery(map_data, train)
		end
		remove_train(map_data, train, pre_train_id)
		if train.entity.valid then
			lock_train(train.entity)
		end
	end
end


local function on_built(event)
	local entity = event.entity or event.created_entity
	if not entity or not entity.valid then return end

	if entity.name == "train-stop" then
		on_stop_built(global, entity)
	elseif entity.name == COMBINATOR_NAME then
		on_combinator_built(global, entity)
	elseif entity.type == "inserter" then
		update_station_from_inserter(global, entity)
	elseif entity.type == "pump" then
		update_station_from_pump(global, entity)
	elseif entity.type == "straight-rail" then
		update_station_from_rail(global, entity)
	end
end
local function on_broken(event)
	local entity = event.entity
	if not entity or not entity.valid then return end

	if entity.name == "train-stop" then
		on_stop_broken(global, entity)
	elseif entity.name == COMBINATOR_NAME then
		on_combinator_broken(global, entity)
	elseif entity.type == "inserter" then
		update_station_from_inserter(global, entity, entity)
	elseif entity.type == "pump" then
		update_station_from_pump(global, entity, entity)
	elseif entity.type == "straight-rail" then
		update_station_from_rail(global, entity, nil)
	elseif entity.train then
		local train = global.trains[entity.train.id]
		if train then
			on_train_broken(global, train)
		end
	end
end
local function on_rotate(event)
	local entity = event.entity or event.created_entity
	if not entity or not entity.valid then return end

	if entity.type == "inserter" then
		update_station_from_inserter(global, entity)
	end
end
local function on_rename(event)
	if event.entity.name == "train-stop" then
		on_station_rename(global, event.entity, event.old_name)
	end
end

local function on_train_built(event)
	local train_e = event.train
	if event.old_train_id_1 then
		on_train_modified(global, event.old_train_id_1, train_e)
	end
	if event.old_train_id_2 then
		on_train_modified(global, event.old_train_id_2, train_e)
	end
end
local function on_train_changed(event)
	local train_e = event.train
	if not train_e.valid then return end
	local train = global.trains[train_e.id]
	if train_e.state == defines.train_state.wait_station then
		local stop = train_e.station
		if stop and stop.valid and stop.name == "train-stop" then
			if global.stations[stop.unit_number] then
				if train then
					on_train_arrives_buffer(global, stop, train)
				end
			else
				local depot_id = stop.unit_number
				if global.depots[depot_id] then
					on_train_arrives_depot(global, depot_id, train_e)
				end
			end
		end
	elseif event.old_state == defines.train_state.wait_station then
		if train then
			on_train_leaves_station(global, train)
		end
	end
end

local function on_surface_removed(event)
	local surface = game.surfaces[event.surface_index]
	if surface then
		local train_stops = surface.find_entities_filtered({type = "train-stop"})
		for _, entity in pairs(train_stops) do
			if entity.valid and entity.name == "train-stop" then
				on_stop_broken(global, entity)
			end
		end
	end
end


local function on_paste(event)
	local entity = event.destination
	if not entity or not entity.valid then return end

	if entity.name == COMBINATOR_NAME then
		combinator_update(global, entity)
	end
end


local function on_settings_changed(event)
	mod_settings.tps = settings.global["cybersyn-ticks-per-second"].value --[[@as int]]
	mod_settings.update_rate = settings.global["cybersyn-update-rate"].value --[[@as int]]
	mod_settings.r_threshold = settings.global["cybersyn-request-threshold"].value--[[@as int]]
	mod_settings.network_flag = settings.global["cybersyn-network-flag"].value--[[@as int]]
	mod_settings.warmup_time = settings.global["cybersyn-warmup-time"].value--[[@as int]]
	mod_settings.stuck_train_time = settings.global["cybersyn-stuck-train-time"].value--[[@as int]]
	if event.setting == "cybersyn-ticks-per-second" then
		local nth_tick = math.ceil(60/mod_settings.tps);
		flib_event.on_nth_tick(nil)
		flib_event.on_nth_tick(nth_tick, function()
			tick(global, mod_settings)
		end)
	end
end


local filter_built = {
	{filter = "name", name = "train-stop"},
	{filter = "name", name = COMBINATOR_NAME},
	{filter = "type", type = "inserter"},
	{filter = "type", type = "pump"},
	{filter = "type", type = "straight-rail"},
}
local filter_broken = {
	{filter = "name", name = "train-stop"},
	{filter = "name", name = COMBINATOR_NAME},
	{filter = "type", type = "inserter"},
	{filter = "type", type = "pump"},
	{filter = "type", type = "straight-rail"},
	{filter = "rolling-stock"},
}
local function main()
	mod_settings.tps = settings.global["cybersyn-ticks-per-second"].value --[[@as int]]
	mod_settings.update_rate = settings.global["cybersyn-update-rate"].value --[[@as int]]
	mod_settings.r_threshold = settings.global["cybersyn-request-threshold"].value--[[@as int]]
	mod_settings.network_flag = settings.global["cybersyn-network-flag"].value--[[@as int]]
	mod_settings.warmup_time = settings.global["cybersyn-warmup-time"].value--[[@as int]]
	mod_settings.stuck_train_time = settings.global["cybersyn-stuck-train-time"].value--[[@as int]]

	--NOTE: There is a concern that it is possible to build or destroy important entities without one of these events being triggered, in which case the mod will have undefined behavior
	flib_event.register(defines.events.on_built_entity, on_built, filter_built)
	flib_event.register(defines.events.on_robot_built_entity, on_built, filter_built)
	flib_event.register({defines.events.script_raised_built, defines.events.script_raised_revive, defines.events.on_entity_cloned}, on_built)

	flib_event.register(defines.events.on_player_rotated_entity, on_rotate)

	flib_event.register(defines.events.on_pre_player_mined_item, on_broken, filter_broken)
	flib_event.register(defines.events.on_robot_pre_mined, on_broken, filter_broken)
	flib_event.register(defines.events.on_entity_died, on_broken, filter_broken)
	flib_event.register(defines.events.script_raised_destroy, on_broken)

	flib_event.register({defines.events.on_pre_surface_deleted, defines.events.on_pre_surface_cleared}, on_surface_removed)

	flib_event.register(defines.events.on_entity_settings_pasted, on_paste)

	local nth_tick = math.ceil(60/mod_settings.tps);
	flib_event.on_nth_tick(nth_tick, function()
		tick(global, mod_settings)
	end)

	flib_event.register(defines.events.on_train_created, on_train_built)
	flib_event.register(defines.events.on_train_changed_state, on_train_changed)

	flib_event.register(defines.events.on_entity_renamed, on_rename)

	flib_event.register(defines.events.on_runtime_mod_setting_changed, on_settings_changed)

	register_gui_actions()

	flib_event.on_init(init_global)

	flib_event.on_configuration_changed(on_config_changed)


	if IS_SE_PRESENT then
		flib_event.on_load(function()
			local se_on_train_teleport_finished_event = remote.call("space-exploration", "get_on_train_teleport_finished_event")
			local se_on_train_teleport_started_event = remote.call("space-exploration", "get_on_train_teleport_started_event")


			flib_event.register(se_on_train_teleport_started_event, function(event)
				---@type MapData
				local map_data = global
				local old_id = event.old_train_id_1
				--NOTE: this is not guaranteed to be unique, it should be fine since the window of time for another train to mistakenly steal this train's event data is miniscule
				--NOTE: please SE dev if you read this fix the issue where se_on_train_teleport_finished_event is returning the wrong old train id
				local train_unique_identifier = event.train.front_stock.backer_name

				local train = map_data.trains[old_id]
				if not train then return end
				--NOTE: IMPORTANT, until se_on_train_teleport_finished_event is called map_data.trains[old_id] will reference an invalid train entity; very few of our events care about this and the ones that do should be impossible to trigger until teleportation is finished
				train.entity = nil
				map_data.se_tele_old_id[train_unique_identifier] = old_id
			end)
			flib_event.register(se_on_train_teleport_finished_event, function(event)
				---@type MapData
				local map_data = global
				---@type LuaTrain
				local train_entity = event.train
				---@type uint
				local new_id = train_entity.id
				local old_surface_index = event.old_surface_index
				local train_unique_identifier = event.train.front_stock.backer_name

				--NOTE: event.old_train_id_1 from this event is useless, it's for one of the many transient trains SE spawns while teleporting the old train, only se_on_train_teleport_started_event returns the correct old train id
				--NOTE: please SE dev if you read this fix the issue where se_on_train_teleport_finished_event is returning the wrong old train id
				local old_id = map_data.se_tele_old_id[train_unique_identifier]
				map_data.se_tele_old_id[train_unique_identifier] = nil
				local train = map_data.trains[old_id]
				if not train then return end

				map_data.trains[new_id] = train
				map_data.trains[old_id] = nil
				train.entity = train_entity

				if train.se_awaiting_removal then
					remove_train(map_data, train, train.se_awaiting_removal)
					lock_train(train.entity)
					send_lost_train_alert(train.entity, train.depot_name)
					return
				elseif train.se_awaiting_rename then
					rename_manifest_schedule(train.entity, train.se_awaiting_rename[1], train.se_awaiting_rename[2])
					train.se_awaiting_rename = nil
				end

				if not (train.status == STATUS_D_TO_P or train.status == STATUS_P_TO_R) then return end

				local schedule = train_entity.schedule
				if schedule then
					local p_station = map_data.stations[train.p_station_id]
					local p_name = p_station.entity_stop.backer_name
					local p_surface_i = p_station.entity_stop.surface.index
					local r_station = map_data.stations[train.r_station_id]
					local r_name = r_station.entity_stop.backer_name
					local r_surface_i = r_station.entity_stop.surface.index
					local records = schedule.records
					local i = schedule.current
					while i <= #records do
						if records[i].station == p_name and p_surface_i ~= old_surface_index then
							table_insert(records, i, create_direct_to_station_order(p_station.entity_stop))
							i = i + 1
						elseif records[i].station == r_name and r_surface_i ~= old_surface_index then
							table_insert(records, i, create_direct_to_station_order(r_station.entity_stop))
							i = i + 1
						end
						i = i + 1
					end
					train_entity.schedule = schedule
				end
			end)
		end)
	end
end


main()
