local mod = {} -- will be filled with public functions

local settings = require('common.settings')
local colors = settings.colors
local fonts = settings.fonts
local players = require('common.players')

local map_pointcount = 560
local map_points = table.new(map_pointcount, 0)
local drivers_laps = table.new(50, 0)
local rotator = quat(0, 0, 0, 1)
local player_rotator = quat()

local map_center = vec3()
local is_clockwise = false

local min_pos = vec2(99999999999, 9999999999)
local max_pos = vec2(-99999999999, -9999999999)
local canvas = nil
local player_canvas = nil;
local multiclass_canvas = nil
local final_canvas = nil

local min_relative_zoom = 1.8
local relative_zoom = 1
local relative_track_width_zoom = 1

local abs_rel_lerp_t = 0              -- TAG: MakeTheLoopQuickAgain, lerp_t of the animation between relative and absolute (0 = abs, 1 = rel)
local player_norm = vec2()            -- TAG: MakeTheLoopQuickAgain, normalized screen pos of the player
local canvas_mapsize = 0              -- TAG: MakeTheLoopQuickAgain
local mapvec = vec2()                 -- TAG: MakeTheLoopQuickAgain
local sectors_table = table.new(3, 0) -- TAG: MakeTheLoopQuickAgain

local canvas_size = 500
local canvas_old_size = 310 -- NOTE(cogno): do not change this! TAG: CanvasResScaling
local map_padding = 40

local missing_data_timer = -10
local showing_missing_data_timer = -10
local is_missing_data_shown = true

-- TAG: CanvasResScaling, we wanted to improve graphics,
-- to do so we simply changed the canvas size. Resolution improved but many things
-- became much smaller, so we decided (rightfully) to scale them up.
-- To do so as easily as possible we resize depending on the new canvas size.
-- If the old canvas size was 100 and the new 200, each element would need to double,
-- aka increase by canvas_size / canvas_old_size
--                   Cogno 2024/07/16

local DriverType = {
    -- we don't need the player, he's so special we don't need to check anything
    Leader = 1,
    Normal = 2,
    LappedByPlayer = 3,
    HasLappedPlayer = 4,
}

-- runs even if app is hidden, use it only if needed
function mod.update()
end

local function world_point_3d_remap(to_remap)
    return vec2(
        settings.remap(to_remap.x, min_pos.x, max_pos.x, 0, 1),
        settings.remap(to_remap.z, min_pos.y, max_pos.y, 0, 1)
    )
end

local function rotate_mappos(mappos)
    local offset = mappos - map_center
    local rotated_offset = offset:rotate(rotator):rotate(player_rotator)
    return map_center + rotated_offset
end

