-- This is a player context script for MoO2 1.50 patch project.
--
-- This script shows you which techs your opponents have and you don't. It shows
-- only opponents in contact and only if /noreport option is off. It brings up a
-- message box for each opponent who has something.
--
-- To run the script press 3 in main screen (the file should be named MAIN3.LUA)

SORT_TECH_BY_RP_INSTEAD_OF_ALPHABETICALLY = true
SHOW_RP_COST_FOR_INDIVIDUAL_TECH = false

if get_conf("/noreport") == 1 then
    msgbox("/noreport option prevents tech check.")
    return
end

CUSTOM_ABBREVIATIONS = {
	['example term that is too long'] = 'shorter version'
}

ALIGN_LEFT = 'left'
ALIGN_MIDDLE = 'middle'
ALIGN_RIGHT = 'right'
PLAYER_COLUMN_ALIGNMENT = ALIGN_MIDDLE

ABBREVIATION_CHARACTER = '.'

--[[ NOTES: 
font index (FONT.LBX) is 3, distance between two characters is 2 pixels
@ is displayed as ~
^ is displayed as •
| is displayed empty 1 pixel wide (producing a 3 pixel gap between characters)
~ is displayed is displayed as 0 pixel wide (producing a 2 pixel gap between characters)
--]]

CHARACTER_TO_PIXEL_WIDTH = {
	[' '] = 3, ['!'] = 2, ['"'] = 6, ['#'] = 7, ['$'] = 6, ['%'] = 11,['&'] = 7, ["'"] = 4, ['('] = 3, [')'] = 3, ['*'] = 13,
	['+'] = 7, [','] = 3, ['-'] = 4, ['.'] = 2, ['/'] = 4, ['0'] = 6, ['1'] = 4, ['2'] = 6, ['3'] = 6, ['4'] = 7,
	['5'] = 6, ['6'] = 6, ['7'] = 6, ['8'] = 6, ['9'] = 6, [':'] = 2, [';'] = 3, ['<'] = 6, ['='] = 6, ['>'] = 6,
	['?'] = 6, ['@'] = 10,['A'] = 8, ['B'] = 8, ['C'] = 7, ['D'] = 8, ['E'] = 7, ['F'] = 7, ['G'] = 8, ['H'] = 8,
	['I'] = 2, ['J'] = 6, ['K'] = 8, ['L'] = 7, ['M'] = 10,['N'] = 8, ['O'] = 8, ['P'] = 8, ['Q'] = 8, ['R'] = 9,
	['S'] = 8, ['T'] = 8, ['U'] = 8, ['V'] = 8, ['W'] = 14,['X'] = 9, ['Y'] = 10,['Z'] = 9, ['['] = 3, ['\\'] = 4,
	[']'] = 3, ['^'] = 9, ['_'] = 8, ['`'] = 3, ['a'] = 6, ['b'] = 6, ['c'] = 6, ['d'] = 6, ['e'] = 6, ['f'] = 4,
	['g'] = 6, ['h'] = 6, ['i'] = 2, ['j'] = 3, ['k'] = 6, ['l'] = 2, ['m'] = 10,['n'] = 6, ['o'] = 6, ['p'] = 6,
	['q'] = 6, ['r'] = 5, ['s'] = 6, ['t'] = 4, ['u'] = 6, ['v'] = 8, ['w'] = 12,['x'] = 8, ['y'] = 8, ['z'] = 6,
	['{'] = 4, ['|'] = 1, ['}'] = 4, ['~'] = 0,
}
DISTANCE_BETWEEN_CHARACTERS = 2
MAXIMUM_WIDTH_FOR_MSGBOX = 339
ROW_SEPARATOR_CHARACTER = ' ' -- only a single character is supported yet, need to adjust ROW_SEPARATOR_WIDTH otherwise
ROW_SEPARATOR_WIDTH = DISTANCE_BETWEEN_CHARACTERS + CHARACTER_TO_PIXEL_WIDTH[ROW_SEPARATOR_CHARACTER] + DISTANCE_BETWEEN_CHARACTERS
ADD_TWO_PIXELS_CHARACTER = '~'
ADD_THREE_PIXELS_CHARACTER = '|'
ADD_FOUR_PIXELS_CHARACTER = '~~'
ADD_FIVE_PIXELS_CHARACTER = ' '
ABBREVIATION_CHARACTER_WIDTH = CHARACTER_TO_PIXEL_WIDTH[ABBREVIATION_CHARACTER]

