local Public = {}
local table_shuffle_table = table.shuffle_table
local table_insert = table.insert
local table_remove = table.remove
local math_random = math.random

local room_spacing = 4
local room_spacing2 = room_spacing * 2

local function build_room_rect(surface, position, vector, room_center_position, room_size, offset)
    local room = {}

    --local a = room_radius - 1
    local room_area = {
        left_top = { x = room_center_position.x - room_size.x + 1, y = room_center_position.y - room_size.y + 1 },
        right_bottom = { x = room_center_position.x + room_size.x, y = room_center_position.y + room_size.y }
    }
    room.room_tiles = surface.find_tiles_filtered({ area = room_area })

    room.path_tiles = {}
    for d = 1, room_spacing, 1 do
        local p = { position.x + vector[1] * d + offset.x, position.y + vector[2] * d + offset.y }
        local tile = surface.get_tile(p)
        table_insert(room.path_tiles, tile)
    end

    room.entrance_tile = surface.get_tile({ position.x + offset.x + vector[1] * (room_spacing + 1), position.y + offset.y + vector[2] * (room_spacing + 1) })

    room.room_border_tiles = {}
    local left_top = { x = room_area.left_top.x - 1, y = room_area.left_top.y - 1 }
    local right_bottom = { x = room_area.right_bottom.x, y = room_area.right_bottom.y }
    local t = room.room_border_tiles
    for d = 0, room_size.x * 2, 1 do
        table_insert(t, surface.get_tile({ left_top.x + d, left_top.y }))
        table_insert(t, surface.get_tile({ left_top.x + d, right_bottom.y }))
    end

    for d = 1, room_size.y * 2 - 1, 1 do
        table_insert(t, surface.get_tile({ left_top.x, left_top.y + d }))
        table_insert(t, surface.get_tile({ right_bottom.x, left_top.y + d }))
    end

    room.center = room_center_position
    room.radius = math.min(room_size.x, room_size.y)
    room.size = room_size

    return room
end

local function scan_area_empty(surface, search_area)
    local tiles = surface.find_tiles_filtered({ area = search_area })
    for _, tile in pairs(tiles) do
        if not tile.collides_with('resource') then
            return false
        end
    end
    return true
end

local function scan_strip_empty(surface, position, vector, length)
    for d = 0, length, 1 do
        local p = { position.x + vector[1] * d, position.y + vector[2] * d }
        local tile = surface.get_tile(p)
        if not tile.collides_with('resource') then
            return false
        end
    end
    return true
end

--Scans the X and Y independantly for best size
local function scan_direction_full(surface, position, vector, room_max, room_min)
    local best = { x = room_min, y = room_min }

    -- make the center of the room a steps in the direction of vector.
    -- +2 because with only +1 the calculation would end up overlapping quite often with the current position
    local a = room_min + room_spacing + 2
    local room_center_pos = { x = position.x + vector[1] * a, y = position.y + vector[2] * a }

    local search_area = {
        { x = room_center_pos.x - best.x - room_spacing,     y = room_center_pos.y - best.y - room_spacing },
        { x = room_center_pos.x + best.x + room_spacing + 1, y = room_center_pos.y + best.y + room_spacing + 1 }
    }

    if not scan_area_empty(surface, search_area) then
        return { x = 0, y = 0 }
    end

    local x_end = false
    local y_end = false

    if vector[1] == 0 then
        local yy = position.y + vector[2]
        repeat
            if not x_end then
                if
                    not scan_strip_empty(surface, { x = position.x + best.x + room_spacing + 1, y = yy }, vector, best.y * 2 + room_spacing2) or
                    not scan_strip_empty(surface, { x = position.x - best.x - room_spacing - 1, y = yy }, vector, best.y * 2 + room_spacing2)
                then
                    x_end = true
                else
                    best.x = best.x + 1
                end
                if best.x >= room_max.x then
                    x_end = true
                end
            end
            if not y_end then
                local xx = position.x - best.x - room_spacing - 1
                if
                    not scan_strip_empty(surface, { x = xx, y = position.y + vector[2] * (best.y * 2 + room_spacing2 + 1) }, { 1, 0 }, best.x * 2 + room_spacing2) or
                    not scan_strip_empty(surface, { x = xx, y = position.y + vector[2] * (best.y * 2 + room_spacing2 + 2) }, { 1, 0 }, best.x * 2 + room_spacing2)
                then
                    y_end = true
                else
                    best.y = best.y + 1
                end
                if best.y >= room_max.y then
                    y_end = true
                end
            end
        until (x_end and y_end)
    else
        local xx = position.x + vector[1]
        repeat
            if not y_end then
                if
                    not scan_strip_empty(surface, { x = xx, y = position.y + best.y + room_spacing + 1 }, vector, best.x * 2 + room_spacing2) or
                    not scan_strip_empty(surface, { x = xx, y = position.y - best.y - room_spacing - 1 }, vector, best.x * 2 + room_spacing2)
                then
                    y_end = true
                else
                    best.y = best.y + 1
                end
                if best.y >= room_max.y then
                    y_end = true
                end
            end
            if not x_end then
                local yy = position.y - best.y - room_spacing - 1
                if
                    not scan_strip_empty(surface, { x = position.x + vector[1] * (best.x * 2 + room_spacing2 + 1), y = yy }, { 0, 1 }, best.y * 2 + room_spacing2) or
                    not scan_strip_empty(surface, { x = position.x + vector[1] * (best.x * 2 + room_spacing2 + 2), y = yy }, { 0, 1 }, best.y * 2 + room_spacing2)
                then
                    x_end = true
                else
                    best.x = best.x + 1
                end
                if best.x >= room_max.x then
                    x_end = true
                end
            end
        until (x_end and y_end)
    end

    return best
