--[[
    ZO_AutoComplete is a utility object that connects to an edit control to provide autocomplete functionality.
]]

local COMMIT_BEHAVIOR_KEEP_FOCUS = 1
local COMMIT_BEHAVIOR_LOSE_FOCUS = 2

AUTO_COMPLETION_AUTOMATIC_MODE = true
AUTO_COMPLETION_MANUAL_MODE = false

AUTO_COMPLETION_USE_ARROWS = true
AUTO_COMPLETION_DONT_USE_ARROWS = false

AUTO_COMPLETION_DONT_CALL_HOOKED_HANDLERS = true
AUTO_COMPLETION_CALL_HOOKED_HANDLERS = false

AUTO_COMPLETION_ONLINE_ONLY = true
AUTO_COMPLETION_ONLINE_OR_OFFLINE = false

AUTO_COMPLETION_ANCHOR_TOP = 0
AUTO_COMPLETION_ANCHOR_BOTTOM = 1

AUTO_COMPLETION_SELECTED_BY_SPACE = 0
AUTO_COMPLETION_SELECTED_BY_ENTER = 1
AUTO_COMPLETION_SELECTED_BY_CLICK = 2
AUTO_COMPLETION_SELECTED_BY_TAB = 3

ZO_AutoComplete = ZO_CallbackObject:Subclass()
ZO_AutoComplete.ON_ENTRY_SELECTED = "ZO_AutoComplete_On_Entry_Selected"

AUTO_COMPLETE_FLAG_ALL = -1
AUTO_COMPLETE_FLAG_NONE = 0

ZO_AutoComplete.FlagHandlers = { }

function ZO_AutoComplete.AddFlag(handler)
    table.insert(ZO_AutoComplete.FlagHandlers, handler)
    return #ZO_AutoComplete.FlagHandlers
end


function ZO_AutoComplete:New(...)
    local autoComplete = ZO_CallbackObject.New(self)
    autoComplete:Initialize(...)
    return autoComplete
end

function ZO_AutoComplete:Initialize(editControl, includeFlags, excludeFlags, onlineOnly, maxResults, mode, allowArrows, dontCallHookedHandlers)
    self:SetEnabled(true)
    self:SetIncludeFlags(includeFlags)
    self:SetExcludeFlags(excludeFlags)
    self:SetOnlineOnly(onlineOnly)
    self:SetMaxResults(maxResults)

    self.automaticMode = mode == nil or mode == AUTO_COMPLETION_AUTOMATIC_MODE
    self.anchorStyle = AUTO_COMPLETION_ANCHOR_TOP
    self.useArrows = allowArrows == nil or AUTO_COMPLETION_USE_ARROWS == allowArrows
    self.dontCallHookedHandlers = dontCallHookedHandlers
    if(self.dontCallHookedHandlers == nil) then
        self.dontCallHookedHandlers = true
    end

    self.keepFocusOnCommit = true

    self.widthOffsetLeft = 0
    self.widthOffsetRight = 0

    self:SetEditControl(editControl)
    self:SetOwner(editControl)
end

function ZO_AutoComplete:SetEnabled(enabled)
    self.enabled = enabled
    if not enabled then
        self:Hide()
    end
end

function ZO_AutoComplete:SetIncludeFlags(includeFlags)
    self.includeFlags = includeFlags
end

function ZO_AutoComplete:SetExcludeFlags(excludeFlags)
    self.excludeFlags = excludeFlags
end

function ZO_AutoComplete:SetOnlineOnly(onlineOnly)
    self.onlineOnly = onlineOnly
end

function ZO_AutoComplete:SetMaxResults(maxResults)
    self.maxResults = maxResults
end

