---@class custom_gauge_coords
---@field Top number Top of the gauge in gauge space
---@field Left number Left of the gauge in gauge space
---@field Bottom number Bottom of the gauge in gauge space
---@field Right number Right of the gauge in gauge space
---@field CenterX number Center X of the gauge in gauge space
---@field CenterY number Center Y of the gauge in gauge space
---@field OffsetX number Offset X of the gauge in screen space
---@field OffsetY number Offset Y of the gauge in screen space

---@class custom_gauge_frame_values
---@field CamPos? vector Camera position
---@field UpVec? vector Up vector
---@field FwdVec? vector Forward vector
---@field FwdAngle? number Forward angle

local Utils = require("lib_utils")

AtmosphericGauge = {
    PitchLadder = "Pitch_Ladder",   ---@type string Name of the  pitch ladder gauge
    Compass = "Compass",            ---@type string Name of the compass gauge
    Altitude = "Altitude",          ---@type string Name of the altitude gauge
    Speed = "Speed",                ---@type string Name of the speed gauge

    -- Gauge sizes need to be hard coded similar to how bitmaps have static sizes
    Size = 180,                 ---@type number Size of the gauge
    CompassSize = 180,          ---@type number Size of the compass gauge. Refers to the width, but height takes this into account
    SpeedSize = 180,            ---@type number Size of the speed gauge. Refers to the height, but width takes this into account
    AltitudeSize = 180,         ---@type number Size of the altitude gauge. Refers to the height, but width takes this into account
    FrameValues = {},           ---@type custom_gauge_frame_values Frame values for the gauge

    --- Global values that will be settable by sexp
    GlobalOrient = nil,          ---@type orientation Global orientation offset
    GlobalAltitude = nil,          ---@type number Global altitude offset
    GlobalCrushDepth = nil,      ---@type number Global crush depth
    GlobalSafeDepth = nil,       ---@type number Global safe depth
}

--- Init the gauge coordinates based on the size. Only run this once at mission start.
--- @return nil
function AtmosphericGauge:init()
    -- Reset the frame values
    self.FrameValues = {
        CamPos = nil,
        UpVec = nil,
        FwdVec = nil,
        FwdAngle = nil
    }
    -- Reset the global values
    self.GlobalOrient = nil
    self.GlobalAltitude = nil
    self.GlobalCrushDepth = nil
    self.GlobalSafeDepth = nil

    -- Reset the frame counter
    self.Frame = -1
end

--- Set up the frame values for all atmospheric gauges. Run this once per frame.
--- @return nil
function AtmosphericGauge:setup()
    if not self.GlobalOrient then
        self.GlobalOrient = ba.createOrientation(0, 0, 0)
    end

    self.FrameValues.CamPos = gr.getCurrentCamera(true).Position
    self.FrameValues.UpVec = self.GlobalOrient:getUvec()
    self.FrameValues.FwdVec = hv.Player:getfvec():projectOntoPlane(self.FrameValues.UpVec):getNormalized()

    self.FrameValues.FwdAngle = math.acos(self.FrameValues.FwdVec:getDotProduct(self.GlobalOrient:getFvec()))
    if self.FrameValues.FwdVec:getCrossProduct(self.GlobalOrient:getFvec()):getDotProduct(self.GlobalOrient:getUvec()) > 0.0 then
        self.FrameValues.FwdAngle = -self.FrameValues.FwdAngle
    end
end

--- Convert degrees to radians
--- @param degrees number
--- @return number radians
function AtmosphericGauge:degToRad(degrees)
    return degrees * math.pi / 180
end

--- Diff a set of coordinates
--- @param x1 number
--- @param y1 number
--- @param x2 number
--- @param y2 number
--- @return number, number
function AtmosphericGauge:diffCoords(x1, y1, x2, y2)
    return x1 - x2, y1 - y2
end

