if Binder then return end
Binder = {}
local Binder = Binder

-- Localize builtin functions we use
local ipairs = ipairs
local next = next
local pairs = pairs
local tinsert = table.insert

-- Localize ESO API functions we use
local d = d
local strjoin = zo_strjoin
local strsplit = zo_strsplit
local GetNumActionLayers = GetNumActionLayers
local GetActionLayerInfo = GetActionLayerInfo
local GetActionLayerCategoryInfo = GetActionLayerCategoryInfo
local GetActionInfo = GetActionInfo
local GetActionIndicesFromName = GetActionIndicesFromName

local function print(...)
    d(strjoin("", ...))
end

function Binder.BuildActionTables()
    local actionHierarchy = {}
    local actionNames = {}
    local layers = GetNumActionLayers()
    for layerIndex=1, layers do
        local layerName, categories = GetActionLayerInfo(layerIndex)
        local layer = {}
        for categoryIndex=1, categories do
            local category = {}
            local categoryName, actions = GetActionLayerCategoryInfo(layerIndex, categoryIndex)
            for actionIndex=1, actions do
                local actionName, isRebindable, isHidden = GetActionInfo(layerIndex, categoryIndex, actionIndex)
                if isRebindable then
                    local action = {
                        ["name"] = actionName,
                        ["rebind"] = isRebindable,
                        ["hidden"] = isHidden,
                    }
                    category[actionIndex] = action
                    tinsert(actionNames, actionName)
                end
            end
            if next(category) ~= nil then
                category["name"] = categoryName
                layer[categoryIndex] = category
            end
        end
        if next(layer) ~= nil then
            layer["name"] = layerName
            actionHierarchy[layerIndex] = layer
        end
    end
    Binder.actionHierarchy = actionHierarchy
    Binder.actionNames = actionNames
end

function Binder.BuildBindingsTable()
    if not Binder.actionNames then Binder.BuildActionTables() end
    local bindings = {}
    local bindCount = 0
    local maxBindings = GetMaxBindingsPerAction()
    for index, actionName in ipairs(Binder.actionNames) do
        local layerIndex, categoryIndex, actionIndex = GetActionIndicesFromName(actionName)
        local actionBindings = {}
        for bindIndex=1, maxBindings do
            local keyCode, mod1, mod2, mod3, mod4 = GetActionBindingInfo(layerIndex, categoryIndex, actionIndex, bindIndex)
            if keyCode ~= 0 then
                local bind = {
                    ["keyCode"] = keyCode,
                    ["mod1"] = mod1,
                    ["mod2"] = mod2,
                    ["mod3"] = mod3,
                    ["mod4"] = mod4,
                }
                tinsert(actionBindings, bind)
                bindCount = bindCount + 1
            end
        end
        bindings[actionName] = actionBindings
    end
    Binder.bindings = bindings
    Binder.bindCount = bindCount
end

function Binder.RestoreBindingsFromTable()
    local bindCount = 0
    local skippedBindCount = 0
    local maxBindings = GetMaxBindingsPerAction()
    for actionName, actionBindings in pairs(Binder.bindings) do
        local layerIndex, categoryIndex, actionIndex = GetActionIndicesFromName(actionName)
        CallSecureProtected("UnbindAllKeysFromAction", layerIndex, categoryIndex, actionIndex)
        for bindingIndex, bind in ipairs(actionBindings) do
            if bindingIndex <= maxBindings then
                CallSecureProtected("BindKeyToAction", layerIndex, categoryIndex, actionIndex, bindingIndex, bind["keyCode"], bind["mod1"], bind["mod2"], bind["mod3"], bind["mod4"])
                bindCount = bindCount + 1
            else
                skippedBindCount = skippedBindCount + 1
            end
        end
    end
end

function Binder.SaveBindings(bindSetName)
    if bindSetName == nil or bindSetName == "" then
        print("Usage: /binder save <set name>")
        return
    end
    Binder.BuildBindingsTable()
    Binder.savedVariables.bindings[bindSetName] = Binder.bindings
    print("Saved ", Binder.bindCount, " bindings to bind set '", bindSetName, "'.")
end

function Binder.LoadBindings(bindSetName)
    if bindSetName == nil or bindSetName == "" then
        print("Usage: /binder load <set name>")
        return
    end
    if Binder.savedVariables.bindings[bindSetName] == nil then
        print("Bind set '", bindSetName, "' does not exist.")
        return
    end
    Binder.bindings = Binder.savedVariables.bindings[bindSetName]
    Binder.RestoreBindingsFromTable()
    print("Loaded ", Binder.bindCount, " bindings from bind set '", bindSetName, "'.")
end

function Binder.ListBindings()
    local sets = {}
    for setName in pairs(Binder.savedVariables.bindings) do
        table.insert(sets, setName)
    end
    table.sort(sets)
    print("Bind sets saved:")
    for i,setName in ipairs(sets) do
        print("- ", setName)
    end
end

Binder.commands = {
    ["build"] = Binder.BuildBindingsTable,
    ["save"] = Binder.SaveBindings,
    ["load"] = Binder.LoadBindings,
    ["list"] = Binder.ListBindings,
}

function Binder.SlashCommandHelp()
    print("Binder usage:")
    print("- /binder save <set name> (saves current keybindings)")
    print("- /binder load <set name> (loads specified keybindings)")
    print("- /binder list (lists all saved bind sets)")
end

function Binder.SlashCommand(argtext)
    local args = {strsplit(" ", argtext)}
    if next(args) == nil then
        Binder.SlashCommandHelp()
        return
    end

    local command = Binder.commands[args[1]]
    if not command then
        print("Binder: unknown command '", args[1], "'.")
        Binder.SlashCommandHelp()
        return
    end

    -- Call the selected function with everything except the original command
    command(unpack(args, 2))
end

function Binder.OnAddOnLoaded(event, addonName)
    if addonName ~= "Binder" then return end
    Binder.savedVariables = ZO_SavedVars:NewAccountWide("Binder_SavedVariables", 1, "default", {["bindings"]={}})
end

EVENT_MANAGER:RegisterForEvent("Binder", EVENT_ADD_ON_LOADED, Binder.OnAddOnLoaded)

if FW_SlashCommand ~= nil then
    -- Register via Wykkyd's framework for those who use it, to allow macroing
    FW_SlashCommand("binder", Binder.SlashCommand)
else
    -- But don't require the framework.
    SLASH_COMMANDS["/binder"] = Binder.SlashCommand
end