-- helpers
tau = 2 * math.pi
deg_to_rad = tau / 360
local inv_pi = 1 / math.pi
function degrees(angle)
    return angle * deg_to_rad
end

local function add_entity(tile, entity)
    if type(tile) == 'table' then
        if tile.entities then
            table.insert(tile.entities, entity)
        else
            tile.entities = {entity}
        end
    elseif tile then
        tile = {
            tile = tile,
            entities = {entity}
        }
    end

    return tile
end

local Builders = {}

-- shape builders
function Builders.empty_shape()
    return false
end

function Builders.full_shape()
    return true
end

function Builders.no_entity()
    return nil
end

function Builders.tile(tile)
    return function()
        return tile
    end
end

function Builders.path(thickness, optional_thickness_height)
    local width = thickness / 2
    local thickness2 = optional_thickness_height or thickness
    local height = thickness2 / 2
    return function(x, y)
        return (x > -width and x <= width) or (y > -height and y <= height)
    end
end

function Builders.rectangle(width, height)
    width = width / 2
    if height then
        height = height / 2
    else
        height = width
    end
    return function(x, y)
        return x > -width and x <= width and y > -height and y <= height
    end
end

function Builders.line_x(thickness)
    thickness = thickness / 2
    return function(_, y)
        return y > -thickness and y <= thickness
    end
end

function Builders.line_y(thickness)
    thickness = thickness / 2
    return function(x, _)
        return x > -thickness and x <= thickness
    end
end

function Builders.square_diamond(size)
    size = size / 2
    return function(x, y)
        return math.abs(x) + math.abs(y) <= size
    end
end

local rot = math.sqrt(2) / 2 -- 45 degree rotation.
function Builders.rectangle_diamond(width, height)
    width = width / 2
    height = height / 2
    return function(x, y)
        local rot_x = rot * (x - y)
        local rot_y = rot * (x + y)
        return math.abs(rot_x) < width and math.abs(rot_y) < height
    end
end

function Builders.circle(radius)
    local rr = radius * radius
    return function(x, y)
        return x * x + y * y < rr
    end
end

function Builders.oval(x_radius, y_radius)
    local x_rr = x_radius * x_radius
    local y_rr = y_radius * y_radius
    return function(x, y)
        return ((x * x) / x_rr + (y * y) / y_rr) < 1
    end
end

function Builders.sine_fill(width, height)
    local width_inv = tau / width
    local height_inv = -2 / height
    return function(x, y)
        local x2 = x * width_inv
        local y2 = y * height_inv
        if y <= 0 then
            return y2 < math.sin(x2)
        else
            return y2 > math.sin(x2)
        end
    end
end

function Builders.sine_wave(width, height, thickness)
    local width_inv = tau / width
    local height_inv = 2 / height
    thickness = thickness * 0.5
    return function(x, y)
        local x2 = x * width_inv
        local y2 = math.sin(x2)
        y = y * height_inv
        local d = math.abs(y2 - y)

        return d < thickness
    end
end

function Builders.rectangular_spiral(x_size, optional_y_size)
    optional_y_size = optional_y_size or x_size

    x_size = 1 / x_size
    optional_y_size = 1 / optional_y_size
    return function(x, y)
        x, y = x * x_size, y * optional_y_size
        x, y = math.floor(x + 0.5), math.floor(y + 0.5)
        local a = -math.max(math.abs(x), math.abs(y))

        if a % 2 == 0 then
            return y ~= a or x == a
        else
            return y == a and x ~= a
        end
    end
end

function Builders.circular_spiral(out_thickness, total_thickness)
    local half_total_thickness = total_thickness * 0.5
    return function(x, y)
        local d = math.sqrt(x * x + y * y)

        local angle = 1 + inv_pi * math.atan2(x, y)
        local offset = d + (angle * half_total_thickness)

        return offset % total_thickness >= out_thickness
    end
end

