local enable_debug = false
local print_tip = function(s, ...)
    local f = print_tip or printf
    if enable_debug then
        return f("Geometry Ray: " .. s, ...)
    end
end

local function throttle(func, tg_throttle)
    local tg = 0
    if not tg_throttle or tg_throttle == 0 then
        return function(...)
            local t = time_global()
            if t ~= tg then
                tg = t
                return func(...)
            end
        end
    else
        return function(...)
            local t = time_global()
            if t < tg then return end
            tg = t + tg_throttle
            return func(...)
        end
    end
end

-- Check for material (engine edit required)
function lshift(x, by)
    return x * 2 ^ by
end

function test(x, mask)
    return bit_and(x, mask) == mask
end

local flags_test = {
    ["flBreakable"] = lshift(1, 0),
    ["flBounceable"] = lshift(1, 2),
    ["flSkidmark"] = lshift(1, 3),
    ["flBloodmark"] = lshift(1, 4),
    ["flClimable"] = lshift(1, 5),
    ["flPassable"] = lshift(1, 7),
    ["flDynamic"] = lshift(1, 8),
    ["flLiquid"] = lshift(1, 9),
    ["flSuppressShadows"] = lshift(1, 10),
    ["flSuppressWallmarks"] = lshift(1, 11),
    ["flActorObstacle"] = lshift(1, 12),
    ["flNoRicoshet"] = lshift(1, 13),
    ["flInjurious"] = lshift(1, 28),
    ["flShootable"] = lshift(1, 29),
    ["flTransparent"] = lshift(1, 30),
    ["flSlowDown"] = lshift(1, 31),
}

--[[

// material exports
.def_readonly( "material_name"                    , &script_rq_result::pMaterialName )
.def_readonly( "material_flags"                   , &script_rq_result::pMaterialFlags )
.def_readonly( "material_phfriction"              , &script_rq_result::fPHFriction )
.def_readonly( "material_phdamping"               , &script_rq_result::fPHDamping )
.def_readonly( "material_phspring"                , &script_rq_result::fPHSpring )
.def_readonly( "material_phbounce_start_velocity" , &script_rq_result::fPHBounceStartVelocity )
.def_readonly( "material_phbouncing"              , &script_rq_result::fPHBouncing )
.def_readonly( "material_flotation_factor"        , &script_rq_result::fFlotationFactor )
.def_readonly( "material_shoot_factor"            , &script_rq_result::fShootFactor )
.def_readonly( "material_shoot_factor_mp"         , &script_rq_result::fShootFactorMP )
.def_readonly( "material_bounce_damage_factor"    , &script_rq_result::fBounceDamageFactor )
.def_readonly( "material_injurious_speed"         , &script_rq_result::fInjuriousSpeed )
.def_readonly( "material_vis_transparency_factor" , &script_rq_result::fVisTransparencyFactor )
.def_readonly( "material_snd_occlusion_factor"    , &script_rq_result::fSndOcclusionFactor )
.def_readonly( "material_density_factor"          , &script_rq_result::fDensityFactor )

]]

-- Geometry Ray class by Thial, edited by demonized
class "geometry_ray"

--[[
(At least one range parameter should be specified)
ray_range:
    Defines the total range of the ray. If you want to attach the ray to
    a fast moving object it is good to extend the ray so that you can reduce
    the polling rate by using the get function.
contact_range:
    Defines the distance at which the result will report being in contact.
    You can skip it or set it to a value lower than the ray_range to
    still be able to get the intersection position from the result while
    marking the ray as not being in contact yet
distance_offset:
    Defines how much the intersection position is offset.
    You can use both positive and negative values or you can leave it blank.
flags (bit map = values can be added together for combined effect):
    0 : None
    1 : Objects
    2 : Statics
    4 : Shapes
    8 : Obstacles
]]--
function geometry_ray:__init(args)
    local args = args or {}
    if args.ray_range == nil and args.contact_range == nil then
        return nil
    end

    self.ray_range = args.ray_range or args.contact_range
    self.contact_range = args.contact_range or args.ray_range
    self.distance_offset = args.distance_offset ~= nil and args.distance_offset or 0
    self.ray = ray_pick()
    self.ray:set_flags(args.flags or 2)
    self.ray:set_range(self.ray_range)
    self.visualize = args.visualize
    self.visualize_end_pos_only = args.visualize_end_pos_only
    if args.ignore_object then
        self.ray:set_ignore_object(args.ignore_object)
    end
