---@class Controller
Controller = Object:extend()

--The controller contains all engine logic for how human input interacts with any game objects.
function Controller:init()

--Each of these are calculated per frame to pass along to the corresponding nodes for input handling
self.clicked = {target = nil, handled = true, prev_target = nil} --The node that was clicked this frame
self.focused = {target = nil, handled = true, prev_target = nil } --The node that is being focused on this frame, only applies when using controller
self.dragging = {target = nil, handled = true, prev_target = nil} --The node being dragged this frame
self.hovering = {target = nil, handled = true, prev_target = nil} --The node being hovered this frame
self.released_on = {target = nil, handled = true, prev_target = nil} --The node that the cursor 'Released' on, like letting go of LMB

self.collision_list = {} --A list of all node that the cursor currently collides with

--Input values to be determined by this controller - the actual game objects should not have to see any of this
self.cursor_down = {T = {x=0, y=0}, target = nil, time = 0, handled = true}
self.cursor_up = {T = {x=0, y=0}, target = nil, time = 0.1, handled = true}
self.cursor_hover = {T = {x=0, y=0}, target = nil, time = 0, handled = true}
self.cursor_collider = nil --The node that collides with the cursor this frame
self.cursor_position = {x=0,y=0} --NOT IN GAME UNITS

--For key presses, hold times, and if they are released directly from LOVE
self.pressed_keys = {}
self.held_keys = {}
self.held_key_times = {}
self.released_keys = {}

--For button presses, hold times, and if they are released directly from LOVE
self.pressed_buttons = {}
self.held_buttons = {}
self.held_button_times = {}
self.released_buttons = {}

--For all controller interrupts
self.interrupt = {
    focus = false,
}

--For all controller locks
self.locks = {}
self.locked = nil

--Buttons pressed and released during axis updates
self.axis_buttons = {
    l_stick = {current = '', previous = ''},
    r_stick = {current = '', previous = ''},
    l_trig = {current = '', previous = ''},
    r_trig = {current = '', previous = ''}
}

--The speed that the controller thumbstick moves the cursor
self.axis_cursor_speed = 20

--A registry of buttons, each a valid button input name corresponding to a node (likely a button). This is modified with the registry functions
self.button_registry = {}

--A node representing where the cursor should 'snap' to. When this is set, then next frame should have the cursor to that position or on that node. Use :snap_to
self.snap_cursor_to = nil

--A stack of cursor positions, this stack changes depending on the depth of menus on screen so the game can remember where you last had your cursor
--This needs to keep track of both positions and nodes if possible, as well as the depth
self.cursor_context = {
    layer = 1,
    stack = {}
}

self.cardarea_context = {}

--Human Interface device flags, these are set per frame to ensure that correct controller updates are taking place
self.HID = {
    last_type = '',
    dpad = false,
    pointer = true,
    touch = false,
    controller = false,
    mouse = true,
    axis_cursor = false,
}

--The gamepad most recently used, if any
self.GAMEPAD = {object = nil, mapping = nil, name = nil}
self.GAMEPAD_CONSOLE = '' --Valid button icons for Xbox, Playstation and Nintendo controllers

--If we need an emulated gamepad for keyboard controls
self.keyboard_controller = {
    getGamepadMappingString = function() return 'balatro_kbm' end,
    getGamepadAxis = function() return 0 end
}

self.is_cursor_down = false
end

--Sets the gamepad to be the updated gamepad, searches for the console type and sets the art button pips accordingly
--Some code here is from github.com/idbrii/love-gamepadguesser (MIT License)
function Controller:set_gamepad(_gamepad)
    if self.GAMEPAD.object ~= _gamepad then 
        self.GAMEPAD.object = _gamepad
        self.GAMEPAD.mapping = _gamepad:getGamepadMappingString() or ''
        self.GAMEPAD.name = self.GAMEPAD.mapping:match("^%x*,(.-),") or ''
        self.GAMEPAD.temp_console = self:get_console_from_gamepad(self.GAMEPAD.name)
        if self.GAMEPAD_CONSOLE ~= self.GAMEPAD.temp_console then 
            self.GAMEPAD_CONSOLE = self.GAMEPAD.temp_console
            for k, v in pairs(G.I.SPRITE) do
                if v.atlas == G.ASSET_ATLAS["gamepad_ui"] then
                    v.sprite_pos.y = G.CONTROLLER.GAMEPAD_CONSOLE == 'Nintendo' and 2 or G.CONTROLLER.GAMEPAD_CONSOLE == 'Playstation' and (G.F_PS4_PLAYSTATION_GLYPHS and 3 or 1) or 0
                    v:set_sprite_pos(v.sprite_pos)
                end
            end
        end
        self.GAMEPAD.temp_console = nil
    end
end

--Some code here is from github.com/idbrii/love-gamepadguesser (MIT License)
function Controller:get_console_from_gamepad(_gamepad)
    G.ARGS.gamepad_patterns = G.ARGS.gamepad_patterns or 
    {
        Playstation = {"%f[%w]PS%d%f[%D]", "Sony%f[%W]", "Play[Ss]tation"},
        Nintendo = {"Wii%f[%L]", "%f[%u]S?NES%f[%U]", "%f[%l]s?nes%f[%L]", "%f[%u]Switch%f[%L]", "Joy[- ]Cons?%f[%L]",},
        --Keyboard = {'balatro_kbm'}
    }

    for k, v in pairs(G.ARGS.gamepad_patterns) do
        for kk, vv in ipairs(v) do
            if _gamepad:match(vv) then 
                return k
            end
        end
    end
    return 'Xbox'
end

--The universal controller for what type of HID Device the player is using to interact with the game. The Game should be able to handle switching
--to any viable HID at any time
function Controller:set_HID_flags(HID_type, button)
    --we need to determine it the axis input will be handled like a button or pointer
    --if button and self.HID.controller and not string.find(button, 'dp') then return end
    if HID_type == 'axis' then
        self.HID.controller = true
        self.HID.last_type = 'axis'
    elseif HID_type and HID_type ~= self.HID.last_type then
        self.HID.dpad = HID_type == 'button'
        self.HID.pointer = HID_type == 'mouse' or HID_type == 'axis_cursor' or HID_type == 'touch'
        self.HID.controller = HID_type == 'button' or HID_type == 'axis_cursor'
        self.HID.mouse = HID_type == 'mouse'
        self.HID.touch = HID_type == 'touch'
        self.HID.axis_cursor = HID_type == 'axis_cursor'
        self.HID.last_type = HID_type

        if self.HID.mouse then 
            love.mouse.setVisible(true)
        else
            love.mouse.setVisible(false)
        end
    end
    if not self.HID.controller then
        self.GAMEPAD_CONSOLE = ''
        self.GAMEPAD.object = nil
        self.GAMEPAD.mapping = nil
        self.GAMEPAD.name = nil
    end
end

--Sets the current position of the cursor
function Controller:set_cursor_position()
    --If using a mouse for the cursor
    if self.HID.mouse or self.HID.touch then 
        self.interrupt.focus = false
        --Never focus if using the mouse
        if self.focused.target then
            self.focused.target.states.focus.is = false
            self.focused.target= nil
        end

        --Set the position of the cursor to the love position of the mouse, derive cursor transform from that
        self.cursor_position.x, self.cursor_position.y = love.mouse.getPosition()
        G.CURSOR.T.x = self.cursor_position.x/(G.TILESCALE*G.TILESIZE)
        G.CURSOR.T.y = self.cursor_position.y/(G.TILESCALE*G.TILESIZE)
        G.CURSOR.VT.x = G.CURSOR.T.x
        G.CURSOR.VT.y = G.CURSOR.T.y
    end