--- Get the marker extents
--- @param up_dirx number
--- @param up_diry number
--- @return number, number, number, number
function AtmosphericGauge:getMarkerExtents(up_dirx, up_diry)
    local mag = math.sqrt(up_dirx * up_dirx + up_diry * up_diry)
    up_dirx = up_dirx / mag
    up_diry = up_diry / mag
    return up_diry * -150, up_dirx * 150, up_diry * 150, up_dirx * -150
end

--- Check if a point is within the bounds of the gauge
--- @param x number
--- @param y number
--- @param bx1 number --- Left of the gauge
--- @param bx2 number --- Right of the gauge
--- @param by1 number --- Top of the gauge
--- @param by2 number --- Bottom of the gauge
--- @return boolean
function AtmosphericGauge:checkBounds(x, y, bx1, bx2, by1, by2)
    return bx1 < x and bx2 > x and by1 < y and by2 > y
end

--- Check if a point is within the circular bounds of the gauge
--- @param x number
--- @param y number
--- @param radius number
--- @param cx number --- Center X of the circle
--- @param cy number --- Center Y of the circle
--- @return boolean
function AtmosphericGauge:checkBoundsCircle(x, y, radius, cx, cy)
    return (x - cx)^2 + (y - cy)^2 <= radius^2
end


--- Draw a line in the bounds of the gauge
--- @param gauge HudGaugeDrawFunctions
--- @param x1 number --- Start x
--- @param y1 number --- Start y
--- @param x2 number --- End x
--- @param y2 number --- End y
--- @param bx1 number --- Left of the gauge
--- @param bx2 number --- Right of the gauge
--- @param by1 number --- Top of the gauge
--- @param by2 number --- Bottom of the gauge
function AtmosphericGauge:drawLineInBounds(gauge, x1, y1, x2, y2, bx1, bx2, by1, by2)
    local xmin, xmax = bx1, bx2
    local ymin, ymax = by1, by2

    local min_t = 0
    local max_t = 1
    if y2 > y1 then
        min_t = math.max(min_t, (ymin - y1) / (y2 - y1))
        max_t = math.min(max_t, (ymax - y1) / (y2 - y1))
    elseif y2 < y1 then
        min_t = math.max(min_t, (ymax - y1) / (y2 - y1))
        max_t = math.min(max_t, (ymin - y1) / (y2 - y1))
    end

    if x2 > x1 then
        min_t = math.max(min_t, (xmin - x1) / (x2 - x1))
        max_t = math.min(max_t, (xmax - x1) / (x2 - x1))
    elseif x2 < x1 then
        min_t = math.max(min_t, (xmax - x1) / (x2 - x1))
        max_t = math.min(max_t, (xmin - x1) / (x2 - x1))
    end

    if max_t > min_t then
        gauge:drawLine(x1 + min_t * (x2 - x1), y1 + min_t * (y2 - y1), x1 + max_t * (x2 - x1), y1 + max_t * (y2 - y1))
    end
end

