local LD = require("design.LevelDesignLibrary")
local LM = require("level.loadmonitor")
local thisObj, spawner, myVehicle, boatVisual, customZone, entryZone, exitZone, startEnabled, myWaterLevel, enabled
local saveTransformData = true
local dockingEnabled = true
local alwaysSpawnBoat, lastBoatPos, lastBoatDir, wad_BoatGlobal
local playerInZone = false
local nodeType, beachCheckpointOverrideSon, boatGlobalMonitor, onDockBoatEvents, onEnterBoatEvents, cineOverride_OnDockBoat_Check, cineOverride_OnDockBoat
local cineOverride = false
local mapMarkerID, mapMarkerDiscovered, undockDirection, lastDockPoint, dockForward, dockLeft
function OnScriptLoaded(level, obj)
  thisObj = obj
  local attributes = obj:GetLuaTableAttributes({
    "alwaysSpawnBoat",
    "startEnabled",
    "customZone",
    "onDockBoatEvents",
    "onEnterBoatEvents",
    "cineOverride_OnDockBoat",
    "beachCheckpointOverrideSon",
    "nodeType",
    "undockDirection"
  })
  alwaysSpawnBoat = attributes.alwaysSpawnBoat
  startEnabled = attributes.startEnabled
  customZone = attributes.customZone
  onDockBoatEvents = attributes.onDockBoatEvents
  onEnterBoatEvents = attributes.onEnterBoatEvents
  cineOverride_OnDockBoat = attributes.cineOverride_OnDockBoat
  beachCheckpointOverrideSon = attributes.beachCheckpointOverrideSon
  if beachCheckpointOverrideSon ~= nil and beachCheckpointOverrideSon ~= "" then
    beachCheckpointOverrideSon = GameObjects[beachCheckpointOverrideSon]
  else
    beachCheckpointOverrideSon = nil
  end
  nodeType = attributes.nodeType or "Dock"
  if nodeType == "Dock" then
    undockDirection = attributes.undockDirection
    dockForward = obj:GetWorldForward()
    dockLeft = obj:GetWorldLeft()
  end
  if cineOverride_OnDockBoat ~= nil and cineOverride_OnDockBoat ~= "" then
    cineOverride_OnDockBoat = string.gsub(cineOverride_OnDockBoat, "%s+", "")
    local commaIndex = string.find(cineOverride_OnDockBoat, ",")
    cineOverride_OnDockBoat_Check = string.sub(cineOverride_OnDockBoat, 1, commaIndex - 1)
    cineOverride_OnDockBoat = string.sub(cineOverride_OnDockBoat, commaIndex + 1)
    local bangIndex = string.find(cineOverride_OnDockBoat_Check, "!")
    if bangIndex then
      engine.Warning("cinematic override function (1st function) must exist in the same wad as the dock!")
    end
    cineOverride_OnDockBoat_Check = LD.ExtractCallbacksForEvent(level, obj, cineOverride_OnDockBoat_Check)
    cineOverride_OnDockBoat = LD.ExtractCallbacksForEvent(level, obj, cineOverride_OnDockBoat)
  end
  if onDockBoatEvents ~= nil then
    onDockBoatEvents = LD.ExtractCallbacksForEvent(level, obj, onDockBoatEvents)
  end
  if onEnterBoatEvents ~= nil then
    onEnterBoatEvents = LD.ExtractCallbacksForEvent(level, obj, onEnterBoatEvents)
  end
  spawner = obj:FindSingleGOByName("BoatSpawnPoint")
  boatVisual = obj:FindSingleGOByName("FakeBoatGeo")
  entryZone = obj:FindSingleGOByName("SpawnBoatZone")
  exitZone = obj:FindSingleGOByName("DespawnBoatZone")
  if thisObj:FindLuaTableAttribute("MapMarkerName") ~= nil and thisObj:FindLuaTableAttribute("MapMarkerName") ~= "" then
    mapMarkerID = thisObj:FindLuaTableAttribute("MapMarkerName")
  end
  local refObj = thisObj
  if customZone ~= nil and customZone ~= "" then
    entryZone:Hide()
    exitZone:Hide()
    refObj = GameObjects[customZone]
    entryZone = refObj:FindSingleGOByName("SpawnBoatZone")
    exitZone = refObj:FindSingleGOByName("DespawnBoatZone")
  end
  local entryMonitor = refObj:FindSingleGOByName("EntityZoneEventsEnter")
  if entryMonitor ~= nil then
    entryMonitor.LuaObjectScript.RegisterEntryCallback(PlayerEnteredZone)
  end
  local exitMonitor = refObj:FindSingleGOByName("EntityZoneEventsExit")
  if exitMonitor ~= nil then
    exitMonitor.LuaObjectScript.RegisterExitCallback(PlayerExitedZone)
  end
  local yLocation = obj:GetWorldPosition().y
  if -1 <= yLocation then
    myWaterLevel = 0
  elseif yLocation < -1 and -7 < yLocation then
    myWaterLevel = 1
  elseif yLocation < -17 and -23.5 < yLocation then
    myWaterLevel = 2
  else
    engine.Warning("Dock/Beach is not at a valid location (height ", yLocation, "): ", LD.GetParentTrace(thisObj))
  end
  obj:AddMarker("BoatDock")
  game.SubObject.Sleep(thisObj)
