-- LidPanels_Core.lua
-- LidPanels addon with in-game editor (Panels + Profiles + UI Settings), drag/resize, per-panel lock, profiles.

LidPanelsDB = LidPanelsDB or {}
_G.LIDPANELS_ADDON_NAME = "LidPanels"

_G.LIDPANELS_Debug = _G.LIDPANELS_Debug or { info = {}, errors = {}, silent = {} }

local function GetDebugStore()
  return _G.LIDPANELS_Debug
end

local function GetEditorFrame()
  local e = _G.LIDPANELS_Editor
  if e and e.__lidPanelsEditor then return e end
  return nil
end

local function DebugInit()
  local debugStore = GetDebugStore()
  if not debugStore then
    _G.LIDPANELS_Debug = { info = {}, errors = {}, silent = {} }
    debugStore = _G.LIDPANELS_Debug
  end
  debugStore.info = debugStore.info or {}
  debugStore.errors = debugStore.errors or {}
  debugStore.silent = debugStore.silent or {}
end

-- Keep internal helpers local; expose only namespaced LIDPANELS_* APIs.
local Print, RootDB, DB, DeepCopy
local ExportProfile, ImportProfile
local GetSpecKey, GetCurrentSpecID, GetCharacterKey
local AddCharacterToList, GetCharacterList
local LoadLayoutFromSpec
local EnsureDefaults
local characterListSnapshotSource, characterListSnapshot

local DEBUG_MAX_INFO = 200
local DEBUG_MAX_ERRORS = 200
local DEBUG_MAX_SILENT = 200

