local Quality = {}

Quality.visible_quality_prototypes_count = 0
for _, quality in pairs(prototypes.quality) do
  if (not quality.hidden) then
    Quality.visible_quality_prototypes_count = Quality.visible_quality_prototypes_count + 1
  end
end

-- whether the control stage should take quality into account,
-- this shouldn't be modified anywhere, consider it read only.
Quality.enabled = Quality.visible_quality_prototypes_count > 1

-- we cannot assume the technologies exist or the unlock method is as expected
Quality.use_console_capture_research_trigger_for_epic = false
Quality.use_console_capture_research_trigger_for_legendary = false

Quality.all_module_names_map = {}
for _, item in pairs(prototypes.item) do
  if item.type == "module" then
    Quality.all_module_names_map[item.name] = true
  end
end

do
  local tech_e = prototypes.technology["epic-quality"]
  if tech_e and tech_e.research_trigger and tech_e.research_trigger.type == "capture-spawner" and tech_e.research_trigger.entity == mod_prefix .. "spaceship-console" then
    Quality.use_console_capture_research_trigger_for_epic = true
  end

  local tech_l = prototypes.technology["legendary-quality"]
  if tech_l and tech_l.research_trigger and tech_l.research_trigger.type == "capture-spawner" and tech_l.research_trigger.entity == mod_prefix .. "spaceship-console-alt" then
    Quality.use_console_capture_research_trigger_for_legendary = true
  end
end

Quality.research_trigger_tech = function(force, technology_name)
  local technology = force.technologies[technology_name]
  if technology.researched == false then
    technology.researched = true
    force.print({"technology-researched", string.format("[technology=%s]", technology.name)}, {
      sound_path = "utility/research_completed"
    })
  end
end

Quality.player_visited_zone = function(player, zone)
  if Quality.use_console_capture_research_trigger_for_epic and zone.ruins and zone.ruins["asteroid-belt-ship"] then
    Quality.research_trigger_tech(player.force, "epic-quality")
  end

  if Quality.use_console_capture_research_trigger_for_legendary and zone.type == "anomaly" then
    Quality.research_trigger_tech(player.force, "legendary-quality")
  end
end

-- whilst it would be prettiest if the technology triggers only when you stand near the console it would require extra checks,
-- since this is all just unofficial i am taking the shortcut of having visited the zone the console is supposed to generate in.
Quality.on_player_visited_zone = function(event)
  local player = game.get_player(event.player_index) --[[@as LuaPlayer]]
  local zone = Zone.from_zone_index(event.zone_index)

  Quality.player_visited_zone(player, zone)
end

Quality.entity_types_with_module_slots_list = {
  "furnace",
  "assembling-machine",
  "lab",
  "mining-drill",
  "rocket-silo",
  "beacon",
}
Quality.entity_types_with_module_slots_map = core_util.list_to_map(Quality.entity_types_with_module_slots_list)

Quality.handle_new_entity_with_module_inventory = function(entity)
  local inventory = entity.get_module_inventory() -- if a supported entity has no module slots it still has a module inventory of size 0

  local quality_exploration = storage.quality_exploration

  local proxy_container = quality_exploration.surface.create_entity{
    name = "proxy-container",
    force = "neutral",
    position = {0, 0},
  }
  proxy_container.proxy_target_entity = entity
  proxy_container.proxy_target_inventory = inventory.index

  local wire_connector_a = proxy_container.get_wire_connector(defines.wire_connector_id.circuit_red, true)
  local wire_connector_b = quality_exploration.pole.get_wire_connector(defines.wire_connector_id.circuit_red, true)
  assert(wire_connector_a.connect_to(wire_connector_b))

  quality_exploration.structs[entity.unit_number] = {
    entity = entity,
    inventory = inventory,
    proxy_container = proxy_container,
  }
end

Quality.surface_name = "se-quality-exploration"
Quality.get_or_create_surface = function()
  local surface = game.get_surface(Quality.surface_name)

  if not surface then
    surface = game.create_surface(Quality.surface_name)
    surface.generate_with_lab_tiles = true
  end

  return surface
end

Quality.get_or_create_pole = function()
  local surface = Quality.get_or_create_surface()
  local pole = surface.find_entity("small-electric-pole", {0, -1})

  if not pole then
    pole = surface.create_entity{
      name = "small-electric-pole",
      force = "neutral",
      position = {0, -1},
    }
  end

  return pole
end

Quality.on_init = function(event)
  Quality.on_configuration_changed(event)
end