end
function OnFirstPreStart(level, obj)
  if startEnabled then
    enabled = true
  else
    enabled = false
  end
end
function PlayerInBoat()
  local playerRespawningInBoat = false
  local player = game.Player.FindPlayer()
  if player.IsPlayerEnteringBoatFromCheckpoint then
    playerRespawningInBoat = player.IsPlayerEnteringBoatFromCheckpoint()
  end
  if playerRespawningInBoat == false and game.Boat.GetPlayerBoat() == nil then
    return false
  else
    return true
  end
end
function OnPreStart(level, obj)
  if ValidSpawnPoint() and not PlayerInBoat() then
    DisplayBoatVisual()
  else
    HideBoatVisual()
  end
  if dockingEnabled then
    EnableDocking()
  else
    DisableDocking()
  end
  CreateBoatGlobalMonitor()
end
function CreateBoatGlobalMonitor()
  if boatGlobalMonitor == nil then
    boatGlobalMonitor = LM.CreateLoadMonitor()
    boatGlobalMonitor:AddCallback({
      Wads = {"BoatGlobal"},
      Functions = {
        BoatWadLoaded
      }
    })
  end
end
function ShowDebug()
  if engine.IsDebug() and game.AIUtil.Distance(game.Player.FindPlayer(), thisObj) < 24 then
    local height = thisObj:GetWorldPosition().y
    engine.DrawTextInWorld(thisObj.WorldPosition, "World Y Position: " .. tostring(height), 16711680, -5)
    engine.DrawTextInWorld(thisObj.WorldPosition, "my water level: " .. tostring(myWaterLevel), 16711680, -6)
    engine.DrawTextInWorld(thisObj.WorldPosition, "Always spawn: " .. tostring(alwaysSpawnBoat), 16711680, -1)
    engine.DrawTextInWorld(thisObj.WorldPosition, "In spawn zone: " .. tostring(playerInZone), 16711680, -2)
    if myVehicle ~= nil then
      engine.DrawTextInWorld(thisObj.WorldPosition, "myVehicle is: " .. tostring(myVehicle), 16711680, -3)
      engine.DrawLine(thisObj.WorldPosition, myVehicle.WorldPosition, 16711680)
    end
    engine.DrawTextInWorld(thisObj.WorldPosition, "Docking enabled: " .. tostring(dockingEnabled), 16711680, -4)
    engine.DrawTextInWorld(thisObj.WorldPosition, "Enabled: " .. tostring(enabled), 16711680, 1)
    engine.DrawTextInWorld(thisObj.WorldPosition, "Dock type: " .. tostring(thisObj:GetName()), 16711680)
  end
end
function ValidSpawnPoint()
  local waterLevel = LD.GetCurrentWaterLevel()
  if enabled and (alwaysSpawnBoat or myWaterLevel == waterLevel) then
    return true
  else
    return false
  end
end
function SetWaterLevel(newLevel)
  myWaterLevel = newLevel