--- Draw a line within a circular boundary
--- @param gauge HudGaugeDrawFunctions
--- @param x1 number --- Start x
--- @param y1 number --- Start y
--- @param x2 number --- End x
--- @param y2 number --- End y
--- @param radius number --- Radius of the circle
--- @param cx number --- Center X of the circle
--- @param cy number --- Center Y of the circle
function AtmosphericGauge:drawLineInCircle(gauge, x1, y1, x2, y2, radius, cx, cy)
    -- Function to check if a point is inside the circle
    local function isInsideCircle(x, y)
        return (x - cx)^2 + (y - cy)^2 <= radius^2
    end

    -- Check if both points are inside the circle
    local inside1 = isInsideCircle(x1, y1)
    local inside2 = isInsideCircle(x2, y2)

    -- If both points are inside, draw the line as normal
    if inside1 and inside2 then
        gauge:drawLine(x1, y1, x2, y2)
        return
    end

    -- Function to compute intersection of line with circle
    local function clipToCircle(x1, y1, x2, y2)
        local dx, dy = x2 - x1, y2 - y1
        local a = dx * dx + dy * dy
        local b = 2 * ((x1 - cx) * dx + (y1 - cy) * dy)
        local c = (x1 - cx) * (x1 - cx) + (y1 - cy) * (y1 - cy) - radius * radius
        local discriminant = b * b - 4 * a * c

        if discriminant < 0 then
            return nil, nil  -- No intersection
        end

        local sqrtD = math.sqrt(discriminant)
        local t1 = (-b - sqrtD) / (2 * a)
        local t2 = (-b + sqrtD) / (2 * a)

        local intersections = {}

        if t1 >= 0 and t1 <= 1 then
            table.insert(intersections, {x1 + t1 * dx, y1 + t1 * dy})
        end
        if t2 >= 0 and t2 <= 1 then
            table.insert(intersections, {x1 + t2 * dx, y1 + t2 * dy})
        end

        return intersections[1], intersections[2]
    end

    -- Clip points if necessary
    local new_x1, new_y1 = x1, y1
    local new_x2, new_y2 = x2, y2

    if not inside1 then
        local intersection, _ = clipToCircle(x1, y1, x2, y2)
        if intersection then
            new_x1, new_y1 = intersection[1], intersection[2]
        else
            return  -- No valid intersection, don't draw anything
        end
    end

    if not inside2 then
        local _, intersection = clipToCircle(x1, y1, x2, y2)
        if intersection then
            new_x2, new_y2 = intersection[1], intersection[2]
        else
            return  -- No valid intersection, don't draw anything
        end
    end

    -- Draw the clipped line
    gauge:drawLine(new_x1, new_y1, new_x2, new_y2)
end


--- Draw the compass hud gauge
--- @param gauge HudGaugeDrawFunctions
--- @param gauge_handle HudGauge
--- @return nil
function AtmosphericGauge:drawCompass(gauge, gauge_handle)
    local compass_h = self.CompassSize * 0.04
    for compass_ang_deg = -200, 210, 10 do
        local x_offset = (self:degToRad(compass_ang_deg) - self.FrameValues.FwdAngle) * 1.5 * (self.CompassSize / 2)
        local x = (self.CompassSize / 2) + x_offset
        if x_offset < (self.CompassSize / 2) and x_offset > -(self.CompassSize / 2) then
            gr.setLineWidth(self.CompassSize * 0.0104)
            if compass_ang_deg % 90 == 0 then
                local str = ""
                if compass_ang_deg % 360 == 0 then str = "N"
                elseif compass_ang_deg % 360 == 90 then str = "E"
                elseif compass_ang_deg % 360 == 180 then str = "S"
                elseif compass_ang_deg % 360 == 270 then str = "W"
                end

                if str ~= "" then
                    local w, h = gr.getStringSize(str)
                    gauge:drawString(str, x - w / 2, 0 - compass_h * 2 - h)
                end
                gauge:drawLine(x, 0, x, 0 - compass_h * 2)
            else
                gauge:drawLine(x, 0, x, 0 - compass_h)
            end
        end
    end
    --local str = tostring(math.floor(self.FrameValues.FwdAngle * 180 / math.pi) % 360)
    --gauge:drawString(str, (self.CompassSize / 2) - gr.getStringWidth(str) / 2, 0 + gr.getStringHeight(str))

    gr.setLineWidth()
end

--- Draw the speed hud gauge
--- @param gauge HudGaugeDrawFunctions
--- @param gauge_handle HudGauge
--- @return nil
function AtmosphericGauge:drawSpeed(gauge, gauge_handle)
    local speed_w = self.SpeedSize * 0.08
    gauge:drawLine(0, 0, 0, self.SpeedSize)

    local str = "SPD "..string.format("%.0f", hv.Player.Physics:getForwardSpeed()).."m/s"
    gauge:drawString(str, 0 - speed_w - gr.getStringWidth(str) * 1.5, (self.SpeedSize / 2))

    for i = -50, 50, 10 do
        local speed_diff = hv.Player.Physics:getForwardSpeed() % 10
        local val = i + math.floor(hv.Player.Physics:getForwardSpeed() * 0.1) * 10
        if val >= 0 then
            local y = (self.SpeedSize / 2) - i * 2 + speed_diff * 2
            if y > 0 and y < self.SpeedSize then
                local width = speed_w * 0.7
                if val % 50 == 0 then
                    width = speed_w
                end
                gauge:drawLine(0 - width, y, 0 - self.SpeedSize * 0.02, y)
            end
        end
    end
    gr.setLineWidth()
