---@diagnostic disable: missing-parameter
---@diagnostic disable: param-type-mismatch

-- Advanced NPC System by Jiddo

if KeywordHandler == nil then
    KeywordNode = {
        keywords = nil,
        callback = nil,
        parameters = nil,
        children = nil,
        parent = nil,
        condition = nil,
        action = nil,
    }

    -- Created a new keywordnode with the given keywords, callback function and parameters and without any childNodes.
    function KeywordNode:new(keys, func, param, condition, action)
        local obj = {}
        obj.keywords = keys
        obj.callback = func
        obj.parameters = param
        obj.children = {}
        obj.condition = condition
        obj.action = action
        setmetatable(obj, self)
        self.__index = self
        return obj
    end

    -- Calls the underlying callback function if it is not nil.
    function KeywordNode:processMessage(npc, player, message)
        return (self.callback == nil or self.callback(npc, player, message, self.keywords, self.parameters, self))
    end

    function KeywordNode:processAction(player, message)
        if not self.action then
            return
        end

        self.action(player, self.parameters.npcHandler, message)
    end

    -- Returns true if message contains all patterns/strings found in keywords.
    function KeywordNode:checkMessage(player, message)
        if self.keywords.callback ~= nil then
            local ret, data = self.keywords.callback(self.keywords, message)
            if not ret then
                return false
            end

            if self.condition and not self.condition(Player(player), data) then
                return false
            end
            return true
        end

        local data = {}
        local last = 0
        for _, keyword in ipairs(self.keywords) do
            if type(keyword) == "string" then
                local a, b = string.find(message, keyword)
                if a == nil or b == nil or a < last then
                    return false
                end
                if keyword:sub(1, 1) == "%" then
                    data[#data + 1] = tonumber(message:sub(a, b)) or nil
                end
                last = a
            end
        end

        if self.condition and not self.condition(Player(player), data) then
            return false
        end
        return true
    end

    -- Returns the parent of this node or nil if no such node exists.
    function KeywordNode:getParent()
        return self.parent
    end

    -- Returns an array of the callback function parameters assosiated with this node.
    function KeywordNode:getParameters()
        return self.parameters
    end

    -- Returns an array of the triggering keywords assosiated with this node.
    function KeywordNode:getKeywords()
        return self.keywords
    end

    --[[
        Adds a childNode to this node. Creates the childNode based on the parameters:
        (k = keywords, c = callback, p = parameters)
    ]]
    function KeywordNode:addChildKeyword(keywords, callback, parameters, condition, action)
        local new = KeywordNode:new(keywords, callback, parameters, condition, action)
        return self:addChildKeywordNode(new)
    end

    function KeywordNode:addAliasKeyword(keywords)
        if #self.children == 0 then
            logger.error("[KeywordNode:addAliasKeyword] - No previous node found")
            return false
        end

        local prevNode = self.children[#self.children]
        local new = KeywordNode:new(keywords, prevNode.callback, prevNode.parameters, prevNode.condition, prevNode.action)
        for i = 1, #prevNode.children do
            new:addChildKeywordNode(prevNode.children[i])
        end
        return self:addChildKeywordNode(new)
    end

    -- Adds a pre-created childNode to this node. Should be used for example if several nodes should have a common child.
    function KeywordNode:addChildKeywordNode(childNode)
        self.children[#self.children + 1] = childNode
        childNode.parent = self
        return childNode
    end

    KeywordHandler = {
        root = nil,
        lastNode = nil,
    }

    -- Creates a new keywordhandler with an empty rootnode.
    function KeywordHandler:new()
        local obj = {}
        obj.root = KeywordNode:new(nil, nil, nil)
        obj.lastNode = {}
        setmetatable(obj, self)
        self.__index = self
        return obj
    end

    -- Resets the lastNode field, and this resetting the current position in the node hierarchy to root.
    function KeywordHandler:reset(player)
        local playerId = player:getId()
        if self.lastNode[playerId] then
            self.lastNode[playerId] = nil
        end
    end

    -- Makes sure the correct childNode of lastNode gets a chance to process the message.
    function KeywordHandler:processMessage(npc, player, message)
        local node = self:getLastNode(player)
        if node == nil then
            error("No root node found.")
            return false
        end

        local ret = self:processNodeMessage(node, npc, player, message)
        if ret then
            return true
        end

        if node:getParent() then
            node = node:getParent() -- Search through the parent.
            ret = self:processNodeMessage(node, npc, player, message)
            if ret then
                return true
            end
        end

        if node ~= self:getRoot() then
            node = self:getRoot() -- Search through the root.
            ret = self:processNodeMessage(node, npc, player, message)
            if ret then
                return true
            end
        end
        return false
    end

    -- Tries to process the given message using the node parameter's children
    -- Calls the node's callback function if found
    -- Returns the childNode which processed the message or nil if no such node was found
    function KeywordHandler:processNodeMessage(node, npc, player, message)
        local playerId = player:getId()
        local messageLower = message:lower()
        for _, childNode in pairs(node.children) do
            if childNode:checkMessage(player, messageLower) then
                local oldLast = self.lastNode[playerId]
                self.lastNode[playerId] = childNode
                -- Make sure node is the parent of childNode (as one node can be parent to several nodes).
                childNode.parent = node
                if childNode:processMessage(npc, player, messageLower) then
                    childNode:processAction(player, messageLower)
                    return true
                end
                self.lastNode[playerId] = oldLast
            end
        end
        return false
    end

    -- Returns the root keywordnode
    function KeywordHandler:getRoot()
        return self.root
    end

    -- Returns the last processed keywordnode or root if no last node is found.
    function KeywordHandler:getLastNode(player)
        local playerId = player:getId()
        return self.lastNode[playerId] or self:getRoot()
    end

    -- Adds a new keyword to the root keywordnode. Returns the new node.
    function KeywordHandler:addKeyword(keys, callback, parameters, condition, action)
        return self:getRoot():addChildKeyword(keys, callback, parameters, condition, action)
    end

    -- Adds an alias keyword for the previous node.
    function KeywordHandler:addAliasKeyword(keys)
        return self:getRoot():addAliasKeyword(keys)
    end

    -- Moves the current position in the keyword hierarchy count steps upwards. Count defalut value = 1.
    --	This function MIGHT not work properly yet. Use at your own risk.
    function KeywordHandler:moveUp(player, steps)
        local playerId = player:getId()
        if steps == nil or type(steps) ~= "number" then
            steps = 1
        end
        for i = 1, steps do
            if self.lastNode[playerId] == nil then
                return nil
            end
            self.lastNode[playerId] = self.lastNode[playerId]:getParent() or self:getRoot()
        end
        return self.lastNode[playerId]
    end
end
