--- @within gun
-- @compact

local gun_default = Guns4d.gun
local mat4 = leef.math.mat4

--I dont remember why I made this, used it though lmao
function gun_default.multiplier_coefficient(multiplier, ratio)
    return 1+((multiplier*ratio)-ratio)
end

--- The entry method for the update of the gun
--
-- calls virtually all functions that begin with `update` once. Also updates subclass
--
-- @tparam float dt
function gun_default:update(dt)
    --print(self.subclass_instances)
    assert(self.instance, "attempt to call object method on a class")
    if not self:has_entity() then self:add_entity() self:clear_animation() end
    --player look rotation. I'm going to keep it real, I don't remember what this math does. Player handler just stores the player's rotation from MT in degrees, which is for some reason inverted

    --it's set up like this so that if the gun is fired on auto and the RPM is very fast (faster then globalstep) we know how many rounds to let off.
    if self.rechamber_time > 0 then
        self.rechamber_time = self.rechamber_time - dt
    end

    self.time_since_creation = self.time_since_creation + dt
    self.time_since_last_fire = self.time_since_last_fire + dt

    if self.burst_queue > 0 then self:update_burstfire() end
    --update some vectors
    self:update_look_offsets(dt)
    --I should make this into a list
    if self.consts.HAS_SWAY then self:update_sway(dt) end
    if self.consts.HAS_RECOIL then self:update_recoil(dt) end
    if self.consts.HAS_BREATHING then self:update_breathing(dt) end
    if self.consts.HAS_WAG then self:update_wag(dt) end

    self:update_animation(dt)
    self.dir = self:get_dir(nil,nil,nil,self.consts.ANIMATIONS_OFFSET_AIM)
    self.pos = self:get_pos()+self.handler:get_pos()

    --update subclasses
    self:update_entity()
    --this should really be a list of subclasses so its more easily expansible
    for i, instance in pairs(self.subclass_instances) do
        if instance.update then instance:update(dt) end
        if not self.properties.subclasses[i] then
            instance:prepare_deletion()
            self.subclass_instances[i] = nil
        end
    end

    --finalize transforms
    self:update_muzzle_smoke_ent()
    self:update_transforms()
end

local function recursive_unset_overrides(proxy, accessed)
    if not accessed then accessed = {} end
    if accessed[proxy] then return end
    accessed[proxy]=true
    local overrides = proxy.__LEEF_PROXY_OVERRIDES
    local original = leef.class.proxy_table.objects_by_proxy
    for i, v in pairs(overrides) do
        if (type(v)=="table") then
            if (v.__LEEF_PROXY_PARENT==original[i]) then
                recursive_unset_overrides(proxy, accessed)
            else
                overrides[i]=nil
            end
        else
            overrides[i] = nil
        end
    end
end
function gun_default:regenerate_properties()
    --self._PROPERTIES_UNSAFE = Guns4d.table.deep_copy(self.base_class.properties)
    self.PROXY_MODE_SAFE = false
    recursive_unset_overrides(self.properties)
    for _, func in pairs(self.property_modifiers) do
        func(self.properties)
    end
    self.PROXY_MODE_SAFE = true
    self:repair_subclasses()
    self:regenerate_visuals()
end
function gun_default:repair_subclasses()
    for i, base_class in pairs(self.properties.subclasses) do
        local inst = self.subclass_instances[i]
        -- check to see if doesnt exist or isn't the right class
        if (not inst) or (inst.base_class~=base_class) then
            --if it does exist, delete it.
            if (inst) and inst.prepare_deletion then
                inst:prepare_deletion()
            end
            self.subclass_instances[i] = base_class:new({gun=self})
        end
    end
end
function gun_default:update_muzzle_smoke_ent()
    if self.attached_objects.guns4d_muzzle_smoke then
        local ent = self.attached_objects.guns4d_muzzle_smoke
        local b = self.model_bounding_box
        local f = self.properties.visuals.flash_offset
        local v = self.properties.visuals.scale
        local v4  = mat4.mul_vec4({}, self.b3d_model.root_orientation_rest_inverse, {f.x/v,  f.y/v,  (b[3]+f.z)/v, 0})
        local rotation = {self.b3d_model.root_orientation_rest_inverse:get_rot_irrlicht_bone()}

        rotation = {x=rotation[1]*180/math.pi, y=rotation[2]*180/math.pi, z=rotation[3]*180/math.pi}
        --local rotation = vector.new()
        ent:set_attach(self.entity, self.consts.ROOT_BONE, {x=v4[1], y=v4[2], z=v4[3]}, rotation, nil)
    end