function get_pixel_width_of_string(input)
	local str = tostring(input)
	if str == nil then
		return 0
	end
	local width = 0
	local gain = 0
    for i = 1, #str do
        gain = CHARACTER_TO_PIXEL_WIDTH[str:sub(i, i)]
		if gain ~= nil then
			width = width + gain + DISTANCE_BETWEEN_CHARACTERS
		end
    end
	if #str > 0 then -- first character produces no spacing-between-characters
		width = width - DISTANCE_BETWEEN_CHARACTERS
	end
	return width
end

function trim(str)
	for i = #str, 0, -1 do
		if str:sub(i, i) ~= ' ' then
			return str:sub(0, i)
		end
	end
end

function shorten_without_aligment(input, width_to_lose, desired_pixel_width)
	local str = tostring(input)
	if #str == 0 then
		return str
	end
    for i = #str, 0, -1 do
        loss = CHARACTER_TO_PIXEL_WIDTH[str:sub(i, i)]
		if loss ~= nil then
			width_to_lose = width_to_lose - loss - DISTANCE_BETWEEN_CHARACTERS
		end
		if width_to_lose + DISTANCE_BETWEEN_CHARACTERS + ABBREVIATION_CHARACTER_WIDTH <= 0 then
			str = str:sub(0 ,i - 1)
			break
		end
    end
	return trim(str) .. ABBREVIATION_CHARACTER
end

function fill_up_string_left_aligned(str, missing_width, desired_pixel_width)
	if missing_width == 2 then
		str = str .. ADD_TWO_PIXELS_CHARACTER
	elseif missing_width == 3 then
		str = str .. ADD_THREE_PIXELS_CHARACTER
	elseif missing_width == 4 then
		str = str .. ADD_FOUR_PIXELS_CHARACTER
	elseif missing_width >= 5 then
		local divisible_by_5_remainder = missing_width % 5
		if divisible_by_5_remainder == 4 then -- 9->5, 14->10, ...
			str = str .. ADD_FOUR_PIXELS_CHARACTER
			missing_width = missing_width - 4
		elseif divisible_by_5_remainder == 3 then -- 8->5, 13->10, ...
			str = str .. ADD_THREE_PIXELS_CHARACTER
			missing_width = missing_width - 3
		elseif divisible_by_5_remainder == 2 then -- 7->5, 12->10, ...
			str = str .. ADD_TWO_PIXELS_CHARACTER
			missing_width = missing_width - 2
		elseif divisible_by_5_remainder == 1 then -- 6->0, 11->5, ...
			str = str .. ADD_THREE_PIXELS_CHARACTER .. ADD_THREE_PIXELS_CHARACTER
			missing_width = missing_width - 6
		end
		if (desired_pixel_width - get_pixel_width_of_string(str)) % 5 ~= 0 then
			msgbox('unexpected remainder for str ' .. tostring(str) .. ': ' .. tostring(desired_pixel_width - get_pixel_width_of_string(str)) % 5) -- TODO: remove this after testing
		end
		str = str .. string.rep(ADD_FIVE_PIXELS_CHARACTER, (missing_width) / 5)
	end
	return str
end

function fill_up_string_right_aligned(str, missing_width, desired_pixel_width)
	if missing_width == 2 then
		str = ADD_TWO_PIXELS_CHARACTER .. str
	elseif missing_width == 3 then
		str = ADD_THREE_PIXELS_CHARACTER .. str
	elseif missing_width == 4 then
		str = ADD_FOUR_PIXELS_CHARACTER .. str
	elseif missing_width >= 5 then
		local divisible_by_5_remainder = missing_width % 5
		if divisible_by_5_remainder == 4 then -- 9->5, 14->10, ...
			str = ADD_FOUR_PIXELS_CHARACTER .. str
			missing_width = missing_width - 4
		elseif divisible_by_5_remainder == 3 then -- 8->5, 13->10, ...
			str = ADD_THREE_PIXELS_CHARACTER .. str
			missing_width = missing_width - 3
		elseif divisible_by_5_remainder == 2 then -- 7->5, 12->10, ...
			str = ADD_TWO_PIXELS_CHARACTER .. str
			missing_width = missing_width - 2
		elseif divisible_by_5_remainder == 1 then -- 6->0, 11->5, ...
			str = ADD_THREE_PIXELS_CHARACTER .. ADD_THREE_PIXELS_CHARACTER .. str
			missing_width = missing_width - 6
		end
		if (desired_pixel_width - get_pixel_width_of_string(str)) % 5 ~= 0 then
			msgbox('unexpected remainder for str ' .. tostring(str) .. ': ' .. tostring(desired_pixel_width - get_pixel_width_of_string(str)) % 5)
		end
		str = string.rep(ADD_FIVE_PIXELS_CHARACTER, (missing_width) / 5) .. str
	end
	return str