end

--Called every game logic update frame
function Controller:update(dt)

    --parse all locks and set
    self.locked = false
    if G.screenwipe then self.locks.wipe = true else self.locks.wipe = false end

    for k, v in pairs(self.locks) do
        if v then self.locked = true end
    end
    
    if self.locks.frame_set then 
        self.locks.frame_set = nil
        self.overlay_timer = 0
        G.E_MANAGER:add_event(Event({
            trigger = 'after',
            delay = 0.1,
            timer = 'UPTIME',
            blocking = false,
            blockable = false,
            no_delete = true,
            func = (function()
                self.locks.frame = nil
                return true
            end)
        }))
    end

    self.overlay_timer = self.overlay_timer or 0
    if G.OVERLAY_MENU then 
        self.overlay_timer = self.overlay_timer + dt
    else
        self.overlay_timer = 0
    end

    if self.overlay_timer > 1.5 then self.locks.frame = nil end

    --Remove anything from the registry that is no longer in game
    self:cull_registry()

    --Calculate the axis update and set the HID flags if there is any axis input
    self:set_HID_flags(self:update_axis(dt))

    --Set the cursor to be visible only if we are using a mouse or an axis to control the cursor position
    if self.HID.pointer and not (self.HID.mouse or self.HID.touch) and not self.interrupt.focus then 
        G.CURSOR.states.visible = true
    else
        G.CURSOR.states.visible = false
    end

    --For mouse input, reset any controller things and set the cursor to be where the mouse is
    self:set_cursor_position()
        
    --Handle all the button updates and key updates, call the required functions
    if not G.screenwipe then 
        --Key press and release handling
        for k, v in pairs(self.pressed_keys) do
            if v then self:key_press_update(k, dt) end
        end
        for k, v in pairs(self.held_keys) do
            if v then self:key_hold_update(k, dt) end
        end
        for k, v in pairs(self.released_keys) do
            if v then self:key_release_update(k, dt) end
        end

        --Button press and release handling
        for k, v in pairs(self.pressed_buttons) do
            if v then self:button_press_update(k, dt) end
        end
        for k, v in pairs(self.held_buttons) do
            if v then self:button_hold_update(k, dt) end
        end
        for k, v in pairs(self.released_buttons) do
            if v then self:button_release_update(k, dt) end
        end
    end

    self.frame_buttonpress = false

    --reset press and release lists
    self.pressed_keys = EMPTY(self.pressed_keys)
    self.released_keys = EMPTY(self.released_keys)
    self.pressed_buttons= EMPTY(self.pressed_buttons)
    self.released_buttons = EMPTY(self.released_buttons)


    --If using controller, update context and snap tos
    if self.HID.controller then 
        --If there is a node/position to snap to from the cursor context layer
        if self.cursor_context.stack[self.cursor_context.layer] then 
            local _context = self.cursor_context.stack[self.cursor_context.layer]
            self:snap_to{node = (_context.node and not _context.node.REMOVED and _context.node), T = _context.cursor_pos}
            self.interrupt.stack = _context.interrupt
            self.cursor_context.stack[self.cursor_context.layer] = nil
        end
        --If there is a card the was being dragged but no longer is, snap to it
        if self.dragging.prev_target and not self.dragging.target and getmetatable(self.dragging.prev_target) == Card and not self.dragging.prev_target.REMOVED then
            --Overly complicated coyote time focus, so the user can quickly select cards without things going wonky
            if not self.COYOTE_FOCUS then 
                self:snap_to{node = self.dragging.prev_target}
            else
                self.COYOTE_FOCUS = nil
            end
        end
        --If the cursor should snap to a location
        if self.snap_cursor_to then 
            self.interrupt.focus = self.interrupt.stack
            self.interrupt.stack = false
            if self.snap_cursor_to.type == 'node' and not self.snap_cursor_to.node.REMOVED then 
                self.focused.prev_target = self.focused.target
                self.focused.target = self.snap_cursor_to.node
                self:update_cursor()
            elseif self.snap_cursor_to.type == 'transform' then
                self:update_cursor(self.snap_cursor_to.T)
            end
            if self.focused.prev_target ~= self.focused.target and self.focused.prev_target then self.focused.prev_target.states.focus.is = false end
            self.snap_cursor_to = nil
        end
    end

    --Reset all collision states, get every node that collides with the cursor, then update the focus and hover targets
    self:get_cursor_collision(G.CURSOR.T)
    self:update_focus()
    self:set_cursor_hover()
    if self.L_cursor_queue then 
        self:L_cursor_press(self.L_cursor_queue.x, self.L_cursor_queue.y)
        self.L_cursor_queue = nil
    end

    self.dragging.prev_target = self.dragging.target
    self.released_on.prev_target = self.released_on.target
    self.clicked.prev_target = self.clicked.target
    self.hovering.prev_target = self.hovering.target

    --Cursor is currently down
    if not self.cursor_down.handled then 
        if self.cursor_down.target.states.drag.can then
            self.cursor_down.target.states.drag.is = true
            self.cursor_down.target:set_offset(self.cursor_down.T, 'Click')
            self.dragging.target = self.cursor_down.target
            self.dragging.handled = false
        end
        self.cursor_down.handled = true
    end

    if not self.cursor_up.handled then 
        --First, stop dragging
        if self.dragging.target then
            self.dragging.target:stop_drag()
            self.dragging.target.states.drag.is = false
            self.dragging.target = nil
        end
        --Now, handle the Cursor release
        --Was the Cursor release in the same location as the Cursor press and within Cursor timeout?
        if self.cursor_down.target then 
            if (not self.cursor_down.target.click_timeout or self.cursor_down.target.click_timeout*G.SPEEDFACTOR > self.cursor_up.time - self.cursor_down.time) then
                if Vector_Dist(self.cursor_down.T, self.cursor_up.T) < G.MIN_CLICK_DIST then 
                    if self.cursor_down.target.states.click.can then
                        self.clicked.target = self.cursor_down.target
                        self.clicked.handled = false
                    end
                --if not, was the Cursor dragging some other thing?
                elseif self.dragging.prev_target and self.cursor_up.target and self.cursor_up.target.states.release_on.can then 
                    self.released_on.target = self.cursor_up.target
                    self.released_on.handled = false
                end
            end
        end
        self.cursor_up.handled = true
    end

    --Cursor is currently hovering over something
    if self.cursor_hover.target and self.cursor_hover.target.states.hover.can and (not self.HID.touch or self.is_cursor_down) then 
        self.hovering.target = self.cursor_hover.target
        if self.hovering.prev_target and self.hovering.prev_target ~= self.hovering.target then self.hovering.prev_target.states.hover.is = false end
        self.hovering.target.states.hover.is = true
        self.hovering.target:set_offset(self.cursor_hover.T, 'Hover')
    elseif (self.cursor_hover.target == nil or (self.HID.touch and not self.is_cursor_down)) and self.hovering.target then
        self.hovering.target.states.hover.is = false
        self.hovering.target = nil
    end

    --------------------------------------------------------------------
    -- Sending all input updates to the game objects
    --------------------------------------------------------------------
    --The clicked object
    if not self.clicked.handled then
        self.clicked.target:click()
        self.clicked.handled = true
    end

    --Process registry clicks
    self:process_registry()

    --The object being dragged
    if self.dragging.target then
        self.dragging.target:drag()
    end

    --The object released on
    if not self.released_on.handled and self.dragging.prev_target then
        if self.dragging.prev_target == self.hovering.target then self.hovering.target:stop_hover();self.hovering.target = nil end
        self.released_on.target:release(self.dragging.prev_target)
        self.released_on.handled = true
    end

    --The object being hovered over
    if self.hovering.target then
        self.hovering.target:set_offset(self.cursor_hover.T, 'Hover')
        if self.hovering.prev_target ~= self.hovering.target then 
            if self.hovering.target ~= self.dragging.target and not self.HID.touch then               
                self.hovering.target:hover() 
            elseif self.HID.touch then
                local _ID =  self.hovering.target.ID
                G.E_MANAGER:add_event(Event({
                    trigger = 'after',
                    blockable = false, 
                    blocking = false,
                    delay = G.MIN_HOVER_TIME,
                    func = function()
                        if self.hovering.target and _ID == self.hovering.target.ID then
                            self.hovering.target:hover() 
                        end
                    return true
                    end
                }))
                if self.hovering.prev_target then 
                    self.hovering.prev_target:stop_hover()
                end   
            end
            if self.hovering.prev_target then
                self.hovering.prev_target:stop_hover()
            end
        end
    elseif self.hovering.prev_target then
        self.hovering.prev_target:stop_hover()
    end
    if self.hovering.target and self.hovering.target == self.dragging.target and not self.HID.touch then
        self.hovering.target:stop_hover()
    end