end
--- not typically called every step, updates the gun object's visuals
function gun_default:regenerate_visuals()
    local props = self.properties
    self.entity:set_properties({
        mesh = props.visuals.mesh,
        textures = table.copy(props.visuals.textures),
        backface_culling = props.visuals.backface_culling,
        visual_size = {x=10*props.visuals.scale,y=10*props.visuals.scale,z=10*props.visuals.scale}
    })
    for i, ent in pairs(self.attached_objects) do
        if not self.properties.visuals.attached_objects[i] then
            ent:remove()
        end
    end
    for i, attached in pairs(self.properties.visuals.attached_objects) do
        assert(type(attached)=="table", self.name..": `attached.objects` expects a list of tables, incorrect type given.")
        local obj
        if (not self.attached_objects[i]) or (not self.attached_objects[i]:is_valid()) then
            obj = minetest.add_entity(self.handler:get_pos(), "guns4d:gun_entity")
            self.attached_objects[i] = obj
        else
            obj = self.attached_objects[i]
        end
        obj:set_properties({
            visual = (attached.mesh and "mesh") or "sprite", --if mesh present itll still be "mesh" since itll remain unchanged
            mesh = attached.mesh,
            --by default sprites get no textures, meshes get their parent textures.
            textures = (attached.mesh and table.copy(attached.textures or self.properties.visuals.textures)) or table.copy(attached.textures or {"blank.png"} --[[{"blank.png","blank.png","blank.png","blank.png","blank.png","blank.png"}]]),
            backface_culling = attached.backface_culling,
            visual_size = {x=attached.scale or 1,  y=attached.scale or 1,  z=attached.scale or 1}
        })
        local offset
        local rotation
        if attached.offset then
            offset = attached.offset
            offset = mat4.mul_vec4({}, self.b3d_model.root_orientation_rest_inverse, {offset.x, offset.y, offset.z, 0})
            offset = {x=offset[1], y=offset[2], z=offset[3]}
        end
        if attached.rotation then
            rotation = attached.rotation
            local rotm4 = mat4.set_rot_luanti_entity(mat4.identity(), rotation.x*math.pi/180, rotation.y*math.pi/180, rotation.z*math.pi/180)
            rotation = {mat4.mul(mat4.identity(), {self.b3d_model.root_orientation_rest_inverse, rotm4}):get_rot_luanti_entity()}
            rotation = {x=rotation[1]*180/math.pi, y=rotation[2]*180/math.pi, z=rotation[3]*180/math.pi}
        else
            rotation = {(self.b3d_model.root_orientation_rest_inverse):get_rot_luanti_entity()}
            rotation = {x=rotation[1]*180/math.pi, y=rotation[2]*180/math.pi, z=rotation[3]*180/math.pi}
        end
        obj:set_attach(
            self.entity,
            self.consts.ROOT_BONE,
            offset,
            (attached.mesh and rotation) or nil
            --true
        )
    end
end


--- updates self.total_offsets which stores offsets for bones
function gun_default:update_transforms()
    local total_offset = self.total_offsets
    for type, _ in pairs(total_offset) do
        local this_total = total_offset[type]
        --[[local old_x = this_offset.x
        local old_y = this_offset.y
        local old_z = this_offset.z]]
        this_total.x = 0
        this_total.y = 0
        this_total.z = 0
        for i, offset in pairs(self.offsets) do
            if offset[type] and (self.consts.HAS_GUN_AXIAL_OFFSETS or type~="gun_axial") then
                local this_offset = offset[type]
                this_total.x = this_total.x+this_offset.x
                this_total.y = this_total.y+this_offset.y
                this_total.z = this_total.z+this_offset.z
            end
        end
    end
    --total_offset.gun_axial.x = 0; total_offset.gun_axial.y = 0
end

--- Update and fire the queued weapon burst
function gun_default:update_burstfire()
    if self.rechamber_time <= 0 then
        local iter = 1
        while true do
            local success = self:attempt_fire()
            if success then
                self.burst_queue = self.burst_queue - 1
            else
                if not self.ammo_handler:can_spend_round() then
                    self.burst_queue = 0
                end
                break
            end
            iter = iter + 1
        end
    end
end

--- cycles to the next firemode of the weapon
function gun_default:cycle_firemodes()
    --cannot get length using length operator because it's a proxy table
    local length = 0
    for i, v in ipairs(self.properties.firemodes) do
        length = length+1
    end
    self.current_firemode = ((self.current_firemode)%(length))+1

    self.meta:set_int("guns4d_firemode", self.current_firemode)
    self:update_image_and_text_meta()
    self.player:set_wielded_item(self.itemstack)
end

--- update the inventory information of the gun
function gun_default:update_image_and_text_meta(meta)
    meta = meta or self.meta
    local ammo = self.ammo_handler.ammo
    --set the counter
    if ammo.total_rounds == 0 then
        meta:set_string("count_meta", Guns4d.config.empty_symbol)
    else
        if Guns4d.config.show_gun_inv_ammo_count then
            meta:set_string("count_meta", tostring(ammo.total_rounds))
        else
            meta:set_string("count_meta", "F")
        end
    end
    --pick the image
    local image = self.properties.inventory.inventory_image
    if (ammo.total_rounds > 0) and not ammo.magazine_psuedo_empty then
        image = self.properties.inventory.inventory_image
    elseif self.properties.inventory.inventory_image_magless and ( (ammo.loaded_mag == "empty") or (ammo.loaded_mag == "") or ammo.magazine_psuedo_empty) then
        image = self.properties.inventory.inventory_image_magless
    elseif self.properties.inventory.inventory_image_empty then
        image = self.properties.inventory.inventory_image_empty
    end
    --add the firemode overlay to the image
    local firemodes = 0
    for i, v in pairs(self.properties.firemodes) do
        firemodes = firemodes+1
    end
    if firemodes > 1 and self.properties.inventory.firemode_inventory_overlays[self.properties.firemodes[self.current_firemode]] then
        image = image.."^"..self.properties.inventory.firemode_inventory_overlays[self.properties.firemodes[self.current_firemode]]
    end
    if self.handler.infinite_ammo then
        image = image.."^"..self.properties.infinite_inventory_overlay
    end
    meta:set_string("inventory_image", image)
end