end

--- Draw the altitude hud gauge
--- @param gauge HudGaugeDrawFunctions
--- @param gauge_handle HudGauge
--- @return nil
function AtmosphericGauge:drawAltitude(gauge, gauge_handle)
    local alt_x = 0
    local alt_w = self.AltitudeSize * 0.08
    gauge:drawLine(alt_x, 0, alt_x, self.AltitudeSize)

    -- Compute the Player's altitude relative to global_orient using existing variables
    local player_altitude = hv.Player.Position:getDotProduct(self.FrameValues.UpVec:getNormalized())  -- Project onto up vector

    -- Get the original HUD color
    local r, g, b = gauge_handle:getColor()
    local color = gr.createColor(r, g, b, 255)

    -- Only apply fading if crush_depth is set
    local fade_factor = 0
    if self.GlobalCrushDepth ~= nil then
        -- Default fade_start_altitude if not manually set
        local fade_start_altitude = self.GlobalSafeDepth or (self.GlobalCrushDepth + (self.GlobalCrushDepth / 2))

        -- Ensure fade_start_altitude is always greater than crush depth
        if fade_start_altitude <= self.GlobalCrushDepth then
            fade_start_altitude = self.GlobalCrushDepth + 1
        end

        -- Ensure fade_range is valid to prevent division errors
        local fade_range = fade_start_altitude - self.GlobalCrushDepth
        fade_factor = (fade_range > 0) and Utils.clamp((fade_start_altitude - player_altitude) / fade_range, 0, 1) or 0

        -- Define warning color transitions (yellow → orange → red)
        local warning_colors = {
            {255, 255, 0},  -- Yellow
            {255, 165, 0},  -- Orange
            {255, 0, 0}     -- Red
        }

        -- Get the original HUD color
        local base_r, base_g, base_b = color.Red, color.Green, color.Blue

        -- Function to interpolate between two colors
        local function lerpColor(c1, c2, factor)
            return c1 + (c2 - c1) * factor
        end

        -- Determine transition stage
        local color_stage = fade_factor * (#warning_colors - 1)
        local color_index = math.floor(color_stage) + 1
        local next_color_index = math.min(color_index + 1, #warning_colors)

        -- Get colors for interpolation
        local color1 = warning_colors[color_index]
        local color2 = warning_colors[next_color_index]

        -- Blend smoothly between the two closest colors
        local stage_factor = color_stage % 1
        local r_fade = lerpColor(base_r, lerpColor(color1[1], color2[1], stage_factor), fade_factor)
        local g_fade = lerpColor(base_g, lerpColor(color1[2], color2[2], stage_factor), fade_factor)
        local b_fade = lerpColor(base_b, lerpColor(color1[3], color2[3], stage_factor), fade_factor)

        -- Apply the new color
        gr.setColor(r_fade, g_fade, b_fade, color.Alpha)
    end

    -- Draw the warning strings
	local offset = self.GlobalAltitude or 0
    local altitude = player_altitude + offset
    local str = ""
    if altitude > 10000 then
        local km = math.floor(altitude / 1000)  -- Whole km part
        local meters = math.floor(altitude % 1000)  -- Remaining meters
        str = string.format("%d.%03d km ALT", km, meters)  -- Show km + meters as decimal
    else
        str = string.format("%.0f m ALT", altitude)
    end
    local str_x_offset = 25 -- ARBITRARY OFFSET
    local str_y_offset = -50 -- ARBITRARY OFFSET
    gauge:drawString(str, alt_x + alt_w + str_x_offset, (self.AltitudeSize / 2) + str_y_offset)
    if fade_factor > 0 then
        local should_draw = true
        local str1 = "CRUSH"
        local str2 = "WARNING!"
        if fade_factor == 1 then
            str2 = "IMMINENT!"

            local flash_interval = 0.5
            local mission_time = mn.getMissionTime()

            should_draw = (math.floor(mission_time / flash_interval) % 2) == 0
        end
        if should_draw then
            local y_offset = gr.getStringHeight(str1) * 1.5
            gauge:drawString(str1, alt_x + alt_w + str_x_offset, (self.AltitudeSize / 2) + str_y_offset + y_offset)
            gauge:drawString(str2, alt_x + alt_w + str_x_offset, (self.AltitudeSize / 2) + str_y_offset + y_offset + gr.getStringHeight(str1) + 2)
        end
    end

    for i = -100, 100, 20 do
        local alt_diff = player_altitude % 20
        local y = (self.AltitudeSize / 2) + i + alt_diff
        if y > 0 and y < self.AltitudeSize then
            local val = i - math.floor(player_altitude * 0.05) * 20
            local width = alt_w * 0.7
            if val % 100 == 0 then
                width = alt_w
            end
            gauge:drawLine(alt_x + self.AltitudeSize * 0.02, y, alt_x + width, y)
        end
    end

    gr.setColor(color)
    gr.setLineWidth()
end

--- Draw the pitch ladder hud gauge
--- @param gauge HudGaugeDrawFunctions
--- @param gauge_handle HudGauge
--- @return nil
function AtmosphericGauge:drawPitchLadder(gauge, gauge_handle)
    ---@type custom_gauge_coords Coordinates of the gauge
    Coords = {
        Top = 0,
        Left = 0,
        Bottom = self.Size,
        Right = self.Size,
        CenterX = self.Size / 2,
        CenterY = self.Size / 2,
        OffsetX = 0,
        OffsetY = 0
    }
    Coords.OffsetX = Coords.CenterX - gr.getScreenWidth() / 2
    Coords.OffsetY = Coords.CenterY - gr.getScreenHeight() / 2

    local x1, y1, x2, y2
    for up_ang_deg = -80, 80, 5 do
        local up_ang = self:degToRad(up_ang_deg)
        if (math.cos(up_ang) * self.FrameValues.FwdVec + math.sin(up_ang) * self.FrameValues.UpVec):getDotProduct(hv.Player:getfvec()) > 0.96 then
            local center = (math.cos(up_ang) * self.FrameValues.FwdVec + math.sin(up_ang) * self.FrameValues.UpVec) + self.FrameValues.CamPos

            local center_screenx, center_screeny = center:getScreenCoords()
            if x1 == nil then
                local center_screenmod_x, center_screenmod_y = (center + self.FrameValues.UpVec * 0.3):getScreenCoords()
                local up_dirx, up_diry = self:diffCoords(center_screenx, center_screeny, center_screenmod_x, center_screenmod_y)
                x1, y1, x2, y2 = self:getMarkerExtents(up_dirx, up_diry)
            end

            local r, g, b = gauge_handle:getColor()
            local color = gr.createColor(r, g, b, 255)

            if up_ang_deg == 0 then
                gr.setColor(color.Red, color.Green, color.Blue, 255)
                gr.setLineWidth(self.Size * 0.0174)

                local length = self.Size * 0.00521
                self:drawLineInCircle(gauge,
                    (x1 * length + center_screenx) + Coords.OffsetX,
                    (y1 * length + center_screeny) + Coords.OffsetY,
                    (x2 * length + center_screenx) + Coords.OffsetX,
                    (y2 * length + center_screeny) + Coords.OffsetY,
                    self.Size / 2, Coords.CenterX, Coords.CenterY
                )
            else
                local size = (up_ang_deg % 10 == 0) and self.Size * 0.00278 or self.Size * 0.00243
                local gap_size = self.Size * 0.00174
                gr.setColor(color.Red, color.Green, color.Blue, (up_ang_deg % 10 == 0) and color.Alpha or (color.Alpha * 0.5))
                gr.setLineWidth((up_ang_deg % 10 == 0) and self.Size * 0.0139 or self.Size * 0.0069)
                self:drawLineInCircle(gauge,
                    (x1 * size + center_screenx) + Coords.OffsetX,
                    (y1 * size + center_screeny) + Coords.OffsetY,
                    (x1 * gap_size + center_screenx) + Coords.OffsetX,
                    (y1 * gap_size + center_screeny) + Coords.OffsetY,
                    self.Size / 2, Coords.CenterX, Coords.CenterY
                )
                self:drawLineInCircle(gauge,
                    (x2 * size + center_screenx) + Coords.OffsetX,
                    (y2 * size + center_screeny) + Coords.OffsetY,
                    (x2 * gap_size + center_screenx) + Coords.OffsetX,
                    (y2 * gap_size + center_screeny) + Coords.OffsetY,
                    self.Size / 2, Coords.CenterX, Coords.CenterY
                )
                local w, h = gr.getStringSize(tostring(up_ang_deg))
                local x, y = (x1 * size + center_screenx) + Coords.OffsetX, (y1 * size + center_screeny) + Coords.OffsetY - h / 2
                if x < Coords.CenterX then
                    x = x - w
                end
                if self:checkBoundsCircle(x, y, self.Size / 2 + 10, Coords.CenterX, Coords.CenterY) then
                    gauge:drawString(tostring(up_ang_deg), x, y)
                end
                x, y = (x2 * size + center_screenx) + Coords.OffsetX, (y2 * size + center_screeny) + Coords.OffsetY - h / 2
                if x < Coords.CenterX then
                    x = x - w
                end
                if self:checkBoundsCircle(x, y, self.Size / 2 + 10, Coords.CenterX, Coords.CenterY) then
                    gauge:drawString(tostring(up_ang_deg), x, y)
                end
            end
            gr.setColor(color)
        end
    end
    gr.setLineWidth()
end

--- Draw the atmospheric gauges
---@param gauge HudGaugeDrawFunctions
---@param gauge_handle HudGauge
function AtmosphericGauge:Draw(gauge, gauge_handle, name)
    local func = {
        ["Pitch_Ladder"] = self.drawPitchLadder,
        ["Compass"] = self.drawCompass,
        ["Altitude"] = self.drawAltitude,
        ["Speed"] = self.drawSpeed
    }
    if func[name] == nil then
        return
    end

    -- save the global color
    local color_save = gr.getColor(true)

    -- If we are on a new frame, set up the frame values
    if self.Frame ~= ba.getFrametimeOverall() then
        self:setup()
        self.Frame = ba.getFrametimeOverall()
    end

    -- Draw the gauge based on the gauge handle name
    func[name](self, gauge, gauge_handle)

    -- restore the global color
    gr.setColor(color_save)
end

local pitch_ladder_gauge = hu.getHUDGaugeHandle(AtmosphericGauge.PitchLadder)
if pitch_ladder_gauge ~= nil then
    AtmosphericGauge:init()
    pitch_ladder_gauge.RenderFunction = function(gauge)
        AtmosphericGauge:Draw(gauge, pitch_ladder_gauge, AtmosphericGauge.PitchLadder)
    end
end

local altitude_gauge = hu.getHUDGaugeHandle(AtmosphericGauge.Altitude)
if altitude_gauge ~= nil then
    AtmosphericGauge:init()
    altitude_gauge.RenderFunction = function(gauge)
        AtmosphericGauge:Draw(gauge, altitude_gauge,AtmosphericGauge.Altitude)
    end
end

local altitude_gauge = hu.getHUDGaugeHandle(AtmosphericGauge.Speed)
if altitude_gauge ~= nil then
    AtmosphericGauge:init()
    altitude_gauge.RenderFunction = function(gauge)
        AtmosphericGauge:Draw(gauge, altitude_gauge,AtmosphericGauge.Speed)
    end
end

local compass_gauge = hu.getHUDGaugeHandle(AtmosphericGauge.Compass)
if compass_gauge ~= nil then
    AtmosphericGauge:init()
    compass_gauge.RenderFunction = function(gauge)
        AtmosphericGauge:Draw(gauge, compass_gauge, AtmosphericGauge.Compass)
    end
end