end

--Brute force remove all registries that no longer have valid nodes
function Controller:cull_registry()
    for k, registry in pairs(self.button_registry) do
        for i=#registry, 1, -1 do
            if registry[i].node.REMOVED then 
                table.remove(registry, i)
            end
        end
    end
end

--Adds a node to the controller registry. Supply the button that will be pressed in order to click this node
--
---@param node Node The node that will be clicked when the registry is pressed
---@param registry string The button to register, must be a valid gamepad input
function Controller:add_to_registry(node, registry)
    --If the button doesn't have a registry list yet, add it
    self.button_registry[registry] = self.button_registry[registry] or {}

    --There really should only ever be one entry per registered button, but that is hard sometimes with all the stuff on screen.
    --If that does happen, the most recently registered one will be used and the old one will be kept in case we remove the new button and want to keep the old binding
    table.insert(self.button_registry[registry], 1, {node = node, menu = (not not G.OVERLAY_MENU) or (not not G.SETTINGS.paused)})
end

--Process any click function of any nodes that have been clicked in the button registry
function Controller:process_registry()
    for _, registry in pairs(self.button_registry) do
        for i=1, #registry do
            if registry[i].click and registry[i].node.click then 
                if registry[i].menu == not not G.OVERLAY_MENU and 
                    registry[i].node.T.x > -2 and registry[i].node.T.x < G.ROOM.T.w + 2 and
                    registry[i].node.T.y > -2 and registry[i].node.T.y < G.ROOM.T.h + 2 then
                    registry[i].node:click()
                end
                registry[i].click = nil
            end
        end
    end
end

--Add or remove layers from the context for the cursor. This allows the cursor to 'snap' back to the previous layer when the current layer is removed\
--in such cases where a menu on screen is removed or nested menus are being navigated
--
---@param delta number The direction to modify the cursor context, 1 to add a layer, -1 to remove a layer, -1000 to remove all layers except for the base
function Controller:mod_cursor_context_layer(delta)
    --Add a layer to the context, reference the node but if that node has been removed also save the cursor position too
    if delta == 1 then 
        local prev_cursor_context = {node = self.focused.target, cursor_pos = {x = G.CURSOR.T.x, y = G.CURSOR.T.y}, interrupt = self.interrupt.focus}
        self.cursor_context.stack[self.cursor_context.layer] = prev_cursor_context
        self.cursor_context.layer = self.cursor_context.layer + 1

    --remove the top layer from the stack
    elseif delta == -1 then 
        self.cursor_context.stack[self.cursor_context.layer] = nil
        self.cursor_context.layer = self.cursor_context.layer - 1
    
    --remove all but the base layer from the stack
    elseif delta == -1000 then 
        self.cursor_context.layer = 1
        self.cursor_context.stack = {self.cursor_context.stack[1]}

    --remove all layers
    elseif delta == -2000 then 
        self.cursor_context.layer = 1
        self.cursor_context.stack = {}
    end

    --Navigate focus, will default to the top layer on the stack
    self:navigate_focus()
end

--Snap the cursor to a particular node or transform
function Controller:snap_to(args)
    self.snap_cursor_to = {node = args.node, T = args.T, type = args.node and 'node' or 'transform'}
end

--saves the focus context to be loaded in the future, for example if the shop is rerolled while a card is highlighted
function Controller:save_cardarea_focus(_cardarea)
    if G[_cardarea] then
        if self.focused.target and self.focused.target.area and self.focused.target.area == G[_cardarea] then
            self.cardarea_context[_cardarea] = self.focused.target.rank
            return true
        else
            self.cardarea_context[_cardarea] = nil
        end
    end
end

--recalls the focus context for a particular cardarea
function Controller:recall_cardarea_focus(_cardarea)
    local ca_string = nil
    if type(_cardarea) == 'string' then ca_string = _cardarea; _cardarea = G[_cardarea] end

    if _cardarea and (not self.focused.target or
        self.interrupt.focus or
        (not self.interrupt.focus and self.focused.target.area and self.focused.target.area == _cardarea)) then 
        if ca_string and self.cardarea_context[ca_string] then 
            for i = self.cardarea_context[ca_string], 1, -1 do
                if _cardarea.cards[i] then 
                    self:snap_to({node = _cardarea.cards[i]})
                    self.interrupt.focus = false
                    break
                end
            end
        elseif _cardarea.cards and _cardarea.cards[1] then
            self:snap_to({node = _cardarea.cards[1]})
            self.interrupt.focus = false
        end
    end
    if ca_string then self.cardarea_context[ca_string] = nil end
end

--Updated the location of the cursor, either with a specific T or if there is a Node target
function Controller:update_cursor(hard_set_T)
    if hard_set_T then
        G.CURSOR.T.x = hard_set_T.x
        G.CURSOR.T.y = hard_set_T.y
        self.cursor_position.x = G.CURSOR.T.x*(G.TILESCALE*G.TILESIZE)
        self.cursor_position.y = G.CURSOR.T.y*(G.TILESCALE*G.TILESIZE)
        G.CURSOR.VT.x = G.CURSOR.T.x
        G.CURSOR.VT.y = G.CURSOR.T.y
        return 
    end
    if self.focused.target then 
        self.cursor_position.x, self.cursor_position.y = self.focused.target:put_focused_cursor()
        G.CURSOR.T.x = self.cursor_position.x/(G.TILESCALE*G.TILESIZE)
        G.CURSOR.T.y = self.cursor_position.y/(G.TILESCALE*G.TILESIZE)
        G.CURSOR.VT.x = G.CURSOR.T.x
        G.CURSOR.VT.y = G.CURSOR.T.y
    end
end

--Helper function to set the button presses/releases for the values determined in update_axis()
function Controller:handle_axis_buttons()
    for _, v in pairs(G.CONTROLLER.axis_buttons) do
        --Button is no longer being pressed
        if v.previous ~= '' and (v.current == '' or v.previous ~= v.current) then 
            G.CONTROLLER:button_release(v.previous)
        end
        --New button is being pressed
        if v.current ~= '' and v.previous ~= v.current then
            G.CONTROLLER:button_press(v.current)
        end
    end
