
--ui

local S = minetest.get_translator(minetest.get_current_modname())

local old_values = {}
local index = {}
local change_preview_type = {}
local FOOTER_MODE_OPTIONS = {"Compact", "Advanced"}
local SPAWN_THRESHOLD_OPTIONS = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"}
local HUD_ALIGNMENT_OPTIONS = {
  "Top-Middle",
  "Top-Left",
  "Top-Right",
  "Middle-Left",
  "Middle-Right",
  "Bottom-Left",
  "Bottom-Right",
}
local PREVIEW_DEBOUNCE_SECONDS = 0.4
local LAYOUT_REBUILD_DRAG_IDLE_US = 450000
local LAYOUT_REBUILD_NUDGE_IDLE_US = 60000
local LAYOUT_VAL_AS_NUDGE_GAP_US = 140000

local layout_last_chg_us = {}

eye_spy.ui_drafts = eye_spy.ui_drafts or {}

local LAYOUT_TARGET_OPTIONS = {"Global", "BG", "Icon", "Title", "Subtitle", "Lines", "Footer"}
local LAYOUT_STEP_OPTIONS = {1, 2, 4, 8}
local LAYOUT_PAGE_OPTIONS = {"Position", "Panel", "Text"}
local LAYOUT_META_DEFAULTS = {
  es_layout_page = "Position",
  es_layout_target = "Global",
  es_layout_step = 2,
  es_global_offset_x = 0,
  es_global_offset_y = 0,
  es_bg_offset_x = 0,
  es_bg_offset_y = 0,
  es_icon_offset_x = 0,
  es_icon_offset_y = 0,
  es_title_offset_x = 0,
  es_title_offset_y = 0,
  es_subtitle_offset_x = 0,
  es_subtitle_offset_y = 0,
  es_lines_offset_x = 0,
  es_lines_offset_y = 0,
  es_footer_offset_x = 0,
  es_footer_offset_y = 0,
  es_bg_pad_left = 7,
  es_bg_pad_right = 0,
  es_bg_pad_top = 5,
  es_bg_pad_bottom = 0,
  es_bg_extra_w = 0,
  es_bg_extra_h = 0,
  es_bg_scale_x_pct = 100,
  es_bg_scale_y_pct = 100,
  es_icon_size = 56,
  es_title_scale_pct = 100,
  es_subtitle_scale_pct = 100,
  es_line_scale_pct = 100,
  es_footer_scale_pct = 100,
  es_first_line_y_adj = 0,
  es_line_step_adj = -2,
  es_footer_nudge_adj = 0,
  es_text_base_x_adj = 0,
  es_icon_base_x_adj = 0,
  es_top_margin_adj = 0,
  es_bottom_margin_adj = 0,
}

local LAYOUT_TARGET_KEYS = {
  Global = {x = "es_global_offset_x", y = "es_global_offset_y"},
  BG = {x = "es_bg_offset_x", y = "es_bg_offset_y"},
  Icon = {x = "es_icon_offset_x", y = "es_icon_offset_y"},
  Title = {x = "es_title_offset_x", y = "es_title_offset_y"},
  Subtitle = {x = "es_subtitle_offset_x", y = "es_subtitle_offset_y"},
  Lines = {x = "es_lines_offset_x", y = "es_lines_offset_y"},
  Footer = {x = "es_footer_offset_x", y = "es_footer_offset_y"},
}

local LAYOUT_RESET_GROUPS = {
  Global = {"es_global_offset_x", "es_global_offset_y", "es_top_margin_adj", "es_bottom_margin_adj"},
  BG = {
    "es_bg_offset_x",
    "es_bg_offset_y",
    "es_bg_pad_left",
    "es_bg_pad_right",
    "es_bg_pad_top",
    "es_bg_pad_bottom",
    "es_bg_extra_w",
    "es_bg_extra_h",
    "es_bg_scale_x_pct",
    "es_bg_scale_y_pct",
  },
  Icon = {"es_icon_offset_x", "es_icon_offset_y", "es_icon_size", "es_icon_base_x_adj"},
  Title = {"es_title_offset_x", "es_title_offset_y", "es_title_scale_pct"},
  Subtitle = {"es_subtitle_offset_x", "es_subtitle_offset_y", "es_subtitle_scale_pct"},
  Lines = {
    "es_lines_offset_x",
    "es_lines_offset_y",
    "es_line_scale_pct",
    "es_first_line_y_adj",
    "es_line_step_adj",
    "es_text_base_x_adj",
  },
  Footer = {"es_footer_offset_x", "es_footer_offset_y", "es_footer_scale_pct", "es_footer_nudge_adj"},
}

