--[[
    HUUUGGGGEEEE thanks to.. again, a327ex: https://github.com/a327ex/Anchor-dashed-lines/blob/main/anchor/math.lua
]]

-- Returns the 2D coordinates of a given index with a grid of a given width
-- math.index_to_coordinates(11, 10) -> 1, 2
-- math.index_to_coordinates(2, 4) -> 2, 1
-- math.index_to_coordinates(17, 7) -> 3, 3
-- math.index_to_coordinates(17, 4) -> 1, 5
-- math.index_to_coordinates(4, 4) -> 4, 1
function math.index_to_coordinates(n, w)
    local i, j = n % w, math.ceil(n / w)
    if i == 0 then i = w end
    return i, j
end

-- Returns the 1D index of the given 2D coordinates with a grid of a given width
-- math.coordinates_to_index(1, 2, 10) -> 11
-- math.coordinates_to_index(2, 1, 4) -> 2
-- math.coordinates_to_index(3, 3, 7) -> 17
-- math.coordinates_to_index(1, 5, 4) -> 17
-- math.coordinates_to_index(4, 1, 4) -> 4
function math.coordinates_to_index(i, j, w)
    return i + (j - 1) * w
end

-- Rotates the point by r radians with ox, oy as pivot.
-- x, y = math.rotate_point(player.x, player.y, math.pi/4)
function math.rotate_point(x, y, r, ox, oy)
    return x * math.cos(r) - y * math.sin(r) + ox - ox * math.cos(r) + oy * math.sin(r),
        x * math.sin(r) + y * math.cos(r) + oy - oy * math.cos(r) - ox * math.sin(r)
end

-- Scales the point by sx, sy with ox, oy as pivot.
-- x, y = math.scale_point(player.x, player.y, 2, 2, player.x - player.w/2, player.y - player.h/2)
function math.scale_point(x, y, sx, sy, ox, oy)
    return x * sx + ox - ox * sx, y * sy + oy - oy * sy
end

-- Rotates and scales the point by r radians and sx, sy with ox, oy as pivot.
-- x, y = math.rotate_scale_point(player.x, player.y, math.pi/4, 2, 2, player.x - player.w/2, player.y - player.h/2)
function math.rotate_scale_point(x, y, r, sx, sy, ox, oy)
    local rx, ry = math.rotate_point(x, y, r, ox, oy)
    return math.scale_point(rx, ry, sx, sy, ox, oy)
end

-- Returns -1 if the angle is on either left quadrants and 1 if its on either right quadrants.
-- h = math.angle_to_horizontal(math.pi/4) -> 1
-- h = math.angle_to_horizontal(-math.pi/4) -> 1
-- h = math.angle_to_horizontal(-3*math.pi/4) -> -1
-- h = math.angle_to_horizontal(3*math.pi/4) -> -1
function math.angle_to_horizontal(r)
    if r > math.pi / 2 or r < -math.pi / 2 then
        return -1
    elseif r <= math.pi / 2 and r >= -math.pi / 2 then
        return 1
    end
end

-- Returns -1 if the angle is on either bottom quadrants and 1 if its on either top quadrants.
-- h = math.angle_to_horizontal(math.pi/4) -> -1
-- h = math.angle_to_horizontal(-math.pi/4) -> 1
-- h = math.angle_to_horizontal(-3*math.pi/4) -> 1
-- h = math.angle_to_horizontal(3*math.pi/4) -> -1
function math.angle_to_vertical(r)
    if r > 0 and r < math.pi then
        return -1
    elseif r <= 0 and r >= -math.pi then
        return 1
    end
end

-- Snaps the value v to the closest number divisible by x and then centers it. This is useful when doing calculations for grids where each cell would be of size x, for instance.
-- v = math.snap_center(12, 16) -> 8
-- v = math.snap_center(17, 16) -> 24
-- v = math.snap_center(12, 12) -> 6
-- v = math.snap_center(13, 12) -> 18
function math.snap_center(v, x)
    return math.ceil(v / x) * x - x / 2