--- plays the draw animation and sound for the gun, delays usage.
function gun_default:draw()
    assert(self.instance, "attempt to call object method on a class")
    local props = self.properties
    if props.visuals.animations[props.charging.draw_animation] then
        self:set_animation(props.visuals.animations[props.charging.draw_animation], props.charging.draw_time)
    end
    if props.sounds[props.charging.draw_sound] then
        local sounds = Guns4d.table.deep_copy(props.sounds[props.charging.draw_sound])
        self:play_sounds(sounds)
    end
    self.ammo_handler:chamber_round()
    self.rechamber_time = props.charging.draw_time
end

--- plays the muzzle flash animation
function gun_default:muzzle_flash()
    Guns4d.effects.muzzle_flash(self)
end

--- attempt to fire the gun
function gun_default:attempt_fire()
    assert(self.instance, "attempt to call object method on a class")
    local props = self.properties
    --check if there could have been another round fired between steps.
    if ( (self.rechamber_time + (60/props.firerateRPM) < 0) or (self.rechamber_time <= 0) ) and (not self.ammo_handler.ammo.magazine_psuedo_empty) then
        local spent_bullet = self.ammo_handler:spend_round()
        if spent_bullet and spent_bullet ~= "empty" then
            local dir = self.dir
            local pos = self.pos

            if not Guns4d.ammo.registered_ammo[spent_bullet] then
                minetest.log("error", "unregistered bullet itemstring"..tostring(spent_bullet)..", could not fire gun (player:"..self.player:get_player_name()..")");
                return false
            end

            --begin subtasks
            local bullet_def = Guns4d.table.fill(Guns4d.ammo.registered_ammo[spent_bullet], {
                player = self.player,
                --we don't want it to be doing fuckshit and letting players shoot through walls.
                pos = pos-((self.handler.control_handler.ads and dir*props.ads.offset.z) or dir*props.hip.offset.z),
                --dir = dir, this is now collected directly by calling get_dir so pellets and spread can be handled by the bullet_ray instance.
                gun = self
            })
            Guns4d.bullet_ray:new(bullet_def)
            if props.visuals.animations.fire then
                self:set_animation(props.visuals.animations.fire, nil, false)
            end
            self:recoil()
            self:muzzle_flash()

            --this is gonna have to be optimized eventually because this system is complete ass.
            local fire_sound = Guns4d.table.deep_copy(props.sounds.fire) --important that we copy because play_sounds modifies it.
            fire_sound.pos = self.pos
            self:play_sounds(fire_sound)

            self.rechamber_time = self.rechamber_time + (60/props.firerateRPM)

            --acount for animation rotation in same update firing
            if (self.rechamber_time<(60/props.firerateRPM)) and props.firemodes[self.current_firemode]~="single" then
                self.animation_data.runtime = self.animation_data.runtime + (60/props.firerateRPM)
                self:update_animation_transforms()
            end
            return true
        end
    end
    return false
end
--[[function gun_default:damage()
    assert(self.instance, "attempt to call object method on a class")
    self.itemstack:set_wear(self.itemstack:get_wear()-self.properties.durability.shot_per_wear)
    self.player:set_wielded_item(self.itemstack)
end]]
local function rand_sign(b)
    b = b or .5
    local int = 1
    if math.random() > b then int=-1 end
    return int
end

--- simulate recoil by adding to the recoil velocity (called by attempt_fire)
function gun_default:recoil()
    assert(self.instance, "attempt to call object method on a class")
    local rprops = self.properties.recoil
    for axis, recoil in pairs(self.velocities.recoil) do
        for _, i in pairs({"x","y"}) do
            recoil[i] = recoil[i] + (rprops.angular_velocity[axis][i]
                *rand_sign((rprops.bias[axis][i]/2)+.5))
                *self.multiplier_coefficient(rprops.hipfire_multiplier[axis], 1-self.control_handler.ads_location)
            --set original velocity
            self.velocities.init_recoil[axis][i] = recoil[i]
        end
        local length = math.sqrt(recoil.x^2+recoil.y^2)
        if length > rprops.angular_velocity_max[axis] then
            local co = rprops.angular_velocity_max[axis]/length
            recoil.x = recoil.x*co
            recoil.y = recoil.y*co
        end
    end
    self.time_since_last_fire = 0