end

function fill_up_string_middle_aligned(str, missing_width, desired_pixel_width)
	if missing_width == 2 then
		str = ADD_TWO_PIXELS_CHARACTER .. str
	elseif missing_width == 3 then
		str = ADD_THREE_PIXELS_CHARACTER .. str	
	elseif missing_width == 4 then
		str = ADD_TWO_PIXELS_CHARACTER .. str .. ADD_TWO_PIXELS_CHARACTER
	elseif missing_width == 5 then
		str = ADD_THREE_PIXELS_CHARACTER .. str .. ADD_TWO_PIXELS_CHARACTER
	elseif missing_width == 6 then
		str = ADD_THREE_PIXELS_CHARACTER .. str .. ADD_THREE_PIXELS_CHARACTER
	elseif missing_width == 7 then
		str = ADD_FOUR_PIXELS_CHARACTER .. str .. ADD_THREE_PIXELS_CHARACTER
	elseif missing_width == 8 then
		str = ADD_FOUR_PIXELS_CHARACTER .. str .. ADD_FOUR_PIXELS_CHARACTER
	elseif missing_width == 9 then
		str = ADD_FIVE_PIXELS_CHARACTER .. str .. ADD_FOUR_PIXELS_CHARACTER
	elseif missing_width >= 10 then
		local divisible_by_10_remainder = missing_width % 10
		if divisible_by_10_remainder == 9 then
			str = ADD_FIVE_PIXELS_CHARACTER .. str .. ADD_FOUR_PIXELS_CHARACTER
			missing_width = missing_width - 9
		elseif divisible_by_10_remainder == 8 then
			str = ADD_FOUR_PIXELS_CHARACTER .. str .. ADD_FOUR_PIXELS_CHARACTER
			missing_width = missing_width - 8
		elseif divisible_by_10_remainder == 7 then
			str = ADD_FOUR_PIXELS_CHARACTER .. str .. ADD_THREE_PIXELS_CHARACTER
			missing_width = missing_width - 7
		elseif divisible_by_10_remainder == 6 then
			str = ADD_THREE_PIXELS_CHARACTER .. str .. ADD_THREE_PIXELS_CHARACTER
			missing_width = missing_width - 6
		elseif divisible_by_10_remainder == 5 then
			str = ADD_THREE_PIXELS_CHARACTER .. str .. ADD_TWO_PIXELS_CHARACTER
			missing_width = missing_width - 5
		elseif divisible_by_10_remainder == 4 then -- 14 -> add 2, 2 -> remaining 5, 5
			str = ADD_TWO_PIXELS_CHARACTER .. str .. ADD_TWO_PIXELS_CHARACTER
			missing_width = missing_width - 4
		elseif divisible_by_10_remainder == 3 then -- 13 -> add 7, 6 -> remaining 0, 0
			str = ADD_FOUR_PIXELS_CHARACTER .. ADD_THREE_PIXELS_CHARACTER .. str .. ADD_THREE_PIXELS_CHARACTER .. ADD_THREE_PIXELS_CHARACTER
			missing_width = missing_width - 13
		elseif divisible_by_10_remainder == 2 then -- 12 -> add 6, 6 -> remaining 0, 0
			str = ADD_THREE_PIXELS_CHARACTER .. ADD_THREE_PIXELS_CHARACTER .. str .. ADD_THREE_PIXELS_CHARACTER .. ADD_THREE_PIXELS_CHARACTER
			missing_width = missing_width - 12
		elseif divisible_by_10_remainder == 1 then -- 11 -> add 6, 5 -> remaining 0, 0
			str =  ADD_THREE_PIXELS_CHARACTER .. ADD_THREE_PIXELS_CHARACTER .. str .. ADD_FIVE_PIXELS_CHARACTER
			missing_width = missing_width - 11
		end
		if (desired_pixel_width - get_pixel_width_of_string(str)) % 10 ~= 0 then
			msgbox('unexpected remainder for str ' .. tostring(str) .. ': ' .. tostring(desired_pixel_width - get_pixel_width_of_string(str)) % 10)
		end
		local to_add = string.rep(ADD_FIVE_PIXELS_CHARACTER, (missing_width) / 10)
		str = to_add .. str .. to_add
	end
	return str
