BioInd = require("__" .. script.mod_name .. "__.common")(script.mod_name)
local settings_changed = require("settings_changed")

if BioInd.get_startup_setting("BI_Enable_gvv_support") then
    BioInd.writeDebug("Activating support for gvv!")
    require("__gvv__/gvv")()
end


-- We can't just check if Alien Biomes is active, because we need to know if
-- the tiles we need from it exist in the game! To check this, we must call
-- game.get_tile_prototypes(), but this will crash in script.on_load(). So,
-- let's just declare the variable here and fill it later.
local AlienBiomes

local Event = require('__kry_stdlib__/stdlib/event/event').set_protected_mode(false)
require("util")
require("libs/util_ext")
require("control_tree")
require("control_arboretum")
require("remote") -- milestones



local function Create_dummy_force()
    -- Create dummy force for musk floor if electric grid overlay should NOT be shown in map view
    local f = game.create_force(BioInd.MuskForceName)
    -- Set new force as neutral to every other force
    for name, force in pairs(game.forces) do
        if name ~= BioInd.MuskForceName then
            f.set_friend(force, false)
            f.set_cease_fire(force, true)
        end
    end
    -- New force won't share chart data with any other force
    f.share_chart = false

    BioInd.writeDebug("Created force: %s", { game.forces[BioInd.MuskForceName].name })
end


-- Generate a look-up table with the names of our trees
local function get_bi_trees()
    local list = {}

    local trees = prototypes.get_entity_filtered({ { filter = "type", type = "tree" } })
    for tree_name, tree in pairs(trees) do
        if tree_name:match("^bio%-tree%-.+%-%d$") then
            BioInd.show("Found matching tree", tree_name)
            list[tree_name] = true
        end
    end

    return list
end


-- Generate a look-up table with the names of tiles that can't be changed by fertilizer
local tile_patterns = {
    ".*concrete.*",
    ".*stone%-path.*",
    "^bi%-solar%-mat$",
    "^bi%-wood%-floor$",
}
local function get_fixed_tiles()
    local list = {}

    for tile_name, tile in pairs(prototypes.tile) do
        for p, pattern in ipairs(tile_patterns) do
            if tile_name:match(pattern) then
                BioInd.show("Found matching tile", tile_name)
                -- If a tile is minable and fertilizer is used on it, we must deduct the mined
                -- tiles from the player/robot again!
                list[tile_name] = tile.mineable_properties.products or true
            end
        end
    end
    BioInd.show("Forbidden tiles", list)
    return list
end


-- Generate a look-up table with recipe ingredients, as other mods may have changed them
local function get_arboretum_recipes()
    local list = {}

    local recipes = prototypes.recipe
    local name

    for i = 1, 5 do
        name = "bi-arboretum-r" .. i
        list[name] = {}
        list[name].items = {}
        list[name].fluids = {}

        for i, ingredient in pairs(recipes[name].ingredients) do
            if ingredient.type == "item" then
                list[name].items[ingredient.name] = ingredient.amount
            else
                list[name].fluids[ingredient.name] = ingredient.amount
            end
        end
    end

    BioInd.show("Terraformer recipes", list)
    return list
end


