local input = require('openmw.input')
local util = require('openmw.util')
local async = require('openmw.async')
local storage = require('openmw.storage')

local actionPressHandlers = {}
local function onActionPress(id, handler)
    actionPressHandlers[id] = actionPressHandlers[id] or {}
    table.insert(actionPressHandlers[id], handler)
end

local function bindHold(key, actionId)
    input.bindAction(key, async:callback(function()
        return input.isActionPressed(actionId)
    end), {})
end

local function bindMovement(key, actionId, axisId, direction)
    input.bindAction(key, async:callback(function()
        local actionActive = input.isActionPressed(actionId)
        local axisActive = input.getAxisValue(axisId) * direction > 0
        return (actionActive or axisActive) and 1 or 0
    end), {})
end

local function bindTrigger(key, actionid)
    onActionPress(actionid, function()
        input.activateTrigger(key)
    end)
end

bindTrigger('AlwaysRun', input.ACTION.AlwaysRun)
bindTrigger('ToggleSneak', input.ACTION.Sneak)
bindTrigger('ToggleWeapon', input.ACTION.ToggleWeapon)
bindTrigger('ToggleSpell', input.ACTION.ToggleSpell)
bindTrigger('Jump', input.ACTION.Jump)
bindTrigger('AutoMove', input.ACTION.AutoMove)
bindTrigger('Inventory', input.ACTION.Inventory)
bindTrigger('Journal', input.ACTION.Journal)
bindTrigger('QuickKeysMenu', input.ACTION.QuickKeysMenu)

bindHold('TogglePOV', input.ACTION.TogglePOV)
bindHold('Sneak', input.ACTION.Sneak)

bindHold('Run', input.ACTION.Run)
input.bindAction('Run', async:callback(function(_, value)
    local controllerInput = util.vector2(
        input.getAxisValue(input.CONTROLLER_AXIS.MoveForwardBackward),
        input.getAxisValue(input.CONTROLLER_AXIS.MoveLeftRight)
    ):length2()
    return value or controllerInput > 0.25
end), {})

input.bindAction('Use', async:callback(function()
    -- The value "0.6" shouldn't exceed the triggering threshold in BindingsManager::actionValueChanged.
    -- TODO: Move more logic from BindingsManager to Lua and consider to make this threshold configurable.
    return input.isActionPressed(input.ACTION.Use) or input.getAxisValue(input.CONTROLLER_AXIS.TriggerRight) >= 0.6
end), {})

bindMovement('MoveBackward', input.ACTION.MoveBackward, input.CONTROLLER_AXIS.MoveForwardBackward, 1)
bindMovement('MoveForward', input.ACTION.MoveForward, input.CONTROLLER_AXIS.MoveForwardBackward, -1)
bindMovement('MoveRight', input.ACTION.MoveRight, input.CONTROLLER_AXIS.MoveLeftRight, 1)
bindMovement('MoveLeft', input.ACTION.MoveLeft, input.CONTROLLER_AXIS.MoveLeftRight, -1)

do
    local zoomInOut = 0
    onActionPress(input.ACTION.ZoomIn, function()
        zoomInOut = zoomInOut + 1
    end)
    onActionPress(input.ACTION.ZoomOut, function()
        zoomInOut = zoomInOut - 1
    end)
    input.bindAction('Zoom3rdPerson', async:callback(function(dt, _, togglePOV)
        local Zoom3rdPerson = zoomInOut * 10
        if togglePOV then
            local triggerLeft = input.getAxisValue(input.CONTROLLER_AXIS.TriggerLeft)
            local triggerRight = input.getAxisValue(input.CONTROLLER_AXIS.TriggerRight)
            local controllerZoom = (triggerRight - triggerLeft) * 100 * dt
            Zoom3rdPerson = Zoom3rdPerson + controllerZoom
        end
        zoomInOut = 0
        return Zoom3rdPerson
    end), { 'TogglePOV' })
