-- ---------------------------------------------------------------------------
--  rp_ui - the pre-spawn experience.
--
--  Owns the stage machine (auth -> characters -> creator -> spawn -> live),
--  the NUI bridge, and the transition into the world. The server decides
--  everything that matters; this file only asks and then animates the answer.
-- ---------------------------------------------------------------------------

local stage      = 'boot'
local account    = nil
local characters = {}
local draft      = nil     -- appearance currently being edited
local previewPed = nil
local spawnData  = nil     -- payload from char:select
local chosenChar = nil

local NEUTRAL = vector3(-1037.0, -2738.0, 20.17)   -- flat, always-streamed tarmac

-- ---------------------------------------------------------------------------
--  helpers
-- ---------------------------------------------------------------------------

local function ui(payload)
  SendNUIMessage(payload)
end

local function focusUI(on)
  SetNuiFocus(on, on)
  SetNuiFocusKeepInput(false)
end

local function holdPlayer()
  local ped = PlayerPedId()
  SetEntityVisible(ped, false, false)
  SetEntityCollision(ped, false, false)
  FreezeEntityPosition(ped, true)
  SetEntityInvincible(ped, true)
  SetPlayerControl(PlayerId(), false, 0)
  SetPlayerInvincible(PlayerId(), true)
  DisplayHud(false)
  DisplayRadar(false)
  ClearPedTasksImmediately(ped)
end

local function releasePlayer()
  local ped = PlayerPedId()
  SetEntityVisible(ped, true, false)
  SetEntityCollision(ped, true, true)
  FreezeEntityPosition(ped, false)
  SetEntityInvincible(ped, false)
  SetPlayerControl(PlayerId(), true, 0)
  SetPlayerInvincible(PlayerId(), false)
  DisplayHud(true)
  DisplayRadar(true)
end

--- Dusk, so the sodium palette of the interface matches the world behind it.
local function setSceneMood(on)
  if on then
    NetworkOverrideClockTime(20, 40, 0)
    SetWeatherTypeNowPersist('EXTRASUNNY')
  else
    NetworkClearClockTimeOverride()
    ClearOverrideWeatherType()
    SetWeatherTypeNow('EXTRASUNNY')
  end
end

--- Keep the world streamed around a point while no player ped is there.
local function focusOn(pos)
  SetFocusPosAndVel(pos.x, pos.y, pos.z, 0.0, 0.0, 0.0)
  RequestCollisionAtCoord(pos.x, pos.y, pos.z)
end

local function destroyPreview()
  if previewPed and DoesEntityExist(previewPed) then
    DeleteEntity(previewPed)
  end
  previewPed = nil
end

local function setStage(next, data)
  stage = next
  ui({ action = 'stage', stage = next, data = data })
end

-- ---------------------------------------------------------------------------
--  boot
-- ---------------------------------------------------------------------------

CreateThread(function()
  while not NetworkIsSessionStarted() do Wait(50) end

  holdPlayer()
  setSceneMood(true)

  local first = Config.CinematicShots[1]
  focusOn(first.from)
  SetEntityCoords(PlayerPedId(), NEUTRAL.x, NEUTRAL.y, NEUTRAL.z, false, false, false, false)

  Cam.Shots(Config.CinematicShots)

  -- give the world a moment to stream in behind the loading screen
  SendLoadingScreenMessage(json.encode({
    eventName = 'rp:server',
    name = Config.ServerName,
    status = 'Connected',
  }))
  SendLoadingScreenMessage(json.encode({ eventName = 'rp:rules', rules = Config.Rules }))

  local waited = 0
  while waited < 6000 do
    SendLoadingScreenMessage(json.encode({
      eventName = 'rp:progress',
      fraction = 0.85 + (waited / 6000) * 0.15,
      step = 'Streaming the city',
    }))
    Wait(250)
    waited = waited + 250
  end

  SendLoadingScreenMessage(json.encode({ eventName = 'rp:handover' }))
  Wait(1500)
  ShutdownLoadingScreenNui()

  focusUI(true)
  setStage('auth', {
    serverName = Config.ServerName,
    serverTag  = Config.ServerTag,
    maxChars   = Config.MaxChars,
  })
end)

-- ---------------------------------------------------------------------------
--  authentication
-- ---------------------------------------------------------------------------

RegisterNUICallback('auth:submit', function(data, cb)
  CreateThread(function()
    local mode = (data and data.mode == 'register') and 'auth:register' or 'auth:login'
    local res, err = Core.Callback('rp_session', mode, {
      username = data.username,
      password = data.password,
    })

    if not res then return cb({ ok = false, error = err or 'Something went wrong' }) end

    account = { username = res.username, role = res.role }
    characters = res.characters or {}
    cb({ ok = true, account = account, characters = characters, maxChars = res.maxChars })
  end)
end)