end
function gun_default:open_inventory_menu()
    local props = self.properties
    local player = self.player
    local pname = player:get_player_name()
    local inv = minetest.get_inventory({type="player", name=pname})
    local window = minetest.get_player_window_information(pname)
    local listname = Guns4d.config.inventory_listname
    local form_dimensions = {x=20,y=15}

    local inv_height=4+((4-1)*.125)
    local hotbar_length = player:hud_get_hotbar_itemcount()
    local form = "\
    formspec_version[7]\
     size[".. form_dimensions.x ..",".. form_dimensions.y .."]"

    local hotbar_height = math.ceil(hotbar_length/8)
    form = form.."\
    scroll_container[.25,"..(form_dimensions.y)-inv_height-1.25 ..";10,5;player_inventory;vertical;.05]\
        list[current_player;"..listname..";0,0;"..hotbar_length..","..hotbar_height..";]\
        list[current_player;"..listname..";0,1.5;8,3;"..hotbar_length.."]\
    scroll_container_end[]\
    "
    if math.ceil(inv:get_size("main")/8) > 4 then
        local h = math.ceil(inv:get_size("main")/8)
        form=form.."\
        scrollbaroptions[max="..h+((h-1)*.125).."]\
        scrollbar[10.25,"..(form_dimensions.y)-inv_height-1.25 ..";.5,5;vertical;player_inventory;0]\
        "
    end
    --display gun preview
    local len = math.abs(self.model_bounding_box[3]-self.model_bounding_box[6])/props.visuals.scale
    local hei = math.abs(self.model_bounding_box[2]-self.model_bounding_box[5])/props.visuals.scale
    local offsets = {x=(-self.model_bounding_box[6]/props.visuals.scale)-(len/2), y=(self.model_bounding_box[5]/props.visuals.scale)+(hei/2)}

    local meter_scale = 15
    local image_scale = meter_scale*(props.inventory.render_size or 1)
    local gun_gui_offset = {x=0,y=-2.5}
    form = form.."container["..((form_dimensions.x-image_scale)/2)+gun_gui_offset.x.. ","..((form_dimensions.y-image_scale)/2)+gun_gui_offset.y.."]"
    if props.inventory.render_image then
        form = form.."image["
        ..(offsets.x*meter_scale) ..","
        ..(offsets.y*meter_scale) ..";"
        ..image_scale..","
        ..image_scale..";"
        ..props.inventory.render_image.."]"
    end
    if self.part_handler then
        --local attachment_inv = self.part_handler.virtual_inventory
        if props.inventory.part_slots and self.part_handler then
            for i, attachment in pairs(props.inventory.part_slots) do
                form = form.."label["..(image_scale/2)+(attachment.formspec_offset.x or 0)-.75 ..","..(image_scale/2)+(-attachment.formspec_offset.y or 0)-.2 ..";"..(attachment.description or i).."]"
                --list[<inventory location>;<list name>;<X>,<Y>;<W>,<H>;<starting item index>]
                local width = attachment.slots or 1
                width = width+((width-1)*.125)
                form = form.."list[detached:guns4d_attachment_inv_"..pname..";"..i..";"..(image_scale/2)+(attachment.formspec_offset.x or 0)-(width/2)..","..(image_scale/2)+(-attachment.formspec_offset.y or 0)..";3,5;]"
            end
        end
    end
    form = form.."container_end[]"
    minetest.show_formspec(self.handler.player:get_player_name(), "guns4d:inventory", form)
end
core.register_on_player_receive_fields(function(player, formname, fields)
    if formname=="guns4d:inventory" and fields.quit then
        local gun = Guns4d.players[player:get_player_name()].gun
        gun:regenerate_properties()
    end
end)





--- update the offsets of the player's look created by the gun
function gun_default:update_look_offsets(dt)
    assert(self.instance, "attempt to call object method on a class")
    local handler = self.handler
    local look_rotation = handler.look_rotation --remember that this is in counterclock-wise rotation. For 4dguns we use clockwise so it makes a bit more sense for recoil. So it needs to be inverted.
    local player_rot = self.player_rotation
    player_rot.y = -handler.look_rotation.y
    local rot_factor = Guns4d.config.vertical_rotation_factor*dt
    rot_factor = rot_factor
    local next_vert_aim = ((player_rot.x-look_rotation.x)/(1+rot_factor))+look_rotation.x --difference divided by a value and then added back to the original
    if math.abs(look_rotation.x-next_vert_aim) > .005 then
        player_rot.x = next_vert_aim
    else
        player_rot.x = look_rotation.x
    end

    local props = self.properties
    local hip = props.hip
    local ads = props.ads
    if not handler.control_handler.ads then
        --hipfire rotation offsets
        local pitch = self.total_offsets.player_axial.x+player_rot.x
        local gun_axial = self.offsets.look.gun_axial
        local offset = handler.look_rotation.x-player_rot.x
        gun_axial.x = Guns4d.math.clamp(offset, 0, 15*(offset/math.abs(offset)))
        gun_axial.x = gun_axial.x+(pitch*(1-hip.axis_rotation_ratio))
        self.offsets.look.player_axial.x = -pitch*(1-hip.axis_rotation_ratio)
        self.offsets.look.look_trans.x = 0
    else
        self.offsets.look.gun_axial.x = 0
        self.offsets.look.player_axial.x = 0
    end
    local location = Guns4d.math.clamp(Guns4d.math.smooth_ratio(self.control_handler.ads_location)*2, 0, 1)
    self.offsets.look.look_trans.x = ads.horizontal_offset*location
    local fwd_offset = 0
    if look_rotation.x < 0 then --minetest's pitch is inverted, checking here if it's above horizon.
        fwd_offset = math.abs(math.sin(look_rotation.x*math.pi/180))*props.ads.offset.z*location
    end
    self.offsets.look.player_trans.z = fwd_offset
    self.offsets.look.look_trans.z = fwd_offset
end
--============================================== positional info =====================================


local tmv3_rot = vector.new()
local tmv4_in = {0,0,0,1}
local tmv4_pivot_inv = {0,0,0,0}
local tmv4_offset = {0,0,0,1}
local tmv4_gun = {0,0,0,1}
local empty_vec = {x=0,y=0,z=0}
local ttransform = mat4.identity()
local out = vector.new() --reserve the memory, we still want to create new vectors each time though.
--gets the gun's position relative to the player. Relative indicates wether it's relative to the player's horizontal look
--offset is relative to the's rotation