end

function align(input, desired_pixel_width, alignment_type)
	if alignment_type ~= ALIGN_LEFT and alignment_type ~= ALIGN_RIGHT and alignment_type ~= ALIGN_MIDDLE then
		msgbox("ERROR: Unknown alignment type: " .. tostring(alignment_type))
		return tostring(input)
	end
	if desired_pixel_width < DISTANCE_BETWEEN_CHARACTERS then
		-- attempting this would lead to an infinite while loop!
		msgbox("ERROR: cannot set pixel width to less than " .. tostring(DISTANCE_BETWEEN_CHARACTERS) .. " for\n'" .. tostring(input) .. "'")
		return ''
	end
	local str = tostring(input)
	if str == nil then
		str = '~'
	end
	local current_width = get_pixel_width_of_string(str)
	local missing_width = desired_pixel_width - current_width
	-- allow a hierarchy of abbreviations, because why not
	while CUSTOM_ABBREVIATIONS[str] ~= nil and missing_width < 0 do
		str = CUSTOM_ABBREVIATIONS[str]		
		current_width = get_pixel_width_of_string(str)	
		missing_width = desired_pixel_width - current_width			
	end
	if missing_width < 0 then
		str = align(shorten_without_aligment(str, -missing_width, desired_pixel_width), desired_pixel_width, alignment_type)
	elseif alignment_type == ALIGN_LEFT then
		str = fill_up_string_left_aligned(str, missing_width, desired_pixel_width)
	elseif alignment_type == ALIGN_RIGHT then
		str = fill_up_string_right_aligned(str, missing_width, desired_pixel_width)
	elseif alignment_type == ALIGN_MIDDLE then
		str = fill_up_string_middle_aligned(str, missing_width, desired_pixel_width)
	end
	collectgarbage()
	return str
end


local players = get_players()
local current_player = players[get_current_player_id()]

local function orbit_as_int(planet_id, star_orbits)
	local orbit_int = 0
	for i, planet_of_star_id in pairs(star_orbits) do
		if planet_of_star_id == planet_id then
			break
		end
		orbit_int = orbit_int + 1
	end
	return orbit_int
end

local function orbit_to_roman(orbit_int)
	-- there can be gaps in star-orbits (e.g. 0, 2, 3, 4) so find it with planet.id instead of planet.orbit
	local orbit_str = ''
	if orbit_int == 0 then
		orbit_str = 'I'
	elseif orbit_int == 1 then
		orbit_str = 'II'
	elseif orbit_int == 2 then
		orbit_str = 'III'
	elseif orbit_int == 3 then
		orbit_str = 'IV'
	elseif orbit_int == 4 then
		orbit_str = 'V'
	else
		msgbox('ERROR: invalid  orbit ID ' .. tostring(orbit_int) .. ', expected between 0 and 4')
	end
	return orbit_str
end

local function sort_by_destination_then_eta(a, b)
	if a.destination_name < b.destination_name then
		return true
	elseif a.destination_name == b.destination_name then
		return a.eta < b.eta
	else
		return false
	end
end

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