end
function BoatWadLoaded(wads)
  boatGlobalMonitor = nil
  wad_BoatGlobal = wads.BoatGlobal
  if playerInZone then
    PlayerEnteredZone()
  else
    PlayerExitedZone()
  end
end
function PlayerEnteredZone()
  playerInZone = true
  DiscoverMapMarker()
  if exitZone ~= nil and entryZone ~= nil then
    entryZone:HideEntityVolume()
    exitZone:ShowEntityVolume()
  end
  if ValidSpawnPoint() and not PlayerInBoat() then
    if wad_BoatGlobal == nil then
      wad_BoatGlobal = game.FindLevel("BoatGlobal")
    end
    if wad_BoatGlobal ~= nil then
      if nodeType == "Dock" then
        local preferredDir = GetPreferredUndockDirection()
        if preferredDir ~= nil then
          lastBoatDir = preferredDir
        end
      end
      SpawnVehicle(lastBoatPos, lastBoatDir)
      LD.CallFunctionAfterDelay(function()
        HideBoatVisual()
      end, 0.5)
    else
      CreateBoatGlobalMonitor()
    end
  else
    HideBoatVisual()
  end
end
function PlayerExitedZone()
  playerInZone = false
  if myVehicle ~= nil and saveTransformData then
    lastBoatPos = myVehicle:GetWorldPosition()
    lastBoatDir = myVehicle:GetWorldForward()
  end
  if exitZone ~= nil and entryZone ~= nil then
    entryZone:ShowEntityVolume()
    exitZone:HideEntityVolume()
  end
  if ValidSpawnPoint() and not PlayerInBoat() then
    DisplayBoatVisual()
  else
    HideBoatVisual()
  end
  DestroyVehicle()
end
function DiscoverMapMarker()
  if not mapMarkerDiscovered and mapMarkerID ~= nil then
    local markerString = tostring(mapMarkerID)
    if string.find(markerString, "_Upper*") and game.Level.GetVariable("_GBL_WaterDrop02Triggered") == false then
      LD.SetMarkerState(mapMarkerID, 1)
      mapMarkerDiscovered = true
      LD.UpdateMap(mapMarkerID, 1)
    elseif string.find(markerString, "_Lower*") and game.Level.GetVariable("_GBL_WaterDrop02Triggered") == true then
      LD.SetMarkerState(mapMarkerID, 1)
      mapMarkerDiscovered = true
      LD.UpdateMap(mapMarkerID, 1)
    elseif not string.find(markerString, "_Upper*") and not string.find(markerString, "_Lower*") then
      LD.SetMarkerState(mapMarkerID, 1)
      mapMarkerDiscovered = true
      LD.UpdateMap(mapMarkerID, 1)
    end
  end
end
function OverrideDefaultSpawnLocation(newLocation)
  lastBoatPos = newLocation
  lastBoatDir = spawner:GetWorldForward()
  RestoreLastPosition()
end
function SpawnVehicle(pos, dir)
  if wad_BoatGlobal == nil then
    wad_BoatGlobal = game.FindLevel("BoatGlobal")
  end
  if wad_BoatGlobal == nil then
    return
  end
  myVehicle = spawner.LuaObjectScript.SpawnVehicle(wad_BoatGlobal, pos, dir)
end
function DestroyVehicle()
  if myVehicle ~= nil then
    myVehicle:FindSingleGOByName("puppeteerHelper"):CallScript("DestroyBoat")
    myVehicle = nil
  end
end
function DisplayBoatVisual()
  if boatVisual ~= nil then
    boatVisual:Show()
    boatVisual:ShowCollision()
    RestoreLastPosition()
  end
end
function HideBoatVisual()
  if boatVisual ~= nil then
    boatVisual:Hide()
    boatVisual:HideCollision()
  end
end
function RestoreLastPosition()
  if boatVisual:GetWorldPosition() ~= lastBoatPos and lastBoatPos ~= nil then
    boatVisual:SetWorldPosition(lastBoatPos)
    boatVisual:SetWorldFacing(engine.Vector.New(lastBoatDir.x, boatVisual:GetWorldForward().y, lastBoatDir.z):Normalized())
  end
