local LD = require("design.LevelDesignLibrary")
local monitors, thisObj, thisLevel, zoneObj, player, banterName
local requirementsTable = {}
local hideZoneAfterAllTriggered, startZoneHidden, hasBanterFired, completeTable
local isNonCritical = false
local isBoatExclusive = false
local isInZone = false
local isLookingAt = false
local usesLineOfSight = false
local gameTimeLastBanterPlayed = 0
local numTriggeredEver = 0
local lastBanterIndex
local minPercentFromScreenEdge = 40
local autoTriggerZone
local enableBtrError = {}
local lookAtMonitor, tryToPlayTimer
local logPrefix = "[Banter Look-At]- '"
local parentTrace, onBanterFire, onBanterFinished, onAllBanterFinished, requirementString, gameTimeRequirements
local resetOnCombatFinish = false
local waitingForCombatToEnd = false
local bDebugEnabled = false
local monitors_LoadLibrary = function()
  if monitors == nil then
    monitors = require("level.MonitorLibrary")
  end
end
function OnScriptLoaded(level, obj)
  local hitError = false
  thisObj = obj
  thisLevel = level
  player = game.Player.FindPlayer()
  zoneObj = obj.Parent.Parent
  local banterStringAtrb = thisObj:GetLuaTableAttribute("BanterName")
  requirementString = thisObj:GetLuaTableAttribute("Requirements")
  hideZoneAfterAllTriggered = thisObj:GetLuaTableAttribute("HideZoneAfterTriggered")
  startZoneHidden = thisObj:GetLuaTableAttribute("HideZoneInitially")
  isNonCritical = thisObj:GetLuaTableAttribute("IsNonCritical")
  isBoatExclusive = thisObj:GetLuaTableAttribute("IsBoatExclusive")
  usesLineOfSight = thisObj:GetLuaTableAttribute("UsesLineOfSight")
  minPercentFromScreenEdge = thisObj:GetLuaTableAttribute("MinPercentFromScreenEdge")
  gameTimeRequirements = thisObj:GetLuaTableAttribute("GameTimeRequirements")
  autoTriggerZone = thisObj:GetLuaTableAttribute("AutoTriggerZone")
  onBanterFire = thisObj:GetLuaTableAttribute("OnBanterFire")
  onBanterFinished = thisObj:GetLuaTableAttribute("OnBanterFinished")
  onAllBanterFinished = thisObj:GetLuaTableAttribute("OnAllBanterFinished")
  resetOnCombatFinish = thisObj:GetLuaTableAttribute("AutoEnable_IfDisabledFromCombat")
  onBanterFire = LD.ExtractCallbacksForEvent(level, obj, onBanterFire)
  onBanterFinished = LD.ExtractCallbacksForEvent(level, obj, onBanterFinished)
  onAllBanterFinished = LD.ExtractCallbacksForEvent(level, obj, onAllBanterFinished)
  parentTrace = "(" .. LD.GetParentTrace(thisObj) .. ")"
  banterName = LD.ConvertStringListToTable(banterStringAtrb)
  gameTimeRequirements = LD.ConvertStringListToTable(gameTimeRequirements)
  local stringTable = LD.ConvertStringListToTable(requirementString)
  for _, req in ipairs(stringTable) do
    local tableEntry = {
      min = nil,
      max = nil,
      func = nil
    }
    local delimIndex = string.find(req, "-")
    if delimIndex ~= nil then
      tableEntry.min = tonumber(string.sub(req, 1, delimIndex - 1))
      tableEntry.max = tonumber(string.sub(req, delimIndex + 1))
      if tableEntry.min == nil or tableEntry.max == nil then
        engine.Error("Banter Zone '" .. zoneObj:GetName() .. "' in level '" .. level.Name .. "' got cine number requirement that was not a number value ( " .. req .. " )  (" .. LD.GetParentTrace(zoneObj) .. ")")
        break
      end
    elseif tonumber(req) ~= nil then
      tableEntry.min = tonumber(req)
    else
      tableEntry.func = req
    end
    table.insert(requirementsTable, tableEntry)
  end
  zoneObj = obj.Parent.Parent
  game.SubObject.SetEntityZoneHandler(thisObj, zoneObj)
  if autoTriggerZone ~= nil and autoTriggerZone ~= "" then
    autoTriggerZone = thisLevel:GetGameObject(tostring(autoTriggerZone))
    game.SubObject.SetEntityZoneHandler(thisObj, autoTriggerZone)
  else
    autoTriggerZone = nil
  end
  if #banterName == 0 then
    engine.Warning("Banter Zone '", zoneObj:GetName(), "' in level '", level.Name, "' does not have any banter to play.  Either add the appropriate banter or delete this refnode. (", LD.GetParentTrace(zoneObj), ")")
    hitError = true
  end
  if #requirementsTable == 0 then
    if #banterName == 1 then
      table.insert(requirementsTable, {min = 0})
    else
      engine.Error("Banter Zone '" .. zoneObj:GetName() .. "' in level '" .. level.Name .. "' needs cine number requirements for all " .. tostring(#banterName) .. " banter lines it's set to trigger (" .. LD.GetParentTrace(zoneObj) .. ")")
      hitError = true
    end
  end
  if #banterName ~= #requirementsTable then
    engine.Error("Banter Zone '" .. zoneObj:GetName() .. "' in level '" .. level.Name .. "' needs the same number of banter lines as cine number requirements (" .. LD.GetParentTrace(zoneObj) .. ")")
    hitError = true
  end
  if hitError == false then
    game.SubObject.SetEntityZoneHandler(thisObj, zoneObj)
    monitors_LoadLibrary()
    lookAtMonitor = monitors.CreateLookAtMonitor(thisObj:GetWorldPosition())
    lookAtMonitor:Stop()
    lookAtMonitor:OnLookAt(OnLookAt)
    lookAtMonitor:OnLookAway(OnLookAway)
    lookAtMonitor:SetMinPercentFromScreenEdge(minPercentFromScreenEdge)
    lookAtMonitor:SetPrintsEnabled(false)
    lookAtMonitor:SetObstructable(usesLineOfSight)
    lookAtMonitor:Stop()
  end
  isInZone = player:IsInsideEntityZone(zoneObj)
  if autoTriggerZone and player:IsInsideEntityZone(autoTriggerZone) then
    isLookingAt = true
  end
  for i = 1, #banterName do
    enableBtrError[banterName[i]] = true
  end
  if bDebugEnabled then
    DebugEnable()
  else
    DebugDisable()
  end
end
function OnSaveCheckpoint(level, obj)
  return {
    hasBanterFired = hasBanterFired,
    gameTimeLastBanterPlayed = gameTimeLastBanterPlayed,
    completeTable = completeTable,
    waitingForCombatToEnd = waitingForCombatToEnd
  }
end
function OnRestoreCheckpoint(level, obj, savedInfo)
  hasBanterFired = savedInfo.hasBanterFired
  gameTimeLastBanterPlayed = savedInfo.gameTimeLastBanterPlayed
  completeTable = savedInfo.completeTable
  waitingForCombatToEnd = savedInfo.waitingForCombatToEnd
end
function OnFirstStart(level, obj)
  if startZoneHidden then
    HideEntityZones()
    engine.Print("Zone hidden initially: '", zoneObj:GetName(), "' in level '", level.Name, "' (", LD.GetParentTrace(zoneObj), ")")
  end
end
function OnStart(level, obj)
  if completeTable == nil then
    completeTable = {}
    for i = 1, #banterName do
      completeTable[i] = false
    end
  end
  if #completeTable ~= #banterName then
    engine.Warning(level.Name, " possibly needs a Bookmark Regeneration. CompleteTable and banterName table have differing sizes ", #completeTable, " ~= ", #banterName, " in ", thisObj.Parent)
  end
  if waitingForCombatToEnd then
    waitingForCombatToEnd = false
    Enable()
  end
end
function OnUpdate(level, obj)
  if game.Combat.GetCombatStatus() == false then
    waitingForCombatToEnd = false
    Enable()
    game.SubObject.Sleep(thisObj)
  end
  ShowDebugTable()
end
function OnMarkerEnterZone(level, scriptObj, volumeGameObject, markerGameObject, markerId)
  if markerGameObject == player then
    if CheckCanPlayDuringCombat() and volumeGameObject == zoneObj then
      isInZone = true
      if lookAtMonitor ~= nil then
        lookAtMonitor:Start()
        if lookAtMonitor:IsLookingAt() then
          isLookingAt = true
          TryToPlayBanter()
        end
      end
    elseif volumeGameObject == autoTriggerZone then
      if not player:IsInsideEntityZone(zoneObj) then
        engine.Warning("Auto Trigger Zone '" .. autoTriggerZone:GetName() .. "' is in not inside of parent zone '" .. zoneObj:GetName() .. "' please make sure the AutoTriggerZone is fully encompassed by the LookAtZone")
        return
      end
      isLookingAt = true
      TryToPlayBanter()
    end
  end
end
function OnMarkerExitZone(level, scriptObj, volumeGameObject, markerGameObject, markerId)
  if markerGameObject == player and isInZone == true then
    if volumeGameObject == zoneObj then
      isInZone = false
      if lookAtMonitor ~= nil then
        lookAtMonitor:Stop()
        isLookingAt = false
      end
      if tryToPlayTimer then
        tryToPlayTimer:Stop()
        tryToPlayTimer:Reset()
      end
    else
      isLookingAt = false
    end
  end
end
function OnLookAt()
  isLookingAt = true
  TryToPlayBanter()
end
function OnLookAway()
  isLookingAt = false
  if tryToPlayTimer then
    tryToPlayTimer:Stop()
    tryToPlayTimer:Reset()
  end
end
function ResetLookAtMonitor()
  lookAtMonitor:Stop()
  lookAtMonitor:Terminate()
  lookAtMonitor = nil
  isLookingAt = false
  monitors_LoadLibrary()
  lookAtMonitor = monitors.CreateLookAtMonitor(thisObj:GetWorldPosition())
  lookAtMonitor:Stop()
  lookAtMonitor:OnLookAt(OnLookAt)
  lookAtMonitor:OnLookAway(OnLookAway)
  lookAtMonitor:SetMinPercentFromScreenEdge(minPercentFromScreenEdge)
  lookAtMonitor:SetPrintsEnabled(false)
  lookAtMonitor:SetObstructable(usesLineOfSight)
  lookAtMonitor:Stop()
end
function CheckCanPlayDuringCombat()
  if game.Combat.GetCombatStatus() then
    if resetOnCombatFinish then
      waitingForCombatToEnd = true
      game.SubObject.Wake(thisObj)
    end
    Disable()
    return false
  else
    return true
  end
end
function CheckRequirement(index)
  local completedCineNum = game.Level.GetVariable("CompletedCineNumber")
  if requirementsTable[index] == nil then
    engine.Error(thisLevel.Name, " possibly needs a Bookmark Regeneration. Out of index bounds in requirementsTable. Index = ", index, " in ", thisObj.Parent)
    return
  end
  if requirementsTable[index].func ~= nil and requirementsTable[index].func ~= false then
    return LD.ExecuteSingleCallbackAndReturn(thisLevel, thisObj, requirementsTable[index].func)
  elseif requirementsTable[index].max ~= nil and requirementsTable[index].min ~= nil then
    if completedCineNum > requirementsTable[index].max then
      completeTable[index] = true
    end
    return completedCineNum >= requirementsTable[index].min and completedCineNum <= requirementsTable[index].max
  elseif requirementsTable[index].min ~= nil then
    return completedCineNum >= requirementsTable[index].min
  end
end
function CheckGameTimeRequirement(index)
  if gameTimeRequirements[index] ~= nil then
    local gameTime = game.GetGameTime() - gameTimeLastBanterPlayed
    return gameTime >= GetTimeInSeconds(gameTimeRequirements[index])
  else
    return true
  end
end
function CheckIsInBoatAndExclusive()
  if isBoatExclusive then
    return game.Boat.GetPlayerBoat() ~= nil
  else
    return true
  end
end
function TryToPlayBanter()
  if CheckCanPlayDuringCombat() then
    local numTriggeredNow = 0
    for i = 1, #completeTable do
      if completeTable[i] == false and CheckRequirement(i) and CheckGameTimeRequirement(i) and numTriggeredNow == 0 and isInZone and isLookingAt and CheckIsInBoatAndExclusive() then
        if isNonCritical then
          game.Audio.PlayBanterNonCritical(banterName[i], function()
            OnNonCriticalBanterFinished(i)
          end, nil, enableBtrError[banterName[i]], function()
            OnNonCriticalBanterFired(i)
          end)
        else
          PlayCriticalBanter(i, function()
            OnCriticalBanterFinished(i)
          end, nil, enableBtrError[i])
        end
        numTriggeredNow = 1
        break
      end
    end
    if numTriggeredNow == 0 and numTriggeredEver ~= #banterName then
      engine.Print("No banter in Banter Zone '", zoneObj:GetName(), [[
' meets its requirement for being triggered. 
Requirements: [ ]], requirementString, " ] ", [[

GameTimeRequirements: []], LD.ConvertTableToStringList(gameTimeRequirements), [[
] 
Time Since Last Banter Played: ]], game.GetGameTime() - gameTimeLastBanterPlayed)
      if isLookingAt then
        if tryToPlayTimer then
          tryToPlayTimer:Reset()
          tryToPlayTimer:Start()
        else
          tryToPlayTimer = StartLevelTimer(1.5, TryToPlayBanter)
        end
      else
        tryToPlayTimer:Stop()
        tryToPlayTimer:Reset()
      end
    end
  end
end
function PlayCriticalBanter(i, func)
  game.Audio.PlayBanter(banterName[i], func, nil, enableBtrError[banterName[i]], function()
    if isBoatExclusive then
      LD.ResetSelectedLore()
    end
  end)
  engine.Print("Banter Zone '", zoneObj:GetName(), "' in level '", thisLevel.Name, "' triggered.  Playing banter '", banterName[i], "'")
  LD.ExecuteCallbacksForEvent(thisObj.Level, thisObj, onBanterFire, "Banter Event Fired")
  completeTable[i] = true
  numTriggeredEver = numTriggeredEver + 1
end
function GetLastBanterName()
  if lastBanterIndex ~= nil then
    return banterName[lastBanterIndex]
  end
  return nil
end
function DisableConsoleError(banterKeyName)
  if enableBtrError[banterKeyName] then
    enableBtrError[banterKeyName] = false
  end
end
function OnCriticalBanterFinished(i)
  lastBanterIndex = i
  engine.Print("Banter Look At '", zoneObj:GetName(), "' in level '", thisLevel.Name, "' finished playing Critical banter '", banterName[i], "'")
  LD.ExecuteCallbacksForEvent(thisObj.Level, thisObj, onBanterFinished, "Critical Banter Finished Event ", GetLastBanterName())
  gameTimeLastBanterPlayed = game.GetGameTime()
  if numTriggeredEver == #banterName then
    OnAllBanterFinished()
  end
end
function OnNonCriticalBanterFired(i)
  engine.Print("Banter Look At '", zoneObj:GetName(), "' in level '", thisLevel.Name, "' triggered.  Playing Non Critical banter '", banterName[i], "'")
  LD.ExecuteCallbacksForEvent(thisObj.Level, thisObj, onBanterFire, "NonCritical Banter Event Fired")
  completeTable[i] = true
  numTriggeredEver = numTriggeredEver + 1
end
function OnNonCriticalBanterFinished(i)
  lastBanterIndex = i
  engine.Print("Banter Look At '", zoneObj:GetName(), "' in level '", thisLevel.Name, "' finished playing Non Critical banter '", banterName[i], "'")
  LD.ExecuteCallbacksForEvent(thisObj.Level, thisObj, onBanterFinished, "NonCritical Banter Finished Event ", GetLastBanterName())
  gameTimeLastBanterPlayed = game.GetGameTime()
  if numTriggeredEver == #banterName then
    OnAllBanterFinished()
  end
end
function OnAllBanterFinished()
  local logMsg = "All banter from zone '" .. zoneObj:GetName() .. "' in level '" .. thisLevel.Name .. "' has been triggered."
  if hideZoneAfterAllTriggered == true then
    logMsg = logMsg .. " Hiding entity volume(s)."
    HideEntityZones()
  end
  engine.Print(logMsg)
  LD.ExecuteCallbacksForEvent(thisObj.Level, thisObj, onAllBanterFinished, "All Banter Finished Event ")
end
function ConvertStringListToNumericTable(str)
  local returnTable = {}
  local strTable = LD.ConvertStringListToTable(str)
  for _, req in ipairs(strTable) do
    local num = tonumber(req)
    if num ~= nil then
      table.insert(returnTable, tonumber(req))
    else
      engine.Error("'", zoneObj:GetName(), "' in level '", thisLevel.Name, "' has Non Numerical value for cine number requirement. Check Cine Number requirements in LuaToolBox. String List [ ", str, " ]", parentTrace)
      break
    end
  end
  return returnTable
end
function GetTimeInSeconds(gameTime)
  if tonumber(gameTime) then
    return tonumber(gameTime)
  end
  gameTime = string.lower(gameTime)
  local hoursText = string.gsub(string.match(gameTime, "(%d+)h") or "", "h", "")
  local minText = string.gsub(string.match(gameTime, "(%d+)m") or "", "m", "")
  local secText = string.gsub(string.match(gameTime, "(%d+)s") or "", "s", "")
  local hours = (tonumber(hoursText) or 0) * 60 * 60
  local min = (tonumber(minText) or 0) * 60
  local sec = tonumber(secText) or 0
  return hours + min + sec
end
function ShowEntityZones()
  zoneObj:ShowEntityVolume()
  if autoTriggerZone then
    autoTriggerZone:ShowEntityVolume()
  end
end
function HideEntityZones()
  zoneObj:HideEntityVolume()
  if autoTriggerZone then
    autoTriggerZone:HideEntityVolume()
  end
end
function Enable()
  if numTriggeredEver < #banterName then
    ShowEntityZones()
  end
end
function Disable()
  if lookAtMonitor ~= nil then
    lookAtMonitor:Stop()
    isLookingAt = false
  end
  HideEntityZones()
end
function DebugEnable()
  bDebugEnabled = true
  lookAtMonitor:SetDebugEnabled(true)
  game.SubObject.Wake(thisObj)
end
function DebugDisable()
  bDebugEnabled = false
  lookAtMonitor:SetDebugEnabled(false)
  game.SubObject.Sleep(thisObj)
end
function ShowDebugTable(distance, x, y)
  distance = distance or 7
  if engine.IsDebug() and bDebugEnabled and (distance > (player.WorldPosition - thisObj.WorldPosition):Length() or isInZone and isLookingAt) then
    local debugTable = {}
    debugTable.Y = y or 5
    debugTable.X = x or 50
    debugTable.Title = thisObj.Parent:GetName()
    debugTable.TitleColor = engine.Vector.New(255, 0, 128)
    table.insert(debugTable, {
      "Global Game Time: ",
      game.GetGameTime()
    })
    table.insert(debugTable, {
      "gameTimeLastBanterPlayed: ",
      gameTimeLastBanterPlayed
    })
    table.insert(debugTable, {
      "Time Since Last Played: ",
      game.GetGameTime() - gameTimeLastBanterPlayed
    })
    table.insert(debugTable, {"IsInZone: ", isInZone})
    table.insert(debugTable, {
      "IsLookingAt: ",
      isLookingAt
    })
    table.insert(debugTable, {
      "Time Since Last Played: ",
      game.GetGameTime() - gameTimeLastBanterPlayed
    })
    for i, v in ipairs(banterName) do
      local tableToAdd = {
        "Banter" .. i .. ": ",
        v,
        "Requirements: "
      }
      if requirementsTable[i].min then
        table.insert(tableToAdd, "Min Cine: ")
        table.insert(tableToAdd, requirementsTable[i].min)
      end
      if requirementsTable[i].max then
        table.insert(tableToAdd, "Max Cine: ")
        table.insert(tableToAdd, requirementsTable[i].max)
      end
      if requirementsTable[i].func then
        table.insert(tableToAdd, "Func: ")
        table.insert(tableToAdd, requirementsTable[i].func)
      end
      if gameTimeRequirements[i] ~= nil then
        table.insert(tableToAdd, "Game Time Required: ")
        table.insert(tableToAdd, gameTimeRequirements[i])
        table.insert(tableToAdd, "InSeconds: ")
        table.insert(tableToAdd, GetTimeInSeconds(gameTimeRequirements[i]))
      end
      table.insert(tableToAdd, "HasPlayed: ")
      table.insert(tableToAdd, completeTable[i])
      table.insert(debugTable, tableToAdd)
    end
    engine.DrawDebugTable(debugTable)
  end
end
function ShowDebugText()
  if engine.IsDebug() == false then
    return
  end
  local debugText = " "
  for i, v in ipairs(banterName) do
    debugText = "Banter" .. i .. ": " .. v
    debugText = [[

 Has the Banter played: ]] .. tostring(hasBanterFired) .. "| Is looked at: " .. tostring(isLookingAt)
    debugText = debugText .. [[

 InCombat: ]] .. tostring(game.Combat.GetCombatStatus()) .. "\n"
    debugText = debugText .. [[

 Requirements: ]]
    if requirementsTable[i].func ~= nil then
      debugText = debugText .. " func: " .. requirementsTable[i].func .. "() "
    elseif requirementsTable[i].max ~= nil and requirementsTable[i].min ~= nil then
      debugText = debugText .. " min: " .. tostring(requirementsTable[i].min) .. " max: " .. tostring(requirementsTable[i].max)
    elseif requirementsTable[i].min ~= nil then
      debugText = debugText .. " min: " .. tostring(requirementsTable[i].min)
    else
      debugText = debugText .. (requirementsTable[i] or "nil")
    end
  end
  local color = require("core.color")
  engine.DrawTextInWorld(thisObj:GetWorldPosition(), debugText, color.white)
end