end

-- Returns the squared distance between both points.
-- d = math.distance(player.x, player.y, enemy.x, enemy)
function math.distance(x1, y1, x2, y2)
    return math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1))
end

function math.distance3(x1, y1, z1, x2, y2, z2)
    return math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) + (z2 - z1) * (z2 - z1))
end

-- Loops value t such that is never higher than length and never lower than 0.
-- v = math.loop(3, 2.5) -> 0.5
-- v = math.loop(3*math.pi, 2*math.pi) -> math.pi
function math.loop(t, length)
    return math.clamp(t - math.floor(t / length) * length, 0, length)
end

-- Clamps the value to between 0 and 1.
-- v = math.clamp01(-0.1) -> 0
-- v = math.clamp01(1.1) -> 1
function math.clamp01(v)
    if v < 0 then
        return 0
    elseif v > 1 then
        return 1
    else
        return v
    end
end

-- Lerps the src angle towards dst using value as the lerp amount.
-- enemy.r = math.lerp_angle(0.2, enemy.r, enemy:angle_to_object(player))
-- function math.lerp_angle(value, src, dst)
function math.lerp_angle(from, to, weight)
    local dt = math.loop((to - from), 2 * math.pi)
    if dt > math.pi then dt = dt - 2 * math.pi end
    return from + dt * math.clamp01(weight)
end

-- Rounds the number n to p digits of precision.
-- n = math.round(10.94, 1) -> 10.9
-- n = math.round(45.321, 0) -> 45
-- n = math.round(101.9157289403, 5) -> 101.91572
function math.round(n, p)
    local m = 10 ^ (p or 0)
    return math.floor(n * m + 0.5) / m
end

-- Floors value v to the closest number divisible by x.
-- v = math.snap(15, 16) -> 0
-- v = math.snap(17, 16) -> 16
-- v = math.snap(13, 4) -> 12
function math.snap(v, x)
    return math.round(v / x, 0) * x
end

-- Clamps value v between min and max.
-- v = math.clamp(-4, 0, 10) -> 0
-- v = math.clamp(83, 0, 10) -> 10
-- v = math.clamp(0, -10, -4) -> -4
function math.clamp(v, min, max)
    return math.min(math.max(v, min), max)
end

-- Returns the squared length of x, y.
-- l = math.length(x, y)
function math.length(x, y)
    return math.sqrt(x * x + y * y)
end

-- Returns the normalized values of x, y.
-- nx, ny = math.normalize(x, y)
function math.normalize(x, y)
    if math.abs(x) < 0.0001 and math.abs(y) < 0.0001 then return x, y end
    local l = math.length(x, y)
    return x / l, y / l
end

-- Returns the sign of value v.
-- s = math.sign(10) -> 1
-- s = math.sign(-10) -> -1
-- s = math.sign(0) -> 0
function math.sign(v)
    if v > 0 then
        return 1
    elseif v < 0 then
        return -1
    else
        return 0
    end
end

-- Returns the angle between point x, y and point px, py.
-- r = math.angle(player.x, player.y, enemy.x, enemy)
function math.angle(x, y, px, py)
    return math.atan2(py - y, px - x)
end

-- Remaps value v using its previous range of old_min, old_max into the new range new_min, new_max.
-- v = math.remap(10, 0, 20, 0, 1) -> 0.5 because 10 is 50% of 0, 20 and thus 0.5 is 50% of 0, 1
-- v = math.remap(3, 0, 3, 0, 100) -> 100
-- v = math.remap(2.5, -5, 5, -100, 100) -> 50
function math.remap(v, old_min, old_max, new_min, new_max)
    return ((v - old_min) / (old_max - old_min)) * (new_max - new_min) + new_min
end

local PI = math.pi
local PI2 = math.pi / 2
local LN2 = math.log(2)
local LN210 = 10 * math.log(2)