--- get the global position of the gun. This is customized to rely on the assumption that there are 3-4 main rotations and 2-3 translations. If the behavior of the bones are changed this method may not work.
-- the point of this is to allow the user to find the gun's object origin as well as calculate where a given point should be offset given the parameters.
-- @tparam vec3 offset_pos
-- @tparam bool relative_y wether the y axis player look rotation is set to 0
-- @tparam bool relative_x wether the x axis player look rotation is set to 0
-- @tparam bool with_animation wether rotational and translational offsets from the animation are applied or 0
-- @treturn vec3 position of gun (in global or local orientation) relative to the player's position
function gun_default:get_pos(offset, relative_y, relative_x, with_animation)
    assert(self.instance, "attempt to call object method on a class")
    --local player = self.player
    local px = (relative_x and 0) or nil
    local py = (relative_y and 0) or nil
    local ax = ((not with_animation) and 0) or nil
    local ay = ((not with_animation) and 0) or nil
    local az = ((not with_animation) and 0) or nil
    offset = offset or empty_vec

    local gun_translation = self.gun_translation --needs a refactor
    local root_transform = self.b3d_model.root_orientation_rest
    --dir needs to be rotated twice seperately to avoid weirdness
    local gun_scale = self.properties.visuals.scale
    --generate rotation values based on our output

    ttransform=self:get_rotation_transform(ttransform,nil,nil,nil,nil,nil,px,py,ax,ay,az)

    --change the pivot of `offset` to the root bone by making our vector relative to it (basically setting it to origin)
    tmv4_in[1], tmv4_in[2], tmv4_in[3] = offset.x-root_transform[13]*gun_scale, offset.y-root_transform[14]*gun_scale, offset.z-root_transform[15]*gun_scale
    tmv4_offset = ttransform.mul_vec4(tmv4_offset, ttransform, tmv4_in) --rotate by our rotation transform
    --to bring it back to global space we need to find what we offset it by in `ttransform`'s local space, so we apply the transform to it
    tmv4_in[1], tmv4_in[2], tmv4_in[3] = root_transform[13]*gun_scale, root_transform[14]*gun_scale, root_transform[15]*gun_scale
    tmv4_pivot_inv = ttransform.mul_vec4(tmv4_pivot_inv, ttransform, tmv4_in)

    --quickly add together tmv4_offset+tmv4_pivot_inv to get the global position of the offset relative to the entity
    tmv4_offset[1],tmv4_offset[2],tmv4_offset[3] = tmv4_offset[1]+tmv4_pivot_inv[1], tmv4_offset[2]+tmv4_pivot_inv[2], tmv4_offset[3]+tmv4_pivot_inv[3]

    --get the position of the gun entity in global space relative to the bone which it is attached to.
    ttransform=self:get_rotation_transform(ttransform, 0,0,0,nil,nil,px,py,ax,ay,az)
    tmv4_in[1], tmv4_in[2], tmv4_in[3] = gun_translation.x, gun_translation.y, gun_translation.z
    tmv4_gun = ttransform.mul_vec4(tmv4_gun, ttransform, tmv4_in)

    --get the position of the bone globally
    local bone_location = self.handler.player_model_handler.gun_bone_location
    if relative_y then
        out = vector.new(bone_location)
    else
        tmv3_rot.y = -self.handler.look_rotation.y*math.pi/180
        out = vector.rotate(bone_location, tmv3_rot)
    end
    --add our global translations together
    --bonepos + gunentity + gunoffset + animation offset
    local anim = (with_animation and self.animation_translation) or empty_vec
    out.x, out.y, out.z = out.x+anim.x+tmv4_gun[1]+tmv4_offset[1], out.y+anim.y+tmv4_gun[2]+tmv4_offset[2], out.z+anim.z+tmv4_gun[3]+tmv4_offset[3]
    return out
end
function gun_default:get_pos_relative_to_look()
end

local roll = mat4.identity() --roll offset (future implementation )
local lrot = mat4.identity() --local rotation offset
local grot = mat4.identity() --global rotation offset
local prot = mat4.identity() --global player rotation
local trad = math.pi/180
function gun_default:get_rotation_transform(out, lx,ly,lz,gx,gy,px,py,ax,ay,az)
    --local pitch, global pitch etc.
    local rotations = self.total_offsets
    local arotation = self.animation_rotation
    local protation = self.player_rotation
    --eventually we want to INTERNALLY use radians, for now we have to do this though.
    lz = lz or 0 --roll is currently unused.
    ax, ay, az = ax or -arotation.x*trad, ay or -arotation.y*trad, az or -arotation.z*trad
    lx, ly = lx or -rotations.gun_axial.x*trad, ly or -rotations.gun_axial.y*trad
    gx, gy = gx or -rotations.player_axial.x*trad, gy or -rotations.player_axial.y*trad
    px, py = px or -protation.x*trad, py or -protation.y*trad

    --this doesnt account for the actual rotation of the player
    --reset roll matrix
    roll[1] = 1
    roll[2] = 0
    roll[5] = 0
    roll[6] = 1
    roll = mat4.rotate_Z(roll, lz+az)
        --we use bone rotation because it uses the XYZ order. Overall order is "PGLA", player (ZXY)<-global_offset (XYZ)<-local_offset (XYZ)<-roll (Z)\
    out = mat4.multiply(out, {prot:set_rot_luanti_entity(px, py, 0), grot:set_rot_irrlicht_bone(gx, gy, 0), lrot:set_rot_irrlicht_bone(lx+ax, ly+ay, 0), roll})
    return out
end
local forward = {0,0,1,0}
local tmv4_out = {0,0,0,0}
-- get the direction for firing
function gun_default:get_dir(rltv, offx, offy, suppress_anim)
    local rotations = self.total_offsets
    local anim_x = (suppress_anim and 0) or nil
    local anim_y = (suppress_anim and 0) or nil
    local anim_z = (suppress_anim and 0) or nil
    if rltv then
        ttransform = self:get_rotation_transform(ttransform, (-rotations.gun_axial.x-(offx or 0) )*trad, (-rotations.gun_axial.y-(offy or 0))*trad, nil,   nil, nil,   0, 0,      anim_x,anim_y,anim_z)
    else
        local player_aim
        if (self.properties.subclasses.sprite_scope and self.handler.control_handler.ads) or (self.properties.subclasses.crosshair and not self.handler.control_handler.ads)  then
            player_aim=self.player:get_look_vertical()
        end
        ttransform = self:get_rotation_transform(ttransform, (-rotations.gun_axial.x-(offx or 0))*trad, (-rotations.gun_axial.y-(offy or 0))*trad, nil,   nil, nil,   player_aim, nil,   anim_x,anim_y,anim_z)
    end
    local tmv4 = ttransform.mul_vec4(tmv4_out, ttransform, forward)
    local pos = vector.new(tmv4[1], tmv4[2], tmv4[3])

    return pos