end

--get room tiles and build a room with best fit and add a chance for offset
local function get_room_tiles_wiggle(surface, position, size, shape)
    local vectors = { { 0, -1 }, { 0, 1 }, { 1, 0 }, { -1, 0 } }
    table_shuffle_table(vectors)

    local room_max = { x = size, y = size }

    if shape == 'wide' then
        room_max.x = room_max.x + 4
    elseif shape == 'tall' then
        room_max.y = room_max.y + 4
    elseif shape == 'big' then
        room_max.x = room_max.x + 3
        room_max.y = room_max.y + 3
    end

    for _, v in pairs(vectors) do
        local full = scan_direction_full(surface, position, v, room_max, 3)

        if full.x > 0 then
            local new_pos = position

            local offset = { x = 0, y = 0 }

            if v[1] == 0 then
                if full.x > 6 then
                    local max_roll = math.abs(full.x * 0.5)
                    if shape == 'square' then
                        max_roll = math.min(max_roll, math.abs(full.y * 0.5))
                    end
                    local roll = math_random(1 - max_roll, max_roll - 1)
                    new_pos.x = new_pos.x + roll
                    offset.x = -roll
                    full.x = full.x - (1 + math.abs(roll * 0.5))
                end
            else
                if full.y > 6 then
                    local max_roll = math.abs(full.y * 0.5)
                    if shape == 'square' then
                        max_roll = math.min(max_roll, math.abs(full.x * 0.5))
                    end
                    local roll = math_random(1 - max_roll, max_roll - 1)
                    new_pos.y = new_pos.y + roll
                    offset.y = -roll
                    full.y = full.y - (1 + math.abs(roll * 0.5))
                end
            end

            if shape == 'square' then
                --game.print("forcing square")
                if full.x > full.y then
                    full.x = full.y
                else
                    full.y = full.x
                end
            end

            local room_center_position = { x = new_pos.x + v[1] * (full.x + room_spacing + 1), y = new_pos.y + v[2] * (full.y + room_spacing + 1) }
            return build_room_rect(surface, new_pos, v, room_center_position, full, offset)
        end
    end
end

