-- Copyright (C) 2010 Tyson Brown
-- 
-- This file is part QuestHelper2, an AddOn for World of Warcraft.
-- 
-- QuestHelper2 is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
-- 
-- QuestHelper2 is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
-- 
-- You should have received a copy of the GNU General Public License
-- along with QuestHelper2. If not, see <http://www.gnu.org/licenses/>.

if LibDebug then LibDebug() end
local shared = select(2, ...)

-- Settings as they were saved; replaced when loaded.
local settings_saved = {}

-- Overridden values for settings by other AddOns.
local settings_override = {}

-- The current value of all the settings; will be initialized to be a copy of settings_default in a moment.
local settings_current, settings_current_override = {}, {}

-- Maps a setting to a list of callbacks to be invoked when the value of that setting is changed.
-- when settings get changed.
local settings_callbacks = {}

-- Maps a setting to a list of callbacks to be invoked when a value is overriden... or not.
local settings_callbacks_override = {}

-- The default settings go in here.
local settings_default = {
  locale = GetLocale(),
  menu = true,
  enabled = false, -- Gets set to true when settings are loaded.
  routing_enabled = true,
  routing_relaxed_priorities = false,
  arrow_shown = true,
  arrow_hide_combat = false,
  arrow_locked = false,
  arrow_text_width = 300,
  arrow_scale = 0.7,
  arrow_text = true,
  arrow_distance = false,
  compass_shown = false,
  compass_hide_combat = false,
  compass_locked = false,
  compass_only_route_locations = false,
  compass_scale = 0.7,
  compass_width = 1024,
  digsite_create_objectives = true,
  digsite_filter_continent = true,
  digsite_filter_zone = false,
  tracker_locked = false,
  tracker_hide_combat = false,
  tracker_anchor_left = false,
  tracker_anchor_bottom = false,
  tracker_scale = 0.7,
  tracker_max_height = 512,
  tracker_max_width = 384,
  tracker_shown = true,
  tracker_minimized = false,
  tracker_shows_routes = false,
  tracker_show_objective_tooltips = true,
  tracker_show_item_tooltips = true,
  overlay_shown = true,
  overlay_ant_spacing = 100,
  overlay_ant_speed = 25,
  overlay_ant_opacity = 1,
  overlay_ant_scale = 1,
  overlay_trail_opacity = 1,
  overlay_button_scale = 1,
  quest_show_level = false,
  quest_show_type = true,
  quest_colour_name = false,
  quest_colour_level = true,
  quest_filter_tracked = false,
  quest_filter_zone = false,
  quest_filter_group = false,
  quest_filter_level = false,
  quest_filter_level_offset = -1,
  sound_enabled = true,
  sound_objective_progress = "Sound/Interface/WriteQuestB.ogg",
  sound_objective_complete = "Sound/Interface/UnwrapGift.ogg",
  sound_objective_unblocked = "Sound/Interface/PlayerInviteA.ogg",
  sound_root_complete = "Sound/Interface/MagicClick.ogg",
  flight_timer_shown = true,
  flight_map_estimates = true,
  flight_create_objectives = false,
  flight_filter_by_level = true,
  database_use = true,
}

-- Fill the cache with the current values of all the settings.
for setting in pairs(settings_default) do
  settings_current[setting] = settings_default[setting]
  settings_current_override[setting] = false
end

-- The order of the settings in this array affect how the configuration panels are
-- put together.
local setting_display_order = {
  "locale",
  "enabled",
  "menu",
  
  "quest_show_level",
  "quest_show_type",
  "quest_colour_name",
  "quest_colour_level",
  "quest_filter_tracked",
  "quest_filter_zone",
  "quest_filter_group",
  "quest_filter_level",
  "quest_filter_level_offset",
  
  "digsite_create_objectives",
  "digsite_filter_continent",
  "digsite_filter_zone",
  
  "tracker_shown",
  "tracker_hide_combat",
  "tracker_locked",
  "tracker_minimized",
  "tracker_scale",
  "tracker_anchor_left",
  "tracker_anchor_bottom",
  "tracker_max_height",
  "tracker_max_width",
  "tracker_shows_routes",
  "tracker_show_objective_tooltips",
  "tracker_show_item_tooltips",
  
  "arrow_shown",
  "arrow_hide_combat",
  "arrow_locked",
  "arrow_scale",
  "arrow_text_width",
  "arrow_text",
  "arrow_distance",
  
  "compass_shown",
  "compass_hide_combat",
  "compass_locked",
  "compass_only_route_locations",
  "compass_scale",
  "compass_width",
  
  "overlay_shown",
  "overlay_button_scale",
  "overlay_trail_opacity",
  "overlay_ant_scale",
  "overlay_ant_opacity",
  "overlay_ant_spacing",
  "overlay_ant_speed",
  
  "sound_enabled",
  "sound_objective_progress",
  "sound_objective_complete",
  "sound_objective_unblocked",
  "sound_root_complete",
  
  "flight_timer_shown",
  "flight_map_estimates",
  "flight_create_objectives",
  "flight_filter_by_level",
  
  "routing_enabled",
  "routing_relaxed_priorities",
  
  "database_use"
}