local tile_map = {
    [1] = false,
    [2] = true,
    [3] = 'concrete',
    [4] = 'deepwater-green',
    [5] = 'deepwater',
    [6] = 'dirt-1',
    [7] = 'dirt-2',
    [8] = 'dirt-3',
    [9] = 'dirt-4',
    [10] = 'dirt-5',
    [11] = 'dirt-6',
    [12] = 'dirt-7',
    [13] = 'dry-dirt',
    [14] = 'grass-1',
    [15] = 'grass-2',
    [16] = 'grass-3',
    [17] = 'grass-4',
    [18] = 'hazard-concrete-left',
    [19] = 'hazard-concrete-right',
    [20] = 'lab-dark-1',
    [21] = 'lab-dark-2',
    [22] = 'lab-white',
    [23] = 'out-of-map',
    [24] = 'red-desert-0',
    [25] = 'red-desert-1',
    [26] = 'red-desert-2',
    [27] = 'red-desert-3',
    [28] = 'sand-1',
    [29] = 'sand-2',
    [30] = 'sand-3',
    [31] = 'stone-path',
    [32] = 'water-green',
    [33] = 'water'
}

function Builders.decompress(pic)
    local data = pic.data
    local width = pic.width
    local height = pic.height

    local uncompressed = {}

    for y = 1, height do
        local row = data[y]
        local u_row = {}
        uncompressed[y] = u_row
        local x = 1
        for index = 1, #row, 2 do
            local pixel = tile_map[row[index]]
            local count = row[index + 1]

            for _ = 1, count do
                u_row[x] = pixel
                x = x + 1
            end
        end
    end

    return {width = width, height = height, data = uncompressed}
end

function Builders.picture(pic)
    local data = pic.data
    local width = pic.width
    local height = pic.height

    -- the plus one is because lua tables are one based.
    local half_width = math.floor(width / 2) + 1
    local half_height = math.floor(height / 2) + 1
    return function(x, y)
        x = math.floor(x)
        y = math.floor(y)
        local x2 = x + half_width
        local y2 = y + half_height

        if y2 > 0 and y2 <= height and x2 > 0 and x2 <= width then
            return data[y2][x2]
        else
            return false
        end
    end
end

-- transforms and shape helpers
function Builders.translate(shape, x_offset, y_offset)
    return function(x, y, world)
        return shape(x - x_offset, y - y_offset, world)
    end
end

function Builders.scale(shape, x_scale, y_scale)
    y_scale = y_scale or x_scale

    x_scale = 1 / x_scale
    y_scale = 1 / y_scale

    return function(x, y, world)
        return shape(x * x_scale, y * y_scale, world)
    end
end

function Builders.rotate(shape, angle)
    local qx = math.cos(angle)
    local qy = math.sin(angle)
    return function(x, y, world)
        local rot_x = qx * x - qy * y
        local rot_y = qy * x + qx * y
        return shape(rot_x, rot_y, world)
    end
end

function Builders.flip_x(shape)
    return function(x, y, world)
        return shape(-x, y, world)
    end
end

function Builders.flip_y(shape)
    return function(x, y, world)
        return shape(x, -y, world)
    end
end

function Builders.flip_xy(shape)
    return function(x, y, world)
        return shape(-x, -y, world)
    end
end

function Builders.any(shapes)
    return function(x, y, world)
        for _, s in ipairs(shapes) do
            local tile = s(x, y, world)
            if tile then
                return tile
            end
        end
        return false
    end
end

function Builders.all(shapes)
    return function(x, y, world)
        local tile
        for _, s in ipairs(shapes) do
            tile = s(x, y, world)
            if not tile then
                return false
            end
        end
        return tile
    end
end

function Builders.combine(shapes)
    return function(x, y, world)
        local function combine_table(tile, index)
            local i, s = next(shapes, index)
            while i do
                local t = s(x, y, world)
                if type(t) == 'table' then
                    if not tile.tile then
                        tile.tile = t.tile
                    end

                    local es = t.entities
                    if es then
                        for _, e in ipairs(es) do
                            add_entity(tile, e)
                        end
                    end
                else
                    if not tile.tile then
                        tile.tile = t
                    end
                end

                i, s = next(shapes, i)
            end

            return tile
        end

        local tile = false

        local i, s = next(shapes, nil)
        while i do
            local t = s(x, y, world)
            if not tile then
                tile = t
            elseif type(t) == 'table' then
                t.tile = tile
                return combine_table(t, i)
            end

            if type(tile) == 'table' then
                return combine_table(tile, i)
            end

            i, s = next(shapes, i)
        end

        return tile
    end