end

--=============================================== ENTITY ======================================================

--- adds the gun entity
function gun_default:add_entity()
    assert(self.instance, "attempt to call object method on a class")
    self.entity = minetest.add_entity(self.player:get_pos(), "guns4d:gun_entity")
    Guns4d.gun_by_ObjRef[self.entity] = self
    self:regenerate_visuals()
end
local tmp_mat4_rot = mat4.identity()
local ip_time = Guns4d.config.gun_axial_interpolation_time
local ip_time2 = Guns4d.config.translation_interpolation_time

--- updates the gun's entity
function gun_default:update_entity()
    local obj = self.entity
    local handler = self.handler
    local props = self.properties
    --attach to the correct bone, and rotate
    local visibility = true
    if self.subclass_instances.sprite_scope and self.subclass_instances.sprite_scope.hide_gun and (not (self.control_handler.ads_location == 0)) then
        visibility = false
    end
    --Irrlicht uses counterclockwise but we use clockwise.
    local ads = props.ads.offset
    local hip = props.hip.offset
    local offset = self.total_offsets.gun_trans
    local ip = Guns4d.math.smooth_ratio(Guns4d.math.clamp(handler.control_handler.ads_location*2,0,1))
    local ip_inv = 1-ip

    local pos = self.gun_translation --entity directly dictates the translation of the gun
    pos.x = (ads.x*ip)+(hip.x*ip_inv)+offset.x
    pos.y = (ads.y*ip)+(hip.y*ip_inv)+offset.y
    pos.z = (ads.z*ip)+(hip.z*ip_inv)+offset.z
    local scale = self.properties.visuals.scale

    --some complicated math to get client interpolation to work. It doesn't really account for the root bone having an (oriented) parent bone currently... hopefully that's not an issue.
    local b3d = self.b3d_model
    local rot = self:get_rotation_transform(tmp_mat4_rot, nil,nil,nil, 0,0, 0,0, 0,0,0)
    tmp_mat4_rot = mat4.mul(tmp_mat4_rot, {b3d.root_orientation_rest_inverse, rot, b3d.root_orientation_rest})
    local xr,yr,zr = tmp_mat4_rot:get_rot_irrlicht_bone()

    obj:set_attach(self.player, handler.player_model_handler.bone_aliases.gun, nil, nil, visibility)
    obj:set_bone_override(self.consts.ROOT_BONE, {
        position = {
            vec = {x=pos.x/scale, y=pos.y/scale, z=pos.z/scale},
            interpolation = ip_time2,
        },
        rotation = {
            vec = {x=xr,y=yr,z=zr},
            interpolation = ip_time,
        }
    })
end

--- checks if the gun entity exists...
-- @treturn bool
function gun_default:has_entity()
    assert(self.instance, "attempt to call object method on a class")
    if not self.entity then return false end
    if not self.entity:is_valid() then return false end
    return true
end

--- updates the gun's wag offset for walking
-- @tparam float dt
function gun_default:update_wag(dt)
    local handler = self.handler
    local wag = self.offsets.walking
    local velocity = wag.velocity
    local props = self.properties
    local old_tick
    if handler.walking then
        velocity = self.player:get_velocity()
        wag.velocity = velocity
    end
    old_tick = old_tick or wag.tick
    if velocity then
        if handler.walking then
            wag.tick = wag.tick + (dt*vector.length(velocity))
        else
            wag.tick = wag.tick + (dt*4)
        end
    end
    local walking_offset = self.offsets.walking
    if velocity and (not handler.walking) and (math.ceil(old_tick/props.wag.cycle_speed)+.5 < (math.ceil(wag.tick/props.wag.cycle_speed))+.5) and (wag.tick > old_tick) then
        wag.velocity = nil
        return
    end
    for _, i in ipairs({"x","y"}) do
        for _, axis in ipairs({"player_axial", "gun_axial"}) do
            if velocity then
                local multiplier = 1
                if i == "x" then
                    multiplier = 2
                end
                --if the result is negative we know that it's flipped, and thus can be ended.
                local inp = (wag.tick/props.wag.cycle_speed)*math.pi*multiplier
                --this is a mess, I think that 1.6 is the frequency of human steps or something
                walking_offset[axis][i] = math.sin(inp)*self.properties.wag.offset[axis][i]
            else
                local old_value = walking_offset[axis][i]
                if math.abs(walking_offset[axis][i]) > .005 then
                    local multiplier = 1/props.wag.decay_speed
                    walking_offset[axis][i] = walking_offset[axis][i]-(walking_offset[axis][i]*multiplier*dt)
                else
                    walking_offset[axis][i] = 0
                end
                if math.abs(walking_offset[axis][i]) > math.abs(old_value) then
                    walking_offset[axis][i] = 0
                end
            end
        end
    end
end
local e = 2.7182818284590452353602874713527 --I don't know how to find it otherwise...