end

--Handles all axis input for left stick, right stick and triggers. Treats them as buttons or cursors depending on context
function Controller:update_axis(dt)
    --Keep track of if there is any cursor movement from the axis changes
    local axis_interpretation = nil

    --Advance all the axis buttons to determine if there were any changes
    self.axis_buttons.l_stick.previous = self.axis_buttons.l_stick.current; self.axis_buttons.l_stick.current = ''
    self.axis_buttons.r_stick.previous = self.axis_buttons.r_stick.current; self.axis_buttons.r_stick.current = ''
    self.axis_buttons.l_trig.previous = self.axis_buttons.l_trig.current; self.axis_buttons.l_trig.current = ''
    self.axis_buttons.r_trig.previous = self.axis_buttons.r_trig.current; self.axis_buttons.r_trig.current = ''

    if self.HID.controller then 
        ---------------------------------------------------------------
        --                     Left Thumbstick
        ---------------------------------------------------------------
        local l_stick_x = self.GAMEPAD.object:getGamepadAxis('leftx')
        local l_stick_y = self.GAMEPAD.object:getGamepadAxis('lefty')
        if Handy.controller.is_module_enabled(Handy.cc.swap_controller_cursor_stick) then
            l_stick_x = self.GAMEPAD.object:getGamepadAxis('rightx')
            l_stick_y = self.GAMEPAD.object:getGamepadAxis('righty')
        end
        --If there is something being dragged, we want to treat the left stick as a cursor input
        if self.dragging.target and math.abs(l_stick_x) + math.abs(l_stick_y) > 0.1 then 
            axis_interpretation = 'axis_cursor' --There is some cursor movement

            --deadzone of 10% for each axis of l_stick
            if math.abs(l_stick_x) < 0.1 then l_stick_x = 0 end
            if math.abs(l_stick_y) < 0.1 then l_stick_y = 0 end
            l_stick_x = l_stick_x + (l_stick_x>0 and -0.1 or 0) + (l_stick_x<0 and 0.1 or 0)
            l_stick_y = l_stick_y + (l_stick_y>0 and -0.1 or 0) + (l_stick_y<0 and 0.1 or 0)
    
            --Modify the cursor position according to the l_stick axis values
            G.CURSOR.T.x = G.CURSOR.T.x + dt*l_stick_x*self.axis_cursor_speed
            G.CURSOR.T.y = G.CURSOR.T.y + dt*l_stick_y*self.axis_cursor_speed
            G.CURSOR.VT.x = G.CURSOR.T.x
            G.CURSOR.VT.y = G.CURSOR.T.y

            self.cursor_position.x = G.CURSOR.T.x*(G.TILESCALE*G.TILESIZE)
            self.cursor_position.y = G.CURSOR.T.y*(G.TILESCALE*G.TILESIZE)

        --If nothing is being dragged, we want to treat the left stick as a dpad input
        else 
            self.axis_buttons.l_stick.current = self.axis_buttons.l_stick.previous
            if math.abs(l_stick_x) + math.abs(l_stick_y) > 0.5 then
                axis_interpretation = 'button' --left stick is no longer a cursor, can be set below from the right stick though

                self.axis_buttons.l_stick.current = math.abs(l_stick_x) > math.abs(l_stick_y) and
                (l_stick_x > 0 and 'dpright' or 'dpleft') or 
                (l_stick_y > 0 and 'dpdown' or 'dpup')
            elseif math.abs(l_stick_x) + math.abs(l_stick_y) < 0.3 then 
                self.axis_buttons.l_stick.current = ''
            end
        end

        ---------------------------------------------------------------
        --                     Right Thumbstick
        ---------------------------------------------------------------
        local r_stick_x = self.GAMEPAD.object:getGamepadAxis('rightx')
        local r_stick_y = self.GAMEPAD.object:getGamepadAxis('righty')
        if Handy.controller.is_module_enabled(Handy.cc.swap_controller_cursor_stick) then
            r_stick_x = self.GAMEPAD.object:getGamepadAxis('leftx')
            r_stick_y = self.GAMEPAD.object:getGamepadAxis('lefty')
        end
        G.DEADZONE = 0.2
        local mag = math.sqrt(math.abs(r_stick_x)^2 + math.abs(r_stick_y)^2)
        if mag > G.DEADZONE then 
            axis_interpretation = 'axis_cursor' --There is some cursor movement

            --deadzone of 20% for each axis of l_stick
            if math.abs(r_stick_x) < G.DEADZONE then r_stick_x = 0 end
            if math.abs(r_stick_y) < G.DEADZONE then r_stick_y = 0 end
            r_stick_x = r_stick_x + (r_stick_x>0 and -G.DEADZONE or 0) + (r_stick_x<0 and G.DEADZONE or 0)
            r_stick_y = r_stick_y + (r_stick_y>0 and -G.DEADZONE or 0) + (r_stick_y<0 and G.DEADZONE or 0)
    
            --Modify the cursor position according to the l_stick axis values
            G.CURSOR.T.x = G.CURSOR.T.x + dt*r_stick_x*self.axis_cursor_speed
            G.CURSOR.T.y = G.CURSOR.T.y + dt*r_stick_y*self.axis_cursor_speed
            G.CURSOR.VT.x = G.CURSOR.T.x
            G.CURSOR.VT.y = G.CURSOR.T.y

            self.cursor_position.x = G.CURSOR.T.x*(G.TILESCALE*G.TILESIZE)
            self.cursor_position.y = G.CURSOR.T.y*(G.TILESCALE*G.TILESIZE)
        end

        ---------------------------------------------------------------
        --                         Triggers
        ---------------------------------------------------------------
        --Triggers are just buttons
        local l_trig = self.GAMEPAD.object:getGamepadAxis('triggerleft')
        local r_trig = self.GAMEPAD.object:getGamepadAxis('triggerright')

        --Set the current to be the same as previous, only set to '' if the trigger value is low enough
        self.axis_buttons.l_trig.current = self.axis_buttons.l_trig.previous
        self.axis_buttons.r_trig.current = self.axis_buttons.r_trig.previous
 
        --Make a tilting effect when you press the triggers while hovering over a node that has tilt_var
        if self.focused.target and self.focused.target.tilt_var then 
            --self.focused.target.tilt_var.dx = 0.1*(r_trig - l_trig) + 0.9*self.focused.target.tilt_var.dx
        end

        if l_trig > 0.5 then
            self.axis_buttons.l_trig.current = 'triggerleft'
        elseif l_trig < 0.3 then 
            self.axis_buttons.l_trig.current = ''
        end
        if r_trig > 0.5 then
            self.axis_buttons.r_trig.current = 'triggerright'
        elseif r_trig < 0.3 then 
            self.axis_buttons.r_trig.current = ''
        end

        --return 'button' if trigger is being used and axis is not
        if self.axis_buttons.r_trig.current ~= '' or self.axis_buttons.l_trig.current ~= '' then 
            axis_interpretation = axis_interpretation or 'button'
        end

        self:handle_axis_buttons()
    end

    if axis_interpretation then self.interrupt.focus = false end 
    
    return axis_interpretation
end