function ZO_AutoComplete:SetEditControl(editControl)
    if editControl then
        if self.automaticMode then
            ZO_PreHookHandler(editControl, "OnTextChanged", function() self:OnTextChanged() end)
        end

        ZO_PreHookHandler(editControl, "OnEnter", function()
            if self:IsOpen() then
                if not self.keepFocusOnCommit and self.automaticMode then
                    return self:OnCommit(COMMIT_BEHAVIOR_LOSE_FOCUS, AUTO_COMPLETION_SELECTED_BY_ENTER)
                else
                    return self:OnCommit(COMMIT_BEHAVIOR_KEEP_FOCUS, AUTO_COMPLETION_SELECTED_BY_ENTER)
                end
            end
        end)

        ZO_PreHookHandler(editControl, "OnTab", function()
            if self:IsOpen() then
                if not ZO_Menu_GetSelectedIndex() then
                    self:ChangeAutoCompleteIndex(1)
                end
                return self:OnCommit(COMMIT_BEHAVIOR_KEEP_FOCUS, AUTO_COMPLETION_SELECTED_BY_TAB)
            end
        end)

        if self.useArrows then
            ZO_PreHookHandler(editControl, "OnDownArrow", function() self:ChangeAutoCompleteIndex(1) end)
            ZO_PreHookHandler(editControl, "OnUpArrow", function() self:ChangeAutoCompleteIndex(-1) end)
        end
    
        ZO_PreHookHandler(editControl, "OnFocusLost", function() self:Hide() end)
        ZO_PreHookHandler(editControl, "OnHide", function() self:Hide() end)
    end
    
    self.editControl = editControl
end

function ZO_AutoComplete:SetOwner(owner)
    self.owner = owner
end

function ZO_AutoComplete:SetKeepFocusOnCommit(keepFocus)
    self.keepFocusOnCommit = keepFocus
end

function ZO_AutoComplete:Show(text)
    if not self:ApplyAutoCompletionResults(self:GetAutoCompletionResults(text)) then
        self:Hide()
    end
end

function ZO_AutoComplete:Hide()
    if self:IsOpen() then
        ClearMenu()
    end
end

function ZO_AutoComplete:IsOpen()
    return IsMenuVisible() and GetMenuOwner() == self.owner
end

function ZO_AutoComplete:SetUseCallbacks(useCallbacks)
    self.useCallbacks = useCallbacks
end

function ZO_AutoComplete:SetAnchorStyle(style)
    self.anchorStyle = style
end

function ZO_AutoComplete:SetWidthOffsets(left, right)
    self.widthOffsetLeft = left or 0
    self.widthOffsetRight = right or 0
end