-- TODO: fix this since it doesn't work properly for some reason
-- Lerp corrected for usage with delta time, see more here https://www.construct.net/en/blogs/ashleys-blog-2/using-lerp-delta-time-924
-- f is a value between 0 and 1 that corresponds to how much of the distance between src and dst will be covered per second, regardless of frame rate.
-- math.lerp_dt(0.25, dt, self.x, self.x + 100) -> will cover 25% of the distance between self.x and self.x + 100 per second
function math.lerp_dt(f, dt, src, dst)
    return math.lerp((1 - f ^ dt), src, dst)
end

-- Lerps 'from' to 'to' with lerp 'weight'.
-- v = math.lerp(0.2, self.x, self.x + 100)
function math.lerp(from, to, weight)
    return from * (1 - weight) + to * weight
end

function math.aabb(ax, ay, aw, ah, bx, by, bw, bh)
    return ax < bx + bw and bx < ax + aw and ay < by + bh and by < ay + ah
end


--[[
  Returns the angle from point x1, y1 to point x2, y2
  Examples:
    math.angle_to_point()           -> error, expects 4 numbers
    math.angle_to_point(0, 0, 0, 0) -> 0
    math.angle_to_point(0, 0, 1, 1) -> math.pi/4
    math.angle_to_point(0, 0, 1, 0) -> 0
    math.angle_to_point(0, 0, 0, 1) -> math.pi/2
]]--
function math.angle_to_point(x1, y1, x2, y2)
  return math.atan2(y2 - y1, x2 - x1)
end


local PI = math.pi
local PI2 = math.pi / 2
local LN2 = math.log(2)
local LN210 = 10 * math.log(2)



--[[
    NOTE: for curve's behavior and stuff, see: https://hump.readthedocs.io/en/latest/timer.html
]]
function math.linear(t)
    return t
end

function math.sine_in(t)
    if t == 0 then
        return 0
    elseif t == 1 then
        return 1
    else
        return 1 - math.cos(t * PI2)
    end
end

function math.sine_out(t)
    if t == 0 then
        return 0
    elseif t == 1 then
        return 1
    else
        return math.sin(t * PI2)
    end
end

function math.sine_in_out(t)
    if t == 0 then
        return 0
    elseif t == 1 then
        return 1
    else
        return -0.5 * (math.cos(t * PI) - 1)
    end
end

function math.sine_out_in(t)
    if t == 0 then
        return 0
    elseif t == 1 then
        return 1
    elseif t < 0.5 then
        return 0.5 * math.sin((t * 2) * PI2)
    else
        return -0.5 * math.cos((t * 2 - 1) * PI2) + 1
    end
end

function math.quad_in(t)
    return t * t
end

function math.quad_out(t)
    return -t * (t - 2)
end

function math.quad_in_out(t)
    if t < 0.5 then
        return 2 * t * t
    else
        t = t - 1
        return -2 * t * t + 1
    end
end

function math.quad_out_in(t)
    if t < 0.5 then
        t = t * 2
        return -0.5 * t * (t - 2)
    else
        t = t * 2 - 1
        return 0.5 * t * t + 0.5
    end
end

function math.cubic_in(t)
    return t * t * t
end

function math.cubic_out(t)
    t = t - 1
    return t * t * t + 1
end

function math.cubic_in_out(t)
    t = t * 2
    if t < 1 then
        return 0.5 * t * t * t
    else
        t = t - 2
        return 0.5 * (t * t * t + 2)
    end
end

function math.cubic_out_in(t)
    t = t * 2 - 1
    return 0.5 * (t * t * t + 1)
end

function math.quart_in(t)
    return t * t * t * t
end

function math.quart_out(t)
    t = t - 1
    t = t * t
    return 1 - t * t
end

function math.quart_in_out(t)
    t = t * 2
    if t < 1 then
        return 0.5 * t * t * t * t
    else
        t = t - 2
        t = t * t
        return -0.5 * (t * t - 2)
    end
end