end

function Builders.subtract(shape, minus_shape)
    return function(x, y, world)
        if minus_shape(x, y, world) then
            return false
        else
            return shape(x, y, world)
        end
    end
end

function Builders.invert(shape)
    return function(x, y, world)
        return not shape(x, y, world)
    end
end

function Builders.throttle_x(shape, x_in, x_size)
    return function(x, y, world)
        if x % x_size < x_in then
            return shape(x, y, world)
        else
            return false
        end
    end
end

function Builders.throttle_y(shape, y_in, y_size)
    return function(x, y, world)
        if y % y_size < y_in then
            return shape(x, y, world)
        else
            return false
        end
    end
end

function Builders.throttle_xy(shape, x_in, x_size, y_in, y_size)
    return function(x, y, world)
        if x % x_size < x_in and y % y_size < y_in then
            return shape(x, y, world)
        else
            return false
        end
    end
end

function Builders.throttle_world_xy(shape, x_in, x_size, y_in, y_size)
    return function(x, y, world)
        if world.x % x_size < x_in and world.y % y_size < y_in then
            return shape(x, y, world)
        else
            return false
        end
    end
end

function Builders.choose(condition, true_shape, false_shape)
    return function(x, y, world)
        if condition(x, y, world) then
            return true_shape(x, y, world)
        else
            return false_shape(x, y, world)
        end
    end
end

function Builders.if_else(shape, else_shape)
    return function(x, y, world)
        return shape(x, y, world) or else_shape(x, y, world)
    end
end

function Builders.linear_grow(shape, size)
    return function(x, y, world)
        local t = math.ceil((y / size) + 0.5)
        local n = math.ceil((math.sqrt(8 * t + 1) - 1) / 2)
        local t_upper = n * (n + 1) * 0.5
        local t_lower = t_upper - n

        y = (y - size * (t_lower + n / 2 - 0.5)) / n
        x = x / n

        return shape(x, y, world)
    end
end

function Builders.grow(in_shape, out_shape, size, offset)
    local half_size = size / 2
    return function(x, y, world)
        local tx = math.ceil(math.abs(x) / half_size)
        local ty = math.ceil(math.abs(y) / half_size)
        local t = math.max(tx, ty)

        for i = t, 2.5 * t, 1 do
            local out_t = 1 / (i - offset)
            local in_t = 1 / i

            if out_shape(out_t * x, out_t * y, world) then
                return nil
            end

            local tile = in_shape(in_t * x, in_t * y, world)
            if tile then
                return tile
            end
        end

        return nil
    end
end

function Builders.project(shape, size, r)
    local ln_r = math.log(r)
    local r2 = 1 / (r - 1)
    local a = 1 / size

    return function(x, y, world)
        local offset = 0.5 * size
        local sn = math.ceil(y + offset)

        local n = math.ceil(math.log((r - 1) * sn * a + 1) / ln_r - 1)
        local rn = r ^ n
        local rn2 = 1 / rn
        local c = size * rn

        local sn_upper = size * (r ^ (n + 1) - 1) * r2
        x = x * rn2
        y = (y - (sn_upper - 0.5 * c) + offset) * rn2

        return shape(x, y, world)
    end
end

function Builders.project_pattern(pattern, size, r, columns, rows)
    local ln_r = math.log(r)
    local r2 = 1 / (r - 1)
    local a = 1 / size
    local half_size = size / 2

    return function(x, y, world)
        local offset = 0.5 * size
        local sn = math.ceil(y + offset)

        local n = math.ceil(math.log((r - 1) * sn * a + 1) / ln_r - 1)
        local rn = r ^ n
        local rn2 = 1 / rn
        local c = size * rn

        local sn_upper = size * (r ^ (n + 1) - 1) * r2
        x = x * rn2
        y = (y - (sn_upper - 0.5 * c) + offset) * rn2

        local row_i = n % rows + 1
        local row = pattern[row_i]

        local x2 = ((x + half_size) % size) - half_size
        local col_pos = math.floor(x / size + 0.5)
        local col_i = col_pos % columns + 1

        local shape = row[col_i]

        return shape(x2, y, world)
    end
