-- This is a newgame postprocessor script for MoO2 1.50 patch project.
--
-- Generates mirrored galaxy (except the home system) for multiplayer.

padx = 50
pady = 50
scale = 90

function tsize(t)
    local i = 0
    for k, v in pairs(t) do
        i = i + 1
    end
    return i
end

function abs(v)
    if v < 0 then return -v else return v end
end

function max(a, b)
    if a > b then return a else return b end
end

function min(a, b)
    if a <= b then return a else return b end
end

function bbox(b, obj)
    if b == nil then
        b = { x1 = obj.x, y1 = obj.y, x2 = obj.x, y2 = obj.y }
    end
    b.x1, b.x2 = min(b.x1, obj.x), max(b.x2, obj.x)
    b.y1, b.y2 = min(b.y1, obj.y), max(b.y2, obj.y)
    return b
end

function tbbox(t)
    local b
    for k, v in pairs(t) do
        b = bbox(b, v)
    end
    return b
end

function get_homes()
    local pp = get_planets()
    local ss = get_stars()
    local homes = {}
    for i, y in pairs(get_players()) do
        local sid = pp[y.home_planet].star
        ss[sid].playerid = i
        ss[sid].homeplanet = y.home_planet
        table.insert(homes, ss[sid])
    end
    table.sort(homes, function(a, b) return a.playerid < b.playerid end)
    return ss, pp, homes
end