--------------------------------------------------------------------
local function init()
    BioInd.writeDebug("Entered init!")
    if BioInd.is_debug then
        game.check_prototype_translations()
    end

    storage = storage or {}

    --------------------------------------------------------------------
    -- Settings
    --------------------------------------------------------------------
    -- Global table for storing the last state of certain mod settings
    storage.mod_settings = storage.mod_settings or {}
    if BioInd.get_startup_setting("BI_Easy_Bio_Gardens") then
        storage.mod_settings.garden_pole_connectors = BioInd.get_garden_pole_connectors()
    else
        storage.mod_settings.garden_pole_connectors = nil
    end

    -- Global table for storing the data of compound entities. They may change between
    -- saves (e.g. Bio gardens only need hidden poles when the "Easy gardens" setting
    -- is active).
    storage.compound_entities = BioInd.rebuild_compound_entity_list()


    --------------------------------------------------------------------
    -- Tree stuff!
    --------------------------------------------------------------------
    storage.bi = storage.bi or {}
    storage.bi.tree_growing = storage.bi.tree_growing or {}
    for i = 1, 4 do
        storage.bi["tree_growing_stage_" .. i] = storage.bi["tree_growing_stage_" .. i] or {}
    end

    -- List of tree prototypes created by BI
    storage.bi.trees = get_bi_trees()

    -- List of tile prototypes that can't be fertilized
    storage.bi.barren_tiles = get_fixed_tiles()

    --------------------------------------------------------------------
    -- Compound entities
    --------------------------------------------------------------------
    -- Check what global tables we need for compound entities
    local compound_entity_tables = {}
    for compound, compound_data in pairs(storage.compound_entities) do
        -- BioInd.compound_entities contains entries that point to the same table
        -- (e.g. straight/curved rails, or overlay entities), so we just overwrite
        -- them to remove duplicates
        compound_entity_tables[compound_data.tab] = compound
    end
    BioInd.show("Need to check these tables in global", compound_entity_tables)

    -- Prepare global tables storing data of compound entities
    local result
    for compound_tab, compound_name in pairs(compound_entity_tables) do
        -- Init table
        storage[compound_tab] = storage[compound_tab] or {}
        BioInd.writeDebug("Initialized storage[%s] (%s entities stored)",
            { compound_name, table_size(storage[compound_tab]) })
        -- If this compound entity requires additional tables in global, initialize
        -- them now!
        local related_tables = storage.compound_entities[compound_name].add_global_tables
        if related_tables then
            for t, tab in ipairs(related_tables or {}) do
                storage[tab] = storage[tab] or {}
                BioInd.writeDebug("Initialized storage[%s] (%s values)", { tab, table_size(storage[tab]) })
            end
        end
        -- If this compound entity requires additional values in global, initialize
        -- them now!
        local related_vars = storage.compound_entities[compound_name].add_global_values
        if related_vars then
            for var_name, value in pairs(related_vars or {}) do
                storage[var_name] = storage[var_name] or value
                BioInd.writeDebug("Set storage[%s] to %s", { var_name, storage[var_name] })
            end
        end

        -- Clean up global tables (We can skip this for empty tables!)
        if next(storage[compound_tab]) then
            -- Remove invalid entities
            result = BioInd.clean_global_compounds_table(compound_name)
            BioInd.writeDebug("Removed %s invalid entries from storage[%s]!",
                { result, compound_tab })
            -- Restore missing hidden entities
            result = BioInd.restore_missing_entities(compound_name)
            BioInd.writeDebug("Checked %s compound entities and restored %s missing hidden entries for storage[\"%s\"]!",
                { result.checked, result.restored, compound_tab })
        end
    end
    -- Search all surfaces for unregistered compound entities
    result = BioInd.find_unregistered_entities()
    BioInd.writeDebug("Registered %s forgotten entities!", { result })



    --------------------------------------------------------------------
    -- Musk floor
    --------------------------------------------------------------------
    storage.bi_musk_floor_table = storage.bi_musk_floor_table or {}
    storage.bi_musk_floor_table.tiles = storage.bi_musk_floor_table.tiles or {}
    storage.bi_musk_floor_table.forces = storage.bi_musk_floor_table.forces or {}



    --------------------------------------------------------------------
    -- Arboretum
    --------------------------------------------------------------------
    -- Global table for arboretum radars
    storage.bi_arboretum_radar_table = storage.bi_arboretum_radar_table or {}

    -- Global table of ingredients for terraformer recipes
    storage.bi_arboretum_recipe_table = get_arboretum_recipes()


    --------------------------------------------------------------------
    -- Compatibility with other mods
    --------------------------------------------------------------------
    storage.compatible = storage.compatible or {}
    storage.compatible.AlienBiomes = BioInd.AB_tiles()


    -- enable researched recipes
    for i, force in pairs(game.forces) do
        BioInd.writeDebug("Reset technology effects for force %s.", { force.name })
        force.reset_technology_effects()
    end

    -- Create dummy force for musk floor if electric grid overlay should NOT be shown in map view
    if BioInd.UseMuskForce and not game.forces[BioInd.MuskForceName] then
        Create_dummy_force()
    end
end


--------------------------------------------------------------------
local function On_Load()
    log("Entered On_Load!")
end


