local LD = require("design.LevelDesignLibrary")
local thisObj, thisLevel, zoneObj, player, son, banterName
local requirementsTable = {}
local completeTable, hideZoneAfterAllTriggered
local isNonCritical = false
local isBoatExclusive = false
local lastBanterIndex
local numTriggeredEver = 0
local isInZone = false
local gameTimeLastBanterPlayed = 0
local enableBtrError = {}
local usePlayer = true
local useSon = false
local markerObject, onBanterFire, onBanterFinished, onAllBanterFinished, requirementString, gameTimeRequirements
local resetOnCombatFinish = false
local waitingForCombatToEnd = false
local allowDuringCombat = false
function OnScriptLoaded(level, obj)
  thisObj = obj
  thisLevel = level
  player = game.Player.FindPlayer()
  son = game.AI.FindSon()
  local attributes = obj:GetLuaTableAttributes({
    "Requirements",
    "HideZoneAfterAllTriggered",
    "IsNonCritical",
    "IsBoatExclusive",
    "OnBanterFire",
    "OnBanterFinished",
    "OnAllBanterFinished",
    "usePlayer",
    "useSon",
    "AutoEnable_IfDisabledFromCombat",
    "AllowBanterToPlayDuringCombat",
    BanterName = "list",
    GameTimeRequirements = "list"
  })
  banterName = attributes.BanterName
  requirementString = attributes.Requirements
  gameTimeRequirements = attributes.GameTimeRequirements
  hideZoneAfterAllTriggered = attributes.HideZoneAfterAllTriggered
  isNonCritical = attributes.IsNonCritical
  isBoatExclusive = attributes.IsBoatExclusive
  onBanterFire = attributes.OnBanterFire
  onBanterFinished = attributes.OnBanterFinished
  onAllBanterFinished = attributes.OnAllBanterFinished
  usePlayer = attributes.usePlayer
  useSon = attributes.useSon
  resetOnCombatFinish = attributes.AutoEnable_IfDisabledFromCombat
  allowDuringCombat = attributes.AllowBanterToPlayDuringCombat
  if requirementString == nil then
    requirementString = ""
  end
  if gameTimeRequirements == nil then
    gameTimeRequirements = {}
  end
  if onBanterFire ~= "" then
    onBanterFire = LD.ExtractCallbacksForEvent(level, obj, onBanterFire)
  else
    onBanterFire = nil
  end
  if onBanterFinished ~= "" then
    onBanterFinished = LD.ExtractCallbacksForEvent(level, obj, onBanterFinished)
  else
    onBanterFinished = nil
  end
  if onAllBanterFinished ~= "" then
    onAllBanterFinished = LD.ExtractCallbacksForEvent(level, obj, onAllBanterFinished)
  else
    onAllBanterFinished = nil
  end
  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 #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), ")")
  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) .. ")")
    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) .. ")")
  end
  for i = 1, #banterName do
    enableBtrError[banterName[i]] = true
  end
  SetUpMarkerObject()
  game.SubObject.Sleep(thisObj)
end
function OnSaveCheckpoint(level, obj)
  return {
    completeTable = completeTable,
    gameTimeLastBanterPlayed = gameTimeLastBanterPlayed,
    numTriggeredEver = numTriggeredEver,
    waitingForCombatToEnd = waitingForCombatToEnd
  }
end
function OnRestoreCheckpoint(level, obj, savedInfo)
  completeTable = savedInfo.completeTable
  numTriggeredEver = savedInfo.numTriggeredEver
  gameTimeLastBanterPlayed = savedInfo.gameTimeLastBanterPlayed
  waitingForCombatToEnd = savedInfo.waitingForCombatToEnd
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
end
function OnMarkerEnterZone(level, scriptObj, volumeGameObject, markerGameObject, markerId)
  if markerGameObject == markerObject and isInZone == false then
    isInZone = true
    if CheckCanPlayDuringCombat() then
      local numTriggeredNow = 0
      for i = 1, #completeTable do
        if not completeTable[i] and CheckRequirement(i) and CheckGameTimeRequirement(i) and numTriggeredNow == 0 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)
          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)
      end
    end
  end
end
function OnMarkerExitZone(level, scriptObj, volumeGameObject, markerGameObject, markerId)
  if markerGameObject == markerObject then
    isInZone = false
  end
end
function DisableConsoleError(banterKeyName)
  if enableBtrError[banterKeyName] then
    enableBtrError[banterKeyName] = false
  end
end
function CheckCanPlayDuringCombat()
  if game.Combat.GetCombatStatus() and allowDuringCombat == false then
    if resetOnCombatFinish then
      waitingForCombatToEnd = true
      game.SubObject.Wake(thisObj)
    end
    Disable()
    return false
  else
    return true
  end
