--luacheck: ignore
--[[
It's a Minesweeper thingy - MewMew

Cell Values:
	-- 1 to 8 = adjacent mines
	-- 9 = empty cell with grid
	-- 10 = mine
	-- 11 = marked mine

]] --

require 'modules.satellite_score'

local Functions = require 'maps.minesweeper.functions'
local Map_score = require 'comfy_panel.map_score'
local Map = require 'modules.map_info'
local Global = require 'utils.global'

local minesweeper = {}
Global.register(
    minesweeper,
    function(tbl)
        minesweeper = tbl
    end
)

local number_colors = {
    [1] = {0, 0, 210},
    [2] = {0, 100, 0},
    [3] = {180, 0, 0},
    [4] = {0, 0, 120},
    [5] = {120, 0, 0},
    [6] = {0, 110, 110},
    [7] = {0, 0, 0},
    [8] = {125, 125, 125},
    [11] = {185, 0, 255}
}

local rendering_tile_values = {
    ['nuclear-ground'] = {offset = {0.6, -0.2}, zoom = 3, font = 'scenario-message-dialog'},
    --['stone-path'] = {offset = {0.54, -0.27}, zoom = 3, font = 'default-large'},
    ['stone-path'] = {offset = {0.52, -0.28}, zoom = 3, font = 'default-large-bold'},
    ['concrete'] = {offset = {0.52, -0.28}, zoom = 3, font = 'default-large-bold'},
    ['hazard-concrete-left'] = {offset = {0.52, -0.28}, zoom = 3, font = 'default-large-bold'},
    ['hazard-concrete-right'] = {offset = {0.52, -0.28}, zoom = 3, font = 'default-large-bold'},
    ['refined-concrete'] = {offset = {0.54, -0.26}, zoom = 3, font = 'default-game'},
    ['refined-hazard-concrete-left'] = {offset = {0.54, -0.26}, zoom = 3, font = 'default-game'},
    ['refined-hazard-concrete-right'] = {offset = {0.54, -0.26}, zoom = 3, font = 'default-game'}
}

local chunk_divide_vectors = {}
for x = 0, 30, 2 do
    for y = 0, 30, 2 do
        table.insert(chunk_divide_vectors, {x, y})
    end
end
local size_of_chunk_divide_vectors = #chunk_divide_vectors

local chunk_vectors = {}
for x = -32, 32, 32 do
    for y = -32, 32, 32 do
        table.insert(chunk_vectors, {x, y})
    end
end

local cell_update_vectors = {}
for x = -2, 2, 2 do
    for y = -2, 2, 2 do
        table.insert(cell_update_vectors, {x, y})
    end
end

local cell_adjacent_vectors = {}
for x = -2, 2, 2 do
    for y = -2, 2, 2 do
        if x == 0 and y == 0 then
        else
            table.insert(cell_adjacent_vectors, {x, y})
        end
    end
end

local solving_vector_tables = {}
local i = 1
for r = 3, 10, 1 do
    solving_vector_tables[i] = {}
    for x = r * -2, r * 2, 2 do
        for y = r * -2, r * 2, 2 do
            table.insert(solving_vector_tables[i], {x, y})
        end
    end
    i = i + 1
end
local size_of_solving_vector_tables = #solving_vector_tables

local function update_rendering(cell, position)
    local surface = game.surfaces[1]
    local tile = surface.get_tile(position)
    local tile_values = rendering_tile_values[tile.name]
    if not tile_values then
        tile_values = {offset = {0.6, -0.2}, zoom = 3, font = 'scenario-message-dialog'}
    end

    if cell[2] then
        rendering.destroy(cell[2])
    end
    if cell[3] then
        rendering.destroy(cell[3])
    end

    local cell_value = cell[1]

    local color
    if number_colors[cell_value] then
        color = number_colors[cell_value]
    else
        color = {125, 125, 125}
    end

    local p = {position.x + tile_values.offset[1], position.y + tile_values.offset[2]}
    local text = cell_value
    if cell_value == 10 or cell_value == 9 then
        text = ' '
    end
    if cell_value == 11 then
        text = 'X'
    end

    cell[2] =
        rendering.draw_text {
        text = text,
        surface = surface,
        target = p,
        color = color,
        scale = tile_values.zoom,
        font = tile_values.font,
        draw_on_ground = true,
        scale_with_zoom = false,
        only_in_alt_mode = false
    }

    if not tile.hidden_tile then
        return
    end
    if tile.hidden_tile ~= 'nuclear-ground' then
        return
    end

    cell[3] =
        rendering.draw_rectangle {
        width = 2,
        filled = false,
        surface = surface,
        left_top = position,
        right_bottom = {position.x + 2, position.y + 2},
        color = {0, 0, 0},
        draw_on_ground = true,
        only_in_alt_mode = false
    }