local LAYOUT_PAGE_SLIDERS = {
  Position = {
    ["*"] = {
      {label = "Offset X", key = "$target_x", min = -400, max = 400, x = 0.7, y = 3.7, width = 4.0},
      {label = "Offset Y", key = "$target_y", min = -400, max = 400, x = 5.0, y = 3.7, width = 4.0},
    },
    Global = {
      {label = "Top Margin", key = "es_top_margin_adj", min = -50, max = 120, x = 9.3, y = 3.7, width = 4.0},
      {label = "Bottom Margin", key = "es_bottom_margin_adj", min = -50, max = 140, x = 0.7, y = 5.1, width = 4.0},
      {label = "Text Base X", key = "es_text_base_x_adj", min = -180, max = 220, x = 5.0, y = 5.1, width = 4.0},
      {label = "Icon Base X", key = "es_icon_base_x_adj", min = -180, max = 220, x = 9.3, y = 5.1, width = 4.0},
    },
  },
  Panel = {
    BG = {
      {label = "Pad Left", key = "es_bg_pad_left", min = -200, max = 400, x = 0.7, y = 3.35, width = 4.0},
      {label = "Pad Right", key = "es_bg_pad_right", min = -200, max = 400, x = 5.0, y = 3.35, width = 4.0},
      {label = "Pad Top", key = "es_bg_pad_top", min = -200, max = 400, x = 9.3, y = 3.35, width = 4.0},
      {label = "Pad Bottom", key = "es_bg_pad_bottom", min = -200, max = 400, x = 0.7, y = 4.75, width = 4.0},
      {label = "Extra W", key = "es_bg_extra_w", min = -300, max = 500, x = 5.0, y = 4.75, width = 4.0},
      {label = "Extra H", key = "es_bg_extra_h", min = -300, max = 500, x = 9.3, y = 4.75, width = 4.0},
      {label = "Scale X%", key = "es_bg_scale_x_pct", min = 25, max = 300, x = 0.7, y = 6.15, width = 4.0},
      {label = "Scale Y%", key = "es_bg_scale_y_pct", min = 25, max = 300, x = 5.0, y = 6.15, width = 4.0},
      {label = "BG Offset X", key = "es_bg_offset_x", min = -400, max = 400, x = 9.3, y = 6.15, width = 4.0},
      {label = "BG Offset Y", key = "es_bg_offset_y", min = -400, max = 400, x = 0.7, y = 7.55, width = 4.0},
    },
    Icon = {
      {label = "Icon Offset X", key = "es_icon_offset_x", min = -400, max = 400, x = 0.7, y = 3.35, width = 4.0},
      {label = "Icon Offset Y", key = "es_icon_offset_y", min = -400, max = 400, x = 5.0, y = 3.35, width = 4.0},
      {label = "Icon Size", key = "es_icon_size", min = 16, max = 128, x = 9.3, y = 3.35, width = 4.0},
      {label = "Icon Base X", key = "es_icon_base_x_adj", min = -180, max = 220, x = 0.7, y = 4.75, width = 4.0},
    },
  },
  Text = {
    Title = {
      {label = "Title Scale", key = "es_title_scale_pct", min = 40, max = 300, x = 0.7, y = 3.35, width = 4.0},
      {label = "Title Off X", key = "es_title_offset_x", min = -400, max = 400, x = 5.0, y = 3.35, width = 4.0},
      {label = "Title Off Y", key = "es_title_offset_y", min = -400, max = 400, x = 9.3, y = 3.35, width = 4.0},
    },
    Subtitle = {
      {label = "Main Scale", key = "es_subtitle_scale_pct", min = 40, max = 300, x = 0.7, y = 3.35, width = 4.0},
      {label = "Main Off X", key = "es_subtitle_offset_x", min = -400, max = 400, x = 5.0, y = 3.35, width = 4.0},
      {label = "Main Off Y", key = "es_subtitle_offset_y", min = -400, max = 400, x = 9.3, y = 3.35, width = 4.0},
    },
    Lines = {
      {label = "Line Scale", key = "es_line_scale_pct", min = 40, max = 300, x = 0.7, y = 3.35, width = 4.0},
      {label = "Lines Off X", key = "es_lines_offset_x", min = -400, max = 400, x = 5.0, y = 3.35, width = 4.0},
      {label = "Lines Off Y", key = "es_lines_offset_y", min = -400, max = 400, x = 9.3, y = 3.35, width = 4.0},
      {label = "First Line Y", key = "es_first_line_y_adj", min = -120, max = 220, x = 0.7, y = 4.75, width = 4.0},
      {label = "Line Step", key = "es_line_step_adj", min = -8, max = 50, x = 5.0, y = 4.75, width = 4.0},
      {label = "Text Base X", key = "es_text_base_x_adj", min = -180, max = 220, x = 9.3, y = 4.75, width = 4.0},
    },
    Footer = {
      {label = "Footer Scale", key = "es_footer_scale_pct", min = 40, max = 300, x = 0.7, y = 3.35, width = 4.0},
      {label = "Footer Off X", key = "es_footer_offset_x", min = -400, max = 400, x = 5.0, y = 3.35, width = 4.0},
      {label = "Footer Off Y", key = "es_footer_offset_y", min = -400, max = 400, x = 9.3, y = 3.35, width = 4.0},
      {label = "Footer Nudge", key = "es_footer_nudge_adj", min = -120, max = 120, x = 0.7, y = 4.75, width = 4.0},
    },
  },
}

local LAYOUT_SLIDER_BOUNDS = {
  es_global_offset_x = {min = -400, max = 400},
  es_global_offset_y = {min = -400, max = 400},
  es_bg_offset_x = {min = -400, max = 400},
  es_bg_offset_y = {min = -400, max = 400},
  es_icon_offset_x = {min = -400, max = 400},
  es_icon_offset_y = {min = -400, max = 400},
  es_title_offset_x = {min = -400, max = 400},
  es_title_offset_y = {min = -400, max = 400},
  es_subtitle_offset_x = {min = -400, max = 400},
  es_subtitle_offset_y = {min = -400, max = 400},
  es_lines_offset_x = {min = -400, max = 400},
  es_lines_offset_y = {min = -400, max = 400},
  es_footer_offset_x = {min = -400, max = 400},
  es_footer_offset_y = {min = -400, max = 400},
  es_bg_pad_left = {min = -200, max = 400},
  es_bg_pad_right = {min = -200, max = 400},
  es_bg_pad_top = {min = -200, max = 400},
  es_bg_pad_bottom = {min = -200, max = 400},
  es_bg_extra_w = {min = -300, max = 500},
  es_bg_extra_h = {min = -300, max = 500},
  es_bg_scale_x_pct = {min = 25, max = 300},
  es_bg_scale_y_pct = {min = 25, max = 300},
  es_icon_size = {min = 16, max = 128},
  es_title_scale_pct = {min = 40, max = 300},
  es_subtitle_scale_pct = {min = 40, max = 300},
  es_line_scale_pct = {min = 40, max = 300},
  es_footer_scale_pct = {min = 40, max = 300},
  es_first_line_y_adj = {min = -120, max = 220},
  es_line_step_adj = {min = -8, max = 50},
  es_footer_nudge_adj = {min = -120, max = 120},
  es_text_base_x_adj = {min = -180, max = 220},
  es_icon_base_x_adj = {min = -180, max = 220},
  es_top_margin_adj = {min = -50, max = 120},
  es_bottom_margin_adj = {min = -50, max = 140},
}