if true then
  -- Sanity check.
  local set = {}
  for i, setting in ipairs(setting_display_order) do
    assert(settings_default[setting] ~= nil, "Non-existent setting '"..setting.."' in setting_display_order.")
    set[setting] = true
  end
  for setting in pairs(settings_default) do
    assert(set[setting], "Setting '"..setting.."' doesn't appear in setting_display_order.")
  end
end

local function enumAttribute(...)
  return {
    type = "enum",
    ...
  }
end

local static_sound_attribute = {
  type = "enum",
  subtype = "sound",
  filenames = true,
  "Silence",
  "Sound/Spells/FX_UNI_Crystal_Strike_01.ogg",
  "Sound/Item/UseSounds/UseCrinklingPaper.ogg",
  "Sound/Item/UseSounds/iMagicWand1.ogg",
  "Sound/Interface/UnwrapGift.ogg",
  "Sound/Interface/WriteQuestA.ogg",
  "Sound/Interface/WriteQuestB.ogg",
  "Sound/Interface/WriteQuestC.ogg",
  "Sound/Interface/UiLootPickUpItem.ogg",
  "Sound/Interface/PlaceHolder.ogg",
  "Sound/Interface/PlayerInviteA.ogg",
  "Sound/Interface/MagicClick.ogg",
  "Sound/Interface/SheathShield.ogg",
  "Sound/Interface/iMoneyDialogOpen.ogg",
  "Sound/Interface/JewelcraftingFinalize.ogg",
  "Sound/Interface/AuctionWindowOpen.ogg",
  "Sound/Interface/AuctionWindowClose.ogg",
  "Sound/Doodad/HangingBones_BoneClank04.ogg",
  "Sound/Doodad/Deepholm_MercuryDrip01_02.ogg",
  "Sound/Doodad/GO_GHolyLightWell_Birth_02.ogg",
  "Sound/Doodad/DR_MachineParts01_Open.ogg",
  "Sound/Doodad/TitanChestClose.ogg",
  "Sound/Doodad/Ghostlands_Moonstone_Shutdown.ogg",
  "Sound/Creature/Mekkatorque/VO_QE_Mekkatorque_Event04.ogg",
  "Sound/Creature/Brann/VO_HO_Brann_Final02.ogg",
  "Sound/Creature/TitanComputer/VO_HO_TitanComputer_Event01.ogg"
}

local function soundAttribute()
  return static_sound_attribute
end

local function requiresRestart(attribute)
  attribute.requires_restart = true
  return attribute
end

local function addRequirements(attribute, requirements, failure_value)
  for requirement, value in pairs(requirements) do
    assert(settings_default[requirement] ~= nil, "requirement on missing setting: "..requirement)
  end
  
  assert(attribute.failure_value == nil or attribute.failure_value == failure_value)
  
  if attribute.requires then
    for requirement, value in pairs(requirements) do
      attribute.requires[requirement] = value
    end
  else
    attribute.requires = requirements
  end
  
  attribute.failure_value = failure_value
  
  return attribute
end

local function booleanAttribute()
  return { type = "boolean" }
end

local function linearRangeAttribute(low, high)
  return {
    type = "range",
    low = low,
    high = high,
    exponent = 1
  }
end

local function integerRangeAttribute(low, high)
  return {
    type = "range",
    integer = true,
    low = low,
    high = high,
    exponent = 1
  }
end

local function logrithmicRangeAttribute(low, mid, high)
  return {
    type = "range",
    low = low,
    high = high,
    exponent = math.log((high-low)/(mid-low))/math.log(2)
  }
end

