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

local I = require('openmw.interfaces')

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 keyboardPresses = {}
local keybordHolds = {}
local boundActions = {}

local function bindAction(action)
    if boundActions[action] then return end
    boundActions[action] = true
    input.bindAction(action, async:callback(function()
        if keybordHolds[action] then
            for _, binding in pairs(keybordHolds[action]) do
                if input.isKeyPressed(binding.code) then return true end
            end
        end
        return false
    end), {})
end

local function registerBinding(binding, id)
    if not input.actions[binding.key] and not input.triggers[binding.key] then
        print(string.format('Skipping binding for unknown action or trigger: "%s"', binding.key))
        return
    end
    if binding.type == 'keyboardPress' then
        local bindings = keyboardPresses[binding.code] or {}
        bindings[id] = binding
        keyboardPresses[binding.code] = bindings
    elseif binding.type == 'keyboardHold' then
        local bindings = keybordHolds[binding.key] or {}
        bindings[id] = binding
        keybordHolds[binding.key] = bindings
        bindAction(binding.key)
    else
        error('Unknown binding type "' .. binding.type .. '"')
    end
end

function clearBinding(id)
    for _, boundTriggers in pairs(keyboardPresses) do
        boundTriggers[id] = nil
    end
    for _, boundKeys in pairs(keybordHolds) do
        boundKeys[id] = nil
    end
end

local function updateBinding(id, binding)
    bindingSection:set(id, binding)
    clearBinding(id)
    if binding ~= nil then
        registerBinding(binding, id)
    end
    return id
end

local interfaceL10n = core.l10n('interface')

I.Settings.registerRenderer('inputBinding', function(id, set, arg)
    if type(id) ~= 'string' then error('inputBinding: must have a string default value') end
    if not arg.type then error('inputBinding: type argument is required') end
    if not arg.key then error('inputBinding: key argument is required') end
    local info = input.actions[arg.key] or input.triggers[arg.key]
    if not info then return {} end

    local l10n = core.l10n(info.key)

    local name = {
        template = I.MWUI.templates.textNormal,
        props = {
            text = l10n(info.name),
        },
    }

    local description = {
        template = I.MWUI.templates.textNormal,
        props = {
            text = l10n(info.description),
        },
    }

    local binding = bindingSection:get(id)
    local label = binding and input.getKeyName(binding.code) or interfaceL10n('None')

    local recorder = {
        template = I.MWUI.templates.textEditLine,
        props = {
            readOnly = true,
            text = label,
        },
        events = {
            focusGain = async:callback(function()
                if binding == nil then return end
                updateBinding(id, nil)
                set(id)
            end),
            keyPress = async:callback(function(key)
                if binding ~= nil or key.code == input.KEY.Escape then return end

                local newBinding = {
                    code = key.code,
                    type = arg.type,
                    key = arg.key,
                }
                updateBinding(id, newBinding)
                set(id)
            end),
        },
    }

    local row = {
        type = ui.TYPE.Flex,
        props = {
            horizontal = true,
        },
        content = ui.content {
            name,
            { props = { size = util.vector2(10, 0) } },
            recorder,
        },
    }
    local column = {
        type = ui.TYPE.Flex,
        content = ui.content {
            row,
            description,
        },
    }

    return column
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 bindings = keyboardPresses[e.code]
            if bindings then
                for _, binding in pairs(bindings) do
                    input.activateTrigger(binding.key)
                end
            end
        end,
    }
}