end
function SetUpMarkerObject()
  if useSon and usePlayer then
    engine.Error("Banter Zone: ", zoneObj:GetName(), " can only trigger off of one marker. Please use either the son or player, not both")
  elseif useSon then
    markerObject = son
  elseif usePlayer then
    markerObject = player
  else
    engine.Error("BanterZone: ", zoneObj:GetName(), " requires a marker. Please mark the useSon or usePlayer option in Lua Toolbox")
  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], "'")
  if onBanterFire ~= nil then
    LD.ExecuteCallbacksForEvent(thisObj.Level, thisObj, onBanterFire, "Banter Event Fired")
  end
  completeTable[i] = true
  numTriggeredEver = numTriggeredEver + 1
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
    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 GetLastBanterName()
  if lastBanterIndex ~= nil then
    return banterName[lastBanterIndex]
  end
  return nil
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))
    end
  end
  return returnTable
end
function Reset()
  zoneObj:ShowEntityVolume()
  for i = 1, #completeTable do
    completeTable[i] = false
  end
  numTriggeredEver = 0
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 OnCriticalBanterFinished(i)
  lastBanterIndex = i
  engine.Print("Banter Zone '", zoneObj:GetName(), "' in level '", thisLevel.Name, "' finished playing Critical banter '", banterName[i], "'")
  if onBanterFinished ~= nil then
    LD.ExecuteCallbacksForEvent(thisObj.Level, thisObj, onBanterFinished, "Critical Banter Finished Event ", GetLastBanterName())
  end
  gameTimeLastBanterPlayed = game.GetGameTime()
  if numTriggeredEver == #banterName then
    OnAllBanterFinished()
  end
end
function OnNonCriticalBanterFired(i)
  engine.Print("Banter Zone '", zoneObj:GetName(), "' in level '", thisLevel.Name, "' triggered.  Playing Non Critical banter '", banterName[i], "'")
  if onBanterFire ~= nil then
    LD.ExecuteCallbacksForEvent(thisObj.Level, thisObj, onBanterFire, "NonCritical Banter Event Fired")
  end
  completeTable[i] = true
  numTriggeredEver = numTriggeredEver + 1
end
function OnNonCriticalBanterFinished(i)
  lastBanterIndex = i
  engine.Print("Banter Zone '", zoneObj:GetName(), "' in level '", thisLevel.Name, "' finished playing Non Critical banter '", banterName[i], "'")
  if onBanterFinished ~= nil then
    LD.ExecuteCallbacksForEvent(thisObj.Level, thisObj, onBanterFinished, "NonCritical Banter Finished Event ", GetLastBanterName())
  end
  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)."
    zoneObj:HideEntityVolume()
  end
  engine.Print(logMsg)
  if onAllBanterFinished ~= nil then
    LD.ExecuteCallbacksForEvent(thisObj.Level, thisObj, onAllBanterFinished, "All Banter Finished Event ")
  end
end
function Enable()
  if numTriggeredEver < #banterName then
    zoneObj:ShowEntityVolume()
  end
end
function Disable()
  zoneObj:HideEntityVolume()
end
function ShowDebugTable(x, y)
  if engine.IsDebug() ~= true then
    return
  end
  if (player.WorldPosition - thisObj.WorldPosition):Length() < 7 then
    local debugTable = {}
    debugTable.Y = y or 15
    debugTable.X = x or 15
    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
    })
    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() ~= true then
    return
  end
  local debugText = thisObj:GetName() .. [[

 numTriggeredEver: ]] .. numTriggeredEver .. [[

 InCombat: ]] .. tostring(game.Combat.GetCombatStatus()) .. "\n"
  local reqTable = "ReqTable: "
  reqTable = reqTable .. "\n"
  if requirementsTable ~= nil then
    for index = 1, #requirementsTable do
      reqTable = reqTable .. "BanterName: " .. banterName[index] .. " Played [ " .. tostring(completeTable[index]) .. " ]"
      if requirementsTable[index].func ~= nil then
        reqTable = reqTable .. " func " .. requirementsTable[index].func .. "() "
      elseif requirementsTable[index].max ~= nil and requirementsTable[index].min ~= nil then
        reqTable = reqTable .. " min: " .. tostring(requirementsTable[index].min) .. " max: " .. tostring(requirementsTable[index].max)
      elseif requirementsTable[index].min ~= nil then
        reqTable = reqTable .. " min: " .. tostring(requirementsTable[index].min)
      end
      reqTable = reqTable .. "\n"
    end
  end
  local color = require("core.color")
  engine.DrawTextInWorld(thisObj:GetWorldPosition(), debugText .. "\n" .. reqTable, color.white)
end
