--Author: Hexicube
--The size of each individual maze, in cells.
local maze_width = 17
local maze_height = 17

--The size of each cell within a maze, in tiles. This includes the border.
--This is also the thickness of passages between mazes.
local maze_tile_size = 80
--The thickness of cell borders (walls), in tiles.
local maze_tile_border = 2
--These two values are specifically chosen to only just allow train loops.
--If tile size is reduced or border size is increased, players will be forced to use 4-way crossings instead.

--Number of ore cells per maze, for each type.
local iron_ore_count = 8
local copper_ore_count = 6
local coal_ore_count = 4
local stone_ore_count = 2
local uranium_ore_count = 1

local resource_density_factor = 500

--Warning: Do not exceed the total number of cells in the maze, or it will break!

--DO NOT TOUCH BELOW THIS LINE--

local function get_random_maze_val(cur_seed)
    local new_seed =   bit32.bor(cur_seed   + 0x9E3779B9, 0)
    local return_val = bit32.bor(new_seed   * 0xBF58476D, 0)
    return_val =       bit32.bor(return_val * 0x94D049BB, 0)
    return new_seed,   bit32.bor(return_val             , 0)
end

local last_maze_x, last_maze_y, last_maze = nil, nil, nil
local function get_maze(x, y, seed, width, height)
    if last_maze and last_maze_x == x and last_maze_y == y then return last_maze end

    if not global.maze_data then global.maze_data = {} end
    if not global.maze_data[x] then global.maze_data[x] = {} end
    if global.maze_data[x][y] then
        last_maze = global.maze_data[x][y]
        last_maze_x = x
        last_maze_y = y
        return last_maze
    end

    local maze_data = {}
    local grid = {}
    for tx = 1,width do
        maze_data[tx] = {}
        grid[tx] = {}
        for ty = 1,height do
            maze_data[tx][ty] = 15
            grid[tx][ty] = true
        end
    end

    local maze_seed = bit32.bxor(bit32.lshift(x,16) + bit32.band(y,65535), seed)
    local value = 0

    local queue = {}
    maze_seed, value = get_random_maze_val(maze_seed)
    local pos = {0, 0}
    pos[1] = value % width  + 1
    value = bit32.rshift(value,16)
    pos[2] = value % height + 1

    queue[#queue+1] = {pos[1], pos[2], pos[1]-1, pos[2]  }
    queue[#queue+1] = {pos[1], pos[2], pos[1]+1, pos[2]  }
    queue[#queue+1] = {pos[1], pos[2], pos[1]  , pos[2]-1}
    queue[#queue+1] = {pos[1], pos[2], pos[1]  , pos[2]+1}

    while #queue > 0 do
        maze_seed, value = get_random_maze_val(maze_seed)
        local connection = table.remove(queue, value % #queue + 1)
        local sx, sy = connection[1], connection[2]
        local tx, ty = connection[3], connection[4]
        if tx > 0 and ty > 0 and tx <= width and ty <= height and grid[tx][ty] then
            local dx, dy = sx - tx, sy - ty
            local mod_s, mod_t = 3, 3
            if dy == 1 then
                mod_s = 1
            elseif dy == -1 then
                mod_t = 1
            elseif dx == 1 then
                mod_s = 2
            else
                mod_t = 2
            end
            maze_data[sx][sy] = bit32.band(maze_data[sx][sy], mod_s)
            maze_data[tx][ty] = bit32.band(maze_data[tx][ty], mod_t)
            grid[sx][sy] = false
            grid[tx][ty] = false

            queue[#queue+1] = {tx, ty, tx-1, ty  }
            queue[#queue+1] = {tx, ty, tx+1, ty  }
            queue[#queue+1] = {tx, ty, tx  , ty-1}
            queue[#queue+1] = {tx, ty, tx  , ty+1}
        end
    end

    maze_seed, value = get_random_maze_val(maze_seed)
    maze_data[value % width  + 1][1] = bit32.band(maze_data[value % width  + 1][1], 1)
    value = bit32.rshift(value, 16)
    maze_data[1][value % height + 1] = bit32.band(maze_data[1][value % height + 1], 2)

    maze_seed, value = get_random_maze_val(maze_seed)
    maze_data[width+1] = {0, 0}
    maze_data[width+1][1] = value % width + 1
    value = bit32.rshift(value, 16)
    maze_data[width+1][2] = value % height + 1

    global.maze_data[x][y] = maze_data
    last_maze = maze_data
    last_maze_x = x
    last_maze_y = y
    return maze_data
end

local last_maze_ore_x, last_maze_ore_y, last_maze_ore = nil, nil, nil
local function get_maze_ore(x, y, seed, width, height)
    if last_maze_ore and last_maze_ore_x == x and last_maze_ore_y == y then return last_maze_ore end

    if not global.maze_data_ore then global.maze_data_ore = {} end
    if not global.maze_data_ore[x] then global.maze_data_ore[x] = {} end
    if global.maze_data_ore[x][y] then
        last_maze_ore = global.maze_data_ore[x][y]
        last_maze_ore_x = x
        last_maze_ore_y = y
        return last_maze_ore
    end

    local coord_list = {}
    local maze_data = {}
    for tx=1,width do
        maze_data[tx] = {}
        for ty=1,height do
            maze_data[tx][ty] = nil
            coord_list[#coord_list+1] = {tx,ty}
        end
    end

    local maze_seed = bit32.bxor(bit32.lshift(x,16) + bit32.band(y,65535), seed)
    local value = 0

    for a=1,iron_ore_count do
        maze_seed, value = get_random_maze_val(maze_seed)
        local pos = table.remove(coord_list, value % #coord_list + 1)
        maze_data[pos[1]][pos[2]] = "iron-ore"
    end
    for a=1,copper_ore_count do
        maze_seed, value = get_random_maze_val(maze_seed)
        local pos = table.remove(coord_list, value % #coord_list + 1)
        maze_data[pos[1]][pos[2]] = "copper-ore"
    end
    for a=1,coal_ore_count do
        maze_seed, value = get_random_maze_val(maze_seed)
        local pos = table.remove(coord_list, value % #coord_list + 1)
        maze_data[pos[1]][pos[2]] = "coal"
    end
    for a=1,stone_ore_count do
        maze_seed, value = get_random_maze_val(maze_seed)
        local pos = table.remove(coord_list, value % #coord_list + 1)
        maze_data[pos[1]][pos[2]] = "stone"
    end
    for a=1,uranium_ore_count do
        maze_seed, value = get_random_maze_val(maze_seed)
        local pos = table.remove(coord_list, value % #coord_list + 1)
        maze_data[pos[1]][pos[2]] = "uranium-ore"
    end

    global.maze_data_ore[x][y] = maze_data
    last_maze_ore = maze_data
    last_maze_ore_x = x
    last_maze_ore_y = y
    return maze_data
end

local function global_to_maze_pos(x, y)
    --Ensures we start in the middle of a tile.
    x = x + maze_tile_size/2
    y = y + maze_tile_size/2

    local maze_width_raw  = (maze_width +1) * maze_tile_size
    local maze_height_raw = (maze_height+1) * maze_tile_size

    local global_maze_x = math.floor(x / maze_width_raw)
    local global_maze_y = math.floor(y / maze_height_raw)
    x = x - global_maze_x * maze_width_raw
    y = y - global_maze_y * maze_height_raw

    local inner_x, inner_y = x, y

    local local_maze_x = math.floor(x / maze_tile_size)
    local local_maze_y = math.floor(y / maze_tile_size)
    x = x - local_maze_x * maze_tile_size
    y = y - local_maze_y * maze_tile_size

    return global_maze_x, global_maze_y, local_maze_x, local_maze_y, x, y
end

local function handle_maze_tile(x, y, surf, seed)
    local orig_x, orig_y = x, y
    local global_maze_x, global_maze_y, local_maze_x, local_maze_y = 0, 0, 0, 0
    global_maze_x, global_maze_y, local_maze_x, local_maze_y, x, y = global_to_maze_pos(x, y)

    local maze_data = get_maze(global_maze_x, global_maze_y, seed, maze_width, maze_height)
    local maze_value = 0
    if local_maze_x == 0 or local_maze_y == 0 then
        if local_maze_x == 0 then
            if local_maze_y ~= 0 then
                if maze_data[maze_width+1][1] ~= local_maze_y then maze_value = 1 end
            end
        else
            if maze_data[maze_width+1][2] ~= local_maze_x then maze_value = 2 end
        end
    else
        maze_value = maze_data[local_maze_x][local_maze_y]
    end

    if x < maze_tile_border and y < maze_tile_border then return {name = "out-of-map", position = {orig_x, orig_y}} end
    if x < maze_tile_border and bit32.btest(maze_value,1) then return {name = "out-of-map", position = {orig_x, orig_y}} end
    if y < maze_tile_border and bit32.btest(maze_value,2) then return {name = "out-of-map", position = {orig_x, orig_y}} end
    return nil
end

local function handle_maze_tile_ore(x, y, surf, seed)
    local orig_x, orig_y = x, y
    local spawn_distance_1k = math.sqrt(x*x + y*y) / 1000
    local global_maze_x, global_maze_y, local_maze_x, local_maze_y = 0, 0, 0, 0
    global_maze_x, global_maze_y, local_maze_x, local_maze_y, x, y = global_to_maze_pos(x, y)

    if x < maze_tile_border or y < maze_tile_border then return end

    local seed_x = global_maze_x * maze_width + local_maze_x
    local seed_y = global_maze_x * maze_height + local_maze_y

    local ore_name = nil
    if local_maze_x == 0 or local_maze_y == 0 then
      if global_maze_x == 0 and global_maze_y == 0 then
          if local_maze_x == 1 and local_maze_y == 0 then ore_name = "iron-ore" end
          if local_maze_x == 0 and local_maze_y == 1 then ore_name = "stone" end
      elseif global_maze_x == -1 and global_maze_y == 0 and local_maze_x == maze_width and local_maze_y == 0 then ore_name = "copper-ore"
      elseif global_maze_x == 0 and global_maze_y == -1 and local_maze_x == 0 and local_maze_y == maze_height then ore_name = "coal"
      end
    else
        local maze_data = get_maze_ore(global_maze_x, global_maze_y, seed, maze_width, maze_height)
        ore_name = maze_data[local_maze_x][local_maze_y]
    end

    if ore_name then
        if surf.can_place_entity{name=ore_name, position={orig_x, orig_y}} then
            local dist_x = math.abs(orig_x)
            local dist_y = math.abs(orig_y)


            dist = spawn_distance_1k
            local resource_amount_max = math.floor(resource_density_factor  * (dist * dist + 1))
            local dist_x = maze_tile_size - x - 1
            if (x - maze_tile_border) < dist_x then dist_x = (x - maze_tile_border) end
            local dist_y = maze_tile_size - y - 1
            if (y - maze_tile_border) < dist_y then dist_y = (y - maze_tile_border) end

            dist = dist_x
            if dist_y < dist then dist = dist_y end
            dist = dist + 1

            local resource_amount = resource_amount_max * dist / maze_tile_size * 2
            if resource_amount > resource_amount_max / 2 then
              surf.create_entity{name=ore_name, position={orig_x, orig_y}, amount=resource_amount}
          end
        end
    end
end

local function on_chunk_generated_ore(event)
    local entities = event.surface.find_entities(event.area)
    for _, entity in pairs(entities) do
        if entity.type == "resource" and entity.name ~= "crude-oil" then
            entity.destroy()
        end
    end

    local tx, ty = event.area.left_top.x, event.area.left_top.y
    local ex, ey = event.area.right_bottom.x, event.area.right_bottom.y

    for x=tx,ex do
        for y=ty,ey do
            handle_maze_tile_ore(x, y, game.surfaces[1], global.maze_seed)
        end
    end
end

function run_shape_module(event)
    if not global.maze_seed then global.maze_seed = math.random(0, 65536 * 65536 - 1) end

    local tiles = {}
    local tx, ty = event.area.left_top.x, event.area.left_top.y
    local ex, ey = event.area.right_bottom.x, event.area.right_bottom.y

    for x=tx,ex do
        for y=ty,ey do
            local new_tile = handle_maze_tile(x, y, game.surfaces[1], global.maze_seed)
            if new_tile then table.insert(tiles, new_tile) end
        end
    end
    game.surfaces[1].set_tiles(tiles, true)

    on_chunk_generated_ore(event)
end