end

function Builders.project_overlap(shape, size, r)
    local ln_r = math.log(r)
    local r2 = 1 / (r - 1)
    local a = 1 / size
    local offset = 0.5 * size

    return function(x, y, world)
        local sn = math.ceil(y + offset)

        local n = math.ceil(math.log((r - 1) * sn * a + 1) / ln_r - 1)
        local rn = r ^ n
        local rn2 = 1 / rn
        local c = size * rn

        local sn_upper = size * (r ^ (n + 1) - 1) * r2
        x = x * rn2
        y = (y - (sn_upper - 0.5 * c) + offset) * rn2

        local tile

        tile = shape(x, y, world)
        if tile then
            return tile
        end

        local rn_above = rn / r
        local rn2_above = 1 / rn_above
        local c_above = size * rn_above

        local sn_upper_above = sn_upper - c
        local x_above = x * rn2_above
        local y_above = (y - (sn_upper_above - 0.5 * c_above) + offset) * rn2_above

        tile = shape(x_above, y_above, world)
        if tile then
            return tile
        end

        local rn_below = rn * r
        local rn2_below = 1 / rn_below
        local c_below = size * rn_below

        local sn_upper_below = sn_upper + c_below
        local x_below = x * rn2_below
        local y_below = (y - (sn_upper_below - 0.5 * c_below) + offset) * rn2_below

        return shape(x_below, y_below, world)
    end
end

-- Entity generation
function Builders.entity(shape, name)
    return function(x, y, world)
        if shape(x, y, world) then
            return {name = name}
        end
    end
end

function Builders.entity_func(shape, func)
    return function(x, y, world)
        if shape(x, y, world) then
            return func(x, y, world)
        end
    end
end

function Builders.resource(shape, resource_type, amount_function)
    amount_function = amount_function or function()
            return 404
        end
    return function(x, y, world)
        if shape(x, y, world) then
            return {
                name = resource_type,
                amount = amount_function(world.x, world.y)
            }
        end
    end
end

function Builders.apply_entity(shape, entity_shape)
    return function(x, y, world)
        local tile = shape(x, y, world)

        if not tile then
            return false
        end

        local e = entity_shape(x, y, world)
        if e then
            tile = add_entity(tile, e)
        end

        return tile
    end
end

function Builders.apply_entities(shape, entity_shapes)
    return function(x, y, world)
        local tile = shape(x, y, world)

        if not tile then
            return false
        end

        for _, es in ipairs(entity_shapes) do
            local e = es(x, y, world)
            if e then
                tile = add_entity(tile, e)
            end
        end

        return tile
    end
end

-- pattern builders.
function Builders.single_pattern(shape, width, height)
    shape = shape or Builders.empty_shape
    local half_width = width / 2
    local half_height
    if height then
        half_height = height / 2
    else
        half_height = half_width
    end

    return function(x, y, world)
        y = ((y + half_height) % height) - half_height
        x = ((x + half_width) % width) - half_width

        return shape(x, y, world)
    end
end

function Builders.single_pattern_overlap(shape, width, height)
    shape = shape or Builders.empty_shape
    local half_width = width / 2
    local half_height
    if height then
        half_height = height / 2
    else
        half_height = half_width
    end

    return function(x, y, world)
        y = ((y + half_height) % height) - half_height
        x = ((x + half_width) % width) - half_width

        return shape(x, y, world) or shape(x + width, y, world) or shape(x - width, y, world) or
            shape(x, y + height, world) or
            shape(x, y - height, world)
    end
end

function Builders.single_x_pattern(shape, width)
    shape = shape or Builders.empty_shape
    local half_width = width / 2

    return function(x, y, world)
        x = ((x + half_width) % width) - half_width

        return shape(x, y, world)
    end