local settings_attributes = {
  locale = enumAttribute((GetLocale())), -- We'll fill this with the available locales when they're loaded.
  menu = booleanAttribute(),
  enabled = booleanAttribute(),
  routing_enabled = addRequirements(booleanAttribute(), {enabled=true}, false),
  routing_relaxed_priorities = addRequirements(booleanAttribute(), {routing_enabled=true}),
  arrow_shown = addRequirements(booleanAttribute(), {enabled=true}, false),
  arrow_hide_combat = addRequirements(booleanAttribute(), {arrow_shown=true}),
  arrow_locked = addRequirements(booleanAttribute(), {arrow_shown=true}),
  arrow_scale = addRequirements(logrithmicRangeAttribute(0.25, 1.0, 3.0), {arrow_shown=true}),
  arrow_text = addRequirements(booleanAttribute(), {arrow_shown=true}),
  arrow_text_width = addRequirements(linearRangeAttribute(100, 800), {arrow_shown=true}),
  arrow_distance = addRequirements(booleanAttribute(), {arrow_shown=true}),
  compass_shown = addRequirements(booleanAttribute(), {enabled=true}, false),
  compass_locked = addRequirements(booleanAttribute(), {compass_shown=true}),
  compass_hide_combat = addRequirements(booleanAttribute(), {compass_shown=true}),
  compass_only_route_locations = addRequirements(booleanAttribute(), {compass_shown=true}),
  compass_scale = addRequirements(logrithmicRangeAttribute(0.25, 1.0, 3.0), {compass_shown=true}),
  compass_width = addRequirements(linearRangeAttribute(256, 2048), {compass_shown=true}),
  digsite_create_objectives = booleanAttribute(),
  digsite_filter_continent = addRequirements(booleanAttribute(), {digsite_create_objectives=true, digsite_filter_zone=false}),
  digsite_filter_zone = addRequirements(booleanAttribute(), {digsite_create_objectives=true}),
  tracker_shown = addRequirements(booleanAttribute(), {enabled=true}, false),
  tracker_hide_combat = addRequirements(booleanAttribute(), {tracker_shown=true}),
  tracker_locked = addRequirements(booleanAttribute(), {tracker_shown=true}),
  tracker_scale = addRequirements(logrithmicRangeAttribute(0.25, 1.0, 3.0), {tracker_shown=true}),
  tracker_minimized = addRequirements(booleanAttribute(), {tracker_shown=true}),
  tracker_max_height = addRequirements(linearRangeAttribute(80, 768), {tracker_shown=true}),
  tracker_max_width = addRequirements(linearRangeAttribute(256+16, 768), {tracker_shown=true}),
  tracker_anchor_left = addRequirements(booleanAttribute(), {tracker_shown=true}),
  tracker_anchor_bottom = addRequirements(booleanAttribute(), {tracker_shown=true}),
  tracker_shows_routes = addRequirements(booleanAttribute(), {tracker_shown=true}),
  tracker_show_objective_tooltips = addRequirements(booleanAttribute(), {tracker_shown=true,tracker_locked=false}),
  tracker_show_item_tooltips = addRequirements(booleanAttribute(), {tracker_shown=true}),
  overlay_shown = addRequirements(booleanAttribute(), {enabled=true}, false),
  overlay_ant_spacing = addRequirements(logrithmicRangeAttribute(25, 100, 1000), {overlay_shown=true}),
  overlay_ant_speed = addRequirements(logrithmicRangeAttribute(1, 25, 100), {overlay_shown=true}),
  overlay_ant_opacity = addRequirements(linearRangeAttribute(0, 1), {overlay_shown=true}),
  overlay_trail_opacity = addRequirements(linearRangeAttribute(0, 1), {overlay_shown=true}),
  overlay_ant_scale = addRequirements(logrithmicRangeAttribute(0.25, 1.0, 3.0), {overlay_shown=true}),
  overlay_button_scale = addRequirements(logrithmicRangeAttribute(0.25, 1.0, 3.0), {overlay_shown=true}),
  quest_show_level = booleanAttribute(),
  quest_show_type = addRequirements(booleanAttribute(), {quest_show_level=true}),
  quest_colour_name = booleanAttribute(),
  quest_colour_level = addRequirements(booleanAttribute(), {quest_show_level=true}),
  quest_filter_tracked = booleanAttribute(),
  quest_filter_zone = booleanAttribute(),
  quest_filter_group = booleanAttribute(),
  quest_filter_level = booleanAttribute(),
  quest_filter_level_offset = addRequirements(integerRangeAttribute(-3, 3), {quest_filter_level=true}),
  sound_enabled = addRequirements(booleanAttribute(), {enabled=true}, false),
  sound_objective_progress = addRequirements(soundAttribute(), {sound_enabled=true}),
  sound_objective_complete = addRequirements(soundAttribute(), {sound_enabled=true}),
  sound_objective_unblocked = addRequirements(soundAttribute(), {sound_enabled=true}),
  sound_root_complete = addRequirements(soundAttribute(), {sound_enabled=true}),
  flight_timer_shown = addRequirements(booleanAttribute(), {tracker_shown=true}, false),
  flight_map_estimates = addRequirements(booleanAttribute(), {enabled=true}, false),
  flight_create_objectives = booleanAttribute(),
  flight_filter_by_level = addRequirements(booleanAttribute(), {flight_create_objectives=true}),
  database_use = requiresRestart(booleanAttribute())
}