local DRAFT_KEY_TYPES = {
  es_hud_color = "string",
  es_hud_alignment = "string",
  es_hud_health_in = "string",
  es_auto_text_color = "string",
  es_title_color_val = "int",
  es_subtitle_color_val = "int",
  es_footer_color_val = "int",
  es_line_color_val = "int",
  es_growth_color_val = "int",
  es_soil_color_val = "int",
  es_footer_mode = "string",
  es_show_coords = "string",
  es_show_light_level = "string",
  es_show_spawn_hint = "string",
  es_show_liquid_info = "string",
  es_show_dig_time = "string",
  es_spawn_safe_light_threshold = "int",
  es_layout_page = "string",
  es_layout_target = "string",
  es_layout_step = "int",
}

for key, default_value in pairs(LAYOUT_META_DEFAULTS) do
  if type(default_value) == "number" then
    DRAFT_KEY_TYPES[key] = "int"
  else
    DRAFT_KEY_TYPES[key] = "string"
  end
end

local function get_player_draft(player_name, create)
  if not player_name then
    return nil
  end

  local draft = eye_spy.ui_drafts[player_name]

  if not draft and create then
    draft = {}
    eye_spy.ui_drafts[player_name] = draft
  end

  return draft
end

local function clear_player_draft(player_name)
  if player_name then
    eye_spy.ui_drafts[player_name] = nil
  end
end

local function set_draft_value(player_name, key, value)
  local draft = get_player_draft(player_name, true)

  if draft then
    draft[key] = value
  end
end

local function get_effective_string(meta, player_name, key)
  local draft = get_player_draft(player_name, false)

  if draft and draft[key] ~= nil then
    return tostring(draft[key])
  end

  return meta:get_string(key)
end

local function get_effective_int(meta, player_name, key)
  local draft = get_player_draft(player_name, false)

  if draft and draft[key] ~= nil then
    return tonumber(draft[key]) or 0
  end

  return meta:get_int(key)
end

local function commit_player_draft(player)
  if not player then
    return
  end

  local player_name = player:get_player_name()
  local draft = get_player_draft(player_name, false)

  if not draft then
    return
  end

  local meta = player:get_meta()

  for key, value in pairs(draft) do
    local key_type = DRAFT_KEY_TYPES[key]

    if key_type == "int" then
      meta:set_int(key, tonumber(value) or 0)
    elseif key_type == "string" then
      meta:set_string(key, tostring(value))
    end
  end

  clear_player_draft(player_name)

  if eye_spy.render and eye_spy.render.invalidate_layout_sig then
    eye_spy.render.invalidate_layout_sig(player_name)
  end
end

local function get_option_index(options, value)
  for i, option in ipairs(options) do
    if tostring(option) == tostring(value) then
      return i
    end
  end

  return 1
end

local function save_old_values(meta)
  local values = {
    color = meta:get_string("es_hud_color"),
    hud_alignment = meta:get_string("es_hud_alignment"),
    health_in = meta:get_string("es_hud_health_in"),
    auto_text_color = meta:get_string("es_auto_text_color"),
    title_color_val = meta:get_int("es_title_color_val"),
    subtitle_color_val = meta:get_int("es_subtitle_color_val"),
    footer_color_val = meta:get_int("es_footer_color_val"),
    line_color_val = meta:get_int("es_line_color_val"),
    growth_color_val = meta:get_int("es_growth_color_val"),
    soil_color_val = meta:get_int("es_soil_color_val"),
    footer_mode = meta:get_string("es_footer_mode"),
    show_coords = meta:get_string("es_show_coords"),
    show_light_level = meta:get_string("es_show_light_level"),
    show_spawn_hint = meta:get_string("es_show_spawn_hint"),
    show_liquid_info = meta:get_string("es_show_liquid_info"),
    show_dig_time = meta:get_string("es_show_dig_time"),
    spawn_safe_light_threshold = meta:get_int("es_spawn_safe_light_threshold"),
    show_growth = meta:get_string("es_show_growth"),
    show_icons = meta:get_string("es_show_icons"),
    layout = {},
  }

  for key, default_value in pairs(LAYOUT_META_DEFAULTS) do
    if type(default_value) == "number" then
      values.layout[key] = meta:get_int(key)
    else
      values.layout[key] = meta:get_string(key)
    end
  end

  return values
end

local function restore_old_values(meta, values)
  if not values then
    return
  end

  meta:set_string("es_hud_color", values.color)
  meta:set_string("es_hud_alignment", values.hud_alignment)
  meta:set_string("es_hud_health_in", values.health_in)
  meta:set_string("es_auto_text_color", values.auto_text_color)
  meta:set_int("es_title_color_val", values.title_color_val)
  meta:set_int("es_subtitle_color_val", values.subtitle_color_val)
  meta:set_int("es_footer_color_val", values.footer_color_val)
  meta:set_int("es_line_color_val", values.line_color_val)
  meta:set_int("es_growth_color_val", values.growth_color_val)
  meta:set_int("es_soil_color_val", values.soil_color_val)
  meta:set_string("es_footer_mode", values.footer_mode or "compact")
  meta:set_string("es_show_coords", values.show_coords or "false")
  meta:set_string("es_show_light_level", values.show_light_level or "true")
  meta:set_string("es_show_spawn_hint", values.show_spawn_hint or "false")
  meta:set_string("es_show_liquid_info", values.show_liquid_info or "true")
  meta:set_string("es_show_dig_time", values.show_dig_time or "true")
  meta:set_int("es_spawn_safe_light_threshold", values.spawn_safe_light_threshold or 8)
  meta:set_string("es_show_growth", values.show_growth or "true")
  meta:set_string("es_show_icons", values.show_icons or "true")

  for key, default_value in pairs(LAYOUT_META_DEFAULTS) do
    local saved_value = values.layout and values.layout[key]

    if type(default_value) == "number" then
      meta:set_int(key, tonumber(saved_value) or default_value)
    else
      meta:set_string(key, tostring(saved_value or default_value))
    end
  end
