--[[========================================================================================
      LibAdvancedIconSelector provides a searchable icon selection GUI to World
      of Warcraft addons.
      
      Copyright (c) 2011 - 2012 David Forrester  (Darthyl of Bronzebeard-US)
        Email: darthyl@hotmail.com
      
      Permission is hereby granted, free of charge, to any person obtaining a copy
      of this software and associated documentation files (the "Software"), to deal
      in the Software without restriction, including without limitation the rights
      to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
      copies of the Software, and to permit persons to whom the Software is
      furnished to do so, subject to the following conditions:
      
      The above copyright notice and this permission notice shall be included in
      all copies or substantial portions of the Software.
      
      THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
      IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
      FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
      AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
      LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
      OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
      THE SOFTWARE.
    ========================================================================================]]

--[[========================================================================================
      Please notify me if you wish to make use of this library in your addon!
      I need to know for testing purposes - to make sure that any changes I make
      aren't going to break someone else's addon!
    ========================================================================================]]

local DEBUG = false
if DEBUG and LibDebug then LibDebug() end

local MAJOR_VERSION = "LibAdvancedIconSelector-1.0"
local MINOR_VERSION = 14			-- (do not call GetAddOnMetaData)

if not LibStub then error(MAJOR_VERSION .. " requires LibStub to operate") end
local lib = LibStub:NewLibrary(MAJOR_VERSION, MINOR_VERSION)
if not lib then return end
local L = LibStub("AceLocale-3.0"):GetLocale(MAJOR_VERSION, true)
LibStub("AceTimer-3.0"):Embed(lib)
local libiconpath = LibStub("LibIconPath")
--if not libiconpath:GetRevision() or libiconpath:GetRevision() < 20 then error("You are using an old, incompatible version of LibIconPath - please update it!") end

local ICON_WIDTH = 36
local ICON_HEIGHT = 36
local ICON_SPACING = 4	-- Minimum spacing between icons
local ICON_PADDING = 4	-- Padding around the icon display
local INITIATE_SEARCH_DELAY = 0.3	-- The delay between pressing a key and the start of the search
local SCAN_TICK = 0.1				-- The interval between each search "tick"
local SCAN_PER_TICK = 1000			-- How many icons to scan per tick?

local initialized = false
local MACRO_ICON_FILENAMES = { }
local ITEM_ICON_FILENAMES = { }

local keywordLibrary = nil			-- The currently loaded keyword library

-- List of prepositions that aren't re-capitalized when displaying keyword data.  Not all prepositions; just the ones that stand out.
local PREPS = { a = true, an = true, ["and"] = true, by = true, de = true, from = true, ["in"] = true, of = true, on = true, the = true, to = true, ["vs."] = true }

local defaults = {
	width = 419,
	height = 343,
	enableResize = true,
	enableMove = true,
	okayCancel = true,
	minResizeWidth = 300,
	minResizeHeight = 200,
	insets = { left = 11, right = 11, top = 11, bottom = 10 },
	contentInsets = {
		left = 11 + 8, right = 11 + 8,
		top = 11 + 20, bottom = 10 + 8 },
	edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
	bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
	tile = false,
	tileSize = 32,
	edgeSize = 32,
	headerWidth = 256,
	headerTexture = "Interface\\DialogFrame\\UI-DialogBox-Header",
	headerFont = "GameFontNormal",
	headerOffsetX = 0,
	headerOffsetY = -14,
	headerText = L["FRAME_TITLE"],

	sectionOrder = { "MacroIcons", "ItemIcons" },
	sections = { },				-- (will be filled in automatically, if not set by user)
	sectionVisibility = { },	-- (will be filled in automatically, if not set by user)
}

-- ========================================================================================
-- OBJECT MODEL IMPLEMENTATION

local ObjBase = { }

-- Derives a new object using "self" as the prototype.
function ObjBase:Derive(o)
	o = o or { }
	assert(o ~= self and o.superType == nil)
	setmetatable(o, self)	-- (self = object / prototype being derived from, not necessarily ObjBase!)
	self.__index = self
	o.superType = self
	return o
end

