-- Copyright © 2024 @Groick (Twitch). All rights reserved.
local function L(key, ...) return EasyOptions:Localize(key, ...) end

local m_FormatItemsCache = {} -- Acts as a cache that stores the format item string (e.g., "{1}", "{2}", etc.) that corresponds to each index for improved performance.
local m_IsPrintingFormatStringFormatError = false
local m_HasPrintedFormatStringFormatError = false
local m_TemporaryDialogNames = {}

function EasyOptions:Localize(key, ...)
  return EasyOptions:LocalizeHelper("EasyOptions", EasyOptions.L, key, ...)
end
function EasyOptions:LocalizeHelper(addonName, localizationTable, key, ...)
  -- IMPORTANT: This method cannot rely on the localization table being stored in EasyOptions.Fields.AddonInfo[addonName] because most addons will require this method to be called while their OptionItems are being built which occurs before EasyOptions.Fields.AddonInfo[addonName] is populated.
  local value
  if (localizationTable == nil) then
    EasyOptions:PrintErrorMessageOnce(L("Unable to localize text for the addon \"{1}\" because the specified localization table is nil. Verify that the localization table is properly initialized before it is passed to EasyOptions:LocalizeHelper.", addonName))
    value = key
  elseif (localizationTable[key] == nil) or (localizationTable[key] == "") then
    value = key
  else
    value = localizationTable[key]
  end
  return EasyOptions:Format(value, ...)
end

function EasyOptions:Print(value, ...)
  if (not EasyOptionsDB["EnableMessages"]) then
    return
  end
  local color = nil -- Use the default color.
  EasyOptions:PrintHelper(value, color, ...)
end
function EasyOptions:PrintHelper(value, color, ...)
  local text = EasyOptions:Format(value, ...)
  if (text == nil) or (text == "") then
    return
  end
  local colorLocal = color or EasyOptionsDB["PrintTextColor"]
  DEFAULT_CHAT_FRAME:AddMessage(text, colorLocal[1], colorLocal[2], colorLocal[3])
end
function EasyOptions:PrintMessage(text, ...)
  if (not EasyOptionsDB["EnableMessages"]) then
    return
  end
  local color = nil -- Use the default color.
  EasyOptions:PrintMessageHelper("EasyOptions", text, color, ...)
end
function EasyOptions:PrintMessageHelper(addonName, text, color, ...)
  if (text == nil) then
    return
  end
  local prefix = ""
  if (addonName ~= nil) and (addonName ~= "") then
    prefix = EasyOptions:ConvertTextToColoredText(addonName..": ", EasyOptionsDB["AddonNameColor"])
  end
  local textLocal = prefix..text
  local colorLocal = color or EasyOptionsDB["GeneralMessageColor"]
  EasyOptions:PrintHelper(textLocal, colorLocal, ...)
end
function EasyOptions:PrintErrorMessage(text, ...)
  if (not EasyOptionsDB["EnableMessages"]) then
    return
  end
  local color = nil -- Use the default color.
  EasyOptions:PrintErrorMessageHelper("EasyOptions", text, color, ...)
  if (EasyOptionsDB["ThrowErrorWhenPrintErrorMessage"]) then
    error(EasyOptions:Format(text, ...))
  end
end
function EasyOptions:PrintErrorMessageHelper(addonName, text, color, ...)
  local colorLocal = color or EasyOptionsDB["ErrorMessageColor"]
  EasyOptions:PrintMessageHelper(addonName, text, colorLocal, ...)
end
function EasyOptions:PrintMessageOnce(text, ...)
  if (not EasyOptionsDB["EnableMessages"]) then
    return false
  end
  local isError = false
  return EasyOptions:PrintOnceHelper("EasyOptions", text, isError, EasyOptions.Fields.PrintedMessages, ...)
end
function EasyOptions:PrintErrorMessageOnce(text, ...)
  if (not EasyOptionsDB["EnableMessages"]) then
    return false
  end
  local isError = true
  local result = EasyOptions:PrintOnceHelper("EasyOptions", text, isError, EasyOptions.Fields.PrintedMessages, ...)
  if (result) and (EasyOptionsDB["ThrowErrorWhenPrintErrorMessage"]) then
    error(EasyOptions:Format(text, ...))
  end
  return result
end
function EasyOptions:PrintOnceHelper(addonName, text, isError, printedMessages, ...)
  -- Description: Prints the specified text if the text has not yet been printed.
  --  Returns true if the text was printed, otherwise false.
  if (text == nil) then
    return false
  end
  if (printedMessages[text]) then
    return false
  end
  printedMessages[text] = true
  local color = nil -- Use the default color.
  if (not isError) then
    EasyOptions:PrintMessageHelper(addonName, text, color, ...)
  else
    EasyOptions:PrintErrorMessageHelper(addonName, text, color, ...)
  end
  return true
end
function EasyOptions:PrintItems(items, includeEnclosures)
  EasyOptions:Print(EasyOptions:FormatItems(items, includeEnclosures))
end
function EasyOptions:PrintKeyValuePair(key, value, indent, prefix, valueDelimiter, includeKey, includeValue)
  local text = EasyOptions:FormatKeyValuePair(key, value, indent, prefix, valueDelimiter, includeKey, includeValue)
  EasyOptions:Print(text)
end
function EasyOptions:PrintKeyValuePairs(items, indent, prefix, valueDelimiter, filter, includeLineNumber, includeKey, includeValue)
  local text, lines = EasyOptions:FormatKeyValuePairs(items, indent, prefix, valueDelimiter, filter, includeLineNumber, includeKey, includeValue)
  local length = EasyOptions:GetLength(lines)
  for i = 1, length do
    EasyOptions:Print(lines[i])
  end
end

function EasyOptions:Format(value, ...)
  local hasFormatStringArgs, args = EasyOptions:Format_HasFormatStringArgs(value, ...)
  if (hasFormatStringArgs) then
    return EasyOptions:Format_FormatString(value, args)
  end
  
  if (value == nil) then
    return "nil"
  elseif (type(value) == "string") then
    return value
  elseif (type(value) == "number") then
    return EasyOptions:FormatFloatHighPrecision(value)
  elseif (type(value) == "boolean") then
    return EasyOptions:FormatBoolean(value)
  elseif (EasyOptions:IsColor(value)) then
    return EasyOptions:FormatColor(value)
  elseif (type(value) == "table") then
    return EasyOptions:FormatItems(value, true)
  else
    return type(value)
  end
end
function EasyOptions:Format_HasFormatStringArgs(value, ...) -- hasFormatStringArgs, args
  if (m_IsPrintingFormatStringFormatError) then
    return false, nil
  end
  if (type(value) ~= "string") then
    return false, nil
  end
  local args = {...} -- Adds all extra parameters that were passed to this method to a table.
  if (EasyOptions:GetLength(args) == 0) then
    if (string.find(value, "{0}")) or (string.find(value, "{1}")) then -- Assume that all of the extra parameters are nil if this case is true in which case the length of args will be 0 because items with a value of nil will be excluded from the table.
      return true, args
    else
      return false, nil
    end
  end
  return true, args