end

local function reset_layout_target(meta, target)
  local keys = LAYOUT_RESET_GROUPS[target]

  if not keys then
    return
  end

  for _, key in ipairs(keys) do
    local default_value = LAYOUT_META_DEFAULTS[key]

    if type(default_value) == "number" then
      meta:set_int(key, default_value)
    else
      meta:set_string(key, default_value)
    end
  end
end

local function reset_layout_all(meta)
  for key, default_value in pairs(LAYOUT_META_DEFAULTS) do
    if type(default_value) == "number" then
      meta:set_int(key, default_value)
    else
      meta:set_string(key, default_value)
    end
  end
end

local function update_preview(player, preview_type, force)
  local player_name = player and player:get_player_name()

  if not player_name then
    return
  end

  if force then
    eye_spy.pending_preview_updates[player_name] = nil
    eye_spy.get_hud(player, {
      preview = true,
      preview_type = preview_type,
      force = true,
    })
    return
  end

  local due_at_us = 0
  local now_us = minetest.get_us_time and minetest.get_us_time() or 0
  due_at_us = now_us + math.floor(PREVIEW_DEBOUNCE_SECONDS * 1000000)

  eye_spy.pending_preview_updates[player_name] = {
    preview_type = preview_type,
    force = false,
    due_at_us = due_at_us,
  }
end

local function parse_scroll_input(raw, fallback)
  local parsed
  local event_type

  if type(raw) == "string" and minetest.explode_scrollbar_event then
    local event = minetest.explode_scrollbar_event(raw)

    if event and event.value then
      parsed = tonumber(event.value)
      event_type = event.type
    end
  end

  if parsed == nil then
    parsed = tonumber((tostring(raw) or ""):match("-?%d+"))
  end

  return parsed or fallback, event_type
end

local function layout_slider(label, field_name, value, minv, maxv, x, y, width)
  local slider_width = width or 4.2

  return "label["..x..","..y..";"..label..": "..value.."]"..
    "scrollbaroptions[min="..minv..";max="..maxv..";smallstep=1]"..
    "scrollbar["..x..","..(y + 0.35)..";"..slider_width..",0.5;horizontal;"..field_name..";"..value.."]"
end

local function resolve_layout_slider_key(entry, target_keys)
  if entry.key == "$target_x" then
    return target_keys.x
  end

  if entry.key == "$target_y" then
    return target_keys.y
  end

  return entry.key
end