function Controller:button_press_update(button, dt)
    if self.locks.frame then return end
    self.held_button_times[button] = 0
    self.interrupt.focus = false

    if not self:capture_focused_input(button, 'press', dt) then
        if button == "dpup" then
            self:navigate_focus('U')
        end
        if button == "dpdown" then
            self:navigate_focus('D')
        end
        if button == "dpleft" then
            self:navigate_focus('L')
        end
        if button == "dpright" then
            self:navigate_focus('R')
        end
    end

    if ((self.locked) and not G.SETTINGS.paused) or (self.locks.frame) or (self.frame_buttonpress) then return end
    self.frame_buttonpress = true

    if self.button_registry[button] and self.button_registry[button][1] and not self.button_registry[button][1].node.under_overlay then
        self.button_registry[button][1].click = true
    else
        if button == 'start' then
            if G.STATE == G.STATES.SPLASH then 
                G:delete_run()
                G:main_menu()
            end
        end
        if button == "a" then
            if self.focused.target and
            self.focused.target.config.focus_args and
            self.focused.target.config.focus_args.type == 'slider' and 
            (not G.CONTROLLER.HID.mouse and not G.CONTROLLER.HID.axis_cursor) then 
            else
                self:L_cursor_press()
            end
        end
        if button == 'b' then 
            if G.hand and self.focused.target and
            self.focused.target.area == G.hand then
                self:queue_R_cursor_press()
            else
                self.interrupt.focus = true
            end
        end
    end
end

function Controller:button_hold_update(button, dt)
    if ((self.locked) and not G.SETTINGS.paused) or (self.locks.frame) or (self.frame_buttonpress) then return end
    self.frame_buttonpress = true
    if self.held_button_times[button] then
        self.held_button_times[button] = self.held_button_times[button] + dt
        self:capture_focused_input(button, 'hold', dt)
    end
    if (button == 'dpleft' or button == 'dpright' or button == 'dpup' or button == 'dpdown') and not self.no_holdcap then
        self.repress_timer = self.repress_timer or 0.3
        if self.held_button_times[button] and (self.held_button_times[button] > self.repress_timer) then
            self.repress_timer = 0.1
            self.held_button_times[button] = 0
            self:button_press_update(button, dt)
        end
    end
end

function Controller:button_release_update(button, dt)
    if not self.held_button_times[button] then return end
    self.repress_timer = 0.3
    self.held_button_times[button] = nil
    if button == 'a' then
        self:L_cursor_release()
    end
end

function Controller:key_press_update(key, dt)
    if key == "escape" and (G.ACTIVE_MOD_UI or SMODS.IN_MODS_TAB) then
        G.FUNCS.exit_mods()
    end
    if self.locks.frame then return end
    if string.sub(key, 1, 2) == 'kp' then key = string.sub(key, 3) end
    if key == 'enter' then key = 'return' end

    if self.text_input_hook then
        if key == "escape" then
        	G.cry_edeck_select = nil
            self.text_input_hook = nil
        elseif key == "capslock" then
            self.capslock = not self.capslock
        else
            G.FUNCS.text_input_key{
                e=self.text_input_hook,
                key = key,
                caps = self.held_keys["lshift"] or self.held_keys["rshift"]
            }
        end
        return
    end

    if key == "escape" then
    	G.cry_edeck_select = nil
        if G.STATE == G.STATES.SPLASH then 
            G:delete_run()
            G:main_menu()
        else
            if not G.OVERLAY_MENU then 
                G.FUNCS:options()
            elseif not G.OVERLAY_MENU.config.no_esc then
                G.FUNCS:exit_overlay_menu()
            end
        end
    end

    if ((self.locked) and not G.SETTINGS.paused) or (self.locks.frame) or (self.frame_buttonpress) then return end
    self.frame_buttonpress = true    
    self.held_key_times[key] = 0


    for _, keybind in pairs(SMODS.Keybinds) do
        if keybind.action and keybind.key_pressed == key and keybind.event == 'pressed' then
            local execute = true
            for _, other_key in pairs(keybind.held_keys) do
                if not self.held_keys[other_key] then
                    execute = false
                    break
                end
            end
            if execute then
                keybind:action()
            end
        end
    end
    if not _RELEASE_MODE then
        if key == 'tab' and not G.debug_tools then
            G.debug_tools = UIBox{
                definition = create_UIBox_debug_tools(),
                config = {align='cr', offset = {x=G.ROOM.T.x + 11,y=0},major = G.ROOM_ATTACH, bond = 'Weak'}
            }
            G.E_MANAGER:add_event(Event({
                blockable = false,
                func = function()
                    G.debug_tools.alignment.offset.x = -4
                    return true
                end
            }))
        end
        if self.hovering.target and self.hovering.target:is(Card) then 
            local _card = self.hovering.target
            if  G.OVERLAY_MENU then 
                if key == "1"  then
                    unlock_card(_card.config.center)
                    _card:set_sprites(_card.config.center)
                end
                if key == "2" then
                    unlock_card(_card.config.center)
                    discover_card(_card.config.center)
                    _card:set_sprites(_card.config.center)
                end
                if key == "3" then
                    if _card.ability.set == 'Joker' and G.jokers and #G.jokers.cards < G.jokers.config.card_limit then
                        add_joker(_card.config.center.key)
                        _card:set_sprites(_card.config.center)
                    end
                    if _card.ability.consumeable and G.consumeables and #G.consumeables.cards < G.consumeables.config.card_limit then
                        add_joker(_card.config.center.key)
                        _card:set_sprites(_card.config.center)
                    end
                end
            end
            if key == 'q' then
                if (_card.ability.set == 'Joker' or _card.playing_card or _card.area) then
                    local found_index = 1
                    if _card.edition then
                        for i, v in ipairs(G.P_CENTER_POOLS.Edition) do
                            if v.key == _card.edition.key then
                                found_index = i
                                break
                            end
                        end
                    end
                    found_index = found_index + 1
                    if found_index > #G.P_CENTER_POOLS.Edition then found_index = found_index - #G.P_CENTER_POOLS.Edition end
                    local _edition = G.P_CENTER_POOLS.Edition[found_index].key
                    _card:set_edition(_edition, true, true)
                end
            end
        end
        if key == 'h' then
            G.debug_UI_toggle = not G.debug_UI_toggle
        end
        if key == 'b' then
            G:delete_run()
            G:start_run({})
        end
        if key == 'l' then
            G:delete_run()
            G.SAVED_GAME = get_compressed(G.SETTINGS.profile..'/'..'save.jkr')
            if G.SAVED_GAME ~= nil then G.SAVED_GAME = STR_UNPACK(G.SAVED_GAME) end
            G:start_run({savetext = G.SAVED_GAME})
        end
        if key == 'j' then
            G.debug_splash_size_toggle = not G.debug_splash_size_toggle
            G:delete_run()
            G:main_menu('splash')
        end
        if key == '8' then
            love.mouse.setVisible( not love.mouse.isVisible() )
        end
        if key == '9' then
            G.debug_tooltip_toggle = not G.debug_tooltip_toggle
        end
      if key == "space" then
          live_test()
      end
      if key == 'v' then
        if not G.prof then G.prof = require "engine/profile"; G.prof.start()
        else    G.prof:stop();
            print(G.prof.report()); G.prof = nil end
        end
       if key == "p" then
           G.SETTINGS.perf_mode = not G.SETTINGS.perf_mode
       end
    end
end

function Controller:key_hold_update(key, dt)
    if ((self.locked) and not G.SETTINGS.paused) or (self.locks.frame) or (self.frame_buttonpress) then return end
    --self.frame_buttonpress = true
    if self.held_key_times[key] then
        for _, keybind in pairs(SMODS.Keybinds) do
    if keybind.key_pressed == key and keybind.event == 'held' and keybind.held_duration then
        if self.held_key_times[key] > keybind.held_duration then
            local execute = true
            for _, other_key in pairs(keybind.held_keys) do
                if not self.held_keys[other_key] then
                    execute = false
                    break
                end
            end
            if execute then
                keybind:action()
                self.held_key_times[key] = nil
            end
        else
            self.held_key_times[key] = self.held_key_times[key] + dt
        end
    end