-- ---------------------------------------------------------------------------
--  character select
-- ---------------------------------------------------------------------------

--- Stand the highlighted character up so the list is not just text.
RegisterNUICallback('char:preview', function(data, cb)
  CreateThread(function()
    destroyPreview()

    local appearance
    for _, c in ipairs(characters) do
      if c.id == data.id then appearance = c.appearance end
    end
    if not appearance then return cb({ ok = false }) end

    appearance = Appearance.Sanitise(appearance)
    focusOn(Config.CreatorScene.ped)
    previewPed = PedBuild.CreatePreview(appearance, Config.CreatorScene.ped, Config.CreatorScene.ped.w)
    if previewPed then
      PedBuild.Idle(previewPed)
      Cam.Orbit(previewPed, 2.4, 0.25, Config.CreatorScene.ped.w + 180.0, true)
      Cam.OrbitFrame(2.4, 0.25, 0.35)
    end
    cb({ ok = previewPed ~= nil })
  end)
end)

RegisterNUICallback('char:delete', function(data, cb)
  CreateThread(function()
    local res, err = Core.Callback('rp_session', 'char:delete', { id = data.id })
    if not res then return cb({ ok = false, error = err }) end

    local list = Core.Callback('rp_session', 'char:list')
    characters = (list and list.characters) or {}
    destroyPreview()
    Cam.Shots(Config.CinematicShots)
    cb({ ok = true, characters = characters })
  end)
end)

RegisterNUICallback('char:select', function(data, cb)
  CreateThread(function()
    local res, err = Core.Callback('rp_session', 'char:select', { id = data.id })
    if not res then return cb({ ok = false, error = err }) end

    spawnData  = res
    chosenChar = res.character
    destroyPreview()

    -- become the character now, off-screen, so the fly-down reveals them
    if res.appearance then
      PedBuild.ApplyToPlayer(Appearance.Sanitise(res.appearance))
      holdPlayer()
    end

    cb({ ok = true, spawns = res.spawns, lastPosition = res.lastPosition, character = res.character })
  end)
end)

-- ---------------------------------------------------------------------------
--  character creation
-- ---------------------------------------------------------------------------

