--[[
  Library with some features to adjust ghost behavior.

  To use, include with `local ai = require('shared/sim/ghost')`.
]]

local ghost = {}

---Replaces currently loaded ghost with a new one. Can be used by scripts with gameplay API access only.
---@param filename string @Path to a ghost recording.
---@return boolean @Returns `false` if operation has failed.
function ghost.reload(filename)
  return __util.native('ghost.reload', filename)
end

---Forces a certain lap time for loaded ghost.
---@param timeMs number? @Time in milliseconds (pass `nil` to disable forcing).
function ghost.forceTime(timeMs)
  __util.native('ghost.forceTimeMs', tonumber(timeMs) or -1)
end

---Creates a new ghost recording from positions. Callback will be called for individual time points, time is in seconds. Return car position or 
---`nil` if there is no more data. It’s expected that `carID` would match ID of main user car or at least one of cars present on a track: with that,
---the function would be able to measure relative wheel positions.
---@param driverName string
---@param track string
---@param layout string
---@param carID string
---@param callback fun(timeS: number): vec3?
---@return binary @Returns binary data that you can save as a file on your side if needed.
function ghost.create(driverName, track, layout, carID, callback)
  local car = ac.getCar(0) 
  for i = 0, ac.getSim().carsCount - 1 do
    if ac.getCarID(i) == carID then
      car = ac.getCar(0)
      break
    end
  end
  local w = require('shared/utils/binary').writeData():uint32(4):uint32(1):string(driverName):string(track):string(layout):string(carID)
  local sizeOffset, frame = w:size(), 0
  w:uint32(0):uint32(0)
  local lastPos = (callback(0) or error('At least a single point is required')):clone()
  local worldToPhysics = car.transform:inverse()
  local localWheelPosLF = (worldToPhysics:transformPoint(car.wheels[0].position) + worldToPhysics:transformPoint(car.wheels[1].position) * vec3(-1, 1, 1)) / 2
  local localWheelPosLR = (worldToPhysics:transformPoint(car.wheels[2].position) + worldToPhysics:transformPoint(car.wheels[3].position) * vec3(-1, 1, 1)) / 2
  local localWheelPositions = {localWheelPosLF, localWheelPosLF * vec3(-1, 1, 1), localWheelPosLR, localWheelPosLR * vec3(-1, 1, 1)}
  local hitPositions = {vec3(), vec3(), vec3(), vec3()}
  local totalDistance = 0
  while true do
    local timeS = 0.129 * frame + 0.001
    local pos = callback(timeS)
    if not pos then
      break
    end
    totalDistance = totalDistance + pos:distance(lastPos)
    local carTransform = mat4x4.translation(pos)
    carTransform.look:set(pos):sub(lastPos):normalize()
    carTransform.side:setCrossNormalized(carTransform.look, vec3(0, 1, 0))
    carTransform.up:setCrossNormalized(carTransform.side, carTransform.look)
    local avgOffset, avgCount = 0, 0
    for i = 1, 4 do
      local hit = physics.raycastTrack(carTransform:transformPoint(localWheelPositions[i]) + vec3(0, 2, 0), vec3(0, -1, 0), 4, hitPositions[i])
      if hit ~= -1 then
        avgOffset, avgCount = avgOffset + hit - 2 - car.wheels[i - 1].tyreRadius, avgCount + 1
      end
    end
    avgOffset = avgOffset / avgCount
    carTransform.up:setCrossNormalized(carTransform.look, hitPositions[2] - hitPositions[1])
    carTransform.side:setCrossNormalized(carTransform.look, carTransform.up)
    carTransform.position.y = carTransform.position.y - avgOffset
    lastPos:set(pos)
    w:mat4x4(carTransform)
    for i = 1, 4 do
      w:mat4x4(mat4x4.translation(localWheelPositions[i]) * carTransform)
    end
    for i = 1, 4 do
      w:mat4x4(mat4x4.translation(localWheelPositions[i]) * carTransform)
    end
    frame = frame + 1
    if frame > 1e5 then
      error('Callback should return `nil` eventually (went for %0.1f hours)' % (timeS / 3600))
    end
  end
  return w:seek(sizeOffset):uint32(129 * frame):uint32(frame):stringify()
end

return ghost