lib/LibCraftingProfessions-1.0/LibCraftingProfessions-1.0.lua
--[[
    Name: LibCraftingProfessions-1.0
    Developed by: Refaim (rkharito@yandex.ru, https://github.com/refaim/)
    Website: https://github.com/refaim/LibCraftingProfessions-1.0
    Description: A library designed to provide a universal interface for crafting professions.
    Dependencies: LibStub
    Compatibility: World of Warcraft Vanilla (1.12.1), Turtle (1.18.0)
]]

---@type LibStubDef
local LibStub = getglobal("LibStub")
assert(LibStub ~= nil)

local untyped_lib, _ = LibStub:NewLibrary("LibCraftingProfessions-1.0", 18)
if not untyped_lib then
    return
end

---@class LibCraftingProfessions
---@field event_to_handlers table<string, function[]>
---@field event_frame Frame

local lib = --[[---@type LibCraftingProfessions]] untyped_lib
if lib.event_to_handlers == nil then
    lib.event_to_handlers = {}
end

local IS_TURTLE_WOW = getglobal("TURTLE_WOW_VERSION") ~= nil

---@shape LcpProfessionProps
---@field icon string

---@type table<string, LcpProfessionProps>
local P = {}
P["Alchemy"] = {icon = [[Interface\Icons\trade_alchemy]]}
P["Blacksmithing"] = {icon = [[Interface\Icons\trade_blacksmithing]]}
P["Cooking"] = {icon = [[Interface\Icons\inv_misc_food_15]]}
P["Enchanting"] = {icon = [[Interface\Icons\trade_engraving]]}
P["Engineering"] = {icon = [[Interface\Icons\trade_engineering]]}
P["First Aid"] = {icon = [[Interface\Icons\spell_holy_sealofsacrifice]]}
P["Leatherworking"] = {icon = [[Interface\Icons\inv_misc_armorkit_17]]}
P["Mining"] = {icon = [[Interface\Icons\spell_fire_flameblades]]}
P["Poisons"] = {icon = [[Interface\Icons\trade_brewpoison]]}
P["Tailoring"] = {icon = [[Interface\Icons\trade_tailoring]]}
if IS_TURTLE_WOW then
    P["Jewelcrafting"] = {icon = [[Interface\Icons\inv_helmet_44]]}
end
local PROFESSION_TO_PROPS = P
P = {}

---@type table<string, string>
local ENGLISH_TO_LOCALIZED = {}
---@type table<string, string>
local LOCALIZED_TO_ENGLISH = {}

---@shape LcpProfession
---@field english_name string
---@field localized_name string
---@field icon_texture_path string

---@shape LcpKnownProfession: LcpProfession
---@field cur_rank number

---@alias LcpProfessionFrameType "VanillaCraftFrame" | "VanillaTradeSkillFrame" | "TurtleTradeSkillFrame" | "Artisan" | "AdvancedTradeSkillWindow" | "AdvancedTradeSkillWindow2"

---@type {FrameType: table<LcpProfessionFrameType, LcpProfessionFrameType>}
LibCraftingProfessionsConstants = {
    FrameType = {
        VanillaCraftFrame = "VanillaCraftFrame",
        VanillaTradeSkillFrame = "VanillaTradeSkillFrame",
        TurtleTradeSkillFrame = "TurtleTradeSkillFrame",
        Artisan = "Artisan",
        AdvancedTradeSkillWindow = "AdvancedTradeSkillWindow",
        AdvancedTradeSkillWindow2 = "AdvancedTradeSkillWindow2",
    }
}
local FrameType = LibCraftingProfessionsConstants.FrameType

---@shape LcpProfessionFrame
---@field frame Frame
---@field type LcpProfessionFrameType

---@alias LcpSkillDifficulty "trivial" | "easy" | "medium" | "optimal" | "difficult"

---@shape LcpKnownSkill
---@field localized_name string
---@field item_link string
---@field difficulty LcpSkillDifficulty
---@field num_available number

---@alias LcpScanSkillsHandler fun(profession: LcpKnownProfession, skills: LcpKnownSkill[]):void

---@type table<string, LcpKnownSkill[]>
local skills_by_english_profession_name = {}

---@return boolean
local function ready(value)
    if value == nil then
        return false
    end
    if type(value) == "table" then
        return next(value) ~= nil
    end
    return value ~= "" and value ~= 0
end

---
--- Retrieves a list of all supported crafting professions in the game.
---
---@return LcpProfession[]
function lib:GetSupportedProfessions()
    ---@type LcpProfession[]
    local professions = {}
    for english_name, props in pairs(PROFESSION_TO_PROPS) do
        tinsert(professions, {
            english_name = english_name,
            localized_name = ENGLISH_TO_LOCALIZED[english_name],
            icon_texture_path = props.icon,
        })
    end
    return professions
end

---
--- Retrieves a list of the player's known crafting professions.
--- Returns a table of known professions, or nil if game data is not yet available.
--- Use RegisterEvent("LCP_SKILLS_UPDATE", ...) to ensure data availability.
---
---@return LcpKnownProfession[]|nil
function lib:GetPlayerProfessions()
    local num_of_professions = GetNumSkillLines()
    if not ready(num_of_professions) then
        return nil
    end

    ---@type LcpKnownProfession[]
    local professions = {}
    for i = 1, num_of_professions do
        local localized_name, is_header, _, rank, _, _, _, _, _, _, _, _, _ = GetSkillLineInfo(i)
        if not is_header then
            if not ready(localized_name) then
                return nil
            end

            local english_name = LOCALIZED_TO_ENGLISH[localized_name]
            local crafting_props = PROFESSION_TO_PROPS[english_name]
            if crafting_props ~= nil then
                if not ready(rank) then
                    return nil
                end
                tinsert(professions, {
                    english_name = english_name,
                    localized_name = localized_name,
                    icon_texture_path = crafting_props.icon,
                    cur_rank = rank,
                })
            end
        end
    end

    return professions
end

---
--- Retrieves a list of the player's skills for a specific profession.
--- Returns a table of known skills for the specified profession, or nil if game data is not yet available
--- Use RegisterEvent("LCP_SKILLS_UPDATE", ...) to ensure data availability.
---
--- @param profession_name string
--- @return LcpKnownSkill[]|nil
function lib:GetPlayerProfessionSkills(profession_name)
    return skills_by_english_profession_name[profession_name] or skills_by_english_profession_name[LOCALIZED_TO_ENGLISH[profession_name]]
end

---
--- Registers a handler for the specific library event.
--- LCP_SKILLS_UPDATE is fired when skill data is updated and ready for retrieval.
--- LCP_FRAME_SHOW is fired when a crafting profession frame is shown.
--- LCP_FRAME_CLOSE is fired when a crafting profession frame is closed.
---@param event string
---@param handler function
---@overload fun(event: "LCP_SKILLS_UPDATE", handler: fun(profession: LcpKnownProfession, skills: LcpKnownSkill[]):void)
---@overload fun(event: "LCP_FRAME_SHOW", handler: fun(profession: LcpKnownProfession, frame: Frame, frame_type: LcpProfessionFrameType):void)
---@overload fun(event: "LCP_FRAME_CLOSE", handler: fun(frame: Frame, frame_type: LcpProfessionFrameType):void)
function lib:RegisterEvent(event, handler)
    if lib.event_to_handlers[event] == nil then
        lib.event_to_handlers[event] = {}
    end
    tinsert(lib.event_to_handlers[event], --[[---@type function]] handler)
end

---@shape _LcpFilterOption
---@field name string
---@field type string

---@shape _LcpProfessionAdapter
---@field GetProfessionInfo fun():string|nil, string|nil, number|nil, number|nil
---@field GetNumSkills fun():number|nil
---@field GetSkillInfo fun(index:number):string|nil, string|nil, number|nil, wowboolean
---@field GetSkillItemLink fun(index:number):string|nil
---@field ExpandHeader fun(index:number):void
---@field CollapseHeader fun(index:number):void
---@field DisableFilters fun():_LcpFilterOption[]
---@field EnableFilters fun(options:_LcpFilterOption[]):void

---@param adapter _LcpProfessionAdapter
---@return LcpKnownSkill[]|nil
local function scan_skills(adapter)
    local localized_profession, english_profession, cur_rank, max_rank = adapter.GetProfessionInfo()
    if not ready(localized_profession) or not ready(english_profession) or not ready(cur_rank) or not ready(max_rank) then
        return nil
    end

    local disabled_filters = adapter.DisableFilters()

    local num_of_skills_before_expansion = adapter.GetNumSkills()
    if not ready(num_of_skills_before_expansion) then
        return nil
    end

    ---@type table<string, true>
    local set_of_headers_to_collapse = {}
    for i = 1, num_of_skills_before_expansion do
        local skill_name, skill_type_or_difficulty, _, is_expanded = adapter.GetSkillInfo(i)
        if not ready(skill_name) or not ready(skill_type_or_difficulty) then
            return nil
        end
        if skill_type_or_difficulty == "header" and is_expanded == nil then
            set_of_headers_to_collapse[ --[[---@not nil]] skill_name] = true
        end
    end
    adapter.ExpandHeader(0)

    local num_of_skills_after_expansion = adapter.GetNumSkills()
    if not ready(num_of_skills_after_expansion) then
        return nil
    end

    ---@type LcpKnownSkill[]
    local skills = {}
    for i = 1, num_of_skills_after_expansion do
        local skill_name, skill_type_or_difficulty, num_available, _ = adapter.GetSkillInfo(i)
        if not ready(skill_name) or not ready(skill_type_or_difficulty) then
            return nil
        end
        if skill_type_or_difficulty ~= "header" then
            local item_link = adapter.GetSkillItemLink(i)
            if num_available == nil or not ready(item_link) then
                return nil
            end

            ---@type LcpKnownSkill
            local skill = {
                localized_name = --[[---@not nil]] skill_name,
                item_link = --[[---@not nil]] item_link,
                difficulty = --[[---@type LcpSkillDifficulty]] skill_type_or_difficulty,
                num_available = --[[---@not nil]] num_available,
            }
            tinsert(skills, skill)
        end
    end

    for i = adapter.GetNumSkills(), 1, -1 do
        local skill_name, skill_type_or_difficulty, _, _ = adapter.GetSkillInfo(i)
        if skill_type_or_difficulty == "header" and set_of_headers_to_collapse[ --[[---@not nil]] skill_name] ~= nil then
            adapter.CollapseHeader(i)
        end
    end

    adapter.EnableFilters(disabled_filters)

    return skills
end

---@param known_professions LcpKnownProfession[]|nil
local function forget_obsolete_professions(known_professions)
    if known_professions == nil then
        return
    end

    ---@type table<string, boolean>
    local known_profession_names_set = {}
    for _, profession in ipairs(--[[---@not nil]] known_professions) do
        known_profession_names_set[profession.english_name] = true
    end

    for english_name, _ in pairs(skills_by_english_profession_name) do
        if known_profession_names_set[english_name] == nil then
            skills_by_english_profession_name[english_name] = nil
        end
    end
end

---@param known_professions LcpKnownProfession[]|nil
---@param name string
---@return LcpKnownProfession|nil
local function find_known_profession(known_professions, name)
    local profession
    for _, candidate in ipairs(known_professions or {}) do
        if candidate.localized_name == name or candidate.english_name == name then
            profession = candidate
            break
        end
    end
    return profession
end

---@param english_name string
---@param skills LcpKnownSkill[]
local function save_skills(english_name, skills)
    skills_by_english_profession_name[english_name] = skills

    local known_professions = lib:GetPlayerProfessions()
    forget_obsolete_professions(known_professions)

    local profession = find_known_profession(known_professions, english_name)
    if profession == nil then
        return
    end

    for _, handler in ipairs(lib.event_to_handlers["LCP_SKILLS_UPDATE"] or {}) do
        handler(profession, skills)
    end
end

---@shape LcpThirdPartyProfessionFrameAddon
---@field name string
---@field frame_name string
---@field opens_and_closes_vanilla_profession_frames boolean

---@type LcpThirdPartyProfessionFrameAddon
local artisan = {name = "Artisan", frame_name = "ArtisanFrame", opens_and_closes_vanilla_profession_frames = true}
---@type LcpThirdPartyProfessionFrameAddon
local atsw = {name = "AdvancedTradeSkillWindow", frame_name = "ATSWFrame", opens_and_closes_vanilla_profession_frames = false}
---@type LcpThirdPartyProfessionFrameAddon
local atsw2 = {name = "AdvancedTradeSkillWindow2", frame_name = "ATSWFrame", opens_and_closes_vanilla_profession_frames = true}

local SUPPORTED_THIRD_PARTY_ADDONS = {artisan, atsw, atsw2}

---@param addon LcpThirdPartyProfessionFrameAddon
local function is_third_party_loaded(addon)
    return IsAddOnLoaded(addon.name) == 1 and getglobal(addon.frame_name) ~= nil
end

---@return LcpProfessionFrame
local function detect_profession_frame(professionFrame)
    ---@type Frame
    local atsw1_or_atsw2_frame = getglobal(atsw2.frame_name)
    if atsw1_or_atsw2_frame ~= nil then
        ---@type LcpProfessionFrameType
        local frame_type = FrameType.AdvancedTradeSkillWindow
        if is_third_party_loaded(atsw2) then
            frame_type = FrameType.AdvancedTradeSkillWindow2
        end
        return {frame = atsw1_or_atsw2_frame, type = frame_type}
    end

    if is_third_party_loaded(artisan) then
        return {frame = getglobal(artisan.frame_name), type = FrameType.Artisan}
    end

    if professionFrame == TradeSkillFrame then
        local frame_type = IS_TURTLE_WOW and FrameType.TurtleTradeSkillFrame or FrameType.VanillaTradeSkillFrame
        return {frame = professionFrame, type = frame_type}
    end

    return {frame = professionFrame, type = FrameType.VanillaCraftFrame}
end

---@param english_name string
---@param localized_name string
---@param cur_rank number
---@param frame Frame
local function send_frame_show_event(english_name, localized_name, cur_rank, frame)
    local profession = {
        english_name = english_name,
        localized_name = localized_name,
        cur_rank = cur_rank,
    }
    local real_frame = detect_profession_frame(frame)
    for _, handler in ipairs(lib.event_to_handlers["LCP_FRAME_SHOW"] or {}) do
        handler(profession, real_frame.frame, real_frame.type)
    end
end

---@param frame Frame
local function send_frame_close_event(frame)
    local real_frame = detect_profession_frame(frame)
    for _, handler in ipairs(lib.event_to_handlers["LCP_FRAME_CLOSE"] or {}) do
        handler(real_frame.frame, real_frame.type)
    end
end

---@return CraftFrame
local function get_craft_frame()
    if CraftFrame == nil then
        LoadAddOn("Blizzard_CraftUI")
    end
    return CraftFrame
end

---@type _LcpProfessionAdapter
local craft_adapter = {
    GetProfessionInfo = function()
        local name, cur_rank, max_rank = GetCraftDisplaySkillLine()
        if name == nil then -- GetCraftDisplaySkillLine() returns nil for the "Beast Training" frame
            name = GetCraftName()
        end
        return name, LOCALIZED_TO_ENGLISH[--[[---@type string]] name], cur_rank, max_rank
    end,
    GetNumSkills = GetNumCrafts,
    GetSkillInfo = function(i)
        local name, _, type, num_available, is_expanded = GetCraftInfo(i)
        return name, type, num_available, is_expanded
    end,
    GetSkillItemLink = GetCraftItemLink,
    ExpandHeader = ExpandCraftSkillLine,
    CollapseHeader = CollapseCraftSkillLine,
    DisableFilters = function() return {} end,
    EnableFilters = function(options) end,
}

local function try_send_craft_frame_show_event()
    local localized_name, english_name, cur_rank, _ = craft_adapter.GetProfessionInfo()
    if not ready(localized_name) or not ready(english_name) or not ready(cur_rank) then
        return
    end
    send_frame_show_event(--[[---@not nil]] english_name, --[[---@not nil]] localized_name, --[[---@not nil]] cur_rank, get_craft_frame())
end

local craft_scan_in_progress = false

local function scan_craft_frame()
    craft_scan_in_progress = true

    local localized_name, english_name, _, _ = craft_adapter.GetProfessionInfo()
    if not ready(localized_name) or not ready(english_name) then
        craft_scan_in_progress = false
        return
    end

    local skills = scan_skills(craft_adapter)
    if not ready(skills) then
        craft_scan_in_progress = false
        return
    end

    save_skills(--[[---@not nil]] english_name, --[[---@not nil]] skills)
    craft_scan_in_progress = false
end

---@shape _LcpSkillFilterAdapter
---@field GetType fun(): "inv_slot" | "subclass"
---@field GetOptions fun(): string...
---@field GetOptionState fun(index:number): wowboolean
---@field SetOptionState fun(index:number, enable:wowboolean, exclusive:wowboolean): void

---@param adapter _LcpSkillFilterAdapter
---@return _LcpFilterOption[]
local function disable_trade_skill_filters(adapter)
    if adapter.GetOptionState(0) == 1 then
        return {}
    end

    ---@type _LcpFilterOption[]
    local selected_options = {}
    for i, option in ipairs({adapter.GetOptions()}) do
        if adapter.GetOptionState(i) == 1 then
            tinsert(selected_options, {name = option, type = adapter.GetType()})
        end
    end

    adapter.SetOptionState(0, 1, 1)

    return selected_options
end

---@param adapter _LcpSkillFilterAdapter
---@param options _LcpFilterOption[]
local function enable_trade_skill_filters(adapter, options)
    ---@type table<string, boolean>
    local options_set = {}
    for _, option in ipairs(options) do
        if option.type == adapter.GetType() then
            options_set[option.name] = true
        end
    end

    for i, option in ipairs({adapter.GetOptions()}) do
        if options_set[option] then
            adapter.SetOptionState(i, 1, 1)
        end
    end
end

local function get_trade_skill_frame()
    if TradeSkillFrame == nil then
        LoadAddOn("Blizzard_TradeSkillUI")
    end
    return TradeSkillFrame
end

---@type _LcpSkillFilterAdapter
local inv_slot_filter_adapter = {
    GetType = function() return "inv_slot" end,
    GetOptions = GetTradeSkillInvSlots,
    GetOptionState = GetTradeSkillInvSlotFilter,
    SetOptionState = SetTradeSkillInvSlotFilter,
}

---@type _LcpSkillFilterAdapter
local subclass_filter_adapter = {
    GetType = function() return "subclass" end,
    GetOptions = GetTradeSkillSubClasses,
    GetOptionState = GetTradeSkillSubClassFilter,
    SetOptionState = SetTradeSkillSubClassFilter,
}

---@type _LcpProfessionAdapter
local trade_skill_adapter = {
    GetProfessionInfo = function()
        local name, cur_rank, max_rank = GetTradeSkillLine()
        return name, LOCALIZED_TO_ENGLISH[--[[---@type string]] name], cur_rank, max_rank
    end,
    GetNumSkills = GetNumTradeSkills,
    GetSkillInfo = GetTradeSkillInfo,
    GetSkillItemLink = GetTradeSkillItemLink,
    ExpandHeader = ExpandTradeSkillSubClass,
    CollapseHeader = CollapseTradeSkillSubClass,
    DisableFilters = function()
        local result = {}
        for _, adapter in ipairs({inv_slot_filter_adapter, subclass_filter_adapter}) do
            for _, option in ipairs(disable_trade_skill_filters(adapter)) do
                tinsert(result, option)
            end
        end
        return result
    end,
    EnableFilters = function(options)
        for _, adapter in ipairs({inv_slot_filter_adapter, subclass_filter_adapter}) do
            enable_trade_skill_filters(adapter, options)
        end
    end,
}

local function try_send_trade_skill_frame_show_event()
    local localized_name, english_name, cur_rank, _ = trade_skill_adapter.GetProfessionInfo()
    if not ready(localized_name) or not ready(english_name) or not ready(cur_rank) then
        return false
    end
    send_frame_show_event(--[[---@not nil]] english_name, --[[---@not nil]] localized_name, --[[---@not nil]] cur_rank, get_trade_skill_frame())
end

local trade_skill_scan_in_progress = false

---@return boolean
local function scan_trade_skill_frame()
    trade_skill_scan_in_progress = true

    local localized_name, english_name, _, _ = trade_skill_adapter.GetProfessionInfo()
    if not ready(localized_name) or not ready(english_name) then
        trade_skill_scan_in_progress = false
        return false
    end

    local skills = scan_skills(trade_skill_adapter)
    if not ready(skills) then
        trade_skill_scan_in_progress = false
        return false
    end

    save_skills(--[[---@not nil]] english_name, --[[---@not nil]] skills)

    trade_skill_scan_in_progress = false
    return true
end

local L = ENGLISH_TO_LOCALIZED

local game_locale = GetLocale()
if game_locale == "enUS" or game_locale == "enGB" then
    L["Alchemy"] = "Alchemy"
    L["Blacksmithing"] = "Blacksmithing"
    L["Cooking"] = "Cooking"
    L["Enchanting"] = "Enchanting"
    L["Engineering"] = "Engineering"
    L["First Aid"] = "First Aid"
    L["Leatherworking"] = "Leatherworking"
    L["Mining"] = "Mining"
    L["Poisons"] = "Poisons"
    L["Tailoring"] = "Tailoring"
    if IS_TURTLE_WOW then
        L["Jewelcrafting"] = "Jewelcrafting"
    end
elseif game_locale == "deDE" then
    L["Alchemy"] = "Alchimie"
    L["Blacksmithing"] = "Schmiedekunst"
    L["Cooking"] = "Kochkunst"
    L["Enchanting"] = "Verzauberkunst"
    L["Engineering"] = "Ingenieurskunst"
    L["First Aid"] = "Erste Hilfe"
    L["Leatherworking"] = "Lederverarbeitung"
    L["Mining"] = "Bergbau"
    L["Poisons"] = "Gifte"
    L["Tailoring"] = "Schneiderei"
    if IS_TURTLE_WOW then
        L["Jewelcrafting"] = "Juwelenschleifen"
    end
elseif game_locale == "esES" then
    L["Alchemy"] = "Alquimia"
    L["Blacksmithing"] = "Herrería"
    L["Cooking"] = "Cocina"
    L["Enchanting"] = "Encantamiento"
    L["Engineering"] = "Ingeniería"
    L["First Aid"] = "Primeros auxilios"
    L["Leatherworking"] = "Peletería"
    L["Mining"] = "Minería"
    L["Poisons"] = "Venenos"
    L["Tailoring"] = "Sastrería"
    if IS_TURTLE_WOW then
        L["Blacksmithing"] = "Ferraria"
        L["Cooking"] = "Culinária"
        L["Enchanting"] = "Encantamento"
        L["Engineering"] = "Engenharia"
        L["First Aid"] = "Primeiros Socorros"
        L["Jewelcrafting"] = "Joalheria"
        L["Leatherworking"] = "Couraria"
        L["Mining"] = "Mineração"
        L["Tailoring"] = "Alfaiataria"
    end
elseif game_locale == "esMX" then
    L["Alchemy"] = "Alquimia"
    L["Blacksmithing"] = "Herrería"
    L["Cooking"] = "Cocina"
    L["Enchanting"] = "Encantamiento"
    L["Engineering"] = "Ingeniería"
    L["First Aid"] = "Primeros auxilios"
    L["Leatherworking"] = "Peletería"
    L["Mining"] = "Minería"
    L["Poisons"] = "Venenos"
    L["Tailoring"] = "Sastrería"
    if IS_TURTLE_WOW then
        L["Jewelcrafting"] = "Joyería"
    end
elseif game_locale == "frFR" then
    L["Alchemy"] = "Alchimie"
    L["Blacksmithing"] = "Forge"
    L["Cooking"] = "Cuisine"
    L["Enchanting"] = "Enchantement"
    L["Engineering"] = "Ingénieur"
    L["First Aid"] = "Premiers soins"
    L["Leatherworking"] = "Travail du cuir"
    L["Mining"] = "Minage"
    L["Poisons"] = "Poisons"
    L["Tailoring"] = "Couture"
    if IS_TURTLE_WOW then
        L["Jewelcrafting"] = "Joaillerie"
    end
elseif game_locale == "koKR" then
    L["Alchemy"] = "연금술"
    L["Blacksmithing"] = "대장기술"
    L["Cooking"] = "요리"
    L["Enchanting"] = "마법부여"
    L["Engineering"] = "기계공학"
    L["First Aid"] = "응급치료"
    L["Leatherworking"] = "가죽세공"
    L["Mining"] = "채광"
    L["Poisons"] = "독 조제"
    L["Tailoring"] = "재봉술"
    if IS_TURTLE_WOW then
        L["Jewelcrafting"] = "보석세공"
    end
elseif game_locale == "ptBR" then
    L["Alchemy"] = "Alquimia"
    L["Blacksmithing"] = "Ferraria"
    L["Cooking"] = "Culinária"
    L["Enchanting"] = "Encantamento"
    L["Engineering"] = "Engenharia"
    L["First Aid"] = "Primeiros Socorros"
    L["Leatherworking"] = "Couraria"
    L["Mining"] = "Mineração"
    L["Poisons"] = "Venenos"
    L["Tailoring"] = "Alfaiataria"
    if IS_TURTLE_WOW then
        L["Jewelcrafting"] = "Joalheria"
    end
elseif game_locale == "ruRU" then
    L["Alchemy"] = "Алхимия"
    L["Blacksmithing"] = "Кузнечное дело"
    L["Cooking"] = "Кулинария"
    L["Enchanting"] = "Наложение чар"
    L["Engineering"] = "Инженерное дело"
    L["First Aid"] = "Первая помощь"
    L["Leatherworking"] = "Кожевничество"
    L["Mining"] = "Горное дело"
    L["Poisons"] = "Яды"
    L["Tailoring"] = "Портняжное дело"
    if IS_TURTLE_WOW then
        L["Jewelcrafting"] = "Ювелирное дело"
    end
elseif game_locale == "zhCN" then
    L["Alchemy"] = "炼金术"
    L["Blacksmithing"] = "锻造"
    L["Cooking"] = "烹饪"
    L["Enchanting"] = "附魔"
    L["Engineering"] = "工程学"
    L["First Aid"] = "急救"
    L["Leatherworking"] = "制皮"
    L["Mining"] = "采矿"
    L["Poisons"] = "毒药"
    L["Tailoring"] = "裁缝"
    if IS_TURTLE_WOW then
        L["Jewelcrafting"] = "珠宝加工"
    end
elseif game_locale == "zhTW" then
    L["Alchemy"] = "煉金術"
    L["Blacksmithing"] = "鍛造"
    L["Cooking"] = "烹飪"
    L["Enchanting"] = "附魔"
    L["Engineering"] = "工程學"
    L["First Aid"] = "急救"
    L["Leatherworking"] = "製皮"
    L["Mining"] = "採礦"
    L["Poisons"] = "毒藥"
    L["Tailoring"] = "裁縫"
    if IS_TURTLE_WOW then
        L["Jewelcrafting"] = "珠寶設計"
    end
end

for english_name, localized_name in pairs(ENGLISH_TO_LOCALIZED) do
    LOCALIZED_TO_ENGLISH[localized_name] = english_name
end

---@param frame Frame
---@return table
local function as_table(frame)
    return --[[---@type table]] frame
end

local was_third_party_on_hide_hooked = false

---@param addon LcpThirdPartyProfessionFrameAddon
local function hook_third_party_on_hide(addon)
    if not is_third_party_loaded(addon) then
        return
    end

    ---@type Frame
    local frame = getglobal(addon.frame_name)
    if frame == nil then
        return
    end
    if as_table(frame).is_hooked_by_lcp == true then
        was_third_party_on_hide_hooked = true
        return
    end

    local on_hide = frame:GetScript("OnHide")
    frame:SetScript("OnHide", function()
        if on_hide ~= nil then
            (--[[---@not nil]] on_hide)()
        end
        send_frame_close_event(frame)
    end)

    as_table(frame).is_hooked_by_lcp = true
    was_third_party_on_hide_hooked = true
end

if lib.event_frame ~= nil then
    lib.event_frame:UnregisterAllEvents()
    lib.event_frame:SetScript("OnUpdate", nil)
    lib.event_frame:SetScript("OnEvent", nil)
end

local render_pseudo_time = 0
local craft_frame_successfully_scanned_at = 0
local trade_skill_frame_successfully_scanned_at = 0

lib.event_frame = CreateFrame("Frame")

lib.event_frame:SetScript("OnUpdate", function ()
    -- Third-party addons can trigger multiple UPDATE events
    -- so we count rendered frames to not scan skills more than once per frame
    render_pseudo_time = render_pseudo_time + 1
    if render_pseudo_time >= 100 then
        render_pseudo_time = 1
    end
end)

lib.event_frame:SetScript("OnEvent", function()
    local do_craft_scan = false
    local do_trade_skill_scan = false

    if event == "ADDON_LOADED" then
        for _, addon in ipairs(SUPPORTED_THIRD_PARTY_ADDONS) do
            if addon.name == arg1 and addon.opens_and_closes_vanilla_profession_frames then
                hook_third_party_on_hide(addon)
            end
        end
    elseif event == "CRAFT_SHOW" then
        try_send_craft_frame_show_event()
        do_craft_scan = true
    elseif event == "CRAFT_UPDATE" then
        if not craft_scan_in_progress then
            -- Learned new skill AND craft frame is opened
            do_craft_scan = true
        end
    elseif event == "CRAFT_CLOSE" then
        if not was_third_party_on_hide_hooked then
            send_frame_close_event(get_craft_frame())
        end
    elseif event == "TRADE_SKILL_SHOW" then
        try_send_trade_skill_frame_show_event()
        do_trade_skill_scan = true
    elseif event == "TRADE_SKILL_UPDATE" then
        if not trade_skill_scan_in_progress then
            -- Learned new skill AND trade skill frame is opened
            do_trade_skill_scan = true
        end
    elseif event == "TRADE_SKILL_CLOSE" then
        if not was_third_party_on_hide_hooked then
            send_frame_close_event(get_trade_skill_frame())
        end
    elseif event == "CHARACTER_POINTS_CHANGED" then
        -- Learned or unlearned profession/specialization
        forget_obsolete_professions(lib:GetPlayerProfessions())
    elseif event == "SKILL_LINES_CHANGED" then
        -- Raised the profession skill level OR learned/unlearned profession/specialization
        do_craft_scan = true
        do_trade_skill_scan = true
    end

    if do_craft_scan and craft_frame_successfully_scanned_at ~= render_pseudo_time then
        if scan_craft_frame() then
            craft_frame_successfully_scanned_at = render_pseudo_time
        end
    end

    if do_trade_skill_scan and trade_skill_frame_successfully_scanned_at ~= render_pseudo_time then
        if scan_trade_skill_frame() then
            trade_skill_frame_successfully_scanned_at = render_pseudo_time
        end
    end
end)
lib.event_frame:RegisterEvent("ADDON_LOADED")
lib.event_frame:RegisterEvent("CRAFT_SHOW")
lib.event_frame:RegisterEvent("CRAFT_UPDATE")
lib.event_frame:RegisterEvent("CRAFT_CLOSE")
lib.event_frame:RegisterEvent("TRADE_SKILL_SHOW")
lib.event_frame:RegisterEvent("TRADE_SKILL_UPDATE")
lib.event_frame:RegisterEvent("TRADE_SKILL_CLOSE")
lib.event_frame:RegisterEvent("CHARACTER_POINTS_CHANGED")
lib.event_frame:RegisterEvent("SKILL_LINES_CHANGED")

for _, addon in ipairs(SUPPORTED_THIRD_PARTY_ADDONS) do
    if is_third_party_loaded(addon) and addon.opens_and_closes_vanilla_profession_frames then
        hook_third_party_on_hide(addon)
    end
end