-- Sanity check to make sure settings_attributes is consistent with settings
for setting in pairs(settings_default) do
  assert(settings_attributes[setting], "No attributes assigned to setting: "..setting)
end

for setting in pairs(settings_attributes) do
  assert(settings_default[setting] ~= nil, "Attribute found for non-existent setting: "..setting)
end

local function settingExists(setting)
  return settings_default[setting] ~= nil
end

local function requirementsMet(setting)
  local attribute = assert(settings_attributes[setting])
  
  if attribute.requires then
    for requirement, value in pairs(attribute.requires) do
      if not settings_current[requirement] == value then
        return false
      end
    end
  end
  
  return true
end

local function calculateSettingValue(setting)
  assert(settingExists(setting))
  
  local overridden = false
  
  if not requirementsMet(setting) then
    local failure_value = settings_attributes[setting].failure_value
    
    if failure_value ~= nil then
      return failure_value, true
    end
    
    -- No failure value, we'll calculate the value the setting should have, but
    -- we'll still announce it as overridden.
    -- The purpose of this case is simply to tell the configuration panels that
    -- the value shouldn't be set and to show it as disabled.
    overridden = true
  end
  
  if settings_override[setting] ~= nil then
    return settings_override[setting], true
  end
  
  if settings_saved[setting] ~= nil then
    return settings_saved[setting], overridden
  end
  
  return settings_default[setting], overridden
end

function shared.getSetting(setting)
  assert(settingExists(setting))
  
  local base = settings_saved[setting]
  
  if base == nil then
    base = settings_default[setting]
  end
  
  return settings_current[setting], base, settings_current_override[setting]
end

function shared.onSetting(setting, cb, ...)
  if not settingExists(setting) then
    error("Undefined setting: "..tostring(setting), 2)
  end
  
  cb = shared.callback(cb, ...)
  
  -- Get the list of callbacks to invoke when this setting is changed.
  local list = settings_callbacks[setting]
  
  if not list then
    -- Create the list if it doesn't currently exist.
    list = {}
    settings_callbacks[setting] = list
  end
  
  -- Insert the callback.
  table.insert(list, cb)
  
  -- Invoke the callback with the current value for the setting.
  cb(settings_current[setting])
end

function shared.onSettingOverride(setting, cb, ...)
  if not settingExists(setting) then
    error("Undefined setting: "..tostring(setting), 2)
  end
  
  cb = shared.callback(cb, ...)
  
  -- Get the list of callbacks to invoke when this setting's override state is changed.
  local list = settings_callbacks_override[setting]
  
  if not list then
    -- Create the list if it doesn't currently exist.
    list = {}
    settings_callbacks_override[setting] = list
  end
  
  -- Insert the callback.
  table.insert(list, cb)
  
  -- Invoke the callback with the current override state.
  cb(settings_current_override[setting])
end

-- Used by updateSetting to avoid doing more work than necessary,
-- and prevent infinite loops caused by circular dependencies.
local visited = {}

local function updateSetting(setting)
  if not visited[setting] then
    visited[setting] = true
    
    local attribute = assert(settings_attributes[setting])
    
    -- Make sure everything we depend on has been updated first.
    if attribute.requires then
      for requirement in pairs(attribute.requires) do
        updateSetting(requirement)
      end
    end
    
    local old_value, old_override = settings_current[setting], settings_current_override[setting]
    local new_value, new_override = calculateSettingValue(setting)
    
    local override_changed = old_override ~= new_override
    
    if override_changed and not new_override then
      -- Announce that the override was removed before we announce the new value.
      settings_current_override[setting] = false
      
      if settings_callbacks_override[setting] then
        shared.invokeCallbackList(settings_callbacks_override[setting], false)
      end
    end
    
    if old_value ~= new_value then
      settings_current[setting] = new_value
      local list = settings_callbacks[setting]
      
      if list then
        shared.invokeCallbackList(list, new_value)
      end
    end
    
    if override_changed and new_override then
      -- Announce that an override was added after we announce the new value.
      settings_current_override[setting] = true
      
      if settings_callbacks_override[setting] then
        shared.invokeCallbackList(settings_callbacks_override[setting], true)
      end
    end
  end