end
if key == "r" and not G.SETTINGS.paused or (key == "r" and Handy.__restart_from_game_over)
 and not (G.GAME and G.GAME.USING_CODE) then
            if self.held_key_times[key] > 0.7 then
                if not G.GAME.won and not G.GAME.seeded and not G.GAME.challenge then 
                    G.PROFILES[G.SETTINGS.profile].high_scores.current_streak.amt = 0
                end
                G:save_settings()
                self.held_key_times[key] = nil
                G.SETTINGS.current_setup = 'New Run'
                G.GAME.viewed_back = nil
                G.run_setup_seed = G.GAME.seeded
                G.challenge_tab = G.GAME and G.GAME.challenge and G.GAME.challenge_tab or nil
                if type(G.challenge_tab) == "string" and G.GAME and G.GAME.challenge then
                    G.challenge_tab = G.CHALLENGES[get_challenge_int_from_id(G.GAME.challenge or '') or ''] or {name = 'ERROR'}
                end
                G.forced_seed, G.setup_seed = nil, nil
                if G.GAME.seeded then G.forced_seed = G.GAME.pseudorandom.seed end
                G.forced_stake = G.GAME.stake
                if G.STAGE == G.STAGES.RUN then G.FUNCS.start_setup_run() end
                G.forced_stake = nil
                G.challenge_tab = nil
                G.forced_seed = nil
            else
                self.held_key_times[key] = self.held_key_times[key] + dt
            end
        end
    end
end

function Controller:key_release_update(key, dt)
for _, keybind in pairs(SMODS.Keybinds) do
    if keybind.action and keybind.key_pressed == key and keybind.event == 'released' then
        local execute = true
        for _, other_key in pairs(keybind.held_keys) do
            if not self.held_keys[other_key] then
                execute = false
                break
            end
        end
        if execute then
            keybind:action()
        end
    end
end
    if ((self.locked) and not G.SETTINGS.paused) or (self.locks.frame) or (self.frame_buttonpress) then return end
    self.frame_buttonpress = true
    if key == "a" and self.held_keys["g"] and not _RELEASE_MODE then
        G.DEBUG = not(G.DEBUG)
    end
    if key == 'tab' and G.debug_tools then
        G.debug_tools:remove()
        G.debug_tools = nil
    end
end

function Controller:key_press(key)
    self.pressed_keys[key] = true
    self.held_keys[key] = true
end

function Controller:key_release(key)
    self.held_keys[key] = nil
    self.released_keys[key] = true
end

function Controller:button_press(button)
    self.pressed_buttons[button] = true
    self.held_buttons[button] = true
end

function Controller:button_release(button)
    self.held_buttons[button] = nil
    self.released_buttons[button] = true
end