end

local bindingSection = storage.playerSection('OMWInputBindings')

local devices = {
    keyboard = true,
    mouse = true,
    controller = true
}

local function invalidBinding(binding)
    if not binding.key then
        return 'has no key'
    elseif binding.type ~= 'action' and binding.type ~= 'trigger' then
        return string.format('has invalid type', binding.type)
    elseif binding.type == 'action' and not input.actions[binding.key] then
        return string.format("action %s doesn't exist", binding.key)
    elseif binding.type == 'trigger' and not input.triggers[binding.key] then
        return string.format("trigger %s doesn't exist", binding.key)
    elseif not binding.device or not devices[binding.device] then
        return string.format("invalid device %s", binding.device)
    elseif not binding.button then
        return 'has no button'
    end
end

local boundActions = {}
local actionBindings = {}

local function bindAction(binding, id)
    local action = binding.key
    actionBindings[action] = actionBindings[action] or {}
    actionBindings[action][id] = binding
    if not boundActions[action] then
        boundActions[binding.key] = true
        input.bindAction(action, async:callback(function()
            for _, binding in pairs(actionBindings[action] or {}) do
                if binding.device == 'keyboard' then
                    if input.isKeyPressed(binding.button) then
                        return true
                    end
                elseif binding.device == 'mouse' then
                    if input.isMouseButtonPressed(binding.button) then
                        return true
                    end
                elseif binding.device == 'controller' then
                    if input.isControllerButtonPressed(binding.button) then
                        return true
                    end
                end
            end
            return false
        end), {})
    end
end

local triggerBindings = {}
for device in pairs(devices) do triggerBindings[device] = {} end

local function bindTrigger(binding, id)
    local deviceBindings = triggerBindings[binding.device]
    deviceBindings[binding.button] = deviceBindings[binding.button] or {}
    deviceBindings[binding.button][id] = binding
end

local function registerBinding(binding, id)
    local invalid = invalidBinding(binding)
    if invalid then
        print(string.format('Skipping invalid binding %s: %s', id, invalid))
    elseif binding.type == 'action' then
        bindAction(binding, id)
    elseif binding.type == 'trigger' then
        bindTrigger(binding, id)
    end
end

function clearBinding(id)
    for _, deviceBindings in pairs(triggerBindings) do
        for _, buttonBindings in pairs(deviceBindings) do
            buttonBindings[id] = nil
        end
    end

    for _, bindings in pairs(actionBindings) do
        bindings[id] = nil
    end
end

bindingSection:subscribe(async:callback(function(_, id)
    if not id then return end
    local binding = bindingSection:get(id)
    clearBinding(id)
    if binding ~= nil then
        registerBinding(binding, id)
    end
    return id
end))


local initiated = false

local function init()
    for id, binding in pairs(bindingSection:asTable()) do
        registerBinding(binding, id)
    end
end

return {
    engineHandlers = {
        onFrame = function()
            if not initiated then
                initiated = true
                init()
            end
        end,
        onInputAction = function(id)
            if not actionPressHandlers[id] then
                return
            end
            for _, handler in ipairs(actionPressHandlers[id]) do
                handler()
            end
        end,
        onKeyPress = function(e)
            local buttonTriggers = triggerBindings.keyboard[e.code]
            if not buttonTriggers then return end
            for _, binding in pairs(buttonTriggers) do
                input.activateTrigger(binding.key)
            end
        end,
        onMouseButtonPress = function(button)
            local buttonTriggers = triggerBindings.mouse[button]
            if not buttonTriggers then return end
            for _, binding in pairs(buttonTriggers) do
                input.activateTrigger(binding.key)
            end
        end,
        onControllerButtonPress = function(id)
            local buttonTriggers = triggerBindings.controller[id]
            if not buttonTriggers then return end
            for _, binding in pairs(buttonTriggers) do
                input.activateTrigger(binding.key)
            end
        end,
    }
}