end

function Builders.single_y_pattern(shape, height)
    shape = shape or Builders.empty_shape
    local half_height = height / 2

    return function(x, y, world)
        y = ((y + half_height) % height) - half_height

        return shape(x, y, world)
    end
end

function Builders.grid_x_pattern(pattern, columns, width)
    local half_width = width / 2

    return function(x, y, world)
        local x2 = ((x + half_width) % width) - half_width
        local columns_pos = math.floor(x / width + 0.5)
        local column_i = columns_pos % columns + 1
        local shape = pattern[column_i] or Builders.empty_shape

        return shape(x2, y, world)
    end
end

function Builders.grid_y_pattern(pattern, rows, height)
    local half_height = height / 2

    return function(x, y, world)
        local y2 = ((y + half_height) % height) - half_height
        local row_pos = math.floor(y / height + 0.5)
        local row_i = row_pos % rows + 1
        local shape = pattern[row_i] or Builders.empty_shape

        return shape(x, y2, world)
    end
end

function Builders.grid_pattern(pattern, columns, rows, width, height)
    local half_width = width / 2
    local half_height = height / 2

    return function(x, y, world)
        local y2 = ((y + half_height) % height) - half_height
        local row_pos = math.floor(y / height + 0.5)
        local row_i = row_pos % rows + 1
        local row = pattern[row_i] or {}

        local x2 = ((x + half_width) % width) - half_width
        local col_pos = math.floor(x / width + 0.5)
        local col_i = col_pos % columns + 1

        local shape = row[col_i] or Builders.empty_shape
        return shape(x2, y2, world)
    end
end

function Builders.grid_pattern_overlap(pattern, columns, rows, width, height)
    local half_width = width / 2
    local half_height = height / 2

    return function(x, y, world)
        local y2 = ((y + half_height) % height) - half_height
        local row_pos = math.floor(y / height + 0.5)
        local row_i = row_pos % rows + 1
        local row = pattern[row_i] or {}

        local x2 = ((x + half_width) % width) - half_width
        local col_pos = math.floor(x / width + 0.5)
        local col_i = col_pos % columns + 1

        local shape = row[col_i] or Builders.empty_shape

        local tile = shape(x2, y2, world)
        if tile then
            return tile
        end

        -- edges
        local col_i_left = (col_pos - 1) % columns + 1
        shape = row[col_i_left] or Builders.empty_shape
        tile = shape(x2 + width, y2, world)
        if tile then
            return tile
        end

        local col_i_right = (col_pos + 1) % columns + 1
        shape = row[col_i_right] or Builders.empty_shape
        tile = shape(x2 - width, y2, world)
        if tile then
            return tile
        end

        local row_i_up = (row_pos - 1) % rows + 1
        local row_up = pattern[row_i_up] or {}
        shape = row_up[col_i] or Builders.empty_shape
        tile = shape(x2, y2 + height, world)
        if tile then
            return tile
        end

        local row_i_down = (row_pos + 1) % rows + 1
        local row_down = pattern[row_i_down] or {}
        shape = row_down[col_i] or Builders.empty_shape
        return shape(x2, y2 - height, world)
    end
end