end

--[[
position:
    position from which the ray will start
direction:
    direction in which the ray will be fired
]]--
function geometry_ray:get(position, direction)    
    if position == nil or direction == nil then
        return nil
    end

    local position = vector():set(position)
    local direction = vector():set(direction)

    self.ray:set_position(position)
    self.ray:set_direction(direction)
    local res = self.ray:query()
    local distance = res and self.ray:get_distance() or self.ray_range
    local result = {}
    if self.visualize then
        local init_pos = vector():set(position)
        local end_pos = vector():mad(init_pos, direction, distance)
        VisualizeRay(init_pos, end_pos, nil, nil, nil, self.visualize_end_pos_only)
    end
    result.in_contact = distance <= self.contact_range
    result.position = position:add(direction:mul(distance + self.distance_offset))
    result.distance = distance
    result.raw_distance = self.ray:get_distance()
    result.success = res
    result.object = self.ray:get_object()
    result.element = self.ray:get_element()
    result.result = self.ray:get_result()

    -- Cast to Lua table
    -- if result.result then
    --     local r = {}
    --     r.material_name = result.result.material_name
    --     r.material_flags = result.result.material_flags
    --     r.material_phfriction = result.result.material_phfriction
    --     r.material_phdamping = result.result.material_phdamping
    --     r.material_phspring = result.result.material_phspring
    --     r.material_phbounce_start_velocity = result.result.material_phbounce_start_velocity
    --     r.material_phbouncing = result.result.material_phbouncing
    --     r.material_flotation_factor = result.result.material_flotation_factor
    --     r.material_shoot_factor = result.result.material_shoot_factor
    --     r.material_shoot_factor_mp = result.result.material_shoot_factor_mp
    --     r.material_bounce_damage_factor = result.result.material_bounce_damage_factor
    --     r.material_injurious_speed = result.result.material_injurious_speed
    --     r.material_vis_transparency_factor = result.result.material_vis_transparency_factor
    --     r.material_snd_occlusion_factor = result.result.material_snd_occlusion_factor
    --     r.material_density_factor = result.result.material_density_factor

    --     result.result = r
    -- end

    return result
end

-- Engine edit required for testing materials
-- If not possible to get material - return nil
function geometry_ray:isMaterialFlag(flag)
    local result = self.ray:get_result()
    if not result then
        return
    end

    if not result.material_flags then
        return
    end

    if not flags_test[flag] then
        return
    end

    return test(result.material_flags, flags_test[flag])
end

function geometry_ray:getMaterialFlags()
    local result = self.ray:get_result()
    if not result then
        return
    end

    if not result.material_flags then
        return
    end

    local res = {}
    for k, v in pairs(flags_test) do
        res[k] = test(result.material_flags, v)
    end

    return res
end

-- Utils
-- Check if values are similar to a precision
function similar(float1, float2, epsilon)
    return math.abs(float1 - float2) <= (epsilon or 0.0001)
end

function vec_similar(vec1, vec2, epsilon)
    return similar(vec1.x, vec2.x, epsilon) and similar(vec1.y, vec2.y, epsilon) and similar(vec1.z, vec2.z, epsilon)
end

-- Linear inter/extrapolation
function lerp(a, b, f)
    if a and b and f then
        return a + f * (b - a)
    else
        return a or b or 0
    end
end

-- Visualize ray from one point to other with particles playing at setted step
class "VisualizeRay"
local lineId = 70000
function VisualizeRay:__init(init_pos, end_pos, particle_step, visualize_time, force_stop, draw_end_only, color)
    self.init_pos = init_pos
    self.end_pos = end_pos
    self.visualize_time = visualize_time or 3000
    self.particle_step = particle_step or 0.02
    self.force_stop = force_stop
    self.force_stop_default = force_stop
    self.time = 0
    self.draw_end_only = draw_end_only
    self.color = color or fcolor():set(0,1,0,1)

    self.start = function()
        lineId = lineId + 1
        local id = lineId
        local line = debug_render.add_object(id, DBG_ScriptObject.line):cast_dbg_line()
        line.point_a = init_pos
        line.point_b = end_pos
        line.visible = true
        line.color = self.color
        CreateTimeEvent("geometry_ray_stop", id, self.visualize_time / 1000, function()
            debug_render.remove_object(id)
            lineId = lineId - 1
            return true
        end)
    end
    self.start()

    self.reset_time = function()
        self.time = 0
    end

    self.reset = function()
        self.time = 0
        self.force_stop = self.force_stop_default
        self.start()
    end

    self.stop = function()
        self.time = self.visualize_time + 1
        self.force_stop = true
    end