end

local function get_adjacent_mine_count(position)
    local count = 0
    for _, vector in pairs(cell_adjacent_vectors) do
        local p = {x = position.x + vector[1], y = position.y + vector[2]}
        local key = Functions.position_to_string(p)
        local cell = minesweeper.cells[key]
        if cell and cell[1] >= 10 then
            count = count + 1
        end
    end
    return count
end

local function kill_cell(position)
    local key = Functions.position_to_string(position)
    local cell = minesweeper.cells[key]
    if not cell then
        return
    end
    if cell[2] then
        rendering.destroy(cell[2])
    end
    if cell[3] then
        rendering.destroy(cell[3])
    end
    minesweeper.cells[key] = nil
end

local function visit_cell(position)
    local score_change = 0

    if not Functions.is_minefield_tile(position, true) then
        return score_change
    end

    local key = Functions.position_to_string(position)
    local cell = minesweeper.cells[key]
    local cell_value_before_visit = false

    if cell then
        if cell[1] == 10 then
            Functions.kaboom(position)
            score_change = -8
            cell[1] = -1
            for _, vector in pairs(cell_update_vectors) do
                local p = {x = position.x + vector[1], y = position.y + vector[2]}
                local key = Functions.position_to_string(p)
                if minesweeper.cells[key] and minesweeper.cells[key][1] < 10 then
                    table.insert(minesweeper.visit_queue, {x = p.x, y = p.y})
                end
            end
            return score_change
        end

        if cell[1] == 11 then
            update_rendering(cell, position)
            return score_change
        end

        cell_value_before_visit = cell[1]
    end

    if not cell then
        minesweeper.cells[key] = {}
    end
    local cell = minesweeper.cells[key]

    cell[1] = get_adjacent_mine_count(position)

    if cell[1] == 0 then
        for _, vector in pairs(cell_adjacent_vectors) do
            local adjacent_position = {x = position.x + vector[1], y = position.y + vector[2]}
            if Functions.is_minefield_tile(adjacent_position, true) then
                local adjacent_key = Functions.position_to_string(adjacent_position)
                if not minesweeper.cells[adjacent_key] then
                    minesweeper.cells[adjacent_key] = {}
                end
                local adjacent_cell = minesweeper.cells[adjacent_key]
                local mine_count = get_adjacent_mine_count(adjacent_position)
                adjacent_cell[1] = mine_count
                update_rendering(adjacent_cell, adjacent_position)
                if mine_count == 0 then
                    table.insert(minesweeper.visit_queue, {x = adjacent_position.x, y = adjacent_position.y})
                end
            end
        end
        Functions.uncover_terrain(position)
        kill_cell(position)
        return score_change
    end

    if cell_value_before_visit and cell_value_before_visit ~= cell[1] then
        for _, vector in pairs(cell_adjacent_vectors) do
            local adjacent_position = {x = position.x + vector[1], y = position.y + vector[2]}
            local adjacent_key = Functions.position_to_string(adjacent_position)
            local adjacent_cell = minesweeper.cells[adjacent_key]
            if adjacent_cell and adjacent_cell[1] < 9 then
                table.insert(minesweeper.visit_queue, {x = adjacent_position.x, y = adjacent_position.y})
            end
        end
    end

    update_rendering(cell, position)

    return score_change
end

local function get_solving_vectors(position)
    local distance_to_center = math.sqrt(position.x ^ 2 + position.y ^ 2)
    local key = math.floor(distance_to_center * 0.005) + 1
    if key > size_of_solving_vector_tables then
        key = size_of_solving_vector_tables
    end
    local solving_vectors = solving_vector_tables[key]
    return solving_vectors