--- updates the gun's recoil simulation
-- @tparam float dt
function gun_default:update_recoil(dt)
    for axis, _ in pairs(self.offsets.recoil) do
        for _, i in pairs({"x","y"}) do
            local recoil_vel = self.velocities.recoil[axis][i]
            local recoil = self.offsets.recoil[axis][i]
            recoil = recoil + recoil_vel
            --this is modelled off a geometric sequence where the Y incercept of the sequence is set to recoil_vel.
            local r = (10*self.properties.recoil.velocity_correction_factor[axis])^-1
            local vel_co = e^-( (self.time_since_last_fire^2)/(2*r^2) )
            recoil_vel = self.velocities.init_recoil[axis][i]*vel_co
            if math.abs(recoil_vel) < 0.0001 then
                recoil_vel = 0
            end
            self.velocities.recoil[axis][i] = recoil_vel

            --ax^2+bx+c
            --recoil_velocity_correction_rate
            --recoil_correction_rate
            local old_recoil = recoil
            local abs = math.abs(recoil)
            local sign = old_recoil/abs
            if abs > 0.001 then
                local correction_value = abs*self.time_since_last_fire*self.properties.recoil.target_correction_factor[axis]
                correction_value = Guns4d.math.clamp(correction_value, 0, self.properties.recoil.target_correction_max_rate[axis])
                abs=abs-(correction_value*dt)
                --prevent overcorrection
                if abs < 0 then
                    abs = 0
                end
            end
            if sign~=sign then
                sign = 1
            end
            self.offsets.recoil[axis][i] = abs*sign
        end
    end
end

--- updates the gun's animation data
-- @tparam dt
function gun_default:update_animation(dt)
    --local ent = self.entity
    local data = self.animation_data
    data.runtime = data.runtime + dt
    data.current_frame = Guns4d.math.clamp(data.current_frame+(dt*data.fps), data.frames.x, data.frames.y)
    if data.loop and (data.current_frame > data.frames.y) then
        data.current_frame = data.frames.x
    end
    self:update_animation_transforms()
end
--IMPORTANT!!! this does not directly modify the animation_data table anymore, it's all hooked through ObjRef:set_animation() (init.lua) so if animation is set elsewhere it doesnt break.
--this may be deprecated in the future- as it is no longer really needed now that I hook ObjRef functions.
--- sets the gun's animation in the same format as ObjRef:set_animation() (future deprecation?)
-- @tparam table frames `{x=int, y=int}`
-- @tparam float|nil length length of the animation in seconds
-- @tparam int fps frames per second of the animation
-- @tparam bool loop wether to loop
function gun_default:set_animation(frames, length, fps, loop)
    loop = loop or false --why the fuck default is true? I DONT FUCKIN KNOW (this undoes this)
    assert(type(frames)=="table" and frames.x and frames.y, "frames invalid or nil in set_animation()!")
    assert(not (length and fps), "cannot play animation with both specified length and specified fps. Only one parameter can be used.")
    local num_frames = math.abs(frames.x-frames.y)
    if length then
        fps = num_frames/length
    elseif not fps then
        fps = self.consts.DEFAULT_FPS
    end
    self.entity:set_animation(frames, fps, 0, loop) --see init.lua for modified ObjRef stuff.
end

--- clears the animation to the rest state
function gun_default:clear_animation()
    local loaded = false
    if self.properties.ammo.magazine_only then
        if self.ammo_handler.ammo.loaded_mag ~= "empty" then
            loaded = true
        end
    elseif self.ammo_handler.ammo.total_rounds > 0 then
        loaded = true
    end
    if loaded then
        self.entity:set_animation({x=self.properties.visuals.animations.loaded.x, y=self.properties.visuals.animations.loaded.y}, 0, 0, self.consts.LOOP_IDLE_ANIM)
    else
        self.entity:set_animation({x=self.properties.visuals.animations.empty.x, y=self.properties.visuals.animations.empty.y}, 0, 0, self.consts.LOOP_IDLE_ANIM)
    end
end
local function adjust_gain(tbl, v)
    v = tbl.third_person_gain_multiplier or v
    for i = 1, #tbl do
        adjust_gain(tbl[i], v)
    end
    if tbl.gain and (tbl.split_audio_by_perspective~=false) then
        if type(tbl.gain) == "number" then
            tbl.gain = tbl.gain*v
        else
            tbl.gain.min = tbl.gain.min*v
            tbl.gain.max = tbl.gain.max*v
        end
    end
end

--- plays a list of sounds for the gun's user and thirdpersons
-- @tparam soundspec_list sound parameters following the format of @{Guns4d.play_sounds}
-- @treturn integer thirdperson sound's guns4d sound handle
-- @treturn integer firstperson sound's guns4d sound handle
function gun_default:play_sounds(sound)
    local thpson_sound = Guns4d.table.deep_copy(sound)
    local fsprsn_sound = Guns4d.table.deep_copy(sound)

    thpson_sound.pos = self.pos
    thpson_sound.player = self.player
    thpson_sound.exclude_player = self.player
    adjust_gain(thpson_sound, self.consts.THIRD_PERSON_GAIN_MULTIPLIER)

    fsprsn_sound.player = self.player
    fsprsn_sound.to_player = "from_player"

    return Guns4d.play_sounds(thpson_sound), Guns4d.play_sounds(fsprsn_sound)