end

local EPS = 0.0000100
local function fsimilar(value, to, eps)
    return math.abs(value - to) < eps
end

local function generate_orthonormal_basis_normalized(d)
    local dir = vector():set(d):normalize()
    local up = vector():set(0,0,0)
    local right = vector():set(0,0,0)
    local fInvLength
    if (fsimilar(dir.y, 1.0, EPS)) then
        up:set(0, 0, 1)
        fInvLength = 1 / math.sqrt(dir.x * dir.x + dir.y * dir.y)
        right.x = -dir.y * fInvLength
        right.y = dir.x * fInvLength
        right.z = 0
        up.x = -dir.z * right.y
        up.y = dir.z * right.x
        up.z = dir.x * right.y - dir.y * right.x
    else
        up:set(0, 1, 0)
        fInvLength = 1 / math.sqrt(dir.x * dir.x + dir.z * dir.z)
        right.x = dir.z * fInvLength
        right.y = 0
        right.z = -dir.x * fInvLength
        up.x = dir.y * right.z
        up.y = dir.z * right.x - dir.x * right.z
        up.z = -dir.y * right.x
    end
    return dir, up, right
end

-- Get surface normals by Aoldri, edited by demonized
function get_surface_normal(pos, dir, ray_props, visualize_props, initial_ray)
    local function get_geometry_ray()
        return geometry_ray({
            ray_range = ray_props and ray_props.ray_range or 1000,
            visualize = ray_props and ray_props.visualize,
            flags = ray_props and ray_props.flags or (1+2),
            ignore_object = db.actor,
        })
    end

    -- Get player's camera position and direction in world space
    local pos0 = pos and vector():set(pos) or device().cam_pos
    local angle1 = dir and vector():set(dir) or device().cam_dir

    -- Generate two positions orthogonal to camera direction and each other
    local angle1, pos01, pos02 = generate_orthonormal_basis_normalized(angle1)
    pos01 = pos01:mul(0.01)
    pos02 = pos02:mul(0.01)

    pos01 = pos01:add(pos0)
    pos02 = pos02:add(pos0)

    -- Get positions of intersections of rays around pos0
    local pos1
    local res
    if initial_ray then
        pos1 = initial_ray.position
        res = initial_ray
    else
        local ray = get_geometry_ray()
        res = ray:get(pos0, angle1)
        pos1 = res.position
    end

    local ray = get_geometry_ray()
    local pos2 = ray:get(pos01, angle1).position

    local ray = get_geometry_ray()
    local pos3 = ray:get(pos02, angle1).position

    if not res.success then
        -- print_tip("cant get normal by pos %s, dir %s", pos0, angle1)
        return
    end

    -- VisualizeRay(pos0, pos1, nil, 300)
    -- VisualizeRay(pos01, pos2, nil, 300)
    -- VisualizeRay(pos02, pos3, nil, 300)

    -- Get vectors from intersection points from pos1
    local vec2 = vec_sub(pos1, pos2)
    local vec3 = vec_sub(pos1, pos3)

    -- Find normal vector of surface by taking cross product of intersection vectors
    local cross = (vector_cross(vec2, vec3)):normalize()   

    -- If the direction and normal vectors heading in similar direction - invert normal
    local deg = angle1:dotproduct(cross)
    if deg > 0 then
        cross:invert()
    end

    cross.x = clamp(cross.x, -1, 1)
    cross.y = clamp(cross.y, -1, 1)
    cross.z = clamp(cross.z, -1, 1)

    if visualize_props then
        local time = visualize_props.visualize_time or 300
        local color = visualize_props.color or fcolor():set(0,1,0,1)
        VisualizeRay(pos1, vector():set(pos1):add(cross), nil, time, nil, nil, color)
    end
    return cross
end