end
function UpdateDockState(level, obj)
  if game.Level.GetVariable("_GBL_WaterDrop01Triggered") and not game.Level.GetVariable("_GBL_WaterDrop02Triggered") then
    ClearDock()
  elseif myWaterLevel == 2 or alwaysSpawnBoat then
    RepopulateDock()
  else
    ClearDock()
  end
end
function NotifyDockSystem_PlayerEnteringBoat(level, obj, boatObj)
  if level == nil then
    level = thisObj.Level
  end
  if obj == nil then
    obj = thisObj
  end
  HideBoatVisual()
  if boatObj == myVehicle then
    myVehicle = nil
    spawner.LuaObjectScript.HasDiscoveredDock()
    if onEnterBoatEvents then
      LD.ExecuteCallbacksForEvent(thisObj.Level, thisObj, onEnterBoatEvents, "Start boat approach enter")
    end
  else
    DestroyVehicle()
  end
end
function NotifyDockSystem_PlayerExitingBoat(level, obj, playerDockingAtThisDock, dockpointObject)
  if level == nil then
    level = thisObj.Level
  end
  if obj == nil then
    obj = thisObj
  end
  if playerDockingAtThisDock then
    spawner.LuaObjectScript.HasDiscoveredDock()
    lastDockPoint = dockpointObject
    if not enabled then
      enabled = true
    end
    myVehicle = game.Boat.GetPlayerBoat()
    spawner.LuaObjectScript.SetVehicleReference(myVehicle)
    if onDockBoatEvents then
      LD.ExecuteCallbacksForEvent(thisObj.Level, thisObj, onDockBoatEvents, "Start dock enter")
    end
    if cineOverride_OnDockBoat_Check then
      LD.ExecuteCallbacksForEvent(thisObj.Level, thisObj, cineOverride_OnDockBoat_Check, "Check dock enter cine override - notify boat")
    end
    if cineOverride then
      if myVehicle then
        myVehicle:CallScript("ForwardCineCallback", thisObj)
      end
      if myVehicle then
        myVehicle:FindSingleGOByName("puppeteerHelper"):CallScript("HandleCineDockingOverrideRequest")
      end
    end
    cineOverride = false
  else
    myVehicle = nil
    spawner.LuaObjectScript.ClearVehicleReference()
    if ValidSpawnPoint() then
      DisplayBoatVisual()
    end
  end
end
function FireCinematicDockingOverride(level, obj)
  if cineOverride_OnDockBoat then
    LD.ExecuteCallbacksForEvent(thisObj.Level, thisObj, cineOverride_OnDockBoat, "Check dock enter cine override - notify boat")
  end
end
function EnableCinematicTrigger()
  cineOverride = true
end
function DisableCinematicTrigger()
  cineOverride = false
end
function SetTransformSaving(val)
  saveTransformData = val
  if saveTransformData == false then
    lastBoatPos = nil
    lastBoatDir = nil
  end
end
function GetVehicle()
  return myVehicle
end
function IsPlayerInZone()
  return playerInZone
end
function Enable()
  enabled = true
  if myVehicle then
    myVehicle:FindSingleGOByName("puppeteerHelper"):CallScript("EnableInteractZone")
  end
  if myWaterLevel == LD.GetCurrentWaterLevel() then
    spawner.LuaObjectScript.EnableEventEmitter()
  else
    spawner.LuaObjectScript.DisableAllSonAwareness()
  end
end
function Disable()
  enabled = false
  if myVehicle then
    myVehicle:FindSingleGOByName("puppeteerHelper"):CallScript("DisableInteractZone")
  end
  spawner.LuaObjectScript.DisableAllSonAwareness()
end
function EnableAndRespawn()
  Enable()
  RepopulateDock()
end
function RepopulateDock()
  if ValidSpawnPoint() and not PlayerInBoat() then
    if playerInZone == false then
      DisplayBoatVisual()
    else
      local dir
      if nodeType == "Dock" then
        dir = GetPreferredUndockDirection()
      end
      SpawnVehicle(nil, dir)
    end
  end
end
function DisableAndClear()
  Disable()
  ClearDock()
end
function ClearDock()
  DestroyVehicle()
  HideBoatVisual()