end
function gun_default:update_breathing(dt)
    assert(self.instance)
    local breathing_info = {pause=1.4, rate=4.2}
    --we want X to be between 0 and 4.2. Since math.pi is a positive crest, we want X to be above it before it reaches our-
    --"length" (aka rate-pause), thus it will pi/length or pi/(rate-pause) will represent out slope of our control.
    local x = (self.time_since_creation%breathing_info.rate)*math.pi/(breathing_info.rate-breathing_info.pause)
    local scale = self.properties.breathing_scale
    --now if it's above math.pi we know it's in the pause half of the cycle. For smoothness, we cut the sine off early and decay the value non-linearly.
    --not sure why 8/9 is a constant here... I assume it's if it's 8/9 of the way through the cycle. Not going to worry about it.
    if x > math.pi*(8/9) then
        self.offsets.breathing.player_axial.x=self.offsets.breathing.player_axial.x-(self.offsets.breathing.player_axial.x*2*dt)
    else
        self.offsets.breathing.player_axial.x = scale*(math.sin(x))
    end
end

function gun_default:update_sway(dt)
    assert(self.instance, "attempt to call object method from a base class")
    local sprops = self.properties.sway
    for axis, sway in pairs(self.offsets.sway) do
        local sway_vel = self.velocities.sway[axis]
        local ran
        ran = vector.apply(vector.new(), function(i,v)
            if i ~= "x" then
                return (math.random()-.5)*2
            end
        end)
        ran.z = 0
        local vel_mul = self.multiplier_coefficient(sprops.hipfire_velocity_multiplier[axis], 1-self.control_handler.ads_location)
        sway_vel = vector.normalize(sway_vel+(ran*dt))*sprops.angular_velocity[axis]*vel_mul
        sway=sway+(sway_vel*dt)
        local len_mul = self.multiplier_coefficient(sprops.hipfire_angle_multiplier[axis], 1-self.control_handler.ads_location)
        if vector.length(sway) > sprops.max_angle[axis]*len_mul then
            sway=vector.normalize(sway)*sprops.max_angle[axis]*len_mul
            sway_vel = vector.new()
        end
        self.offsets.sway[axis] = sway
        self.velocities.sway[axis] = sway_vel
    end
end


--should merge these functions eventually...
function gun_default:update_animation_transforms()
    local current_frame = self.animation_data.current_frame+self.consts.KEYFRAME_SAMPLE_PRECISION
    local frame1 = math.floor(current_frame/self.consts.KEYFRAME_SAMPLE_PRECISION)
    local frame2 = math.floor(current_frame/self.consts.KEYFRAME_SAMPLE_PRECISION)+1
    current_frame = current_frame/self.consts.KEYFRAME_SAMPLE_PRECISION
    local rotations = self.b3d_model.global_frames.root_rotation
    local positions = self.b3d_model.global_frames.root_translation
    local euler_rot
    local trans
    if not rotations[frame1] then --note that we are inverting the rotations, this is because b3d turns the wrong way or something? It might be an issue with LEEF idk.
        euler_rot = vector.new(rotations[1]:get_euler_irrlicht_bone())*-1
    else
        local ip_ratio = (frame2 and (current_frame-frame1)/(frame2-frame1)) or 1
        local vec1 = rotations[frame1]
        local vec2 = rotations[frame2] or rotations[frame1]
        euler_rot = vector.new(vec1:slerp(vec2, ip_ratio):get_euler_irrlicht_bone())*-180/math.pi
    end

    if not positions[frame1] then --note that we are inverting the rotations, this is because b3d turns the wrong way or something? It might be an issue with LEEF idk.
        trans = positions[1]*-1
    else
        local ip_ratio = (frame2 and (current_frame-frame1)/(frame2-frame1)) or 1
        local vec1 = positions[frame1]
        local vec2 = positions[frame2] or positions[frame1]
        trans = (vec1*(1-ip_ratio))+(vec2*ip_ratio)
    end
    self.animation_rotation = euler_rot
    self.animation_translation = trans
end

--relative to the gun's entity. Returns left, right vectors.
local out = {arm_left=vector.new(), arm_right=vector.new()}
function gun_default:get_arm_aim_pos()
    local current_frame = self.animation_data.current_frame
    local frame1 = math.floor(current_frame/self.consts.KEYFRAME_SAMPLE_PRECISION)
    local frame2 = math.floor(current_frame/self.consts.KEYFRAME_SAMPLE_PRECISION)+1
    current_frame = current_frame/self.consts.KEYFRAME_SAMPLE_PRECISION

    for i, v in pairs(out) do
        if self.b3d_model.global_frames[i] then
            if self.b3d_model.global_frames[i][frame1] then
                if (not self.b3d_model.global_frames[i][frame2]) or (current_frame==frame1) then
                    out[i] = vector.copy(self.b3d_model.global_frames[i][frame1])
                else --to stop nan
                    local ip_ratio = (current_frame-frame1)/(frame2-frame1)
                    local vec1 = self.b3d_model.global_frames[i][frame1]
                    local vec2 = self.b3d_model.global_frames[i][frame2]
                    out[i] = vec1+((vec1-vec2)*ip_ratio)
                end
            else
                out[i]=vector.copy(self.b3d_model.global_frames[i][1])
            end
        else
            out[i] = vector.new()
        end
    end
    return out.arm_left, out.arm_right
    --return vector.copy(self.b3d_model.global_frames.arm_left[1]), vector.copy(self.b3d_model.global_frames.arm_right[1])
end

--- ready the gun to be deleted
function gun_default:prepare_deletion()
    assert(self.instance, "attempt to call object method on a class")
    if self:has_entity() then self.entity:remove() end

    for i, instance in pairs(self.subclass_instances) do
        if instance.prepare_deletion then instance:prepare_deletion() end
    end
end