local function PushDebug(list, msg, maxItems)
  list[#list + 1] = msg
  local overflow = #list - maxItems
  if overflow > 0 then
    for _ = 1, overflow do
      table.remove(list, 1)
    end
  end
end

local function LogInfo(msg)
  DebugInit()
  if type(msg) ~= "string" then msg = tostring(msg) end
  local debugStore = GetDebugStore()
  PushDebug(debugStore.info, msg, DEBUG_MAX_INFO)
end

local function LogError(msg)
  DebugInit()
  if type(msg) ~= "string" then msg = tostring(msg) end
  local debugStore = GetDebugStore()
  PushDebug(debugStore.errors, msg, DEBUG_MAX_ERRORS)
  local e = GetEditorFrame()
  if e and e.RefreshAbout then e:RefreshAbout() end
end

local function LogSilent(msg)
  local ui = LidPanelsDB and LidPanelsDB.ui
  if not (ui and ui.developerMode == true) then
    return
  end
  DebugInit()
  if type(msg) ~= "string" then msg = tostring(msg) end
  local debugStore = GetDebugStore()
  PushDebug(debugStore.silent, msg, DEBUG_MAX_SILENT)
  local e = GetEditorFrame()
  if e and e.RefreshAbout then e:RefreshAbout() end
end

_G.LIDPANELS_LogInfo = LogInfo
_G.LIDPANELS_LogError = LogError
_G.LIDPANELS_LogSilent = LogSilent

LogInfo("Core.lua loaded")

function Print(msg)
  DEFAULT_CHAT_FRAME:AddMessage("|cff66ccff[LidPanels]|r " .. tostring(msg))
end

-- ============================================================
-- ##--------------------------DB--------------------------------
-- ============================================================

function RootDB()
  LidPanelsDB = LidPanelsDB or {}
  LidPanelsDB.profiles = LidPanelsDB.profiles or {}
  LidPanelsDB.profileGroups = LidPanelsDB.profileGroups or {}
  LidPanelsDB.currentProfile = LidPanelsDB.currentProfile or "Default"
  for _, p in pairs(LidPanelsDB.profiles) do
    if type(p) == "table" then
      p.panelOrder = nil
    end
  end

  LidPanelsDB.ui = LidPanelsDB.ui or {}
  LidPanelsDB.ui.skin = "LP"
  LidPanelsDB.ui.headerBg = nil
  -- Remove legacy custom-skin settings; LidPanels uses one fixed skin preset.
  LidPanelsDB.ui.theme = nil
  LidPanelsDB.ui.font = nil
  LidPanelsDB.ui.__themeFromPreset = nil
  LidPanelsDB.ui.__fontFromPreset = nil
  if LidPanelsDB.ui.disableTutorialImages == nil then
    LidPanelsDB.ui.disableTutorialImages = false
  end
  if LidPanelsDB.ui.developerMode == nil then
    LidPanelsDB.ui.developerMode = false
  end
  if LidPanelsDB.ui.panelEditMode ~= "PREVIEW" then
    LidPanelsDB.ui.panelEditMode = "EDITOR"
  end
  -- Keep editor scale fixed to the 4K baseline; size adapts to screen via layout.
  LidPanelsDB.ui.editorScalePreset = "4K"
  LidPanelsDB.ui.editorScale = 1.0
  local defaultMinimapSize = 28
  if LidPanelsDB.ui.editorScalePreset == "4K" then defaultMinimapSize = 43 end
  LidPanelsDB.ui.minimap = LidPanelsDB.ui.minimap or { size = defaultMinimapSize, angle = 45, alpha = 1, onlyOnHover = false }
  if LidPanelsDB.ui.minimap.size == nil then LidPanelsDB.ui.minimap.size = defaultMinimapSize end
  if LidPanelsDB.ui.minimap.angle == nil then LidPanelsDB.ui.minimap.angle = 45 end
  if LidPanelsDB.ui.minimap.alpha == nil then LidPanelsDB.ui.minimap.alpha = 1 end
  if LidPanelsDB.ui.minimap.onlyOnHover == nil then LidPanelsDB.ui.minimap.onlyOnHover = false end

  LidPanelsDB.characters = LidPanelsDB.characters or {}

  return LidPanelsDB
end

function DB()
  local root = RootDB()
  local name = root.currentProfile

  root.profiles[name] = root.profiles[name] or {
    panels = {},
    positions = {},
    spec = { auto = false, layouts = {} },
    __seeded = false
  }

  local db = root.profiles[name]
  db.spec = db.spec or { auto = false, layouts = {} }
  db.spec.layouts = db.spec.layouts or {}
  if db.spec.auto == nil then db.spec.auto = false end
  db.profileSpec = db.profileSpec or { enabled = false, mode = "SPEC", specID = nil, character = nil }
  if db.profileSpec.enabled == nil then db.profileSpec.enabled = false end
  if db.profileSpec.mode ~= "CHARACTER" then db.profileSpec.mode = "SPEC" end

  return db
end

function DeepCopy(src)
  if type(src) ~= "table" then return src end
  local dst = {}
  for k, v in pairs(src) do dst[k] = DeepCopy(v) end
  return dst
end

-- ============================================================
-- ##--------------------------SAFE IMPORT / EXPORT------------- 
-- ============================================================

local EXPORT_PREFIX = "LIDJ1:"

local function is_array(t)
  local n = 0
  for k, _ in pairs(t) do
    if type(k) ~= "number" or k < 1 or k % 1 ~= 0 then return false end
    if k > n then n = k end
  end
  for i = 1, n do if t[i] == nil then return false end end
  return true, n
end

local function json_escape(s)
  s = s:gsub("\\", "\\\\")
  s = s:gsub("\"", "\\\"")
  s = s:gsub("\n", "\\n")
  s = s:gsub("\r", "\\r")
  s = s:gsub("\t", "\\t")
  return s
end

local function json_encode(v)
  local tv = type(v)
  if tv == "nil" then
    return "null"
  elseif tv == "boolean" then
    return v and "true" or "false"
  elseif tv == "number" then
    if v ~= v or v == math.huge or v == -math.huge then return "null" end
    return tostring(v)
  elseif tv == "string" then
    return "\"" .. json_escape(v) .. "\""
  elseif tv == "table" then
    local arr, n = is_array(v)
    if arr then
      local parts = {"["}
      for i = 1, n do
        parts[#parts+1] = json_encode(v[i])
        if i < n then parts[#parts+1] = "," end
      end
      parts[#parts+1] = "]"
      return table.concat(parts)
    else
      local parts = {"{"}
      local first = true
      for k, val in pairs(v) do
        local kt = type(k)
        local key
        if kt == "string" then key = k
        elseif kt == "number" then key = tostring(k)
        end
        if key ~= nil then
          if not first then parts[#parts+1] = "," end
          first = false
          parts[#parts+1] = "\"" .. json_escape(key) .. "\":" .. json_encode(val)
        end
      end
      parts[#parts+1] = "}"
      return table.concat(parts)
    end
  end
  return "null"
end

local function json_decode(str)
  if type(str) ~= "string" then return nil end
  local i, len = 1, #str

  local function skip_ws()
    while i <= len do
      local c = str:sub(i, i)
      if c == " " or c == "\n" or c == "\r" or c == "\t" then i = i + 1 else break end
    end
  end

  local function parse_string()
    i = i + 1
    local out = {}
    while i <= len do
      local c = str:sub(i, i)
      if c == "\"" then i = i + 1; return table.concat(out) end
      if c == "\\" then
        local n = str:sub(i+1, i+1)
        if n == "\"" or n == "\\" or n == "/" then out[#out+1] = n; i = i + 2
        elseif n == "n" then out[#out+1] = "\n"; i = i + 2
        elseif n == "r" then out[#out+1] = "\r"; i = i + 2
        elseif n == "t" then out[#out+1] = "\t"; i = i + 2
        else return nil end
      else
        out[#out+1] = c
        i = i + 1
      end
    end
    return nil
  end

  local function parse_number()
    local start = i
    local c = str:sub(i, i)
    if c == "-" then i = i + 1 end
    while i <= len and str:sub(i, i):match("%d") do i = i + 1 end
    if str:sub(i, i) == "." then
      i = i + 1
      while i <= len and str:sub(i, i):match("%d") do i = i + 1 end
    end
    c = str:sub(i, i)
    if c == "e" or c == "E" then
      i = i + 1
      c = str:sub(i, i)
      if c == "+" or c == "-" then i = i + 1 end
      while i <= len and str:sub(i, i):match("%d") do i = i + 1 end
    end
    return tonumber(str:sub(start, i-1))
  end

  local parse_value

  local function parse_array()
    i = i + 1
    local arr = {}
    skip_ws()
    if str:sub(i, i) == "]" then i = i + 1; return arr end
    local idx = 1
    while i <= len do
      local v = parse_value()
      if v == nil and str:sub(i-4, i-1) ~= "null" then return nil end
      arr[idx] = v; idx = idx + 1
      skip_ws()
      local c = str:sub(i, i)
      if c == "," then i = i + 1; skip_ws()
      elseif c == "]" then i = i + 1; return arr
      else return nil end
    end
    return nil
  end

  local function parse_object()
    i = i + 1
    local obj = {}
    skip_ws()
    if str:sub(i, i) == "}" then i = i + 1; return obj end
    while i <= len do
      skip_ws()
      if str:sub(i, i) ~= "\"" then return nil end
      local key = parse_string()
      if not key then return nil end
      skip_ws()
      if str:sub(i, i) ~= ":" then return nil end
      i = i + 1
      skip_ws()
      local val = parse_value()
      if val == nil and str:sub(i-4, i-1) ~= "null" then return nil end

      local nkey = tonumber(key)
      if nkey and tostring(nkey) == key then obj[nkey] = val else obj[key] = val end

      skip_ws()
      local c = str:sub(i, i)
      if c == "," then i = i + 1
      elseif c == "}" then i = i + 1; return obj
      else return nil end
    end
    return nil
  end

  parse_value = function()
    skip_ws()
    if i > len then return nil end
    local c = str:sub(i, i)
    if c == "\"" then return parse_string()
    elseif c == "{" then return parse_object()
    elseif c == "[" then return parse_array()
    elseif c == "t" and str:sub(i, i+3) == "true" then i = i + 4; return true
    elseif c == "f" and str:sub(i, i+4) == "false" then i = i + 5; return false
    elseif c == "n" and str:sub(i, i+3) == "null" then i = i + 4; return nil
    elseif c == "-" or c:match("%d") then return parse_number()
    end
    return nil
  end

  local val = parse_value()
  skip_ws()
  if i <= len then return nil end
  return val
end

function ExportProfile(profileName)
  local root = RootDB()
  local p = root.profiles[profileName]
  if not p then return nil end
  return EXPORT_PREFIX .. json_encode(p)
end

function ImportProfile(newName, dataStr)
  local root = RootDB()
  if root.profiles[newName] then
    Print("Profile already exists: " .. newName)
    return false
  end
  if type(dataStr) ~= "string" or dataStr:sub(1, #EXPORT_PREFIX) ~= EXPORT_PREFIX then
    Print("Import failed: string not recognized (missing Lid prefix).")
    return false
  end

  local payload = dataStr:sub(#EXPORT_PREFIX + 1)
  local data = json_decode(payload)
  if type(data) ~= "table" or type(data.panels) ~= "table" then
    Print("Import failed: invalid data.")
    return false
  end

  data.positions = data.positions or {}
  data.spec = data.spec or { auto = false, layouts = {} }
  data.spec.layouts = data.spec.layouts or {}
  data.profileSpec = data.profileSpec or { enabled = false, mode = "SPEC", specID = nil, character = nil }
  if data.profileSpec.mode ~= "CHARACTER" then data.profileSpec.mode = "SPEC" end
  data.panelOrder = nil

  root.profiles[newName] = data
  Print("Imported profile: " .. newName)
  return true
end

-- ============================================================
-- ##--------------------------SPEC LAYOUTS----------------------
-- ============================================================

function GetSpecKey()
  if not GetSpecialization or not GetSpecializationInfo then return "nospec" end
  local idx = GetSpecialization()
  if not idx then return "nospec" end
  local specID = GetSpecializationInfo(idx)
  return tostring(specID or idx or "nospec")
end

function GetCurrentSpecID()
  if not GetSpecialization or not GetSpecializationInfo then return nil end
  local idx = GetSpecialization()
  if not idx then return nil end
  local specID = GetSpecializationInfo(idx)
  return specID
end

function GetCharacterKey()
  if not UnitName then return nil end
  local name = UnitName("player")
  if not name then return nil end
  local realm = nil
  if GetNormalizedRealmName then
    realm = GetNormalizedRealmName()
  elseif GetRealmName then
    realm = GetRealmName()
  end
  if realm and realm ~= "" then
    return name .. "-" .. realm
  end
  return name
end

local function EnsureCharacterList()
  local root = RootDB()
  root.characters = root.characters or {}
  return root.characters
end

function AddCharacterToList(key)
  if not key or key == "" then return end
  local list = EnsureCharacterList()
  for _, v in ipairs(list) do
    if v == key then return end
  end
  list[#list + 1] = key
  table.sort(list)
  characterListSnapshotSource = nil
  characterListSnapshot = nil
end

function GetCharacterList()
  local list = EnsureCharacterList()
  if characterListSnapshotSource ~= list or type(characterListSnapshot) ~= "table" then
    local snapshot = {}
    for i, v in ipairs(list) do snapshot[i] = v end
    table.sort(snapshot)
    characterListSnapshotSource = list
    characterListSnapshot = snapshot
  end
  local out = {}
  for i, v in ipairs(characterListSnapshot) do out[i] = v end
  return out
end

function LoadLayoutFromSpec(specKey)
  local db = DB()
  local lay = db.spec.layouts[specKey]
  if not lay then
    Print("No saved layout for this spec yet.")
    return false
  end
  db.panels = DeepCopy(lay.panels or {})
  db.positions = DeepCopy(lay.positions or {})
  Print("Loaded layout for spec: " .. specKey)
  return true
end

-- ============================================================
-- ##--------------------------DEFAULT PANELS--------------------
-- ============================================================

function EnsureDefaults()
  local db = DB()
  local root = RootDB()
  local BG_SEED_VERSION = 2
  root.backgroundCategories = root.backgroundCategories or {}
  local function findCategory(name)
    local needle = string.lower(tostring(name or ""))
    for _, cat in ipairs(root.backgroundCategories) do
      if string.lower(tostring(cat.name or "")) == needle then return cat end
    end
    return nil
  end
  local function isLegacyDefaultBackground(name)
    local n = string.lower(tostring(name or ""))
    return n == "xalatath" or n == "chc3" or n == "chc9"
  end
  local function pruneLegacyDefaults(list)
    if type(list) ~= "table" then return end
    for i = #list, 1, -1 do
      if isLegacyDefaultBackground(list[i]) then
        table.remove(list, i)
      end
    end
  end
  local function hasBackground(list, name)
    local needle = string.lower(tostring(name or ""))
    for _, item in ipairs(list or {}) do
      if string.lower(tostring(item or "")) == needle then
        return true
      end
    end
    return false
  end
  local cat = findCategory("My Collection")
  if root.__bgCategoriesSeeded ~= true then
    if not cat then
      cat = { name = "My Collection", items = {} }
      root.backgroundCategories[#root.backgroundCategories + 1] = cat
    end
    cat.items = cat.items or {}
    pruneLegacyDefaults(cat.items)
    if not hasBackground(cat.items, "DH_Devourer") then
      cat.items[#cat.items + 1] = "DH_Devourer"
    end
    if not hasBackground(cat.items, "Mage_Fire") then
      cat.items[#cat.items + 1] = "Mage_Fire"
    end
    root.__bgSeedVersion = BG_SEED_VERSION
    root.__bgCategoriesSeeded = true
  else
    if cat then
      cat.items = cat.items or {}
      pruneLegacyDefaults(cat.items)
    end
    if (tonumber(root.__bgSeedVersion) or 0) < BG_SEED_VERSION then
      if cat then
        if not hasBackground(cat.items, "DH_Devourer") then
          cat.items[#cat.items + 1] = "DH_Devourer"
        end
        if not hasBackground(cat.items, "Mage_Fire") then
          cat.items[#cat.items + 1] = "Mage_Fire"
        end
      end
      root.__bgSeedVersion = BG_SEED_VERSION
    end
  end

  if db.__seeded then return end
  db.__seeded = true
end

-- ============================================================

-- Stable, namespaced API surface for cross-file/internal lookups.
local API = _G.LIDPANELS_API
if type(API) ~= "table" then
  API = {}
end
API.__internal = API.__internal or {}
_G.LIDPANELS_API = API

local function PublishApi(name, value)
  _G[name] = value
  API[name] = value
end

PublishApi("LIDPANELS_ADDON_NAME", _G.LIDPANELS_ADDON_NAME)
PublishApi("LIDPANELS_RootDB", RootDB)
PublishApi("LIDPANELS_DB", DB)
PublishApi("LIDPANELS_DeepCopy", DeepCopy)
PublishApi("LIDPANELS_Print", Print)
PublishApi("LIDPANELS_ExportProfile", ExportProfile)
PublishApi("LIDPANELS_ImportProfile", ImportProfile)
PublishApi("LIDPANELS_GetSpecKey", GetSpecKey)
PublishApi("LIDPANELS_GetCurrentSpecID", GetCurrentSpecID)
PublishApi("LIDPANELS_GetCharacterKey", GetCharacterKey)
PublishApi("LIDPANELS_AddCharacterToList", AddCharacterToList)
PublishApi("LIDPANELS_GetCharacterList", GetCharacterList)
PublishApi("LIDPANELS_LoadLayoutFromSpec", LoadLayoutFromSpec)
PublishApi("LIDPANELS_EnsureDefaults", EnsureDefaults)