function Builders.grid_pattern_full_overlap(pattern, columns, rows, width, height)
    local half_width = width / 2
    local half_height = height / 2

    return function(x, y, world)
        local y2 = ((y + half_height) % height) - half_height
        local row_pos = math.floor(y / height + 0.5)
        local row_i = row_pos % rows + 1
        local row = pattern[row_i] or {}

        local x2 = ((x + half_width) % width) - half_width
        local col_pos = math.floor(x / width + 0.5)
        local col_i = col_pos % columns + 1

        local row_i_up = (row_pos - 1) % rows + 1
        local row_up = pattern[row_i_up] or {}
        local row_i_down = (row_pos + 1) % rows + 1
        local row_down = pattern[row_i_down] or {}

        local col_i_left = (col_pos - 1) % columns + 1
        local col_i_right = (col_pos + 1) % columns + 1

        -- start from top left, move left to right then down
        local shape = row_up[col_i_left] or Builders.empty_shape
        local tile = shape(x2 + width, y2 + height, world)
        if tile then
            return tile
        end

        shape = row_up[col_i] or Builders.empty_shape
        tile = shape(x2, y2 + height, world)
        if tile then
            return tile
        end

        shape = row_up[col_i_right] or Builders.empty_shape
        tile = shape(x2 - width, y2 + height, world)
        if tile then
            return tile
        end

        shape = row[col_i_left] or Builders.empty_shape
        tile = shape(x2 + width, y2, world)
        if tile then
            return tile
        end

        shape = row[col_i] or Builders.empty_shape
        tile = shape(x2, y2, world)
        if tile then
            return tile
        end

        shape = row[col_i_right] or Builders.empty_shape
        tile = shape(x2 - width, y2, world)
        if tile then
            return tile
        end

        shape = row_down[col_i_left] or Builders.empty_shape
        tile = shape(x2 + width, y2 - height, world)
        if tile then
            return tile
        end

        shape = row_down[col_i] or Builders.empty_shape
        tile = shape(x2, y2 - height, world)
        if tile then
            return tile
        end

        shape = row_down[col_i_right] or Builders.empty_shape
        return shape(x2 - width, y2 - height, world)
    end
end

local function is_spiral(x, y)
    local a = -math.max(math.abs(x), math.abs(y))

    if a % 2 == 0 then
        return y ~= a or x == a
    else
        return y == a and x ~= a
    end
end

function Builders.single_spiral_pattern(shape, width, height)
    local inv_width = 1 / width
    local inv_height = 1 / height
    return function(x, y, world)
        local x1 = math.floor(x * inv_width + 0.5)
        local y1 = math.floor(y * inv_height + 0.5)

        if is_spiral(x1, y1) then
            x1 = x - x1 * width
            y1 = y - y1 * height
            return shape(x1, y1, world)
        else
            return false
        end
    end
end

local function rotate_0(x, y)
    return x, y
end

local function rotate_90(x, y)
    return y, -x
end

local function rotate_180(x, y)
    return -x, -y
end

local function rotate_270(x, y)
    return -y, x
end

local function spiral_rotation(x, y)
    local a = -math.max(math.abs(x), math.abs(y))

    if a % 2 == 0 then
        if y ~= a or x == a then
            if x == a then
                return rotate_0
            elseif y >= x then
                return rotate_270
            else
                return rotate_180
            end
        end
    else
        if y == a and x ~= a then
            return rotate_90
        end
    end
end

function Builders.single_spiral_rotate_pattern(shape, width, optional_height)
    optional_height = optional_height or width

    local inv_width = 1 / width
    local inv_height = 1 / optional_height
    return function(x, y, world)
        local x1 = math.floor(x * inv_width + 0.5)
        local y1 = math.floor(y * inv_height + 0.5)

        local t = spiral_rotation(x1, y1)
        if t then
            x1 = x - x1 * width
            y1 = y - y1 * optional_height
            x1, y1 = t(x1, y1)
            return shape(x1, y1, world)
        else
            return false
        end
    end
end

function Builders.segment_pattern(pattern)
    local count = #pattern

    return function(x, y, world)
        local angle = math.atan2(-y, x)
        local index = math.floor(angle / tau * count) % count + 1
        local shape = pattern[index] or Builders.empty_shape
        return shape(x, y, world)
    end
end

function Builders.pyramid_pattern(pattern, columns, rows, width, height)
    local half_width = width / 2
    local half_height = height / 2

    return function(x, y, world)
        local y2 = ((y + half_height) % height) - half_height
        local row_pos = math.floor(y / height + 0.5)
        local row_i = row_pos % rows + 1
        local row = pattern[row_i] or {}

        if row_pos % 2 ~= 0 then
            x = x - half_width
        end

        local x2 = ((x + half_width) % width) - half_width
        local col_pos = math.floor(x / width + 0.5)
        local col_i = col_pos % columns + 1

        if col_pos > row_pos / 2 or -col_pos > (row_pos + 1) / 2 then
            return false
        end

        local shape = row[col_i] or Builders.empty_shape
        return shape(x2, y2, world)
    end