end
function EasyOptions:Format_FormatString(value, args)
  -- Description: Converts specified items to strings and inserts them into the specified format string.

  -- Check to see if the string contains "{0}" to ensure that the developer is not using 0-based indexes.
  if (not m_HasPrintedFormatStringFormatError) and (string.find(value, "{0}")) then -- IMPORTANT: We must check m_HasPrintedFormatStringFormatError in order to prevent a possible stack overflow. We also do so in order to prevent possible error message spam.
    m_HasPrintedFormatStringFormatError = true
    m_IsPrintingFormatStringFormatError = true
    EasyOptions:PrintErrorMessageOnce(L("The following format string contains a 0-based index. Format items in format strings must use 1-based indexes.\n  ")..value)
    m_IsPrintingFormatStringFormatError = false
  end

  -- Replace each format item (e.g., "{1}", "{2}", etc.) in the source string with the string representation of the corresponding item in the specified arguments.
  --  NOTE: We use {1}, {2}, etc. format (rather than lua's %1$s, %2$s, etc. format) so the formating structure is similar to .NET languages.
  local length = math.max(EasyOptions:GetLength(args), 20) -- Always search for a minimum number of format items in case one or more of the final items in args are nil in which case those items will be excluded from the table and therefore excluded when we determine the length of the table.
  local result = value -- init. Example: "The value type is {1} and the value is {2}."
  local stringToFind
  local stringToInsert
  for i = 1, length do
    if (m_FormatItemsCache[i] == nil) then
      m_FormatItemsCache[i] = "{"..tostring(i).."}" -- Example: "{1}"
    end
    stringToFind = m_FormatItemsCache[i]
    stringToInsert = EasyOptions:Format(args[i]) -- Example: "number"
    result = string.gsub(result, stringToFind, stringToInsert) -- Example: "The value type is number and the value is {2}."
  end
  for i = length, 999 do -- Replace all remaining format items with nil. IMPORTANT: This is required if one or more of the final items in the extra parameters were nil in which case those items will be excluded from the args table and therefore excluded when we determine the length of the table.
    if (m_FormatItemsCache[i] == nil) then
      m_FormatItemsCache[i] = "{"..tostring(i).."}" -- Example: "{1}"
    end
    stringToFind = m_FormatItemsCache[i]
    stringToInsert = nil
    if (not string.find(result, stringToFind)) then
      break
    end
    result = string.gsub(result, stringToFind, stringToInsert)
  end
  return result
end
function EasyOptions:FormatName(value)
  if (value == nil) or (value == "") then
    return L("[No Name]")
  end
  return EasyOptions:Format(value)
end
function EasyOptions:FormatValue(value, customFormatString)
  if (customFormatString ~= nil) and (customFormatString ~= "") and (type(customFormatString) == "string") then
    return string.format(customFormatString, value) -- tostring(value)
  else
    return EasyOptions:FormatFloatMaxPrecision(value)
  end
end
function EasyOptions:FormatBoolean(value)
  if (value) then
    return "true"
  else
    return "false"
  end
end
function EasyOptions:FormatNumber(value, formatType)
  if (formatType == "float") then
    return EasyOptions:FormatFloat(value)
  elseif (formatType == "max") then
    return EasyOptions:FormatFloatMaxPrecision(value)
  elseif (formatType == "high") then
    return EasyOptions:FormatFloatHighPrecision(value)
  elseif (formatType == "low") then
    return EasyOptions:FormatFloatLowPrecision(value)
  elseif (formatType == "integer") then
    return EasyOptions:FormatInteger(value)
  elseif (formatType == "large") then
    return EasyOptions:FormatLargeInteger(value)
  elseif (formatType == "percent") then
    return EasyOptions:FormatPercent(value)
  elseif (formatType == "percenthigh") then
    return EasyOptions:FormatPercentHighPrecision(value)
  elseif (formatType == "percentlow") then
    return EasyOptions:FormatPercentLowPrecision(value)
  elseif (formatType == "time") then
    return EasyOptions:FormatTime(value)
  elseif (formatType == "cooldown") then
    return EasyOptions:FormatCooldownTime(value)
  else
    return string.formatType(formatType, value)
  end
end
function EasyOptions:FormatFloat(value)
  local valueAbs = math.abs(value)
  local result
  if (valueAbs >= 1000) then
    result = string.format("%.0f", value) -- IMPORTANT: Don't use "%i" as the format string because doing so will cause an integer overflow error if the value is too large to be represented as an integer. -- NOTE: 999,999,999,999,999 is the largest value that can be represented by the "%.0f" format string without a loss of precision.
  elseif (valueAbs >= 10) then
    result = string.format("%.1f", value)
  elseif (valueAbs >= 1) then
    result = string.format("%.2f", value)
  else
    result = string.format("%.3f", value)
  end
  return EasyOptions:RemoveTrailingZeros(result)
  -- IMPORTANT: We don't use the following technique because it can result in small numbers being formated in scientific notation (e.g., string.format("%.4g", 0.000049) = "4.9e-05").
  -- local valueLocal = EasyOptions:Round(value, 0.0001)
  -- local valueAbs = math.abs(valueLocal)
  -- if (valueAbs < 0.00005) then -- NOTE: This is to account for potential rounding errors in the WoW API.
  --   return "0"
  -- elseif (valueAbs > 9999) then -- This case is implemented to prevent large numbers from being formated in scientific notation.
  --   return string.format("%.0f", valueLocal) -- IMPORTANT: Don't use "%i" as the format string because doing so will cause an integer overflow error if the value is too large to be represented as an integer. -- NOTE: 999,999,999,999,999 is the largest value that can be represented by the "%.0f" format string without a loss of precision.
  -- else
  --   return string.format("%.4g", valueLocal)
  -- end
end
function EasyOptions:FormatFloatHighPrecision(value)
  local valueAbs = math.abs(value)
  local result
  if (valueAbs >= 100000) then
    result = string.format("%.0f", value) -- IMPORTANT: Don't use "%i" as the format string because doing so will cause an integer overflow error if the value is too large to be represented as an integer. -- NOTE: 999,999,999,999,999 is the largest value that can be represented by the "%.0f" format string without a loss of precision.
  elseif (valueAbs >= 10000) then
    result = string.format("%.1f", value)
  elseif (valueAbs >= 1000) then
    result = string.format("%.2f", value)
  elseif (valueAbs >= 100) then
    result = string.format("%.3f", value)
  elseif (valueAbs >= 10) then
    result = string.format("%.3f", value)
  elseif (valueAbs >= 1) then
    result = string.format("%.4f", value)
  else
    result = string.format("%.5f", value)
  end
  return EasyOptions:RemoveTrailingZeros(result)
  -- IMPORTANT: We don't use the following technique because it can result in small numbers being formated in scientific notation (e.g., string.format("%.4g", 0.000049) = "4.9e-05").
  -- local valueLocal = EasyOptions:Round(value, 0.000001)
  -- local valueAbs = math.abs(valueLocal)
  -- if (valueAbs < 0.0000005) then -- NOTE: This is to account for potential rounding errors in the WoW API.
  --   return "0"
  -- elseif (valueAbs > 999999) then -- This case is implemented to prevent large numbers from being formated in scientific notation.
  --   return string.format("%.0f", valueLocal) -- IMPORTANT: Don't use "%i" as the format string because doing so will cause an integer overflow error if the value is too large to be represented as an integer. -- NOTE: 999,999,999,999,999 is the largest value that can be represented by the "%.0f" format string without a loss of precision.
  -- else
  --   return string.format("%.6g", valueLocal)
  -- end
end
function EasyOptions:FormatFloatMaxPrecision(value)
  -- NOTE: This method does not use tostring(value) because doing so can cause rounding errors to be represented in the string (e.g., we assume that we want a value like 1.2300000001 to be formated to "1.23", not "1.2300000001").
  local valueAbs = math.abs(value)
  local result
  if (valueAbs > 99999999) then -- This case is implemented to prevent large numbers from being formated in scientific notation.
    result = string.format("%.0f", valueLocal) -- IMPORTANT: Don't use "%i" as the format string because doing so will cause an integer overflow error if the value is too large to be represented as an integer. -- NOTE: 999,999,999,999,999 is the largest value that can be represented by the "%.0f" format string without a loss of precision.
  else
    result = string.format("%.8f", value)
  end
  return EasyOptions:RemoveTrailingZeros(result)
  -- IMPORTANT: We don't use the following technique because it can result in small numbers being formated in scientific notation (e.g., string.format("%.4g", 0.000049) = "4.9e-05").
  -- local valueLocal = EasyOptions:Round(value, 0.00000001)
  -- local valueAbs = math.abs(valueLocal)
  -- if (valueAbs < 0.000000005) then -- NOTE: This is to account for potential rounding errors in the WoW API.
  --   return "0"
  -- elseif (valueAbs > 99999999) then -- This case is implemented to prevent large numbers from being formated in scientific notation.
  --   return string.format("%.0f", valueLocal) -- IMPORTANT: Don't use "%i" as the format string because doing so will cause an integer overflow error if the value is too large to be represented as an integer. -- NOTE: 999,999,999,999,999 is the largest value that can be represented by the "%.0f" format string without a loss of precision.
  -- else
  --   return string.format("%.8g", valueLocal)
  -- end
end
function EasyOptions:FormatFloatLowPrecision(value)
  local valueLocal = EasyOptions:Round(value, 0.1)
  local valueAbs = math.abs(valueLocal)
  if (valueAbs < 0.05) then -- NOTE: This is to account for potential rounding errors in the WoW API.
    return "0.0"
  elseif (valueAbs >= 10) then
    return string.format("%.0f", valueLocal) -- IMPORTANT: Don't use "%i" as the format string because doing so will cause an integer overflow error if the value is too large to be represented as an integer. -- NOTE: 999,999,999,999,999 is the largest value that can be represented by the "%.0f" format string without a loss of precision.
  else
    return string.format("%.1f", valueLocal)
  end
end
function EasyOptions:FormatInteger(value)
  return string.format("%i", value)
end
function EasyOptions:FormatLargeInteger(value)
  local valueLocal, formatStringLocal, suffixLocal
  if (value <= 1000) then
    valueLocal = value
    formatStringLocal = "%i"
    suffixLocal = ""

  elseif (value <= 10000) then
    valueLocal = value / 1000
    formatStringLocal = "%.2f"
    suffixLocal = "k"
  elseif (value <= 100000) then
    valueLocal = value / 1000
    formatStringLocal = "%.1f"
    suffixLocal = "k"
  elseif (value <= 1000000) then
    valueLocal = value / 1000
    formatStringLocal = "%i"
    suffixLocal = "k"
    
  elseif (value <= 10000000) then
    valueLocal = value / 1000000
    formatStringLocal = "%.2f"
    suffixLocal = "M"
  elseif (value <= 100000000) then
    valueLocal = value / 1000000
    formatStringLocal = "%.1f"
    suffixLocal = "M"
  elseif (value <= 1000000000) then
    valueLocal = value / 1000000
    formatStringLocal = "%i"
    suffixLocal = "M"
  
  elseif (value <= 10000000000) then
    valueLocal = value / 1000000000
    formatStringLocal = "%.2f"
    suffixLocal = "B"
  elseif (value <= 100000000000) then
    valueLocal = value / 1000000000
    formatStringLocal = "%.1f"
    suffixLocal = "B"
  elseif (value <= 1000000000000) then
    valueLocal = value / 1000000000
    formatStringLocal = "%i"
    suffixLocal = "B"
  
  elseif (value <= 10000000000000) then
    valueLocal = value / 1000000000000
    formatStringLocal = "%.2f"
    suffixLocal = "T"
  elseif (value <= 100000000000000) then
    valueLocal = value / 1000000000000
    formatStringLocal = "%.1f"
    suffixLocal = "T"
  elseif (value <= 1000000000000000) then
    valueLocal = value / 1000000000000
    formatStringLocal = "%i"
    suffixLocal = "T"
  end
  return string.format(formatStringLocal, valueLocal)..suffixLocal
end
function EasyOptions:FormatPercent(value)
  if (value < 9.95) then
    return (string.format("%.1f", value).."%")
  else
    return (string.format("%i", value).."%")
  end
end
function EasyOptions:FormatPercentHighPrecision(value)
  if (value < 9.995) then
    return (string.format("%.2f", value).."%")
  elseif (value < 99.95) then
    return (string.format("%.1f", value).."%")
  else
    return (string.format("%i", value).."%")
  end
end
function EasyOptions:FormatPercentLowPrecision(value)
  return (string.format("%i", value).."%")
end
function EasyOptions:FormatTime(duration)
  if (duration <= 2) then
    return string.format("%.1f", duration)
  else
    local durationLocal = duration
    local hours = floor((durationLocal + 30) / 3600) -- NOTE: We add 30 seconds (i.e., half of one minute) in order to avoid results like 1:60:00, 2:60:00, etc. (e.g., if duration = 7170 seconds then we want the result to be 2:00:00 rather than 1:60:00).
    durationLocal = (durationLocal - (hours * 3600))
    local minutes = floor((durationLocal + 0.5) / 60) -- NOTE: We add 0.5 seconds (i.e., half of one second) in order to avoid results like 1:60, 2:60, etc. (e.g., if duration = 119.5 seconds then we want the result to be 2:00 rather than 1:60).
    durationLocal = (durationLocal - (minutes * 60))
    local seconds = durationLocal
    if (hours > 0) then
      return string.format("%.1fh", duration / 3600)
      --return string.format(">%ih", hours)
      --return string.format("%i:%02i:%02i", hours, minutes, seconds)
      --local h = EasyOptions:FormatTimeElement(0, hours)
      --local m = EasyOptions:FormatTimeElement(hours, minutes)
      --local s = EasyOptions:FormatTimeElement(minutes, seconds)
      --return (h..":"..m..":"..s)
    elseif (minutes >= 10) then
      return string.format("%.1fm", duration / 60)
      --return string.format(">%im", minutes)
      --return string.format("%i:%02i", minutes, seconds)
      --local m = EasyOptions:FormatTimeElement(0, minutes)
      --local s = EasyOptions:FormatTimeElement(minutes, seconds)
      --return (m..":"..s)
    elseif (minutes > 0) then
      return string.format("%i:%02i", minutes, seconds)
      --local m = EasyOptions:FormatTimeElement(0, minutes)
      --local s = EasyOptions:FormatTimeElement(minutes, seconds)
      --return (m..":"..s)
    else
      return string.format("%i", seconds)
      --return EasyOptions:FormatTimeElement(0, seconds)
    end
  end
end
function EasyOptions:FormatTimeElement(parentDuation, duration)
  if (parentDuation == 0) then
    return string.format("%i", duration)
  else
    return string.format("%02i", duration)
  end
  --local timeElement = string.format("%i",duration)
  --if (parentDuation == 0) then
    --return timeElement
  --else -- (parentDuation ~= 0)
    --local timeElementLength = EasyOptions:GetStringLength(timeElement)
    --if (timeElementLength == 1) then
      --return ("0"..timeElement)
    --else
      --return timeElement
    --end
  --end
end
function EasyOptions:FormatCooldownTime(duration)
  if (duration <= 0) then
    return ""
  else
    return EasyOptions:FormatTime(duration)
  end
end
function EasyOptions:FormatColor(value)
  if (type(value) == "number") then
    return string.format("%.3f", value)
  elseif (type(value) == "table") then
    local length = EasyOptions:GetLength(value)
    local includeEnclosures = (length >= 2)
    local text = ""
    if (includeEnclosures) then
      text = text.."("
    end
    for i = 1, length do
      if (i > 1) then
        text = text..", "
      end
      text = text..EasyOptions:FormatColor(value[i])
    end
    if (includeEnclosures) then
      text = text..")"
    end
    return text
  else
    return EasyOptions:Format(value)
  end
end
function EasyOptions:FormatItems(items, includeEnclosures)
  if (items == nil) or (type(items) ~= "table") then
    return EasyOptions:Format(items)
  end

  local length = EasyOptions:GetLength(items)
  local text = ""
  local textCurrent
  local stringLengthLimit = EasyOptionsDB["TableToStringCharacterLimit"] + EasyOptions:IIF(includeEnclosures, 1, 0) -- Allow one extra character for the initial enclosure if includeEnclosures = true.
  if (includeEnclosures) then
    text = text.."("
  end
  for i = 1, length do
    if (i > 1) then
      text = text..", "
    end
    textCurrent = EasyOptions:Format(items[i])
    if (string.len(text) >= stringLengthLimit) then
      text = text.."..."
      break
    else
      text = text..textCurrent
    end
  end
  if (includeEnclosures) then
    text = text..")"
  end
  return text
end
function EasyOptions:FormatItemsAdvanced(items, delimiter, formatString, prefix, suffix)
  if (type(items) == "string") then
    return items
  elseif (items == nil) or (type(items) ~= "table") then
    return EasyOptions:Format(items)
  end

  local length = EasyOptions:GetLength(items)
  local text = ""
  local textCurrent
  local delimiterLocal = delimiter or ""
  local useToString = (formatString == nil)
  if (prefix ~= nil) then
    text = text..prefix
  end
  for i = 1, length do
    if (i > 1) then
      text = text..delimiterLocal
    end
    if (type(items[i]) == "string") then
      textCurrent = tostring(items[i])
    elseif (type(items[i]) ~= "number") then
      textCurrent = EasyOptions:Format(items[i])
    elseif (useToString) then
      textCurrent = tostring(items[i])
    else
      textCurrent = string.format(formatString, items[i])
    end
    text = text..textCurrent
  end
  if (suffix ~= nil) then
    text = text..suffix
  end
  return text
end
function EasyOptions:FormatKeyValuePair(key, value, indent, prefix, valueDelimiter, includeKey, includeValue)
  local includeKeyLocal = (includeKey == nil) or includeKey
  local includeValueLocal = (includeValue == nil) or includeValue
  if (not includeKeyLocal) and (not includeValueLocal) then
    return ""
  end

  local result = indent or ""
  if (prefix ~= nil) and (prefix ~= "") then
    result = result..prefix
  end
  if (includeKeyLocal) then
    result = result..tostring(key)
  end
  if (includeKeyLocal) and (includeValueLocal) then
    local valueDelimiterLocal = valueDelimiter or " = "
    result = result..valueDelimiterLocal
  end
  if (includeValueLocal) then
    result = result..EasyOptions:Format(value)
  end
  return result
end
function EasyOptions:FormatKeyValuePairs(items, indent, prefix, valueDelimiter, filter, includeLineNumber, includeKey, includeValue) -- text, lines
  local includeLineNumberLocal = (includeLineNumber ~= nil) and includeLineNumber
  local includeKeyLocal = (includeKey == nil) or includeKey
  local includeValueLocal = (includeValue == nil) or includeValue
  if (not includeKeyLocal) and (not includeValueLocal) then
    return ""
  end

  local indentLocal = indent or ""
  if (items == nil) then
    return indentLocal..EasyOptions:Format(nil)
  end
  
  local hasFilter = (filter ~= nil) and (filter ~= "")
  local lines = {}
  local itemText, itemLines
  for key, value in pairs(items) do -- Iterate through all key, value pairs in the table.
    if (type(value) == "table") then
      local prefixLocal = ""
      itemText, itemLines = EasyOptions:FormatKeyValuePairs(value, indentLocal.."  ", prefixLocal, valueDelimiter, filter, includeLineNumberLocal, includeKeyLocal, includeValueLocal)
      if (not hasFilter) or (EasyOptions:StringContains(key, filter)) or (itemText ~= nil) or (itemText ~= "") then
        itemText = indentLocal..tostring(key)..":"..Apex:IIF(itemText == "", "", "\n")..itemText
        table.insert(lines, itemText)
      end
    else
      itemText = EasyOptions:FormatKeyValuePair(key, value, indentLocal, prefix, valueDelimiter, includeKeyLocal, includeValueLocal)
      if (not hasFilter) or (EasyOptions:StringContains(itemText, filter)) then
        table.insert(lines, itemText)
      end
    end
  end

  local ignoreCase = true
  EasyOptions:Sort(lines, ignoreCase)
  
  local length = Apex:GetLength(lines)
  local prefix = ""
  for i = 1, length do
    if (includeLineNumberLocal) then
      prefix = tostring(i).." "
    end
    lines[i] = prefix..lines[i]
  end

  local text = "" -- init
  for i = 1, length do
    if (text ~= nil) and (text ~= "") then
      text = text.."\n"
    end
    text = text..lines[i]
  end

  return text, lines
end

function EasyOptions:GetFunctionParametersAsString(func, includeEnclosures)
  -- NOTE: Logic in this method should be valid, but it requires the "debug" Lua namespace which appears to be set to nil by the WoW API.

  if (func == nil) then
    return ""
  end

  local parameters = {}
  length = debug.getinfo(func).nparams
  for i = 1, length do
    table.insert(parameters, debug.getlocal(func, i))
  end

  return EasyOptions:FormatItems(parameters, includeEnclosures)
end

function EasyOptions:StringContains(text, value)
  if (text == nil) then
    return false
  end
  if (value == nil) then
    return false
  elseif (value == "") then
    return true
  end
  if (#text < #value) then
    return false
  end
  local iStart, iEnd = string.find(text, value, 1, true)
  return (iStart ~= nil)
end
function EasyOptions:StringStartsWith(text, value)
  if (text == nil) then
    return false
  end
  if (value == nil) then
    return false
  elseif (value == "") then
    return true
  end
  if (#text < #value) then
    return false
  end
  local iStart, iEnd = string.find(text, value, 1, true)
  return (iStart == 1)
end
function EasyOptions:StringEndsWith(text, value)
  if (text == nil) then
    return false
  end
  if (value == nil) then
    return false
  elseif (value == "") then
    return true
  end
  if (#text < #value) then
    return false
  end
  local iStart, iEnd = string.find(text, value, #text - #value + 1, true)
  return (iEnd == #text)
end

function EasyOptions:SplitString(text, pattern, trimSubStrings, disablePatternMatching)
  -- Description: Splits the specified string by searching for the specified pattern and then returns the result as a table.
  --  trimSubStrings: Indicates whether to trim each of the found sub-strings.
  --  disablePatternMatching: Indicates whether to disable pattern matching, in which case the search treats the pattern parameter as plain text and no characters in the pattern parameter are considered to be "magic".
  -- HINT: Use EasyOptions:SplitString(text, "", false, false) to get the individual characters of a string. See also EasyOptions:GetCharacters(text).
  local result = {}
  for match in EasyOptions:SplitString_FindMatch(text, pattern, disablePatternMatching) do
    if (trimSubStrings) and (match ~= nil) then
      match = EasyOptions:TrimString(match)
    end
    if (match ~= nil) then
      table.insert(result, match)
    end
  end
  return result
end
function EasyOptions:SplitString_FindMatch(text, pattern, disablePatternMatching)
  local splitStart = 1
  local length = EasyOptions:GetStringLength(text)
  return function ()
    if (splitStart) then
      if (text == nil) then
        splitStart = nil
        return nil
      end
      if (pattern == nil) then
        splitStart = nil
        return text
      end
      local delimiterStart, delimiterEnd = string.find(text, pattern, splitStart, disablePatternMatching)
      local result
      if (not delimiterStart) then
        result = string.sub(text, splitStart)
        splitStart = nil
      elseif (delimiterEnd < delimiterStart) then -- This case will be true if delimiter is an empty string, in which case we split each character in the specified string.
        result = string.sub(text, splitStart, delimiterStart)
        if (delimiterStart < length) then
          splitStart = delimiterStart + 1
        else
          splitStart = nil
        end
      else
        result = ((delimiterStart > splitStart) and string.sub(text, splitStart, delimiterStart - 1)) or "" -- NOTE: We set the result to an empty string if we found two consecutive delimiters.
        splitStart = delimiterEnd + 1
      end
      return result
    end
  end
end
function EasyOptions:GetCharacters(text)
  -- Description: Returns the individual characters of the string as a table.
  return EasyOptions:SplitString(text, "", false, false)
end
function EasyOptions:GetStringLength(text)
  if (text == nil) then
    return 0
  else
    return string.len(text) -- #text
  end
end
function EasyOptions:TrimString(text)
  if (text == nil) then
    return nil
  end
  return text:gsub("^%s*(.-)%s*$", "%1")
end
function EasyOptions:TrimStrings(list)
  -- Description: Trims all strings in the specified list, including strings in child lists.

  local length = EasyOptions:GetLength(list)
  local item
  for i = 1, length do
    if (type(list[i]) == "table") then
      EasyOptions:TrimStrings(list[i])
    else
      list[i] = EasyOptions:TrimString(list[i])
    end
  end
end
function EasyOptions:AppendName(text, name, ignoreEmptyStrings)
  if (text == nil) then
    return name
  end
  if (name == nil) then
    return text
  end
  if (text == "") and (ignoreEmptyStrings) then
    return name
  end
  if (name == "") and (ignoreEmptyStrings) then
    return text
  end
  return text.."."..name
end
function EasyOptions:RemoveTrailingZeros(text)
  if (text == nil) then
    return nil
  end
  local result = text
  result = result:gsub("(%..-)0*$", "%1")
  result = result:gsub("%.$", "")
  return result
end
function EasyOptions:ReplaceCharactersInStringBeforeSearch(text)
  -- Description: Replaces characters or sets of characters in a string in order to prepare the string for a standard search.
  if (text == nil) then
    return nil
  end
  local result = text
  result = result:gsub("||", "|") -- The WoW API always replaces each "|" character with two "|" characters so we want to undo that action. Note that this behavior can be difficult to detect because using the WoW API to print "String1|String2" and "String1||String2" will output "String1|String2" for both strings. But, using EasyOptions:PrintItems(EasyOptions:SplitString(text, "", false, false)) will show the individual characters.
  return result
end
function EasyOptions:GetDefaultItemNameFromValue(value, defaultItemName)
  -- Description: Returns a string that is appropriate to use as a default name for the specified value.
  if (type(value) ~= "string") then
    return defaultItemName
  end

  local valueLocal = value -- init
  local valueLocal = EasyOptions:TrimString(valueLocal)
  if (valueLocal == "") then
    return defaultItemName
  end
  
  valueLocal = EasyOptions:ReplaceCharactersInStringBeforeSearch(value)
  local splitString = EasyOptions:SplitString(valueLocal, "[\\/|]", true, false) -- Split the string in to substrings that are separated by common divider characters (e.g., "\", "/", and "|").
  
  local length = EasyOptions:GetLength(splitString)
  local result = splitString[length]
  if (result == nil) or (result == "") then
    result = EasyOptions:TrimString(value)
  end
  return result
end
function EasyOptions:GetSlashCommandArgs(text, delimiter)
  -- Description: Splits the specified string in to a table of slash commands and returns the result. Also returns the modified string from which the slash commands were determined.
  local textLocal = EasyOptions:ReplaceCharactersInStringBeforeSearch(text)
  if (textLocal == nil) then
    return nil
  end
  return EasyOptions:SplitString(textLocal, delimiter, true, true), textLocal
end

function EasyOptions:AreEqual(value1, value2, allowAreApproximatelyEqual)
  if (value1 == value2) then -- NOTE: This will be true if both values are nil (in which case type(value1) == type(value2) == nil).
    return true
  elseif (value1 == nil) or (value2 == nil) then
    return false
  end

  local type1 = type(value1)
  local type2 = type(value2)
  if (allowAreApproximatelyEqual) and (type1 == "number") and (value1 ~= 0) and (type2 == "number") and (value2 ~= 0) then -- NOTE: We don't allow the values to be considered approximately equal if one of the values are 0 and the other value is not 0 because we assume that we want both values to be exactly equal to 0 in that case.
    local referenceValue = 0.5 * (math.abs(value1) + math.abs(value2)) -- Use the mean of the magnitudes of the values as the reference value from which the allowed delta is determined.
    local valueEquivalencyFactor = EasyOptions:GetValueEquivalencyFactor()
    local deltaAllowed = EasyOptions:LimitValue(valueEquivalencyFactor * referenceValue, valueEquivalencyFactor, 1.0 - valueEquivalencyFactor) -- Limit the allowed delta to a value between 0.000002 and 0.999998
    local delta = math.abs(value2 - value1)
    return (delta <= deltaAllowed)
  end
  if (type1 ~= "table") or (type2 ~= "table") then
    return false
  end
  
  local length1 = EasyOptions:GetLengthByCount(value1) -- IMPORTANT: We must use EasyOptions:GetLengthByCount rather than EasyOptions:GetLength in order to ensure that we account for all items in the list.
  local length2 = EasyOptions:GetLengthByCount(value2) -- IMPORTANT: We must use EasyOptions:GetLengthByCount rather than EasyOptions:GetLength in order to ensure that we account for all items in the list.
  if (length1 ~= length2) then
    return false
  end
  
  for key, value in pairs(value1) do -- Iterate through all key, value pairs in the table. Use of value1 rather than value2 is arbitrary.
    if (not EasyOptions:AreEqual(value1[key], value2[key], allowAreApproximatelyEqual)) then
      return false
    end
  end
  return true
end
function EasyOptions:GetValueEquivalencyFactor()
  -- Description: Specifies the maximum allowed relative difference between two values that are considered approximately equal.
  return 0.000002
end

function EasyOptions:IsNumeric(value) -- isNumeric, number
  -- Description: Indicates whether value represents a number.
  if (value == nil) then
    return false, nil
  end
  if (type(value) == "number") then
    return true, value
  end

  local number = tonumber(value) -- NOTE: tonumber will return nil if value cannot be converted to a number.
  return (number ~= nil), number
end
function EasyOptions:IsColor(value)
  -- Validate value.
  if (value == nil) then
    return false
  end
  if (type(value) ~= "table") then
    return false
  end

  local length = EasyOptions:GetLength(value)
  if (length ~= 3) and (length ~= 4) then
    return false
  end
  for i = 1, length do
    if (type(value[i]) ~= "number") then
      return false
    end
    if (value[i] < 0.0) or (value[i] > 1.0) then
      return false
    end
  end
  return true
end

function EasyOptions:IIF(test, trueValue, falseValue)
  if (test) then
    return trueValue
  else
    return falseValue
  end
end

function EasyOptions:Round(value, roundIncrement)
  if (roundIncrement == 0.0) then
    return value
  end
  return roundIncrement * math.floor((value / roundIncrement) + 0.5)
end
function EasyOptions:RoundColor(value)
  local roundIncrement = 0.001
  if (type(value) == "number") then
    return EasyOptions:Round(value, roundIncrement)
  elseif (type(value) ~= "table") then
    return value
  end

  local length = EasyOptions:GetLength(value)
  local result = {}
  for i = 1, length do
    result[i] = EasyOptions:RoundColor(value[i])
  end
  return result
end
function EasyOptions:LimitValue(value, customMinValue, customMaxValue, useDefaultLimitValuesIfNoCustomLimitValues)
  local minValue
  local maxValue
  local minValueDefault = -1000000000 -- -2147483648
  local maxValueDefault = 1000000000 -- 2147483647
  if (customMinValue ~= nil) then
    minValue = customMinValue
  elseif (useDefaultLimitValuesIfNoCustomLimitValues) then
    minValue = minValueDefault
  else
    minValue = nil
  end
  if (customMaxValue ~= nil) then
    maxValue = customMaxValue
  elseif (useDefaultLimitValuesIfNoCustomLimitValues) then
    maxValue = maxValueDefault
  else
    maxValue = nil
  end
  
  local valueLimited = value
  if (minValue ~= nil) and (valueLimited < minValue) then
    valueLimited = minValue
  elseif (maxValue ~= nil) and (valueLimited > maxValue) then
    valueLimited = maxValue
  end
  return valueLimited
end

function EasyOptions:GetColorValueFromColor(color, colorIndex, convertTo8bit)
  if (color == nil) then
    return nil
  end

  local defaultColorValue = EasyOptions:IIF(colorIndex == 4, 1.0, 0.0) -- Use a default value of 1.0 for the alpha channel and a default value of 0.0 for all other channels.
  local colorValue = defaultColorValue -- init
  if (colorIndex ~= nil) and (color[colorIndex] ~= nil) then
    colorValue = color[colorIndex]
  end
  if (convertTo8bit) then
    colorValue = EasyOptions:ConvertColorFromFloatTo8bit(colorValue)
  end
  return colorValue
end
function EasyOptions:SetColorValueInColor(color, value, colorIndex, is8bit)
  local colorValue = value -- init
  if (is8bit) then
    colorValue = EasyOptions:ConvertColorFrom8bitToFloat(colorValue)
  end
  local result = EasyOptions:CopyTable(color) -- Copy the original color.
  result[colorIndex] = colorValue -- Modify the appropriate color value.
  return result
end
function EasyOptions:DesaturateColor(value)
  local desaturateAlphaBlend = 0.5 -- Specifies how much to desaturate the color.
  local desaturateColorValue = 128 / 255 -- Specifies the desaturate color value.
  if (type(value) == "number") then
    return ((1.0 - desaturateAlphaBlend) * value) + (desaturateAlphaBlend * desaturateColorValue)
  elseif (type(value) ~= "table") then
    return value
  end

  local length = EasyOptions:GetLength(value)
  local indexOfAlphaChannel = 4
  local result = {}
  for i = 1, length do
    if (i < indexOfAlphaChannel) then
      result[i] = EasyOptions:DesaturateColor(value[i])
    else
      result[i] = value[i] -- Do not modify the Alpha channel when desaturating a color.
    end
  end
  return result
end
function EasyOptions:AlphaBlendColors(colors, weights)
  -- Description: Alpha blends the specified colors using the optional specified weights.
  local length = EasyOptions:GetLength(colors)
  if (length == 0) then
    return {0, 0, 0, 0}
  elseif (length == 1) then -- This is for optimization.
    return colors[1]
  end
  
  local lengthWeights = EasyOptions:GetLength(weights)
  local hasWeights = (lengthWeights > 0)
  local color
  local weight = 1.0
  local r, g, b, a = 0.0, 0.0, 0.0, 0.0 -- init
  local weightSum = 0.0 -- init
  for i = 1, length do
    color = colors[i]
    if (hasWeights) then
      weight = weights[i] or 0.0
    end
    r = r + weight * (color[1] or 0.0)
    g = g + weight * (color[2] or 0.0)
    b = b + weight * (color[3] or 0.0)
    a = a + weight * (color[4] or 1.0)
    weightSum = weightSum + weight
  end
  if (weightSum ~= 0.0) then
    local weightSumReciprocal = 1.0 / weightSum -- This is for optimization.
    r = r * weightSumReciprocal
    g = g * weightSumReciprocal
    b = b * weightSumReciprocal
    a = a * weightSumReciprocal
  end
  return {r, g, b, a}
end
function EasyOptions:DarkenColor(color, weight)
  -- Description: Darkens the specified color by the specified weights.
  local r = (1.0 - weight) * color[1] -- + weight * 0.0
  local g = (1.0 - weight) * color[2] -- + weight * 0.0
  local b = (1.0 - weight) * color[3] -- + weight * 0.0
  if (color[4] == nil) then
    return {r, g, b}
  else
    return {r, g, b, color[4]}
  end
end
function EasyOptions:LightenColor(color, weight)
  -- Description: Lightens the specified color by the specified weights.
  local r = (1.0 - weight) * color[1] + weight -- * 1.0
  local g = (1.0 - weight) * color[2] + weight -- * 1.0
  local b = (1.0 - weight) * color[3] + weight -- * 1.0
  if (color[4] == nil) then
    return {r, g, b}
  else
    return {r, g, b, color[4]}
  end
end
function EasyOptions:ConvertColorFromFloatTo8bit(value)
  local roundIncrement = 1.0
    if (type(value) == "number") then
    return EasyOptions:Round(255.0 * value, roundIncrement)
  elseif (type(value) ~= "table") then
    return value
  end

  local length = EasyOptions:GetLength(value)
  local result = {}
  for i = 1, length do
    result[i] = EasyOptions:ConvertColorFromFloatTo8bit(value[i])
  end
  return result
end
function EasyOptions:ConvertColorFrom8bitToFloat(value)
  if (type(value) == "number") then
    return value / 255.0
  elseif (type(value) ~= "table") then
    return value
  end

  local length = EasyOptions:GetLength(value)
  local result = {}
  for i = 1, length do
    result[i] = EasyOptions:ConvertColorFrom8bitToFloat(value[i])
  end
  return result
end

function EasyOptions:ConvertBooleanToNumber(value)
  if (value == nil) then
    return nil
  elseif (not value) then
    return 0
  else
    return 1
  end
end
function EasyOptions:ConvertNumberToBoolean(value)
  if (value == nil) then
    return nil
  elseif (value == 0) then
    return false
  else
    return true
  end
end
function EasyOptions:ConvertValueToHex(value)
  local hexValue = ""
  local valueLocal = value
	while (valueLocal > 0) do
		local index = math.fmod(valueLocal, 16) + 1
		valueLocal = math.floor(valueLocal / 16)
		hexValue = string.sub("0123456789ABCDEF", index, index)..hexValue			
	end

  local length = EasyOptions:GetStringLength(hexValue)
	if (length == 0) then
		hexValue = "00"
	elseif (length == 1) then
		hexValue = "0"..hexValue
	end
	return hexValue
end
function EasyOptions:ConvertColorToHex(color)
  -- Description: Returns a string that is a hex representation of the specified color that is in the format "AARRGGBB".

  if (not EasyOptions:IsColor(color)) then
    EasyOptions:PrintErrorMessage(L("Unable to convert \"{1}\" to a hex representation of a color.", color))
    return EasyOptions:ColorToHex({1.0, 1.0, 1.0})
  end

	local hexValue = ""
  local length = EasyOptions:GetLength(color)
  if (length == 4) then -- Append alpha.
    hexValue = hexValue..EasyOptions:ConvertValueToHex(EasyOptions:ConvertColorFromFloatTo8bit(color[4]))
  else
    hexValue = hexValue.."FF"
  end
  for i = 1, math.min(3, length) do -- Append the color values.
    hexValue = hexValue..EasyOptions:ConvertValueToHex(EasyOptions:ConvertColorFromFloatTo8bit(color[i]))
  end
	return hexValue
end
function EasyOptions:ConvertTextToColoredText(text, color)
  -- Description: Returns a string that contains the specified color in the format "|cAARRGGBBText|r".

  if (text == nil) then
    return nil
  end

  local colorLocal = color or {1.0, 1.0, 1.0}
  local colorHex = EasyOptions:ConvertColorToHex(colorLocal)
  return "|c"..colorHex..text.."|r"
end

function EasyOptions:GetLength(list)
  -- Description: Returns the length of the contiguous numbered items in the specified table starting at an index of 1. Named items, non-contiguous numbered items, or items with an index < 1 are ignored.
  --  IMPORTANT: The indexes of the items in the table must start at 1 and be contiguous (e.g., list[1], list[2], list[3], etc.). Otherwise, the result of this method could be invalid.

  -- Validate list.
  if (list == nil) then
    return 0
  end

  -- Attempt to use table.getn.
  --  IMPORTANT: Using table.getn (or #list) will exclude named items (e.g., list["MyItem"]) and its result will often be invalid if the numbered items in the list are not contiguous (e.g., list[1], list[5], list[8], ...) or if the list contains items that are set to nil.
  --  HINT: Use ipairs if you need to iterate through all index, value pairs in a table, starting at the first index that is >= 1 and stopping if there is no index equal to the previous index + 1.
  local length = table.getn(list)
  if (length == nil) then
    return 0
  else
    return length
  end
  -- local length = table.getn(list)
  -- if (length ~= nil) and (length > 0) then
  --   return length
  -- end
  -- 
  -- if (not preventGetLengthByCount) then
  --   return EasyOptions:GetLengthByCount(list)
  -- else
  --   return 0
  -- end
end
function EasyOptions:GetLengthByCount(items)
  -- Description: Returns the total number of items in the specified table, including all numbered and all named items.

  -- Validate items.
  if (items == nil) then
    return 0
  end

  -- Count the number of items in the table.
  local count = 0
  for _ in pairs(items) do -- See also ipairs which iterates through all index, value pairs in a table, starting at the first index that is >= 1 and stopping if there is no index equal to the previous index + 1.
    count = count + 1
  end
  return count
end
function EasyOptions:IsEmptyTable(items)
  -- Validate items.
  if (items == nil) then
    return true
  end

  for key in pairs(items) do
    return false -- If we get here then the table is not empty.
  end
  for i, value in ipairs(items) do
    return false -- If we get here then the table is not empty.
  end
  return true
end
function EasyOptions:GetNumberOfDimensions(value)
  -- Description: Returns the number of dimensions in the specified table, or 0 if the specified value is not a table.

  if (type(value) ~= "table") then -- NOTE: type(nil) == nil.
    return 0
  end
  
  local length = EasyOptions:GetLength(value)
  local numberOfDimensions = 1 -- init
  for i = 1, length do
    numberOfDimensions = math.max(1 + EasyOptions:GetNumberOfDimensions(value[i]), numberOfDimensions)
  end
  return numberOfDimensions
end
function EasyOptions:AddItems(destinationItems, sourceItems)
  -- Description: Adds all items in the source list to the destination list.

  -- Validate sourceItems.
  if (sourceItems == nil) then
    return
  end
  
  local length = EasyOptions:GetLength(sourceItems)
  for i = 1, length do
    table.insert(destinationItems, sourceItems[i])
  end
end
function EasyOptions:CombineLists(items1, items2)
  -- Description: Combines the two specified lists to create a new list that contains all of the items in the original lists.
  -- Alias for search: function EasyOptions:CombineItems(items1, items2)

  local items = EasyOptions:CopyTable(items1)
  if (items == nil) then
    items = {}
  end

  EasyOptions:AddItems(items, items2)

  return items
end
function EasyOptions:ContainsValue(items, value, searchForItemsInValue)
  -- Validate parameters.
  if (items == nil) then
    return false
  end
  if (value == nil) then
    return false
  end

  if (searchForItemsInValue) and (type(value) == "table") then
    local length = EasyOptions:GetLength(value)
    for i = 1, length do
      if (EasyOptions:ContainsValue(items, value[i], false)) then -- Call this method recursively.
        return true
      end
    end
  else
    local allowAreApproximatelyEqual = false
    for key, valueCurrent in pairs(items) do
      if (EasyOptions:AreEqual(valueCurrent, value, allowAreApproximatelyEqual)) then
        return true
      end
    end
  end
  return false
end
function EasyOptions:CopyTable(items)
  -- Description: Creates and returns a deep copy of the specified table.

  -- Validate items.
  if (items == nil) then
    return nil
  end

  -- IMPORTANT: We must not use result = {unpack(items)} because that technique fails if table contains a combination of numbered and named keys (e.g., items[1] and items["A"]).
  local result = {}
  for key, value in pairs(items) do -- Iterate through all key, value pairs in the table.
    if (type(value) ~= "table") then -- NOTE: type(nil) == nil.
      result[key] = value
    else
      result[key] = EasyOptions:CopyTable(value)
    end
  end
  return result
  -- return {unpack(table)}
end
function EasyOptions:CopyItemsToTable(sourceItems, destinationItems, clearDestinationBeforeCopy)
  -- Description: Performs a shallow copy of items from the source table to the destination table.

  if (clearDestinationBeforeCopy) then
    destinationItems = {}
  end

  -- Validate sourceItems.
  if (sourceItems == nil) then
    return
  end

  for key, value in pairs(sourceItems) do -- Iterate through all key, value pairs in the table.
    destinationItems[key] = sourceItems[key]
  end
end
function EasyOptions:Sort(items, ignoreCase)
  if (items == nil) then
    return
  end

  if (not ignoreCase) then
    table.sort(items)
  else
    table.sort(items, function(value1, value2) 
      return EasyOptions:Compare(value1, value2, ignoreCase)
    end)
  end
  --
  -- local result = EasyOptions:CopyTable(items)
  -- table.sort(result)
  -- return result
end
function EasyOptions:SortByValuesInColumn(iColumn, items, ignoreCase)
  -- Description: Sorts the specified 2D table by the values in the specified column and returns the result.
  if (iColumn == nil) then
    return nil
  end

  local length = EasyOptions:GetLength(items)
  if (length == 0) then
    return nil
  end

  -- Copy the source items to a temporary table.
  local itemsCache = {}
  for i = 1, length do
    itemsCache[i] = items[i]
  end

  -- Get the values in the specified column and sort the values.
  local sortValues = EasyOptions:GetValuesInColumn(iColumn, items)
  EasyOptions:Sort(sortValues, ignoreCase)

  -- Use the sorted values to sort the items in the source table.
  local sortValue
  local value
  local result = {}
  for iDestination = 1, length do
    sortValue = sortValues[iDestination] -- Get the value that we want to find in the specified column.
    for iSource = 1, length do -- Search for the first item in the source table that contains the desired value in the specified column.
      if (itemsCache[iSource] ~= nil) then -- Verify that we haven't already added this item to the result.
        value = itemsCache[iSource][iColumn] -- Get the value in the specified column.
        if (value == sortValue) then -- Determine whether the current item from the source table is a match.
          result[iDestination] = itemsCache[iSource] -- Add the current item from the source table to the result.
          itemsCache[iSource] = nil -- Clear the current item from the cache.
          break -- Continue to the next value that we want to find.
        end
      end
    end
  end
  return result
end
function EasyOptions:Compare(value1, value2, ignoreCase)
  if (not ignoreCase) or (type(value1) ~= "string") or (type(value2) ~= "string") then
    return value1 < value2
  else
    return string.lower(value1) < string.lower(value2)
  end
end
function EasyOptions:GetKeys(items, sortResult, ignoreCase)
  if (items == nil) then
    return nil
  end
  
  local result = {}
  for key, value in pairs(items) do -- Iterate through all key, value pairs in the table.
    table.insert(result, key)
  end
  if (sortResult) then
    EasyOptions:Sort(result, ignoreCase)
  end
  return result
end
function EasyOptions:GetValues(items, sortResult, ignoreCase)
  if (items == nil) then
    return nil
  end
  
  local result = {}
  for key, value in pairs(items) do -- Iterate through all key, value pairs in the table.
    table.insert(result, value)
  end
  if (sortResult) then
    EasyOptions:Sort(result, ignoreCase)
  end
  return result
end
function EasyOptions:GetValuesInColumn(iColumn, items)
  -- Description: Returns the values in the specified column of the specified 2D table.
  if (iColumn == nil) then
    return nil
  end

  local length = EasyOptions:GetLength(items)
  if (length == 0) then
    return nil
  end
  
  local result = {}
  for i = 1, length do
    result[i] = items[i][iColumn]
  end
  return result
end

function EasyOptions:CreateBarDelimiterInfo()
  local info = {}
  info.Delimiter = "|"
  info.DelimiterPrefix = " "
  info.DelimiterSuffix = " "
  info.TrimItems = true
  return info
end
function EasyOptions:CreateCommaDelimiterInfo()
  local info = {}
  info.Delimiter = ","
  info.DelimiterPrefix = ""
  info.DelimiterSuffix = " "
  info.TrimItems = true
  return info
end
function EasyOptions:CreateLinesDelimiterInfo()
  local info = {}
  info.Delimiter = "\n"
  info.DelimiterPrefix = ""
  info.DelimiterSuffix = ""
  info.TrimItems = true
  return info
end
function EasyOptions:CreateDefaultDelimiterInfo()
  return EasyOptions:CreateCommaDelimiterInfo()
end
function EasyOptions:GetDelimiterInfoParameterValues(delimiterInfo) -- delimiter, delimiterPrefix, delimiterSuffix, trimItems
  local delimiterInfoDefault = EasyOptions.Fields.DefaultDelimiterInfo
  local delimiterInfoLocal = delimiterInfo or delimiterInfoDefault
  local delimiter = delimiterInfoLocal.Delimiter or delimiterInfoDefault.Delimiter -- Specifies the delimiter.
  local delimiterPrefix = delimiterInfoLocal.DelimiterPrefix or "" -- delimiterInfoDefault.DelimiterPrefix -- Indicates a string to insert before the delimiter when converting the items in a table to a string.
  local delimiterSuffix = delimiterInfoLocal.DelimiterSuffix or "" -- delimiterInfoDefault.DelimiterSuffix -- Indicates a string to add after the delimiter when converting the items in a table to a string.
  local trimItems = delimiterInfoLocal.TrimItems or (delimiterInfoLocal.TrimItems ~= false) -- Set to true if is nil. -- delimiterInfoDefault.TrimItems -- Indicates whether to trim each item when converting a string to a table of items.
  return delimiter, delimiterPrefix, delimiterSuffix, trimItems
end
function EasyOptions:GetNumberOfDimensionsOfDelimiterInfo(delimiterInfo, numberOfDimensionsMaxAllowed)
  if (delimiterInfo == nil) then
    return 0
  end
  if (numberOfDimensionsMaxAllowed <= 0) then
    return 0
  end

  if (delimiterInfo.Delimiter == EasyOptions.Fields.DefaultLinesDelimiterInfo.Delimiter) then
    return math.min(1, numberOfDimensionsMaxAllowed)
  else
    return math.min(2, numberOfDimensionsMaxAllowed)
  end
end
function EasyOptions:ConvertValueToNumberOfDimensions(value, numberOfDimensions)
  local numberOfDimensionsOfValue = EasyOptions:GetNumberOfDimensions(value)
  if (numberOfDimensionsOfValue == numberOfDimensions) then
    return value

  elseif (numberOfDimensionsOfValue < numberOfDimensions) then
    local result = value
    for i = 1, (numberOfDimensions - numberOfDimensionsOfValue) do
      result = {result}
    end
    return result

  else -- numberOfDimensionsOfValue > numberOfDimensions
    return EasyOptions:ConvertValueToNumberOfDimensions(value[1], numberOfDimensions)
  end
end

function EasyOptions:CombineStrings(items, separator, iStart, iEnd)
  -- Description: Combines the the specified strings.
  --  NOTE: All parameters are optional.
  --  NOTE: This method will return an empty string if items is an empty table and all other parameters are nil.
  if (items == nil) then
    return nil
  end
  return table.concat(items, separator, iStart, iEnd)
end
function EasyOptions:ConvertListToString(list, delimiterInfo, numberOfDimensionsMaxAllowed)
  -- Description: Joins the specified list as a string using the specified delimiterInfo (Optional) and limited by the specified numberOfDimensionsMaxAllowed (Optional).
  -- Alias for search: function EasyOptions:JoinListAsString(list, delimiterInfo, numberOfDimensionsMaxAllowed)

  local valueForNil = ""
  if (list == nil) then
    return valueForNil
  end
  
  local numberOfDimensions = EasyOptions:GetNumberOfDimensions(list)
  if (numberOfDimensionsMaxAllowed ~= nil) then
    numberOfDimensions = math.min(numberOfDimensions, numberOfDimensionsMaxAllowed)
  end
  if (numberOfDimensions <= 0) then
    return tostring(list)
  end

  local delimiter, delimiterPrefix, delimiterSuffix, trimItems = EasyOptions:GetDelimiterInfoParameterValues(delimiterInfo)
  delimiter = delimiterPrefix..delimiter..delimiterSuffix -- Include the prefix and suffix in the delimiter.
  if (numberOfDimensions == 1) then
    local length = EasyOptions:GetLength(list)
    if (length == 0) then
      return valueForNil
    end
    local items = {}
    for i = 1, length do
      if (list[i] == nil) then
        items[i] = valueForNil
      elseif (type(list[i]) == "table") then
        items[i] = table.concat(list[i], ", ")
      else
        items[i] = tostring(list[i])
      end
    end
    return table.concat(items, delimiter)
  else -- numberOfDimensions >= 2
    local length = EasyOptions:GetLength(list)
    if (length == 0) then
      return ""
    end
    local lines = {}
    for i = 1, length do
      lines[i] = EasyOptions:ConvertListToString(list[i], delimiterInfo, 1)
    end
    return table.concat(lines, "\n")
  end
end
function EasyOptions:SplitStringAsList(text, delimiterInfo, numberOfDimensionsMaxAllowed, isTextFromWoWAPI, returnNilIfIsEmptyString)
  -- Description: Splits the specified string using the specified delimiterInfo (Optional) into a list whose dimensions are limited by the specified numberOfDimensionsMaxAllowed (Optional).

  local textLocal = text or ""
  if (isTextFromWoWAPI) then
    textLocal = EasyOptions:ReplaceCharactersInStringBeforeSearch(textLocal)
  end
  local numberOfDimensions = numberOfDimensionsMaxAllowed or 2
  local delimiter, delimiterPrefix, delimiterSuffix, trimItems = EasyOptions:GetDelimiterInfoParameterValues(delimiterInfo)

  if (returnNilIfIsEmptyString) and (textLocal == "") then
    return nil
  end

  if (numberOfDimensions == 0) then
    if (not trimItems) then
      return textLocal
    else
      return EasyOptions:TrimString(textLocal)
    end
  elseif (numberOfDimensions == 1) then
    return EasyOptions:SplitString(textLocal, delimiter, trimItems, true)
  else -- numberOfDimensions >= 2
    local items = {}
    local lines = EasyOptions:SplitString(textLocal, "\n", false, true)
    local length = EasyOptions:GetLength(lines)
    for i = 1, length do
      items[i] = EasyOptions:SplitStringAsList(lines[i], delimiterInfo, 1, false, false)
    end
    if (length == 1) and (numberOfDimensionsMaxAllowed == nil) then
      return items[1] -- Return a 1D list if no dimensions were specified and text corresponds to a single line of text.
    else
      return items
    end
  end
end

function EasyOptions:IsValidCVarName(cVarName)
  -- Description: Indicates whether the specified CVar name is valid and can be queried by addons.
  if (cVarName == nil) then
    return false
  end
  local value = GetCVar(cVarName)
  return (value ~= nil) -- GetCVar will return nil if it is called with an invalid variable name, or a variable name that cannot be queried by addons (e.g., "accountName").
end
function EasyOptions:SearchForCVarLimitValues(cVarName, stepSize, minAllowedValue, maxAllowedValue) -- success, minValue, maxValue
  -- Description: Searches for the minimum and maximum allowed values for the specified console variable.
  -- IMPORTANT: Use caution when calling this method because the technique that is used in this method can cause errors for some CVars if inappropriate parameter values are passed to this method, including causing the game to crash.
  -- NOTE: This method is only intended to be used for testing purposes.
  -- NOTE: This method can take a significant amount of time to execute depending on the values of stepSize, minAllowedValue, and maxAllowedValue.
  -- NOTE: Some CVars (e.g., "farclip" or "cameraDistanceMaxZoomFactor" in classic) can be set to a value that is smaller or larger than the actual limit value that is used by WoW.
  if (cVarName == nil) or (cVarName == "") then
    return false, nil, nil
  end

  local valueOriginal = tonumber(GetCVar(cVarName)) -- Store the original value of the console variable.
  if (valueOriginal == nil) then -- Validate valueOriginal. GetCVar will return nil if it is called with an invalid variable name, or a variable name that cannot be queried by addons (e.g., "accountName").
    EasyOptions:PrintErrorMessage(L("Unable to determine the value of the CVar named \"{1}\". Verify that the CVar name is valid and allowed to be queried by addons.", cVarName))
    return false, nil, nil
  end
  if (stepSize <= 0) then
    EasyOptions:PrintErrorMessage(L("Unable to determine the limit values of the CVar named \"{1}\" because the specified step size has a value of {2}.", cVarName, stepSize))
    return false, nil, nil
  end
  if (not SetCVar(cVarName, valueOriginal)) then -- Attempt to set the console variable. SetCVar will return nil if attempting to set a secure CVar in combat.
    EasyOptions:PrintErrorMessage(L("Unable to determine the limit values of the CVar named \"{1}\" because the CVar cannot be set. Verify that the CVar is allowed to be set in the current game state (e.g., you are not in combat if the CVar is secure while in combat).", cVarName))
    return false, nil, nil
  end

  local valueCurrent
  local valueDesired
  local allowAreApproximatelyEqual = true
  local iterationCount
  local iterationsMaxAllowed = 10000
  local foundLimitValue
  --
  local minValue = valueOriginal -- init
  valueDesired = EasyOptions:Round(valueOriginal - stepSize, stepSize)
  iterationCount = 1
  foundLimitValue = false
  while ((minAllowedValue == nil) or (valueDesired >= minAllowedValue)) and (iterationCount <= iterationsMaxAllowed) do
    SetCVar(cVarName, valueDesired)
    valueCurrent = tonumber(GetCVar(cVarName))
    minValue = math.min(valueCurrent, minValue)
    if (not EasyOptions:AreEqual(valueCurrent, valueDesired, allowAreApproximatelyEqual)) then
      foundLimitValue = true
      break
    end
    valueDesired = EasyOptions:Round(valueDesired - stepSize, stepSize)
    iterationCount = iterationCount + 1
  end
  if (not foundLimitValue) then
    SetCVar(cVarName, valueOriginal) -- Restore the original value.
    EasyOptions:PrintErrorMessage(L("Unable to find the minimum allowed value for the CVar named \"{1}\" because no result was found after searching for the maximum allowed number of iterations ({2}).", cVarName, iterationsMaxAllowed))
    return false, nil, nil
  end
  --
  local maxValue = valueOriginal -- init
  valueDesired = EasyOptions:Round(valueOriginal + stepSize, stepSize)
  iterationCount = 1
  foundLimitValue = false
  while ((maxAllowedValue == nil) or (valueDesired >= maxAllowedValue)) and (iterationCount <= iterationsMaxAllowed) do
    SetCVar(cVarName, valueDesired)
    valueCurrent = tonumber(GetCVar(cVarName))
    maxValue = math.max(valueCurrent, maxValue)
    if (not EasyOptions:AreEqual(valueCurrent, valueDesired, allowAreApproximatelyEqual)) then
      foundLimitValue = true
      break
    end
    valueDesired = EasyOptions:Round(valueDesired + stepSize, stepSize)
    iterationCount = iterationCount + 1
  end
  if (not foundLimitValue) then
    SetCVar(cVarName, valueOriginal) -- Restore the original value.
    EasyOptions:PrintErrorMessage(L("Unable to find the maximum allowed value for the CVar named \"{1}\" because no result was found after searching for the maximum allowed number of iterations ({2}).", cVarName, iterationsMaxAllowed))
    return false, nil, nil
  end
  
  -- Restore the original value.
  SetCVar(cVarName, valueOriginal)

  return true, minValue, maxValue
end

function EasyOptions:IsInInstance()
  local isInInstance, instanceType = IsInInstance()
  return isInInstance
end
function EasyOptions:IsInDungeonInstance()
  local isInInstance, instanceType = IsInInstance() -- instanceType: "none" when outside an instance, "pvp" when in a battleground, "arena" when in an arena, "party" when in a 5-man instance, "raid" when in a raid instance, nil when in an unknown kind of instance (e.g. in a scenario)
  return (isInInstance and (instanceType == "party"))
end
function EasyOptions:IsInRaidInstance()
  local isInInstance, instanceType = IsInInstance() -- instanceType: "none" when outside an instance, "pvp" when in a battleground, "arena" when in an arena, "party" when in a 5-man instance, "raid" when in a raid instance, nil when in an unknown kind of instance (e.g. in a scenario)
  return (isInInstance and (instanceType == "raid"))
end
function EasyOptions:IsInArenaInstance()
  local isInInstance, instanceType = IsInInstance() -- instanceType: "none" when outside an instance, "pvp" when in a battleground, "arena" when in an arena, "party" when in a 5-man instance, "raid" when in a raid instance, nil when in an unknown kind of instance (e.g. in a scenario)
  return (isInInstance and (instanceType == "arena"))
end
function EasyOptions:IsInBattlegroundInstance()
  local isInInstance, instanceType = IsInInstance() -- instanceType: "none" when outside an instance, "pvp" when in a battleground, "arena" when in an arena, "party" when in a 5-man instance, "raid" when in a raid instance, nil when in an unknown kind of instance (e.g. in a scenario)
  return (isInInstance and (instanceType == "pvp"))
end

function EasyOptions:GetRangeCheckInfo(unitID, rangeCheckProviders) -- hasRange, isOverMaxRange, minRange, maxRange
  -- Description returns hasRange, isOverMaxRange, minRange, maxRange that are appropriate for the specified unit using the specified range check providers (e.g., a range check library).
  --  The method uses data from all range check providers to determine a range that has the highest possible precision.
  --  Each item in rangeCheckProviders must implement a GetRange(unitID) method that returns minRange, maxRange (in yards). minRange should be nil if no range could be determined and maxRange should be nil if the range is greater than the maximum possible range that is supported by the GetRange method.
  if (unitID == nil) or (unitID == "") then
    return false, false, 0, 0
  end
  if (not UnitExists(unitID)) then
    return false, false, 0, 0
  end
  
  local length = EasyOptions:GetLength(rangeCheckProviders)
  local hasRange, isOverMaxRange, minRange, maxRange = false, false, 0, 0 -- init
  local hasRangeCurrent, isOverMaxRangeCurrent, minRangeCurrent, maxRangeCurrent
  local rangeCheckProvider
  for i = 1, length do
    rangeCheckProvider = rangeCheckProviders[i]
    hasRangeCurrent, isOverMaxRangeCurrent, minRangeCurrent, maxRangeCurrent = EasyOptions:GetRangeCheckInfo_FromProvider(unitID, rangeCheckProvider)
    if (hasRangeCurrent) then
      if (not hasRange) then -- If this is true then this is the first set of valid ranges.
        hasRange, isOverMaxRange, minRange, maxRange = hasRangeCurrent, isOverMaxRangeCurrent, minRangeCurrent, maxRangeCurrent
      elseif (not isOverMaxRangeCurrent) and (isOverMaxRange) then -- If this is true then the range from a previous library was over its maximum possible range, but the current library has a valid range.
        hasRange, isOverMaxRange, minRange, maxRange = hasRangeCurrent, isOverMaxRangeCurrent, minRangeCurrent, maxRangeCurrent
      elseif (isOverMaxRangeCurrent) and (not isOverMaxRange) then -- If this is true then the range from the current library is over its maximum possible range, but a previous library had a valid range.
        -- Do nothing.
      elseif (isOverMaxRangeCurrent) and (isOverMaxRange) then -- If this is true then the range is beyond the maximum possible range of the current and all previous libraries.
        minRange = math.max(minRange, minRangeCurrent)
        maxRange = minRange
      elseif (minRangeCurrent > maxRange) or (minRange > maxRangeCurrent) then -- If this is true then there is a conflict between the results of the current library and a previous library that cannot be resolved.
        hasRange, isOverMaxRange, minRange, maxRange = hasRangeCurrent, isOverMaxRangeCurrent, minRangeCurrent, maxRangeCurrent -- Use the results from the current library if there is a conflict that cannot be resolved with the assumption that subsequent libraries should override the values of previous libraries.
      elseif (maxRange == minRangeCurrent) or (minRange == maxRangeCurrent) then -- If this is true then we assume that the range has very recently crossed a threshold and only one of the libraries has updated. NOTE: This logic was implemented to avoid the min and max ranges briefly being set to the same value when the range crosses a threshold (e.g., to avoid having the range briefly being reported as 10-10 if the range has recently gone from 2-10 to 10-15, but only one of the range check libraries has updated).
        hasRange, isOverMaxRange, minRange, maxRange = hasRangeCurrent, isOverMaxRangeCurrent, minRangeCurrent, maxRangeCurrent -- Use the results from the current library if there is a conflict that cannot be resolved with the assumption that subsequent libraries should override the values of previous libraries.
      else
        minRange = math.max(minRange, minRangeCurrent)
        maxRange = math.min(maxRange, maxRangeCurrent)
      end
    end
  end
  return hasRange, isOverMaxRange, minRange, maxRange
end
function EasyOptions:GetRangeCheckInfo_FromProvider(unitID, rangeCheckProvider) -- hasRange, isOverMaxRange, minRange, maxRange
  local minRange, maxRange = rangeCheckProvider:GetRange(unitID)
  if (minRange == nil) then -- If minRange is nil then a range estimate to the specified unit could not be determined.
    return false, false, 0, 0
  elseif (maxRange == nil) then -- If maxRange is nil then the distance to the specified unit is over minRange (i.e., minRange is expected to be at the maximum possible range that can be returned by rangeCheckProvider:GetRange and maxRange would be even larger than that value).
    return true, true, minRange, minRange
  else
    return true, false, minRange, maxRange
  end
end

function EasyOptions:CanPlaySound(soundNameOrFileName, soundThrottle, timeOfPlaySoundTable)
  if (soundNameOrFileName == nil) or (soundNameOrFileName == "") then
    return false
  end
  if (soundThrottle == nil) or (timeOfPlaySoundTable == nil) or (timeOfPlaySoundTable[soundNameOrFileName] == nil) then
    return true
  end

  local timeSincePlayedSound = GetTime() - timeOfPlaySoundTable[soundNameOrFileName]
  return (timeSincePlayedSound >= soundThrottle)
end
function EasyOptions:PlaySound(soundName, soundChannel, soundThrottle, timeOfPlaySoundTable)
  --
  -- See also the following:
  --  https://wow.gamepedia.com/API_PlaySound
  --  https://github.com/Gethe/wow-ui-source/blob/live/SharedXML/SoundKitConstants.lua
  --
  -- Examples:
  -- AUCTION_WINDOW_OPEN
  -- AUCTION_WINDOW_CLOSE
  -- RAID_WARNING
  -- READY_CHECK
  -- IG_QUEST_LOG_ABANDON_QUEST
  -- IG_QUEST_CANCEL
  if (not EasyOptions:CanPlaySound(soundName, soundThrottle, timeOfPlaySoundTable)) then
    return false
  end
  if (timeOfPlaySoundTable ~= nil) then
    timeOfPlaySoundTable[soundName] = GetTime()
  end
  local soundKitID = SOUNDKIT[soundName]
  PlaySound(soundKitID, soundChannel)
  return true
end
function EasyOptions:PlaySoundFile(soundFileName, soundChannel, soundThrottle, timeOfPlaySoundTable)
  if (not EasyOptions:CanPlaySound(soundFileName, soundThrottle, timeOfPlaySoundTable)) then
    return false
  end
  if (timeOfPlaySoundTable ~= nil) then
    timeOfPlaySoundTable[soundFileName] = GetTime()
  end
  PlaySoundFile(soundFileName, soundChannel)
  return true
end

function EasyOptions:PlaySynchronizedAnimation(animationGroup, reversed)
  --
  -- NOTE: This method does not work as intended because the elapsedOffset parameter in AnimationGroup:Play(reversed, elapsedOffset) does not have the expected effect and no documentation for that parameter was found. Therefore, the behavior of this method might change in a future version so it provides the desired behavior.
  --

  -- Plays an animation with a standardized value for the elapsedOffset in order to synchronize the animation with other animations that are played by this method.
  local reversedLocal = reversed or false
  local elapsedOffset = GetTime()
  -- if (animationGroup:IsPlaying()) then
  --   animationGroup:Stop()
  -- end
  animationGroup:Play(reversedLocal, elapsedOffset)
  -- animationGroup:Restart(reversedLocal, elapsedOffset)
end

function EasyOptions:SetFont(label, fontFilePath, fontSize, fontFlags, textColor)
  -- Sets the font and text color of a specified FontString. Includes logic to support the use of custom fonts.
  local customFontInfo = {fontFilePath, fontSize, fontFlags}
  local customTextColor = textColor
  EasyOptions:SetFontFromCustomFontInfo(label, customFontInfo, customTextColor)
end

function EasyOptions:ShowDialog(text, uniqueID, button1, button2, button3, textBoxInfo, onButton1Handler, onButton2Handler, onButton3Handler)
  -- HINT: You can use the in-game /fstack slash command to view the Frame Stack of the dialog frame.
  -- NOTE: The uniqueID parameter may be set to nil in which this method will create a temporary dialog with a unique name each time this method is called. A caveat with this technique is that multiple dialogs that correspond to the same action (e.g., clicking a button) can be shown simultaneously if the user fails to close a previous dialog before they repeat the action that causes the dialog to be shown.
  -- NOTE: The following global variables are automatically converted to localized strings: YES, NO, ACCEPT, CANCEL, DELETE, OKAY, and CONTINUE (for button labels) and CONFIRM_CONTINUE for text (e.g., "Do you wish to continue?").
  -- References:
  --  https://wowpedia.fandom.com/wiki/Creating_simple_pop-up_dialog_boxes
  --  https://wowwiki-archive.fandom.com/wiki/Creating_simple_pop-up_dialog_boxes

  local controlType = "Dialog"
  local name
  if (uniqueID ~= nil) then
    name = uniqueID
  else
    name = "EasyOptions_"..EasyOptions:GetControlName(controlType, uniqueID)
    m_TemporaryDialogNames[name] = true
  end
  local textBoxInfoLocal = textBoxInfo
  local textBoxCount, textBox1LabelText, textBox1Text, textBox1Tooltip, textBox1Width, isTextBox1ValueRequired, textBox2LabelText, textBox2Text, textBox2Tooltip, textBox2Width, isTextBox2ValueRequired = EasyOptions:GetDialogTextBoxParametersFromTextBoxInfo(textBoxInfo)

  local allowAutoOnCancel = (button2 == nil) or (button2 == CANCEL) or (button3 == nil) -- YES, NO, ACCEPT, CANCEL, DELETE, OKAY, CONTINUE -- IMPORTANT: We only allow the dialog to raise its OnCancel event when the user clicks the Escape key when it is appropriate for button2 to raise the OnCancel event because the WoW API always assumes that button2 is the Cancel button.
  local hideOnEscape = allowAutoOnCancel
  local notClosableByLogout = (not allowAutoOnCancel)
  local noCancelOnReuse = (not allowAutoOnCancel)
  
  local hasEditBox = (textBoxCount >= 1)
  local editBoxWidth = textBox1Width

  local onShowHandlerCustom = nil -- Reserved.
  local onShowHandlerLocal = function(self, data)
    EasyOptions:ShowDialog_OnShowHandler(self, data, textBoxInfoLocal)
    if (onShowHandlerCustom ~= nil) then
      onShowHandlerCustom()
    end
  end
  local onHideHandlerCustom = nil -- Reserved.
  local onHideHandlerLocal = function(self)
    EasyOptions:ShowDialog_OnHideHandler(self, data, textBoxInfoLocal)
    if (onHideHandlerCustom ~= nil) then
      onHideHandlerCustom()
    end
    if (m_TemporaryDialogNames[name]) then
      StaticPopupDialogs[name] = nil -- Release resources for this dialog.
      m_TemporaryDialogNames[name] = nil
    end
  end
  --
  local onButton1HandlerCustom = onButton1Handler
  local onButton1HandlerLocal = function(self, data, data2)
    if (onButton1HandlerCustom ~= nil) then
      local userText = EasyOptions:GetDialogUserText(self, textBoxInfoLocal)
      onButton1HandlerCustom(userText)
    end
  end
  local onButton2HandlerCustom = onButton2Handler -- NOTE: The WoW API always assigns Button2 with the OnCancel event (even if the text of Button3 is set to CANCEL).
  local onButton2HandlerLocal = function(self, data, reason) -- NOTE: The reason parameter is only passed to Button2 and will be one of the following: "override", "timeout", or "clicked"
    if (onButton2HandlerCustom ~= nil) then
      local userText = EasyOptions:GetDialogUserText(self, textBoxInfoLocal)
      onButton2HandlerCustom(userText)
    end
  end
  local onButton3HandlerCustom = onButton3Handler
  local onButton3HandlerLocal = function(self, data)
    if (onButton3HandlerCustom ~= nil) then
      local userText = EasyOptions:GetDialogUserText(self, textBoxInfoLocal)
      onButton3HandlerCustom(userText)
    end
  end
  
  -- Hide the tooltip so the tooltip does not hide the dialog window (if required).
  if (EasyOptionsDB["AutoHideTooltipWhenShowDialogWindow"]) then
    GameTooltip:Hide()
  end

  -- if (StaticPopupDialogs[name] == nil) then -- NOTE: We do not check this case because we assume that we want to create a new Dialog each time this method is called, even if name is a duplicate from a previous call. Otherwise, the dialog will always use settings from the first time that this method was called for a given name.
  StaticPopupDialogs[name] = { 
    text = text, 
    button1 = button1, 
    button2 = button2, 
    button3 = button3, 
    OnShow = onShowHandlerLocal, 
    OnHide = onHideHandlerLocal, 
    OnAccept = onButton1HandlerLocal, -- IMPORTANT: We set OnAccept, OnCancel, etc. rather than OnButton1, OnButton2, etc. because the WoW API does not seem to support OnButton2 or OnButton3.
    OnCancel = onButton2HandlerLocal, 
    OnAlt = onButton3HandlerLocal, 
    -- OnButton1 = nil, 
    -- OnButton2 = nil, 
    -- OnButton3 = nil, 
    hasEditBox = hasEditBox, 
    editBoxWidth = editBoxWidth, 
    -- maxLetters = maxLetters, 
    timeout = 0, 
    whileDead = true, 
    enterClicksFirstButton = true, 
    hideOnEscape = hideOnEscape, 
    notClosableByLogout = notClosableByLogout, 
    noCancelOnReuse = noCancelOnReuse, 
    preferredIndex = 3, -- 4 -- Avoid some UI taint, see http://www.wowace.com/announcements/how-to-avoid-some-ui-taint/
    }
  -- end
  StaticPopup_Show(name)
  return name
end
function EasyOptions:ShowDialog_OnShowHandler(frame, data, textBoxInfo)
  local frameName = frame:GetName()
  local marginX = 16
  local marginY = 20
  local paddingX = EasyOptions:GetDefaultControlPaddingX()
  local label = frame.text
  local editBox = frame.editBox
  local textBoxCount, textBox1LabelText, textBox1Text, textBox1Tooltip, textBox1Width, isTextBox1ValueRequired, textBox2LabelText, textBox2Text, textBox2Tooltip, textBox2Width, isTextBox2ValueRequired = EasyOptions:GetDialogTextBoxParametersFromTextBoxInfo(textBoxInfo)
  local frameWidthDefault = 320
  local frameWidthInitial = frame:GetWidth()
  local frameWidthRequired = textBox1Width + textBox2Width + paddingX + 2 * marginX + 16
  local frameWidth = math.max(frameWidthRequired, frameWidthInitial)
  -- local frameHeight = nil -- NOTE: The WoW API does not allow the height of a Dialog frame (i.e., a StaticPopup frame) to be modified. But, a possible workaround if needed is to set the dialog's text to a multiline string in which case the dialog frame will be resized to fit the height of that string.
  local marginX_Actual = marginX + math.max(0.5 * (frameWidth - frameWidthRequired), 0)
  local showEditBox = (textBoxCount == 1)
  local showTextBoxes = (textBoxCount == 2)

  frame:SetWidth(frameWidth)
  EasyOptions:AddHideGameTooltipScripts(frame) -- See comments in EasyOptions:AddHideGameTooltipScripts for why we call that method.

  -- Build the TextBox controls (if required).
  local controlNameCurrent
  local valueCurrent
  local textCurrent
  local tooltipTitleTextCurrent
  local tooltipTextCurrent
  local positionInfoCurrent = {"TOPLEFT", "TOPLEFT", marginX_Actual, -marginY} -- init -- "LEFT", "RIGHT", "TOP", "BOTTOM", "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT", "CENTER"
  local customSizeCurrent
  local onTextChangedHandler = nil
  local onTextAcceptedHandler = nil
  local controlCurrent, controlRegionWidthCurrent, controlRegionHeightCurrent
  if (not frame.HasInitializedControls) then
    frame.HasInitializedControls = true

    -- TextBox1
    if (frameName == nil) then
      controlNameCurrent = nil
    else
      controlNameCurrent = frameName..".TextBox1"
    end
    valueCurrent = nil
    textCurrent = " " -- Set the text to a non-empty string so the control's label will be created.
    tooltipTitleTextCurrent = nil
    tooltipTextCurrent = nil
    positionInfoCurrent = {positionInfoCurrent[1], positionInfoCurrent[2], positionInfoCurrent[3], positionInfoCurrent[4]}
    customSizeCurrent = nil
    controlCurrent, controlRegionWidthCurrent, controlRegionHeightCurrent = EasyOptions:CreateTextBox(frame, controlNameCurrent, valueCurrent, nil, nil, nil, textCurrent, tooltipTitleTextCurrent, tooltipTextCurrent, positionInfoCurrent, customSizeCurrent, false, false, nil, nil, nil, onTextChangedHandler, onTextAcceptedHandler, nil)
    frame.TextBox1 = controlCurrent

    -- TextBox2
    if (frameName == nil) then
      controlNameCurrent = nil
    else
      controlNameCurrent = frameName..".TextBox2"
    end
    valueCurrent = nil
    textCurrent = " " -- Set the text to a non-empty string so the control's label will be created.
    tooltipTitleTextCurrent = nil
    tooltipTextCurrent = nil
    positionInfoCurrent = {positionInfoCurrent[1], positionInfoCurrent[2], positionInfoCurrent[3], positionInfoCurrent[4]} -- + controlRegionWidthCurrent + paddingX -- NOTE: We use the default PositionInfo because TextBox2 will be docked to the right side of TextBox1 later in this method (see below).
    customSizeCurrent = nil
    controlCurrent, controlRegionWidthCurrent, controlRegionHeightCurrent = EasyOptions:CreateTextBox(frame, controlNameCurrent, valueCurrent, nil, nil, nil, textCurrent, tooltipTitleTextCurrent, tooltipTextCurrent, positionInfoCurrent, customSizeCurrent, false, false, nil, nil, nil, onTextChangedHandler, onTextAcceptedHandler, nil)
    controlCurrent:ClearAllPoints()
    controlCurrent:SetPoint("TOPLEFT", frame.TextBox1, "TOPRIGHT", paddingX + 4, 0) -- Dock TextBox2 to the right side of TextBox1 so TextBox2 will be repositioned if a custom width is specified for TextBox1 when the dialog is shown.
    frame.TextBox2 = controlCurrent
  end
  
  -- EditBox
  local defaultEditBoxText
  if (not showEditBox) then
    defaultEditBoxText = ""
  else
    defaultEditBoxText = textBox1Text
  end
  EasyOptions:SetDialogEditBoxText(frame, defaultEditBoxText)

  -- TextBox1
  if (not showTextBoxes) then
    valueCurrent = ""
    textCurrent = ""
    tooltipTitleTextCurrent = nil
    tooltipTextCurrent = nil
  else
    valueCurrent = textBox1Text
    textCurrent = textBox1LabelText
    tooltipTitleTextCurrent = textCurrent
    tooltipTextCurrent = textBox1Tooltip
  end
  frame.TextBox1:SetValue(valueCurrent)
  frame.TextBox1.Label:SetText(textCurrent)
  frame.TextBox1:SetWidth(textBox1Width)
  EasyOptions:SetTooltip(frame.TextBox1, frame.TextBox1, tooltipTitleTextCurrent, tooltipTextCurrent)
  
  -- TextBox2
  if (not showTextBoxes) then
    valueCurrent = ""
    textCurrent = ""
    tooltipTitleTextCurrent = nil
    tooltipTextCurrent = nil
  else
    valueCurrent = textBox2Text
    textCurrent = textBox2LabelText
    tooltipTitleTextCurrent = textCurrent
    tooltipTextCurrent = textBox2Tooltip
  end
  frame.TextBox2:SetValue(valueCurrent)
  frame.TextBox2.Label:SetText(textCurrent)
  frame.TextBox2:SetWidth(textBox2Width)
  EasyOptions:SetTooltip(frame.TextBox2, frame.TextBox2, tooltipTitleTextCurrent, tooltipTextCurrent)
  
  EasyOptions:ShowDialog_UpdateControlStates(frame, showEditBox, showTextBoxes)
end
function EasyOptions:ShowDialog_OnHideHandler(frame, data, textBoxInfo)
  local showEditBox = false
  local showTextBoxes = false
  EasyOptions:ShowDialog_UpdateControlStates(frame, showEditBox, showTextBoxes)
end
function EasyOptions:ShowDialog_UpdateControlStates(frame, showEditBox, showTextBoxes)
  local editBox, label = EasyOptions:GetDialogEditBoxAndLabel(frame)
  if (not showTextBoxes) then
    if (label ~= nil) then
      label:Show()
    end
  else
    if (label ~= nil) then
      label:Hide()
    end
  end
  if (showEditBox) then
    if (editBox ~= nil) then
      editBox:Show()
    end
  else
    if (editBox ~= nil) then
      editBox:Hide()
    end
  end

  if (frame.HasInitializedControls) then
    if (showTextBoxes) then
      frame.TextBox1:Show()
      frame.TextBox1.Label:Show()
      frame.TextBox2:Show()
      frame.TextBox2.Label:Show()
    else
      frame.TextBox1:Hide()
      frame.TextBox1.Label:Hide()
      frame.TextBox2:Hide()
      frame.TextBox2.Label:Hide()
    end
  end
end
function EasyOptions:ShowDialogOk(text, uniqueID, onAcceptHandler)
  local button1 = OKAY -- YES, NO, ACCEPT, CANCEL, DELETE, OKAY, CONTINUE
  local button2 = nil
  local button3 = nil
  local textBoxInfo = nil
  local onButton1Handler = onAcceptHandler
  local onButton2Handler = nil
  local onButton3Handler = nil
  return EasyOptions:ShowDialog(text, uniqueID, button1, button2, button3, textBoxInfo, onButton1Handler, onButton2Handler, onButton3Handler)
end
function EasyOptions:ShowDialogOkCancel(text, uniqueID, onAcceptHandler, onCancelHandler)
  local button1 = OKAY -- YES, NO, ACCEPT, CANCEL, DELETE, OKAY, CONTINUE
  local button2 = CANCEL
  local button3 = nil
  local textBoxInfo = nil
  local onButton1Handler = onAcceptHandler
  local onButton2Handler = onCancelHandler
  local onButton3Handler = nil
  return EasyOptions:ShowDialog(text, uniqueID, button1, button2, button3, textBoxInfo, onButton1Handler, onButton2Handler, onButton3Handler)
end
function EasyOptions:ShowDialogYesNoCancel(text, uniqueID, onYesHandler, onNoHandler, onCancelHandler)
  local button1 = YES -- YES, NO, ACCEPT, CANCEL, DELETE, OKAY, CONTINUE
  local button2 = NO
  local button3 = CANCEL
  local textBoxInfo = nil
  local onButton1Handler = onYesHandler
  local onButton2Handler = onNoHandler
  local onButton3Handler = onCancelHandler
  return EasyOptions:ShowDialog(text, uniqueID, button1, button2, button3, textBoxInfo, onButton1Handler, onButton2Handler, onButton3Handler)
end
function EasyOptions:ShowDialogEditOkCancel(text, uniqueID, defaultEditBoxText, onAcceptHandler, onCancelHandler)
  local button1 = OKAY -- YES, NO, ACCEPT, CANCEL, DELETE, OKAY, CONTINUE
  local button2 = CANCEL
  local button3 = nil
  local textBoxInfo = {
    TextBoxCount = 1, -- 0, 1, or 2
    TextBox1LabelText = text, 
    TextBox1Text = defaultEditBoxText, 
    TextBox1Tooltip = nil, 
    TextBox1Width = nil, 
    TextBox2LabelText = nil, 
    TextBox2Text = nil, 
    TextBox2Tooltip = nil, 
    TextBox2Width = nil, 
  }
  local onButton1Handler = onAcceptHandler
  local onButton2Handler = onCancelHandler
  local onButton3Handler = nil
  return EasyOptions:ShowDialog(text, uniqueID, button1, button2, button3, textBoxInfo, onButton1Handler, onButton2Handler, onButton3Handler)
end
function EasyOptions:ShowDialogEditAdvancedOkCancel(textBoxInfo, uniqueID, onAcceptHandler, onCancelHandler)
  local text
  if (textBoxInfo == nil) then
    text = nil
  else
    text = textBoxInfo.TextBox1LabelText
  end
  local button1 = OKAY -- YES, NO, ACCEPT, CANCEL, DELETE, OKAY, CONTINUE
  local button2 = CANCEL
  local button3 = nil
  local onButton1Handler = onAcceptHandler
  local onButton2Handler = onCancelHandler
  local onButton3Handler = nil
  return EasyOptions:ShowDialog(text, uniqueID, button1, button2, button3, textBoxInfo, onButton1Handler, onButton2Handler, onButton3Handler)
end
function EasyOptions:GetDialogEditBoxAndLabel(frame) -- editBox, label
  if (frame == nil) then
    return nil, nil
  end
  return frame.editBox, frame.text
  -- return getglobal(frame:GetName().."EditBox")
end
function EasyOptions:GetDialogUserText(frame, textBoxInfo) -- Returns a string or a table of strings.
  local textBoxCount, textBox1LabelText, textBox1Text, textBox1Tooltip, textBox1Width, isTextBox1ValueRequired, textBox2LabelText, textBox2Text, textBox2Tooltip, textBox2Width, isTextBox2ValueRequired = EasyOptions:GetDialogTextBoxParametersFromTextBoxInfo(textBoxInfo)
  local showEditBox = (textBoxCount == 1)
  local showTextBoxes = (textBoxCount == 2)
  
  local emptyTextResult = "" -- nil
  if (showEditBox) then
    local editBox, label = EasyOptions:GetDialogEditBoxAndLabel(frame)
    if (editBox == nil) then
      return emptyTextResult
    end
    local result = editBox:GetText() or emptyTextResult
    return result

  elseif (showTextBoxes) then
    if (not frame.HasInitializedControls) then
      return {emptyTextResult, emptyTextResult}
    end
    local result = {frame.TextBox1:GetText() or emptyTextResult, frame.TextBox2:GetText() or emptyTextResult}
    return result

  else
    return nil
  end
end
function EasyOptions:SetDialogEditBoxText(frame, text)
  local editBox, label = EasyOptions:GetDialogEditBoxAndLabel(frame)
  if (editBox == nil) then
    return
  end
  editBox:SetText(text or "")
  editBox:HighlightText()
end
function EasyOptions:GetDialogTextBoxParametersFromTextBoxInfo(textBoxInfo)
  local textBoxWidthDefault = 130 -- This is the default width of frame.editBox. -- EasyOptions:GetDefaultTextBoxWidth()

  local textBoxCount = 0
  local textBox1LabelText = ""
  local textBox1Text = ""
  local textBox1Tooltip = nil
  local textBox1Width = textBoxWidthDefault
  local isTextBox1ValueRequired = false
  local textBox2LabelText = ""
  local textBox2Text = ""
  local textBox2Tooltip = nil
  local textBox2Width = textBoxWidthDefault
  local isTextBox2ValueRequired = false
  if (textBoxInfo ~= nil) then
    textBoxCount = textBoxInfo.TextBoxCount or textBoxCount
    textBox1LabelText = textBoxInfo.TextBox1LabelText or textBox1LabelText
    textBox1Text = textBoxInfo.TextBox1Text or textBox1Text
    textBox1Tooltip = textBoxInfo.TextBox1Tooltip or textBox1Tooltip
    textBox1Width = textBoxInfo.TextBox1Width or textBox1Width
    textBox2LabelText = textBoxInfo.TextBox2LabelText or textBox2LabelText
    textBox2Text = textBoxInfo.TextBox2Text or textBox2Text
    textBox2Tooltip = textBoxInfo.TextBox2Tooltip or textBox2Tooltip
    textBox2Width = textBoxInfo.TextBox2Width or textBox2Width
  end
  if (textBoxCount < 0) or (textBoxCount > 2) then
    EasyOptions:PrintErrorMessage(L("textBoxInfo.TextBoxCount was set to {1} which is not supported. TextBoxCount must be 0, 1, or 2.", textBoxCount))
    textBoxCount = math.min(math.max(textBoxCount, 0), 2)
  end
  return textBoxCount, textBox1LabelText, textBox1Text, textBox1Tooltip, textBox1Width, isTextBox1ValueRequired, textBox2LabelText, textBox2Text, textBox2Tooltip, textBox2Width, isTextBox2ValueRequired
end

function EasyOptions:DebugPrintFrameAnchorPoints(frame)
  -- This method was implemented for testing purposes.
  local name = frame.Name or "Frame"
  EasyOptions:PrintMessage(EasyOptions:Format("{1} Points:", name))
  local length = frame:GetNumPoints()
  local point, relativeTo, relativePoint, offsetX, offsetY
  for i = 1, length do
    point, relativeTo, relativePoint, offsetX, offsetY = frame:GetPoint(i)
    EasyOptions:Print("  {1}", {point, relativePoint, offsetX, offsetY})
  end
end
function EasyOptions:DebugPrintFrameMouseStatus(frame)
  -- This method was implemented for testing purposes.
  local name = frame.Name or "Frame"
  EasyOptions:PrintMessage(EasyOptions:Format("{1} Mouse Status:", name))
  EasyOptions:Print("  IsMouseEnabled = {1}", frame:IsMouseEnabled())
  EasyOptions:Print("  IsMouseClickEnabled = {1}", frame:IsMouseClickEnabled())
  EasyOptions:Print("  IsMouseWheelEnabled = {1}", frame:IsMouseWheelEnabled())
  EasyOptions:Print("  IsMouseMotionEnabled = {1}", frame:IsMouseMotionEnabled())
  EasyOptions:Print("  IsMouseMotionFocus = {1}", frame:IsMouseMotionFocus())
  EasyOptions:Print("  IsMovable = {1}", frame:IsMovable())
end

function EasyOptions:HandleErrorMessage(text, allowPrintMessage, allowShowDialog)
  EasyOptions:HandleErrorMessageHelper("EasyOptions", text, allowPrintMessage and EasyOptionsDB["EnableMessages"], allowShowDialog)
end
function EasyOptions:HandleErrorMessageHelper(addonName, text, allowPrintMessage, allowShowDialog)
  if (text == nil) or (text == "") then
    return
  end
  if (allowPrintMessage) then
    EasyOptions:PrintErrorMessageHelper(addonName, text)
  end
  if (allowShowDialog) then
    local uniqueID = nil
    local onAcceptHandler = nil
    EasyOptions:ShowDialogOk(text, uniqueID, onAcceptHandler)
  end
end