end

local function updateSettings()
  for setting in pairs(settings_default) do
    updateSetting(setting)
  end
  
  table.wipe(visited)
end

local function queueUpdateSettings()
  shared.callOnce(updateSettings)
end

function shared.enumerateSettings()
  local i = 0
  
  return function ()
    i = i + 1
    local setting = setting_display_order[i]
    return setting, settings_attributes[setting]
  end
end

-- Returns true if the setting is valid, followed by the (possible modified) value,
-- or returns false followed by a string describing why the setting isn't valid.
local function validateSetting(setting, value)
  local attribute = settings_attributes[setting]
  
  if attribute then
    if attribute.type == "boolean" then
      return true, value and true or false
    elseif attribute.type == "range" then
      local val = tonumber(value)
      if val then
        if attribute.integer then
          val = math.floor(val+0.5)
        end
        
        return true, math.min(attribute.high, math.max(attribute.low, val))
      else
        return false, ("Can't use '%s' as value for '%s'; not a number."):format(value, setting)
      end
    elseif attribute.type == "enum" then
      local str = tostring(value):lower()
      for i, enum in ipairs(attribute) do
        if tostring(enum):lower() == str then
          return true, enum
        end
      end
      
      return false, ("Can't use '%s' as value for '%s'; expected one of: %s"):format(value, setting, table.concat(attribute, " "))
    end
    
    return false, ("Setting '%s' has unknown attribute type '%s', can't validate."):format(setting, tostring(attribute.type))
  end
  
  return false, ("Setting '%s' has no attributes, can't validate."):format(setting)
end

-- Make sure the default settings are valid.
for setting, value in pairs(settings_default) do
  local valid, message = validateSetting(setting, value)
  assert(valid, "Invalid default setting for setting: "..tostring(message))
  assert(value == message, "Default value for setting required conversion: "..setting)
end

-- metatable for shared.settings
local settings_meta = {}
local override_meta = {}

function settings_meta.__index(self, setting)
  if not settingExists(setting) then
    error("Undefined setting: "..tostring(setting), 2)
  end
  
  return settings_current[setting]
end

function settings_meta.__newindex(self, setting, value)
  if not settingExists(setting) then
    error("Undefined setting: "..tostring(setting), 2)
  end
  
  if settings_override[setting] ~= nil then
    print("Attempt to alter '"..setting.."' ignored; its value is currently overridden.")
    return
  end
  
  if value ~= nil then
    local valid, result = validateSetting(setting, value)
    if not valid then error(result, 2)
    else value = result end
  end
  
  settings_saved[setting] = value
  
  queueUpdateSettings()
end

function override_meta.__index(self, setting)
  if not settingExists(setting) then
    error("Undefined setting: "..tostring(setting), 2)
  end
  
  if not requirementsMet(setting) then
    return shared.settings_attributes[setting].failure_value
  end
  
  return settings_override[setting]
end

function override_meta.__newindex(self, setting, value)
  if not settingExists(setting) then
    error("Undefined setting: "..tostring(setting), 2)
  end
  
  if value ~= nil then
    local valid, result = validateSetting(setting, value)
    if not valid then error(result, 2)
    else value = result end
  end
  
  settings_override[setting] = value
  
  queueUpdateSettings()
end

shared.onLoad(function ()
  -- Remove the list of locale enumerations (probably only whatever the game client is configured to use at the moment.)
  local locale_attr = settings_attributes.locale
  locale_attr.translate_keys = true
  for i, enum in ipairs(locale_attr) do locale_attr[i] = nil end
  
  -- Fill the list of locale enumerations with all the locales that were loaded.
  for locale, keys in pairs(shared.locale) do table.insert(locale_attr, locale) end
  
  -- Change the default setting to enabled.
  settings_default.enabled = true
  
  -- Load the settings.
  _G.QuestHelper2_Settings = _G.QuestHelper2_Settings or settings_saved
  settings_saved = _G.QuestHelper2_Settings
  
  -- Make sure all the saved settings have valid values.
  for setting, value in pairs(settings_saved) do
    local valid, result = validateSetting(setting, value)
    if valid then
      settings_saved[setting] = result
    else
      settings_saved[setting] = nil
    end
  end
  
  updateSettings()
end)

shared.settings = setmetatable({}, settings_meta)
shared.settings_override = setmetatable({}, override_meta)

-- Export the attributes, as a convenience to the configuration panel.
shared.settings_attributes = settings_attributes