-- note: lua's string.gsub, string.gmatch and string.find methods are not supported
local function format_tech_string(str, custom_tech_names)
    if custom_tech_names[str] ~= nil then
        return custom_tech_names[str]
    end
    if str == 'anti_missile_rocket' then
        return 'Anti-Missile Rockets'
    elseif str == 'anti_matter_bomb' then
        return 'Anti-Matter Bomb'
    elseif str == 'anti_matter_drive' then
        return 'Anti-Matter Drive'
    elseif str == 'anti_matter_torpedo' then
        return 'Anti-Matter Torpedo'
    elseif str == 'anti_grav_harness' then
        return 'Anti-Grav Harness'
    elseif str == 'bio_terminator' then
        return 'Bio-Terminator'
    elseif str == 'class_iii_shield' then
        return 'Class III Shield'
    elseif str == 'class_vii_shield' then
        return 'Class VII Shield'
    elseif str == 'cyber_security_link' then
        return 'Cyber-Security Link'
    elseif str == 'hyper_x_capacitors' then
        return 'Hyper-X Capacitors'
    elseif str == 'multi_wave_ecm_jammer' then
        return 'Multi-Wave Ecm Jammer'
    elseif str == 'robo_miners' then
        return 'Robo-Miners'
    elseif str == 'sub_space_communications' then
        return 'Sub-Space Communications'
    end
    local formated_string = str:sub(1,1):upper()
    local next_char_uppercase = false
    for i = 2, #str do
        if str:sub(i,i) == '_' then
            formated_string = formated_string .. ' '
            next_char_uppercase = true
        else
            if next_char_uppercase then
                formated_string = formated_string .. str:sub(i,i):upper()
                next_char_uppercase = false
            else
                formated_string = formated_string .. str:sub(i,i)
            end
        end
    end
    return formated_string
end

local function separate_thousands(integer)
    input_string = tostring(integer)
    output_string = ""
    for i = #input_string, 1, -1 do
        output_string = input_string:sub(i,i) .. output_string
        if (#input_string - i + 1) % 3 == 0 and i ~= 1 then
            output_string = "," .. output_string
        end
    end
    return output_string
end

local function beautify(str, rp_cost, custom_tech_names, highest_rp_cost_width)
    if SHOW_RP_COST_FOR_INDIVIDUAL_TECH == true then
        return align(separate_thousands(rp_cost), highest_rp_cost_width + 2, ALIGN_RIGHT) .. ' - ' .. format_tech_string(str, custom_tech_names)
    else
        return format_tech_string(str, custom_tech_names)
    end
end

local function sort_tech_by_rp(a, b)
    local cost_a = fields[techs[a].field].cost
    local cost_b = fields[techs[b].field].cost
    if cost_a > cost_b then
        return true
    elseif cost_a == cost_b then
        return a < b
    else
        return false
    end
end

c = get_current_player_id()
ct = get_player_techs(c)
techs = rget_techs()
fields = rget_fields()
all_custom_tech_names = get_conf("tech_name")
custom_tech_names = {}
for original_name, custom_name in pairs(all_custom_tech_names) do
    if custom_name ~= 0 then
        custom_tech_names[original_name] = custom_name
    end
end
shown = 0
for i,p in pairs(get_players()) do
    if i ~= c and p ~= nil then
        local out = ""
        local tt = get_player_techs(i)
        local found = {}
        local rp_costs = {}
        if tt ~= nil then
            for t,status in pairs(tt) do
                if status == 3 and ct[t] ~= 3 then
                    table.insert(found, t)
                end
            end
        end
        if #found > 0 then
            local summed_rp_value = 0
            shown = 1
            if SORT_TECH_BY_RP_INSTEAD_OF_ALPHABETICALLY then
                table.sort(found, sort_tech_by_rp)
            else
                table.sort(found)
            end
            local step = 20
            for j = 1, #found, step do
                local highest_rp_cost_width = 0
                for k = j, j + step -1 do
                    if k <= #found then
                        highest_rp_cost_width = get_maximum_value(highest_rp_cost_width, get_pixel_width_of_string(separate_thousands(fields[techs[found[k]].field].cost)))
                    end
                end
                local o = p.race .. " tech unknown to us:\n\n"
                for k = j, j + step -1 do
                    if k <= #found then
                        rp_cost = fields[techs[found[k]].field].cost
                        o = o .. beautify(found[k], rp_cost, custom_tech_names, highest_rp_cost_width) .. "\n"
                        summed_rp_value = summed_rp_value + rp_cost
                    end
                end
                if j / step + 1 == #found / step + 1 or #found <= step then
                    o = o .. "\nTotal: " .. #found .. " techs, worth " .. separate_thousands(summed_rp_value) .. " RP\n"
                end
                if #found > step then
                    o = o .. "\n(page " .. (j / step + 1) .. "/" .. (#found / step + 1) .. ")"
                end
                msgbox(o)
            end
        end
        collectgarbage()
    end
end
if shown == 0 then
    msgbox("No race in contact has tech unknown to us.")
end