end

local function are_mines_marked_around_target(position)
    local marked_positions = {}
    for _, vector in pairs(get_solving_vectors(position)) do
        local p = {x = position.x + vector[1], y = position.y + vector[2]}
        local key = Functions.position_to_string(p)
        local cell = minesweeper.cells[key]
        if cell then
            if cell[1] == 10 then
                return
            end
            if cell[1] == 11 then
                table.insert(marked_positions, p)
            end
        end
    end
    return marked_positions
end

local function solve_attempt(position)
    local solved = false
    for _, vector in pairs(get_solving_vectors(position)) do
        local p = {x = position.x + vector[1], y = position.y + vector[2]}
        local key = Functions.position_to_string(p)
        local cell = minesweeper.cells[key]
        if cell and cell[1] > 10 then
            local marked_positions = are_mines_marked_around_target(p)
            if marked_positions then
                solved = true
                for _, p in pairs(marked_positions) do
                    minesweeper.cells[Functions.position_to_string(p)][1] = -1
                    visit_cell(p)
                    Functions.disarm_reward(p)
                end
            end
        end
    end
    return solved
end

local function mark_mine(entity, player)
    local position = Functions.position_to_cell_position(entity.position)
    local key = Functions.position_to_string(position)
    local cell = minesweeper.cells[key]
    local score_change = 0

    --Success
    if cell and cell[1] > 9 then
        local surface = game.surfaces.nauvis

        if cell[1] == 10 then
            score_change = 1
        end

        surface.create_entity(
            {
                name = 'flying-text',
                position = entity.position,
                text = 'Mine marked.',
                color = {r = 0.98, g = 0.66, b = 0.22}
            }
        )

        cell[1] = 11
        update_rendering(cell, position)
        entity.destroy()

        local solved = solve_attempt(position)
        if solved then
            player.insert({name = 'stone-furnace', count = 1})
            return score_change
        end

        local e = surface.create_entity({name = 'item-on-ground', position = {position.x + 1, position.y + 1}, stack = {name = 'stone-furnace', count = 1}})
        if e and e.valid then
            e.to_be_looted = true
        end

        return score_change
    end

    --Trigger all adjacent mines when missplacing a disarming furnace.
    for _, vector in pairs(cell_update_vectors) do
        local p = {x = position.x + vector[1], y = position.y + vector[2]}
        local key = Functions.position_to_string(p)
        if minesweeper.cells[key] and minesweeper.cells[key][1] == 10 then
            Functions.kaboom(p)
            score_change = score_change - 8
            minesweeper.cells[key][1] = -1
            solve_attempt(p)
            table.insert(minesweeper.visit_queue, {x = p.x, y = p.y})
        end
    end
    return score_change
end

local function add_mines_to_chunk(left_top, distance_to_center)
    local base_mine_count = 40
    local max_mine_count = 128
    local mine_count = distance_to_center * 0.043 + base_mine_count
    if mine_count > max_mine_count then
        mine_count = max_mine_count
    end

    local shuffle_index = {}
    for i = 1, size_of_chunk_divide_vectors, 1 do
        table.insert(shuffle_index, i)
    end
    table.shuffle_table(shuffle_index)

    -- place shuffled mines
    if distance_to_center < 128 then
        for i = 1, mine_count, 1 do
            local vector = chunk_divide_vectors[shuffle_index[i]]
            local position = {x = left_top.x + vector[1], y = left_top.y + vector[2]}
            if not Functions.is_spawn(position) then
                local key = Functions.position_to_string(position)
                minesweeper.cells[key] = {10}
                minesweeper.active_mines = minesweeper.active_mines + 1
            end
        end
    else
        for i = 1, mine_count, 1 do
            local vector = chunk_divide_vectors[shuffle_index[i]]
            local position = {x = left_top.x + vector[1], y = left_top.y + vector[2]}
            local key = Functions.position_to_string(position)
            minesweeper.cells[key] = {10}
            minesweeper.active_mines = minesweeper.active_mines + 1
        end
    end

    -- remove mines that would form a 3x3 block
    for _, chunk_vector in pairs(chunk_vectors) do
        local left_top_2 = {x = left_top.x + chunk_vector[1], y = left_top.y + chunk_vector[2]}

        for _, vector in pairs(chunk_divide_vectors) do
            local position = {x = left_top_2.x + vector[1], y = left_top_2.y + vector[2]}
            local key = Functions.position_to_string(position)
            local cell = minesweeper.cells[key]
            if cell and cell[1] == 10 then
                if get_adjacent_mine_count(position) == 8 then
                    --if cell[2] then rendering.destroy(cell[2]) end
                    minesweeper.cells[key] = nil
                end
            end
        end
        --[[
		for _, vector in pairs(chunk_divide_vectors) do
			local position = {x = left_top_2.x + vector[1], y = left_top_2.y + vector[2]}
			local key = Functions.position_to_string(position)
			local cell = minesweeper.cells[key]
			if cell then update_rendering(cell, position) end
		end
		]]
    end