function math.quart_out_in(t)
    if t < 0.5 then
        t = t * 2 - 1
        t = t * t
        return -0.5 * t * t + 0.5
    else
        t = t * 2 - 1
        t = t * t
        return 0.5 * t * t + 0.5
    end
end

function math.quint_in(t)
    return t * t * t * t * t
end

function math.quint_out(t)
    t = t - 1
    return t * t * t * t * t + 1
end

function math.quint_in_out(t)
    t = t * 2
    if t < 1 then
        return 0.5 * t * t * t * t * t
    else
        t = t - 2
        return 0.5 * t * t * t * t * t + 1
    end
end

function math.quint_out_in(t)
    t = t * 2 - 1
    return 0.5 * (t * t * t * t * t + 1)
end

function math.expo_in(t)
    if t == 0 then
        return 0
    else
        return math.exp(LN210 * (t - 1))
    end
end

function math.expo_out(t)
    if t == 1 then
        return 1
    else
        return 1 - math.exp(-LN210 * t)
    end
end

function math.expo_in_out(t)
    if t == 0 then
        return 0
    elseif t == 1 then
        return 1
    end
    t = t * 2
    if t < 1 then
        return 0.5 * math.exp(LN210 * (t - 1))
    else
        return 0.5 * (2 - math.exp(-LN210 * (t - 1)))
    end
end

function math.expo_out_in(t)
    if t < 0.5 then
        return 0.5 * (1 - math.exp(-20 * LN2 * t))
    elseif t == 0.5 then
        return 0.5
    else
        return 0.5 * (math.exp(20 * LN2 * (t - 1)) + 1)
    end
end

function math.circ_in(t)
    if t < -1 or t > 1 then
        return 0
    else
        return 1 - math.sqrt(1 - t * t)
    end
end

function math.circ_out(t)
    if t < 0 or t > 2 then
        return 0
    else
        return math.sqrt(t * (2 - t))
    end
end

function math.circ_in_out(t)
    if t < -0.5 or t > 1.5 then
        return 0.5
    else
        t = t * 2
        if t < 1 then
            return -0.5 * (math.sqrt(1 - t * t) - 1)
        else
            t = t - 2
            return 0.5 * (math.sqrt(1 - t * t) + 1)
        end
    end
end

function math.circ_out_in(t)
    if t < 0 then
        return 0
    elseif t > 1 then
        return 1
    elseif t < 0.5 then
        t = t * 2 - 1
        return 0.5 * math.sqrt(1 - t * t)
    else
        t = t * 2 - 1
        return -0.5 * ((math.sqrt(1 - t * t) - 1) - 1)
    end
end

function math.bounce_in(t)
    t = 1 - t
    if t < 1 / 2.75 then
        return 1 - (7.5625 * t * t)
    elseif t < 2 / 2.75 then
        t = t - 1.5 / 2.75
        return 1 - (7.5625 * t * t + 0.75)
    elseif t < 2.5 / 2.75 then
        t = t - 2.25 / 2.75
        return 1 - (7.5625 * t * t + 0.9375)
    else
        t = t - 2.625 / 2.75
        return 1 - (7.5625 * t * t + 0.984375)
    end
end

function math.bounce_out(t)
    if t < 1 / 2.75 then
        return 7.5625 * t * t
    elseif t < 2 / 2.75 then
        t = t - 1.5 / 2.75
        return 7.5625 * t * t + 0.75
    elseif t < 2.5 / 2.75 then
        t = t - 2.25 / 2.75
        return 7.5625 * t * t + 0.9375
    else
        t = t - 2.625 / 2.75
        return 7.5625 * t * t + 0.984375
    end
end