--------------------------------------------------------------------
local function On_Config_Change(ConfigurationChangedData)
    BioInd.writeDebug("On Configuration changed: %s", { ConfigurationChangedData })


    -- Re-initialize global tables etc.
    init()

    -- Has setting BI_Show_musk_floor_in_mapview changed?
    if ConfigurationChangedData.mod_startup_settings_changed then
        settings_changed.musk_floor()
        -- Has this been obsoleted by the new init process? Turn it off for now!
    end

    -- We've made a list of the tree prototypes that are currently available. Now we
    -- need to make sure that the lists of growing trees don't contain removed tree
    -- prototypes! (This fix is needed when "Alien Biomes" has been removed; it should
    -- work with all other mods that create trees as well.)
    local trees = storage.bi.trees
    local tab
    -- Growing stages
    for i = 1, 4 do
        tab = storage.bi["tree_growing_stage_" .. i]
        BioInd.writeDebug("Number of trees in growing stage %s: %s", { i, table_size(tab) })
        for t = #tab, 1, -1 do
            if not trees[tab[t].tree_name] then
                BioInd.writeDebug("Removing invalid tree %s (%s)", { t, tab[t].tree_name })
                table.remove(tab, t)
            end
        end

        -- Removing trees will create gaps in the table, but we need it as a continuous
        -- list. (Trees need to be sorted by growing time, and we always look at the
        -- tree with index 1 when checking if a tree has completed the growing stage, so
        -- lets sort the table after all invalid trees have been removed!)
        table.sort(tab, function(a, b) return a.time < b.time end)
        BioInd.show("Number of trees in final list", #tab)
    end
end


--------------------------------------------------------------------
--- Used for some compatibility with Angels Mods
Event.register(defines.events.on_player_joined_game, function(event)
    local player = game.players[event.player_index]
    local force = player.force
    local techs = force.technologies

    if BioInd.get_startup_setting("angels-use-angels-barreling") then
        techs['fluid-handling'].researched = false
        techs['bi-tech-fertilizer'].reload()
        local _t = techs['angels-fluid-barreling'].researched
        techs['angels-fluid-barreling'].researched = false
        techs['angels-fluid-barreling'].researched = _t
    end
end)


---------------------------------------------
Event.register(defines.events.on_trigger_created_entity, function(event)
    --- Used for Seed-bomb
    local ent = event.entity
    local surface = ent.surface
    local position = ent.position

    -- 'AlienBiomes' is a bool value -- we don't want to read it again if it's false,
    -- but only if it hasn't been set yet!
    AlienBiomes = AlienBiomes ~= nil and AlienBiomes or BioInd.AB_tiles()

    -- Basic
    if ent.name == "seedling" then
        BioInd.writeDebug("Seed Bomb Activated - Basic")
        seed_planted_trigger(event)

        -- Standard
    elseif ent.name == "seedling-2" then
        BioInd.writeDebug("Seed Bomb Activated - Standard")
        local currTile = surface.get_tile(position).name
        if storage.bi.barren_tiles[currTile] then
            BioInd.writeDebug("Can't fertilize %s!", { currTile })
        else
            BioInd.writeDebug("Using fertilizer!")
            local terrain_name_s = AlienBiomes and "vegetation-green-grass-3" or "grass-3"
            surface.set_tiles { { name = terrain_name_s, position = position } }
        end
        seed_planted_trigger(event)

        -- Advanced
    elseif ent.name == "seedling-3" then
        BioInd.writeDebug("Seed Bomb Activated - Advanced")
        local currTile = surface.get_tile(position).name
        if storage.bi.barren_tiles[currTile] then
            BioInd.writeDebug("Can't fertilize %s!", { currTile })
        else
            BioInd.writeDebug("Using fertilizer!")
            local terrain_name_a = AlienBiomes and "vegetation-green-grass-1" or "grass-1"
            surface.set_tiles { { name = terrain_name_a, position = position } }
        end
        seed_planted_trigger(event)
    end
end)

--------------------------------------------------------------------
local function On_Built(event)
    BioInd.writeDebug("Entered function On_Built with these data: " .. serpent.block(event))
    local entity = event.created_entity or event.entity
    if not (entity and entity.valid) then
        BioInd.arg_err(entity or "nil", "entity")
    end

    local surface = BioInd.is_surface(entity.surface) or
        BioInd.arg_err(entity.surface or "nil", "surface")
    local position = BioInd.normalize_position(entity.position) or
        BioInd.arg_err(entity.position or "nil", "position")
    local force = entity.force


    -- We can ignore ghosts -- if ghosts are revived, there will be
    -- another event that triggers where actual entities are placed!
    if entity.name == "entity-ghost" then
        BioInd.writeDebug("Built ghost of %s -- return!", { entity.ghost_name })
        return
    end

    BioInd.show("Built entity", BioInd.print_name_id(entity))

    local base_entry = storage.compound_entities[entity.name]
    local base = base_entry and entity

    -- We've found a compound entity!
    if base then
        -- Make sure we work with a copy of the original table! We don't want to
        -- remove anything from it for real.
        local hidden_entities = util.table.deepcopy(base_entry.hidden)

        BioInd.writeDebug("%s (%s) is a compound entity. Need to create %s",
            { base.name, base.unit_number, hidden_entities })
        BioInd.show("hidden_entities", hidden_entities)
        local new_base
        local new_base_name = base_entry.new_base_name
        -- If the base entity is only an overlay, we'll replace it with the real base
        -- entity and raise an event. The hidden entities will be created in the second
        -- pass (triggered by building the final entity).
        BioInd.show("base_entry.new_base_name", base_entry.new_base_name)
        BioInd.show("base_entry.new_base_name == base.name", base_entry.new_base_name == base.name)
        BioInd.show("base_entry.optional", base_entry.optional)
        if new_base_name and new_base_name ~= base.name then
            new_base = surface.create_entity({
                name = new_base_name,
                position = base.position,
                direction = base.direction,
                force = base.force,
                raise_built = true
            })
            new_base.health = base.health
            BioInd.show("Created final base entity", BioInd.print_name_id(new_base))

            base.destroy({ raise_destroy = true })
            base = new_base
            BioInd.writeDebug("Destroyed old base entity!")

            -- Second pass: We've placed the final base entity now, so we can create the
            -- the hidden entities!
        else
            BioInd.writeDebug("Second pass -- creating hidden entities!")
            BioInd.show("base_entry", base_entry)

            BioInd.writeDebug("storage[%s]: %s", { base_entry.tab, storage[base_entry.tab] })
            BioInd.show("base.name", base.name)
            BioInd.show("base.unit_number", base.unit_number)
            BioInd.show("hidden_entities", hidden_entities)

            -- We must call create_entities even if there are no hidden entities (e.g. if
            -- the "Easy Gardens" setting is disabled and no hidden poles are required)
            -- because the compound entity gets registered there!
            BioInd.create_entities(storage[base_entry.tab], base, hidden_entities)
            BioInd.writeDebug("Stored %s in table: %s",
                { BioInd.print_name_id(base), storage[base_entry.tab][base.unit_number] })
        end

        -- The built entity isn't one of our compound entities.
    else
        BioInd.writeDebug("%s is not a compound entity!", { BioInd.print_name_id(entity) })

        -- If one of our hidden entities has been built, we'll have raised this event
        -- ourselves and have passed on the base entity.
        base = event.base_entity

        local entities = BioInd.compound_entities
        BioInd.show("Base entity", BioInd.print_name_id(base))

        -- The hidden entities are listed with a common handle ("pole", "panel" etc.). We
        -- can get it from the reverse-lookup list via the entity type!
        local h_key = BioInd.HE_map_reverse[entity.type]
        BioInd.show("h_key", h_key or "nil")

        -- Arboretum radar -- we need to add it to the table!
        if entity.type == "radar" and
            entity.name == entities["bi-arboretum-area"].hidden[h_key].name and base then
            storage.bi_arboretum_radar_table[entity.unit_number] = base.unit_number
            entity.backer_name = ""
            BioInd.writeDebug("Added %s to storage.bi_arboretum_radar_table", { BioInd.print_name_id(entity) })

            -- Electric poles -- we need to take care that they don't hook up to hidden poles!
        elseif entity.type == "electric-pole" then
            local pole = entity
            -- Make sure hidden poles of the Bio gardens are connected correctly!
            local garden_names = { "bi-bio-garden", "bi-bio-garden-larger", "bi-bio-garden-huge" }
            for _, gname in ipairs(garden_names) do
                if entities[gname] and pole.name == entities[gname].hidden[h_key].name and base then
                    BioInd.writeDebug("Bio garden (" .. gname .. ")!")
                    BioInd.connect_garden_pole(base, pole)
                    BioInd.writeDebug("Connected %s (%s)", { pole.name, pole.unit_number or "nil" })
                    break
                end
            end

            -- A seedling has been planted
        elseif entity.name == "seedling" then
            seed_planted(event)
            BioInd.writeDebug("Planted seedling!")

            -- Something else has been built
        else
            BioInd.writeDebug("Nothing to do for %s!", { entity.name })
        end
    end
    BioInd.writeDebug("End of function On_Built")
end


local function remove_plants(entity_position, tabl)
    BioInd.writeDebug("Entered function remove_plants(%s, %s)", { entity_position or "nil", tabl or "nil" })
    local e = BioInd.normalize_position(entity_position)
    if not e then
        BioInd.arg_err(entity_position or "nil", "position")
    end
    BioInd.check_args(tabl, "table")

    local pos

    for k, v in pairs(tabl or {}) do
        pos = BioInd.normalize_position(v.position)
        if pos and pos.x == e.x and pos.y == e.y then
            BioInd.writeDebug("Removing entry %s from table: %s", { k, v })
            table.remove(tabl, k)
            break
        end
    end
end


--------------------------------------------------------------------
local function On_Pre_Remove(event)
    BioInd.writeDebug("Entered function On_Pre_Remove(%s)", { event })
    local entity = event.entity

    if not (entity and entity.valid) then
        BioInd.writeDebug("No valid entity -- nothing to do!")
        return
    end

    local compound_entity = storage.compound_entities[entity.name]
    local base_entry = compound_entity and storage[compound_entity.tab][entity.unit_number]
    BioInd.show("entity.name", entity.name)
    BioInd.show("entity.unit_number", entity.unit_number)

    BioInd.show("compound_entity", compound_entity)
    BioInd.show("base_entry", base_entry)
    BioInd.show("compound_entity.tab", compound_entity and compound_entity.tab or "nil")
    BioInd.writeDebug("storage[%s]: %s",
        { compound_entity and compound_entity.tab or "nil", compound_entity and storage[compound_entity.tab] or "nil" })

    -- Found a compound entity from our list!
    if base_entry then
        BioInd.writeDebug("Found compound entity %s",
            { base_entry.base and BioInd.print_name_id(base_entry.base) })

        -- Arboretum: Need to separately remove the entry from the radar table
        if entity.name == "bi-arboretum" and base_entry.radar and base_entry.radar.valid then
            storage.bi_arboretum_radar_table[base_entry.radar.unit_number] = nil
            BioInd.show("Removed arboretum radar! Table", storage.bi_arboretum_radar_table)
        end

        -- Power rails: Connections must be explicitely removed, otherwise the poles
        -- from the remaining rails will automatically connect and bridge the gap in
        -- the power supply!
        if entity.name:match("bi%-%a+%-rail%-power") and base_entry.pole and base_entry.pole.valid then
            BioInd.writeDebug("Before")
            BioInd.writeDebug("Disconnecting %s!", { BioInd.print_name_id(base_entry.pole) })
            base_entry.pole.disconnect_neighbour()
            BioInd.writeDebug("After")
        end

        -- Default: Remove all hidden entities!
        for hidden, h_name in pairs(compound_entity.hidden or {}) do
            BioInd.show("hidden", hidden)

            BioInd.writeDebug("Removing hidden entity %s", { BioInd.print_name_id(base_entry[hidden]) })
            BioInd.remove_entity(base_entry[hidden])
            base_entry[hidden] = nil
        end
        storage[compound_entity.tab][entity.unit_number] = nil

        -- Rail-to-power: Connections must be explicitely removed, otherwise the poles
        -- from the different rail tracks hooked up to this connector will automatically
        -- keep the separate power networks connected!
    elseif entity.name == "bi-power-to-rail-pole" then
        BioInd.writeDebug("Rail-to-power connector has been removed")
        entity.disconnect_neighbour()
        BioInd.writeDebug("Removed copper wires from %s (%g)", { entity.name, entity.unit_number })

        -- Removed seedling
    elseif entity.name == "seedling" then
        BioInd.writeDebug("Seedling has been removed")
        remove_plants(entity.position, storage.bi.tree_growing)

        -- Removed tree
    elseif entity.type == "tree" and storage.bi.trees[entity.name] then
        BioInd.show("Removed tree", entity.name)

        local tree_stage = entity.name:match('^.+%-(%d)$')
        BioInd.writeDebug("Removed tree %s (grow stage: %s)", { entity.name, tree_stage or nil })
        if tree_stage then
            remove_plants(entity.position, storage.bi["tree_growing_stage_" .. tree_stage])
        else
            error(string.format("Tree %s does not have a valid tree_stage: %s", entity.name, tree_stage or "nil"))
        end

        -- Removed something else
    else
        BioInd.writeDebug("%s has been removed -- nothing to do!", { entity.name })
    end
end


--------------------------------------------------------------------
local function On_Damage(event)
    local f_name = "On_Damage"
    BioInd.writeDebug("Entered function %s(%s)", { f_name, event })
    local entity = event.entity
    local final_health = event.final_health

    local arb = "bi-arboretum"
    local associated

    -- Base was damaged: Find the radar associated with it!
    if entity.name == arb then
        associated = storage.bi_arboretum_table[entity.unit_number].radar
        -- Radar was damaged: Find the base entity!
    elseif entity.name == storage.compound_entities[arb].hidden.radar.name then
        local base_id = storage.bi_arboretum_radar_table[entity.unit_number]
        associated = storage.bi_arboretum_table[base_id].base
    end

    if associated and associated.valid then
        associated.health = final_health
        BioInd.writeDebug("%s was damaged (%s). Reducing health of %s to %s!", {
            BioInd.print_name_id(entity),
            event.final_damage_amount,
            entity.name == arb and "associated radar" or "base",
            associated.health
        })
    end
end

--------------------------------------------------------------------
local function On_Death(event)
    local f_name = "On_Death"
    BioInd.writeDebug("Entered function %s(%s)", { f_name, event })

    local entity = event.entity
    if not entity then
        error("Something went wrong -- no entity data!")
    end

    if
    -- Table checks
        storage.compound_entities[entity.name] or
        storage.bi.trees[entity.name] or
        -- Entity checks
        entity.name == storage.compound_entities["bi-arboretum"].hidden.radar.name or
        entity.name == "bi-power-to-rail-pole" or
        entity.name == "seedling" then
        BioInd.writeDebug("Divert to On_Pre_Remove!")
        On_Pre_Remove(event)
    else
        BioInd.writeDebug("Nothing to do!")
    end
end


--------------------------------------------------------------------
-- Radar stuff
--------------------------------------------------------------------

-- Robust sector scanned handler for Arboretum radar
local function On_Sector_Scanned(event)
    -- defensive checks
    BioInd.writeDebug("On_Sector_Scanned fired")
    --game.print("On_Sector_Scanned fired")
    if not (event and event.radar) then return end
    local radar = event.radar
    if not (radar.valid and radar.unit_number) then return end

    -- Make sure compound-entity data is available before accessing it
    local arb_proto = BioInd.compound_entities and BioInd.compound_entities["bi-arboretum"]
    if not (arb_proto and arb_proto.hidden and arb_proto.hidden.radar and arb_proto.hidden.radar.name) then
        -- not ready yet (init not finished) — bail out safely
        return
    end

    -- Only handle scans from our arboretum radar type
    if radar.name ~= arb_proto.hidden.radar.name then return end

    -- Look up the base arboretum unit_number (stored when the hidden radar was created)
    local base_unit_number = storage.bi_arboretum_radar_table and storage.bi_arboretum_radar_table[radar.unit_number]
    if not base_unit_number then
        -- no mapping found -> nothing to do
        return
    end

    local arb_table = storage.bi_arboretum_table and storage.bi_arboretum_table[base_unit_number]
    if not arb_table then return end

    -- All good: call the arboretum recipe handler
    Get_Arboretum_Recipe(arb_table, event)
end


--------------------------------------------------------------------
-- Solar Mat stuff
--------------------------------------------------------------------

--------------------------------------------------------------------
-- Solar mat was removed
local function solar_mat_removed(event)
    BioInd.writeDebug("Entered solar_mat_removed (\"%s\")", { event })

    local surface = game.surfaces[event.surface_index]
    local tiles = event.tiles

    local pos, x, y
    -- tiles contains an array of the old tiles and their position
    for t, tile in pairs(tiles) do
        if tile.old_tile and tile.old_tile.name == "bi-solar-mat" then
            pos = BioInd.normalize_position(tile.position)
            x, y = pos.x, pos.y

            BioInd.writeDebug("Looking for hidden entities to remove")
            for _, o in pairs(surface.find_entities_filtered {
                name = { 'bi-musk-mat-hidden-pole', 'bi-musk-mat-hidden-panel' },
                position = { x + 0.5, y + 0.5 }
            } or {}) do
                BioInd.show("Removing", o.name)
                o.destroy()
            end

            -- Remove tile from global tables
            local force_name = storage.bi_musk_floor_table.tiles and
                storage.bi_musk_floor_table.tiles[x] and
                storage.bi_musk_floor_table.tiles[x][y]
            if force_name then
                BioInd.writeDebug("Removing Musk floor tile from tables!")
                storage.bi_musk_floor_table.tiles[x][y] = nil
                if not next(storage.bi_musk_floor_table.tiles[x]) then
                    storage.bi_musk_floor_table.tiles[x] = nil
                end

                if storage.bi_musk_floor_table.forces[force_name] and
                    storage.bi_musk_floor_table.forces[force_name][x] then
                    storage.bi_musk_floor_table.forces[force_name][x][y] = nil
                    if not next(storage.bi_musk_floor_table.forces[force_name][x]) then
                        storage.bi_musk_floor_table.forces[force_name][x] = nil
                    end
                end
            end
        end
    end

    BioInd.writeDebug("bi-solar-mat: removed %g tiles", { table_size(tiles) })
end


--------------------------------------------------------------------
-- A solar mat must be placed
local function place_musk_floor(force, position, surface)
    BioInd.check_args(force, "string")
    position = BioInd.normalize_position(position) or BioInd.arg_err(position, "position")
    surface = BioInd.is_surface(surface) or BioInd.arg_err(surface, "surface")

    local x, y = position.x, position.y
    local created
    for n, name in ipairs({ "bi-musk-mat-hidden-pole", "bi-musk-mat-hidden-panel" }) do
        created = surface.create_entity({ name = name, position = { x + 0.5, y + 0.5 }, force = force })
        created.minable = false
        created.destructible = false
        BioInd.writeDebug("Created %s: %s", { name, created.unit_number })
    end

    -- Add to global tables!
    storage.bi_musk_floor_table.tiles[x] = storage.bi_musk_floor_table.tiles[x] or {}
    storage.bi_musk_floor_table.tiles[x][y] = force

    storage.bi_musk_floor_table.forces[force] = storage.bi_musk_floor_table.forces[force] or {}
    storage.bi_musk_floor_table.forces[force][x] = storage.bi_musk_floor_table.forces[force][x] or {}
    storage.bi_musk_floor_table.forces[force][x][y] = true
end

--------------------------------------------------------------------
-- Solar mat was built
local function solar_mat_built(event)
    BioInd.show("Entered function \"solar_mat_built\"", event)
    -- Called from player, bot and script-raised events, so event may
    -- contain "robot" or "player_index"

    local tile = event.tile
    local surface = game.surfaces[event.surface_index]
    local player = event.player_index and game.players[event.player_index]
    local robot = event.robot
    local force = (BioInd.UseMuskForce and BioInd.MuskForceName) or
        (event.player_index and game.players[event.player_index].force.name) or
        (event.robot and event.robot.force.name) or
        event.force.name
    BioInd.show("Force.name", force)

    -- Item that was used to place the tile
    local item = event.item
    local old_tiles = event.tiles


    local position --, x, y


    -- Musk floor has been built -- create hidden entities!
    if tile.name == "bi-solar-mat" then
        BioInd.writeDebug("Solar Mat has been built -- must create hidden entities!")
        BioInd.show("Tile data", tile)

        for index, t in pairs(old_tiles or { tile }) do
            BioInd.show("Read old_tile inside loop", t)
            -- event.tiles will also contain landscape tiles like "grass-1", and it will always
            -- contain at least one tile
            position = BioInd.normalize_position(t.position)
            -- If we got here by a call from script_raised_built, force may be stored
            -- with the tile
            force = force or t.force
            BioInd.show("Got force from tile data", t.force or "false")
            BioInd.writeDebug("Building solar mat for force %s at position %s",
                { tostring(type(force) == "table" and force.name or force), position })

            place_musk_floor(force, position, surface)
        end

        -- Fertilizer/Advanced fertilizer has been used. Check if the tile was valid
        -- (no Musk floor, no wooden floor, no concrete etc.)
    elseif item and (item.name == "fertilizer" or item.name == "bi-adv-fertilizer") then
        local restore_tiles = {}
        local products, remove_this

        for index, t in pairs(old_tiles or { tile }) do
            BioInd.show("index", index)
            BioInd.show("t.old_tile.name", t.old_tile.name)

            -- We want to restore removed tiles if nothing is supposed to grow on them!
            if storage.bi.barren_tiles[t.old_tile.name] then
                BioInd.writeDebug("%s was used on forbidden ground (%s)!", { item.name, t.old_tile.name })
                restore_tiles[#restore_tiles + 1] = { name = t.old_tile.name, position = t.position }

                -- Is that tile minable?
                products = storage.bi.barren_tiles[t.old_tile.name]
                if type(products) == "table" then
                    for p, product in ipairs(products) do
                        remove_this = { name = product.name, count = product.amount }
                        if player then
                            BioInd.writeDebug("Removing %s (%s) from player %s",
                                { product.name, product.amount, player.name })
                            player.remove_item(remove_this)
                        elseif robot then
                            BioInd.writeDebug("Removing %s (%s) from robot %s",
                                { product.name, product.amount, robot.unit_number })
                            robot.remove_item(remove_this)
                        end
                    end
                end
            end
        end
        BioInd.show("restore_tiles", restore_tiles)
        if restore_tiles then
            surface.set_tiles(
                restore_tiles,
                true, -- correct_tiles
                true, -- remove_colliding_entities
                true, -- remove_colliding_decoratives
                true -- raise_event
            )
        end

        -- Some other tile has been built -- check if it replaced musk floor!
    else
        local test
        local removed_tiles = {}
        for index, t in pairs(old_tiles or { tile }) do
            position = BioInd.normalize_position(t.position)
            test = storage.bi_musk_floor_table and
                storage.bi_musk_floor_table.tiles and
                storage.bi_musk_floor_table.tiles[position.x] and
                storage.bi_musk_floor_table.tiles[position.x][position.y]
            if test then
                removed_tiles[#removed_tiles + 1] = {
                    old_tile = { name = "bi-solar-mat" },
                    position = position
                }
            end
        end
        if next(removed_tiles) then
            solar_mat_removed({ surface_index = event.surface_index, tiles = removed_tiles })
        else
            BioInd.writeDebug("%s has been built -- nothing to do!", { tile.name })
        end
    end
end


--------------------------------------------------------------------
-- A tille has been changed
local function Tile_Changed(event)
    local f_name = "Tile_Changed"
    BioInd.writeDebug("Entered function %s(%s)", { f_name, event })

    -- The event gives us only a list of the new tiles that have been placed.
    -- So let's check if any Musk floor has been built!
    local new_musk_floor_tiles = {}
    local old_musk_floor_tiles = {}
    local remove_musk_floor_tiles = {}
    local pos, old_tile, force

    local tile_force

    for t, tile in ipairs(event.tiles) do
        BioInd.show("t", t)
        pos = BioInd.normalize_position(tile.position)
        tile_force = storage.bi_musk_floor_table.tiles[pos.x] and
            storage.bi_musk_floor_table.tiles[pos.x][pos.y]
        BioInd.show("Placed tile", tile.name)

        -- Musk floor was placed
        if tile.name == "bi-solar-mat" then
            BioInd.writeDebug("Musk floor tile was placed!")
            new_musk_floor_tiles[#new_musk_floor_tiles + 1] = {
                old_tile = { name = tile.name },
                position = pos,
                force = tile_force or
                    BioInd.UseMuskForce and BioInd.MuskForceName or
                    "neutral"
            }
            -- Other tile was placed -- by one of our fertilizers?
        elseif tile.name:match("^vegetation%-green%-grass%-[13]$") or
            tile.name:match("^green%-grass%-[13]$") then
            BioInd.writeDebug("Fertilizer was used!")

            -- Fertilizer was used on a Musk floor tile -- restore the tile!
            BioInd.show("Musk floor tile in position", tile_force)
            if tile_force then
                old_musk_floor_tiles[#old_musk_floor_tiles + 1] = {
                    old_tile = { name = "bi-solar-mat" },
                    position = pos,
                    force = tile_force
                }
            end
            -- Other tile was placed on a Musk floor tile -- remove Musk floor from lists!
        elseif tile_force then
            remove_musk_floor_tiles[#remove_musk_floor_tiles + 1] = {
                old_tile = { name = "bi-solar-mat" },
                position = pos,
            }
        end
    end
    BioInd.show("new_musk_floor_tiles", new_musk_floor_tiles)
    BioInd.show("old_musk_floor_tiles", old_musk_floor_tiles)
    BioInd.show("remove_musk_floor_tiles", remove_musk_floor_tiles)

    if next(new_musk_floor_tiles) then
        solar_mat_built({
            surface_index = event.surface_index,
            tile = { name = "bi-solar-mat" },
            force = BioInd.MuskForceName,
            tiles = new_musk_floor_tiles
        })
    end
    if next(old_musk_floor_tiles) then
        solar_mat_built({
            surface_index = event.surface_index,
            tile = { name = "bi-solar-mat" },
            tiles = old_musk_floor_tiles
        })
    end
    if next(remove_musk_floor_tiles) then
        solar_mat_removed({ surface_index = event.surface_index, tiles = remove_musk_floor_tiles })
    end
    BioInd.show("End of function", f_name)
end


--------------------------------------------------------------------


Event.register(Event.core_events.configuration_changed, On_Config_Change)
Event.register(Event.core_events.init, init)
Event.register(Event.core_events.load, On_Load)


Event.build_events = {
    defines.events.on_built_entity,
    defines.events.on_robot_built_entity,
    defines.events.script_raised_built,
    defines.events.script_raised_revive
}
Event.pre_remove_events = {
    defines.events.on_pre_player_mined_item,
    defines.events.on_robot_pre_mined,
    defines.events.on_player_mined_entity,
    defines.events.on_robot_mined_entity,
}
Event.death_events = {
    defines.events.on_entity_died,
    defines.events.script_raised_destroy
}
Event.tile_build_events = {
    defines.events.on_player_built_tile,
    defines.events.on_robot_built_tile
}
Event.tile_remove_events = {
    defines.events.on_player_mined_tile,
    defines.events.on_robot_mined_tile
}
Event.tile_script_action = {
    defines.events.script_raised_set_tiles
}

Event.register(Event.build_events, On_Built)
Event.register(Event.pre_remove_events, On_Pre_Remove)
Event.register(Event.death_events, On_Death)
Event.register(Event.tile_build_events, solar_mat_built)
Event.register(Event.tile_remove_events, solar_mat_removed)


Event.register(defines.events.on_entity_damaged, On_Damage, function(event)
    -- A function is needed for event filtering with stdlib!
    local entity = event.entity

    -- Ignore damage without effect (invulnerable/resistant entities)
    if event.final_damage_amount ~= 0 and
        -- Terraformer/Terraformer radar was damaged
        (storage.bi_arboretum_table[entity.unit_number] or
            storage.bi_arboretum_radar_table[entity.unit_number]) then
        return true
    end
end)

-- Radar scan
Event.register(defines.events.on_sector_scanned, On_Sector_Scanned, function(event)
    -- A function is needed for event filtering with stdlib!
    if event.radar.name == BioInd.compound_entities["bi-arboretum"].hidden.radar.name then
        return true
    end
end)

-- Tile changed
Event.register(Event.tile_script_action, Tile_Changed)


------------------------------------------------------------------------------------
--                    FIND LOCAL VARIABLES THAT ARE USED GLOBALLY                 --
--                              (Thanks to eradicator!)                           --
------------------------------------------------------------------------------------
setmetatable(_ENV, {
    __newindex = function(self, key, value) --locked_global_write
        error('\n\n[ER Global Lock] Forbidden global *write*:\n'
            .. serpent.line { key = key or '<nil>', value = value or '<nil>' } .. '\n')
    end,
    __index = function(self, key) --locked_global_read
        if not (key == "game" or key == "mods" or key == "storage") then
            error('\n\n[ER Global Lock] Forbidden global *read*:\n'
                .. serpent.line { key = key or '<nil>' } .. '\n')
        end
    end
})