do
    local ComputeStringDistance = ComputeStringDistance

    local function ComputeSubStringMatchScore(source, startIndex, trimmedTextToScore)
        local startIndex, endIndex = source:find(trimmedTextToScore, startIndex, true)
        if startIndex then
            return ((endIndex - startIndex) / (#source * .1)) * 10
        end
        return 0
    end

    function ComputeScore(source, scoringText, startIndex, trimmedTextToScore)
        return ComputeStringDistance(source, scoringText) - ComputeSubStringMatchScore(source, startIndex, trimmedTextToScore)
    end

    local POOR_MATCH_RATIO = .75
    local POOR_MATCH_MIN = 1

    local scores = {}
    local function BinaryInsertComparer(leftScore, _, rightIndex)
        return leftScore - scores[rightIndex]
    end

    local zo_binarysearch = zo_binarysearch
    function GetTopMatchesByLevenshteinSubStringScore(stringsToScore, scoringText, startingIndex, maxResults, noMinScore)
        maxResults = maxResults or 10
        if maxResults == 0 then return end

        startingIndex = startingIndex or 1
        scoringText = zo_strlower(scoringText)
        local trimmedTextToScore = scoringText:sub(startingIndex, 20)

        local results = {}
        ZO_ClearNumericallyIndexedTable(scores)

        local minScore = POOR_MATCH_RATIO * (zo_min(#scoringText, 10 + startingIndex) - startingIndex - POOR_MATCH_MIN)
        for lowerSource, source in pairs(stringsToScore) do
            local score = ComputeScore(lowerSource, scoringText, startingIndex, trimmedTextToScore)
            if noMinScore or (score <= minScore) then
                local _, insertPosition = zo_binarysearch(score, results, BinaryInsertComparer)
                table.insert(results, insertPosition, source)
                table.insert(scores, insertPosition, score)

                results[maxResults + 1] = nil
                scores[maxResults + 1] = nil
            end
        end

        return results
    end
end

do
    local function TryCallFlagHandler(flag, possibleMatches, input, onlineOnly, include)
        local handler = ZO_AutoComplete.FlagHandlers[flag]
        if handler then
            handler(possibleMatches, input, onlineOnly, include)
        end
    end

    local INCLUDE = true
    local EXCLUDE = false

    local function GenerateAutoCompletionResults(input, maxResults, onlineOnly, includeFlags, excludeFlags, noMinScore)
        local possibleMatches = {}

        if not includeFlags or includeFlags[1] == AUTO_COMPLETE_FLAG_ALL then
            for flag in ipairs(ZO_AutoComplete.FlagHandlers) do
                TryCallFlagHandler(flag, possibleMatches, input, onlineOnly, INCLUDE)
            end
        else
            for i, flag in ipairs(includeFlags) do
                TryCallFlagHandler(flag, possibleMatches, input, onlineOnly, INCLUDE)
            end
        end

        if excludeFlags then
            if excludeFlags[1] == AUTO_COMPLETE_FLAG_ALL then
                return
            end
            for i, flag in ipairs(excludeFlags) do
                TryCallFlagHandler(flag, possibleMatches, input, onlineOnly, EXCLUDE)
            end
        end 

        local results = GetTopMatchesByLevenshteinSubStringScore(possibleMatches, input, 1, maxResults, noMinScore)
        if results then
            return unpack(results)
        end
    end

    function GetAutoCompletion(input, maxResults, onlineOnly, includeFlags, excludeFlags, noMinScore)
        maxResults = maxResults or 10
        input = input:lower()

        return GenerateAutoCompletionResults(input, maxResults, onlineOnly, includeFlags, excludeFlags, noMinScore)
    end

    function ZO_AutoComplete:GetAutoCompletionResults(text)
        return GetAutoCompletion(text, self.maxResults, self.onlineOnly, self.includeFlags, self.excludeFlags)
    end
end

function ZO_AutoComplete:ApplyAutoCompletionResults(...)
    if ... and ... ~= "" then
        ClearMenu()
        SetMenuMinimumWidth(self.editControl:GetWidth() - GetMenuPadding() * 2)

        local numResults = select("#", ...)
        for i=1, numResults do
            local name = select(i, ...)
            AddMenuItem(name, function()
                if self.useCallbacks then
                    self:FireCallbacks(self.ON_ENTRY_SELECTED, name, AUTO_COMPLETION_SELECTED_BY_CLICK)
                else
                    self.editControl:SetText(name) 
                end
            end)
        end
        
        ShowMenu(self.owner, nil, MENU_TYPE_TEXT_ENTRY_DROP_DOWN)

        if self.anchorStyle == AUTO_COMPLETION_ANCHOR_BOTTOM then
            ZO_Menu:ClearAnchors()
            ZO_Menu:SetAnchor(BOTTOMLEFT, self.editControl, TOPLEFT, -8 + self.widthOffsetLeft, -2)
            ZO_Menu:SetAnchor(BOTTOMRIGHT, self.editControl, TOPRIGHT, 8 + self.widthOffsetRight, -2)
        else
            ZO_Menu:ClearAnchors()
            ZO_Menu:SetAnchor(TOPLEFT, self.editControl, BOTTOMLEFT, -8 + self.widthOffsetLeft, 2)
            ZO_Menu:SetAnchor(TOPRIGHT, self.editControl, BOTTOMRIGHT, 8 + self.widthOffsetRight, 2)
        end
        
        return true
    end
    
    return false
end

function ZO_AutoComplete:OnTextChanged()
    if self.enabled and self.editControl:HasFocus() and self.editControl:GetText() ~= "" then
        self:Show(self.editControl:GetText())        
    else
        self:Hide()
    end
end

function ZO_AutoComplete:ChangeAutoCompleteIndex(offset)
    if self:IsOpen() then
        local numItems = ZO_Menu_GetNumMenuItems()
        if numItems > 0 then
            local index = (ZO_Menu_GetSelectedIndex() or 0) + offset
        
            ZO_Menu_SetSelectedIndex(index)
        end
    end
end

function ZO_AutoComplete:GetNumAutoCompleteEntries()
    if self:IsOpen() then
        return ZO_Menu_GetNumMenuItems()
    end
end

function ZO_AutoComplete:GetAutoCompleteIndex()
    if self:IsOpen() then
        return ZO_Menu_GetSelectedIndex()
    end
end 

function ZO_AutoComplete:OnCommit(commitBehavior, commitMethod)
    if self:IsOpen() then
        local selectedIndex = ZO_Menu_GetSelectedIndex()
        if selectedIndex then
            local name = ZO_Menu_GetSelectedText()
            if self.useCallbacks then
                self:FireCallbacks(self.ON_ENTRY_SELECTED, name, commitMethod)
            else
                self.editControl:SetText(name) 
            end
        end
        if not commitBehavior or commitBehavior == COMMIT_BEHAVIOR_LOSE_FOCUS then
            self.editControl:LoseFocus()
        end

        self:Hide()

        if selectedIndex then
            return self.dontCallHookedHandlers
        else
            return false
        end
    end
end