-- ====================================================================================
-- DCSEX.MATH - MATH AND MATH-RELATED FUNCTIONS
-- ====================================================================================
-- (Constant) DCSEx.math.TWO_PI
-- DCSEx.math.addVec(vecA, vecB)
-- DCSEx.math.clamp(val, min, max)
-- DCSEx.math.getBearing(point, refPoint, returnAsNESWstring)
-- DCSEx.math.getDistance2D(vec2a, vec2b)
-- DCSEx.math.getDistance3D(vec3a, vec3b)
-- DCSEx.math.getRelativeHeading(point, refObject, format)
-- DCSEx.math.getVec2FromAngle(angle)
-- DCSEx.math.isPointInsideCircle(center, radius, vec2)
-- DCSEx.math.isPointInsidePolygon(polygon, vec2)
-- DCSEx.math.isSamePoint(pointA, pointB)
-- DCSEx.math.lerp(val0, val1, t)
-- DCSEx.math.multVec2(vec2, mult)
-- DCSEx.math.normalizeVec2(vec2)
-- DCSEx.math.randomBoolean()
-- DCSEx.math.randomFloat(min, max)
-- DCSEx.math.randomPointAtDistance(point, distance)
-- DCSEx.math.randomPointInCircle(center, radius, minRadius, surfaceType)
-- DCSEx.math.randomSign()
-- DCSEx.math.toBoolean(val)
-- DCSEx.math.vec2ToVec3(vec2, y)
-- DCSEx.math.vec3ToVec2(vec3)
-- ====================================================================================

DCSEx.math = {}

-------------------------------------
-- Two times Pi
-------------------------------------
DCSEx.math.TWO_PI = math.pi * 2

-------------------------------------
-- Returns the sum of two vec2 or vec3
-------------------------------------
-- @param vecA A vector
-- @param vecB Another vector
-- @return The sum of both vectors
-------------------------------------
function DCSEx.math.addVec(vecA, vecB)
    if vecA.z then
        return { x = vecA.x + vecB.x, y = vecA.y + vecB.y, z = vecA.z + vecB.z }
    end

    return { x = vecA.x + vecB.x, y = vecA.y + vecB.y }
end

-------------------------------------
-- Clamp a number value between min and max
-------------------------------------
-- @param value The value to clamp
-- @param min Minimum allowed value
-- @param max Maximum allowed value
-- @return Clamped value
-------------------------------------
function DCSEx.math.clamp(val, min, max)
    return math.max(min, math.min(max, val))
end

-------------------------------------
-- Gets the bearing between two vectors, in degrees
-------------------------------------
-- @param point A vec2/vec3
-- @param refPoint Vec2/vec3 to use as a reference point
-- @param returnAsNESWstring Should the value be returned as a N/S/E/W string instead of a numeric value
-- @return Bearing, as a number, in degrees
-------------------------------------
function DCSEx.math.getBearing(point, refPoint, returnAsNESWstring)
    returnAsNESWstring = returnAsNESWstring or false

    refPoint = refPoint or {x = 0, y = 0}
    if point.z then point = DCSEx.math.vec3ToVec2(point) end -- Point is a vec3, convert it
    if refPoint.z then refPoint = DCSEx.math.vec3ToVec2(refPoint) end -- Point is a vec3, convert it

    local bearing = math.atan2(point.y - refPoint.y, point.x - refPoint.x)

    if bearing < 0 then
        bearing = bearing + DCSEx.math.TWO_PI
    end

    bearing = math.floor(bearing * (180 / math.pi))

    if returnAsNESWstring then
        bearing = math.floor((bearing / 45.0) + .5)
        local namesArray = { "north", "northeast", "east", "southeast", "south", "southwest", "west", "northwest" }
        return namesArray[(bearing % 8) + 1]
    end

    return bearing
end

-------------------------------------
-- Returns the pythagorean distance between two 2D points or the length of a single vector
-------------------------------------
-- @param vec2a A 2D point
-- @param vec2b (optional) Another 2D point
-- @return Distance between the points
-------------------------------------
function DCSEx.math.getDistance2D(vec2a, vec2b)
    vec2b = vec2b or { x = 0, y = 0 }
    if vec2a.z then vec2a = DCSEx.math.vec3ToVec2(vec2a) end
    if vec2b.z then vec2b = DCSEx.math.vec3ToVec2(vec2b) end

    return math.sqrt((vec2a.x - vec2b.x) ^ 2 + (vec2a.y - vec2b.y) ^ 2)