function math.bounce_in_out(t)
    if t < 0.5 then
        t = 1 - t * 2
        if t < 1 / 2.75 then
            return (1 - (7.5625 * t * t)) * 0.5
        elseif t < 2 / 2.75 then
            t = t - 1.5 / 2.75
            return (1 - (7.5625 * t * t + 0.75)) * 0.5
        elseif t < 2.5 / 2.75 then
            t = t - 2.25 / 2.75
            return (1 - (7.5625 * t * t + 0.9375)) * 0.5
        else
            t = t - 2.625 / 2.75
            return (1 - (7.5625 * t * t + 0.984375)) * 0.5
        end
    else
        t = t * 2 - 1
        if t < 1 / 2.75 then
            return (7.5625 * t * t) * 0.5 + 0.5
        elseif t < 2 / 2.75 then
            t = t - 1.5 / 2.75
            return (7.5625 * t * t + 0.75) * 0.5 + 0.5
        elseif t < 2.5 / 2.75 then
            t = t - 2.25 / 2.75
            return (7.5625 * t * t + 0.9375) * 0.5 + 0.5
        else
            t = t - 2.625 / 2.75
            return (7.5625 * t * t + 0.984375) * 0.5 + 0.5
        end
    end
end

function math.bounce_out_in(t)
    if t < 0.5 then
        t = t * 2
        if t < 1 / 2.75 then
            return (7.5625 * t * t) * 0.5
        elseif t < 2 / 2.75 then
            t = t - 1.5 / 2.75
            return (7.5625 * t * t + 0.75) * 0.5
        elseif t < 2.5 / 2.75 then
            t = t - 2.25 / 2.75
            return (7.5625 * t * t + 0.9375) * 0.5
        else
            t = t - 2.625 / 2.75
            return (7.5625 * t * t + 0.984375) * 0.5
        end
    else
        t = 1 - (t * 2 - 1)
        if t < 1 / 2.75 then
            return 0.5 - (7.5625 * t * t) * 0.5 + 0.5
        elseif t < 2 / 2.75 then
            t = t - 1.5 / 2.75
            return 0.5 - (7.5625 * t * t + 0.75) * 0.5 + 0.5
        elseif t < 2.5 / 2.75 then
            t = t - 2.25 / 2.75
            return 0.5 - (7.5625 * t * t + 0.9375) * 0.5 + 0.5
        else
            t = t - 2.625 / 2.75
            return 0.5 - (7.5625 * t * t + 0.984375) * 0.5 + 0.5
        end
    end
end

local overshoot = 1.70158

function math.back_in(t)
    if t == 0 then
        return 0
    elseif t == 1 then
        return 1
    else
        return t * t * ((overshoot + 1) * t - overshoot)
    end
end

function math.back_out(t)
    if t == 0 then
        return 0
    elseif t == 1 then
        return 1
    else
        t = t - 1
        return t * t * ((overshoot + 1) * t + overshoot) + 1
    end
end

function math.back_in_out(t)
    if t == 0 then
        return 0
    elseif t == 1 then
        return 1
    else
        t = t * 2
        if t < 1 then
            return 0.5 * (t * t * (((overshoot * 1.525) + 1) * t - overshoot * 1.525))
        else
            t = t - 2
            return 0.5 * (t * t * (((overshoot * 1.525) + 1) * t + overshoot * 1.525) + 2)
        end
    end
end

function math.back_out_in(t)
    if t == 0 then
        return 0
    elseif t == 1 then
        return 1
    elseif t < 0.5 then
        t = t * 2 - 1
        return 0.5 * (t * t * ((overshoot + 1) * t + overshoot) + 1)
    else
        t = t * 2 - 1
        return 0.5 * t * t * ((overshoot + 1) * t - overshoot) + 0.5
    end
end

local amplitude = 1
local period = 0.0003

function math.elastic_in(t)
    if t == 0 then
        return 0
    elseif t == 1 then
        return 1
    else
        t = t - 1
        return -(amplitude * math.exp(LN210 * t) * math.sin((t * 0.001 - period / 4) * (2 * PI) / period))
    end
end

function math.elastic_out(t)
    if t == 0 then
        return 0
    elseif t == 1 then
        return 1
    else
        return math.exp(-LN210 * t) * math.sin((t * 0.001 - period / 4) * (2 * PI) / period) + 1
    end
end