end

function Builders.pyramid_pattern_inner_overlap(pattern, columns, rows, width, height)
    local half_width = width / 2
    local half_height = height / 2

    return function(x, y, world)
        local y2 = ((y + half_height) % height) - half_height
        local row_pos = math.floor(y / height + 0.5)
        local row_i = row_pos % rows + 1
        local row = pattern[row_i] or {}

        local x_odd
        local x_even
        if row_pos % 2 == 0 then
            x_even = x
            x_odd = x - half_width
        else
            x_even = x - half_width
            x_odd = x
            x = x - half_width
        end

        x_even = ((x_even + half_width) % width) - half_width
        x_odd = ((x_odd + half_width) % width) - half_width

        local col_pos = math.floor(x / width + 0.5)

        local offset = 1
        local offset_odd = 0
        if (col_pos % 2) == (row_pos % 2) then
            offset = 0
            offset_odd = 1
        end

        local col_i = (col_pos - offset) % columns + 1
        local col_i_odd = (col_pos - offset_odd) % columns + 1

        if col_pos > row_pos / 2 or -col_pos > (row_pos + 1) / 2 then
            return false
        end

        local row_i_up = (row_pos - 1) % rows + 1
        local row_up = pattern[row_i_up] or {}
        local row_i_down = (row_pos + 1) % rows + 1
        local row_down = pattern[row_i_down] or {}

        local col_i_left = (col_pos - 1) % columns + 1
        local col_i_right = (col_pos + 1) % columns + 1

        -- start from top left, move left to right then down
        local shape = row_up[col_i_left] or Builders.empty_shape
        local tile = shape(x_even + width, y2 + height, world)
        if tile then
            return tile
        end

        shape = row_up[col_i_odd] or Builders.empty_shape
        tile = shape(x_odd, y2 + height, world)
        if tile then
            return tile
        end

        shape = row_up[col_i_right] or Builders.empty_shape
        tile = shape(x_even - width, y2 + height, world)
        if tile then
            return tile
        end

        shape = row[col_i_left] or Builders.empty_shape
        tile = shape(x_even + width, y2, world)
        if tile then
            return tile
        end

        shape = row[col_i] or Builders.empty_shape
        tile = shape(x_even, y2, world)
        if tile then
            return tile
        end

        shape = row[col_i_right] or Builders.empty_shape
        tile = shape(x_even - width, y2, world)
        if tile then
            return tile
        end

        shape = row_down[col_i_left] or Builders.empty_shape
        tile = shape(x_even + width, y2 - height, world)
        if tile then
            return tile
        end

        shape = row_down[col_i_odd] or Builders.empty_shape
        tile = shape(x_odd, y2 - height, world)
        if tile then
            return tile
        end

        shape = row_down[col_i_right] or Builders.empty_shape
        return shape(x_even - width, y2 - height, world)
    end
end

function Builders.grid_pattern_offset(pattern, columns, rows, width, height)
    local half_width = width / 2
    local half_height = height / 2

    return function(x, y, world)
        local y2 = ((y + half_height) % height) - half_height
        local row_pos = math.floor(y / height + 0.5)
        local row_i = row_pos % rows + 1
        local row = pattern[row_i] or {}

        local x2 = ((x + half_width) % width) - half_width
        local col_pos = math.floor(x / width + 0.5)
        local col_i = col_pos % columns + 1

        y2 = y2 + height * math.floor((row_pos + 1) / rows)
        x2 = x2 + width * math.floor((col_pos + 1) / columns)

        local shape = row[col_i] or Builders.empty_shape
        return shape(x2, y2, world)
    end
end

-- tile converters
function Builders.change_tile(shape, old_tile, new_tile)
    return function(x, y, world)
        local tile = shape(x, y, world)

        if type(tile) == 'table' then
            if tile.tile == old_tile then
                tile.tile = new_tile
            end
        else
            if tile == old_tile then
                tile = new_tile
            end
        end

        return tile
    end