function Controller:get_cursor_collision(cursor_trans)
    self.collision_list = EMPTY(self.collision_list)
    self.nodes_at_cursor = EMPTY(self.nodes_at_cursor)
    
    if self.COYOTE_FOCUS then return end 
    if self.dragging.target then 
        self.dragging.target.states.collide.is = true
        self.nodes_at_cursor[#self.nodes_at_cursor+1] = self.dragging.target
        self.collision_list[#self.collision_list+1] = self.dragging.target
    end

    if not G.DRAW_HASH[1] or
        cursor_trans.x-G.ROOM.T.x < -G.DRAW_HASH_BUFF or cursor_trans.x-G.ROOM.T.x > G.TILE_W + G.DRAW_HASH_BUFF or
        cursor_trans.y-G.ROOM.T.y < -G.DRAW_HASH_BUFF or cursor_trans.y-G.ROOM.T.y > G.TILE_H + G.DRAW_HASH_BUFF then
        return 
    end

    local DRAW_HASH_SQUARE = G.DRAW_HASH
    for i = #DRAW_HASH_SQUARE, 1, -1 do
        local v = DRAW_HASH_SQUARE[i]
        if v:collides_with_point(cursor_trans) and not v.REMOVED and v:inside_overflow_boundaries(cursor_trans) then
            self.nodes_at_cursor[#self.nodes_at_cursor+1] = v
            if v.states.collide.can then
                v.states.collide.is = true
                self.collision_list[#self.collision_list+1] = v
            end
        end
    end
end

function Controller:set_cursor_hover()
    self.cursor_hover.T = self.cursor_hover.T or {}
    self.cursor_hover.T.x, self.cursor_hover.T.y =G.CURSOR.T.x, G.CURSOR.T.y
    self.cursor_hover.time = G.TIMERS.TOTAL

    self.cursor_hover.prev_target = self.cursor_hover.target
    self.cursor_hover.target = nil

    if self.interrupt.focus or ((self.locked) and (not G.SETTINGS.paused or G.screenwipe)) or self.locks.frame or self.COYOTE_FOCUS then self.cursor_hover.target = G.ROOM; return end 

    if self.HID.controller and self.focused.target and self.focused.target.states.hover.can then
        if (self.HID.dpad or self.HID.axis_cursor) and self.focused.target.states.collide.is then 
            self.cursor_hover.target = self.focused.target 
        else
            for _, v in ipairs(self.collision_list) do
                if v.states.hover.can then
                    self.cursor_hover.target = v
                    break 
                end
            end
        end
    else
        for _, v in ipairs(self.collision_list) do
            if v.states.hover.can and (not v.states.drag.is or self.HID.touch) then
                self.cursor_hover.target = v
                break 
            end
        end
    end

    if not self.cursor_hover.target or (self.dragging.target and not self.HID.touch) then self.cursor_hover.target = G.ROOM end
    if self.cursor_hover.target ~= self.cursor_hover.prev_target then self.cursor_hover.handled = false end 
end

function Controller:queue_L_cursor_press(x, y)
    if self.locks.frame then return end
    if G.STATE == G.STATES.SPLASH then 
        self:key_press('escape')
    end
    self.L_cursor_queue = {x = x, y = y}
end

function Controller:queue_R_cursor_press(x, y)
    if self.locks.frame then return end
    if not G.SETTINGS.paused and G.hand and G.hand.highlighted[1] then 
        if (G.play and #G.play.cards > 0) or
        Handy.deselect_hand.should_prevent() or
        (self.locked) or 
        (self.locks.frame) or
        (G.GAME.STOP_USE and G.GAME.STOP_USE > 0) then return end
        G.hand:unhighlight_all()
    end
end

function Controller:L_cursor_press(x, y)
    x = x or self.cursor_position.x
    y = y or self.cursor_position.y

    if ((self.locked) and (not G.SETTINGS.paused or G.screenwipe)) or (self.locks.frame) then return end

    self.cursor_down.T = {x = x/(G.TILESCALE*G.TILESIZE), y = y/(G.TILESCALE*G.TILESIZE)}
    self.cursor_down.time = G.TIMERS.TOTAL
    self.cursor_down.handled = false
    self.cursor_down.target = nil
    self.is_cursor_down = true

    local press_node =  (self.HID.touch and self.cursor_hover.target) or self.hovering.target or self.focused.target

    if press_node then 
        self.cursor_down.target = press_node.states.click.can and press_node or press_node:can_drag() or nil
    end

    if self.cursor_down.target == nil then 
        self.cursor_down.target = G.ROOM
    end
end

function Controller:L_cursor_release(x, y)
    x = x or self.cursor_position.x
    y = y or self.cursor_position.y

    if ((self.locked) and (not G.SETTINGS.paused or G.screenwipe)) or (self.locks.frame) then return end

    self.cursor_up.T = {x = x/(G.TILESCALE*G.TILESIZE), y = y/(G.TILESCALE*G.TILESIZE)}
    self.cursor_up.time = G.TIMERS.TOTAL
    self.cursor_up.handled = false
    self.cursor_up.target = nil
    self.is_cursor_down = false

    self.cursor_up.target = self.hovering.target or self.focused.target

    if self.cursor_up.target == nil then 
        self.cursor_up.target = G.ROOM
    end
end

function Controller:is_node_focusable(node)
    local ret_val = false
    if node.T.y > G.ROOM.T.h + 3 then return false end
    if not node.REMOVED and not node.under_overlay and (node.states.hover.can and not self.dragging.target or self.dragging.target == node) and
        (not not node.created_on_pause) == (not not G.SETTINGS.paused) and
        (node.states.visible) and (not node.UIBox or node.UIBox.states.visible) then
        if self.screen_keyboard then 
            if node.UIBox and node.UIBox == self.screen_keyboard and node.config.button then 
                ret_val = true
            end
        else
            if node:is(Card) and (node.facing == 'front' or node.area == G.hand or node.area == G.jokers or (node == G.deck)) and
                node.states.hover.can and not node.jimbo then
                ret_val = true
            end
            if node.config and node.config.force_focus then ret_val = true end
            if node.config and node.config.button then ret_val = true end
            if node.config and node.config.focus_args then
                if node.config.focus_args.type == 'none' or node.config.focus_args.funnel_from then
                    ret_val = false
                else
                    ret_val = true
                end
            end
        end
    end
    return ret_val
end

--essentially works like 'hovering' with any nodes that are focusable, but
--the nodes can also be navigated to via controller key inputs. If no direction is supplied,
--this function focuses on any nodes that collide with this cursor. If a direction is
--supplied, focuses on the nearest node in that direction.
function Controller:update_focus(dir)
    self.focused.prev_target = self.focused.target

    --Only needed when using a controller, hovering covers all KBM scenarios
    if not self.HID.controller or self.interrupt.focus or (self.locked) and (not G.SETTINGS.paused or G.screenwipe) then
        if self.focused.target then self.focused.target.states.focus.is = false end 
        self.focused.target = nil
        return 
    end

    G.ARGS.focus_list = EMPTY(G.ARGS.focus_list)
    G.ARGS.focusables = EMPTY(G.ARGS.focusables)

    -------------------------------------------------
    --  Find the focusable starting point
    -------------------------------------------------
    --First, is there currently a focusable target that is still valid?
    if self.focused.target then
        self.focused.target.states.focus.is = false

        --If that node is no longer focusable or if the cursor no longer collides with the node, remove the target
        if not self:is_node_focusable(self.focused.target) or not self.focused.target:collides_with_point(G.CURSOR.T) or self.HID.axis_cursor then 
            self.focused.target = nil
        end
    end

    --Now we check for 3 criteria:
    --1: is there a current focused target and no dpad direction? if so, we simply add the currsnt focused target to the focusable list and set the state to true
    --2: if not, and there is no dpad direction, iterate through the node list that the cursor intersects and check if any are focusable, only add the first one
    --3: if there is a direction, add all focusable moveables to the focusable list to check later
    if not dir and self.focused.target then 
        self.focused.target.states.focus.can = true
        G.ARGS.focusables[#G.ARGS.focusables+1] = self.focused.target
    else
        if not dir then 
            for k, v in ipairs(self.nodes_at_cursor) do
                v.states.focus.can = false
                v.states.focus.is = false
                if #G.ARGS.focusables == 0 and self:is_node_focusable(v) then 
                    v.states.focus.can = true
                    G.ARGS.focusables[#G.ARGS.focusables+1] = v
                end
            end
        else
            for k, v in pairs(G.MOVEABLES) do
                v.states.focus.can = false
                v.states.focus.is = false
                if self:is_node_focusable(v) then 
                    v.states.focus.can = true
                    G.ARGS.focusables[#G.ARGS.focusables+1] = v
                end
            end
        end
    end

    --If there are any valid focusables
    if #G.ARGS.focusables > 0 then 
        --If a direction control is supplied, set the target to be the closest node in that direction
        if dir then 
            if (dir == 'L' or dir == 'R') and self.focused.target and self.focused.target:is(Card) and self.focused.target.area == G.hand and G.hand then 
                local nu_rank = self.focused.target.rank + (dir == 'L' and -1 or 1)
                if nu_rank > #G.hand.cards then nu_rank = 1 end
                if nu_rank == 0 then nu_rank = #G.hand.cards end
                if nu_rank ~= self.focused.target.rank then G.ARGS.focus_list[1] = {node = G.hand.cards[nu_rank]} end
            else
                --set the cursor position to where it currently is on screen
                G.ARGS.focus_cursor_pos = G.ARGS.focus_cursor_pos or {}
                G.ARGS.focus_cursor_pos.x, G.ARGS.focus_cursor_pos.y = G.CURSOR.T.x - G.ROOM.T.x, G.CURSOR.T.y - G.ROOM.T.y

                --if there is a focused target, set the cursor to the midpoint
                if self.focused.target then
                    _ft =  self.focused.target
                    if self.focused.target.config.focus_args and self.focused.target.config.focus_args.funnel_to then 
                        _ft = self.focused.target.config.focus_args.funnel_to
                    end
                    G.ARGS.focus_cursor_pos.x, G.ARGS.focus_cursor_pos.y = _ft.T.x + 0.5*_ft.T.w,_ft.T.y + 0.5*_ft.T.h
                --if not but there is a focusable hovering target, put the cursor on it instead
                elseif self.hovering.target and self.hovering.target.states.focus.can then
                    G.ARGS.focus_cursor_pos.x, G.ARGS.focus_cursor_pos.y = self.hovering.target:put_focused_cursor()
                    G.ARGS.focus_cursor_pos.x = G.ARGS.focus_cursor_pos.x / (G.TILESCALE*G.TILESIZE) - G.ROOM.T.x
                    G.ARGS.focus_cursor_pos.y = G.ARGS.focus_cursor_pos.y / (G.TILESCALE*G.TILESIZE) - G.ROOM.T.y
                end

                --set the list to be all the nodes in that direction sorted by the closest node
                for _, v in pairs(G.ARGS.focusables) do
                    if v ~= self.hovering.target and v ~= self.focused.target then 
                        local eligible = false

                        if v.config.focus_args and v.config.focus_args.funnel_to then
                            v = v.config.focus_args.funnel_to
                        end

                        G.ARGS.focus_vec = G.ARGS.focus_vec or {}
                        G.ARGS.focus_vec.x = v.T.x + 0.5*v.T.w - (G.ARGS.focus_cursor_pos.x)
                        G.ARGS.focus_vec.y = v.T.y + 0.5*v.T.h - (G.ARGS.focus_cursor_pos.y)

                        if v.config.focus_args and v.config.focus_args.nav then
                            if v.config.focus_args.nav == 'wide' then
                                if G.ARGS.focus_vec.y > 0.1 and dir == 'D' then eligible = true
                                elseif G.ARGS.focus_vec.y < -0.1 and dir == 'U' then eligible = true
                                elseif math.abs(G.ARGS.focus_vec.y) < v.T.h/2 then eligible = true end
                            elseif v.config.focus_args.nav == 'tall' then
                                if G.ARGS.focus_vec.x > 0.1 and dir == 'R' then eligible = true
                                elseif G.ARGS.focus_vec.x < -0.1 and dir == 'L' then eligible = true
                                elseif math.abs(G.ARGS.focus_vec.x) < v.T.w/2 then eligible = true end
                            end
                        elseif math.abs(G.ARGS.focus_vec.x) > math.abs(G.ARGS.focus_vec.y) then
                            if G.ARGS.focus_vec.x > 0 and dir == 'R' then eligible = true
                            elseif G.ARGS.focus_vec.x < 0 and dir == 'L' then eligible = true end
                        else
                            if G.ARGS.focus_vec.y > 0 and dir == 'D' then eligible = true
                            elseif G.ARGS.focus_vec.y < 0 and dir == 'U' then eligible = true end
                        end

                        if eligible then 
                            G.ARGS.focus_list[#G.ARGS.focus_list+1] = {node = v, dist = math.abs(G.ARGS.focus_vec.x) + math.abs(G.ARGS.focus_vec.y)}
                        end
                    end
                end
                if #G.ARGS.focus_list < 1 then
                    if self.focused.target then self.focused.target.states.focus.is = true end
                    return
                end
                table.sort(G.ARGS.focus_list, function (a, b) return a.dist < b.dist end)
            end
        else
            if self.focused.target then
                G.ARGS.focus_list[#G.ARGS.focus_list+1] = {node = self.focused.target, dist = 0}
            else
            --otherwise, get the focusable that collides
            G.ARGS.focus_list[#G.ARGS.focus_list+1] = {node = G.ARGS.focusables[1], dist = 0}
            end
        end
    end

    --now with the lists created, set the focused target to be the first node in the list
    if G.ARGS.focus_list[1] then 
        if G.ARGS.focus_list[1].node.config and G.ARGS.focus_list[1].node.config.focus_args and G.ARGS.focus_list[1].node.config.focus_args.funnel_from then 
            self.focused.target = G.ARGS.focus_list[1].node.config.focus_args.funnel_from
        else
            self.focused.target = G.ARGS.focus_list[1].node
        end
        if self.focused.target ~= self.focused.prev_target then G.VIBRATION = G.VIBRATION + 0.7 end
    else
        self.focused.target = nil
    end

    if self.focused.target then self.focused.target.states.focus.is = true end
end

function Controller:capture_focused_input(button, input_type, dt)
    local ret = false
    local focused = self.focused.target
    local extern_button = false
    self.no_holdcap = nil
    
    --Implementing 'coyote time' type selection where a full button press isnt needed to select a card in hand. As long as a button down has been registered
    --before a timer is up and the dpad is used to move to the next card it should register
    if input_type == 'press' and (button == 'dpleft' or button == 'dpright') and
        focused and self.dragging.target and
        (self.held_button_times['a'] and self.held_button_times['a'] < 0.12) and
        focused.area and focused.area:can_highlight(focused) then
            self:L_cursor_release()
            self:navigate_focus(button == 'dpleft' and 'L' or 'R')
            self.held_button_times['a'] = nil
            self.COYOTE_FOCUS = true
        ret = true
    elseif input_type == 'press' and focused and focused.area and focused == self.dragging.target then 
        focused.states.drag.is = false
        if button == 'dpleft' and focused.rank > 1 then
            focused.rank = focused.rank - 1 
            focused.area.cards[focused.rank].rank = focused.rank + 1
            table.sort(focused.area.cards, function (a, b) return a.rank < b.rank end)
            focused.area:align_cards()
            self:update_cursor()
        elseif button == 'dpright' and focused.rank < #focused.area.cards then 
            focused.rank = focused.rank + 1 
            focused.area.cards[focused.rank].rank = focused.rank - 1
            table.sort(focused.area.cards, function (a, b) return a.rank < b.rank end)
            focused.area:align_cards()
            self:update_cursor()
        end
        focused.states.drag.is = true
        ret = true
    end

    if G.OVERLAY_MENU and not self.screen_keyboard and input_type == 'press' and G.OVERLAY_MENU:get_UIE_by_ID('tab_shoulders') and (button == 'leftshoulder' or button == 'rightshoulder') then 
        focused = G.OVERLAY_MENU:get_UIE_by_ID('tab_shoulders')
        extern_button = true
    end
    if G.OVERLAY_MENU and not self.screen_keyboard and input_type == 'press' and G.OVERLAY_MENU:get_UIE_by_ID('cycle_shoulders') and (button == 'leftshoulder' or button == 'rightshoulder') then 
        focused = G.OVERLAY_MENU:get_UIE_by_ID('cycle_shoulders').children[1]
        extern_button = true
    end
    if focused and focused.config.focus_args then
        if focused.config.focus_args.type == 'cycle' and input_type == 'press' then
            if ((extern_button and button == 'leftshoulder') or (not extern_button and button == 'dpleft')) then
                focused.children[1]:click()
                ret = true
            end
            if ((extern_button and button == 'rightshoulder') or (not extern_button and button == 'dpright'))  then
                focused.children[3]:click()
                ret = true
            end
        end
        if focused.config.focus_args.type == 'tab' and input_type == 'press' then
            local proto_choices = focused.UIBox:get_group(nil, focused.children[1].children[1].config.group)
            local choices = {}
            for k, v in ipairs(proto_choices) do
                if v.config.choice and v.config.button then choices[#choices+1] = v end
            end
            for k, v in ipairs(choices) do
                if v.config.chosen then 
                    if ((extern_button and button == 'leftshoulder') or (not extern_button and button == 'dpleft')) then 
                        local next_i = k ~= 1 and (k-1) or (#choices)
                        if focused.config.focus_args.no_loop and next_i > k then ret = nil
                        else
                            choices[next_i]:click()
                            self:snap_to({node = choices[next_i]})
                            self:update_cursor()
                            ret = true
                        end
                    elseif ((extern_button and button == 'rightshoulder') or (not extern_button and button == 'dpright')) then 
                        local next_i = k ~= #choices and (k+1) or (1)
                        if focused.config.focus_args.no_loop and next_i < k then ret = nil
                        else
                            choices[next_i]:click()
                            self:snap_to({node = choices[next_i]})
                            self:update_cursor()
                            ret = true
                        end
                    end
                    break
                end
            end
        end
        if focused.config.focus_args.type == 'slider' then
            if button == 'dpleft' then
                self.no_holdcap = true
                if input_type == 'hold' and self.held_button_times[button] > 0.2 then
                    G.FUNCS.slider_descreet(focused.children[1], -dt*self.held_button_times[button]*0.6)
                end
                if input_type == 'press' then 
                    G.FUNCS.slider_descreet(focused.children[1], -0.01)
                end
                ret = true
            end
            if button == 'dpright' then
                self.no_holdcap = true
                if input_type == 'hold' and self.held_button_times[button] > 0.2 then
                    G.FUNCS.slider_descreet(focused.children[1], dt*self.held_button_times[button]*0.6)
                end
                if input_type == 'press' then 
                    G.FUNCS.slider_descreet(focused.children[1], 0.01)
                end
                ret = true
            end
        end
    end
    if ret == true then G.VIBRATION = G.VIBRATION +1 end
    return ret
end

function Controller:navigate_focus(dir)
    --Get the corresponding focus target first, with or without a direction
    self:update_focus(dir)

    --Set the cursor to be in the correct position for that target
    self:update_cursor()
end