function go()
    set_stars({}) -- early check that we're in universal context

    -- store home stars in homes array
    local ss, pp, homes = get_homes()
    local max_stars = tsize(ss)

    -- undoing bshowring-like effects
    for i,s in pairs(ss) do
        if s.playerid ~= nil then
            s.visited = 1 << s.playerid
        else
            s.visited = 0
        end
    end
    set_stars(ss)

    -- determine sector size base on number of players
    local np = rawlen(homes)
    local nslicex, nslicey
    local g = get_game()
    if np == 2 then
        nslicex = 2
        nslicey = 1
    elseif np <= 4 then
        nslicex = 2
        nslicey = 2
    elseif np <= 6 then
        nslicex = 3
        nslicey = 2
    else
        nslicex = 4
        nslicey = 2
    end
    local nslice = nslicex * nslicey
    local dx = (g.map_max_x - 2 * padx) / nslicex
    local dy = (g.map_max_y - 2 * pady) / nslicey
    local max_stars_per_slice = (max_stars + nslice - 1) / nslice

    -- build surrounding sector for each homeworld
    local stars_by_dist = {}
    for _, s in pairs(ss) do
        if s.homeplanet == nil then
            table.insert(stars_by_dist, s)
        end
    end
    local slices = {}
    local sdims = {}
    local ssizes = {}
    for ii, h in ipairs(homes) do
        local m = ""
        local dist = function(s) return max(abs(s.x - h.x), abs(s.y - h.y)) end
        table.sort(stars_by_dist, function(a, b) return dist(a) < dist(b) end)
        local slice = { [h.id] = h }
        local cnt = 1
        local b = bbox(nil, h)
        for jj, s in ipairs(stars_by_dist) do
            if abs(b.x1 - s.x) <= dx and abs(b.x2 - s.x) <= dx and
               abs(b.y1 - s.y) <= dy and abs(b.y2 - s.y) <= dy
            then
                b = bbox(b, s)
                cnt = cnt + 1
                slice[s.id] = s
                if cnt + 1 >= max_stars_per_slice then break end
            end
        end
        table.insert(sdims, { dx = b.x2 - b.x1, dy = b.y2 - b.y1 })
        table.insert(ssizes, cnt)
        table.insert(slices, slice)
    end

    -- find sector most conforming to slice shape
    local best_i, best_val
    for ii, sl in ipairs(slices) do
        local val = abs(dy * 100 / dx - sdims[ii].dy * 100 / sdims[ii].dx)
        if best_val == nil or ssizes[best_i] < ssizes[ii] or val < best_val then
            best_i, best_val = ii, val
        end
    end
    local best_slice = slices[best_i]

    -- move best slice to top left grid cell (align centers)
    local x0 = padx + dx / 2
    local y0 = pady + dy / 2
    local b = tbbox(best_slice)
    local mx = x0 - (b.x2 + b.x1) / 2
    local my = y0 - (b.y2 + b.y1) / 2
    for _, s in pairs(best_slice) do
        s.x = s.x + mx
        s.y = s.y + my
    end
    set_stars(best_slice)

    -- flip best slice if needed
    local ident = function(w) return w end
    local flipx = function(x) return 2 * padx + dx - x end
    local flipy = function(y) return 2 * pady + dy - y end
    local home = table.remove(homes, best_i)
    local tx, ty = ident, ident
    if home.x > x0 then tx = flipx end
    if home.y > y0 then ty = flipy end
    for _, s in pairs(best_slice) do
        s.x = tx(s.x)
        s.y = ty(s.y)
    end
    set_stars(best_slice)

    -- shrinking best slice a bit
    if scale ~= 100 then
        for _, s in pairs(best_slice) do
            s.x = x0 + (s.x - x0) * scale / 100
            s.y = y0 + (s.y - y0) * scale / 100
        end
        set_stars(best_slice)
    end

    --[[
    -- strip other homeworlds to simplify cloning later
    local del_pp = {}
    for ii, s in ipairs(homes) do
        for o, op in pairs(s.orbits) do
            if op ~= s.homeplanet then
                table.insert(del_pp, op)
            end
        end
    end
    del_planets(del_pp)
    ]]--

    -- delete everything but best slice
    local del_ss = {}
    for i, s in pairs(ss) do
        if ss[i].homeplanet == nil and best_slice[i] == nil then
            table.insert(del_ss, i)
        end
    end
    del_stars(del_ss)

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

    -- reread everything, since star ids has changed after deletion
    local ss, pp, homes = get_homes()
    local home = table.remove(homes, best_i)

    --[[
    -- adjust other homes orbits to match best home's orbit
    for ii, s in ipairs(homes) do
        if pp[s.homeplanet].orbit ~= pp[home.homeplanet].orbit then
            set_planets({ [s.homeplanet] = { orbit = pp[home.homeplanet].orbit }})
        end
    end

    -- mirror home systems
    for ii, s in ipairs(homes) do
        for o, op in pairs(home.orbits) do
            if op ~= -1 and op ~= home.homeplanet then
                pp[op].star = s.id
                add_planets({pp[op]})
            end
        end
    end
    ]]--

    -- cloner for marooned leaders
    local ll
    local leader_list
    local mar_map
    function clone_marooned_leader_maybe(s1, s2)
        if s1.special ~= 8 then
            return
        end
        if ll == nil then
            ll = rget_leaders()
            leader_list = {}
            mar_map = {}
            for i, l in pairs(ll) do
                if l.status == "marooned" then
                    mar_map[l.location] = l
                else
                    table.insert(leader_list, i)
                end
            end
        end
        local m = mar_map[s1.id]
        if m == nil then
            error("star ", s1.id, " has no marooned leader")
        end
        -- take random leader as a base
        local cloneid = table.remove(leader_list, random(rawlen(leader_list)))
        m.location = s2.id
        rset_leaders({ [cloneid] = m })
    end

    -- clone best slice
    local islice = 1
    local image = {}
    while rawlen(homes) > 0 do
        local curhome = table.remove(homes, random(rawlen(homes)))
        image[curhome.id] = {}
        local new_stars = {}
        local mx = (islice % nslicex) * dx
        local my = (islice / nslicex) * dy
        local tx, ty = ident, ident
        if nslice == 2 then
            -- 2 way, both flips for 2nd player
            if my > 0 or mx > 0 then
                tx = flipx
                ty = flipy
            end
        elseif nslice == 4 then
            -- 4-3 way, 4 corners
            if mx > 0 then tx = flipx end
            if my > 0 then ty = flipy end
        else
            -- 5-8 way, both flips below horizontal symmetry axis
            if my > 0 then
                tx = flipx
                ty = flipy
            end
        end
        for i, s in pairs(ss) do
            if s.homeplanet == nil then
                local j, s2 = next(add_stars(1))
                image[curhome.id][i] = s2
                if s2 == nil then
                    msgbox("nil star j=", j)
                end
                s2.x = tx(s.x) + mx
                s2.y = ty(s.y) + my
                s2.color = s.color
                s2.special = s.special
                clone_marooned_leader_maybe(s, s2)
                new_stars[j] = s2
                local add_pp = {}
                for o, op in pairs(s.orbits) do
                    if op ~= -1 then
                        pp[op].star = j
                        table.insert(add_pp, pp[op])
                    end
                end
                add_planets(add_pp)
            end
        end
        islice = islice + 1
        new_stars[curhome.id] = curhome
        curhome.x = tx(home.x) + mx
        curhome.y = ty(home.y) + my
        curhome.color = home.color
        set_stars(new_stars)
    end

    fix_wormholes(ss, image)
    mirror_monsters(ss, image)
    move_nebulas(padx, pady, nslice, dx, dy)
    showring()
end

function fix_wormholes(ss, image)
    for i, s in pairs(ss) do
        if s.wormhole > i then
            for _, img in pairs(image) do
                local a, b = img[i].id, img[s.wormhole].id
                set_stars({ [a] = { wormhole = b }, [b] = { wormhole = a } })
            end
        end
    end
end