end

function Builders.change_collision_tile(shape, collides, new_tile)
    return function(x, y, world)
        local tile = shape(x, y, world)

        if type(tile) == 'table' then
            if tile.tile.collides_with(collides) then
                tile.tile = new_tile
                return tile
            end
        else
            if tile.collides_with(collides) then
                return new_tile
            end
        end

        return tile
    end
end

-- only changes tiles made by the factorio map generator.
function Builders.change_map_gen_tile(shape, old_tile, new_tile)
    return function(x, y, world)
        local function handle_tile(tile)
            if type(tile) == 'boolean' and tile then
                local gen_tile = world.surface.get_tile(world.x, world.y).name
                if gen_tile == old_tile then
                    return new_tile
                end
            end
            return tile
        end

        local tile = shape(x, y, world)

        if type(tile) == 'table' then
            tile.tile = handle_tile(tile.tile)
        else
            tile = handle_tile(tile)
        end

        return tile
    end
end

-- only changes tiles made by the factorio map generator.
function Builders.change_map_gen_collision_tile(shape, collides, new_tile)
    return function(x, y, world)
        local function handle_tile(tile)
            if type(tile) == 'boolean' and tile then
                local gen_tile = world.surface.get_tile(world.x, world.y)
                if gen_tile.collides_with(collides) then
                    return new_tile
                end
            end
            return tile
        end

        local tile = shape(x, y, world)

        if type(tile) == 'table' then
            tile.tile = handle_tile(tile.tile)
        else
            tile = handle_tile(tile)
        end

        return tile
    end
end

local bad_tiles = {
    ['out-of-map'] = true,
    ['water'] = true,
    ['deepwater'] = true,
    ['water-green'] = true,
    ['deepwater-green'] = true
}

function Builders.overlay_tile_land(shape, tile_shape)
    return function(x, y, world)
        local function handle_tile(tile)
            if type(tile) == 'boolean' then
                return tile and not world.surface.get_tile(world.x, world.y).collides_with('water-tile')
            else
                return not bad_tiles[tile]
            end
        end

        local tile = shape(x, y, world)

        if type(tile) == 'table' then
            if handle_tile(tile.tile) then
                tile.tile = tile_shape(x, y, world) or tile.tile
            end
        else
            if handle_tile(tile) then
                tile = tile_shape(x, y, world) or tile
            end
        end

        return tile
    end
end

local water_tiles = {
    ['water'] = true,
    ['deepwater'] = true,
    ['water-green'] = true,
    ['deepwater-green'] = true
}

function Builders.fish(shape, spawn_rate)
    return function(x, y, world)
        local function handle_tile(tile)
            if type(tile) == 'string' then
                if water_tiles[tile] and spawn_rate >= math.random() then
                    return {name = 'fish'}
                end
            elseif tile then
                if world.surface.get_tile(world.x, world.y).collides_with('water-tile') and spawn_rate >= math.random() then
                    return {name = 'fish'}
                end
            end
        end

        local tile = shape(x, y, world)

        if type(tile) == 'table' then
            local entity = handle_tile(tile.tile)
            if entity then
                add_entity(tile, entity)
            end
        else
            local entity = handle_tile(tile)
            if entity then
                tile = {
                    tile = tile,
                    entities = {entity}
                }
            end
        end

        return tile
    end
end

function Builders.apply_effect(shape, func)
    return function(x, y, world)
        local tile = shape(x, y, world)
        return func(x, y, world, tile)
    end
end

function Builders.manhattan_value(base, mult)
    return function(x, y)
        return mult * (math.abs(x) + math.abs(y)) + base
    end
end

function Builders.euclidean_value(base, mult)
    return function(x, y)
        return mult * math.sqrt(x * x + y * y) + base
    end
end

function Builders.prepare_weighted_array(array)
    local total = 0
    local weights = {}

    for _, v in ipairs(array) do
        total = total + v.weight
        table.insert(weights, total)
    end

    weights.total = total

    return weights
end

return Builders