-- Overlays the entries of "self" over the inherited entries of "o".
-- This is very useful for adding methods to an existing object, such as one created by CreateFrame().
-- (Note: replaces o's metatable, and only retains __index of the original metatable)
function ObjBase:MixInto(o)
	assert(o ~= nil and o ~= self and o.superType == nil)
	local superType = { }	-- (indexing this object will index the super type instead)
	o.superType = superType
	setmetatable(superType, getmetatable(o))
	setmetatable(o, {
		__index = function(t, k)	-- (note: do NOT index t from __index or it may loop)
			local r = self[k]		-- (mixed-in prototype)
			if r ~= nil then		-- (don't use "self[k] or superType[k]" or false won't work)
				return r
			else
				return superType[k]	-- (super type)
			end
		end
	})
	return o
end

-- ========================================================================================
-- OBJECT DEFINITIONS

local SearchObject = ObjBase:Derive()
local IconSelectorWindow = ObjBase:Derive()
local IconSelectorFrame = ObjBase:Derive()
local Helpers = ObjBase:Derive()

-- ================================================================
-- LIB IMPLEMENTATION

-- If DEBUG == true, then prints a console message.
function lib:Debug(...)
	if DEBUG then
		local prefix = "|cff00ff00[LAIS] [Debug]|r"
		if LibDebug then	-- (get  around LibDebug's print replacement)
			getmetatable(_G).__index.print(prefix, ...)
		else
			print(prefix, ...)
		end
	end
end

lib:Debug("Addon loaded.")

-- Embeds this library's functions into an addon for ease of use.
function lib:Embed(addon)
	addon.CreateIconSelectorWindow = lib.CreateIconSelectorWindow
	addon.CreateIconSelectorFrame = lib.CreateIconSelectorFrame
	addon.CreateSearch = lib.CreateSearch
	addon.GetNumMacroIcons = lib.GetNumMacroIcons
	addon.GetNumItemIcons = lib.GetNumItemIcons
	addon.GetRevision = lib.GetRevision
	addon.LoadKeywords = lib.LoadKeywords
	addon.LookupKeywords = lib.LookupKeywords
end

-- Creates and returns a new icon selector window.
function lib:CreateIconSelectorWindow(name, parent, options)
	Helpers.InitialInit()
	return IconSelectorWindow:Create(name, parent, options)
end

-- Creates and returns a new icon selector frame (i.e., no window, search box, or buttons).
function lib:CreateIconSelectorFrame(name, parent, options)
	Helpers.InitialInit()
	return IconSelectorFrame:Create(name, parent, options)
end

-- Creates and returns a new search object.
function lib:CreateSearch(options)
	Helpers.InitialInit()
	return SearchObject:Create(options)
end

-- Returns the number of "macro" icons.  This may go slow the first time it is run if icon filenames aren't yet loaded.
function lib:GetNumMacroIcons()	-- (was removed from the API, but can still be useful when you don't need filenames)
	Helpers.InitialInit()
	return #MACRO_ICON_FILENAMES
end

-- Returns the number of "item" icons.  This may go slow the first time it is run if icon filenames aren't yet loaded.
function lib:GetNumItemIcons()	-- (was removed from the API, but can still be useful when you don't need filenames)
	Helpers.InitialInit()
	return #ITEM_ICON_FILENAMES
end

-- Returns the revision # of the loaded library instance.
function lib:GetRevision()
	return MINOR_VERSION
end

-- Attempts to load the keyword library contained in the addon with the specified name.  BUT - it will load the one
-- at "AdvancedIconSelector-KeywordData" instead if (a) it's newer, or (b) the addon with the specified name cannot
-- be found / loaded.
--
-- You don't necessarily need to do this manually - it will automatically be done when an icon window / icon frame /
-- search object is first used, assuming a keywordAddonName field is specified in options.
function lib:LoadKeywords(addonName)

	-- Get the revision # of the specified addon (if it's enabled and loadable).
	local addonRevision = nil
	local addonLoadable = addonName and select(5, GetAddOnInfo(addonName))
	if addonLoadable then
		addonRevision = tonumber(GetAddOnMetadata(addonName, "X-Revision"))
	end

	-- Then, get the revision # of the default library (if it's enabled and loadable).
	local defaultRevision = nil
	local defaultLoadable = select(5, GetAddOnInfo("BetterIconSelector-KeywordData"))
	if defaultLoadable then
		defaultRevision = tonumber(GetAddOnMetadata("BetterIconSelector-KeywordData", "X-Revision"))
	end

	-- Finally, get the revision that is already loaded.
	keywordLibrary = LibStub("LibAdvancedIconSelector-KeywordData-1.0", true)
	local currentRevision = keywordLibrary and tonumber(keywordLibrary:GetRevision())	-- (old revisions may yield a string)

	-- Load the specified addon if it's newer than the current library and at least as new as the default library.
	local source = nil
	if addonRevision and (not currentRevision or addonRevision > currentRevision) and (not defaultRevision or addonRevision >= defaultRevision) then
		LoadAddOn(addonName)
		source = addonName
	
	-- Otherwise, load the default library if it's newer than the current library.
	elseif defaultRevision and (not currentRevision or defaultRevision > currentRevision) then
		LoadAddOn("BetterIconSelector-KeywordData")
		source = "BetterIconSelector-KeywordData"
	end

	-- Whatever happens, update keywordLibrary to point to the currently loaded instance.
	keywordLibrary = LibStub("LibAdvancedIconSelector-KeywordData-1.0", true)
	
	if source then
		lib:Debug("Loaded keyword library revision:", keywordLibrary and keywordLibrary:GetRevision(), "source:", source)
	end
end

-- Looks up keywords for the given icon.  Returns nil if no keywords exist, or the keyword library has not been loaded
-- yet using LoadKeywords().
function lib:LookupKeywords(texture)
	return keywordLibrary and keywordLibrary:GetKeywords(texture)
end

-- ================================================================
-- ICON WINDOW IMPLEMENTATION

-- Creates a new icon selector window, which includes an icon selector frame, search box, etc.
-- See Readme.txt for a list of all supported options.
function IconSelectorWindow:Create(name, parent, options)
	assert(name, "The icon selector window must have a name")
	if not parent then parent = UIParent end
	options = Helpers.ApplyDefaults(options, defaults)

	self = self:MixInto(CreateFrame("Frame", name, parent))
	self:Hide()
	self:SetFrameStrata("MEDIUM")
	self:SetSize(options.width, options.height)
	self:SetMinResize(options.minResizeWidth, options.minResizeHeight)
	self:SetToplevel(true)
	self.options = options

	if options.customFrame then options.customFrame:SetParent(self) end

	self:SetBackdrop({
		edgeFile = options.edgeFile,
		bgFile = options.bgFile,
		tile = options.tile,
		tileSize = options.tileSize,
		edgeSize = options.edgeSize,
		insets = options.insets })

	if not options.noHeader then
		self.header = self:CreateTexture()
		self.header:SetTexture(options.headerTexture)
		self.header:SetWidth(options.headerWidth, 64)
		self.header:SetPoint("TOP", 0, 12)

		self.headerText = self:CreateFontString()
		self.headerText:SetFontObject(options.headerFont)
		self.headerText:SetPoint("TOP", self.header, "TOP", options.headerOffsetX, options.headerOffsetY)
		self.headerText:SetText(options.headerText)
	end

	if not options.noCloseButton then
		self.closeButton = CreateFrame("Button", nil, self, "UIPanelCloseButton")
		self.closeButton:SetPoint("TOPRIGHT", 0, 0)

		self.closeButton:SetScript("OnClick", function(...)
			if self.OnCancel then
				self:OnCancel(...)
			else
				self:Hide()
			end
		end)
	end

	if options.enableResize then self:SetResizable(true) end
	if options.enableMove then self:SetMovable(true) end
	if not options.allowOffscreen then self:SetClampedToScreen(true) end
	if options.enableResize or options.enableMove then self:EnableMouse(true) end

	self:RegisterForDrag("LeftButton")

	self:SetScript("OnDragStart", function(self, button)
		local x, y = self.mouseDownX, self.mouseDownY
		if x and y then
			local scale = UIParent:GetEffectiveScale()
			x = x / scale
			y = y / scale
			y = (y - self:GetBottom()) * scale
			x = (self:GetRight() - x) * scale

			-- (set anchorTo if you want your frame to conditionally be moveable / resizeable)
			if not options.anchorFrame or not options.anchorFrame:IsShown() then
				if options.enableResize and x < 20 and y < 20 then
					self:StartSizing()
				elseif options.enableMove then
					self:StartMoving()
				end
			end
		end
	end)
	
	self:SetScript("OnDragStop", function(self, button)
		self:StopMovingOrSizing()
	end)

	self:SetScript("OnMouseDown", function(self, button)
		if button == "LeftButton" then
			self.mouseDownX, self.mouseDownY = GetCursorPosition()
		end
	end)

	self:SetScript("OnMouseUp", function(self, button)
		if button == "LeftButton" then
			self.mouseDownX, self.mouseDownY = nil, nil
		end
	end)

	self.iconsFrame = lib:CreateIconSelectorFrame(name .. "_IconsFrame", self, options)

	self.searchLabel = self:CreateFontString()
	self.searchLabel:SetFontObject("GameFontNormal")
	self.searchLabel:SetText(L["Search:"])
	self.searchLabel:SetHeight(22)

	self.searchBox = CreateFrame("EditBox", name .. "_SearchBox", self, "InputBoxTemplate")
	self.searchBox:SetAutoFocus(false)
	self.searchBox:SetHeight(22)
	self.searchBox:SetScript("OnTextChanged", function(editBox, userInput)
		if userInput then
			self.iconsFrame:SetSearchParameter(editBox:GetText())
		end
	end)

	self.cancelButton = CreateFrame("Button", name .. "_Cancel", self, "UIPanelButtonTemplate")
	if options.okayCancel then
		self.cancelButton:SetText(L["Cancel"])
	else
		self.cancelButton:SetText(L["Close"])
	end
	self.cancelButton:SetSize(78, 22)
	self.cancelButton:SetScript("OnClick", function(...)
		if self.OnCancel then
			self:OnCancel(...)
		else
			self:Hide()
		end
	end)

	if options.okayCancel then
		self.okButton = CreateFrame("Button", name .. "_OK", self, "UIPanelButtonTemplate")
		self.okButton:SetText(L["Okay"])
		self.okButton:SetSize(78, 22)
		self.okButton:SetScript("OnClick", function(...)
			if self.OnOkay then
				self:OnOkay(...)
			end
		end)
	end

	if options.visibilityButtons then
		self.visibilityButtons = { }
		for _, buttonInfo in ipairs(options.visibilityButtons) do
			local sectionName, buttonText = unpack(buttonInfo)
			local buttonName = name .. "_" .. sectionName .. "_Visibility"
			local button = CreateFrame("CheckButton", buttonName, self, "UICheckButtonTemplate")
			_G[buttonName .. "Text"]:SetText(buttonText)
			button:SetChecked(self.iconsFrame:GetSectionVisibility(sectionName))
			button:SetSize(24, 24)
			button:SetScript("OnClick", function(button, mouseButton, down)
				self.iconsFrame:SetSectionVisibility(sectionName, button:GetChecked())
			end)
			tinsert(self.visibilityButtons, button)
		end
	end

	self:SetScript("OnSizeChanged", self.private_OnWindowSizeChanged)

	return self
end

-- Provides additional script types for the icon selector WINDOW (not frame).
function IconSelectorWindow:SetScript(scriptType, handler)
	if scriptType == "OnOkayClicked" then
		self.OnOkay = handler
	elseif scriptType == "OnCancelClicked" then
		self.OnCancel = handler
	else
		return self.superType.SetScript(self, scriptType, handler)
	end
end

-- Specifies a new search parameter, setting the text of the search box and starting the search.
-- Setting immediateResults to true will eliminate the delay before the search actually starts.
function IconSelectorWindow:SetSearchParameter(searchText, immediateResults)
	if searchText then
		self.searchBox:SetText(searchText)
	else
		self.searchBox:SetText("")
	end
	self.iconsFrame:SetSearchParameter(searchText, immediateResults)
end

-- Called when the size of the icon selector window changes.
function IconSelectorWindow:private_OnWindowSizeChanged(width, height)
	local spacing = 4
	local options = self.options
	local contentInsets = options.contentInsets

	if options.customFrame then
		options.customFrame:SetPoint("TOPLEFT", contentInsets.left, -contentInsets.top)
		options.customFrame:SetPoint("RIGHT", -contentInsets.right, 0)
		self.iconsFrame:SetPoint("TOPLEFT", options.customFrame, "BOTTOMLEFT", 0, -spacing)
	else
		self.iconsFrame:SetPoint("TOPLEFT", contentInsets.left, -contentInsets.top)
	end
	self.iconsFrame:SetPoint("RIGHT", -contentInsets.right, 0)
	self.cancelButton:SetPoint("BOTTOMRIGHT", -contentInsets.right, contentInsets.bottom)
	if self.okButton then
		self.okButton:SetPoint("BOTTOMRIGHT", self.cancelButton, "BOTTOMLEFT", -2, 0)
	end
	self.searchLabel:SetPoint("BOTTOMLEFT", contentInsets.left, contentInsets.bottom)
	self.searchBox:SetPoint("LEFT", self.searchLabel, "RIGHT", 6, 0)
	self.searchBox:SetPoint("RIGHT", self.okButton or self.cancelButton, "LEFT", -spacing, 0)

	local lastButton = nil

	-- Lay out the visibility buttons in a row
	if self.visibilityButtons then
		for _, button in ipairs(self.visibilityButtons) do
			if lastButton then
				button:SetPoint("LEFT", _G[lastButton:GetName() .. "Text"], "RIGHT", 2, 0)
			else
				button:SetPoint("BOTTOMLEFT", self.searchLabel, "TOPLEFT", -2, 0)
			end
			lastButton = button
		end
	end

	-- Attach the bottom of the icons frame
	if lastButton then
		self.iconsFrame:SetPoint("BOTTOM", lastButton, "TOP", 0, spacing)
	else
		self.iconsFrame:SetPoint("BOTTOM", self.cancelButton, "TOP", 0, spacing)
	end
end

-- ================================================================
-- ICON FRAME IMPLEMENTATION

-- Creates a new icon selector frame (no window, search box, etc.)
-- See Readme.txt for a list of all supported options.
function IconSelectorFrame:Create(name, parent, options)
	assert(name, "The icon selector frame must have a name")
	options = Helpers.ApplyDefaults(options, defaults)

	self = self:MixInto(CreateFrame("Frame", name, parent))
	self.scrollOffset = 0
	self.iconsX = 1
	self.iconsY = 1
	self.fauxResults = 0	-- (fake results to keep top-left icon stationary when resizing)
	self.searchResults = { }
	self.icons = { }
	self.showDynamicText = options.showDynamicText

	self:SetScript("OnSizeChanged", self.private_OnIconsFrameSizeChanged)

	self:SetScript("OnShow", function(self)
		-- Call the BeforeShow handler (useful for replacing icon sections, etc.)
		if self.BeforeShow then self:BeforeShow() end

		-- Restart the search, since we stopped it when the frame was hidden.
		self.search:RestartSearch()
	end)

	-- Create the scroll bar
	self.scrollFrame = CreateFrame("ScrollFrame", name .. "_ScrollFrame", self, "FauxScrollFrameTemplate")
	self.scrollFrame:SetScript("OnVerticalScroll", function(scrollFrame, offset)
		if offset == 0 then self.fauxResults = 0 end	-- Remove all faux results when the top of the list is hit.
		FauxScrollFrame_OnVerticalScroll(self.scrollFrame, offset, ICON_HEIGHT + ICON_SPACING, function() self:private_UpdateScrollFrame() end)
	end)

	-- Create the internal frame to display the icons
	self.internalFrame = CreateFrame("Frame", name .. "_Internal", self)
	self.internalFrame.parent = self
	self.internalFrame:SetScript("OnSizeChanged", self.private_OnInternalFrameSizeChanged)

	self.internalFrame:SetScript("OnHide", function(internalFrame)
		-- When the frame is hidden, immediately stop the search.
		self.search:Stop()

		-- Release any textures that were being displayed.
		for i = 1, #self.icons do
			local button = self.icons[i]
			if button then
				button:SetNormalTexture(nil)
			end
		end
	end)

	self.search = lib:CreateSearch(options)
	self.search.owner = self
	self.search:SetScript("OnSearchStarted", self.private_OnSearchStarted)
	self.search:SetScript("OnSearchResultAdded", self.private_OnSearchResultAdded)
	self.search:SetScript("OnSearchComplete", self.private_OnSearchComplete)
	self.search:SetScript("OnSearchTick", self.private_OnSearchTick)
	self.search:SetScript("OnIconScanned", self.private_OnIconScanned)

	-- Set the visibility of all sections
	for sectionName, _ in pairs(options.sections) do
		if options.sectionVisibility[sectionName] == false then		-- FALSE ONLY, not nil.  Sections are visible by default.
			self.search:ExcludeSection(sectionName, true)
		end
	end

	-- NOTE: Do not start the search until the frame is shown!  Some addons may choose to create
	-- the frame early and not display it until later, and we don't want to load the keyword library early!
	
	return self
end

-- Called when a new search is started.
function IconSelectorFrame.private_OnSearchStarted(search)
	local self = search.owner
	wipe(self.searchResults)
	self.updateNeeded = true
	self.resetScroll = true
end

-- Called each time a search result is found.
function IconSelectorFrame.private_OnSearchResultAdded(search, texture, globalID, localID, kind)
	local self = search.owner
	tinsert(self.searchResults, globalID)
	self.updateNeeded = true
end

-- Called when the search is completed.
function IconSelectorFrame.private_OnSearchComplete(search)
	local self = search.owner
	lib:Debug("Found " .. #self.searchResults .. " results")
	self.initialSelection = nil	-- (if we didn't find the initial selection first time, we're not going to find it next time)
end

-- Called after each search tick.
function IconSelectorFrame.private_OnSearchTick(search)
	local self = search.owner

	-- Update the icon display if new results have been found
	if self.updateNeeded then
		self.updateNeeded = false

		-- To reduce flashing, scroll to top JUST before calling private_UpdateScrollFrame() on the first search tick.
		if self.resetScroll then
			self.resetScroll = false
			self.fauxResults = 0
			FauxScrollFrame_Update(self.scrollFrame, 1, 1, 1)	-- (scroll to top)
		end

		self:private_UpdateScrollFrame()
	end
end

-- Called as each icon is passed in the search.
function IconSelectorFrame.private_OnIconScanned(search, texture, globalID, localID, kind)
	local self = search.owner

	if self.initialSelection then

		assert(self.selectedID == nil)	-- (user selection should have cleared the initial selection)
			
		-- If we find the texture we're looking for...
		if texture and strupper(texture) == strupper(self.initialSelection) then
			
			-- Set the selection.
			lib:Debug("Found selected texture at global index", globalID)
			self:SetSelectedIcon(globalID)

			assert(self.initialSelection == nil)	-- (should have been cleared by SetSelectedIcon)
		end
	end
end

-- Updates the scroll frame and refreshes the main display based on current parameters.
function IconSelectorFrame:private_UpdateScrollFrame()
	local maxLines = ceil((self.fauxResults + #self.searchResults) / self.iconsX)
	local displayedLines = self.iconsY
	local lineHeight = ICON_HEIGHT + ICON_SPACING
	FauxScrollFrame_Update(self.scrollFrame, maxLines, displayedLines, lineHeight)

	self.scrollOffset = FauxScrollFrame_GetOffset(self.scrollFrame)

	-- Update the icon display to match the new scroll offset
	self:private_UpdateIcons()
end

-- Specifies a new search parameter, restarting the search.
-- Setting immediateResults to true will eliminate the delay before the search actually starts.
function IconSelectorFrame:SetSearchParameter(searchText, immediateResults)
	self.search:SetSearchParameter(searchText, immediateResults)
end

-- Selects the icon at the given global index.  Does not trigger an OnSelectedIconChanged event.
function IconSelectorFrame:SetSelectedIcon(index)
	if self.selectedID ~= index then
		self.selectedID = index
		self.initialSelection = nil

		-- Fire an event.
		if self.OnSelectedIconChanged then self:OnSelectedIconChanged() end

		-- Update the icon display
		self:private_UpdateIcons()
	end
end

-- Selects the icon with the given filename (without the INTERFACE\\ICONS\\ prefix)
-- (NOTE: the icon won't actually be selected until it's found)
function IconSelectorFrame:SetSelectionByName(texture)
	self:SetSelectedIcon(nil)
	self.initialSelection = libiconpath:getName(texture)
	if texture then
		self.search:RestartSearch()
	else
		self:private_UpdateIcons()
	end
end

-- Returns information about the icon at the given global index, or nil if out of range.
-- (Returns a tuple of id (within section), kind, texture)
function IconSelectorFrame:GetIconInfo(index)
	return self.search:GetIconInfo(index, self)
end

-- Returns the ID of the selected icon.
function IconSelectorFrame:GetSelectedIcon()
	return self.selectedID
end

-- Provides additional script types for the icon selector FRAME (not window).
function IconSelectorFrame:SetScript(scriptType, handler)
	if scriptType == "OnSelectedIconChanged" then	-- Called when the selected icon is changed
		self.OnSelectedIconChanged = handler
	elseif scriptType == "OnButtonUpdated" then		-- Hook for the icon keyword editor (IKE) button overlays
		self.OnButtonUpdated = handler
	elseif scriptType == "BeforeShow" then			-- Called just before the window is shown - useful for replacing icon sections, etc.
		self.BeforeShow = handler
	else
		return self.superType.SetScript(self, scriptType, handler)
	end
end

-- Selects the next icon.  Used by the icon keyword editor, and not intended for external use.
-- (i.e., it doesn't take the current search filter into account)
-- If you'd like me to officially include such a feature, please email me.
function IconSelectorFrame:unofficial_SelectNextIcon()
	if self.selectedID then
		self:SetSelectedIcon(self.search:private_Skip(self.selectedID + 1))
		self:private_UpdateIcons()
	end
end

-- Replaces the specified section of icons.  Useful to change the icons within a custom section.
-- Also, causes the search to start over.
-- (see CreateDefaultSection for example section definitions)
-- (also, see EquipmentSetPopup.lua and MacroPopup.lua of AdvancedIconSelector for an example of actual use)
function IconSelectorFrame:ReplaceSection(sectionName, section)
	self.search:ReplaceSection(sectionName, section)
end

-- Shows or hides the icon section with the given name.
function IconSelectorFrame:SetSectionVisibility(sectionName, visible)
	self.search:ExcludeSection(sectionName, not visible)
	self.search:RestartSearch()
end

-- Returns true if the icon section with the given name is visible, or false otherwise.
function IconSelectorFrame:GetSectionVisibility(sectionName)
	return not self.search:IsSectionExcluded(sectionName)
end

-- (private) Called when the icon frame's size has changed.
function IconSelectorFrame:private_OnIconsFrameSizeChanged(width, height)
	self.scrollFrame:SetPoint("TOP", self)
	self.scrollFrame:SetPoint("BOTTOMRIGHT", self, -21, 0)

	self.internalFrame:SetPoint("TOPLEFT", self)
	self.internalFrame:SetPoint("BOTTOMRIGHT", self, -16, 0)
end

-- (private) Called when the internal icon frame's size has changed (i.e., the part without the scroll bar)
function IconSelectorFrame.private_OnInternalFrameSizeChanged(internalFrame, width, height)
	local self = internalFrame.parent

	local oldFirstIcon = 1 + self.scrollOffset * self.iconsX - self.fauxResults
	self.iconsX = floor((floor(width + 0.5) - 2 * ICON_PADDING + ICON_SPACING) / (ICON_WIDTH + ICON_SPACING))
	self.iconsY = floor((floor(height + 0.5) - 2 * ICON_PADDING + ICON_SPACING) / (ICON_HEIGHT + ICON_SPACING))

	-- Center the icons
	local leftPadding = (width - 2 * ICON_PADDING - (self.iconsX * (ICON_WIDTH + ICON_SPACING) - ICON_SPACING)) / 2
	local topPadding = (height - 2 * ICON_PADDING - (self.iconsY * (ICON_HEIGHT + ICON_SPACING) - ICON_SPACING)) / 2

	local lastIconY = nil
	for y = 1, self.iconsY do
		local lastIconX = nil
		for x = 1, self.iconsX do
			local i = (y - 1) * self.iconsX + x

			-- Create the button if it doesn't exist (but don't set its normal texture yet)
			local button = self.icons[i]
			if not button then
				button = CreateFrame("CheckButton", nil, self.internalFrame)
				self.icons[i] = button
				button:SetSize(36, 36)
				button:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square")
				button:SetCheckedTexture("Interface\\Buttons\\CheckButtonHilight")

				button:SetScript("OnClick", function(button, mouseButton, down)
					if button.textureKind and button.textureID then
						if self.selectedButton then
							self.selectedButton:SetChecked(false)
						end
						button:SetChecked(true)
						self.selectedButton = button
						self:SetSelectedIcon(button.globalID)
					else
						button:SetChecked(false)
					end
				end)
				
				button:SetScript("OnEnter", function(button, motion)
					if button.texture then
						button.texture = libiconpath:getName(button.texture)
						local keywordString = lib:LookupKeywords(button.texture)
						local keywords = Helpers.GetTaggedStrings(keywordString, nil)
						local spells = Helpers.GetTaggedStrings(keywordString, "spell")

						GameTooltip:SetOwner(button, "ANCHOR_TOPRIGHT")
						GameTooltip:ClearLines()
						if button.textureKind == "Equipment" then
							GameTooltip:AddDoubleLine(NORMAL_FONT_COLOR_CODE .. L["Equipped item texture:"] .. FONT_COLOR_CODE_CLOSE, GRAY_FONT_COLOR_CODE .. tostring(button.textureID) .. FONT_COLOR_CODE_CLOSE)
						elseif button.textureKind == "Macro" then
							GameTooltip:AddDoubleLine(NORMAL_FONT_COLOR_CODE .. L["Macro texture:"] .. FONT_COLOR_CODE_CLOSE, GRAY_FONT_COLOR_CODE .. tostring(button.textureID) .. FONT_COLOR_CODE_CLOSE)
						elseif button.textureKind == "Item" then
							GameTooltip:AddDoubleLine(NORMAL_FONT_COLOR_CODE .. L["Item texture:"] .. FONT_COLOR_CODE_CLOSE, GRAY_FONT_COLOR_CODE .. tostring(button.textureID) .. FONT_COLOR_CODE_CLOSE)
						elseif button.textureKind == "Spell" then
							GameTooltip:AddDoubleLine(NORMAL_FONT_COLOR_CODE .. L["Spell texture:"] .. FONT_COLOR_CODE_CLOSE, GRAY_FONT_COLOR_CODE .. tostring(button.textureID) .. FONT_COLOR_CODE_CLOSE)
						elseif button.textureKind == "Dynamic" then
							GameTooltip:AddLine(NORMAL_FONT_COLOR_CODE .. L["Default / dynamic texture:"] .. FONT_COLOR_CODE_CLOSE)
						end
						GameTooltip:AddLine(tostring(button.texture), 1, 1, 1)
						Helpers.AddTaggedInformationToTooltip(keywordString, "spell", L["Spell: "], NORMAL_FONT_COLOR)
						Helpers.AddTaggedInformationToTooltip(keywordString, "companion", L["Companion: "], NORMAL_FONT_COLOR)
						Helpers.AddTaggedInformationToTooltip(keywordString, "mount", L["Mount: "], NORMAL_FONT_COLOR)
						Helpers.AddTaggedInformationToTooltip(keywordString, "talent", L["Talent: "], NORMAL_FONT_COLOR)
						Helpers.AddTaggedInformationToTooltip(keywordString, "tab", L["Tab: "], NORMAL_FONT_COLOR)
						Helpers.AddTaggedInformationToTooltip(keywordString, "passive", L["Passive: "], NORMAL_FONT_COLOR)
						Helpers.AddTaggedInformationToTooltip(keywordString, "racial", L["Racial: "], NORMAL_FONT_COLOR)
						Helpers.AddTaggedInformationToTooltip(keywordString, "racial_passive", L["Racial Passive: "], NORMAL_FONT_COLOR)
						Helpers.AddTaggedInformationToTooltip(keywordString, "mastery", L["Mastery: "], NORMAL_FONT_COLOR)
						Helpers.AddTaggedInformationToTooltip(keywordString, "professions", L["Professions: "], NORMAL_FONT_COLOR)
						Helpers.AddTaggedInformationToTooltip(keywordString, "pet_command", L["Pet Command: "], NORMAL_FONT_COLOR)
						Helpers.AddTaggedInformationToTooltip(keywordString, "pet_stance", L["Pet Stance: "], NORMAL_FONT_COLOR)
						if keywords and strlen(keywords) > 0 then
							GameTooltip:AddLine(GRAY_FONT_COLOR_CODE .. L["Additional keywords: "] .. tostring(keywords) .. FONT_COLOR_CODE_CLOSE, 1, 1, 1, true)
						end
						GameTooltip:Show()
					end
				end)

				button:SetScript("OnLeave", function(button, motion)
					GameTooltip:Hide()
				end)
			end
			button:Show()	-- (yes, this is necessary; we hide excess buttons)

			if not lastIconX then
				if not lastIconY then
					button:SetPoint("TOPLEFT", self.internalFrame, leftPadding + ICON_PADDING, -topPadding - ICON_PADDING)
				else
					button:SetPoint("TOPLEFT", lastIconY, "BOTTOMLEFT", 0, -ICON_SPACING)
				end
				lastIconY = button
			else
				button:SetPoint("TOPLEFT", lastIconX, "TOPRIGHT", ICON_SPACING, 0)
			end
			
			lastIconX = button
		end
	end

	-- Hide any excess buttons.  Release the textures, but keep the buttons 'til the window is closed.
	for i = self.iconsY * self.iconsX + 1, #self.icons do
		local button = self.icons[i]
		if button then
			button:SetNormalTexture(nil)
			button:Hide()
		end
	end
	
	-- Add padding at the top to make the old and new first icon constant
	local newFirstIcon = 1 + self.scrollOffset * self.iconsX - self.fauxResults
	self.fauxResults = self.fauxResults + newFirstIcon - oldFirstIcon

	-- Increase faux results if below 0
	if self.fauxResults < 0 then
		local scrollDown = -ceil((self.fauxResults + 1) / self.iconsX) + 1	-- careful!  Lots of OBOBs here if not done right.
		self.fauxResults = self.fauxResults + scrollDown * self.iconsX
		assert(self.fauxResults >= 0)
		local newOffset = max(FauxScrollFrame_GetOffset(self.scrollFrame) + scrollDown, 0)
		FauxScrollFrame_SetOffset(self.scrollFrame, newOffset)
	end
	
	-- Decrease faux results if above iconsX
	if self.fauxResults > self.iconsX and self.iconsX > 0 then
		local scrollUp = floor(self.fauxResults / self.iconsX)
		self.fauxResults = self.fauxResults - scrollUp * self.iconsX
		assert(self.fauxResults < self.iconsX)
		local newOffset = max(FauxScrollFrame_GetOffset(self.scrollFrame) - scrollUp, 0)
		FauxScrollFrame_SetOffset(self.scrollFrame, newOffset)
	end

	self:private_UpdateScrollFrame()
end

-- Refreshes the icon display.
function IconSelectorFrame:private_UpdateIcons()
	if self:IsShown() then
		local firstIcon = 1 + self.scrollOffset * self.iconsX - self.fauxResults
		local last = self.iconsX * self.iconsY
	
		if self.selectedButton then
			self.selectedButton:SetChecked(false)
			self.selectedButton = nil
		end

		for i = 1, last do
			local button = self.icons[i]
			if button then
				local resultIndex = firstIcon + i - 1
				if self.searchResults[resultIndex] then
					button.globalID = self.searchResults[resultIndex]
					button.textureID, button.textureKind, button.texture = self:GetIconInfo(button.globalID)
					if button.globalID == self.selectedID then
						button:SetChecked(true)
						self.selectedButton = button
					end

					if self.showDynamicText then
						if button.textureKind == "Dynamic" then
							if not button.dynamicText then
								button.dynamicText = button:CreateFontString()
								button.dynamicText:SetFontObject("GameFontNormalSmall")
								button.dynamicText:SetPoint("BOTTOM", button, "BOTTOM", 0, 2)
								button.dynamicText:SetText("(dynamic)")
							end
							button.dynamicText:Show()
						else
							if button.dynamicText then button.dynamicText:Hide() end
						end
					end
				else
					button.globalID = nil
					button.textureID = nil
					button.texture = nil
					button.textureKind = nil
					button:SetChecked(false)
					if button.dynamicText then button.dynamicText:Hide() end
				end

				if button.texture then
					button:SetNormalTexture("Interface\\Icons\\" .. button.texture)
				else
					button:SetNormalTexture(nil)
				end

				-- Hook for the icon keyword editor (IKE) overlay
				if self.OnButtonUpdated then self.OnButtonUpdated(button) end
			end
		end
	end
end

-- ========================================================================================
-- HELPER FUNCTIONS

-- To prevent slow loading time, don't have WoW traverse the icons directory until an
-- icon selector is actually created.
function Helpers.InitialInit()
	if not initialized then
		initialized = true
		GetMacroIcons(MACRO_ICON_FILENAMES)
		GetMacroItemIcons(ITEM_ICON_FILENAMES)
	end
end

-- Creates a new object that is the overlay of "options" onto "defaults", and applies
-- a few dynamic defaults as well.
function Helpers.ApplyDefaults(options, defaults)
	if not options then options = { } end	-- (yes, some addons pass no options)
	
	local result = { }
	setmetatable(result, {
		__index = function(t, k)	-- (note: do NOT index t from __index or it may loop)
			local r = options[k]
			if r ~= nil then		-- (don't use "options[k] or defaults[k]" or false won't work)
				return r
			else
				return defaults[k]
			end
		end
	})

	-- Create any sections that weren't explicitly defined by the user.
	for _, sectionName in ipairs(result.sectionOrder) do
		if not result.sections[sectionName] then
			result.sections[sectionName] = Helpers.CreateDefaultSection(sectionName)
		end
	end

	-- 2012-03-20 COMPATIBILITY ISSUE: LibAdvancedIconSelector-1.0 revision 7 (v1.0.4) and above vs. AdvancedIconSelector v1.0.3 and below
	-- Old versions of AdvancedIconSelector relied on options.width and options.height being set to the default width / height upon return.
	-- THIS BEHAVIOR WILL BE REMOVED IN THE FUTURE, so don't rely on it or your addon WILL break.
	if not options.width then options.width = defaults.width end
	if not options.height then options.height = defaults.height end

	return result
end

-- Returns the strings that have the given tag (filters out the tags if nil)
function Helpers.GetTaggedStrings(str, tag)
	if not str then return nil end
	if not tag then
		return gsub(str, "[^ ]+:[^ ]+[ ]*", "")
	else
		local result = nil	-- (nil is by far the most common result - so don't return an empty table)
		for match in gmatch(" " .. str, " " .. tag .. ":([^ ]+)") do if not result then result = { match } else tinsert(result, match) end end
		return result
	end
end

-- Re-capitalizes each word of a string, excluding certain prepositions.  This is needed 'cause
-- all keyword data is lowercase (for quick searching).
function Helpers.Capitalize(str)
	local s, e, cap = 0, 0
	repeat
		s, e, cap = strfind(str, "([^ %-%.%(%)]+)[ %-%.%(%)]*", e + 1)
		if e then
			if s == 1 or not PREPS[cap] then
				str = strsub(str, 1, s - 1) .. strupper(strsub(str, s, s)) .. strsub(str, s + 1)
			end
		end
	until not e
	return str
end

-- Converts a keyword string like "ice_lance(mage)" into "Ice Lance (Mage)", r, g, b
function Helpers.MakeSpellTooltipString(str, defaultColor)
	str = gsub(str, "_", " ")	-- Re-insert spaces
	local spellName, className = strmatch(str, "^(.+)%(([^%)]+)%)$")
	if not className then spellName = str end	-- Sometimes class isn't specified.
	if spellName then
		-- Capitalize the first letter of each word, excluding certain prepositions.
		spellName = Helpers.Capitalize(spellName)

		if className then	-- (note: class name is sometimes also a profession name, race name, etc.)
			className = Helpers.Capitalize(className)
			local classColorIndex = strupper(className)
			if classColorIndex == "DEATH KNIGHT" then classColorIndex = "DEATHKNIGHT" end
			local classColor = RAID_CLASS_COLORS[classColorIndex]
			if classColor then
				return spellName .. " (" .. className .. ")", classColor.r, classColor.g, classColor.b
			else
				return spellName .. " (" .. className .. ")", defaultColor.r, defaultColor.g, defaultColor.b	-- invalid / multiple classes
			end
		end
		return spellName, defaultColor.r, defaultColor.g, defaultColor.b
	end
end

function Helpers.AddTaggedInformationToTooltip(keywordString, tag, tooltipTag, defaultColor)
	local spellNames = Helpers.GetTaggedStrings(keywordString, tag)
	if spellNames then
		for _,spellBundle in ipairs(spellNames) do
			local spellName, classR, classG, classB = Helpers.MakeSpellTooltipString(spellBundle, defaultColor)
			GameTooltip:AddLine(tooltipTag .. tostring(spellName), classR, classG, classB, false)
		end
	end
end

-- Creates one of several sections that don't have to be defined by the user.
function Helpers.CreateDefaultSection(name)
	if name == "DynamicIcon" then
		return { count = 1, GetIconInfo = function(index) return index, "Dynamic", "INV_Misc_QuestionMark" end }
	elseif name == "MacroIcons" then
		return { count = #MACRO_ICON_FILENAMES, GetIconInfo = function(index) return index, "Macro", libiconpath:getName(MACRO_ICON_FILENAMES[index]) end }
	elseif name == "ItemIcons" then	
		return { count = #ITEM_ICON_FILENAMES, GetIconInfo = function(index) return index, "Item", libiconpath:getName(ITEM_ICON_FILENAMES[index]) end }
	end
end

-- ================================================================
-- SEARCH OBJECT IMPLEMENTATION

-- Creates a new search object based on the specified options.
function SearchObject:Create(options)
	options = Helpers.ApplyDefaults(options, defaults)

	local search = SearchObject:Derive()
	search.options = options
	search.sections = options.sections
	search.sectionOrder = options.sectionOrder
	search.firstSearch = true
	search.shouldSkip = { }

	return search
end

-- Provides a callback for an event, with error checking.
function SearchObject:SetScript(script, callback)
	if script == "BeforeSearchStarted" then			-- Called just before the search is (re)started.  Parameters: (search)
		self.BeforeSearchStarted = callback
	elseif script == "OnSearchStarted" then			-- Called when the search is (re)started.  Parameters: (search)
		self.OnSearchStarted = callback
	elseif script == "OnSearchResultAdded" then		-- Called for each search result found.  Parameters: (search, texture, globalID, localID, kind)
		self.OnSearchResultAdded = callback
	elseif script == "OnSearchComplete" then		-- Called when the search is completed.  Parameters: (search)
		self.OnSearchComplete = callback
	elseif script == "OnIconScanned" then			-- Called for each icon scanned.  Parameters: (search, texture, globalID, localID, kind)
		self.OnIconScanned = callback
	elseif script == "OnSearchTick" then			-- Called after each search tick (or at a constant rate, if the search is not tick-based).  Parameters: (search)
		self.OnSearchTick = callback
	else
		error("Unsupported script type")
	end
end

-- Sets the search parameter and restarts the search.  Since this is generally called many times in a row
-- (whenever a textbox is changed), the search is delayed by about half a second unless immediateResults is
-- set to true, except for the first search performed with this search object.
function SearchObject:SetSearchParameter(searchText, immediateResults)
	self.searchParameter = searchText
	if self.initiateSearchTimer then lib:CancelTimer(self.initiateSearchTimer) end
	local delay = (immediateResults or self.firstSearch) and 0 or INITIATE_SEARCH_DELAY
	self.firstSearch = false
	self.initiateSearchTimer = lib:ScheduleTimer(self.private_OnInitiateSearchTimerElapsed, delay, self)
end

-- Returns the current search parameter.
function SearchObject:GetSearchParameter()
	return self.searchParameter
end

-- Replaces the specified section of icons.  Useful to change the icons within a custom section.
-- Also, causes the search to start over.
-- (see CreateDefaultSection for example section definitions)
-- (also, see EquipmentSetPopup.lua and MacroPopup.lua of AdvancedIconSelector for an example of actual use)
function SearchObject:ReplaceSection(sectionName, section)
	self.sections[sectionName] = section
	self:RestartSearch()
end

-- Sets whether or not icons of the given section will be skipped.  This does not restart the search, so you
-- may wish to call RestartSearch() afterward.
function SearchObject:ExcludeSection(sectionName, exclude)
	self.shouldSkip[sectionName] = exclude
end

-- Returns whether or not the icons of the given section will be skipped.
function SearchObject:IsSectionExcluded(sectionName)
	return self.shouldSkip[sectionName]
end

-- Called when the search actually starts - usually about half a second after the search parameter is changed.
function SearchObject:private_OnInitiateSearchTimerElapsed()
	self.initiateSearchTimer = nil	-- (single-shot timer handles become invalid IMMEDIATELY after elapsed)
	self:RestartSearch()
end

-- Restarts the search.
function SearchObject:RestartSearch()

	if self.BeforeSearchStarted then self.BeforeSearchStarted(self) end

	-- Load / reload the keyword library.
	-- (if keywordAddonName isn't specified, the default will be loaded)
	lib:LoadKeywords(self.options.keywordAddonName)

	-- Cancel any pending restart; we don't want to start twice.
	if self.initiateSearchTimer then
		lib:CancelTimer(self.initiateSearchTimer)
		self.initiateSearchTimer = nil
	end

	-- Parse the search parameter
	if self.searchParameter then
		local tmp = self:private_FixSearchParameter(self.searchParameter)
		local parts = { strsplit(";", tmp) }
		for i = 1, #parts do
			local tmp = strtrim(parts[i])
			parts[i] = { }
			for v in gmatch(tmp, "[^ ,]+") do
				tinsert(parts[i], v)
			end
		end
		self.parsedParameter = parts
	else
		self.parsedParameter = nil
	end

	-- Start at the icon with global ID of 1
	self.searchIndex = 0

	if self.OnSearchStarted then self.OnSearchStarted(self) end

	if not self.searchTimer then
		self.searchTimer = lib:ScheduleRepeatingTimer(self.private_OnSearchTick, SCAN_TICK, self)
	end
end

-- Immediately terminates any running search.  You can restart the search by calling RestartSearch(),
-- but it will start at the beginning.
function SearchObject:Stop()
	-- Stop any pending search.
	if self.initiateSearchTimer then
		lib:CancelTimer(self.initiateSearchTimer)
		self.initiateSearchTimer = nil	-- (timer handles are invalid once canceled)
	end

	-- Cancel any occurring search.
	if self.searchTimer then
		lib:CancelTimer(self.searchTimer)
		self.searchTimer = nil			-- (timer handles are invalid once canceled)
	end
end

-- Called on every tick of a search.
function SearchObject:private_OnSearchTick()
	for entriesScanned = 0,SCAN_PER_TICK do
		self.searchIndex = self.searchIndex + 1
		self.searchIndex = self:private_Skip(self.searchIndex)

		-- Is the search complete?
		if not self.searchIndex then
			lib:CancelTimer(self.searchTimer)
			self.searchTimer = nil  -- timer handles are invalid once canceled
			if self.OnSearchComplete then self:OnSearchComplete() end
			break
		end

		local id, kind, texture = self:GetIconInfo(self.searchIndex)
		
		texture = libiconpath:getName(texture)
		
		if self.OnIconScanned then self:OnIconScanned(texture, self.searchIndex, id, kind) end

		if texture then
			local keywordString = lib:LookupKeywords(texture)
			if self:private_Matches(texture, keywordString, self.parsedParameter) then
				if self.OnSearchResultAdded then self:OnSearchResultAdded(texture, self.searchIndex, id, kind) end
			end
		end
	end

	-- Notify that a search tick has occurred.
	if self.OnSearchTick then self:OnSearchTick() end
end

-- Returns the given global ID after skipping the designated categories, or nil if past the max global id.
function SearchObject:private_Skip(id)
	local origID = id
	
	if not id or id < 1 then
		return nil
	end

	local sectionStart = 1
	for _, sectionName in ipairs(self.sectionOrder) do
		local section = self.sections[sectionName]
		if section then
			if id >= 1 and id <= section.count then
				if self.shouldSkip[sectionName] then
					id = section.count + 1
				else
					return sectionStart + (id - 1)
				end
			end
			
			id = id - section.count
			sectionStart = sectionStart + section.count
		end
	end

	return nil
end

-- Lowercases a string, but preserves the case of any characters after a %.
function SearchObject:private_StrlowerPattern(str)
	local lastE = -1	-- (since -1 + 2 = 1)
	local result = ""

	repeat
		local s, e = strfind(str, "%", lastE + 2, true)
		if e then
			local nextLetter = strsub(str, e + 1, e + 1)
			result = result .. strlower(strsub(str, lastE + 2, e - 1)) .. "%" .. nextLetter
		else
			result = result .. strlower(strsub(str, lastE + 2))
		end
		lastE = e
	until not e
	return result
end

-- This function makes up for LUA's primitive string matching support and replaces all occurrances of a word
-- in a string, regardless of whether it's at the beginning, middle, or end.  This is extremely inefficient, so you
-- should only do it once: when the search parameter changes.
function SearchObject:private_ReplaceWord(str, word, replacement)
	local n
	str = gsub(str, "^" .. word .. "$", replacement)			-- (entire string)
	str = gsub(str, "^" .. word .. " ", replacement .. " ")		-- (beginning of string)
	str = gsub(str, " " .. word .. "$", " " .. replacement)		-- (end of string)
	repeat
		str, n = gsub(str, " " .. word .. " ", " " .. replacement .. " ")	-- (middle of string)
	until not n or n == 0
	return str
end

-- Coerces a search parameter to something useable.  Replaces NOT with !, etc.
function SearchObject:private_FixSearchParameter(parameter)

	-- Trim the parameter
	parameter = strtrim(parameter)

	-- Lowercase the string except for stuff after % signs. (since you can search by pattern)
	parameter = self:private_StrlowerPattern(parameter)

	-- Replace all "NOT" with !
	parameter = self:private_ReplaceWord(parameter, "not", "!")

	-- Replace all "AND" with ,
	parameter = self:private_ReplaceWord(parameter, "and", " ")

	-- Replace all "OR" with ;
	parameter = self:private_ReplaceWord(parameter, "or", ";")

	-- Join any !s to the word that follows it. (but only if the ! is standing on it's own)
	repeat
		local n1, n2
		parameter, n1 = gsub(parameter, "^(!+) +", "%1")
		parameter, n2 = gsub(parameter, " (!+) +", " %1")
	until n1 == 0 and n2 == 0

	-- Get rid of quotes; they have no meaning as of now and are used only to allow searching for "AND", "OR", and "NOT".
	parameter = gsub(parameter, '"+', "")

	-- Finally, get rid of any extra spaces
	parameter = gsub(strtrim(parameter), "  +", " ")

	return parameter
end

-- Returns true if the given texture / keywords matches the search parameter
function SearchObject:private_Matches(texture, keywords, parameter)
	if not parameter then return true end
	texture = libiconpath:getName(texture)
	texture = strlower(texture)
	keywords = keywords and strlower(keywords)
	for i = 1, #parameter do		-- OR parameters
		local p_i = parameter[i]
		local termFailed = false
		
		for j = 1, #p_i do			-- AND parameters
			local s = p_i[j]
			if #s > 0 then
				local xor = 0
				local plainText = true

				while strsub(s, 1, 1) == "!" do	-- ! indicates negation of this term
					s = strsub(s, 2)
					xor = bit.bxor(xor, 1)
				end

				if strsub(s, 1, 1) == "=" then	-- = indicates pattern matching for this term
					s = strsub(s, 2)
					plainText = false
				end

				local ok1, result1 = pcall(strfind, texture, s, 1, plainText)
				local result2 = false
				if keywords then result2 = select(2, pcall(strfind, keywords, s, 1, plainText)) end
				if not ok1 or bit.bxor((result1 or result2) and 1 or 0, xor) == 0 then
					termFailed = true
					break
				end
			end
		end

		if not termFailed then
			return true
		end
	end
end

-- Returns information about the icon at the given global index, or nil if out of range.
-- (Returns a tuple of id (within section), kind, texture)
function SearchObject:GetIconInfo(id)
	if not id or id < 1 then
		return nil
	end

	for _, sectionName in ipairs(self.sectionOrder) do
		local section = self.sections[sectionName]
		if section then
			if id >= 1 and id <= section.count then
				return section.GetIconInfo(id)	-- (returns 3 values: id, kind, texture)
			else
				id = id - section.count
			end
		end
	end

	return nil
end
