local re = re
local sdk = sdk
local d2d = d2d
local imgui = imgui
local log = log
local json = json
local draw = draw
local thread = thread
local require = require
local tostring = tostring
local pairs = pairs
local ipairs = ipairs
local math = math
local string = string
local table = table
local type = type
local _ = _


local SDK = require("_CatLib.sdk")
local CACHE = require("_CatLib.cache")
local Utils = require("_CatLib.utils")

local _M = {}

_M.Player = nil
_M.WeaponHandling = nil
_M.MotionSupporter = nil
_M.InputCache = {}

function _M.Init()
    if _M.Player == nil then
        _M.Player = sdk.get_managed_singleton("app.PlayerManager"):getMasterPlayer():get_Character()
        _M.WeaponHandling = _M.Player:get_WeaponHandling()
        _M.MotionSupporter = _M.Player._HunterMotionSupporter
    end
end

function _M.Refresh()
    _M.Player = nil
    _M.Init()
end

SDK.HookFunc("app.GUIManager", "onPlEquipChange()", function ()
    _M.Refresh()    
end)

-------------------------------
-- Global Vars
-------------------------------

local KeyboardKeyIndex = Utils.GetEnumMap("ace.ACE_MKB_KEY.INDEX")
local KeyboardKeyIndex_ToValue = {}
for idx, name in pairs(KeyboardKeyIndex) do
    KeyboardKeyIndex_ToValue[name] = idx
end

local KeyboardManager = SDK.GetSingleton("ace.MouseKeyboardManager")
_M.KeyboardManager = KeyboardManager

---@param key string
local function KeyOn(key)
    return KeyboardManager:get_MainMouseKeyboard():isOn(KeyboardKeyIndex_ToValue[key])
end

local CheckCommandResult = SDK.TypeMethod("app.PlayerUtil", "checkCommandResult(app.cPlayerBTableCommandWork, app.HunterDef.BTABLE_COMMAND_OPTION, app.PlayerCommand.TYPE)")

local PlayerCommand_Type = Utils.GetEnumMap("app.PlayerCommand.TYPE")
local PlayerCommand_Type_ToValue = {}
for idx, name in pairs(PlayerCommand_Type) do
    PlayerCommand_Type_ToValue[name] = idx
end

local PlayerCommand_TypeFixed = Utils.GetEnumMap("app.PlayerCommand.TYPE_Fixed")
local PlayerCommand_TypeFixed_ToValue = {}
for idx, name in pairs(PlayerCommand_TypeFixed) do
    PlayerCommand_TypeFixed_ToValue[name] = idx
end

function _M.CheckCommandResult(commandWork, option, CommandType)
    return CheckCommandResult:call(nil, commandWork, option, CommandType)
end

---@param commandType string
---@param commandWork ace.btable.cCommandWork
---@param debug boolean show debug log
function _M.LogCommandResult(commandType, commandWork, debug)
    if debug then
        local CommandType = PlayerCommand_Type_ToValue[commandType]
        local NoneAtk01 = CheckCommandResult:call(nil, commandWork, 0, CommandType)
        local WatchCancelAtk01 = CheckCommandResult:call(nil, commandWork, 1, CommandType)
        local NotWatchCancelAtk01 = CheckCommandResult:call(nil, commandWork, 2, CommandType)
        log.info(string.format("CmdResult %s: %s, %s, %s [NonCancel]", PlayerCommand_Type[CommandType], tostring(NoneAtk01), tostring(WatchCancelAtk01), tostring(NotWatchCancelAtk01)))
    end
end

---@param key string
---@param cancelIndex app.ACTION_CANCEL_INDEX
---@param debug boolean show debug log
function _M.LogRawInput(key, cancelIndex, debug)
    local Pressed = KeyOn(key)
    local CachedPressed = _M.InputCache[key]

    if cancelIndex then
        if debug then
            log.info(string.format("Keyboard %s: %s (Cached: %s), PreCancel: %s, Cancel: %s", key, tostring(Pressed), tostring(CachedPressed), tostring(_M.MotionPreCancel(cancelIndex)), tostring(_M.MotionCancel(cancelIndex))))
        end
    else
        if debug then
            log.info(string.format("Keyboard %s: %s (Cached: %s)", key, tostring(Pressed), tostring(CachedPressed)))
        end
    end
end

-------------------------------
-- State Management
-------------------------------

---@param layer app.AppActionDef.LAYER
---@param actionID ace.ACTION_ID
function _M.ChangeAction(layer, actionID)
    _M.Init()
    return _M.Player:call("changeActionRequest(app.AppActionDef.LAYER, ace.ACTION_ID, System.Boolean)", layer, actionID, false)
end


-------------------------------
-- FSM
-------------------------------

---@param log_str nil|string
function _M.AbortFSM(log_str)
    local storage = thread.get_hook_storage()
    storage["retval"] = true
    if log_str then
        log.info(log_str)
    end
    return sdk.PreHookResult.SKIP_ORIGINAL
end