function math.elastic_in_out(t)
    if t == 0 then
        return 0
    elseif t == 1 then
        return 1
    else
        t = t * 2
        if t < 1 then
            t = t - 1
            return -0.5 * (amplitude * math.exp(LN210 * t) * math.sin((t * 0.001 - period / 4) * (2 * PI) / period))
        else
            t = t - 1
            return amplitude * math.exp(-LN210 * t) * math.sin((t * 0.001 - period / 4) * (2 * PI) / period) * 0.5 + 1
        end
    end
end

function math.elastic_out_in(t)
    if t < 0.5 then
        t = t * 2
        if t == 0 then
            return 0
        else
            return (amplitude / 2) * math.exp(-LN210 * t) * math.sin((t * 0.001 - period / 4) * (2 * PI) / period) + 0.5
        end
    else
        if t == 0.5 then
            return 0.5
        elseif t == 1 then
            return 1
        else
            t = t * 2 - 1
            t = t - 1
            return -((amplitude / 2) * math.exp(LN210 * t) * math.sin((t * 0.001 - period / 4) * (2 * PI) / period)) +
            0.5
        end
    end
end



-- simple collision detection check

local function point_in_polygon(point, polygon)
    -- Ray-casting algorithm
    local x, y = point[1], point[2]
    local inside = false
    local j = #polygon

    for i = 1, #polygon do
        local xi, yi = polygon[i][1], polygon[i][2]
        local xj, yj = polygon[j][1], polygon[j][2]

        if ((yi > y) ~= (yj > y)) and
           (x < (xj - xi) * (y - yi) / (yj - yi + 1e-12) + xi) then
            inside = not inside
        end
        j = i
    end

    return inside
end

local function point_to_segment_distance(px, py, x1, y1, x2, y2)
    local dx, dy = x2 - x1, y2 - y1
    local lengthSq = dx * dx + dy * dy
    local t = ((px - x1) * dx + (py - y1) * dy) / (lengthSq + 1e-12)
    t = math.max(0, math.min(1, t))

    local closestX = x1 + t * dx
    local closestY = y1 + t * dy

    local distX = px - closestX
    local distY = py - closestY

    return math.sqrt(distX * distX + distY * distY)
end

--[[
cx, cy is circle middle position
r is circle radius
polygon is a list of vertices [(x1, y1), (x2, y2), ..., (xn, yn)]
]]
function math.polygon_circle_intersect(polygon, cx, cy, r)
    -- Step 1: check if circle center is inside polygon
    if pointInPolygon({cx, cy}, polygon) then
        return true
    end

    -- Step 2: check distance from circle center to polygon edges
    for i = 1, #polygon do
        local j = i % #polygon + 1
        local x1, y1 = polygon[i][1], polygon[i][2]
        local x2, y2 = polygon[j][1], polygon[j][2]
        local dist = pointToSegmentDistance(cx, cy, x1, y1, x2, y2)
        if dist <= r then
            return true
        end
    end

    return false
end

--[[
ox, oy is ray origin point
dx, dy is ray direction vector
cx, cy is circle middle position
r is circle radius

returns is_hit, length
]]
function math.ray_intersects_circle(ox, oy, dx, dy, cx, cy, r)
    local ocx = ox - cx
    local ocy = oy - cy

    local a = dx*dx + dy*dy
    local b = 2 * (ocx*dx + ocy*dy)
    local c = ocx*ocx + ocy*ocy - r*r

    local discriminant = b*b - 4*a*c

    if discriminant < 0 then
        return false -- no intersection
    end

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

    -- Only consider intersections in front of the ray origin
    if t1 >= 0 then
        return true, t1
    elseif t2 >= 0 then
        return true, t2
    else
        return false
    end
end

-- returns is_hit, length
function math.thick_ray_intersects_circle(ox, oy, dx, dy, cx, cy, target_radius, ray_radius)
    return math.ray_intersects_circle(ox, oy, dx, dy, cx, cy, target_radius + ray_radius)
end