Quality.on_configuration_changed = function(event)
  storage.quality_exploration = storage.quality_exploration or {}
  local quality_exploration = storage.quality_exploration

  quality_exploration.structs = quality_exploration.structs or {}
  local structs = quality_exploration.structs

  if Quality.enabled then
    quality_exploration.surface = Quality.get_or_create_surface()
    quality_exploration.pole = Quality.get_or_create_pole()

    log("proxying module inventories (this can take a while)")
    local seen_unit_numbers = {}

    for _, surface in pairs(game.surfaces) do
      for _, entity in pairs(surface.find_entities_filtered{type = Quality.entity_types_with_module_slots_list}) do
        seen_unit_numbers[entity.unit_number] = true
        local struct = structs[entity.unit_number]
        if not struct then
          Quality.handle_new_entity_with_module_inventory(entity)
        else
          struct.inventory = struct.entity.get_module_inventory()
          struct.proxy_container.proxy_target_inventory = struct.inventory.index -- if the entity type supports modules it is always present
        end
      end
    end

    -- entity now invalid or migrated their type (away from a type that supports modules)
    for unit_number, struct in pairs(structs) do
      if not seen_unit_numbers[unit_number] then
        structs[unit_number] = nil
        struct.proxy_container.destroy()
      end
    end
  else
    -- having thousands of proxy containers wired up takes their toll, if the user disabled quality again then this cleans them up.
    if quality_exploration.surface then
      game.delete_surface(quality_exploration.surface)
      quality_exploration.surface = nil
      quality_exploration.pole = nil
      quality_exploration.structs = {}
    end
  end
end

Quality.loses_quality_when_placed_name_map = {}
for _, entity in ipairs(prototypes.mod_data[mod_prefix .. "loses-quality-when-placed"].data.entities) do
  Quality.loses_quality_when_placed_name_map[entity.name] = true
end

Quality.on_entity_created = function(event)
  local entity = event.entity or event.destination -- destination unused since it currently does not clone
  if not entity.valid then return end

  if Quality.entity_types_with_module_slots_map[entity.type] then -- branches off prior to the quality check
    Quality.handle_new_entity_with_module_inventory(entity)
  end

  if entity.quality.name == "normal" then return end

  local entity_name = entity.type == "entity-ghost" and entity.ghost_name or entity.name
  if not Quality.loses_quality_when_placed_name_map[entity_name] then return end

  local pretty_print = string.format("%s (%s)", entity.name, entity.type)

  local marked_for_upgrade = entity.order_upgrade{
    target = {name = entity_name, quality = "normal"},
    force = entity.force,
    player = event.player_index,
  }
  assert(marked_for_upgrade, pretty_print) -- find another way if the entity is not allowed to be marked for upgrade.

  if entity.valid and entity.type ~= "entity-ghost" then -- editor mode could have upgraded the old entity instantly.
    assert(entity.apply_upgrade(), pretty_print) -- upgrade and check if it succeeded, might fail if it got canceled?
  end
end

Quality.eject_quality_modules_option = settings.startup["se-modules-with-quality-in-module-slots"].value
Quality.eject_quality_modules = function()
  local quality_exploration = storage.quality_exploration

  -- odly this only seems to cause a lag spike the very first time, subsequent triggers on large worlds are very tiny
  for unit_number, struct in pairs(quality_exploration.structs) do
    if not struct.entity.valid then
      struct.proxy_container.destroy()
      quality_exploration.structs[unit_number] = nil
    else
      local inventory = struct.inventory
      for _, item in pairs(inventory.get_contents()) do
        if item.quality ~= "normal" then -- quality never nil here
          inventory.remove(item)
          if Quality.eject_quality_modules_option == "downgrade" then
            item.quality = "normal"
            inventory.insert(item)
          elseif Quality.eject_quality_modules_option == "eject" then
            struct.entity.surface.spill_item_stack{
              position = struct.entity.position,
              stack = item,
              enable_looted = true,
              force = struct.entity.force,
              allow_belts = false,
            }
          end
        end
      end
    end
  end
end

Quality.on_nth_tick_60 = function(event)
  local quality_exploration = storage.quality_exploration

  local circuit_network = quality_exploration.pole.get_circuit_network(defines.wire_connector_id.circuit_red)
  if not circuit_network then return end -- nothing with a module inventory exists yet, likely a fresh world.

  for _, signal in ipairs(circuit_network.signals or {}) do
    if signal.signal.quality then -- nil when normal
      Quality.eject_quality_modules()
      return
    end
  end
end

Event.addListener("on_init", Quality.on_init, true)
Event.addListener("on_configuration_changed", Quality.on_configuration_changed, true)

if Quality.enabled then

  if Quality.use_console_capture_research_trigger_for_epic or Quality.use_console_capture_research_trigger_for_legendary then
    Event.addListener("on_player_visited_zone", Quality.on_player_visited_zone, true)
    Event.addListener("on_configuration_changed", function()
      for _, player in pairs(game.players) do
        local playerdata = get_make_playerdata(player)
        for zone_index, _ in pairs(playerdata.visited_zone or {}) do
          local zone = Zone.from_zone_index(zone_index)
          Quality.player_visited_zone(player, zone)
        end
      end
    end, true)
  end

  Event.addListener(defines.events.on_robot_built_entity, Quality.on_entity_created)
  Event.addListener(defines.events.on_built_entity, Quality.on_entity_created)
  Event.addListener(defines.events.script_raised_built, Quality.on_entity_created)
  Event.addListener(defines.events.script_raised_revive, Quality.on_entity_created)

  -- note: on the ignore setting the structs & proxy containers do not clean themselves up
  if Quality.eject_quality_modules_option ~= "ignore" then
    Event.addListener("on_nth_tick_60", Quality.on_nth_tick_60)
  end
end

return Quality