local function expand_path_tiles_width(surface, room)
    if not room then
        return
    end

    local max_expansion_count = 1
    if math_random(1, 4) == 1 then
        max_expansion_count = 2
    end
    if math_random(1, 16) == 1 then
        max_expansion_count = 3
    end
    --if max_expansion_count == 0 then return end

    local path_tiles = room.path_tiles
    local vectors = { { 0, -1 }, { 0, 1 }, { 1, 0 }, { -1, 0 } }

    local entrance_tile
    local exit_tile

    local position = path_tiles[1].position
    local expansion_vectors = {}
    for _, v in pairs(vectors) do
        local tile = surface.get_tile({ position.x + v[1], position.y + v[2] })
        if not tile.collides_with('resource') then
            entrance_tile = tile
            exit_tile = surface.get_tile({ path_tiles[#path_tiles].position.x + v[1] * -1, path_tiles[#path_tiles].position.y + v[2] * -1 })
            if v[1] == 0 then
                expansion_vectors = { { 1, 0 }, { -1, 0 } }
            else
                expansion_vectors = { { 0, 1 }, { 0, -1 } }
            end
            break
        end
    end

    if not entrance_tile then
        return
    end
    local ent_tile_pos = entrance_tile.position
    for k, v in pairs(expansion_vectors) do
        local tile = surface.get_tile({ ent_tile_pos.x + v[1], ent_tile_pos.y + v[2] })
        if tile.collides_with('resource') then
            table_remove(expansion_vectors, k)
        end
    end
    if #expansion_vectors == 0 then
        return
    end

    if not exit_tile.collides_with('resource') then
        local exit_tile_pos = exit_tile.position
        for k, v in pairs(expansion_vectors) do
            local tile = surface.get_tile({ exit_tile_pos.x + v[1], exit_tile_pos.y + v[2] })
            if tile.collides_with('resource') then
                table_remove(expansion_vectors, k)
            end
        end
    end
    if #expansion_vectors == 0 then
        return
    end
    if #expansion_vectors > 1 then
        table_shuffle_table(expansion_vectors)
    end

    local tiles = {}
    for k, v in pairs(expansion_vectors) do
        if k > max_expansion_count then
            break
        end
        for _, path_tile in pairs(path_tiles) do
            local tile = surface.get_tile({ path_tile.position.x + v[1], path_tile.position.y + v[2] })
            if tile.collides_with('resource') then
                table_insert(tiles, tile)
            end
        end
    end

    for k, tile in pairs(tiles) do
        table_insert(path_tiles, tile)
    end
end

local function is_bridge_valid(surface, vector, room)
    local bridge_tiles = room.path_tiles
    local scan_vector
    if vector[1] == 0 then
        scan_vector = { 1, 0 }
    else
        scan_vector = { 0, 1 }
    end

    for _, tile in pairs(bridge_tiles) do
        for d = -5, 5, 1 do
            local p = { tile.position.x + scan_vector[1] * d, tile.position.y + scan_vector[2] * d }
            tile = surface.get_tile(p)
            if not tile.collides_with('resource') then
                return
            end
        end
    end

    return true
end

local function build_bridge(surface, position)
    if math_random(1, 8) == 1 then
        return
    end

    local vectors = { { 0, -1 }, { 0, 1 }, { 1, 0 }, { -1, 0 } }
    table_shuffle_table(vectors)

    local room = {}
    room.path_tiles = {}
    room.room_border_tiles = {}
    room.room_tiles = {}

    local a = room_spacing * 4

    for _, v in pairs(vectors) do
        for d = 1, a, 1 do
            local p = { position.x + v[1] * d, position.y + v[2] * d }
            local tile = surface.get_tile(p)
            if not tile.collides_with('resource') then
                break
            end
            table_insert(room.path_tiles, tile)
            if d == a then
                room.path_tiles = {}
            end
        end
        if room.path_tiles[1] then
            if is_bridge_valid(surface, v, room) then
                return room
            else
                room.path_tiles = {}
            end
        end
    end
end

function Public.get_room(surface, position, shape)
    if not shape then
        shape = 'square'
    end

    local room_max = math_random(3, 14)
    local room = get_room_tiles_wiggle(surface, position, room_max, shape)

    if room then
        expand_path_tiles_width(surface, room)
        return room
    end

    room = build_bridge(surface, position)
    if room then
        expand_path_tiles_width(surface, room)
        return room
    end
end

function Public.draw_random_room(surface, position, shape)
    if not shape then
        shape = 'square'
    end

    local room = Public.get_room(surface, position, shape)
    if not room then
        return
    end

    for _, tile in pairs(room.path_tiles) do
        surface.set_tiles({ { name = 'dirt-3', position = tile.position } }, true)
    end

    for _, tile in pairs(room.room_border_tiles) do
        surface.set_tiles({ { name = 'dirt-7', position = tile.position } }, true)
        if math_random(1, 2) == 1 then
            surface.create_entity({ name = 'big-rock', position = tile.position })
        end
    end

    for _, tile in pairs(room.room_tiles) do
        surface.set_tiles({ { name = 'dirt-5', position = tile.position } }, true)
    end
end

return Public