--- Read the game's own colour tables so the swatches in the UI are the real
--- colours, not an approximation that drifts from what the ped ends up wearing.
local paletteCache = nil
local function palettes()
  if paletteCache then return paletteCache end
  local hair, makeup = {}, {}
  for i = 0, 63 do
    local ok, r, g, b = pcall(GetPedHairRgbColor, i)
    hair[#hair + 1] = ok and { r = r, g = g, b = b } or { r = 40, g = 40, b = 40 }
    local ok2, r2, g2, b2 = pcall(GetPedMakeupRgbColor, i)
    makeup[#makeup + 1] = ok2 and { r = r2, g = g2, b = b2 } or { r = 60, g = 40, b = 40 }
  end
  paletteCache = { hair = hair, makeup = makeup }
  return paletteCache
end

--- The creator UI is built from this schema rather than a hardcoded copy, so
--- adding a slider is a one-line change in shared/appearance.lua.
local function pushDraft(cb)
  local payload = {
    ok = true,
    appearance = draft,
    limits = PedBuild.Limits(previewPed),
    palettes = palettes(),
    schema = {
      features   = Appearance.FEATURES,
      overlays   = Appearance.OVERLAYS,
      components = Appearance.COMPONENTS,
      props      = Appearance.PROPS,
    },
  }
  if cb then cb(payload) else ui({ action = 'creator:sync', data = payload }) end
end

local function rebuildPreview()
  destroyPreview()
  focusOn(Config.CreatorScene.ped)
  previewPed = PedBuild.CreatePreview(draft, Config.CreatorScene.ped, Config.CreatorScene.ped.w)
  if previewPed then
    PedBuild.Idle(previewPed)
    Cam.Orbit(previewPed, 1.5, 0.62, Config.CreatorScene.ped.w + 180.0, true)
  end
end

RegisterNUICallback('creator:start', function(data, cb)
  CreateThread(function()
    draft = Appearance.Default(data and data.gender or 'm')
    rebuildPreview()
    pushDraft(cb)
  end)
end)

RegisterNUICallback('creator:gender', function(data, cb)
  CreateThread(function()
    draft = Appearance.Default(data.gender == 'f' and 'f' or 'm')
    rebuildPreview()
    pushDraft(cb)
  end)
end)

RegisterNUICallback('creator:parent', function(data, cb)
  if not draft or not previewPed then return cb({ ok = false }) end
  local key = data.key
  if key == 'father' or key == 'mother' then
    draft.parents[key] = math.floor(tonumber(data.value) or 0)
  elseif key == 'shapeMix' or key == 'skinMix' then
    draft.parents[key] = tonumber(data.value) or 0.5
  end
  SetPedHeadBlendData(previewPed,
    draft.parents.father, draft.parents.mother, 0,
    draft.parents.father, draft.parents.mother, 0,
    draft.parents.shapeMix + 0.0, draft.parents.skinMix + 0.0, 0.0, false)
  cb({ ok = true })
end)

RegisterNUICallback('creator:feature', function(data, cb)
  if not draft or not previewPed then return cb({ ok = false }) end
  local id = math.floor(tonumber(data.id) or 0)
  local v = math.max(-1.0, math.min(1.0, tonumber(data.value) or 0.0))
  draft.features[tostring(id)] = v
  SetPedFaceFeature(previewPed, id, v + 0.0)
  cb({ ok = true })
end)

RegisterNUICallback('creator:overlay', function(data, cb)
  if not draft or not previewPed then return cb({ ok = false }) end
  local slot
  for _, o in ipairs(Appearance.OVERLAYS) do if o.key == data.key then slot = o end end
  if not slot then return cb({ ok = false }) end

  local ov = draft.overlays[data.key]
  if data.index ~= nil then ov.index = math.floor(tonumber(data.index)) end
  if data.opacity ~= nil then ov.opacity = tonumber(data.opacity) end
  if data.colour ~= nil then ov.colour = math.floor(tonumber(data.colour)) end

  local index = (ov.index == nil or ov.index < 0) and 255 or ov.index
  SetPedHeadOverlay(previewPed, slot.id, index, ov.opacity + 0.0)
  if slot.tint and index ~= 255 then
    SetPedHeadOverlayColor(previewPed, slot.id, slot.tint == 'hair' and 1 or 2, ov.colour, ov.colour)
  end
  cb({ ok = true })
end)

RegisterNUICallback('creator:hair', function(data, cb)
  if not draft or not previewPed then return cb({ ok = false }) end
  if data.style ~= nil then draft.hair.style = math.floor(tonumber(data.style)) end
  if data.colour ~= nil then draft.hair.colour = math.floor(tonumber(data.colour)) end
  if data.highlight ~= nil then draft.hair.highlight = math.floor(tonumber(data.highlight)) end
  SetPedComponentVariation(previewPed, 2, draft.hair.style, 0, 0)
  SetPedHairColor(previewPed, draft.hair.colour, draft.hair.highlight)
  cb({ ok = true })
end)

RegisterNUICallback('creator:eyes', function(data, cb)
  if not draft or not previewPed then return cb({ ok = false }) end
  draft.eyeColour = math.floor(tonumber(data.value) or 0)
  SetPedEyeColor(previewPed, draft.eyeColour)
  cb({ ok = true })
end)

RegisterNUICallback('creator:component', function(data, cb)
  if not draft or not previewPed then return cb({ ok = false }) end
  local comp
  for _, c in ipairs(Appearance.COMPONENTS) do if c.key == data.key then comp = c end end
  if not comp then return cb({ ok = false }) end

  local cc = draft.components[data.key]
  if data.drawable ~= nil then cc.drawable = math.floor(tonumber(data.drawable)) end
  if data.texture ~= nil then cc.texture = math.floor(tonumber(data.texture)) end

  -- a new drawable usually has a different number of textures
  local maxTex = math.max(0, GetNumberOfPedTextureVariations(previewPed, comp.id, cc.drawable) - 1)
  if cc.texture > maxTex then cc.texture = 0 end

  SetPedComponentVariation(previewPed, comp.id, cc.drawable, cc.texture, 0)
  cb({ ok = true, texture = cc.texture, maxTexture = maxTex })
end)

RegisterNUICallback('creator:prop', function(data, cb)
  if not draft or not previewPed then return cb({ ok = false }) end
  local prop
  for _, p in ipairs(Appearance.PROPS) do if p.key == data.key then prop = p end end
  if not prop then return cb({ ok = false }) end

  local pp = draft.props[data.key]
  if data.drawable ~= nil then pp.drawable = math.floor(tonumber(data.drawable)) end
  if data.texture ~= nil then pp.texture = math.floor(tonumber(data.texture)) end

  if pp.drawable < 0 then
    ClearPedProp(previewPed, prop.id)
    cb({ ok = true, texture = 0, maxTexture = 0 })
  else
    local maxTex = math.max(0, GetNumberOfPedPropTextureVariations(previewPed, prop.id, pp.drawable) - 1)
    if pp.texture > maxTex then pp.texture = 0 end
    SetPedPropIndex(previewPed, prop.id, pp.drawable, pp.texture, true)
    cb({ ok = true, texture = pp.texture, maxTexture = maxTex })
  end
end)

--- Camera framing per creator section.
local FRAMES = {
  heritage = { radius = 1.05, height = 0.66, pitch = 0.02 },
  face     = { radius = 0.85, height = 0.68, pitch = 0.0 },
  hair     = { radius = 1.05, height = 0.70, pitch = 0.0 },
  body     = { radius = 2.60, height = 0.20, pitch = 0.45 },
  clothing = { radius = 2.30, height = 0.28, pitch = 0.40 },
  identity = { radius = 1.90, height = 0.45, pitch = 0.25 },
}

RegisterNUICallback('creator:frame', function(data, cb)
  local f = FRAMES[data.section] or FRAMES.face
  Cam.OrbitFrame(f.radius, f.height, f.pitch)
  cb({ ok = true })
end)

RegisterNUICallback('creator:rotate', function(data, cb)
  Cam.Nudge(tonumber(data.delta) or 0.0)
  cb({ ok = true })
end)

RegisterNUICallback('creator:submit', function(data, cb)
  CreateThread(function()
    local res, err = Core.Callback('rp_session', 'char:create', {
      firstName = data.firstName,
      lastName  = data.lastName,
      dob       = data.dob,
      gender    = draft and draft.model or 'm',
      backstory = data.backstory,
      appearance = draft,
    })
    if not res then return cb({ ok = false, error = err }) end

    local list = Core.Callback('rp_session', 'char:list')
    characters = (list and list.characters) or {}
    destroyPreview()
    draft = nil
    cb({ ok = true, characters = characters })
  end)
end)

RegisterNUICallback('creator:cancel', function(_, cb)
  destroyPreview()
  draft = nil
  Cam.Shots(Config.CinematicShots)
  cb({ ok = true })
end)

-- ---------------------------------------------------------------------------
--  spawn
-- ---------------------------------------------------------------------------

RegisterNUICallback('spawn:preview', function(data, cb)
  CreateThread(function()
    if not spawnData then return cb({ ok = false }) end
    for _, sp in ipairs(spawnData.spawns) do
      if sp.id == data.id then
        local from = Cam.Position()
        focusOn(vector3(sp.coords.x, sp.coords.y, sp.coords.z))
        Cam.FlyTo(
          from, vector3(sp.cam.look.x, sp.cam.look.y, sp.cam.look.z),
          vector3(sp.cam.pos.x, sp.cam.pos.y, sp.cam.pos.z),
          vector3(sp.coords.x, sp.coords.y, sp.coords.z),
          2200
        )
        break
      end
    end
    cb({ ok = true })
  end)
end)

RegisterNUICallback('spawn:confirm', function(data, cb)
  CreateThread(function()
    local res, err = Core.Callback('rp_session', 'spawn:confirm', { id = data.id })
    if not res then return cb({ ok = false, error = err }) end

    cb({ ok = true })
    focusUI(false)
    ui({ action = 'stage', stage = 'flying' })

    local c = res.coords
    local ped = PlayerPedId()

    -- stream the ground before anyone stands on it
    focusOn(vector3(c.x, c.y, c.z))
    RequestCollisionAtCoord(c.x, c.y, c.z)
    local waited = 0
    while not HasCollisionLoadedAroundEntity(ped) and waited < 8000 do
      RequestCollisionAtCoord(c.x, c.y, c.z)
      Wait(50)
      waited = waited + 50
    end

    SetEntityCoordsNoOffset(ped, c.x, c.y, c.z, false, false, false)
    SetEntityHeading(ped, c.h or 0.0)

    local from = Cam.Position()
    Cam.FlyTo(
      from, vector3(c.x, c.y, c.z),
      vector3(c.x - 2.4, c.y - 2.4, c.z + 1.4), vector3(c.x, c.y, c.z + 0.2),
      3400,
      function()
        SetEntityVisible(ped, true, false)
        releasePlayer()
        setSceneMood(false)
        ClearFocus()
        Cam.Release(1400)
        ui({ action = 'stage', stage = 'live' })
        TriggerEvent('rp:ui:spawned', chosenChar)
      end
    )
  end)
end)

-- ---------------------------------------------------------------------------
--  misc
-- ---------------------------------------------------------------------------

RegisterNUICallback('ui:log', function(data, cb)
  print(('^3[rp_ui]^7 %s'):format(tostring(data and data.message)))
  cb({ ok = true })
end)

AddEventHandler('onResourceStop', function(res)
  if res ~= GetCurrentResourceName() then return end
  destroyPreview()
  Cam.Release(0)
  focusUI(false)
  releasePlayer()
  setSceneMood(false)
  ClearFocus()
end)