end

local function on_chunk_generated(event)
    local left_top = event.area.left_top
    local surface = event.surface
    if surface.index ~= 1 then
        return
    end

    local distance_to_center = math.sqrt((left_top.x + 16) ^ 2 + (left_top.y + 16) ^ 2)
    local tiles = {}

    if distance_to_center < 128 then
        for x = 0, 31, 1 do
            for y = 0, 31, 1 do
                local position = {x = left_top.x + x, y = left_top.y + y}
                if Functions.is_spawn(position) then
                    table.insert(tiles, {name = Functions.get_terrain_tile(surface, position), position = position})
                else
                    table.insert(tiles, {name = 'nuclear-ground', position = position})
                end
            end
        end
    else
        for x = 0, 31, 1 do
            for y = 0, 31, 1 do
                local position = {x = left_top.x + x, y = left_top.y + y}
                table.insert(tiles, {name = 'nuclear-ground', position = position})
                --table.insert(tiles, {name = Functions.get_terrain_tile(surface, position), position = position})
            end
        end
    end
    surface.set_tiles(tiles, true)

    --surface.clear() will cause to trigger on_chunk_generated twice
    local key = Functions.position_to_string(left_top)
    if minesweeper.chunks[key] then
        return
    end
    minesweeper.chunks[key] = true

    add_mines_to_chunk(left_top, distance_to_center)
end

local function on_player_changed_position(event)
    local player = game.players[event.player_index]
    if not Functions.is_minefield_tile(player.position) then
        return
    end
    local cell_position = Functions.position_to_cell_position(player.position)
    local score_change = visit_cell(cell_position)
    if score_change < 0 then
        solve_attempt(cell_position)
    end
    Map_score.set_score(player, Map_score.get_score(player) + score_change)
end

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

    if not game.item_prototypes[entity.name] then
        return
    end
    if not Functions.is_minefield_tile(entity.position, true) then
        return
    end

    if event.player_index then
        local player = game.players[event.player_index]
        if entity.position.x % 2 == 1 and entity.position.y % 2 == 1 and entity.name == 'stone-furnace' then
            local score_change = mark_mine(entity, player)
            Map_score.set_score(player, Map_score.get_score(player) + score_change)
            return
        end
        player.insert({name = entity.name, count = 1})
    else
        local inventory = event.robot.get_inventory(defines.inventory.robot_cargo)
        inventory.insert({name = entity.name, count = 1})
    end
    entity.destroy()
end

local function on_built_entity(event)
    deny_building(event)
end

local function on_robot_built_entity(event)
    deny_building(event)
end

local function update_built_tiles(surface, tiles)
    for _, placed_tile in pairs(tiles) do
        local cell_position = Functions.position_to_cell_position(placed_tile.position)
        local key = Functions.position_to_string(cell_position)
        local cell = minesweeper.cells[key]
        if not cell and Functions.is_minefield_tile(placed_tile.position) then
            minesweeper.cells[key] = {9}
        end
        local cell = minesweeper.cells[key]
        if cell then
            update_rendering(cell, cell_position)
        end
    end
end

local function on_player_built_tile(event)
    update_built_tiles(game.surfaces[event.surface_index], event.tiles)
end

local function on_robot_built_tile(event)
    update_built_tiles(event.robot.surface, event.tiles)
end

local function on_player_mined_tile(event)
    update_built_tiles(game.surfaces[event.surface_index], event.tiles)