end

-------------------------------------
-- Returns the pythagorean distance between two 3D points or the length of a single vector
-------------------------------------
-- @param vec3a A 3D point
-- @param vec3b (optional) Another 3D point
-- @return Distance between the points
-------------------------------------
function DCSEx.math.getDistance3D(vec3a, vec3b)
    vec3b = vec3b or { x = 0, y = 0, z = 0 }

    return math.sqrt((vec3a.x - vec3b.x) ^ 2 + (vec3a.y - vec3b.y) ^ 2 + (vec3a.z - vec3b.z) ^ 2)
end

-------------------------------------
-- Returns the relative heading difference between refObject and a given point
-------------------------------------
-- @param point The point for which to check the relative heading
-- @param refObject The reference object against which relative heading should be measured
-- @param format (optional) Return format. Possible formats are "clock" (1 o'clock...) or "cardinal" (NNW...)
-- @return A number in degrees, or a string if a special format was provided
-------------------------------------
function DCSEx.math.getRelativeHeading(point, refObject, format)
    if not point then return nil end
    if not refObject then return nil end

    local unitpos = refObject:getPosition()
    local bearing = DCSEx.math.getBearing(point, unitpos:getPoint())

    local unitBearing = math.atan2(unitpos.x.z, unitpos.x.x)
    if unitBearing < 0 then unitBearing = unitBearing + DCSEx.math.TWO_PI end
    unitBearing = math.floor(unitBearing * (180 / math.pi))

    local relBearing = bearing - unitBearing
    if relBearing < 0 then relBearing = relBearing + 360 end

    if format == "oclock" then
        local oClock = math.floor(relBearing / 30)
        if oClock <= 0 then oClock = 12 end

        return tostring(oClock).." o'clock"
    elseif format == "cardinal" then
        return "TODO" -- TODO: cardinal
    end

    return math.floor(relBearing)
end

-------------------------------------
-- Returns an normalized vec2 from an angle/bearing in radians
-------------------------------------
-- @param unit Angle/bearing in radians
-- @return A normalized vec2
-------------------------------------
function DCSEx.math.getVec2FromAngle(angle)
    return {x = math.cos(angle), y = math.sin(angle)}
end

-------------------------------------
-- Is a point inside a circle?
-------------------------------------
-- @param center The center of the circle, as a vec2
-- @param radius The radius of the circle
-- @param vec2 A vec2
-- @return True if vec2 is inside the circle, false otherwise
-------------------------------------
function DCSEx.math.isPointInsideCircle(center, radius, vec2)
    return (vec2.x - center.x) ^ 2 + (vec2.y - center.y) ^ 2 < radius ^ 2
end

-------------------------------------
-- Is a point inside a polygon?
-------------------------------------
-- @param vec2[] A polygon, as a table of vec2
-- @param vec2 A vec2
-- @return True if vec2 is inside the polygon, false otherwise
-------------------------------------
function DCSEx.math.isPointInsidePolygon(polygon, vec2)
    if not polygon or not vec2 then return false end

    local oddNodes = false
    local j = #polygon
    for i = 1, #polygon do
        if (polygon[i].y < vec2.y and polygon[j].y >= vec2.y or polygon[j].y < vec2.y and polygon[i].y >= vec2.y) then
            if
                (polygon[i].x + (vec2.y - polygon[i].y) / (polygon[j].y - polygon[i].y) * (polygon[j].x - polygon[i].x) <
                    vec2.x)
             then
                oddNodes = not oddNodes
            end
        end
        j = i
    end

    return oddNodes
end

-------------------------------------
-- Compares two 2D or 3D points
-------------------------------------
-- @param pointA a Point2 or Point3
-- @param pointB another Point2 or Point3
-- @return True if points are the same, false otherwise
-------------------------------------
function DCSEx.math.isSamePoint(pointA, pointB)
    if pointA.x ~= pointB.x then return false end
    if pointA.y ~= pointB.y then return false end
    if pointA.z or pointB.z then
        if pointA.z ~= pointB.z then return false end
    end

    return true
end

-------------------------------------
-- Linearly interpolates two numbers
-------------------------------------
-- @param val0 Value vers l=0
-- @param val1 Value vers l=1
-- @param t Interpolation between 0 and 1
-- @return A number value
-------------------------------------
function DCSEx.math.lerp(val0, val1, t)
    return val0 + t * (val1 - val0);
end

-------------------------------------
-- Multiplies both the x and y components of a vec2 by a floating-point value
-------------------------------------
-- @param vec2 A vec2
-- @param mult A floating-point value
-- @return A vec2
-------------------------------------
function DCSEx.math.multVec2(vec2, mult)
    return {x = vec2.x * mult, y = vec2.y * mult}
end

-------------------------------------
-- Returns an normalized vec2
-------------------------------------
-- @param unit A vec2
-- @return A normalized vec2
-------------------------------------
function DCSEx.math.normalizeVec2(vec2)
    local length = DCSEx.math.getDistance2D(vec2)
    return {x = vec2.x / length, y = vec2.y / length}
end

-------------------------------------
-- Returns a random boolean
-------------------------------------
-- @return A boolean
-------------------------------------
function DCSEx.math.randomBoolean()
    return DCSEx.math.random(1, 2) == 1
end

-------------------------------------
-- Returns a random floating-point number between min and max
-------------------------------------
-- @param min Minimum floating-point value
-- @param max Maximum floating-point value
-- @return A number
-------------------------------------
function DCSEx.math.randomFloat(min, max)
    if min >= max then return min end
    return min + math.random() * (max - min)
end

-------------------------------------
-- Returns a random vec2 at a given distance of another vec2
-------------------------------------
-- @param point Reference point
-- @param distance Distance from the reference point
-- @return A vec2
-------------------------------------
function DCSEx.math.randomPointAtDistance(point, distance)
    local angle = math.random() * math.pi * 2.0

    local x = point.x + math.cos(angle) * distance
    local y = point.y + math.sin(angle) * distance
    return { x = x, y = y }
end

-------------------------------------
-- Returns a random vec2 in circle of a given center and radius
-------------------------------------
-- @param center Center of the circle as a vec2
-- @param radius Radius of the circle
-- @param minRadius (optional) Minimum inner radius circle in which points should not be spawned
-- @param surfaceType (optional) If not nil, point must be of given surface type
-- @return A vec2 or nil if no point was found
-------------------------------------
function DCSEx.math.randomPointInCircle(center, radius, minRadius, surfaceType)
    local minRadius = minRadius or 0
    local dist = minRadius + math.random() * (radius - minRadius)
    local angle = math.random() * math.pi * 2.0

    local point = nil
    for i=1,36 do
        local x = center.x + math.cos(angle) * dist
        local y = center.y + math.sin(angle) * dist

        point = { x = x, y = y }

        if not surfaceType then return point end
        if land.getSurfaceType(point) == surfaceType then return point end
        angle = angle + 0.174533 -- Increment angle by 10° (in radians)
    end

    return nil
end

-------------------------------------
-- Returns a random sign as a number, -1 or 1
-------------------------------------
-- @return -1 50% of the time, 1 50% of the time
-------------------------------------
function DCSEx.math.randomSign()
    if math.random() < .5 then
        return -1
    end
    return 1
end

-------------------------------------
-- Converts a value to a boolean
-------------------------------------
-- @param val Value to convert
-- @return A boolean, or nil if val was nil
-------------------------------------
function DCSEx.math.toBoolean(val)
    if val == nil then return nil end
    if not val then return false end
    if val == 0 then return false end
    if val:lower() == "false" or val:lower() == "no" or val:lower() == "off" then return false end

    return true
end

-------------------------------------
-- Converts a vec2 to a vec3
-------------------------------------
-- @param vec2 A vec2
-- @param y (Optional) A value for the vec3's y component or "land" to use land height
-- @return A vec3 where v3.x=v2.x, v3.y=y and v3.z=v2.y
-------------------------------------
function DCSEx.math.vec2ToVec3(vec2, y)
    -- Value was already a vec3
    if vec2.z then
        return {x = vec2.x, y = vec2.y, z = vec2.z}
    end

    y = y or 0
    if y == "land" then y = land.getHeight(vec2) end

    return {x = vec2.x, y = y, z = vec2.y}
end

-------------------------------------
-- Converts a vec3 to a vec2
-------------------------------------
-- @param vec3 A vec3
-- @return A vec2 where v2.x=v3.x and v2.y=v3.z
-------------------------------------
function DCSEx.math.vec3ToVec2(vec3)
    -- Value was already a vec2
    if not vec3.z then
        return {x = vec3.x, y = vec3.y}
    end

    return {x = vec3.x, y = vec3.z}
end