end
function EnableDocking()
  dockingEnabled = true
  local mrkr = "DockPoint"
  for _, desc in pairs(thisObj.Descendants) do
    local descName = string.lower(desc:GetName())
    if (descName == "dockfront" or descName == "dockleft" or descName == "dockright") and desc:HasMarker(mrkr) == false then
      desc:AddMarker(mrkr)
    end
  end
  if myWaterLevel == LD.GetCurrentWaterLevel() then
    spawner.LuaObjectScript.EnableEventEmitter()
  else
    spawner.LuaObjectScript.DisableAllSonAwareness()
  end
end
function DisableDocking()
  dockingEnabled = false
  local mrkr = "DockPoint"
  for _, desc in pairs(thisObj.Descendants) do
    if desc:HasMarker(mrkr) then
      desc:RemoveMarker(mrkr)
    end
  end
  spawner.LuaObjectScript.DisableAllSonAwareness()
end
function UpdateBoatLocationAndStoreCheckpoint(level, obj, storeCheckpoint)
  if saveTransformData and myVehicle then
    lastBoatPos = myVehicle:GetWorldPosition()
    lastBoatDir = myVehicle:GetWorldForward()
    if nodeType == "Beach" then
      lastBoatDir = lastBoatDir * -1
    else
      local preferredDir = GetPreferredUndockDirection()
      if preferredDir ~= nil and math.abs(LD.GetAngleBetweenVector(lastBoatDir, preferredDir)) > 5 then
        lastBoatDir = preferredDir
        if myVehicle ~= nil then
          spawner.LuaObjectScript.ClearVehicleReference()
          DestroyVehicle()
          SpawnVehicle(lastBoatPos, lastBoatDir)
        end
      end
    end
  end
  if beachCheckpointOverrideSon == nil and nodeType == "Beach" then
    beachCheckpointOverrideSon = obj.Parent.Parent:FindSingleGOByName("beachCheckpointOverrideSon")
    if beachCheckpointOverrideSon then
      beachCheckpointOverrideSon = beachCheckpointOverrideSon.Child
    end
  end
  if storeCheckpoint then
    if beachCheckpointOverrideSon then
      game.World.StoreCheckpoint({OverrideObject = beachCheckpointOverrideSon})
    else
      game.World.StoreCheckpoint()
    end
  end
end
function GetPreferredUndockDirection()
  if lastDockPoint == nil then
    local closestDist = 99
    local closestPoint
    local dockPoints = {
      thisObj:FindSingleGOByName("DockFront"),
      thisObj:FindSingleGOByName("DockLeft"),
      thisObj:FindSingleGOByName("DockRight")
    }
    local prevBoatPosition = lastBoatPos or dockPoints[1]
    for _, point in pairs(dockPoints) do
      local distToDockPoint = game.AIUtil.Distance(point, prevBoatPosition)
      if closestDist > distToDockPoint then
        closestDist = distToDockPoint
        closestPoint = point
      end
    end
    lastDockPoint = closestPoint
  end
  local dockPointName = string.lower(lastDockPoint:GetName())
  if dockPointName == "dockleft" or dockPointName == "dockright" then
    return dockForward
  elseif undockDirection ~= nil and undockDirection ~= "--" then
    if undockDirection == "Left" then
      return dockLeft
    elseif undockDirection == "Right" then
      return dockLeft * -1
    end
  end
  return nil
end
function DisableAllSonAwareness()
  spawner.LuaObjectScript.DisableAllSonAwareness()
end
function HasDiscoveredDock()
  spawner.LuaObjectScript.HasDiscoveredDock()
end
function OnSaveCheckpoint(level, obj)
  return {
    enabled = enabled,
    mapMarkerDiscovered = mapMarkerDiscovered,
    dockingEnabled = dockingEnabled,
    lastBoatPos = lastBoatPos,
    lastBoatDir = lastBoatDir,
    playerInZone = playerInZone
  }
end
function OnRestoreCheckpoint(level, obj, savedInfo)
  enabled = savedInfo.enabled
  mapMarkerDiscovered = savedInfo.mapMarkerDiscovered
  dockingEnabled = savedInfo.dockingEnabled
  lastBoatPos = savedInfo.lastBoatPos
  lastBoatDir = savedInfo.lastBoatDir
  playerInZone = savedInfo.playerInZone or false
end