end

local function on_robot_mined_tile(event)
    update_built_tiles(event.robot.surface, event.tiles)
end

local function on_player_created(event)
    local player = game.players[event.player_index]
    player.insert({name = 'stone-furnace', count = 1})
end

local function on_player_respawned(event)
    local player = game.players[event.player_index]
    player.insert({name = 'stone-furnace', count = 1})

    game.surfaces.nauvis.destroy_decoratives({name = 'nuclear-ground-patch'})
end

local function on_entity_died(event)
    local entity = event.entity
    if not entity.valid then
        return
    end
    if entity.force.index ~= 2 then
        return
    end
    local force = event.force
    if not force then
        return
    end
    if force.name ~= 'minesweeper' then
        return
    end
    local revived_entity = entity.clone({position = entity.position})
    revived_entity.health = entity.prototype.max_health
    entity.destroy()
end

local function on_nth_tick()
    for k, position in pairs(minesweeper.visit_queue) do
        visit_cell(position)
        table.remove(minesweeper.visit_queue, k)
        break
    end
end

local function on_init()
    game.create_force('minesweeper')

    global.custom_highscore.description = 'Minesweep rank:'

    local surface = game.surfaces[1]
    local mgs = surface.map_gen_settings
    mgs.water = 0
    mgs.cliff_settings = {cliff_elevation_interval = 0, cliff_elevation_0 = 0}
    mgs.autoplace_controls = {
        ['coal'] = {frequency = 0, size = 0, richness = 0},
        ['stone'] = {frequency = 0, size = 0, richness = 0},
        ['copper-ore'] = {frequency = 0, size = 0, richness = 0},
        ['iron-ore'] = {frequency = 0, size = 0, richness = 0},
        ['uranium-ore'] = {frequency = 0, size = 0, richness = 0},
        ['crude-oil'] = {frequency = 0, size = 0, richness = 0},
        ['trees'] = {frequency = 4, size = 0.5, richness = 0.1}
    }
    surface.map_gen_settings = mgs
    surface.clear(true)

    minesweeper.chunks = {}
    minesweeper.cells = {}
    minesweeper.visit_queue = {}
    minesweeper.player_data = {}
    minesweeper.active_mines = 0
    minesweeper.disarmed_mines = 0
    minesweeper.triggered_mines = 0

    local T = Map.Pop_info()
    T.main_caption = 'Minesweeper'
    T.sub_caption = ''
    T.text =
        table.concat(
        {
            'Mechanical lifeforms once dominated this world.\n',
            'They have left long ago, leaving an inhabitable wasteland.\n',
            'It also seems riddled with buried explosives.\n\n',
            'Mark mines with your stone furnace.\n',
            'Marked mines are save to walk on.\n',
            'When enough mines in an area are marked,\n',
            'they will disarm and yield rewards!\n',
            'Faulty marking may trigger surrounding mines!!\n\n',
            'As you move away from spawn,\n',
            'mine density and radius required to disarm will increase.\n',
            'Crates will contain more loot and ore will have higher yield.\n\n',
            'The paint for the numerics does not work very well with the dirt.\n',
            'Laying some stone bricks or better may help.\n'
        }
    )
    T.main_caption_color = {r = 255, g = 125, b = 55}
    T.sub_caption_color = {r = 0, g = 250, b = 150}
end

local Event = require 'utils.event'
Event.on_init(on_init)
Event.on_nth_tick(2, on_nth_tick)
Event.add(defines.events.on_chunk_generated, on_chunk_generated)
Event.add(defines.events.on_player_changed_position, on_player_changed_position)
Event.add(defines.events.on_built_entity, on_built_entity)
Event.add(defines.events.on_entity_died, on_entity_died)
Event.add(defines.events.on_robot_built_entity, on_robot_built_entity)
Event.add(defines.events.on_player_created, on_player_created)
Event.add(defines.events.on_player_respawned, on_player_respawned)
Event.add(defines.events.on_robot_built_tile, on_robot_built_tile)
Event.add(defines.events.on_player_built_tile, on_player_built_tile)
Event.add(defines.events.on_robot_mined_tile, on_robot_mined_tile)
Event.add(defines.events.on_player_mined_tile, on_player_mined_tile)