local function center_to_map(normalized, map_screen_center)
    -- normalized goes from 0,0 (map screen bottom left) to 1,1 (map screen top right)
    -- at least I think, it might have the y axis flipped, I don't remember

    -- TAG: MakeTheLoopQuickAgain, since this is a hot loop (meaning it's done A LOT),
    -- we want to keep this QUICK. Some info is common for the entire frame,
    -- so instead of calculating it every time we can skip it and only do it sometimes.
    --                         Cogno 2024/06/11

    -- calculate screen pos if we show the whole map, if it's time to show it return immediately
    local corner_tl = map_screen_center - mapvec / 2
    local abs_x = corner_tl.x + normalized.x * mapvec.x
    local abs_y = corner_tl.y + normalized.y * mapvec.y
    if abs_rel_lerp_t <= 0 then
        return abs_x, abs_y
    end

    -- calculate screen pos if we show the map in relative mode, if it's time to show return it
    local relative_norm = (normalized - player_norm) * relative_zoom
    local rela_x = map_screen_center.x + relative_norm.x * mapvec.x
    local rela_y = map_screen_center.y + relative_norm.y * mapvec.y
    if abs_rel_lerp_t >= 1 then
        return rela_x, rela_y
    end

    -- we're animating between them, lerp the values!
    local out_x = settings.remap(abs_rel_lerp_t, 0, 1, abs_x, rela_x)
    local out_y = settings.remap(abs_rel_lerp_t, 0, 1, abs_y, rela_y)
    return out_x, out_y
end

local function draw_map_path(center, map_start, map_end)
    --if table.nkeys(map_points) == 0 then return end

    ui.pathClear()
    local table_len = table.nkeys(map_points)
    local idx_start = math.floor(map_start * table_len)
    local idx_end = math.ceil(map_end * table_len)
    local rmp = settings.remap     -- TAG: MakeTheLoopQuickAgain
    local pathline = ui.pathLineTo -- TAG: MakeTheLoopQuickAgain
    local to_avoid_gc1 = vec3()    -- TAG: GarbageSucks, with these 2 and LuaJIT we skip 13KB per frame
    local to_avoid_gc2 = vec2()    -- TAG: GarbageSucks, with these 2 and LuaJIT we skip 13KB per frame
    -- NOTE(cogno): to avoid creating garbage we avoid using vectors as much as possible
    for i = idx_start, idx_end - 1 do
        local point_curr = map_points[i]
        to_avoid_gc1:set(point_curr.x, 0, point_curr.y)
        to_avoid_gc1.x = to_avoid_gc1.x - map_center.x
        to_avoid_gc1.y = to_avoid_gc1.y - map_center.y
        to_avoid_gc1.z = to_avoid_gc1.z - map_center.z
        local rotated_offset = to_avoid_gc1:rotate(player_rotator) -- the map is prerotated, so we can skip that one
        local rotated_x = map_center.x + rotated_offset.x
        local rotated_z = map_center.z + rotated_offset.z
        to_avoid_gc2:set(
            rmp(rotated_x, min_pos.x, max_pos.x, 0, 1),
            rmp(rotated_z, min_pos.y, max_pos.y, 0, 1)
        )
        local px, py = center_to_map(to_avoid_gc2, center)
        to_avoid_gc2.x = px
        to_avoid_gc2.y = py
        pathline(to_avoid_gc2)
    end
end

local function draw_normal_behaviour(lerp_t, is_in_pitlane, center, size, bg_color, radius, is_outlined, fontsize,
                                     string_color, show_text, car_racepos, wheels_outside, car_index)
    -- normal behaviour is:
    -- if not moving animate until small
    -- if in pitlane and moving then square
    -- if not in pitlane and moving then big circle with number
    -- if outside track also add a yellow ring (except player but we don't draw him here)

    -- lerp_t 0 = pallino grande / quadrato, lerp_t 1 = pallino piccolo
    if lerp_t == 0 then
        -- when we are here we have either big ones or square ones
        if is_in_pitlane then
            -- square, don't outline them
            if is_outlined then return end

            ui.drawRectFilled(center - size, center + size, bg_color, radius)
        else
            -- big, always outline them
            if not is_outlined then return end

            ui.drawCircleFilled(center, radius, bg_color, 20) -- HIGH RES
        end

        if wheels_outside >= 4 and not is_in_pitlane then
            ui.drawCircle(center, radius + 1, colors.TEXT_YELLOW, 12, 2)
        end

        if show_text then
            local number_size = ui.measureDWriteText(car_racepos, fontsize)
            ui.dwriteDrawText(car_racepos, fontsize, center - number_size / 2, string_color)
        end
    else
        -- here we have all stuff animating and small, never draw outlines
        if is_outlined then return end

        ui.drawRectFilled(center - size, center + size, bg_color, radius)

        if wheels_outside >= 4 and not is_in_pitlane then
            ui.drawCircle(center, radius + 1, colors.TEXT_YELLOW, 12, 2)
        end
    end
end

local function draw_driver_arc(lb_index, player_racepos, radius_big, is_in_pitlane, car_index, leader_drawn, arc_color,
                               center, car_direction)
    local player_move_data = players.get_player_movement_info(car_index)
    local elapsed = Time
    if player_move_data ~= nil then elapsed = elapsed - player_move_data.movement_change_time end

    local multiclass_elapsed = Time - Leaderboard_Multiclass_EnabledTime
    local mc_lerp_t = settings.anim_lerp(multiclass_elapsed, Leaderboard_Multiclass_Enabled, 0.5)
    local max_angle = mc_lerp_t * math.pi / 2

    local focused, changed = players.get_focused_car()
    local draw_arc_for_player = car_index == focused and Map_Relative == false
    local draw_arc_for_others = car_index ~= focused and elapsed <= 3 and not is_in_pitlane

    if Map_LowProfile then
        local car_racepos = 0
        if Leaderboard_Multiclass_Enabled and Leaderboard_Multiclass_FilterEnabled then
            car_racepos = lb_index + 1
        else
            car_racepos = players.get_racepos(lb_index)
        end
        local racepos_dist = math.abs(car_racepos - player_racepos)
        if racepos_dist <= 2 or leader_drawn == false then
            -- time to draw the arc, do it only if we can, skip the rest
            if draw_arc_for_player or draw_arc_for_others then
                local rotated_offset = car_direction:rotate(rotator):rotate(player_rotator)
                local direction_angle = math.atan2(rotated_offset.z, rotated_offset.x)
                ui.pathClear()
                ui.pathArcTo(center, radius_big + 1, direction_angle + max_angle, direction_angle - max_angle, 10);
                ui.pathStroke(arc_color, false, 8.3)
            end
        end
    else
        -- time to draw the arc, do it only if we can, skip the rest
        if draw_arc_for_player or draw_arc_for_others then
            local rotated_offset = car_direction:rotate(rotator):rotate(player_rotator)
            local direction_angle = math.atan2(rotated_offset.z, rotated_offset.x)
            ui.pathClear()
            ui.pathArcTo(center, radius_big + 1, direction_angle + max_angle, direction_angle - max_angle, 10);
            ui.pathStroke(arc_color, false, 8.3)
        end
    end
end


local function draw_driver_dot(lb_index, is_outlined, dot_kind, player_racepos, force_small, map_center, radius_big,
                               radius_mid, radius_small, is_in_pitlane, car_index, wheels_outside, car_position)
    local bg_color = colors.BLACK      -- if you see this there's a problem!
    local string_color = colors.PURPLE -- if you see this there's a problem!
    local show_text = true
    if dot_kind == DriverType.Leader then
        bg_color = colors.PURPLE
        string_color = colors.WHITE
    elseif dot_kind == DriverType.Normal then
        bg_color = colors.LIGHT_GREEN
        string_color = colors.DARK_BG
    elseif dot_kind == DriverType.HasLappedPlayer then
        bg_color = rgbm(0 / 255, 156 / 255, 255 / 255, 1)
        string_color = colors.WHITE
    elseif dot_kind == DriverType.LappedByPlayer then
        bg_color = rgbm(0 / 255, 94 / 255, 159 / 255, 1)
        string_color = colors.WHITE
        show_text = false -- we don't need to!
    end

    local player_move_data = players.get_player_movement_info(car_index)
    local elapsed = Time
    if player_move_data ~= nil then elapsed = elapsed - player_move_data.movement_change_time end
    local lerp_t = math.clamp(settings.remap(elapsed, 3, 3.1, 0, 1), 0, 1)
    local radius = math.lerp(radius_big, radius_small, lerp_t)
    local size   = math.lerp(radius_big, radius_small, lerp_t)
    if is_in_pitlane and lerp_t <= 0 then
        radius = radius_mid / 2
        size   = radius_mid
    end

    local car_worldpos = rotate_mappos(car_position)
    local player_normalized_trackpos = world_point_3d_remap(car_worldpos)
    local adjusted_pos_x, adjusted_pos_y = center_to_map(player_normalized_trackpos, map_center)
    local center = vec2(adjusted_pos_x, adjusted_pos_y)
    if force_small then
        if not is_outlined then
            ui.drawRectFilled(center - radius_small, center + radius_small, bg_color, radius)
        end
        return
    end

    local car_racepos = 0
    if Leaderboard_Multiclass_Enabled and Leaderboard_Multiclass_FilterEnabled then
        car_racepos = lb_index + 1
    else
        car_racepos = players.get_racepos(lb_index)
    end
    local fontsize = settings.fontsize(9) * canvas_size / canvas_old_size -- TAG: CanvasResScaling
    if Map_LowProfile then
        local racepos_dist = math.abs(car_racepos - player_racepos)
        if racepos_dist <= 2 or dot_kind == DriverType.Leader then
            draw_normal_behaviour(lerp_t, is_in_pitlane, center, size, bg_color, radius, is_outlined, fontsize,
                string_color, show_text, car_racepos, wheels_outside, car_index)
        else
            -- too far, only the small ones here
            if not is_outlined then
                ui.drawRectFilled(center - radius_small, center + radius_small, bg_color, radius)
            end
        end
    else
        draw_normal_behaviour(lerp_t, is_in_pitlane, center, size, bg_color, radius, is_outlined, fontsize, string_color,
            show_text, car_racepos, wheels_outside, car_index)
    end
end

local on_show_animation_start = 0
local is_showing = true
local is_paused = false
function mod.on_open()
    if is_paused == false then
        on_show_animation_start = Time
        is_showing = true
        missing_data_timer = Time
        is_missing_data_shown = true
    end
    is_paused = false
end

function mod.on_close()
    is_paused = ac.getSim().isPaused
    if is_paused == false then
        is_showing = false
    end
end

local function get_flag_color_and_width(flagtype)
    local map_color = colors.WHITE
    local width = 10
    if flagtype == nil then
        map_color = colors.WHITE
    elseif flagtype == ac.FlagType.Caution then
        map_color = colors.YELLOW
        width = 14
    elseif flagtype == ac.FlagType.Stop then
        map_color = colors.BLACK
        width = 14
    elseif flagtype == ac.FlagType.ReturnToPits then
        map_color = colors.RED
        width = 14
    elseif flagtype == ac.FlagType.FasterCar then
        map_color = colors.BLUE
        width = 14
    elseif flagtype == ac.FlagType.OneLapLeft then
        map_color = colors.WHITE
    elseif flagtype == ac.FlagType.Finished then
        map_color = colors.WHITE
    end
    return map_color, width
end

function mod.on_session_start()
    table.clear(drivers_laps)
end

local map_anim_start_time = Time
local old_flag_type = nil
local old_map_color = colors.WHITE
local old_width = 0

local function draw_arcs()
    local draw_center           = ui.windowSize() / 2
    local map_center            = draw_center
    local players_circle_radius = 11 * canvas_size / canvas_old_size     -- TAG: CanvasResScaling

    local leaderboard           = players.get_leaderboard()
    if Leaderboard_Multiclass_Enabled and Leaderboard_Multiclass_FilterEnabled then
        leaderboard = players.get_multiclass_leaderboard()
    end

    local player_racepos = 0
    if Leaderboard_Multiclass_Enabled and Leaderboard_Multiclass_FilterEnabled then
        local player_lb_index = players.get_player_multiclass_leaderboard_index() -- PERF(cogno): no need to calculate it here, it changes only across frames
        player_racepos = player_lb_index + 1
    else
        local player_lb_index = players.get_player_leaderboard_index() -- PERF(cogno): no need to calculate it here, it changes only across frames
        player_racepos        = players.get_racepos(player_lb_index)   -- PERF(cogno): no need to calculate it here,   it changes only across frames
    end

    local leader_drawn = false
    for i = 0, #leaderboard - 1 do
        local car_info = leaderboard[i].car
        local multiclass_info = players.get_multiclass_info(car_info.index)
        if car_info.isConnected and multiclass_info ~= nil then
            local car_worldpos = rotate_mappos(car_info.position)
            local player_normalized_trackpos = world_point_3d_remap(car_worldpos)
            local adjusted_pos_x, adjusted_pos_y = center_to_map(player_normalized_trackpos, map_center)
            local center = vec2(adjusted_pos_x, adjusted_pos_y)
            local lookdir = vec3(car_info.look.x, car_info.look.y, car_info.look.z)
            draw_driver_arc(i, player_racepos, players_circle_radius, car_info.isInPitlane, car_info.index, leader_drawn,
                multiclass_info.color, center, lookdir)
        end
        if car_info.isConnected then leader_drawn = true end
    end
end

local function get_lapcount(car_index, player_spline, driver_spline)
    local driver_lapcount = players.get_lapcount(car_index)
    local splits = players.get_sector_splits()
    local finish_line = splits[0]

    if drivers_laps[car_index] == nil or driver_spline > finish_line then
        drivers_laps[car_index] = driver_lapcount
    end

    if driver_spline < finish_line and drivers_laps[car_index] ~= driver_lapcount then
        driver_lapcount = driver_lapcount - 1
    end

    if driver_spline < finish_line then driver_lapcount = driver_lapcount + 1 end
    if player_spline < finish_line then driver_lapcount = driver_lapcount - 1 end
    
    return driver_lapcount
end

local function players_render(is_outlined)
    local draw_center               = ui.windowSize() / 2
    local map_center                = draw_center
    local players_circle_radius     = 11 * canvas_size / canvas_old_size -- TAG: CanvasResScaling
    local players_circle_radius_min = 6 * canvas_size / canvas_old_size  -- TAG: CanvasResScaling
    local players_stopped_radius    = 4 * canvas_size / canvas_old_size  -- TAG: CanvasResScaling

    local leaderboard               = players.get_leaderboard()
    local other_classes_leaderboard = players.get_other_multiclass_leaderboard()
    if Leaderboard_Multiclass_Enabled and Leaderboard_Multiclass_FilterEnabled then
        leaderboard = players.get_multiclass_leaderboard()
    end

    ui.pushDWriteFont(fonts.archivo_medium)
    local focused, changed = players.get_focused_car()
    local my_car_info = ac.getCar(focused)
    local player_lapcount = players.get_lapcount(focused)
    local player_spline_total = player_lapcount + my_car_info.splinePosition

    local player_racepos = 0
    if Leaderboard_Multiclass_Enabled and Leaderboard_Multiclass_FilterEnabled then
        local player_lb_index = players.get_player_multiclass_leaderboard_index() -- PERF(cogno): no need to calculate it here, it changes only across frames
        player_racepos = player_lb_index + 1
    else
        local player_lb_index = players.get_player_leaderboard_index() -- PERF(cogno): no need to calculate it here, it changes only across frames
        player_racepos        = players.get_racepos(player_lb_index)   -- PERF(cogno): no need to calculate it here, it changes only across frames
    end

    local session = players.get_current_session()
    if session.type ~= ac.SessionType.Practice and session.type ~= ac.SessionType.Qualify then
        -- first draw lapped people
        for i = 0, #leaderboard - 1 do
            local car_info = leaderboard[i].car
            if car_info.isConnected then
                local driver_lapcount = get_lapcount(car_info.index, my_car_info.splinePosition, car_info.splinePosition)
                local car_spline_total = driver_lapcount + car_info.splinePosition
                local spline_delta = player_spline_total - car_spline_total
                if (math.abs(spline_delta) >= 1) and car_info.index ~= focused then
                    local dot_kind = nil
                    if spline_delta <= -1 then
                        dot_kind = DriverType.HasLappedPlayer
                    else
                        dot_kind = DriverType.LappedByPlayer
                    end

                    draw_driver_dot(i, is_outlined, dot_kind, player_racepos, false, map_center, players_circle_radius,
                        players_circle_radius_min, players_stopped_radius, car_info.isInPitlane, car_info.index,
                        car_info.wheelsOutside, car_info.position)
                end
            end
        end

        if Leaderboard_Multiclass_Enabled and Leaderboard_Multiclass_FilterEnabled then
            -- first draw lapped people
            for i = 0, #other_classes_leaderboard - 1 do
                local car_info = other_classes_leaderboard[i].car
                if car_info.isConnected then
                    local driver_lapcount = get_lapcount(car_info.index, my_car_info.splinePosition, car_info.splinePosition)
                    local car_spline_total = driver_lapcount + car_info.splinePosition
                    local spline_delta = player_spline_total - car_spline_total
                    if (math.abs(spline_delta) >= 1) and car_info.index ~= focused then
                        local dot_kind = nil
                        if spline_delta <= -1 then
                            dot_kind = DriverType.HasLappedPlayer
                        else
                            dot_kind = DriverType.LappedByPlayer
                        end

                        draw_driver_dot(i, is_outlined, dot_kind, player_racepos, true, map_center, players_circle_radius,
                            players_circle_radius_min, players_stopped_radius, car_info.isInPitlane, car_info.index,
                            car_info.wheelsOutside, car_info.position)
                    end
                end
            end
        end
    end

    -- then normal people!
    for i = 0, #leaderboard - 1 do
        local car_info = leaderboard[i].car
        if car_info.isConnected == true and car_info.index ~= focused then
            local car_racepos = players.get_racepos(i)
            local driver_lapcount = get_lapcount(car_info.index, my_car_info.splinePosition, car_info.splinePosition)
            local car_spline_total = driver_lapcount + car_info.splinePosition
            local spline_delta = player_spline_total - car_spline_total
            local to_draw = (math.abs(spline_delta) <= 1) and car_info.index ~= 0 and car_racepos ~= 1
            if session.type == ac.SessionType.Practice or session.type == ac.SessionType.Qualify then
                to_draw = true
            end
            if to_draw then
                draw_driver_dot(i, is_outlined, DriverType.Normal, player_racepos, false, map_center,
                    players_circle_radius, players_circle_radius_min, players_stopped_radius, car_info.isInPitlane,
                    car_info.index, car_info.wheelsOutside, car_info.position)
            end
        end
    end

    if Leaderboard_Multiclass_Enabled and Leaderboard_Multiclass_FilterEnabled then
        -- then normal people!
        for i = 0, #other_classes_leaderboard - 1 do
            local car_info = other_classes_leaderboard[i].car
            if car_info.isConnected == true and car_info.index ~= focused then
                local car_racepos = players.get_racepos(i)
                local driver_lapcount = get_lapcount(car_info.index, my_car_info.splinePosition, car_info.splinePosition)
                local car_spline_total = driver_lapcount + car_info.splinePosition
                local spline_delta = player_spline_total - car_spline_total
                local to_draw = (math.abs(spline_delta) <= 1) and car_info.index ~= 0 and car_racepos ~= 1
                if session.type == ac.SessionType.Practice or session.type == ac.SessionType.Qualify then
                    to_draw = true
                end
                if to_draw then
                    draw_driver_dot(i, is_outlined, DriverType.Normal, player_racepos, true, map_center,
                        players_circle_radius, players_circle_radius_min, players_stopped_radius, car_info.isInPitlane,
                        car_info.index, car_info.wheelsOutside, car_info.position)
                end
            end
        end
    end

    -- then the first one of the race
    ui.pushDWriteFont(fonts.archivo_bold) -- both first person and player are important
    for i = 0, #leaderboard - 1 do
        local car_info = leaderboard[i].car
        if car_info.index == focused then break end -- the player is the first, no need to draw it
        if car_info.isConnected == true then -- find the first real leader, then draw him
            draw_driver_dot(i, is_outlined, DriverType.Leader, player_racepos, false, map_center, players_circle_radius,
                players_circle_radius_min, players_stopped_radius, car_info.isInPitlane, car_info.index,
                car_info.wheelsOutside, car_info.position)
            break -- no need to continue
        end
    end

    -- we never do the player here because he's always big, so he always needs the outline
end


local function player_canvas_update()
    local draw_center = ui.windowSize() / 2
    local map_center  = draw_center
    local leaderboard = players.get_leaderboard()
    if Leaderboard_Multiclass_Enabled and Leaderboard_Multiclass_FilterEnabled then
        leaderboard = players.get_multiclass_leaderboard()
    end
    local big_fontsize = settings.fontsize(10) * canvas_size / canvas_old_size -- TAG: CanvasResScaling
    ui.pushDWriteFont(fonts.archivo_medium)

    players_render(true)
    local focused, changed = players.get_focused_car()

    -- then the player!
    local players_main_size = 22 * canvas_size / canvas_old_size -- TAG: CanvasResScaling
    for i = 0, #leaderboard - 1 do
        local car_info = leaderboard[i].car
        if car_info.index == focused then
            local car_worldpos = rotate_mappos(car_info.position)
            local player_normalized_trackpos = world_point_3d_remap(car_worldpos)
            local adjusted_x, adjusted_y = center_to_map(player_normalized_trackpos, map_center)
            local adjusted_pos = vec2(adjusted_x, adjusted_y)
            local car_racepos = 0
            if Leaderboard_Multiclass_Enabled and Leaderboard_Multiclass_FilterEnabled then
                car_racepos = i + 1
            else
                car_racepos = players.get_racepos(i)
            end
            local color = colors.TEXT_YELLOW
            if Map_Relative then
                local r = players_main_size * 0.45
                ui.drawImage(
                    settings.get_asset("cursor"),
                    adjusted_pos - r,
                    adjusted_pos + r,
                    color
                )
            else
                ui.drawCircleFilled(adjusted_pos, players_main_size / 2, color, 20)
                local number_size = ui.measureDWriteText(car_racepos, big_fontsize)
                ui.dwriteDrawText(car_racepos, big_fontsize, adjusted_pos - number_size / 2, colors.DARK_BG)
            end

            -- NOTE(cogno): no need to draw the yellow circle for yourself, we don't care!
            break -- no need to continue...
        end
    end
    ui.popDWriteFont()
    ui.popDWriteFont()
end

local function map_canvas_update()
    local zoom                 = settings.remap(abs_rel_lerp_t, 0, 1, 1, relative_zoom)
    local width_zoom           = settings.remap(abs_rel_lerp_t, 0, 1, 1, relative_track_width_zoom)
    local draw_center          = ui.windowSize() / 2
    local map_center           = draw_center

    local sim_info             = ac.getSim()
    local flagtype             = sim_info.raceFlagType
    local map_color, new_width = get_flag_color_and_width(flagtype)
    if old_flag_type == nil or old_flag_type ~= flagtype then
        old_map_color, old_width = get_flag_color_and_width(old_flag_type)
        old_flag_type = flagtype
        map_anim_start_time = Time
    end
    local w1 = new_width * canvas_size / canvas_old_size -- TAG: CanvasResScaling
    local w2 = old_width * canvas_size / canvas_old_size -- TAG: CanvasResScaling
    local w3 = 6 * canvas_size / canvas_old_size         -- TAG: CanvasResScaling

    -- DEBUG(cogno): map area
    -- ui.drawRect(map_center - mapsize / 2, map_center + mapsize / 2, colors.BLUE)

    local start_p = ac.trackCoordinateToWorld(vec3(0, 0, 0))
    local end_p   = ac.trackCoordinateToWorld(vec3(0, 0, 0.99))
    local dist = math.distance(start_p, end_p)
    local is_connected = true
    if dist > 100 then 
        is_connected = false
    end

    local anim_t = math.clamp(Time - map_anim_start_time, 0, 0.5) * 2
    local can_loop = false
    if anim_t >= 1 then can_loop = true end

    draw_map_path(map_center, 0, anim_t)
    ui.pathStroke(map_color, can_loop and is_connected, w1 * width_zoom)
    if anim_t < 1 then
        draw_map_path(map_center, anim_t, 1)
        ui.pathStroke(old_map_color, false, w2 * width_zoom)
    end
    draw_map_path(map_center, 0, 1)
    ui.pathStroke(rgbm(43 / 255, 43 / 255, 43 / 255, 1), is_connected, w3 * width_zoom)

    -- DEBUG(cogno): to see how many points we draw
    if false then
        local map_points_len = table.nkeys(map_points)
        local idx_start = math.floor(0 * map_points_len)
        local idx_end = math.ceil(1 * map_points_len)
        for i = idx_start, idx_end - 1 do
            local point_curr = map_points[i]
            local point_3d_curr = vec3(point_curr.x, 0, point_curr.y)
            local norm_curr = world_point_3d_remap(point_3d_curr)
            local px, py = center_to_map(norm_curr, map_center)
            ui.drawCircle(vec2(px, py), 1, colors.RED)
        end
    end


    -- sectors lines
    local sectors = players.get_sector_splits()
    for i = 0, #sectors - 1 do
        local data           = sectors_table[i]

        -- PERF(cogno): I'm sure this can be made faster (I don't think left/right need to go
        -- through the whole chain, we just need the direction), so maybe this can get faster,
        -- but most tracks only have 3 sectors so I don't think it's that much gain...
        local road_right     = data.road_right
        local road_left      = data.road_left
        local road_center    = data.road_center
        local line_left      = world_point_3d_remap(rotate_mappos(road_left))
        local line_right     = world_point_3d_remap(rotate_mappos(road_right))
        local line_center    = world_point_3d_remap(rotate_mappos(road_center))
        local adj_lx, adj_ly = center_to_map(line_left, map_center)
        local adj_rx, adj_ry = center_to_map(line_right, map_center)
        local adj_cx, adj_cy = center_to_map(line_center, map_center)
        local adjusted_left  = vec2(adj_lx, adj_ly)
        local adjusted_right = vec2(adj_rx, adj_ry)
        local adj_center     = vec2(adj_cx, adj_cy)
        local adj_dir        = (adjusted_right - adjusted_left):normalize()

        local sector_color   = map_color
        local line_width     = 3
        local line_len       = 4
        if i == 0 then
            sector_color = colors.WARNING_RED
            line_width = 5
            line_len = new_width
        end
        line_width = line_width * canvas_size / canvas_old_size -- TAG: CanvasResScaling
        line_len   = line_len   * canvas_size / canvas_old_size -- TAG: CanvasResScaling

        local p1   = adj_center + adj_dir * line_len * zoom
        local p2   = adj_center - adj_dir * line_len * zoom
        if i == 0 then
            local flag_size = (vec2(16, 32) / 2) * canvas_size / canvas_old_size -- TAG: CanvasResScaling
            if Map_Relative then
                flag_size = vec2(13, 29) * canvas_size / canvas_old_size        -- TAG: CanvasResScaling
            end
            local rotated_vec = vec2(-adj_dir.y, adj_dir.x) * -1
            local flag_pos = (p2 + adj_dir * -1 * 2) - rotated_vec * flag_size.x + rotated_vec * line_width / 2
            local flag_dir = -adj_dir
            if (map_points[0] - map_points[1]).x < 0 then
                flag_dir = -flag_dir
                rotated_vec = -rotated_vec
                flag_pos = (p1 + adj_dir * 2) - rotated_vec * line_width / 2
            end

            local p01 = flag_pos
            local p02 = flag_pos + rotated_vec * flag_size.x
            local p03 = flag_pos + (flag_dir * flag_size.y) + rotated_vec * flag_size.x
            local p04 = flag_pos + (flag_dir * flag_size.y)

            ui.drawImageQuad(settings.get_asset("check"), p01, p02, p03, p04)
        end

        ui.drawLine(p1, p2, sector_color, line_width)
    end

    if Map_ShowDRS then
        local drs_zones = players.get_drs_zones()
        for i=0, table.nkeys(drs_zones)-1 do
            local data = drs_zones[i]
            -- PERF(cogno): I'm sure this can be made faster (I don't think left/right need to go
            -- through the whole chain, we just need the direction), so maybe this can get faster,
            -- but most tracks only have 3 sectors so I don't think it's that much gain...
            local line_left      = world_point_3d_remap(rotate_mappos(data.road_left))
            local line_right     = world_point_3d_remap(rotate_mappos(data.road_right))
            local line_center    = world_point_3d_remap(rotate_mappos(data.road_center))
            local adj_lx, adj_ly = center_to_map(line_left, map_center)
            local adj_rx, adj_ry = center_to_map(line_right, map_center)
            local adj_cx, adj_cy = center_to_map(line_center, map_center)
            local adjusted_left  = vec2(adj_lx, adj_ly)
            local adjusted_right = vec2(adj_rx, adj_ry)
            local adj_center     = vec2(adj_cx, adj_cy)
            local adj_dir        = (adjusted_right - adjusted_left):normalize()

            local sector_color   = colors.LIGHT_GREEN
            local line_width     = 3
            local line_len       = 3
            line_width = line_width * canvas_size / canvas_old_size -- TAG: CanvasResScaling
            line_len   = line_len   * canvas_size / canvas_old_size -- TAG: CanvasResScaling

            local p1   = adj_center + adj_dir * line_len * zoom
            local p2   = adj_center - adj_dir * line_len * zoom
            ui.drawLine(p1, p2, sector_color, line_width)
        end
    end

    -- then the player background triangle texture maybe:
    -- when we are in absolute mode the background is a circle, so we do it with the classic method,
    -- but when we are in relative mode, the player is a triangle. If we use the classic method the
    -- background sucks so we use a texture instead here
    local players_main_size = 22 * canvas_size / canvas_old_size -- TAG: CanvasResScaling
    if Map_Relative then
        local focused, changed = players.get_focused_car()
        local car_info = ac.getCar(focused)
        local car_worldpos = rotate_mappos(car_info.position)
        local player_normalized_trackpos = world_point_3d_remap(car_worldpos)
        local adjusted_pos_x, adjusted_pos_y = center_to_map(player_normalized_trackpos, map_center)
        local adjusted_pos = vec2(adjusted_pos_x, adjusted_pos_y)
        local r = players_main_size * 0.45
        ui.drawImage(
            settings.get_asset("cursor_out"),
            adjusted_pos - r * 1.4,
            adjusted_pos + r * 1.4,
            colors.BLACK_TRACK
        )
    end

    -- then all the players as normal, but only the ones without the outline (we're in the no-outline texture!)
    players_render(false)
end


function mod.init()
    missing_data_timer = Time
    canvas_mapsize = canvas_size - map_padding * 2 * canvas_size / canvas_old_size

    canvas = ui.ExtraCanvas(canvas_size, 1, render.AntialiasingMode.None, render.TextureFormat.R8G8B8A8.UNorm,
        render.TextureFlags.None)
    canvas:setName("map canvas")

    player_canvas = ui.ExtraCanvas(canvas_size, 1, render.AntialiasingMode.None, render.TextureFormat.R8G8B8A8.UNorm,
        render.TextureFlags.None)
    player_canvas:setName("player canvas")

    multiclass_canvas = ui.ExtraCanvas(canvas_size, 1, render.AntialiasingMode.None, render.TextureFormat.R8G8B8A8.UNorm,
        render.TextureFlags.None)
    multiclass_canvas:setName("multiclass canvas")

    -- PERF(cogno): right now we only need a second one to have a shader on top, can we avoid?
    final_canvas = ui.ExtraCanvas(canvas_size, 1, render.AntialiasingMode.None, render.TextureFormat.R8G8B8A8.UNorm,
        render.TextureFlags.None)
    final_canvas:setName("canvas 2")

    local sim_info = ac.getSim()
    local track_length = sim_info.trackLengthM
    if track_length > 7000 then
        map_pointcount = math.max(math.floor(track_length / 20), map_pointcount)
    end
    relative_zoom = math.clamp(settings.remap(track_length, 4300, 10000, min_relative_zoom, 2.2), min_relative_zoom,
        10000)
    relative_track_width_zoom = math.clamp(settings.remap(track_length, 4300, 10000, min_relative_zoom, 1.2),
        min_relative_zoom, 5)

    local track_start_spline = players.get_sector_splits()[0]
    local worldpos1 = ac.trackCoordinateToWorld(vec3(0, 0, track_start_spline))
    local worldpos2 = ac.trackCoordinateToWorld(vec3(0, 0, track_start_spline + 0.01))
    local worldpos1_2d = vec2(worldpos1.x, worldpos1.z)
    local worldpos2_2d = vec2(worldpos2.x, worldpos2.z)
    local world_dir = (worldpos2_2d - worldpos1_2d):normalize()
    MapAngle = math.atan2(world_dir.y, world_dir.x) -- will be off, we'll fix it later

    local max_3d = vec3()
    local min_3d = vec3()
    local to_avoid_gc1 = vec3()                    -- TAG: GarbageSucks
    for i = 0, map_pointcount - 1 do
        to_avoid_gc1:set(0, 0, i / map_pointcount) -- TAG: GarbageSucks reduces by 16KB of GC
        local world_point_pos = ac.trackCoordinateToWorld(to_avoid_gc1)

        if world_point_pos.x > max_3d.x then max_3d.x = world_point_pos.x end
        if world_point_pos.y > max_3d.y then max_3d.y = world_point_pos.y end
        if world_point_pos.z > max_3d.z then max_3d.z = world_point_pos.z end
        if world_point_pos.x < min_3d.x then min_3d.x = world_point_pos.x end
        if world_point_pos.y < min_3d.y then min_3d.y = world_point_pos.y end
        if world_point_pos.z < min_3d.z then min_3d.z = world_point_pos.z end
    end

    map_center = (max_3d + min_3d) / 2

    -- some tracks are run clockwise and others counterclockwise, the angle above
    -- will be off by 180 degrees in one of those 2 cases!
    -- to fix it we check in which case we are and add 180 degrees if needed
    -- solution source: answer by Sean the Bean: https://stackoverflow.com/questions/1165647/how-to-determine-if-a-list-of-polygon-points-are-in-clockwise-order
    local the_sum = 0
    local to_avoid_gc2 = vec3()                                             -- TAG: GarbageSucks
    for i = 0, map_pointcount - 1 do
        to_avoid_gc1:set(0, 0, i / map_pointcount)                          -- TAG: GarbageSucks reduces by 11KB of GC
        to_avoid_gc2:set(0, 0, ((i + 1) % map_pointcount) / map_pointcount) -- TAG: GarbageSucks reduces by 10KB of GC
        local worldpos_curr = ac.trackCoordinateToWorld(to_avoid_gc1)
        local worldpos_next = ac.trackCoordinateToWorld(to_avoid_gc2)
        the_sum = the_sum + (worldpos_curr.x * worldpos_next.z - worldpos_next.x * worldpos_curr.z)
    end
    if the_sum > 0 then is_clockwise = true end
    if is_clockwise then
        rotator = quat():setAngleAxis(MapAngle + math.pi, 0, 1, 0)
        MapAngle = MapAngle + math.pi
    else
        rotator = quat():setAngleAxis(MapAngle, 0, 1, 0)
    end

    -- now that we can rotate correctly we can store map data pre-rotated (so we don't to these calculations every frame!)
    local threshold = 0.04
    local add_index = 0
    local last_angle = 0
    for i = 0, map_pointcount - 1 do
        to_avoid_gc1:set(0, 0, i / map_pointcount)                          -- TAG: GarbageSucks reduces by 17KB of GC
        to_avoid_gc2:set(0, 0, ((i + 1) % map_pointcount) / map_pointcount) -- TAG: GarbageSucks reduces by 10KB of GC
        local world_point_pos = ac.trackCoordinateToWorld(to_avoid_gc1)
        local world_point_pos_next = ac.trackCoordinateToWorld(to_avoid_gc2)

        local d = (world_point_pos_next - world_point_pos)
        local angle = math.atan2(d.z, d.x)
        if math.abs(angle - last_angle) > threshold then
            local offset = world_point_pos - map_center
            local rotated = offset:rotate(rotator)
            local new_pos = map_center + rotated

            -- recalculate bounds after rotation as if we never rotated anything
            if new_pos.x > max_pos.x then max_pos.x = new_pos.x end
            if new_pos.z > max_pos.y then max_pos.y = new_pos.z end
            if new_pos.x < min_pos.x then min_pos.x = new_pos.x end
            if new_pos.z < min_pos.y then min_pos.y = new_pos.z end
            map_points[add_index] = vec2(new_pos.x, new_pos.z)
            add_index = add_index + 1
            last_angle = angle
        end
    end

    local map_full_width   = max_pos.x - min_pos.x
    local map_full_height  = max_pos.y - min_pos.y
    local map_aspect_ratio = (map_full_width / map_full_height)
    local aspect_x         = math.min(1, map_aspect_ratio)
    local aspect_y         = math.min(1, 1 / map_aspect_ratio)
    mapvec                 = vec2(aspect_x, aspect_y) * canvas_mapsize

    local sectors          = players.get_sector_splits()
    for i = 0, #sectors - 1 do
        local sector_percentage = sectors[i]
        local road_right        = ac.trackCoordinateToWorld(vec3(1, 0, sector_percentage))
        local road_left         = ac.trackCoordinateToWorld(vec3(-1, 0, sector_percentage))
        local road_center       = ac.trackCoordinateToWorld(vec3(0, 0, sector_percentage))
        sectors_table[i]        = { road_right = road_right, road_left = road_left, road_center = road_center }
    end
end

local function update_multiclass()
    draw_arcs()
end

function mod.main()
    local focused, changed = players.get_focused_car()
    abs_rel_lerp_t = settings.anim_lerp(Time - Map_Relative_SwitchTime, Map_Relative, 0.3)
    local my_car = ac.getCar(focused)
    player_norm = world_point_3d_remap(rotate_mappos(my_car.position)) -- TAG: MakeTheLoopQuickAgain
    local world_dir = my_car.look
    local angle = math.atan2(world_dir.z, world_dir.x) - MapAngle + math.pi / 2
    if angle < 0 then angle = angle + math.pi * 2 end
    -- if is_clockwise then angle = angle + math.pi end -- BUG(cogno): if we activate this line in nordschleife the map is 180° wrong in relative, but if we disable other maps SHOULD BE 180° off, but we haven't confirmed it. Either 1. prove that this can be removed or 2. fix this
    if angle <= math.pi then
        player_rotator = quat():setAngleAxis(settings.remap(abs_rel_lerp_t, 0, 1, 0, angle), 0, 1, 0)
    else
        player_rotator = quat():setAngleAxis(settings.remap(abs_rel_lerp_t, 0, 1, math.pi * 2, angle), 0, 1, 0)
    end

    if #map_points > 0 then
        canvas:clear()
        canvas:update(map_canvas_update)

        player_canvas:clear()
        player_canvas:update(player_canvas_update)

        local multiclass_elapsed = Time - Leaderboard_Multiclass_EnabledTime
        multiclass_canvas:clear()
        if Leaderboard_Multiclass_Enabled or (multiclass_elapsed < 0.5) then
            multiclass_canvas:update(update_multiclass)
        end
    end

    final_canvas:updateWithShader({
        textures = { c1in = canvas, c2in = player_canvas, c3in = multiclass_canvas },
        values = { lerp_t = abs_rel_lerp_t },
        shader = [[
        #define DIV_SQRT_2 0.70710678118

        SamplerState NoWrapSampler{
            Filter = MIN_MAG_MIP_LINEAR;
            AddressU = NoWrap;
            AddressV = NoWrap;
        };

        float2 uvPerWorldUnit(float2 uv, float2 space){
            float2 uvPerPixelX = abs(ddx(uv));
            float2 uvPerPixelY = abs(ddy(uv));
            float unitsPerPixelX = length(ddx(space));
            float unitsPerPixelY = length(ddy(space));
            float2 uvPerUnitX = uvPerPixelX / unitsPerPixelX;
            float2 uvPerUnitY = uvPerPixelY / unitsPerPixelY;
            return (uvPerUnitX + uvPerUnitY);
        }


        float4 main(PS_IN pin){
            //Player canvas with outline
            float4 col = c2in.Sample(NoWrapSampler, pin.Tex);
            float4 multi_col = c3in.Sample(NoWrapSampler, pin.Tex);

            float thickness = 0.01;

            float2 directions[8] = {float2(1, 0), float2(0, 1), float2(-1, 0), float2(0, -1),
            float2(DIV_SQRT_2, DIV_SQRT_2), float2(-DIV_SQRT_2, DIV_SQRT_2),
            float2(-DIV_SQRT_2, -DIV_SQRT_2), float2(DIV_SQRT_2, -DIV_SQRT_2)};

            float maxAlpha = 0;
            for(uint index = 0; index<8; index++){
                float2 sampleUV = pin.Tex + directions[index] * 0.011/*magic number*/;
                maxAlpha = max(maxAlpha, c2in.Sample(NoWrapSampler, sampleUV).a);
            }

            float distfromcenter=distance(float2(0.5f, 0.5f), pin.Tex);
            float4 rColor = lerp(float4(0,0,0,1), float4(1,1,1,1), distfromcenter);
            float radius = 0.4;
            float4 clamped = clamp((rColor - radius) * 15, 0, 1);
            float4 map = c1in.Sample(NoWrapSampler, pin.Tex);
            float4 lerped = lerp(float4(0,0,0,1), clamped, lerp_t);
            //return lerped;

            float4 old_col = col;
            col.rgb = lerp(float3(43.0 / 255.0, 43.0 / 255.0, 43.0 / 255.0), col.rgb, col.a);
            col.a = max(col.a, maxAlpha);
            float t = saturate(map.a * (1 - col.a) * (1 - multi_col.a));

            float3 base_map = map.rgb * t;
            float3 outline_final = col.rgb * (1 - t) * saturate((1 - multi_col.a) + old_col.a);
            float3 multiclass_final = multi_col.rgb * (1-old_col.a);
            float alpha = (map.a * (1 - col.a) + col.a) * (1 - lerped.x);


            //return float4(base_map + col * saturate(1 - multi_col.a + old_col.a) + multi_col * (1-old_col.a));
            return float4(base_map + outline_final + multiclass_final, alpha);
        }
        ]]
    })

    local draw_top_left = vec2(0, 22)
    local draw_size = ui.windowSize() - vec2(0, 22)
    local draw_center = draw_top_left + draw_size / 2

    local map_padding = 20 * MapScale
    local corner_tl = draw_center - draw_size / 2
    local mapsize = 270 * MapScale + map_padding * 2
    local dotsize = 300 * MapScale

    local map_center = corner_tl + mapsize / 2
    players.play_intro_anim_setup(map_center, vec2(dotsize, dotsize) * 1.4, on_show_animation_start, is_showing)
    
    --if not MapRemoveBGWhenDataMissing and #map_points >= 0 then

    local show_bg = true
    if #map_points <= 0 and MapRemoveBGWhenDataMissing then
        show_bg = false
    end

    if not MapRemoveBG and show_bg then
        ui.drawImage(
            settings.get_asset("map_bg"),
            map_center - dotsize / 2,
            map_center + dotsize / 2
        )
    end
    --end

    if #map_points <= 0 then
        local draw_center = ui.windowSize() / 2
        ui.pushDWriteFont(fonts.archivo_bold)
        local text_size = ui.measureDWriteText("NO VALID DATA", 16)

        local lerp_t = settings.anim_lerp(Time - showing_missing_data_timer, 0, 0.2)

        if Time - missing_data_timer < 8 then
            lerp_t = 0
        end

        ui.pushClipRect((draw_center - text_size / 2) + vec2(text_size.x * lerp_t ,0), (draw_center - text_size / 2) + text_size)
        if Time - missing_data_timer > 8 then
            if is_missing_data_shown == true then
                showing_missing_data_timer = Time
            end

            is_missing_data_shown = false
        end
        ui.dwriteDrawText("NO VALID DATA", 16, draw_center - text_size / 2, colors.WHITE)
        ui.popDWriteFont()
        ui.popClipRect()
    end

    -- draw the map render texture
    ui.drawImage(final_canvas, map_center - mapsize / 2, map_center + mapsize / 2)
    local app_size = vec2(mapsize, mapsize)
    players.play_intro_anim(map_center, app_size, on_show_animation_start, MapScale)
    settings.lock_app(map_center, app_size, APPNAMES.map, MapScale)
    settings.auto_scale_window(app_size * 1.02, APPNAMES.map)
    settings.auto_place_once(app_size, APPNAMES.map)


    --ui.drawRect(draw_center + vec2(-1, -1) * mapsize, draw_center + vec2(map_aspect_ratio, 1) * mapsize, colors.BLUE)

    -- DEBUG(cogno): draw area
    -- ui.drawRect(draw_top_left, draw_top_left + draw_size, colors.WHITE)
end

return mod -- expose functions to the outside