local function get_layout_page_entries(selected_page, selected_target)
  local page = LAYOUT_PAGE_SLIDERS[selected_page] or LAYOUT_PAGE_SLIDERS.Position
  local entries = {}
  local common = page["*"] or {}
  local specific = page[selected_target] or {}

  for _, entry in ipairs(common) do
    entries[#entries + 1] = entry
  end

  for _, entry in ipairs(specific) do
    entries[#entries + 1] = entry
  end

  return entries
end

local function build_layout_page_content(meta, player_name, selected_page, selected_target, target_keys)
  local page_entries = get_layout_page_entries(selected_page, selected_target)
  local parts = {
    "box[0.4,3.0;14.7,6.8;#00000020]",
  }

  if selected_page == "Position" then
    parts[#parts + 1] = "label[0.7,3.25;"..S("Selected Target Position").."]"
  elseif #page_entries == 0 then
    parts[#parts + 1] = "label[0.7,3.45;"..S("No controls for this target on this page").."]"
  end

  for _, entry in ipairs(page_entries) do
    local key = resolve_layout_slider_key(entry, target_keys)
    local value = get_effective_int(meta, player_name, key)

    parts[#parts + 1] = layout_slider(
      S(entry.label),
      key,
      value,
      entry.min,
      entry.max,
      entry.x,
      entry.y,
      entry.width
    )
  end

  return table.concat(parts)
end

function eye_spy.get_layout_ui(player)
  local player_name = player:get_player_name()
  local meta = player:get_meta()
  local selected_page = get_effective_string(meta, player_name, "es_layout_page")
  local selected_target = get_effective_string(meta, player_name, "es_layout_target")

  if selected_page == "" then
    selected_page = "Position"
    meta:set_string("es_layout_page", selected_page)
  end

  if selected_target == "" then
    selected_target = "Global"
    meta:set_string("es_layout_target", selected_target)
  end

  local step = get_effective_int(meta, player_name, "es_layout_step")

  if step <= 0 then
    step = 2
    meta:set_int("es_layout_step", step)
  end

  local target_keys = LAYOUT_TARGET_KEYS[selected_target] or LAYOUT_TARGET_KEYS.Global
  local page_content = build_layout_page_content(meta, player_name, selected_page, selected_target, target_keys)

  local formspec = (
    "formspec_version[6]"..
    "size[15.5,11.2]"..
    "no_prepend[]"..
    "bgcolor[#0F1319C0;false]"..
    "label[0.4,0.4;"..S("Advanced Layout Editor").."]"..
    "label[0.4,0.85;"..S("Use pages to edit layout without crowding the form").."]"..
    "dropdown[0.4,1.25;2.5,0.8;layout_page;Position,Panel,Text;"..get_option_index(LAYOUT_PAGE_OPTIONS, selected_page)..";false]"..
    "dropdown[3.1,1.25;2.6,0.8;layout_target;Global,BG,Icon,Title,Subtitle,Lines,Footer;"..get_option_index(LAYOUT_TARGET_OPTIONS, selected_target)..";false]"..
    "dropdown[5.9,1.25;1.4,0.8;layout_step;1,2,4,8;"..get_option_index(LAYOUT_STEP_OPTIONS, step)..";false]"..
    "label[7.7,1.33;"..S("Step")..": "..step.."]"..
    "button[10.0,1.05;2.2,0.8;reset_layout_target;"..S("Reset Target").."]"..
    "button[12.4,1.05;2.0,0.8;reset_layout_all;"..S("Reset All").."]"..
    page_content..
    "button[11.6,10.05;1.7,0.8;back_main;"..S("Back").."]"..
    "label[0.5,10.2;"..S("Tips: use step 1 for fine tuning and drag sliders for live preview").."]"
  )

  return formspec
end

minetest.register_chatcommand("eye_spy_settings", {
  description = "Open Eye Spy settings and live preview editor.",
  func = function(name, param)
    local player = minetest.get_player_by_name(name)

    eye_spy.enter_edit_mode(player)
end})

minetest.register_on_joinplayer(function(player)
  index[player:get_player_name()] = 0
end)

minetest.register_on_dieplayer(function(player)
  local player_name = player:get_player_name()
  local meta = player:get_meta()

  if old_values[player_name] then
    restore_old_values(meta, old_values[player_name])
  end

  eye_spy.exit_edit_mode(player)
end)

function eye_spy.get_ui(player, data)
  local player_name = player:get_player_name()

  index[player_name] = index[player_name] + 1

  local player_index = index[player_name]

  local meta = player:get_meta()
  local string_to_num = {
    health_in = {
      ["Points"] = 1, ["Hearts"] = 2
    }
  }

  if not data or not data.reopen then
    change_preview_type[player_name] = "Entity"
    old_values[player_name] = save_old_values(meta)
  end

  local r, g, b = eye_spy.get_rgb(get_effective_string(meta, player_name, "es_hud_color"))
  local hud_alignment = get_effective_string(meta, player_name, "es_hud_alignment")
  local health_in = get_effective_string(meta, player_name, "es_hud_health_in")
  local auto_text_color = get_effective_string(meta, player_name, "es_auto_text_color") ~= "false"
  local show_light_level = get_effective_string(meta, player_name, "es_show_light_level") ~= "false"
  local show_spawn_hint = get_effective_string(meta, player_name, "es_show_spawn_hint") ~= "false"
  local show_liquid_info = get_effective_string(meta, player_name, "es_show_liquid_info") ~= "false"
  local show_dig_time = get_effective_string(meta, player_name, "es_show_dig_time") ~= "false"
  local show_growth = get_effective_string(meta, player_name, "es_show_growth") ~= "false"
  local show_icons = get_effective_string(meta, player_name, "es_show_icons") ~= "false"
  local show_coords = get_effective_string(meta, player_name, "es_show_coords") ~= "false"
  local footer_mode = get_effective_string(meta, player_name, "es_footer_mode") == "advanced" and "Advanced" or "Compact"
  local spawn_threshold = math.max(0, math.min(15, get_effective_int(meta, player_name, "es_spawn_safe_light_threshold")))
  local footer_mode_idx = get_option_index(FOOTER_MODE_OPTIONS, footer_mode)
  local spawn_threshold_idx = get_option_index(SPAWN_THRESHOLD_OPTIONS, tostring(spawn_threshold))
  local title_color_val = get_effective_int(meta, player_name, "es_title_color_val")
  local subtitle_color_val = get_effective_int(meta, player_name, "es_subtitle_color_val")
  local footer_color_val = get_effective_int(meta, player_name, "es_footer_color_val")
  local line_color_val = get_effective_int(meta, player_name, "es_line_color_val")
  local growth_color_val = get_effective_int(meta, player_name, "es_growth_color_val")
  local soil_color_val = get_effective_int(meta, player_name, "es_soil_color_val")

  local formspec = table.concat({
    "formspec_version[6]",
    "size[13.4,12.5]",
    "no_prepend[]",
    "bgcolor[#0F1319C0;false]",
    "box[0.2,0.2;13.0,0.5;#1C2433C0]",
    "label[0.45,0.28;Eye Spy]",
    "label[11.4,0.28;"..S("Preview")..": "..S(change_preview_type[player_name] == "Node" and "Entity" or "Node").."]",

    "box[0.3,0.9;6.35,3.9;#00000020]",
    "label[0.55,1.15;"..S("Background Color").."]",
    "scrollbaroptions[min=0;max=255;smallstep=1]",
    "label[0.55,1.55;R]",
    "box[0.88,1.57;5.3,0.34;#FF0000CC]",
    "scrollbar[0.88,1.55;5.3,0.42;horizontal;r_"..player_index..";"..(r or 26).."]",
    "label[0.55,2.25;G]",
    "box[0.88,2.27;5.3,0.34;#00FF00CC]",
    "scrollbar[0.88,2.25;5.3,0.42;horizontal;g_"..player_index..";"..(g or 26).."]",
    "label[0.55,2.95;B]",
    "box[0.88,2.97;5.3,0.34;#0000FFCC]",
    "scrollbar[0.88,2.95;5.3,0.42;horizontal;b_"..player_index..";"..(b or 27).."]",
    "button[0.55,3.85;2.5,0.7;default_color;"..S("Set to Default").."]",

    "box[6.75,0.9;6.35,3.9;#00000020]",
    "label[7.0,1.15;"..S("HUD Setup").."]",
    "label[7.0,1.55;"..S("Hud Alignment").."]",
    "dropdown[7.0,1.8;3.2,0.8;hud_alignment;"..table.concat(HUD_ALIGNMENT_OPTIONS, ",")..";"..get_option_index(HUD_ALIGNMENT_OPTIONS, hud_alignment)..";false]",
    "button[10.35,1.75;2.55,0.7;default_hud_alignment;"..S("Default").."]",
    "label[7.0,2.85;"..S("Health in").."]",
    "dropdown[7.0,3.1;3.2,0.8;health_in;Points,Hearts;"..(string_to_num.health_in[health_in] or 1)..";false]",
    "button[10.35,3.05;2.55,0.7;default_health_in;"..S("Default").."]",
    "button[7.0,3.85;2.8,0.7;change_change_preview_type;"..S("Preview Type").."]",
    "button[10.0,3.85;2.9,0.7;advanced_layout;"..S("Advanced Layout").."]",

    "box[0.3,5.05;12.8,3.5;#00000020]",
    "label[0.55,5.3;"..S("Text Styling").."]",
    "checkbox[0.55,5.65;auto_text_color;"..S("Auto Text Colors")..";"..(auto_text_color and "true" or "false").."]",
    "scrollbaroptions[min=0;max=255;smallstep=1]",
    "label[0.55,6.05;"..S("Title").."]",
    "scrollbar[0.55,6.3;3.9,0.45;horizontal;title_color_"..player_index..";"..title_color_val.."]",
    "label[4.8,6.05;"..S("Info").."]",
    "scrollbar[4.8,6.3;3.9,0.45;horizontal;subtitle_color_"..player_index..";"..subtitle_color_val.."]",
    "label[9.05,6.05;"..S("Footer").."]",
    "scrollbar[9.05,6.3;3.6,0.45;horizontal;footer_color_"..player_index..";"..footer_color_val.."]",
    "label[0.55,6.95;"..S("Line").."]",
    "scrollbar[0.55,7.2;3.9,0.45;horizontal;line_color_"..player_index..";"..line_color_val.."]",
    "label[4.8,6.95;"..S("Growth").."]",
    "scrollbar[4.8,7.2;3.9,0.45;horizontal;growth_color_"..player_index..";"..growth_color_val.."]",
    "label[9.05,6.95;"..S("Soil").."]",
    "scrollbar[9.05,7.2;3.6,0.45;horizontal;soil_color_"..player_index..";"..soil_color_val.."]",

    "box[0.3,8.8;12.8,2.15;#00000020]",
    "label[0.55,9.05;"..S("Info Lines").."]",
    "checkbox[0.55,9.4;show_light_level;"..S("Light Level")..";"..(show_light_level and "true" or "false").."]",
    "checkbox[0.55,9.78;show_spawn_hint;"..S("Spawn Hint")..";"..(show_spawn_hint and "true" or "false").."]",
    "label[3.25,9.43;"..S("Spawn Thresh").."]",
    "dropdown[3.25,9.66;1.6,0.8;spawn_threshold;0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15;"..spawn_threshold_idx..";false]",
    "checkbox[5.45,9.4;show_liquid_info;"..S("Liquid Info")..";"..(show_liquid_info and "true" or "false").."]",
    "checkbox[5.45,9.78;show_dig_time;"..S("Dig Time")..";"..(show_dig_time and "true" or "false").."]",
    "checkbox[8.65,9.4;show_growth;"..S("Growth")..";"..(show_growth and "true" or "false").."]",
    "checkbox[11.6,9.4;show_icons;"..S("Icons")..";"..(show_icons and "true" or "false").."]",
    "checkbox[11.6,9.78;show_coords;"..S("Coords")..";"..(show_coords and "true" or "false").."]",
    "label[10.7,10.06;"..S("Footer Mode").."]",
    "dropdown[10.7,10.29;2.0,0.8;footer_mode;Compact,Advanced;"..footer_mode_idx..";false]",

    "style[cancel;bgcolor=#8D2930C0]",
    "style[apply;bgcolor=#2D7F46C0]",
    "button_exit[7.9,11.5;2.4,0.8;cancel;"..S("Cancel").."]",
    "button[10.55,11.5;2.55,0.8;apply;"..S("Apply Changes").."]",
  }, "")

  update_preview(player, change_preview_type[player_name], true)

  return formspec
end

minetest.register_on_player_receive_fields(function(player, formname, fields)
  if formname ~= "eye_spy:ui" then
    return false
  end

  local player_name = player:get_player_name()
  local meta = player:get_meta()

  local player_index = index[player_name]
  local player_change_preview_type = change_preview_type[player_name]
  local main_slider_event = false

  local function is_live_scrollbar(raw)
    if type(raw) ~= "string" or not minetest.explode_scrollbar_event then
      return false
    end
    local ev = minetest.explode_scrollbar_event(raw)
    return ev and (ev.type == "CHG" or ev.type == "VAL")
  end

  if fields["r_"..player_index] or fields["g_"..player_index] or fields["b_"..player_index] then
    local current_r, current_g, current_b = eye_spy.get_rgb(get_effective_string(meta, player_name, "es_hud_color"))
    local r = current_r
    local g = current_g
    local b = current_b
    local color_changed = false

    if fields["r_"..player_index] and is_live_scrollbar(fields["r_"..player_index]) then
      local value = parse_scroll_input(fields["r_"..player_index], current_r)
      if value ~= current_r then r = value; color_changed = true end
    end

    if fields["g_"..player_index] and is_live_scrollbar(fields["g_"..player_index]) then
      local value = parse_scroll_input(fields["g_"..player_index], current_g)
      if value ~= current_g then g = value; color_changed = true end
    end

    if fields["b_"..player_index] and is_live_scrollbar(fields["b_"..player_index]) then
      local value = parse_scroll_input(fields["b_"..player_index], current_b)
      if value ~= current_b then b = value; color_changed = true end
    end

    if color_changed then
      set_draft_value(player_name, "es_hud_color", r..", "..g..", "..b)
      main_slider_event = true
    end
  end

  local function apply_main_int_slider(field_name, meta_key)
    if not fields[field_name] or not is_live_scrollbar(fields[field_name]) then
      return
    end

    local current = get_effective_int(meta, player_name, meta_key)
    local value = parse_scroll_input(fields[field_name], current)

    if value ~= current then
      set_draft_value(player_name, meta_key, value)
      main_slider_event = true
    end
  end

  apply_main_int_slider("title_color_"..player_index, "es_title_color_val")
  apply_main_int_slider("subtitle_color_"..player_index, "es_subtitle_color_val")
  apply_main_int_slider("footer_color_"..player_index, "es_footer_color_val")
  apply_main_int_slider("line_color_"..player_index, "es_line_color_val")
  apply_main_int_slider("growth_color_"..player_index, "es_growth_color_val")
  apply_main_int_slider("soil_color_"..player_index, "es_soil_color_val")

  if main_slider_event then
    local now_us = minetest.get_us_time and minetest.get_us_time() or 0
    eye_spy.pending_preview_updates[player_name] = {
      preview_type = player_change_preview_type,
      force = true,
      due_at_us = now_us,
    }
    return true
  end

  if fields.auto_text_color then
    set_draft_value(player_name, "es_auto_text_color", fields.auto_text_color)
    update_preview(player, player_change_preview_type)
    minetest.show_formspec(player_name, "eye_spy:ui", eye_spy.get_ui(player, {reopen = true}))
  end

  local info_settings_changed = false

  if fields.show_light_level then
    set_draft_value(player_name, "es_show_light_level", fields.show_light_level)
    info_settings_changed = true
  end

  if fields.show_spawn_hint then
    set_draft_value(player_name, "es_show_spawn_hint", fields.show_spawn_hint)
    info_settings_changed = true
  end

  if fields.show_liquid_info then
    set_draft_value(player_name, "es_show_liquid_info", fields.show_liquid_info)
    info_settings_changed = true
  end

  if fields.show_dig_time then
    set_draft_value(player_name, "es_show_dig_time", fields.show_dig_time)
    info_settings_changed = true
  end

  if fields.show_growth then
    set_draft_value(player_name, "es_show_growth", fields.show_growth)
    info_settings_changed = true
  end

  if fields.show_icons then
    set_draft_value(player_name, "es_show_icons", fields.show_icons)
    info_settings_changed = true
  end

  if fields.show_coords then
    set_draft_value(player_name, "es_show_coords", fields.show_coords)
    info_settings_changed = true
  end

  if fields.footer_mode then
    if fields.footer_mode == "Advanced" then
      set_draft_value(player_name, "es_footer_mode", "advanced")
    else
      set_draft_value(player_name, "es_footer_mode", "compact")
    end

    info_settings_changed = true
  end

  if fields.spawn_threshold then
    local threshold = tonumber(fields.spawn_threshold)

    if threshold then
      set_draft_value(player_name, "es_spawn_safe_light_threshold", math.max(0, math.min(15, threshold)))
      info_settings_changed = true
    end
  end

  if info_settings_changed then
    update_preview(player, player_change_preview_type)
    minetest.show_formspec(player_name, "eye_spy:ui", eye_spy.get_ui(player, {reopen = true}))
  end

  if fields.default_color then
    set_draft_value(player_name, "es_hud_color", "26, 26, 27")
    minetest.show_formspec(player_name, "eye_spy:ui", eye_spy.get_ui(player, {reopen = true}))
    update_preview(player, player_change_preview_type)
  end

  if fields.hud_alignment then
    set_draft_value(player_name, "es_hud_alignment", fields.hud_alignment)
    update_preview(player, player_change_preview_type)
  end

  if fields.default_hud_alignment then
    set_draft_value(player_name, "es_hud_alignment", "Top-Middle")
    minetest.show_formspec(player_name, "eye_spy:ui", eye_spy.get_ui(player, {reopen = true}))
    update_preview(player, player_change_preview_type)
  end

  if fields.health_in then
    set_draft_value(player_name, "es_hud_health_in", fields.health_in)
    update_preview(player, player_change_preview_type)
  end

  if fields.default_health_in then
    set_draft_value(player_name, "es_hud_health_in", "Points")
    minetest.show_formspec(player_name, "eye_spy:ui", eye_spy.get_ui(player, {reopen = true}))
    update_preview(player, player_change_preview_type)
  end

  if fields.change_change_preview_type then
    if player_change_preview_type == "Node" then
      change_preview_type[player_name] = "Entity"
      minetest.show_formspec(player_name, "eye_spy:ui", eye_spy.get_ui(player, {reopen = true}))
    else
      change_preview_type[player_name] = "Node"
      minetest.show_formspec(player_name, "eye_spy:ui", eye_spy.get_ui(player, {reopen = true}))
    end
  end

  if fields.advanced_layout then
    eye_spy.pending_layout_refreshes[player_name] = nil
    layout_last_chg_us[player_name] = nil
    minetest.close_formspec(player_name, "eye_spy:ui")
    minetest.show_formspec(player_name, "eye_spy:layout_ui", eye_spy.get_layout_ui(player))
    return true
  end

  if fields.apply then
    commit_player_draft(player)
    minetest.close_formspec(player_name, formname)

    eye_spy.exit_edit_mode(player)
    eye_spy.update_player(player, {preview = false, force = true})

    return true
  end

  if fields.quit then
    restore_old_values(meta, old_values[player_name])
    clear_player_draft(player_name)
    eye_spy.pending_preview_updates[player_name] = nil

    eye_spy.exit_edit_mode(player)
    eye_spy.update_player(player, {preview = false, force = true})
  end

  return true
end)

minetest.register_on_player_receive_fields(function(player, formname, fields)
  if formname ~= "eye_spy:layout_ui" then
    return false
  end

  local player_name = player:get_player_name()
  local meta = player:get_meta()
  local player_change_preview_type = change_preview_type[player_name] or "Entity"
  local selected_page = get_effective_string(meta, player_name, "es_layout_page")

  local selected_target = get_effective_string(meta, player_name, "es_layout_target")

  if selected_page == "" then
    selected_page = "Position"
    set_draft_value(player_name, "es_layout_page", selected_page)
  end

  if selected_target == "" then
    selected_target = "Global"
    set_draft_value(player_name, "es_layout_target", selected_target)
  end

  if fields.back_main then
    eye_spy.pending_layout_refreshes[player_name] = nil
    layout_last_chg_us[player_name] = nil
    minetest.close_formspec(player_name, "eye_spy:layout_ui")
    minetest.show_formspec(player_name, "eye_spy:ui", eye_spy.get_ui(player, {reopen = true}))
    return true
  end

  if fields.quit then
    eye_spy.pending_layout_refreshes[player_name] = nil
    layout_last_chg_us[player_name] = nil
    minetest.close_formspec(player_name, "eye_spy:layout_ui")
    minetest.show_formspec(player_name, "eye_spy:ui", eye_spy.get_ui(player, {reopen = true}))
    return true
  end

  local rebuild = false

  if fields.layout_page and fields.layout_page ~= selected_page then
    set_draft_value(player_name, "es_layout_page", fields.layout_page)
    selected_page = fields.layout_page
    rebuild = true
  end

  if fields.layout_target and LAYOUT_TARGET_KEYS[fields.layout_target] then
    if fields.layout_target ~= selected_target then
      set_draft_value(player_name, "es_layout_target", fields.layout_target)
      selected_target = fields.layout_target
      rebuild = true
    end
  end

  if fields.layout_step then
    local new_step = tonumber(fields.layout_step)

    if new_step then
      local clamped_step = math.max(1, new_step)

      if clamped_step ~= get_effective_int(meta, player_name, "es_layout_step") then
        set_draft_value(player_name, "es_layout_step", clamped_step)
        rebuild = true
      end
    end
  end

  local step = math.max(1, get_effective_int(meta, player_name, "es_layout_step"))

  local slider_event = false
  local slider_had_chg = false
  local slider_had_val = false
  local slider_chg_count = 0
  local slider_val_count = 0
  local slider_max_delta = 0
  local slider_had_small_step_val = false

  local function apply_slider(name, minv, maxv)
    if fields[name] == nil then
      return
    end

    local current = get_effective_int(meta, player_name, name)
    local value, event_type = parse_scroll_input(fields[name], current)

    -- Ignore values from full form submissions (button clicks, quit).
    -- Only act on live scrollbar CHG/VAL events.
    if event_type ~= "CHG" and event_type ~= "VAL" then
      return
    end

    if event_type == "CHG" then
      slider_had_chg = true
      slider_chg_count = slider_chg_count + 1
    elseif event_type == "VAL" then
      slider_had_val = true
      slider_val_count = slider_val_count + 1
    end

    if minv then
      value = math.max(minv, value)
    end

    if maxv then
      value = math.min(maxv, value)
    end

    local delta = math.abs(value - current)
    slider_max_delta = math.max(slider_max_delta, delta)

    if event_type == "VAL" and delta <= 2 then
      slider_had_small_step_val = true
    end

    if value ~= current then
      set_draft_value(player_name, name, value)
    end

    slider_event = true
  end

  for key, bounds in pairs(LAYOUT_SLIDER_BOUNDS) do
    apply_slider(key, bounds.min, bounds.max)
  end

  if slider_event and not fields.reset_layout_target and not fields.reset_layout_all then
    -- Queue a HUD update rather than calling get_hud directly. Multiple CHG
    -- events between step ticks overwrite the same entry, so the HUD fires at
    -- most once per step tick instead of on every CHG.
    local now_us = minetest.get_us_time and minetest.get_us_time() or 0
    eye_spy.pending_preview_updates[player_name] = {
      preview_type = player_change_preview_type,
      force = true,
      due_at_us = now_us,
    }

    if slider_had_chg then
      layout_last_chg_us[player_name] = now_us
    end

    local rebuild_delay_us = LAYOUT_REBUILD_DRAG_IDLE_US
    local last_chg_us = layout_last_chg_us[player_name] or 0
    local recent_chg = last_chg_us > 0 and (now_us - last_chg_us) <= LAYOUT_VAL_AS_NUDGE_GAP_US

    -- Treat tiny VAL deltas as click-like nudges unless they are part of a
    -- recent drag burst.
    if slider_had_small_step_val and slider_chg_count <= 1 and not recent_chg then
      rebuild_delay_us = LAYOUT_REBUILD_NUDGE_IDLE_US
    elseif slider_had_val and slider_val_count > 0 and slider_chg_count == 0 and not recent_chg then
      rebuild_delay_us = LAYOUT_REBUILD_NUDGE_IDLE_US
    end

    eye_spy.pending_layout_refreshes[player_name] = {
      due_at_us = now_us + rebuild_delay_us,
    }
    return true
  end

  if fields.reset_layout_target then
    local keys = LAYOUT_RESET_GROUPS[selected_target]

    if keys then
      for _, key in ipairs(keys) do
        set_draft_value(player_name, key, LAYOUT_META_DEFAULTS[key])
      end
    end

    rebuild = true
  end

  if fields.reset_layout_all then
    for key, default_value in pairs(LAYOUT_META_DEFAULTS) do
      set_draft_value(player_name, key, default_value)
    end

    selected_target = "Global"
    set_draft_value(player_name, "es_layout_target", selected_target)
    rebuild = true
  end

  if rebuild then
    eye_spy.pending_preview_updates[player_name] = nil
    eye_spy.get_hud(player, {
      preview = true,
      preview_type = player_change_preview_type,
      force = true,
    })
    minetest.show_formspec(player_name, "eye_spy:layout_ui", eye_spy.get_layout_ui(player))
  end

  return true
end)
