--[[--------------------------------------------------------------------------
  Developed by Nathan Pieper - nrpieper (@) gmail (dot) com
  Misspelled is an interactive chat text spell checker for World of Warcraft

  This code freely distributed for your use in any GPL compliant project.
  Portions of this code are covered by the Gnu Public License (GPL)
  Dictionaries use the OpenOffice/HunSpell/ASpell dictionary format
  (http://wiki.services.openoffice.org/wiki/Dictionaries)
  Phonetic cache information created via the NetSpell dictionary util.
  References & Credits:
  Paul Welter & NetSpell: http://www.loresoft.com/projects/netspell
  Metaphone Algorithm http://aspell.net/metaphone/
  Edit Distance Algorithm http://en.wikipedia.org/wiki/Levenshtein_distance

--------------------------------------------------------------------------]]--


--[[
Challenges:


Issue #1
(http://www.wowwiki.com/ItemLink)
Color tags can be destroyed or mangled as you perform edit near the start
and end of the color tag.

If you press delete with the cursor positioned, just before, a color tag,
the start of the tag (|cffffffff) and the next character are removed.  The
closing color tag (|r) is left at the end.

If you press backspace with the cursor positioned just past the (|r) end
of a color tag, the (|r) is removed along with the character just before it.

If you press delete with the cursor positioned, just before the last char,
in a color tagged string, the character and the closing (|r) are both removed.

If you press backspace with the cursor positioned, just after the first char,
in a color tagged string, the first char and the beginning color tag are both removed.

In the case of colored item links, Wow just deletes the entire item, if you
hit a delete or backspace.

Solution:
First parse the line to flag any valid item links or textures so they don't get
destroyed by the next step.
Then if there are any remaining start or end color tag, remove them.

Issue #2
The Chat Edit box's edit cursor position is relative to characters that don't display in the
chat edit text, i.e. item link tags, color tags, and texture tags.
When we add, or remove, a color tag, to highlight a misspelled word,  we should adjust the
cursor position so it acts natural.
Note: An OnCursorChanged event doesn't exist or doesn't fire for the ChatEditBox.

Solution:
Methods that insert or delete text from the ChatEditBox need to properly adjust the cursor
position as needed when, visible or hidden, characters are inserted or deleted to the left of the cursor.

Two techniques could be used.
#1: Track the number of, printable and non-printable characters inserted or deleted
    to the left of the cursor, and adjust the cursor position to compensate.

#2: Insert a tracking char byte (example: \1), at the cursor position.  After making
	inserts and deletes, find the tracking char byte, remove it and reposition the cursor
    at its location.

I have implemented technique #1.

Issue #3 WIM Integration
Wim allows multiple chat windows at once.  So we need to track WordLocations per editbox.
Wim breaks long chat messages into multiple messages.
Wim has a EditBox right click handler to insert emoticons and previous chat messages.
WIM sends chat messages using ChatThrottleLib:SendChatMessage.  This can hot SendChatMessage before Misspelled and bypasses the step that cleans the highlighting.
(10/10/2009) Updated hooks, from WIM, will be provided via WIM.RegisterPreSendFilterText(func()), to remove the need to manually hook ChatThrottleLib

User Dictionary Editor Added
(12/6/2009) - Used AceGUI to add the ability to remove words from the user dictionary.

(1/12/2010) - Added enGB UK English dictionary.

(5/12/2010) - Added support for PTR Patch 3.5.  Multiple ChatEditBoxes get created.  Hook into the activate routine.

(5/23/2010) - Fixed enGB dictionary load issue.

(6/24/2010) - Added itIT Italian dictionary

(7/22/2010) - The Addon Gryphonheart Items (GHI) begins it's item color tags with a (|C) capitol C character.  I added that possibility to the chat text parsing.

(9/14/2010) - Client 4 has some issues with trying to set the owner of the popmenu items.  Looks like it's not needed.

(9/7/2019) - Wow Classic: When starting you can't have friends and the global function: GetNumFriends() is nil.  Detect and skip to eliminate the error.

(4/30/2025) - Changes added to RemoveHighlighting to parse new Item Quality # colors and Global Colors UI escape sequences.
--]]--

local _G = _G

Misspelled = LibStub("AceAddon-3.0"):NewAddon("Misspelled", "AceEvent-3.0", "AceHook-3.0")

local Misspelled = _G.Misspelled

Misspelled.Version = C_AddOns.GetAddOnMetadata("Misspelled", "Version")

local AceGUI = LibStub("AceGUI-3.0")
local L = LibStub("AceLocale-3.0"):GetLocale("Misspelled", true)

local table_insert = table.insert
local string_byte = string.byte
local string_find = string.find
local string_format = string.format
local string_gsub = string.gsub
local string_len = string.len
local string_lower = string.lower
local string_match = string.match
local string_rep = string.rep
local string_sub = string.sub
local string_upper = string.upper
local tostring = tostring
local pairs = pairs
local ipairs = ipairs

--local SPELLED_WRONG_HIGHLIGHT =   "|cffff91c8" --The color misspelled words will get changed into.
--local SPELLED_WRONG_HIGHLIGHT =   "|cffdea1d3" --The color misspelled words will get changed into.
--local SPELLED_WRONG_HIGHLIGHT =   "|cfffefe80" --The color misspelled words will get changed into.
local SPELLED_WRONG_HIGHLIGHT_HEX_COLOR_CODE =   "ff7dc6fb" --The color misspelled words will get changed into. (Medium-Cyan-ish)
local SPELLED_WRONG_HIGHLIGHT =   "|c"..SPELLED_WRONG_HIGHLIGHT_HEX_COLOR_CODE --The color misspelled words will get changed into. (Medium-Cyan-ish)

local WordCache = {}           --Stores a cache of every word checked, along with the suggestions for words misspelled
local WordCacheCount = 0       --Counter used to track when we should clean the WordCache table to save memory
local WordCacheCountMax = 7000 --Number of entries tha can live in the WordCache before we clean the cache
local WordLocations = {}       --Lookup table used by multiple functions to determine where each work starts and ends
							   --There will be a sub-table for each EditBox:GetName() so we can store multiple sets of info at once.
local SkipOnTextChanged = false -- use to avoid OnTextChanged event firing after spell checking highlights chat text.
local RightClickedWord = nil   --The current word under the CursorPosition that was right-clicked
local RightClickedWordStartPos --Where that word starts
local RightClickedWordEndPos   --and where that word ends
local RightClickedEditBox      --and what EditBox was right clicked
local OldLineLength            --Tracks the previous length of the ChatEditBox.text
local GuildRosterCalled = false
local MaxColorCodes = 12       --The max amount of color codes we will add to the editbox text.

local Misspelled_Saved_CTL_SendChatMessage
local Misspelled_CTL_hookedversion=0

function Misspelled:OnInitialize()
	if Misspelled_DB == nil then
		Misspelled_DB = {}
	end

	--Check if the user has chosen to override the auto dictionary selection
	if Misspelled_DB.LoadDictionary == nil then
		Misspelled_DB.LoadDictionary = "enUS"
	end

	if Misspelled_DB.AutoSelectDictionary == nil then
		Misspelled_DB.AutoSelectDictionary = true
	end

	local dict = "Auto"
	if  Misspelled_DB.AutoSelectDictionary == true then
		dict = "Auto"
	else
		dict = Misspelled_DB.LoadDictionary or "Auto"
	end

	--Load the Dictionary
	local dictLoaded
	if dict == "Auto" then
		dictLoaded = WordDict:Init()
	else
		dictLoaded = WordDict:Init(dict)
	end

	Misspelled:print("Misspelled: " .. L["Dictionary Loaded"] .." - " .. dictLoaded)

	--Load user dict
	Misspelled:LoadUserDict()
	Misspelled:print("Misspelled: " .. L["User Dictionary Loaded"])

	-- Build Interface Options window
	self:CreateInterfaceOptions()

	-- Slash commands (manual menu testing)
	SLASH_MISSPELLED1 = "/misspelled"
	SLASH_MISSPELLED2 = "/msp"
	SlashCmdList["MISSPELLED"] = function(msg)
		msg = msg and msg:lower() or ""
		-- /misspelled menu [word]
		-- If a word is provided, we'll set the chat edit box text to that word (plus a trailing space)
		-- and open the suggestions menu. This avoids the "I can't both type a word and a slash command" problem.
		local menuWord = msg:match("^menu%s+(.+)$")
		if msg == "menu" or menuWord then
			local eb = _G.ChatEdit_GetActiveWindow and _G.ChatEdit_GetActiveWindow()
			if not eb then
				eb = _G.ACTIVE_CHAT_EDIT_BOX
			end
			if not eb then
				eb = _G.ChatFrame1EditBox or _G.ChatFrameEditBox
			end
			if not eb then
				Misspelled:print("Misspelled: No chat edit box found.")
				return
			end

			if menuWord and #menuWord > 0 then
				-- Put the word into the edit box so SuggestionsFrame_Click can replace it.
				eb:SetText(menuWord .. " ")
				eb:SetCursorPosition(#menuWord)
			end

			Misspelled:SpellCheckChat(eb)
			local wl = WordLocations[eb:GetName()]
			if wl then
				for _, w in ipairs(wl) do
					if WordCache[w.Word] and WordCache[w.Word].Correct == false then
						if WordCache[w.Word].Suggestions == nil then
							WordCache[w.Word].Suggestions = WordDict:Suggest(w.Word)
						end
						RightClickedWord = w.Word
						RightClickedWordStartPos = w.StartPos
						RightClickedWordEndPos = w.EndPos
						RightClickedEditBox = eb
						Misspelled:ShowSuggestionsMenu()
						return
					end
				end
			end
			Misspelled:print("Misspelled: No misspelled word found to show a menu for (type a misspelled word first).")
			return
		end

		-- /misspelled demo
		-- One-shot demo that should highlight and open the suggestions menu automatically.
		if msg == "demo" then
			local eb = _G.ChatEdit_GetActiveWindow and _G.ChatEdit_GetActiveWindow()
			if not eb then
				eb = _G.ACTIVE_CHAT_EDIT_BOX
			end
			if not eb then
				eb = _G.ChatFrame1EditBox or _G.ChatFrameEditBox
			end
			if not eb then
				Misspelled:print("Misspelled: No chat edit box found.")
				return
			end

			eb:SetText("helo ")
			eb:SetCursorPosition(4)
			Misspelled:SpellCheckChat(eb)

			-- Reuse the menu logic above
			local wl = WordLocations[eb:GetName()]
			if wl then
				for _, w in ipairs(wl) do
					if WordCache[w.Word] and WordCache[w.Word].Correct == false then
						if WordCache[w.Word].Suggestions == nil then
							WordCache[w.Word].Suggestions = WordDict:Suggest(w.Word)
						end
						RightClickedWord = w.Word
						RightClickedWordStartPos = w.StartPos
						RightClickedWordEndPos = w.EndPos
						RightClickedEditBox = eb
						Misspelled:ShowSuggestionsMenu()
						return
					end
				end
			end

			Misspelled:print("Misspelled: Demo failed to find misspelled word (unexpected).")
			return
		end

		Misspelled:print("Misspelled commands: /misspelled menu [word]  |  /misspelled demo")
	end

	--Watch for other chat addons: Wim, to load and then integrate.
	Misspelled:RegisterEvent("ADDON_LOADED")

	--Guild members and Friends are valid words.
	--Wait for the GUILD_ROSTER_UPDATE event to load
	--the guild members & friends into the dictionary as valid words.

	--Register the GUILD_ROSTER_UPDATE event, so we know when we can load the
	--Guild member names into the database
	Misspelled:RegisterEvent("GUILD_ROSTER_UPDATE")


	--GuildRoster can only be called every so often.
	--If another addon triggered it first, we might miss notification.
	--Moved to OnTextChanged
	--GuildRoster()

	--Patch 3.5 has multiple ChatEditBoxes.  We need to hook in differently.
	-- Modern retail is more sensitive to function replacement (taint/protected frames).
	-- Wire up known Blizzard chat edit boxes up front, and securely hook activation for any others.
	Misspelled:WireUpAllChatEditBoxes()
	if ChatEdit_ActivateChat ~= nil then
		Misspelled:SecureHook("ChatEdit_ActivateChat")
	elseif ChatFrameEditBox ~= nil then
		Misspelled:WireUpEditBox(ChatFrameEditBox)
	end


	-- Before a chat message is sent, remove any highlighting that Misspelled might have added.
	-- (Attempting to send color tags in chat can disconnect you.)
	if C_ChatInfo and C_ChatInfo.SendChatMessage then
		Misspelled:RawHook(C_ChatInfo, "SendChatMessage", Misspelled.SendChatMessage, true)
	else
		Misspelled:RawHook("SendChatMessage", Misspelled.SendChatMessage, true)
	end
end


function Misspelled:WireUpEditBox(editbox)
	if not editbox then return end

	-- Avoid re-hooking the same editbox repeatedly.
	if self._wiredEditBoxes == nil then self._wiredEditBoxes = {} end
	local name = editbox.GetName and editbox:GetName() or tostring(editbox)
	if self._wiredEditBoxes[name] then return end
	self._wiredEditBoxes[name] = true

	Misspelled:SecureHookScript(editbox, "OnEscapePressed", Misspelled.EditBox_OnEscapePressed)
	Misspelled:SecureHookScript(editbox, "OnEnterPressed", Misspelled.EditBox_OnEnterPressed)
	Misspelled:SecureHookScript(editbox, "OnTextChanged", Misspelled.EditBox_OnTextChanged)

	-- Mouse interaction: use the frame's native HookScript (reliable on modern retail chat edit boxes).
	if editbox.EnableMouse then editbox:EnableMouse(true) end
	if editbox.HookScript then
		editbox:HookScript("OnMouseDown", Misspelled.EditBox_OnMouseDown)
		editbox:HookScript("OnMouseUp", Misspelled.EditBox_OnMouseUp)  -- Used to show suggestions menu
	end

	-- 11.2.x: Create overlay to capture mouse events on protected editboxes.
	-- Propagate clicks to editbox so cursor position is set correctly, then handle our logic on mouse up.
	if self._mouseOverlays == nil then self._mouseOverlays = {} end
	if self._mouseOverlays[name] == nil and CreateFrame then
		local overlay = CreateFrame("Button", nil, editbox)  -- Use Button to get click events
		overlay:SetAllPoints(editbox)
		overlay:EnableMouse(true)
		overlay:RegisterForClicks("AnyUp", "AnyDown")  -- Register for both left and right clicks
		overlay:SetFrameStrata(editbox:GetFrameStrata() or "DIALOG")
		overlay:SetFrameLevel((editbox:GetFrameLevel() or 0) + 10)
		overlay:SetClampedToScreen(true)
		-- Propagate clicks so the editbox sets cursor position correctly
		if overlay.SetPropagateMouseClicks then
			overlay:SetPropagateMouseClicks(true)
		end
		if overlay.SetPropagateMouseMotion then
			overlay:SetPropagateMouseMotion(true)
		end
		-- OnMouseDown: let the click propagate to editbox for cursor positioning
		overlay:SetScript("OnMouseDown", function(self, btn)
			-- Click propagates to editbox automatically
		end)
		-- OnMouseUp: check if we clicked on a misspelled word
		overlay:SetScript("OnMouseUp", function(self, btn)
			-- Small delay to ensure cursor position is updated first
			C_Timer.After(0, function()
				Misspelled.EditBox_OnMouseUp(editbox, btn)
			end)
		end)
		self._mouseOverlays[name] = overlay
	end
end

-- Wire up the default Blizzard chat edit boxes (ChatFrame1EditBox, ChatFrame2EditBox, ...)
function Misspelled:WireUpAllChatEditBoxes()
	local n = _G.NUM_CHAT_WINDOWS or 10
	for i = 1, n do
		local eb = _G["ChatFrame" .. i .. "EditBox"]
		if eb then
			self:WireUpEditBox(eb)
		end
	end
end

function Misspelled:GUILD_ROSTER_UPDATE()
	Misspelled:LoadGuildAndFriendRoster()
end

function Misspelled:ADDON_LOADED(event, addonName)
	if event == "ADDON_LOADED" and addonName == "WIM" then
		WIM.RegisterWidgetTrigger("msg_box", "whisper,chat,w2w", "OnEscapePressed", Misspelled.EditBox_OnEscapePressed)
		WIM.RegisterWidgetTrigger("msg_box", "whisper,chat,w2w", "OnEnterPressed", Misspelled.EditBox_OnEnterPressed)
		WIM.RegisterWidgetTrigger("msg_box", "whisper,chat,w2w", "OnTextChanged", Misspelled.EditBox_OnTextChanged)
		--WIM.RegisterWidgetTrigger("msg_box", "whisper,chat,w2w", "OnMouseUp", Misspelled.EditBox_OnMouseUp)


		--Before a chat message is sent, remove any highlighting that Misspelled might have added.
		--The Wow client will disconnect if you attempt to send a colored chat message.

		--If available use the WIM API: Wim.RegisterPreSendFilterText

		--WIM sends its chat messages via the API ChatThrottleLib,
		--ChatThrottleLib hooks the default SendChatMessage api, many times, before Misspelled can.
		--ChatThrottleLib is used in many addons, that potentially load before Misspelled.
		--So we have to hook ChatThrottleLib just to be safe.


		if(WIM.RegisterPreSendFilterText) then -- avoid error if WIM not up to date.
			WIM.RegisterPreSendFilterText(function(text)
											return Misspelled:RemoveHighlighting(text)
										  end)
		else
			if(ChatThrottleLib and Misspelled_CTL_hookedversion < ChatThrottleLib.version) then
				Misspelled_Saved_CTL_SendChatMessage=ChatThrottleLib.SendChatMessage

				function ChatThrottleLib:SendChatMessage(prio, prefix, text, ...)
					text = Misspelled:RemoveHighlighting(text)
					--print("Misspelled Hooked ChatThrottleLib_SendChatMessaged called")
					return Misspelled_Saved_CTL_SendChatMessage(ChatThrottleLib, prio, prefix, text, ...)
				end
				Misspelled_CTL_hookedversion=ChatThrottleLib.version
			end
		end
	end
end

--Patch 3.5 Hook ChatFrame.lua ChatEdit_ActivateChat(editBox)
function Misspelled:ChatEdit_ActivateChat(editBox)
	--Make sure this editbox is hooked
	--print("EditBox to hook: " .. editBox:GetName())
	local hooked, hookHandler  = Misspelled:IsHooked(editBox, "OnTextChanged")
	if  hooked == false then
		Misspelled:WireUpEditBox(editBox)
	end
end

--Before a chat message is sent, remove any highlighting that Misspelled might have added.
--The Wow client will disconnect if you attempt to send Hex code colored text in a chat message.
function Misspelled.SendChatMessage(message, chatType, languageID, target, ...)
	local cleanedMessage = Misspelled:RemoveHighlighting(message)
	if C_ChatInfo and C_ChatInfo.SendChatMessage then
		Misspelled.hooks[C_ChatInfo]["SendChatMessage"](cleanedMessage, chatType, languageID, target, ...)
	else
		Misspelled.hooks["SendChatMessage"](cleanedMessage, chatType, languageID, target, ...)
	end
end

--Possible changes:
--A single new character was inserted at the end of the line (line length grew by 1)
--	  If this char is a word separator, then spell check the line
--
--Any other change in the line, requires we recheck the entire line.
--More than a single character was pasted or linked into the line.
--	remove any highlighting and recheck the entire line.
--
--A single character was removed from the line
function Misspelled.EditBox_OnTextChanged(editbox)
	if SkipOnTextChanged == true then
		SkipOnTextChanged = false
		return
	end

	-- If the suggestions popup is open, close it when text changes
	-- (user is manually typing a correction)
	if CustomSuggestionsFrame and CustomSuggestionsFrame:IsShown() then
		POPUP_IS_LOCKED = true  -- Prevent reopening
		CustomSuggestionsFrame:Hide()
		C_Timer.After(0.3, function() POPUP_IS_LOCKED = false end)
		-- Clear the stored word positions since text has changed
		RightClickedWord = nil
		RightClickedWordStartPos = nil
		RightClickedWordEndPos = nil
	end

	--print("Editbox name:", editbox:GetName())

	--Load the guild roster if needed
	if GuildRosterCalled == false then
		if IsInGuild() then
			C_GuildInfo.GuildRoster() --Request updated guid roseter info from the server
		else
			Misspelled:LoadGuildAndFriendRoster()
		end
		GuildRosterCalled = true
	end


	local text = editbox:GetText()
	local pos = editbox:GetCursorPosition()

	--print ("TextChaged:", editbox:GetCursorPosition(), string.gsub(editbox:GetText(), "\124", "\124\124"))

	local newLineLength = #text

	--Check if we should clear the WordCache table to save memory, if it's gotten very large
	if newLineLength == 0 then
		if WordCacheCount > WordCacheCountMax then
			WordCache = {}
			WordCacheCount = 0
		end
	end


	--if the first char is a /, indicating some slash command, skip spellchecking the text.
	if (string_sub(text, 1, 1) == "/" ) then
		local cleanedChatMessage = Misspelled:RemoveHighlighting(text)
		if text ~= cleanedChatMessage then
			editbox:SetText(cleanedChatMessage)
			if pos == 1 or pos== 0  then
			  editbox:SetCursorPosition(pos)
			end
			OldLineLength = #text
		end

		RightClickedWord = nil
		WordLocations[editbox:GetName()] = {}
		return
	end


	--If we currently just added one new char to the end of the line, see if it's a word boundary char
	--CursorPosition (pos) must be at the end of the line, and the line size must have had grown by
	--one character.
	if pos == #text and newLineLength -1 == OldLineLength then
		local lastChar = string_sub(text, -1, -1)
		if string_match(lastChar, "[ %(%);,%.!%?:\"]") ~= nil then
			Misspelled:SpellCheckChat(editbox)
		end
	elseif pos ~= #text then
		----We must be making some other kind of edit someplace other than at the end of the line.
		----Recheck the entire line
		Misspelled:SpellCheckChat(editbox)
	end

	--Save the new line length for use with the next round of OnTextChanged processing.
	OldLineLength = #editbox:GetText()
end

function Misspelled.EditBox_OnEscapePressed(editbox)
	RightClickedWord = nil
	WordLocations[editbox:GetName()] = {}
end

function Misspelled.EditBox_OnEnterPressed(editbox)
	RightClickedWord = nil
	WordLocations[editbox:GetName()] = {}

	--before message is sent, remove and misspelled highlighting
	local cleanedChatMessage = Misspelled:RemoveHighlighting(editbox:GetText())
	WordLocations[editbox:GetName()] = {}
	editbox:SetText(cleanedChatMessage)
end


--Return a string where misspelled words are highlighted
--If this string is a slash command "/", don't perform any highlighting
--We should keep track of the current edit cursor position,
--so we can report it's new position after highlighting.
function Misspelled:SpellCheckChat(editbox)
	local editboxText = editbox:GetText()

	if #editboxText < 2 then return end

	-- On modern retail, avoid trying to replace protected scripts at runtime (taint-prone).
	-- If the editbox wasn't wired yet, wire it once; otherwise rely on SecureHookScript set up in WireUpEditBox().
	Misspelled:WireUpEditBox(editbox)

	local newText = editboxText

	--Watch how many characters we insert or remove from the left side of the cursor position.
	--We'll adjust the cursor position to compensate for any misspelled word Hex code colored text added by Misspelled.
	local newCPos

	--remove any previous misspelling highlighting before checking the editboxText for misspellings.
	newText, newCPos = Misspelled:RemoveHighlighting(editboxText, editbox:GetCursorPosition())

	WordLocations[editbox:GetName()] = {}

	--If this is a command, don't spellcheck or highlight
	if string_sub(editboxText, 1, 1) == "/" then
		if newText ~= editboxText then
			editbox:SetText(newText)
			editbox:SetCursorPosition(newCPos)
		end
	end

	--Find misspelled words & populate the WordLocations info.
	Misspelled:CheckLine(newText, editbox)

	local colorCodesAdded = 0
	--Use the WordLocation info to march backwards through the input text,
	--highlighting misspellings
	--tprint(WordLocations)
	local w
	for x = #WordLocations[editbox:GetName()], 1, -1 do
		w = WordLocations[editbox:GetName()][x]
		if WordCache[w.Word].Correct == false then
			--Insert highlighting
			newText = string_sub(newText, 1, w.StartPos -1) .. SPELLED_WRONG_HIGHLIGHT .. string_sub(newText, w.StartPos, w.EndPos) .. FONT_COLOR_CODE_CLOSE .. string_sub(newText, w.EndPos + 1)

			--Adjust cursor position if the cursor was to the right of the first char in the word we're highlighting.
			if newCPos >= w.EndPos then
				newCPos = newCPos + #SPELLED_WRONG_HIGHLIGHT + #FONT_COLOR_CODE_CLOSE
			elseif newCPos >= w.StartPos then
				newCPos = newCPos + #SPELLED_WRONG_HIGHLIGHT
			end

			colorCodesAdded = colorCodesAdded + 1
			if colorCodesAdded >= MaxColorCodes then 
				--Truncate WordLocations to here, because there are too many misspelled words.
				--The WoW chatbox won't let us add more than so many color coded sections.
				
				--Delete all entries before x
				WordLocations[editbox:GetName()] = { unpack( WordLocations[editbox:GetName()], x ) }
				
				break
			end
		end
	end

	--Adjust the word's WordLocation StartPos and EndPos, wherever we added highlighting.
	--The right click handler uses this position info. to detect the misspelled word that was right clicked.
	--March forward this time
	local n = 0 --NewCharsAddedCounter
	for x = 1, #WordLocations[editbox:GetName()] do
		w = WordLocations[editbox:GetName()][x]
		if WordCache[w.Word].Correct == false then
			w.StartPos = w.StartPos + n + #SPELLED_WRONG_HIGHLIGHT
			n = n + #SPELLED_WRONG_HIGHLIGHT
			w.EndPos = w.EndPos + n
			n = n + #FONT_COLOR_CODE_CLOSE
		end
	end

	if newText ~= editboxText then
		--When we call settext, an OnSetText event will fire.
		--Execution of this event should be skipped to avoid SpellCheckChat from running twice.
		--Use a local toggle to skip this second firing
		SkipOnTextChanged = true
		editbox:SetText(newText)
		editbox:SetCursorPosition(newCPos)
		
	end
end



--Finds all words in the input text string, ignoring any Wow hyperlinks or textures.
--These links or textures will be replaced with a sequence of # characters, the same length of the color+link,
--so the contents of these are not spellchecked.
--
--Next populate the table, WordLocations, storing the following info. on each word:
--	WordLocations[x] = {["word"] = word, ["StartPos"] = matchPosStart, ["EndPos"] = matchPosEnd}
--		x == Ordinal position of the word in the text string
--
--Next check each word to see we need to cache spell check info for the word.
--Words with numbers or words in all upper case are ignored.
--Store in the WordCache table whether the word is spell correctly, along with the
--any misspelled replacement suggestions.
--
--Trim the WordCache table if it's grown very large
function Misspelled:CheckLine(text, editbox)
	--Reset the info on where each word is located
	WordLocations[editbox:GetName()] = {}

	if text == nil then return end
	if #text == 0 then return end

	--Find if there are any WoW UI escape sequences on this line, and replace them with # chars, 
	--so they don't match as words in the next stage of parsing and get ignored for spellchecking.
	--ref: https://warcraft.wiki.gg/wiki/UI_escape_sequences
	-- |cn[^:]+:.*|r          -- Global Colors text (new in 11.1.5 - |cncolorname:text|r)
	-- |cnIQ%d:.*|r           -- Item Quality Colors (new in 11.1.5 - |cnIQn:text|r
	-- |[Cc]%x-|H.+|h.+|h|r   -- Hex color coded text with links (format: |cffxxxxxx|Htype:payload|h[text]|h|r ) https://warcraft.wiki.gg/wiki/Hyperlinks
	-- |cn[^:]+:|H.+|h.+h|r   -- Global Colors / Custom item color links (new in 11.1.5 - |cncolorname:text|r) https://warcraft.wiki.gg/wiki/UI_escape_sequences#:~:text=back%20to%20white-,Global%20Colors,-%7Ccncolorname%3A
	-- |H.*|h                 -- Links
	-- |T.*|t                 -- Textures
	-- |A.*|a                 -- Texture Atlas
	-- {.-}                   -- Raid target icons
	-- |n                     -- newline character
	local newText = text
	-- newText = string_gsub(newText, "(|[Cc]%x-|H.-|h.-|h|r)", function(x) return string_rep("#", #x) end)
	-- newText = string_gsub(newText, "(|H.*|h)", function(x) return string_rep("#", #x) end)
	-- newText = string_gsub(newText, "(|T.*|t)", function(x) return string_rep("#", #x) end)
	-- newText = string_gsub(newText, "({.-})", function(x) return string_rep("#", #x) end)
	-- newText = string_gsub(newText, "(|n)", function(x) return string_rep("#", #x) end)

	local WowTextMarkupEscapes = {
		["(|cn[^:]+:.-|r)"] = "#",        -- Global Colors text
		["(|cnIQ%d:.-|r)"] = "#",         -- Item Quality Colors text
		["(|[Cc]%x-|H.-|h.-|h|r)"] = "#", -- Hex color coded text with optional colored links
		["(|H.-|h)"] = "#",               -- Links
		["(|T.-|t)"] = "#",               -- Textures
		["(|A.-|a)"] = "#",               -- Texture Atlas
		["({.-})"] = "#",                 -- Raid target icons
		["(|n)"] = "#"                    -- Newline character
	}

	for k, v in pairs(WowTextMarkupEscapes) do
        newText = string_gsub(newText, k, function(x) return string_rep(v, #x) end)
    end

	--March through the text, finding the words, record there start & end positions, and spell check status
	local patt = "[A-Za-z0-9_'À-ÿœæŒÆ]+"
	local matchPosStart
	local matchPosEnd

	matchPosStart, matchPosEnd = string_find(newText, patt)

	local x = 0
	local word
	local correct
	while matchPosStart ~= nil do
		word = string_sub(newText, matchPosStart, matchPosEnd)

		--ignore all uppercase words
		if word ~= string_upper(word) then
			--ignore words with numbers in them
			if string_match(word, "[%d]") == nil then
				x = x + 1
				WordLocations[editbox:GetName()][x] = {["Word"] = word, ["StartPos"] = matchPosStart, ["EndPos"] = matchPosEnd}

				if WordCache[word] == nil then
					correct = false

					--Ignore words in all upper case
					if word == string_upper(word) then
						correct = true
					end

					--See if the dictionary contains the word
					if correct == false then
						correct = WordDict:Contains(word)
					end

					--Try the lower case version of the word
					if correct == false then
						correct = WordDict:Contains(string_lower(word))
					end

					--Cache the results
					WordCache[word] = {["Correct"] = correct} --, ["Suggestions"] = {}}
					WordCacheCount = WordCacheCount + 1
					--Changed to delay searching for suggestions until someone right-clicks on a misspelled word.
					--Adding UTF8 support slows the suggestion generation.
--~ 					if correct == false then
--~ 						local suggestions = {}
--~ 						suggestions = WordDict:Suggest(word)
--~ 						if #suggestions > 0 then
--~ 							WordCache[word].Suggestions = suggestions
--~ 						end
--~ 					end
				end
			end
		end

		matchPosStart, matchPosEnd = string_find(newText, patt, matchPosEnd+1)
	end
end



--Return a string where the highlighting has been removed from any misspelled words,
--with the goal to not change or destroy any other UI escaped sequences present in the chat message,
--such as colored text, Wow itemLinks or textures. 
--(https://warcraft.wiki.gg/wiki/UI_escape_sequences & http://www.wowwiki.com/ItemLink)
--
--Misspelled word highlighting is added with a Hex color coded UI escape sequence: |cff7dc6fbMisspelledtext|r
--The chat message text may contain other UI escaped sequences such at item links.
--The chat message text could also contain one or more pipe characters ||, complicating parsing.
--
-- Note: Lua's standard regular expression library has limitations compared to some other regex engines.
-- It does not support features like negative lookahead assertions ((?!...)),
-- which are typically used to assert that a sequence is not present.
-- Therefore, a single Lua regular expression cannot directly say "match everything until |r, but fail if |h is encountered before that".
--
--Potential refactor: 
-- 1) Use a regular expression to match any block starting with |cff7dc6fb and ending with |r, capturing everything in between.
--    regex capture: |c%x-(.-)|r
-- 2) Check the captures text to ensure it does not contain the sequence |h.
--
--It's possible that a recent edit has started to destroy the color tags, either at the
--beginning or end of a highlighted misspelled word.
--Attempt to detect this and remove any dangling colored text tags.
function Misspelled:RemoveHighlighting(text, ...)
	-- \124 is the ASCII code for the pipe '|' character.
	--Blizzard uses string.gsub(textString, "[|]", "||"), in the /dump source code 
	
	local cleanedChatMessage
	local newText = text

	--Track the number of characters removed from the left side of the current cursor position.
	local cPos
	if ... ~= nil then
		cPos = ...
	else
		cPos = 0
	end

	local itemLinks = {}
	local itemLink
	local itemLinkNum = 1

	local patt, matchPosStart, matchPosEnd
	local tokenSize
	local tempToken
	local tempText
	local charsRemoved  --Tracks the chars removed from the left of the cursor

	--Try and match Global Colors text. (|cncolorname:text|r)
	--Use a non-greedy match character (-) in the match pattern rather than a greed match character (*)
	patt = "(|cn[^:]+:.-|r)"
	matchPosStart, matchPosEnd = string_find(newText, patt)

	while matchPosStart ~= nil do
		--Store the itemlink and it's relative position so it can be replaced latter
		itemLink = string_sub(newText, matchPosStart, matchPosEnd)
		itemLinks[itemLinkNum] = itemLink

		tokenSize = #itemLink
		tempToken = "{<<" .. tostring(itemLinkNum)
		tempToken = tempToken .. string_rep(">", tokenSize - #tempToken - 1) .. "}"

		--Replace this itemlink with a temporary placeholder code
		newText = string_gsub(newText, patt, tempToken, 1)

		itemLinkNum = itemLinkNum + 1
		matchPosStart, matchPosEnd = string_find(newText, patt)
	end

	--Try and match (IQn) Item Quality Colors text. (|cnIQn:text|r)
	--Use a non-greedy match character (-) in the match pattern rather than a greed match character (*)
	patt = "(|cnIQ%d:.-|r)"
	matchPosStart, matchPosEnd = string_find(newText, patt)

	while matchPosStart ~= nil do
		--Store the itemlink and it's relative position so it can be replaced latter
		itemLink = string_sub(newText, matchPosStart, matchPosEnd)
		itemLinks[itemLinkNum] = itemLink

		tokenSize = #itemLink
		tempToken = "{<<" .. tostring(itemLinkNum)
		tempToken = tempToken .. string_rep(">", tokenSize - #tempToken - 1) .. "}"

		--Replace this itemlink with a temporary placeholder code
		newText = string_gsub(newText, patt, tempToken, 1)

		itemLinkNum = itemLinkNum + 1
		matchPosStart, matchPosEnd = string_find(newText, patt)
	end

	--Try and match Hex code colored Item links.  The Addon GHI (Gryphonheart Items) colors links with a capitol C, non-standard.
	patt = "|[Cc]%x+|H.-|h.-|h|r"
	matchPosStart, matchPosEnd = string_find(newText, patt)

	while matchPosStart ~= nil do
		--Store the itemlink and it's relative position so it can be replaced latter
		itemLink = string_sub(newText, matchPosStart, matchPosEnd)
		itemLinks[itemLinkNum] = itemLink

		tokenSize = #itemLink
		tempToken = "{<<" .. tostring(itemLinkNum)
		tempToken = tempToken .. string_rep(">", tokenSize - #tempToken - 1) .. "}"

		--Replace this itemlink with a temporary placeholder code
		newText = string_gsub(newText, patt, tempToken, 1)

		itemLinkNum = itemLinkNum + 1
		matchPosStart, matchPosEnd = string_find(newText, patt)
	end

	--Try to match, non-colored Item Links
	patt = "|H.-|h"
	matchPosStart, matchPosEnd = string_find(newText, patt)

	while matchPosStart ~= nil do
		--Store the itemlink and it's relative position so it can be replaced latter
		itemLink = string_sub(newText, matchPosStart, matchPosEnd)
		itemLinks[itemLinkNum] = itemLink

		tokenSize = #itemLink
		tempToken = "{<<" .. tostring(itemLinkNum)
		tempToken = tempToken .. string_rep(">", tokenSize - #tempToken - 1) .. "}"

		--Replace this itemlink with a temporary placeholder code
		newText = string_gsub(newText, patt, tempToken, 1)

		itemLinkNum = itemLinkNum + 1
		matchPosStart, matchPosEnd = string_find(newText, patt)
	end

	--Try to match and textures links.  (i.e. Raid targets and there used when chatting with a GM)
	patt = "|T.-|t"
	matchPosStart, matchPosEnd = string_find(newText, patt)

	while matchPosStart ~= nil do
		--Store the itemlink and it's relative position so it can be replaced latter
		itemLink = string_sub(newText, matchPosStart, matchPosEnd)
		itemLinks[itemLinkNum] = itemLink

		tokenSize = #itemLink
		tempToken = "{<<" .. tostring(itemLinkNum)
		tempToken = tempToken .. string_rep(">", tokenSize - #tempToken - 1) .. "}"

		--Replace this itemlink with a temporary placeholder code
		newText = string_gsub(newText, patt, tempToken, 1)

		itemLinkNum = itemLinkNum + 1
		matchPosStart, matchPosEnd = string_find(newText, patt)
	end


	--Remove the highlighting from any misspelled words.
	--i.e. When the beginning SPELLED_WRONG_HIGHLIGHT Hex coded color tag and ending |r tag wrap text.
	--Adjust the cursor position as needed.
	patt = SPELLED_WRONG_HIGHLIGHT .. "(.-)|r"

	matchPosStart, matchPosEnd = string_find(newText, patt)

	while matchPosStart ~= nil do
		--strings.gsub(input, pattern, replaceText, n=limit the number of substations to be made)
		tempText = string_gsub(newText, patt, "%1", 1)
		--tempText = string_gsub(newText, patt, function(x) return x end, 1)

		if #newText - #tempText ~= 0 then
			charsRemoved = 0
			--If the cursor was to the right of the start, subtract the num of deleted chars from the cursor position
			if cPos >= matchPosStart then
				charsRemoved = (#newText - #tempText)
			end
			--If the cursor was to the left of the end color tag and to the right of the start, add 2
			if cPos >= matchPosStart and cPos < matchPosEnd then
				charsRemoved = charsRemoved - 2
			end
			cPos = cPos - charsRemoved
		end
		newText = tempText

		matchPosStart, matchPosEnd = string_find(newText, patt)
	end

	--Remove any remaining orphaned beginning color tags.
	patt = "|[Cc]%x%x%x%x%x%x%x%x"
	matchPosStart, matchPosEnd = string_find(newText, patt)
	while matchPosStart ~= nil do
		tempText = string_gsub(newText, patt, "", 1)

		if #newText - #tempText ~= 0 then
			--If the cursor was to the right of the start, subtract the num of deleted chars from the cursor position
			if cPos >= matchPosStart then
				cPos = cPos - (#newText - #tempText)
			end
		end
		newText = tempText

		matchPosStart, matchPosEnd = string_find(newText, patt)
	end

	--Remove any remaining orphaned ending color tags.
	patt = "|r"
	matchPosStart, matchPosEnd = string_find(newText, patt)
	while matchPosStart ~= nil do
		tempText = string_gsub(newText, patt, "", 1)

		if #newText - #tempText ~= 0 then
			--If the cursor was to the right of the start, subtract the num of deleted chars from the cursor position
			if cPos >= matchPosStart then
				cPos = cPos - (#newText - #tempText)
			end
		end
		newText = tempText

		matchPosStart, matchPosEnd = string_find(newText, patt)
	end

	--Replace back the escape sequences extracted
	if #itemLinks > 0 then
		for i,val in ipairs(itemLinks) do
			newText = string_gsub(newText, "{<<" .. tostring(i) .. ">-}", val)
		end
	end


	--If by chance the tracked cursor position went negative, set it to 0
	if cPos < 0 then
		cPos = 0
	end

	
	cleanedChatMessage = newText
	--cPos should never be > #newText, unless there's some unfound error above
	return cleanedChatMessage, cPos
end

-------------------------------------------------------------------------
--
-- Custom Styled Suggestions Popup
--
-------------------------------------------------------------------------

-- Custom popup frame (created once, reused)
local CustomSuggestionsFrame = nil
local POPUP_WIDTH = 220
local POPUP_BUTTON_HEIGHT = 24
local POPUP_PADDING = 12
local POPUP_HEADER_HEIGHT = 36
local POPUP_IS_LOCKED = false -- Lock flag to prevent immediate close after showing

-- Color scheme for the custom popup
local COLORS = {
    background = {0.08, 0.08, 0.12, 0.95},
    border = {0.4, 0.45, 0.55, 0.8},
    headerBg = {0.12, 0.14, 0.20, 1},
    highlight = {0.3, 0.5, 0.8, 0.3},
    hoverBg = {0.18, 0.22, 0.32, 1},
    text = {0.95, 0.95, 0.98, 1},
    textMuted = {0.6, 0.65, 0.75, 1},
    accent = {0.49, 0.78, 0.98, 1},
    accentGlow = {0.49, 0.78, 0.98, 0.4},
    separator = {0.3, 0.35, 0.45, 0.5},
    buttonHover = {0.25, 0.35, 0.5, 0.8},
    guildTag = {0.2, 0.8, 0.4, 1},
    friendTag = {0.95, 0.75, 0.3, 1},
    actionButton = {0.15, 0.18, 0.25, 1},
}

-- Click blocker frame (prevents clicks from going through to editbox)
local ClickBlocker = nil
local function CreateClickBlocker()
	if ClickBlocker then return ClickBlocker end
	
	local blocker = CreateFrame("Frame", "MisspelledClickBlocker", UIParent)
	blocker:SetFrameStrata("FULLSCREEN")
	blocker:SetAllPoints(UIParent)
	blocker:EnableMouse(true)
	blocker:SetScript("OnMouseDown", function(self, button)
		-- Close the popup when clicking outside
		if CustomSuggestionsFrame and CustomSuggestionsFrame:IsShown() and not POPUP_IS_LOCKED then
			POPUP_IS_LOCKED = true  -- Prevent reopening
			CustomSuggestionsFrame:Hide()
			C_Timer.After(0.3, function() POPUP_IS_LOCKED = false end)
		end
	end)
	blocker:Hide()
	
	ClickBlocker = blocker
	return blocker
end

-- Create the custom suggestions frame
local function CreateCustomSuggestionsFrame()
	if CustomSuggestionsFrame then return CustomSuggestionsFrame end
	
	-- Ensure click blocker exists
	CreateClickBlocker()
    
    local frame = CreateFrame("Frame", "MisspelledCustomSuggestions", UIParent, "BackdropTemplate")
	frame:SetFrameStrata("FULLSCREEN_DIALOG")
	frame:SetFrameLevel(100)
	frame:SetClampedToScreen(true)
	frame:EnableMouse(true)
	frame:SetMovable(true)
	frame:RegisterForDrag("LeftButton")
	frame:SetScript("OnDragStart", frame.StartMoving)
	frame:SetScript("OnDragStop", frame.StopMovingOrSizing)
	frame:SetToplevel(true)
    
    -- Main backdrop with subtle shadow effect
    frame:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        edgeSize = 1,
        insets = {left = 1, right = 1, top = 1, bottom = 1}
    })
    frame:SetBackdropColor(unpack(COLORS.background))
    frame:SetBackdropBorderColor(unpack(COLORS.border))
    
	-- Outer glow/shadow frame
	local shadowFrame = CreateFrame("Frame", nil, frame, "BackdropTemplate")
	shadowFrame:SetPoint("TOPLEFT", -3, 3)
	shadowFrame:SetPoint("BOTTOMRIGHT", 3, -3)
	shadowFrame:SetFrameLevel(frame:GetFrameLevel() - 1)
	shadowFrame:SetBackdrop({
		bgFile = "Interface\\Buttons\\WHITE8X8",
		edgeFile = "Interface\\Buttons\\WHITE8X8",
		edgeSize = 3,
	})
	shadowFrame:SetBackdropColor(0, 0, 0, 0.5)
	shadowFrame:SetBackdropBorderColor(0, 0, 0, 0.4)
	shadowFrame:EnableMouse(false) -- Don't capture mouse events
	frame.shadowFrame = shadowFrame
    
    -- Header section with gradient effect
    local header = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    header:SetPoint("TOPLEFT", 1, -1)
    header:SetPoint("TOPRIGHT", -1, -1)
    header:SetHeight(POPUP_HEADER_HEIGHT)
    header:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
    })
    header:SetBackdropColor(unpack(COLORS.headerBg))
    frame.header = header
    
    -- Header accent line
    local accentLine = header:CreateTexture(nil, "OVERLAY")
    accentLine:SetPoint("TOPLEFT", 0, 0)
    accentLine:SetPoint("TOPRIGHT", 0, 0)
    accentLine:SetHeight(2)
    accentLine:SetColorTexture(unpack(COLORS.accent))
    frame.accentLine = accentLine
    
    -- Title label
    local titleLabel = header:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    titleLabel:SetPoint("TOPLEFT", POPUP_PADDING, -6)
    titleLabel:SetTextColor(unpack(COLORS.textMuted))
    titleLabel:SetText(L and L["Suggestions for:"] or "Suggestions for:")
    frame.titleLabel = titleLabel
    
    -- Misspelled word display
    local wordLabel = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    wordLabel:SetPoint("TOPLEFT", POPUP_PADDING, -18)
    wordLabel:SetTextColor(unpack(COLORS.accent))
    frame.wordLabel = wordLabel
    
    -- Close button (X)
    local closeBtn = CreateFrame("Button", nil, header)
    closeBtn:SetSize(16, 16)
    closeBtn:SetPoint("TOPRIGHT", -6, -6)
    closeBtn:SetNormalFontObject("GameFontNormal")
    local closeBtnText = closeBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    closeBtnText:SetPoint("CENTER", 0, 0)
    closeBtnText:SetText("×")
    closeBtnText:SetTextColor(unpack(COLORS.textMuted))
    closeBtn:SetScript("OnEnter", function(self)
        closeBtnText:SetTextColor(1, 0.3, 0.3, 1)
    end)
    closeBtn:SetScript("OnLeave", function(self)
        closeBtnText:SetTextColor(unpack(COLORS.textMuted))
    end)
    closeBtn:SetScript("OnClick", function()
        POPUP_IS_LOCKED = true  -- Prevent reopening
        frame:Hide()
        C_Timer.After(0.3, function() POPUP_IS_LOCKED = false end)
    end)
    frame.closeBtn = closeBtn
    
    -- Content area for suggestions
    local content = CreateFrame("Frame", nil, frame)
    content:SetPoint("TOPLEFT", 0, -POPUP_HEADER_HEIGHT)
    content:SetPoint("TOPRIGHT", 0, -POPUP_HEADER_HEIGHT)
    frame.content = content
    
    -- Pool of suggestion buttons
    frame.buttons = {}
    frame.buttonPool = {}
    
	-- Keyboard navigation state
	frame.selectedIndex = 0
	
	-- Function to update visual selection
	local function UpdateSelection(newIndex)
		local buttons = frame.buttons
		local oldIndex = frame.selectedIndex
		
		-- Clamp index
		if newIndex < 0 then newIndex = #buttons end
		if newIndex > #buttons then newIndex = 1 end
		if #buttons == 0 then newIndex = 0 end
		
		-- Remove highlight from old selection
		if oldIndex > 0 and oldIndex <= #buttons then
			local oldBtn = buttons[oldIndex]
			if oldBtn then
				oldBtn.highlight:Hide()
				if oldBtn.isAction then
					oldBtn.text:SetTextColor(unpack(COLORS.textMuted))
				else
					oldBtn.text:SetTextColor(unpack(COLORS.text))
				end
			end
		end
		
		-- Add highlight to new selection
		if newIndex > 0 and newIndex <= #buttons then
			local newBtn = buttons[newIndex]
			if newBtn then
				newBtn.highlight:Show()
				newBtn.text:SetTextColor(1, 1, 1, 1)
			end
		end
		
		frame.selectedIndex = newIndex
	end
	
	-- Handle keyboard input
	frame:SetScript("OnKeyDown", function(self, key)
		if key == "ESCAPE" then
			self:SetPropagateKeyboardInput(false)
			POPUP_IS_LOCKED = true  -- Prevent reopening
			self:Hide()
			C_Timer.After(0.3, function() POPUP_IS_LOCKED = false end)
		elseif key == "UP" then
			self:SetPropagateKeyboardInput(false)
			UpdateSelection(self.selectedIndex - 1)
		elseif key == "DOWN" then
			self:SetPropagateKeyboardInput(false)
			UpdateSelection(self.selectedIndex + 1)
		elseif key == "ENTER" or key == "SPACE" then
			self:SetPropagateKeyboardInput(false)
			if self.selectedIndex > 0 and self.selectedIndex <= #self.buttons then
				local btn = self.buttons[self.selectedIndex]
				if btn then
					btn:Click()
				end
			end
		elseif key == "TAB" then
			self:SetPropagateKeyboardInput(false)
			if IsShiftKeyDown() then
				UpdateSelection(self.selectedIndex - 1)
			else
				UpdateSelection(self.selectedIndex + 1)
			end
		else
			-- Allow number keys 1-9 for quick selection
			local num = tonumber(key)
			if num and num >= 1 and num <= 9 then
				self:SetPropagateKeyboardInput(false)
				if num <= #self.buttons then
					local btn = self.buttons[num]
					if btn and not btn.isAction then
						-- Remove the number that was typed into the editbox
						-- (editbox receives it before we can intercept)
						if RightClickedEditBox then
							C_Timer.After(0, function()
								local text = RightClickedEditBox:GetText()
								local pos = RightClickedEditBox:GetCursorPosition()
								-- Remove the digit that was just typed
								if pos > 0 and text:sub(pos, pos):match("%d") then
									local newText = text:sub(1, pos - 1) .. text:sub(pos + 1)
									RightClickedEditBox:SetText(newText)
									RightClickedEditBox:SetCursorPosition(pos - 1)
								end
							end)
						end
						btn:Click()
					end
				end
			else
				-- User pressed a letter key - they want to type manually
				-- Close popup and let them edit freely
				self:SetPropagateKeyboardInput(true)
				POPUP_IS_LOCKED = true
				self:Hide()
				-- Clear stored word positions since user is manually editing
				RightClickedWord = nil
				RightClickedWordStartPos = nil
				RightClickedWordEndPos = nil
				C_Timer.After(0.3, function() POPUP_IS_LOCKED = false end)
			end
		end
	end)
	
	-- NOTE: NOT registering with UISpecialFrames to avoid automatic ESC closing
	-- which might interfere with click handling
	
	-- OnShow handler
	frame:SetScript("OnShow", function(self)
		-- Lock is already set before Show() is called
		POPUP_IS_LOCKED = true
		
		-- Show click blocker after a delay to avoid catching the opening click
		C_Timer.After(0.15, function()
			if self:IsShown() and ClickBlocker then
				ClickBlocker:Show()
			end
		end)
		
		-- Unlock after a longer delay
		C_Timer.After(0.7, function()
			POPUP_IS_LOCKED = false
		end)
		
		-- Delay keyboard enable to avoid stealing focus
		C_Timer.After(0.2, function()
			if self:IsShown() then
				self:EnableKeyboard(true)
				self.selectedIndex = 0
				if #self.buttons > 0 then
					UpdateSelection(1)
				end
			end
		end)
	end)
	
	frame:SetScript("OnHide", function(self)
		-- Note: POPUP_IS_LOCKED is managed by button handlers to prevent reopening
		-- Don't reset it here - buttons set a brief lock then release after delay
		
		-- Hide click blocker
		if ClickBlocker then
			ClickBlocker:Hide()
		end
		self:EnableKeyboard(false)
		self.selectedIndex = 0
		-- Return buttons to pool
		for _, btn in ipairs(self.buttons) do
			btn:Hide()
			table_insert(self.buttonPool, btn)
		end
		wipe(self.buttons)
	end)
	
	frame.UpdateSelection = UpdateSelection
    
    CustomSuggestionsFrame = frame
    return frame
end

-- Create or get a suggestion button from the pool
local function GetSuggestionButton(parent, index)
    local frame = CustomSuggestionsFrame
    local btn = tremove(frame.buttonPool)
    
    if not btn then
        btn = CreateFrame("Button", nil, parent, "BackdropTemplate")
        btn:SetHeight(POPUP_BUTTON_HEIGHT)
        btn:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
        })
        btn:SetBackdropColor(0, 0, 0, 0)
        
        -- Button text
        local text = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        text:SetPoint("LEFT", POPUP_PADDING, 0)
        text:SetTextColor(unpack(COLORS.text))
        btn.text = text
        
        -- Tag text (Guild/Friend)
        local tag = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        tag:SetPoint("RIGHT", -POPUP_PADDING, 0)
        btn.tag = tag
        
        -- Highlight texture
        local highlight = btn:CreateTexture(nil, "BACKGROUND")
        highlight:SetAllPoints()
        highlight:SetColorTexture(unpack(COLORS.buttonHover))
        highlight:Hide()
        btn.highlight = highlight
        
        -- Hover animations
        btn:SetScript("OnEnter", function(self)
            self.highlight:Show()
            self.text:SetTextColor(1, 1, 1, 1)
            if self.tag:IsShown() then
                -- Keep tag color
            end
        end)
        
        btn:SetScript("OnLeave", function(self)
            self.highlight:Hide()
            if self.isAction then
                self.text:SetTextColor(unpack(COLORS.textMuted))
            else
                self.text:SetTextColor(unpack(COLORS.text))
            end
        end)
    end
    
    btn:SetParent(parent)
    btn:Show()
    return btn
end

-- Create a separator line
local function CreateSeparator(parent, yOffset)
    local sep = parent:CreateTexture(nil, "OVERLAY")
    sep:SetPoint("TOPLEFT", 10, yOffset)
    sep:SetPoint("TOPRIGHT", -10, yOffset)
    sep:SetHeight(1)
    sep:SetColorTexture(unpack(COLORS.separator))
    return sep
end

-- Show the custom suggestions popup
function Misspelled:ShowCustomSuggestionsPopup(word, suggestions, editbox)
	local frame = CreateCustomSuggestionsFrame()
	
	-- Update word display
	frame.wordLabel:SetText(word)
	
	-- Calculate required height
	local numSuggestions = suggestions and #suggestions or 0
	local hasNoSuggestions = (numSuggestions == 0)
	
	local contentHeight = 10
	if hasNoSuggestions then
		contentHeight = contentHeight + POPUP_BUTTON_HEIGHT + 10 -- "No suggestions" message
	else
		contentHeight = contentHeight + (numSuggestions * POPUP_BUTTON_HEIGHT) -- suggestions
	end
	contentHeight = contentHeight + 12 -- spacing before actions
	contentHeight = contentHeight + (3 * POPUP_BUTTON_HEIGHT) -- action buttons (Ignore, Add, Cancel)
	contentHeight = contentHeight + 20 -- keyboard hint footer
	contentHeight = contentHeight + POPUP_PADDING
    
    local totalHeight = POPUP_HEADER_HEIGHT + contentHeight
    frame:SetSize(POPUP_WIDTH, totalHeight)
    frame.content:SetHeight(contentHeight)
    
	-- Clear previous content (separators, no-suggestions label, etc.)
	for _, child in pairs({frame.content:GetRegions()}) do
		if child:GetObjectType() == "Texture" or child:GetObjectType() == "FontString" then
			child:Hide()
		end
	end
	
	-- Show/hide no suggestions label
	if frame._noSugLabel then
		if hasNoSuggestions then
			frame._noSugLabel:Show()
		else
			frame._noSugLabel:Hide()
		end
	end
    
    -- Position near cursor
    local cursorX, cursorY = GetCursorPosition()
    local scale = UIParent:GetEffectiveScale()
    cursorX, cursorY = cursorX / scale, cursorY / scale
    
    -- Adjust position to stay on screen
    local screenWidth = GetScreenWidth()
    local screenHeight = GetScreenHeight()
    
    local posX = cursorX + 10
    local posY = cursorY - 10
    
    if posX + POPUP_WIDTH > screenWidth then
        posX = cursorX - POPUP_WIDTH - 10
    end
    if posY - totalHeight < 0 then
        posY = totalHeight + 10
    end
    
    frame:ClearAllPoints()
    frame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", posX, posY)
    
	-- Create suggestion buttons
	local yOffset = -6
	local buttonIndex = 0
	
	-- Show "no suggestions" message if empty
	if hasNoSuggestions then
		local noSugLabel = frame.content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
		noSugLabel:SetPoint("TOPLEFT", POPUP_PADDING, yOffset - 8)
		noSugLabel:SetTextColor(0.5, 0.55, 0.65, 1)
		noSugLabel:SetText("No suggestions found")
		frame._noSugLabel = noSugLabel
		yOffset = yOffset - POPUP_BUTTON_HEIGHT - 10
	else
		-- Hide previous "no suggestions" label if exists
		if frame._noSugLabel then
			frame._noSugLabel:Hide()
		end
	end
	
	-- Add suggestion word buttons
	for i, s in ipairs(suggestions or {}) do
		buttonIndex = buttonIndex + 1
		local btn = GetSuggestionButton(frame.content, buttonIndex)
		btn:SetPoint("TOPLEFT", 4, yOffset)
		btn:SetPoint("TOPRIGHT", -4, yOffset)
		btn.isAction = false
		
		-- Add number hint for keyboard shortcut (1-9)
		local displayText = s.Word
		if i <= 9 then
			displayText = "|cff666677" .. i .. ".|r  " .. s.Word
		end
		btn.text:SetText(displayText)
		btn.text:SetTextColor(unpack(COLORS.text))
        
        -- Show guild/friend tag
        if Misspelled:IsGuildMember(s.Word) then
            btn.tag:SetText(L and L["(Guild)"] or "(Guild)")
            btn.tag:SetTextColor(unpack(COLORS.guildTag))
            btn.tag:Show()
        elseif Misspelled:IsFriend(s.Word) then
            btn.tag:SetText(L and L["(Friend)"] or "(Friend)")
            btn.tag:SetTextColor(unpack(COLORS.friendTag))
            btn.tag:Show()
        else
            btn.tag:Hide()
        end
        
        local suggWord = s.Word
        btn:SetScript("OnClick", function()
            POPUP_IS_LOCKED = true  -- Prevent reopening
            frame:Hide()
            SuggestionsFrame_Click(suggWord, editbox)
            C_Timer.After(0.3, function() POPUP_IS_LOCKED = false end)
        end)
        
        table_insert(frame.buttons, btn)
        yOffset = yOffset - POPUP_BUTTON_HEIGHT
    end
    
    -- Add separator before action buttons
    yOffset = yOffset - 8
    CreateSeparator(frame.content, yOffset)
    yOffset = yOffset - 8
    
    -- Action: Ignore All
    buttonIndex = buttonIndex + 1
    local ignoreBtn = GetSuggestionButton(frame.content, buttonIndex)
    ignoreBtn:SetPoint("TOPLEFT", 4, yOffset)
    ignoreBtn:SetPoint("TOPRIGHT", -4, yOffset)
    ignoreBtn.isAction = true
    ignoreBtn.text:SetText("⊘  " .. (L and L["Ignore All"] or "Ignore All"))
    ignoreBtn.text:SetTextColor(unpack(COLORS.textMuted))
    ignoreBtn.tag:Hide()
    ignoreBtn:SetScript("OnClick", function()
        POPUP_IS_LOCKED = true  -- Prevent reopening
        frame:Hide()
        SuggestionsFrame_Click("###IgnoreAll", editbox)
        C_Timer.After(0.3, function() POPUP_IS_LOCKED = false end)
    end)
    table_insert(frame.buttons, ignoreBtn)
    yOffset = yOffset - POPUP_BUTTON_HEIGHT
    
    -- Action: Add to Dictionary
    buttonIndex = buttonIndex + 1
    local addBtn = GetSuggestionButton(frame.content, buttonIndex)
    addBtn:SetPoint("TOPLEFT", 4, yOffset)
    addBtn:SetPoint("TOPRIGHT", -4, yOffset)
    addBtn.isAction = true
    addBtn.text:SetText("＋  " .. (L and L["Add to Dictionary"] or "Add to Dictionary"))
    addBtn.text:SetTextColor(unpack(COLORS.textMuted))
    addBtn.tag:Hide()
    addBtn:SetScript("OnClick", function()
        POPUP_IS_LOCKED = true  -- Prevent reopening
        frame:Hide()
        SuggestionsFrame_Click("###AddToDictionary", editbox)
        C_Timer.After(0.3, function() POPUP_IS_LOCKED = false end)
    end)
    table_insert(frame.buttons, addBtn)
    yOffset = yOffset - POPUP_BUTTON_HEIGHT
    
	-- Action: Cancel
	buttonIndex = buttonIndex + 1
	local cancelBtn = GetSuggestionButton(frame.content, buttonIndex)
	cancelBtn:SetPoint("TOPLEFT", 4, yOffset)
	cancelBtn:SetPoint("TOPRIGHT", -4, yOffset)
	cancelBtn.isAction = true
	cancelBtn.text:SetText("✕  " .. (L and L["Cancel"] or "Cancel"))
	cancelBtn.text:SetTextColor(unpack(COLORS.textMuted))
	cancelBtn.tag:Hide()
	cancelBtn:SetScript("OnClick", function()
		POPUP_IS_LOCKED = true  -- Prevent reopening
		frame:Hide()
		C_Timer.After(0.3, function() POPUP_IS_LOCKED = false end)
	end)
	table_insert(frame.buttons, cancelBtn)
	yOffset = yOffset - POPUP_BUTTON_HEIGHT
	
	-- Keyboard hint footer
	yOffset = yOffset - 6
	if not frame._keyboardHint then
		local hint = frame.content:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
		hint:SetTextColor(0.4, 0.45, 0.55, 0.8)
		hint:SetText("↑↓ Navigate • Enter Select • 1-9 Quick Pick")
		frame._keyboardHint = hint
	end
	frame._keyboardHint:ClearAllPoints()
	frame._keyboardHint:SetPoint("BOTTOMLEFT", frame.content, "BOTTOMLEFT", POPUP_PADDING, 6)
	frame._keyboardHint:Show()
	
	-- Lock BEFORE showing to prevent race conditions with duplicate click events
	POPUP_IS_LOCKED = true
	
	-- Show the frame (simple, no animation that could cause issues)
	frame:SetAlpha(1)
	frame:Show()
	frame:Raise() -- Ensure it's on top
	
	-- Play a subtle sound
	PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON or 856)
end

-------------------------------------------------------------------------
--
-- Routines for the right click misspelled suggestions popup.
--
-------------------------------------------------------------------------
function Misspelled.EditBox_OnMouseDown(editbox, button)
	-- Intentionally no-op: we trigger menus on mouse-up to avoid double-firing
	-- (both OnMouseDown and OnMouseUp can fire for the same click).
end

-- Compatibility wrapper: many hook call sites expect a plain function named Misspelled.EditBox_OnMouseUp
function Misspelled.EditBox_OnMouseUp(editbox, button)
	return Misspelled:OnMouseClick(editbox, button)
end

function Misspelled:ShowSuggestionsMenu()
	-- Basic validation
	if not RightClickedWord or not RightClickedEditBox then
		return
	end
	
	-- Use the new custom styled popup
	local suggestions = WordCache[RightClickedWord] and WordCache[RightClickedWord].Suggestions or {}
	Misspelled:ShowCustomSuggestionsPopup(RightClickedWord, suggestions, RightClickedEditBox)
end

-- Legacy fallback function (kept for compatibility but redirects to custom popup)
function Misspelled:ShowSuggestionsMenuLegacy()
	local title = (L and L["Suggestions for:"] or "Suggestions for:") .. " " .. (RightClickedWord or "")

	local function buildMenuList()
		local menu = {}
		table_insert(menu, { text = title, isTitle = true, notCheckable = true })

		for _, s in ipairs(WordCache[RightClickedWord].Suggestions or {}) do
			local text = s.Word
			if Misspelled:IsGuildMember(s.Word) == true then
				text = text .. " " .. (L and L["(Guild)"] or "(Guild)")
			elseif Misspelled:IsFriend(s.Word) == true then
				text = text .. " " .. (L and L["(Friend)"] or "(Friend)")
			end
			table_insert(menu, {
				text = text,
				notCheckable = true,
				func = function() SuggestionsFrame_Click(s.Word, RightClickedEditBox) end
			})
		end

		table_insert(menu, { text = "", notClickable = true, notCheckable = true })
		table_insert(menu, { text = (L and L["Ignore All"] or "Ignore All"), notCheckable = true, func = function() SuggestionsFrame_Click("###IgnoreAll", RightClickedEditBox) end })
		table_insert(menu, { text = (L and L["Add to Dictionary"] or "Add to Dictionary"), notCheckable = true, func = function() SuggestionsFrame_Click("###AddToDictionary", RightClickedEditBox) end })
		table_insert(menu, { text = (L and L["Cancel"] or "Cancel"), notCheckable = true, func = function() end })
		return menu
	end

	-- Prefer modern context menu API when available (11.x+).
	if MenuUtil and MenuUtil.CreateContextMenu then
		local ok = pcall(function()
			MenuUtil.CreateContextMenu(UIParent, function(_, rootDescription)
				-- Defensive: API has evolved; if these methods are missing we fall back.
				if not (rootDescription and rootDescription.CreateTitle and rootDescription.CreateButton) then
					error("MenuUtil rootDescription missing expected methods")
				end

				rootDescription:CreateTitle(title)
				for _, s in ipairs(WordCache[RightClickedWord].Suggestions or {}) do
					local text = s.Word
					if Misspelled:IsGuildMember(s.Word) == true then
						text = text .. " " .. (L and L["(Guild)"] or "(Guild)")
					elseif Misspelled:IsFriend(s.Word) == true then
						text = text .. " " .. (L and L["(Friend)"] or "(Friend)")
					end
					rootDescription:CreateButton(text, function()
						SuggestionsFrame_Click(s.Word, RightClickedEditBox)
					end)
				end
				if rootDescription.CreateDivider then
					rootDescription:CreateDivider()
				end
				rootDescription:CreateButton((L and L["Ignore All"] or "Ignore All"), function()
					SuggestionsFrame_Click("###IgnoreAll", RightClickedEditBox)
				end)
				rootDescription:CreateButton((L and L["Add to Dictionary"] or "Add to Dictionary"), function()
					SuggestionsFrame_Click("###AddToDictionary", RightClickedEditBox)
				end)
				rootDescription:CreateButton((L and L["Cancel"] or "Cancel"), function() end)
			end)
		end)

		if ok then
			return
		end
	end

	-- Fallback #1: EasyMenu (still widely available on retail).
	if EasyMenu and CreateFrame then
		if _G.Misspelled_EasyMenuFrame == nil then
			_G.Misspelled_EasyMenuFrame = CreateFrame("Frame", "Misspelled_EasyMenuFrame", UIParent, "UIDropDownMenuTemplate")
		end
		local ok = pcall(function()
			EasyMenu(buildMenuList(), _G.Misspelled_EasyMenuFrame, "cursor", 0, 0, "MENU")
		end)
		if ok then
			return
		end
	end

	-- Fallback #2: legacy dropdown menu if present.
	if ToggleDropDownMenu and MisspelledSuggestions_DropDown then
		pcall(function()
			ToggleDropDownMenu(1, nil, MisspelledSuggestions_DropDown, "cursor")
		end)
	end
end

-- Build a "visible text" version of a chat editbox string and a mapping back to raw indices.
-- This is needed because EditBox:GetCursorPosition() counts only visible characters (not |c...|r, |H...|h, |T...|t, etc.).
function Misspelled:_BuildVisibleTextMap(rawText)
	if not rawText or #rawText == 0 then
		return "", {}, {}
	end

	local vis = {}
	local mapStart = {} -- visibleIndex -> raw start index
	local mapLen = {}   -- visibleIndex -> raw byte length consumed for this visible char

	local i = 1
	local v = 0
	local n = #rawText

	while i <= n do
		local c = string_sub(rawText, i, i)

		-- Escaped pipe: "||" displays as "|"
		if c == "|" and i < n and string_sub(rawText, i + 1, i + 1) == "|" then
			v = v + 1
			vis[v] = "|"
			mapStart[v] = i
			mapLen[v] = 2
			i = i + 2

		-- Color start: |cAARRGGBB
		elseif c == "|" and i + 9 <= n and (string_sub(rawText, i + 1, i + 1) == "c" or string_sub(rawText, i + 1, i + 1) == "C") then
			-- skip "|c" + 8 hex
			i = i + 10

		-- Color end: |r
		elseif c == "|" and i < n and string_sub(rawText, i + 1, i + 1) == "r" then
			i = i + 2

		-- Hyperlink chunk: |H...|h   (hidden)
		elseif c == "|" and i < n and string_sub(rawText, i + 1, i + 1) == "H" then
			local s, e = string_find(rawText, "|H.-|h", i)
			if s == i and e then
				i = e + 1
			else
				-- fall back to treating as a visible char
				v = v + 1
				vis[v] = c
				mapStart[v] = i
				mapLen[v] = 1
				i = i + 1
			end

		-- Hyperlink terminator: |h (hidden)
		elseif c == "|" and i < n and string_sub(rawText, i + 1, i + 1) == "h" then
			i = i + 2

		-- Texture: |T...|t (hidden)
		elseif c == "|" and i < n and string_sub(rawText, i + 1, i + 1) == "T" then
			local s, e = string_find(rawText, "|T.-|t", i)
			if s == i and e then
				i = e + 1
			else
				v = v + 1
				vis[v] = c
				mapStart[v] = i
				mapLen[v] = 1
				i = i + 1
			end

		-- Texture atlas: |A...|a (hidden)
		elseif c == "|" and i < n and string_sub(rawText, i + 1, i + 1) == "A" then
			local s, e = string_find(rawText, "|A.-|a", i)
			if s == i and e then
				i = e + 1
			else
				v = v + 1
				vis[v] = c
				mapStart[v] = i
				mapLen[v] = 1
				i = i + 1
			end

		else
			v = v + 1
			vis[v] = c
			mapStart[v] = i
			mapLen[v] = 1
			i = i + 1
		end
	end

	return table.concat(vis), mapStart, mapLen
end

function Misspelled:_GetWordAtCursor(editbox)
	if not editbox or not editbox.GetText or not editbox.GetCursorPosition then return end

	local rawText = editbox:GetText() or ""
	if #rawText == 0 then return end

	-- CursorPosition is 0-based in many editboxes; treat as visible index between characters.
	local cPos = editbox:GetCursorPosition() or 0

	local visText, mapStart, mapLen = self:_BuildVisibleTextMap(rawText)
	if not visText or #visText == 0 then return end

	-- Convert to 1-based visible character index; clamp into the string.
	local vPos = cPos
	if vPos < 1 then vPos = 1 end
	if vPos > #visText then vPos = #visText end

	local patt = "[A-Za-z0-9_'À-ÿœæŒÆ]"
	if string_match(string_sub(visText, vPos, vPos), patt) == nil then
		-- If the cursor isn't on a word char, try the char just to the left (common for cursor being "between" chars).
		if vPos > 1 and string_match(string_sub(visText, vPos - 1, vPos - 1), patt) ~= nil then
			vPos = vPos - 1
		else
			return
		end
	end

	local vStart = vPos
	while vStart > 1 and string_match(string_sub(visText, vStart - 1, vStart - 1), patt) ~= nil do
		vStart = vStart - 1
	end

	local vEnd = vPos
	while vEnd < #visText and string_match(string_sub(visText, vEnd + 1, vEnd + 1), patt) ~= nil do
		vEnd = vEnd + 1
	end

	local word = string_sub(visText, vStart, vEnd)
	if not word or #word == 0 then return end

	local rawStart = mapStart[vStart]
	local rawEnd = mapStart[vEnd] + (mapLen[vEnd] or 1) - 1

	return word, rawStart, rawEnd
end

function Misspelled:OnMouseClick(editbox, button)
	-- Click on a highlighted misspelled word opens the suggestion menu.
	if button == "LeftButton" or button == "RightButton" then
		local isRightClick = (button == "RightButton")
		local badWordFound = false
		
		-- If popup is already shown, close it (always allowed, even when locked)
		if CustomSuggestionsFrame and CustomSuggestionsFrame:IsShown() then
			POPUP_IS_LOCKED = true  -- Prevent reopening
			CustomSuggestionsFrame:Hide()
			C_Timer.After(0.3, function() POPUP_IS_LOCKED = false end)
			return
		end
		
		-- If popup is locked (just closed or just shown), don't open a new one
		if POPUP_IS_LOCKED then
			return
		end
		
		CloseDropDownMenus()

		--check if we are positioned on a misspelled word
		local pos = editbox:GetCursorPosition()
		if WordLocations[editbox:GetName()] ~= nil then
			for i, w in ipairs(WordLocations[editbox:GetName()]) do
				if	pos >= w.StartPos and pos <= w.EndPos then
					if WordCache[w.Word].Correct == false then
						--If not cached, lookup Suggestions for the misspelled word
						if WordCache[w.Word].Suggestions == nil then
							WordCache[w.Word].Suggestions = WordDict:Suggest(w.Word)
						end

						RightClickedWord = w.Word
						RightClickedWordStartPos = w.StartPos
						RightClickedWordEndPos = w.EndPos

						RightClickedEditBox = editbox

						badWordFound = true

						Misspelled:ShowSuggestionsMenu()
					else
						RightClickedWord = nil
					end
					break
				end
			end
		end

		--Show where the user right clicked.
		--print("cursor position:" , ChatFrameEditBox:GetCursorPosition())

		--If we are not positioned on a bad, word call and MouseUp handler that was hooked to the box.
		-- Fallback: On modern retail, cursor positions can ignore hidden sequences (color/link tags),
		-- which can cause the WordLocations (raw indices) to not match GetCursorPosition().
		-- If that happens, derive the word under the cursor from a visible-text mapping.
		if badWordFound == false then
			local word, rawStart, rawEnd = self:_GetWordAtCursor(editbox)
			if word and rawStart and rawEnd then
				local cacheKey = word
				if WordCache[cacheKey] == nil then
					cacheKey = string_lower(word)
				end
				if WordCache[cacheKey] and WordCache[cacheKey].Correct == false then
					if WordCache[cacheKey].Suggestions == nil then
						WordCache[cacheKey].Suggestions = WordDict:Suggest(cacheKey)
					end
					RightClickedWord = cacheKey
					RightClickedWordStartPos = rawStart
					RightClickedWordEndPos = rawEnd
					RightClickedEditBox = editbox
					badWordFound = true
					Misspelled:ShowSuggestionsMenu()
				end
			end
		end

		-- Last-resort fallback: only for right-click.
		-- If we still can't map the click to a specific word, just open suggestions for the first misspelled
		-- word on the line. We do NOT do this for left-click, otherwise normal cursor moves would pop menus.
		if badWordFound == false and isRightClick then
			local wl = WordLocations[editbox:GetName()]
			if wl then
				for _, w in ipairs(wl) do
					if WordCache[w.Word] and WordCache[w.Word].Correct == false then
						if WordCache[w.Word].Suggestions == nil then
							WordCache[w.Word].Suggestions = WordDict:Suggest(w.Word)
						end
						RightClickedWord = w.Word
						RightClickedWordStartPos = w.StartPos
						RightClickedWordEndPos = w.EndPos
						RightClickedEditBox = editbox
						badWordFound = true
						Misspelled:ShowSuggestionsMenu()
						break
					end
				end
			end
		end
	end
end

function MisspelledSuggestions_InitializeDropDown(level)
	if RightClickedWord == nil then return end
	if #RightClickedWord == 0 then return end

	do
	  local info = UIDropDownMenu_CreateInfo()
	info.text = L["Suggestions for:"] .. " " .. RightClickedWord
	info.isTitle = 1
	info.notClickable = 1
	info.notCheckable = true
	UIDropDownMenu_AddButton(info)
	end

	--Add suggestions to the DropDown
	for i, s in ipairs(WordCache[RightClickedWord].Suggestions) do
		do
			local info = UIDropDownMenu_CreateInfo()
                --Line below causes a this == nil error in 4.0.  Looks like it's not needed.
		--info.owner = this:GetParent()

		--If the misspelled word's first
		info.text = s.Word

		--If this suggestion is either a guild member or friend append a note.
		if Misspelled:IsGuildMember(s.Word) == true then
			info.text = info.text .. " " .. L["(Guild)"]
		else
			if Misspelled:IsFriend(s.Word) == true then
				info.text = info.text .. " " .. L["(Friend)"]
			end
		end

		info.isTitle = nil
		info.notCheckable = true
		info.value = s.Word
		info.func = function() SuggestionsFrame_Click(s.Word, RightClickedEditBox) end
		--Add the above info to the options menu as clickable item
		UIDropDownMenu_AddButton(info)
		end
	end

	do
		--Add a non-clickable separator
		local info = UIDropDownMenu_CreateInfo()
	--info.owner = this:GetParent()
	info.text = ""
	info.isTitle = nil
	info.value = ""
	info.notClickable = 1
	info.notCheckable = true
	UIDropDownMenu_AddButton(info)
	end

	do
		local info = UIDropDownMenu_CreateInfo()
	--info.owner = this:GetParent()
	info.text = L["Ignore All"]
	info.isTitle = nil
	info.value = RightClickedWord
	info.func = function() SuggestionsFrame_Click("###IgnoreAll", RightClickedEditBox) end
	info.notClickable = nil
	info.notCheckable = true
	UIDropDownMenu_AddButton(info)
	end

	do
		local info = UIDropDownMenu_CreateInfo()
	--info.owner = this:GetParent()
	info.text = L["Add to Dictionary"]
	info.isTitle = nil
	info.value = RightClickedWord
	info.func = function() SuggestionsFrame_Click("###AddToDictionary", RightClickedEditBox) end
	info.notClickable = nil
	info.notCheckable = true
	UIDropDownMenu_AddButton(info)
	end

	do
		local info = UIDropDownMenu_CreateInfo()
	--info.owner = this:GetParent()
	info.text = L["Cancel"]
	info.isTitle = nil
	info.value = nil
	info.notClickable = nil
	info.notCheckable = true
	UIDropDownMenu_AddButton(info)
	end
end

function MisspelledSuggestions_DropDownOnLoad(self)
	UIDropDownMenu_Initialize(self, MisspelledSuggestions_InitializeDropDown, "MENU")
end

function SuggestionsFrame_Click(value, editbox)
	--value will equal the suggestion word clicked,
	--or a "special" tag, for the 'Ignore' and 'Add to Dictionary' functions

	--print("Suggestion Clicked: ", value)

	local newText = editbox:GetText()
	local newCursorPos = nil

	local isCapitalizedRightClickedWord = false

	--Check if the local (global) var RightClickedWord is populated with a non nil value
    assert(RightClickedWord ~= nil, "Misspelled: SuggestionsFrame_Click, Unexpected: RightClickedWord == nil")

	if string_sub(RightClickedWord, 1, 1) == string_upper(string_sub(RightClickedWord, 1, 1)) then
		isCapitalizedRightClickedWord = true
	end

	if value == "###IgnoreAll" then
		--Add this word to the WordCache so it will be ignored as misspelled until you reload
		--print("Ignore:", RightClickedWord)
		WordCache[RightClickedWord].Correct = true
		WordCache[RightClickedWord].Suggestions = {}

		--Remove the misspelled highlighting
		newText = string_sub(newText, 1, RightClickedWordStartPos - 1 - #SPELLED_WRONG_HIGHLIGHT) .. RightClickedWord .. string_sub(newText, RightClickedWordEndPos + #FONT_COLOR_CODE_CLOSE + 1)
		newCursorPos = RightClickedWordStartPos + #RightClickedWord - #SPELLED_WRONG_HIGHLIGHT - #FONT_COLOR_CODE_CLOSE + 1

	elseif value == "###AddToDictionary" then
		-- Add this word to the user dictionary
		--print("Add to UserDict", RightClickedWord)
		Misspelled:AddToUserDict(RightClickedWord)
		WordCache[RightClickedWord].Correct = true
		WordCache[RightClickedWord].Suggestions = {}

		--Remove the misspelled highlighting
		newText = string_sub(newText, 1, RightClickedWordStartPos - 1 - #SPELLED_WRONG_HIGHLIGHT) .. RightClickedWord .. string_sub(newText, RightClickedWordEndPos + #FONT_COLOR_CODE_CLOSE + 1)
		newCursorPos = RightClickedWordStartPos + #RightClickedWord - #SPELLED_WRONG_HIGHLIGHT - #FONT_COLOR_CODE_CLOSE + 1

	else
		--replace this word with the selected suggestion.
		--Remove the misspelled highlighting in the process.
		--
		--If the misspelled word was capitalized, capitalize the replacement.
		if isCapitalizedRightClickedWord == true then
			value = string_upper(string_sub(value, 1, 1)) .. string_sub(value, 2)
		end
		newText = string_sub(newText, 1, RightClickedWordStartPos - 1 - #SPELLED_WRONG_HIGHLIGHT) .. value .. string_sub(newText, RightClickedWordEndPos + #FONT_COLOR_CODE_CLOSE + 1)

		--save the cursor position just after the corrected word, so we can set it later
		newCursorPos = RightClickedWordStartPos + #value - #SPELLED_WRONG_HIGHLIGHT - #FONT_COLOR_CODE_CLOSE + 1

		--Note: if the replacement word is a different length, then the WordLocations will be messed up.
	end

	editbox:SetText(newText)

	--printable = gsub(newText, "\124", "\124\124")  --\124 == "|"
    --print("New ChatText:", printable)

	--If we replaced a word, with a suggestion, move the cursor to the end of the new word.
	if newCursorPos ~= nil then
		--Check if the character to the right of the cursor is a space.  If so adjust the cursor to the right by 1 char.
		if #newText > newCursorPos then
			if string_sub(newText, newCursorPos + 1, newCursorPos + 1) == " " then
				newCursorPos = newCursorPos + 1
			end
		end

		editbox:SetCursorPosition(newCursorPos)
	end

	-- Clear all stored word data after correction
	RightClickedWord = nil
	RightClickedWordStartPos = nil
	RightClickedWordEndPos = nil
	
	-- Clear WordLocations for this editbox since positions have changed
	WordLocations[editbox:GetName()] = {}
end
-----------------------------------------------------
-- End: Right click Suggestions popup
-----------------------------------------------------



-------------------------------------------------------------------------
--
-- Routines for dealing with the user dictionary
--
-------------------------------------------------------------------------

--Load the words saved in the Users dictionary into the baseWords table.
--In r18 we changed the in memory format used to store the baseWords, affixCode and PhoneticCode,
--Saved a ton of memory not using a sub-table per baseWord.
--If necessary convert the user dictionary storage to match the newer format.
function Misspelled:LoadUserDict()
	if Misspelled_DB == nil then
		Misspelled_DB = {UserDict = {}}
	end

	local affixKeys, pCode
	if Misspelled_DB.UserDict ~= nil then
		for k, v in pairs(Misspelled_DB.UserDict) do
			--If needed convert the user dictionary format to the, post r18 format.
			if type(v) == "table" then
				affixKeys = v[1]
				pCode = v[2]
				if affixKeys == nil then affixKeys = "" end
				if pCode == nil then pCode = "" end
				v = affixKeys .. "/" .. pCode
				Misspelled_DB.UserDict[k] = v
			end

			if WordDict.baseWords[k] == nil then
				WordDict.baseWords[k] = v
			end
		end
	end
end


--Add a new word to the users dictionary, and the currently loaded baseWords table.
--Store both the word and it's phonetic code.
function Misspelled:AddToUserDict(word)
	if word == nil then return end
	if #word == 0 then return end

	if Misspelled_DB.UserDict == nil then
		Misspelled_DB.UserDict = {}
	end
	--Add the new word to the UserDict saved variable
	local pcode = WordDict:PhoneticCode(word)
	Misspelled_DB.UserDict[word] = "/" .. pcode

	--And add it to the currently loaded dictionary
	WordDict.baseWords[word] = "/" .. pcode

	--Fixup the WordCache
	WordCache[word].Correct = true
	WordCache[word].Suggestions = {}
end


local Misspelled_Words_To_Delete = {}

-- Enhanced User Dictionary Editor with improved styling
function Misspelled:EditUserDict()
	local gui = AceGUI

	local f = AceGUI:Create("Window")
	f:SetCallback("OnClose",function(widget, event) AceGUI:Release(widget) end )
	f:SetLayout("Flow")
	f:SetWidth(340)
	f:SetHeight(520)
	f:SetTitle("Misspelled - " .. L["User Dictionary"])
	f:ReleaseChildren()
	f:PauseLayout()
	
	-- Try to style the window frame if possible
	local frame = f.frame
	if frame and frame.SetBackdrop then
		pcall(function()
			frame:SetBackdrop({
				bgFile = "Interface\\Buttons\\WHITE8X8",
				edgeFile = "Interface\\Buttons\\WHITE8X8",
				edgeSize = 1,
			})
			frame:SetBackdropColor(0.08, 0.08, 0.12, 0.98)
			frame:SetBackdropBorderColor(0.4, 0.45, 0.55, 0.8)
		end)
	end

	Misspelled_Words_To_Delete = {}
	
	-- Word count label
	local wordCount = 0
	if Misspelled_DB and Misspelled_DB.UserDict then
		for _ in pairs(Misspelled_DB.UserDict) do
			wordCount = wordCount + 1
		end
	end
	
	local countLabel = gui:Create("Label")
	countLabel:SetFullWidth(true)
	countLabel:SetText("|cff7dc6fb" .. wordCount .. "|r words in your dictionary")
	countLabel:SetFont("Fonts\\FRIZQT__.TTF", 11, "")
	f:AddChild(countLabel)
	
	-- Spacer
	local spacer1 = gui:Create("Label")
	spacer1:SetFullWidth(true)
	spacer1:SetText(" ")
	f:AddChild(spacer1)

	local i = gui:Create("InlineGroup")
	i:SetLayout("List")
	i:SetFullWidth(true)
	i:SetHeight(370)
	i:SetTitle("|cffcccccc" .. L["Select words to remove:"] .. "|r")
	f:AddChild(i)

	local scroll = AceGUI:Create("ScrollFrame")
	scroll:SetLayout("Flow")
	scroll:SetFullWidth(true)
	scroll:SetHeight(370)
	i:AddChild(scroll)
	
	-- Selection counter
	local selectionLabel = gui:Create("Label")
	selectionLabel:SetFullWidth(true)
	selectionLabel:SetText(" ")
	f:AddChild(selectionLabel)
	
	local function UpdateSelectionLabel()
		local count = 0
		for _ in pairs(Misspelled_Words_To_Delete) do
			count = count + 1
		end
		if count > 0 then
			selectionLabel:SetText("|cffff6b6b" .. count .. " word(s) selected for removal|r")
		else
			selectionLabel:SetText("|cff888888Click words to select them|r")
		end
	end
	UpdateSelectionLabel()

	local delButton = AceGUI:Create("Button")
	delButton:SetText("|cffff6b6b✕|r  Delete Selected")
	delButton:SetWidth(160)
	delButton:SetCallback("OnClick", function()
		PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON or 856)
		--Delete selected words from the user dictionary
		Misspelled:print("|cff7dc6fbMisspelled:|r " .. L["Removing the following words from the user dictionary"])
		for k,v in pairs(Misspelled_Words_To_Delete) do
			Misspelled:print("  |cffff6b6b-|r " .. k)
			Misspelled_DB.UserDict[k] = nil

			--Remove the word from the, in memory dictionary
			WordDict.baseWords[k] = nil
		end

		--Clear the word cache
		WordCache = {}
		WordCacheCount = 0

		delButton:SetDisabled(true)
		f:Hide()
	end )
	delButton:SetDisabled(true)

	f:AddChild(delButton)

	--Check if the UserDict exist.  If not initialize it, creating a blank user dictionary.
	if Misspelled_DB == nil then
		Misspelled_DB = {UserDict = {}}
	end
	if Misspelled_DB.UserDict == nil then
		Misspelled_DB.UserDict = {}
	end
	
	-- Sort the words alphabetically for better UX
	local sortedWords = {}
	for k, v in pairs(Misspelled_DB.UserDict) do
		table_insert(sortedWords, k)
	end
	table.sort(sortedWords, function(a, b) return string_lower(a) < string_lower(b) end)
	
	-- Display empty state if no words
	if #sortedWords == 0 then
		local emptyLabel = gui:Create("Label")
		emptyLabel:SetFullWidth(true)
		emptyLabel:SetText("\n\n|cff888888Your dictionary is empty.\n\nRight-click misspelled words and select\n'Add to Dictionary' to add words.|r")
		scroll:AddChild(emptyLabel)
	end

	for _, k in ipairs(sortedWords) do
		local x = AceGUI:Create("InteractiveLabel")
		x:SetHighlight(0.25, 0.35, 0.5, 0.6)
		x:SetFullWidth(true)
		x:SetText("  " .. k)
		x:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
		x:SetColor(0.9, 0.9, 0.95)

		x:SetCallback("OnClick", function (widget, event, text)
			PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON or 856)
			--Check if item is selected or not
			local r,g,b,a = widget.label:GetTextColor()

			local wordText = string_match(widget.label:GetText(), "^%s*(.-)%s*$") -- trim
			wordText = string_gsub(wordText, "^%s+", "") -- remove leading spaces
			
			if b < 0.3 then
				--Selected, process an unselect
				widget:SetColor(0.9, 0.9, 0.95)
				widget.label:SetText("  " .. wordText)
				Misspelled_Words_To_Delete[wordText] = nil
				
				local count = 0
				for _ in pairs(Misspelled_Words_To_Delete) do
					count = count + 1
				end

				if count == 0 then
					delButton:SetDisabled(true)
				else
					delButton:SetDisabled(false)
				end
			else	--Unselected, process a select
				widget:SetColor(1, 0.42, 0.42)
				widget.label:SetText("  ✓ " .. wordText)
				Misspelled_Words_To_Delete[wordText] = 1
				delButton:SetDisabled(false)
			end
			UpdateSelectionLabel()
		end )

		scroll:AddChild(x)
	end


	f:ResumeLayout()
	f:DoLayout()
	f:Show()
end

-------------------------------------------------------------------------
-- End: Routines for dealing with the user dictionary
-------------------------------------------------------------------------



-------------------------------------------------------------------------
--
-- Load Guild and Friend Roster Routine
--
-------------------------------------------------------------------------

--Load the player's guild members and friends, as valid words into the
--loaded dictionary.
function Misspelled:LoadGuildAndFriendRoster()

	local numFriends, f --FriendInfo (https://warcraft.wiki.gg/wiki/API_C_FriendList.GetFriendInfo)
	local pcode

	--print("Misspelled: Friends names and guild members loading...")

	--First check your friends list
	if C_FriendList.GetNumFriends ~= nil then
		numFriends = C_FriendList.GetNumFriends()
		if numFriends > 0 then
			for i = 1, numFriends do
				f = C_FriendList.GetFriendInfoByIndex(i)
				if f ~= nil then
					if #f.name > 0 then
						if WordDict:Contains(f.name) == false then
							--Look up the phonetic code for this friend name
							if WordDict.soundslike == WordDict.Const.SoundslikeAlgorithms.PHONETIC then
								pcode = WordDict:PhoneticCode(f.name)
							elseif WordDict.soundslike == WordDict.Const.SoundslikeAlgorithms.GENERIC then
								pcode = WordDict:GenericSoundsLike(f.name)
							else
								pcode = ""
							end
							--Add the friend name to the loaded dictionary
							WordDict.baseWords[f.name] = "/" .. pcode
						end
					end
				end
			end
		end
	end

	-- Guild members are valid words.
	local numTotalInGuild, guildMemberName
	if GetNumGuildMembers and GetGuildRosterInfo and GetNumGuildMembers() ~= 0 then
		numTotalInGuild = GetNumGuildMembers()
		if ( numTotalInGuild > 0 ) then
			for i=1, numTotalInGuild do
				guildMemberName = GetGuildRosterInfo(i);
				if guildMemberName ~= nil then
					if #guildMemberName ~= 0 then
						if WordDict:Contains(guildMemberName) == false then
							--Look up the phonetic code for this guild member name
							if WordDict.soundslike == WordDict.Const.SoundslikeAlgorithms.PHONETIC then
								pcode = WordDict:PhoneticCode(guildMemberName)
							elseif WordDict.soundslike == WordDict.Const.SoundslikeAlgorithms.GENERIC then
								pcode = WordDict:GenericSoundsLike(guildMemberName)
							else
								pcode = ""
							end
							--Add the guild member to the loaded dictionary
							WordDict.baseWords[guildMemberName] = "/" .. pcode
						end
					end
				end
			end
		end
		--print("Misspelled: Guild Members Loaded")
		Misspelled:UnregisterEvent("GUILD_ROSTER_UPDATE")
	end
end

-------------------------------------------------------------------------
-- End: Load Guild and Friend Roster Routine
-------------------------------------------------------------------------

--[[ Interface Options Window ]]--
function Misspelled:CreateInterfaceOptions()
	local cfgFrame = CreateFrame("FRAME", nil, UIParent)
	cfgFrame.name = "Misspelled"

	local cfgFrameHeader = cfgFrame:CreateFontString("OVERLAY", nil, "GameFontNormalLarge")
	cfgFrameHeader:SetPoint("TOPLEFT", 15, -15)
	cfgFrameHeader:SetText(self.Version)

	local cfgFrameReloadTip = cfgFrame:CreateFontString("OVERLAY", nil, "GameFontNormal")
	cfgFrameReloadTip:SetPoint("TOPLEFT", 20, -252)
	cfgFrameReloadTip:SetText(L["Note: reload the game UI to load a different selected dictionary"])

	local cfgAutoSelectDict = CreateFrame("CHECKBUTTON", "Misspelled_cfgAutoSelectDict", cfgFrame, "InterfaceOptionsCheckButtonTemplate")
	Misspelled_cfgAutoSelectDict:SetPoint("TOPLEFT", 20, -40)
	Misspelled_cfgAutoSelectDictText:SetText(L["Auto Select Dictionary to Load"])
	Misspelled_cfgAutoSelectDict:SetChecked(Misspelled_DB.AutoSelectDictionary )
	Misspelled_cfgAutoSelectDict:SetScript("OnClick", function(self)
		if self:GetChecked() then
			PlaySound(856) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON
		else
			PlaySound(857) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF
		end
		Misspelled_DB.AutoSelectDictionary  = not Misspelled_DB.AutoSelectDictionary
		--Toggle the sub options
		if Misspelled_DB.AutoSelectDictionary == true then
			Misspelled_cfgDictdeDE:Disable()
			Misspelled_cfgDictenGB:Disable()
			Misspelled_cfgDictenUS:Disable()
			Misspelled_cfgDictesES:Disable()
			Misspelled_cfgDictfrFR:Disable()
			Misspelled_cfgDictruRU:Disable()
			Misspelled_cfgDictitIT:Disable()
		else
			Misspelled_cfgDictdeDE:Enable()
			Misspelled_cfgDictenGB:Enable()
			Misspelled_cfgDictenUS:Enable()
			Misspelled_cfgDictesES:Enable()
			Misspelled_cfgDictfrFR:Enable()
			Misspelled_cfgDictruRU:Enable()
			Misspelled_cfgDictitIT:Enable()
			if Misspelled_DB.LoadDictionary == nil or #Misspelled_DB.LoadDictionary == 0 then
				Misspelled_DB.LoadDictionary = "enUS"
				Misspelled_cfgDictenUS:SetChecked(true)
			end
		end
	end)

	local cfgDictdeDE = CreateFrame("CHECKBUTTON", "Misspelled_cfgDictdeDE", cfgFrame, "InterfaceOptionsCheckButtonTemplate")
	Misspelled_cfgDictdeDE:SetPoint("TOPLEFT", 40, -64)
	Misspelled_cfgDictdeDEText:SetText("deDE")
	Misspelled_cfgDictdeDE:SetChecked(Misspelled_DB.LoadDictionary == "deDE")
	Misspelled_cfgDictdeDE:SetScript("OnClick", function(self)
		if self:GetChecked() then
			PlaySound(856) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON
			Misspelled_DB.LoadDictionary = "deDE"
			Misspelled_cfgDictenGB:SetChecked(false)
			Misspelled_cfgDictenUS:SetChecked(false)
			Misspelled_cfgDictesES:SetChecked(false)
			Misspelled_cfgDictfrFR:SetChecked(false)
			Misspelled_cfgDictruRU:SetChecked(false)
			Misspelled_cfgDictitIT:SetChecked(false)
		else
			PlaySound(857) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF
		end
	end)

	local cfgDictenGB = CreateFrame("CHECKBUTTON", "Misspelled_cfgDictenGB", cfgFrame, "InterfaceOptionsCheckButtonTemplate")
	Misspelled_cfgDictenGB:SetPoint("TOPLEFT", 40, -88)
	Misspelled_cfgDictenGBText:SetText("enGB")
	Misspelled_cfgDictenGB:SetChecked(Misspelled_DB.LoadDictionary == "enGB")
	Misspelled_cfgDictenGB:SetScript("OnClick", function(self)
		if self:GetChecked() then
			PlaySound(856) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON
			Misspelled_DB.LoadDictionary = "enGB"
			Misspelled_cfgDictdeDE:SetChecked(false)
			Misspelled_cfgDictenUS:SetChecked(false)
			Misspelled_cfgDictesES:SetChecked(false)
			Misspelled_cfgDictfrFR:SetChecked(false)
			Misspelled_cfgDictruRU:SetChecked(false)
			Misspelled_cfgDictitIT:SetChecked(false)
		else
			PlaySound(857) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF
		end
	end)

	local cfgDictenUS = CreateFrame("CHECKBUTTON", "Misspelled_cfgDictenUS", cfgFrame, "InterfaceOptionsCheckButtonTemplate")
	Misspelled_cfgDictenUS:SetPoint("TOPLEFT", 40, -112)
	Misspelled_cfgDictenUSText:SetText("enUS")
	Misspelled_cfgDictenUS:SetChecked(Misspelled_DB.LoadDictionary == "enUS")
	Misspelled_cfgDictenUS:SetScript("OnClick", function(self)
		if self:GetChecked() then
			PlaySound(856) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON
			Misspelled_DB.LoadDictionary = "enUS"
			Misspelled_cfgDictdeDE:SetChecked(false)
			Misspelled_cfgDictenGB:SetChecked(false)
			Misspelled_cfgDictesES:SetChecked(false)
			Misspelled_cfgDictfrFR:SetChecked(false)
			Misspelled_cfgDictruRU:SetChecked(false)
			Misspelled_cfgDictitIT:SetChecked(false)
		else
			PlaySound(857) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF
		end
	end)

	local cfgDictesES = CreateFrame("CHECKBUTTON", "Misspelled_cfgDictesES", cfgFrame, "InterfaceOptionsCheckButtonTemplate")
	Misspelled_cfgDictesES:SetPoint("TOPLEFT", 40, -136)
	Misspelled_cfgDictesESText:SetText("esES")
	Misspelled_cfgDictesES:SetChecked(Misspelled_DB.LoadDictionary == "esES")
	Misspelled_cfgDictesES:SetScript("OnClick", function(self)
		if self:GetChecked() then
			PlaySound(856) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON
			Misspelled_DB.LoadDictionary = "esES"
			Misspelled_cfgDictdeDE:SetChecked(false)
			Misspelled_cfgDictenGB:SetChecked(false)
			Misspelled_cfgDictenUS:SetChecked(false)
			Misspelled_cfgDictfrFR:SetChecked(false)
			Misspelled_cfgDictruRU:SetChecked(false)
			Misspelled_cfgDictitIT:SetChecked(false)
		else
			PlaySound(857) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF
		end

	end)

	local cfgDictfrFR = CreateFrame("CHECKBUTTON", "Misspelled_cfgDictfrFR", cfgFrame, "InterfaceOptionsCheckButtonTemplate")
	Misspelled_cfgDictfrFR:SetPoint("TOPLEFT", 40, -160)
	Misspelled_cfgDictfrFRText:SetText("frFR")
	Misspelled_cfgDictfrFR:SetChecked(Misspelled_DB.LoadDictionary == "frFR")
	Misspelled_cfgDictfrFR:SetScript("OnClick", function(self)
		if self:GetChecked() then
			PlaySound(856) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON
			Misspelled_DB.LoadDictionary = "frFR"
			Misspelled_cfgDictdeDE:SetChecked(false)
			Misspelled_cfgDictenGB:SetChecked(false)
			Misspelled_cfgDictenUS:SetChecked(false)
			Misspelled_cfgDictesES:SetChecked(false)
			Misspelled_cfgDictruRU:SetChecked(false)
			Misspelled_cfgDictitIT:SetChecked(false)
		else
			PlaySound(857) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF
		end

	end)

	local cfgDictruRU = CreateFrame("CHECKBUTTON", "Misspelled_cfgDictruRU", cfgFrame, "InterfaceOptionsCheckButtonTemplate")
	Misspelled_cfgDictruRU:SetPoint("TOPLEFT", 40, -184)
	Misspelled_cfgDictruRUText:SetText("ruRU")
	Misspelled_cfgDictruRU:SetChecked(Misspelled_DB.LoadDictionary == "ruRU")
	Misspelled_cfgDictruRU:SetScript("OnClick", function(self)
		if self:GetChecked() then
			PlaySound(856) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON
			Misspelled_DB.LoadDictionary = "ruRU"
			Misspelled_cfgDictdeDE:SetChecked(false)
			Misspelled_cfgDictenGB:SetChecked(false)
			Misspelled_cfgDictenUS:SetChecked(false)
			Misspelled_cfgDictfrFR:SetChecked(false)
			Misspelled_cfgDictesES:SetChecked(false)
			Misspelled_cfgDictitIT:SetChecked(false)
		else
			PlaySound(857) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF
		end

	end)

	local cfgDictitIT = CreateFrame("CHECKBUTTON", "Misspelled_cfgDictitIT", cfgFrame, "InterfaceOptionsCheckButtonTemplate")
	Misspelled_cfgDictitIT:SetPoint("TOPLEFT", 40, -208)
	Misspelled_cfgDictitITText:SetText("itIT")
	Misspelled_cfgDictitIT:SetChecked(Misspelled_DB.LoadDictionary == "itIT")
	Misspelled_cfgDictitIT:SetScript("OnClick", function(self)
		if self:GetChecked() then
			PlaySound(856) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON
			Misspelled_DB.LoadDictionary = "itIT"
			Misspelled_cfgDictdeDE:SetChecked(false)
			Misspelled_cfgDictenGB:SetChecked(false)
			Misspelled_cfgDictenUS:SetChecked(false)
			Misspelled_cfgDictfrFR:SetChecked(false)
			Misspelled_cfgDictesES:SetChecked(false)
			Misspelled_cfgDictruRU:SetChecked(false)
		else
			PlaySound(857) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF
		end

	end)

	--Edit User Dictionary Button
	local cfgEditUserDict = CreateFrame("Button", "EdutUserDictButton", cfgFrame, "UIPanelButtonTemplate")
        cfgEditUserDict:SetPoint("TOPLEFT", 20, -287)
        cfgEditUserDict:SetText(L["Edit User Dictionary..."])
	cfgEditUserDict:SetWidth(200)
	cfgEditUserDict:SetHeight(24)
	cfgEditUserDict:SetScript("OnClick", function(self)
		--PlaySound("igMainMenuOptionCheckBoxOn")
		Misspelled:EditUserDict()
	end)

	--set options on startup
	Misspelled_cfgDictdeDE:SetChecked(false)
	Misspelled_cfgDictenUS:SetChecked(false)
	Misspelled_cfgDictenGB:SetChecked(false)
	Misspelled_cfgDictesES:SetChecked(false)
	Misspelled_cfgDictfrFR:SetChecked(false)
	Misspelled_cfgDictruRU:SetChecked(false)
	Misspelled_cfgDictitIT:SetChecked(false)

	if Misspelled_DB.LoadDictionary == "deDE" then
		Misspelled_cfgDictdeDE:SetChecked(true)
	elseif Misspelled_DB.LoadDictionary == "enGB" then
		Misspelled_cfgDictenGB:SetChecked(true)
	elseif Misspelled_DB.LoadDictionary == "enUS" then
		Misspelled_cfgDictenUS:SetChecked(true)
	elseif Misspelled_DB.LoadDictionary == "esES" then
		Misspelled_cfgDictesES:SetChecked(true)
	elseif Misspelled_DB.LoadDictionary == "frFR" then
		Misspelled_cfgDictfrFR:SetChecked(true)
	elseif Misspelled_DB.LoadDictionary == "ruRU" then
		Misspelled_cfgDictruRU:SetChecked(true)
	elseif Misspelled_DB.LoadDictionary == "itIT" then
		Misspelled_cfgDictitIT:SetChecked(true)
	end

	if Misspelled_DB.AutoSelectDictionary == true then
			Misspelled_cfgDictdeDE:Disable()
			Misspelled_cfgDictenGB:Disable()
			Misspelled_cfgDictenUS:Disable()
			Misspelled_cfgDictesES:Disable()
			Misspelled_cfgDictfrFR:Disable()
			Misspelled_cfgDictruRU:Disable()
			Misspelled_cfgDictitIT:Disable()
	else
			Misspelled_cfgDictdeDE:Enable()
			Misspelled_cfgDictenGB:Enable()
			Misspelled_cfgDictenUS:Enable()
			Misspelled_cfgDictesES:Enable()
			Misspelled_cfgDictfrFR:Enable()
			Misspelled_cfgDictruRU:Enable()
			Misspelled_cfgDictitIT:Enable()
	end

	--Add options frame to the list of in-game addon options
	--Adding addon options changed in Wow 11.0
	if InterfaceOptions_AddCategory then -- Check for compatiability for older Wow clients.
		InterfaceOptions_AddCategory(cfgFrame)  -- For Wow clients < v11
	elseif Settings then -- For Wow clients > v11
		local category, layout = Settings.RegisterCanvasLayoutCategory(cfgFrame, cfgFrame.name)
		Settings.RegisterAddOnCategory(category)
	end
end


-------------------------------------------------------------------------
--
-- Utility Routines
--
-------------------------------------------------------------------------

--Split a string, at patt delimiter, into a table
function Misspelled:split(str, patt)
	local vals = {}
	local valindex = 0
	local word = ""
	-- need to add a trailing separator to catch the last value.
	str = str .. patt
	for i = 1, string_len(str) do
		local cha = string_sub(str, i, i)
		if cha ~= patt then
			word = word .. cha
		else
			if word ~= nil then
				vals[valindex] = word
				valindex = valindex + 1
				word = ""
			else
				-- in case we get a line with no data.
				break
			end
		end

	end
	return vals
end

function Misspelled:tprint (t, indent, done)
  -- in case we run it standalone outside of the Wow client Lua env
  local Note = Note or print
  --local Tell = Tell or io.write

  -- show strings differently to distinguish them from numbers
  local function show (val)
    if type (val) == "string" then
      return '"' .. val .. '"'
    else
      return tostring(val)
    end -- if
  end -- show
  
  -- entry point here
  done = done or {}
  indent = indent or 0
  for key, value in pairs (t) do
    print(string_rep(" ", indent)) -- indent it
    if type (value) == "table" and not done [value] then
      done [value] = true
      Note (show (key), ":");
      Misspelled:tprint(value, indent + 2, done)
    else
      print(show (key), "=")
      print (show (value))
    end
  end
end

function Misspelled:print(...)
	local frame = _G.SELECTED_DOCK_FRAME or _G.DEFAULT_CHAT_FRAME or _G.ChatFrame1
	if frame and frame.AddMessage then
		frame:AddMessage(...)
	else
		_G.print(...)
	end
end

--Check your friends list to see if the string: name is on the friends list.
function Misspelled:IsFriend(name)
	local numFriends

	if C_FriendList.GetNumFriends ~= nil then
		numFriends = C_FriendList.GetNumFriends()
		if numFriends > 0 then
			for i = 1, numFriends do
				local f = C_FriendList.GetFriendInfoByIndex(i)
				if f and f.name and name == f.name then
					return true
				end
			end
		end
	end
	return false
end

--check the guild roster to see if the string: name, is a guild member.
function Misspelled:IsGuildMember(name)
	local numGuildMembers

	if IsInGuild() then
		numGuildMembers = GetNumGuildMembers(true);	-- true to include offline members
		if ( numGuildMembers > 0 ) then
			for i=1, numGuildMembers do
				if name == GetGuildRosterInfo(i) then
					return true
				end
			end
		end
	end

	return false
end
-------------------------------------------------------------------------
-- End: Utility Routines
-------------------------------------------------------------------------


--[[
--testing
require("bit")
require("WordDict")
--~ require("Dict\\Dic_en_US")
--~ Misspelled_DB = {}
--~ WordDict:Init()
--~ ChatFrameEditBox = {}
--~ function ChatFrameEditBox:GetText()
--~ 	return "Leatherworking"
--~ end
s = "Test |cffff91c8A|cffxxxxxxBSecondWord|r|r"
s2 = string.gsub(s, SPELLED_WRONG_HIGHLIGHT .. "(.-)|r", function(x) return x end)
print(s2)
---]]