function _M.AbortFSMPostHook(retval)
    local storage = thread.get_hook_storage()
    local result = storage["retval"]
    if result then
        -- log.info(string.format("Skipped"))
        return sdk.to_ptr(result)
    end

    return retval
end

-------------------------------
-- Input Check
-------------------------------

---@param cancelIndex app.ACTION_CANCEL_INDEX
function _M.MotionPreCancel(cancelIndex)
    _M.Init()

    return _M.Player:call("checkPreCancelMotion(app.ACTION_CANCEL_INDEX)", cancelIndex) 
end

---@param cancelIndex app.ACTION_CANCEL_INDEX
function _M.MotionCancel(cancelIndex)
    if cancelIndex == nil then
        return true
    end
    _M.Init()

    local result = _M.Player:call("checkCancelMotion(app.ACTION_CANCEL_INDEX, System.Boolean)", cancelIndex, false)
    -- log.info(string.format("Player can cancel motion? %s", tostring(result)))

    return result
end

---@param cancelIndex app.ACTION_CANCEL_INDEX
function _M.IsValidMotionInputCache(cancelIndex)
    if cancelIndex == nil then
        return true
    end
    _M.Init()

    return _M.MotionPreCancel(cancelIndex) or _M.MotionCancel(cancelIndex)
end

function _M._CacheInputCanProcess(commandWork, key, cancelIndex)
    _M.Init()

    return _M.InputCache[key] and _M.MotionCancel(cancelIndex)
end

---@param commandWork ace.btable.cCommandWork
---@param command string
---@param state boolean
---@param cancelIndex app.ACTION_CANCEL_INDEX
function _M.CommandStateInputCancelMotion(commandWork, command, state, cancelIndex)
    _M.Init()

    local canCancel = _M.IsValidMotionInputCache(cancelIndex)

    if canCancel then
        _M.InputCache[command] = state
    else
        _M.InputCache[command] = false
    end

    return _M._CacheInputCanProcess(commandWork, command, cancelIndex)
end

---@param commandWork ace.btable.cCommandWork
---@param key string
---@param cancelIndex app.ACTION_CANCEL_INDEX
function _M.RawInputCancelMotion(commandWork, key, cancelIndex)
    _M.Init()

    local canCancel = _M.IsValidMotionInputCache(cancelIndex)

    if canCancel then
        _M.InputCache[key] = KeyOn(key)
    else
        _M.InputCache[key] = false
    end

    return _M._CacheInputCanProcess(commandWork, key, cancelIndex)
end

---@param commandWork ace.btable.cCommandWork
---@param commandName string
---@param cancelIndex app.ACTION_CANCEL_INDEX
function _M.InputCancelMotion(commandWork, commandName, cancelIndex)
    _M.Init()

    local canCancel = _M.IsValidMotionInputCache(cancelIndex)

    if canCancel then
        -- todo fixme?
        -- log.info(string.format("Input was: %s", tostring(_M.InputCache[commandName])))
        _M.InputCache[commandName] = CheckCommandResult:call(nil, commandWork, 2, PlayerCommand_Type_ToValue[commandName])
        -- log.info(string.format("Input become: %s", tostring(_M.InputCache[commandName])))
    else
        -- log.info("Not in cancel")
        _M.InputCache[commandName] = false
    end

    return _M._CacheInputCanProcess(commandWork, commandName, cancelIndex)
end

-- function _M._CacheInputActionPhase(commandWork, commandName, ignorePhase)
--     if ignorePhase and not _M.InputCache[commandName] then
--         local NotWatchCancel = CheckCommandResult:call(nil, commandWork, 2, PlayerCommand_Type_ToValue[commandName])
--         _M.InputCache[commandName] = NotWatchCancel
--         return
--     end

--     _M.Init()
--     local phase = _M.MotionSupporter._BaseBasicMotionSeq._ActionPhase

--     -- log.info(string.format("%x", _M.MotionSupporter._BaseBasicMotionSeq:get_address()))

--     if phase == 0 then
--         _M.InputCache[commandName] = false
--     elseif phase == 1 or phase == 2 then
--         if _M.InputCache[commandName] then
--             return
--         end
    
--         local NotWatchCancel = CheckCommandResult:call(nil, commandWork, 2, PlayerCommand_Type_ToValue[commandName])
--         _M.InputCache[commandName] = NotWatchCancel
--     end
-- end

-- function _M._CacheInputActionPhaseCanProcess(commandWork, commandName, ignorePhase)
--     if ignorePhase then
--         if _M.InputCache[commandName] then
--             _M.InputCache[commandName] = nil
--             return true
--         end
--         return false
--     end

--     _M.Init()
--     local phase = _M.MotionSupporter._BaseBasicMotionSeq._ActionPhase

--     if phase == 2 and _M.InputCache[commandName] then
--         -- _M.InputCache[commandName] = nil
--         return _M.InputCache[commandName]
--     end
--     return false
-- end

-- function _M.InputActionPhase(commandWork, commandName, cancelIndex)
--     _M._CacheInputActionPhase(commandWork, commandName, cancelIndex)
--     return _M._CacheInputActionPhaseCanProcess(commandWork, commandName, cancelIndex)
-- end

return _M