function mirror_monsters(ss, image)
    -- print_ships("before cloning:\n")
    local hh = rget_ships()
    local sid2h = {}
    for i, h in pairs(hh) do
        if h.status == "ship_is_at_star" and h.player >= 8 then
            sid2h[h.location] = h
        end
    end
    for i, s in pairs(ss) do
        if sid2h[i] ~= nil then
            local h = sid2h[i]
            for j, img in pairs(image) do
                if img[i] == nil then
                    msgbox("error: couldn't clone monster from ", s.name,
                           " into ", ss[j].name,
                           " sector, image system is missing.")
                else
                    h.location = img[i].id
                    h.x = img[i].x
                    h.y = img[i].y
                    radd_ships({ h })
                end
            end
        end
    end
    -- print_ships("after cloning:\n")
end

function move_nebulas(padx, pady, nslice, dx, dy)
    local ee = rget_nebulas()
    local nee = tsize(ee)
    local nebdx, nebdy = 180, 180
    local g = get_game()
    local xmax, ymax = g.map_max_x - 2 * padx, g.map_max_y - 2 * pady
    local xc, yc = xmax / 2, ymax / 2

    function move_neb(i, x, y)
        ee[i].x = x + padx - nebdx / 2
        ee[i].y = y + pady - nebdy / 2
        rset_nebulas({ [i] = ee[i] })
    end

    function twin_neb(i)
        local x = random(xmax - nebdx) + nebdx / 2
        local y = random(ymax - nebdy) + nebdy / 2
        move_neb(i, x, y)
        move_neb(i + 1, xmax - x, ymax - y)
    end

    function xtwin_neb(i)
        local rx = random(xmax - nebdx) + nebdx / 2
        move_neb(i,     rx,        yc)
        move_neb(i + 1, xmax - rx, yc)
    end

    function ytwin_neb(i)
        local ry = random(ymax - nebdy) + nebdy / 2
        move_neb(i,     xc, ry)
        move_neb(i + 1, xc, ymax - ry)
    end

    if nee == 1 then
        move_neb(0, xc, yc)

    elseif nslice == 2 then
        if nee == 2 then
            twin_neb(0)
        elseif nee == 3 then
            move_neb(0, xc, yc)
            twin_neb(1)
        elseif nee == 4 then
            twin_neb(0)
            twin_neb(2)
        end

    elseif nslice == 4 then
        if nee == 2 then
            xtwin_neb(0)
        elseif nee == 3 then
            move_neb(0, xc, yc)
            xtwin_neb(1)
        elseif nee == 4 then
            xtwin_neb(0)
            ytwin_neb(2)
        end

    elseif nslice == 6 then
        if nee == 2 then
            move_neb(0, dx,     dy)
            move_neb(1, 2 * dx, dy)
        elseif nee == 3 then
            move_neb(0, dx / 2,          dy)
            move_neb(1, dx / 2 + dx,     dy)
            move_neb(2, dx / 2 + 2 * dx, dy)
        elseif nee == 4 then
            move_neb(0, dx,     dy / 2)
            move_neb(1, dx,     dy / 2)
            move_neb(2, 2 * dx, dy + dy / 2)
            move_neb(3, 2 * dx, dy + dy / 2)
        end

    elseif nslice == 8 then
        if nee == 2 then
            move_neb(0, 1 * dx, dy)
            move_neb(1, 3 * dx, dy)
        elseif nee == 3 then
            move_neb(0, 1 * dx, dy)
            move_neb(1, 2 * dx, dy)
            move_neb(2, 3 * dx, dy)
        elseif nee == 4 then
            move_neb(0, dx / 2 + 0 * dx, dy)
            move_neb(1, dx / 2 + 1 * dx, dy)
            move_neb(2, dx / 2 + 2 * dx, dy)
            move_neb(3, dx / 2 + 3 * dx, dy)
        end

    end
end

function showring()
    local bsr = get_conf("mapgen", "-bshowring")
    if bsr > 0 then
        local ss, pp, homes = get_homes()
        for i,s in pairs(ss) do
            s.visited = 0
        end
        for _,h in ipairs(homes) do
            for i,s in pairs(ss) do
                local dx = s.x - h.x
                local dy = s.y - h.y
                if dx * dx + dy * dy < bsr * bsr * 900 then
                    s.visited = s.visited | (1 << h.playerid)
                end
            end
        end
        set_stars(ss)
    end
end

function list_marooned()
    local ll = rget_leaders()
    local specs = {}
    for i, s in pairs(ss) do
        if s.special ~= 0 then
            specs[i] = s.special
        end
    end
    m = ""
    for i, l in pairs(ll) do
        if l.location ~= -1 then
            m = m .. tostring("@", ss[l.location].name, " ", l.name, " ", ss[l.location].special, "\n")
        end
    end
    msgbox(m)
end

function print_ships(msg)
    if msg ~= nil then print(msg) end
    local ss = get_stars()
    for i, h in pairs(rget_ships()) do
        local m = ""
        if h.status == "ship_is_at_star" then
            if ss[h.location] then
                m = tostring(" (", ss[h.location].name, ")")
            else
                m = "(?)"
            end
        end
        print("ship: ", h.design.name, " loc=", h.location, m, " status=", h.status);
    end
end

go()
