local timers
local profile = require("core.profile")
local monitors = require("level.MonitorLibrary")
local timers_LoadLibrary = function()
  if timers == nil then
    timers = require("level.timer")
  end
end
local SoundDebug = function(text)
  if engine.VFSGetBool("/Sound/Emitter Data") == 1 then
    local stacktrace = {}
    local i = 1
    for str in string.gmatch(debug.traceback(), "%S+") do
      stacktrace[i] = str
      i = i + 1
    end
    print([[

*************************************************************************************************************
*SOUND DEBUG* - ]], stacktrace[#stacktrace], [[

*SOUND DEBUG* - ]], text, [[

*************************************************************************************************************]])
  end
end
local IsLoopingSound = function(soundEvent)
  if soundEvent ~= nil and type(soundEvent) == "string" then
    if string.match(string.lower(soundEvent), "_lp") then
      return true
    else
      return false
    end
  else
    SoundDebug("Attempted to check if " .. tostring(soundEvent) .. " is a looping sound and failed")
    return false
  end
end
local PlaySound = function(emitter, sound, ignoreLoopCheck)
  if emitter ~= nil then
    if sound ~= nil and sound ~= "" then
      if type(sound) == "string" then
        sound = string.lower(sound)
        if string.sub(sound, 1, string.len("snd_")) ~= "snd_" then
          engine.Warning(tostring(emitter) .. " is attempting to play '" .. "'" .. tostring(sound) .. "'" .. "' without the 'SND_' preface")
        end
        if not ignoreLoopCheck and IsLoopingSound(sound) then
          if emitter:IsPlaying(sound) then
            SoundDebug(tostring(emitter) .. " is already playing loop sound " .. "'" .. tostring(sound) .. "'")
          else
            emitter:Start(sound)
            SoundDebug(tostring(emitter) .. " playing " .. "'" .. tostring(sound) .. "'")
          end
        else
          emitter:Start(sound)
          SoundDebug(tostring(emitter) .. " playing " .. "'" .. tostring(sound) .. "'")
        end
      else
        engine.Warning(tostring(emitter) .. " cannot play '" .. tostring(sound) .. "' because it is not a string")
      end
    else
      SoundDebug(tostring(emitter) .. " cannot play sound that is nil")
    end
  else
    SoundDebug("Emitter is nil or empty")
  end
end
local PlaySoundRingOut = function(emitter, sound, ignoreLoopCheck)
  if game.CHECK_FEATURE and not game.CHECK_FEATURE("AUDIO_PLAYRINGINGSOUND") then
    SoundDebug("Cannot play " .. tostring(sound) .. " as ringing sound; code-side Lua function does not exist. Falling back to PlaySound")
    PlaySound(emitter, sound, ignoreLoopCheck)
    return
  end
  if emitter ~= nil then
    if sound ~= nil and sound ~= "" then
      if type(sound) == "string" then
        sound = string.lower(sound)
        if string.sub(sound, 1, string.len("snd_")) ~= "snd_" then
          engine.Warning(tostring(emitter) .. " is attempting to play '" .. "'" .. tostring(sound) .. "'" .. "' without the 'SND_' preface")
        end
        if not IsLoopingSound(sound) then
          emitter:StartRingingSound(sound)
          SoundDebug(tostring(emitter) .. " playing " .. "'" .. tostring(sound) .. "'")
        else
          SoundDebug(tostring(emitter) .. " is attempting to play looping sound'" .. "'" .. tostring(sound) .. "'" .. "' as a ringing sound")
        end
      else
        engine.Warning(tostring(emitter) .. " cannot play '" .. tostring(sound) .. "' because it is not a string")
      end
    else
      SoundDebug(tostring(emitter) .. " cannot play sound that is nil")
    end
  else
    SoundDebug("Emitter is nil or empty")
  end
end
local ReplaySound = function(emitter, sound)
  if emitter ~= nil then
    if sound ~= nil and sound ~= "" then
      if type(sound) == "string" then
        sound = string.lower(sound)
        if string.sub(sound, 1, string.len("snd_")) ~= "snd_" then
          engine.Warning(tostring(emitter) .. " is attempting to play '" .. "'" .. tostring(sound) .. "'" .. "' without the 'SND_' preface")
        end
        if emitter:IsPlaying(sound) then
          emitter:Stop(sound)
          SoundDebug(tostring(emitter) .. " is stopping sound " .. "'" .. tostring(sound) .. "'")
        end
        emitter:Start(sound)
        SoundDebug(tostring(emitter) .. " is playing sound " .. "'" .. tostring(sound) .. "'")
      else
        engine.Warning(tostring(emitter) .. " cannot play '" .. tostring(sound) .. "' because it is not a string")
      end
    else
      SoundDebug(tostring(emitter) .. " cannot play sound that is nil")
    end
  else
    SoundDebug("Emitter is nil or empty")
  end
end
local PlayRestartableSoundLoop = function(emitter, sound)
  if emitter ~= nil then
    if sound ~= nil and sound ~= "" then
      if type(sound) == "string" then
        sound = string.lower(sound)
        if string.sub(sound, 1, string.len("snd_")) ~= "snd_" then
          engine.Warning(tostring(emitter) .. " is attempting to play '" .. "'" .. tostring(sound) .. "'" .. "' without the 'SND_' preface")
        end
        if emitter:IsPlaying(sound) then
          SoundDebug(tostring(emitter) .. " is already playing loop sound " .. "'" .. tostring(sound) .. "'")
        else
          emitter:StartLoop(sound)
          SoundDebug(tostring(emitter) .. " playing " .. "'" .. tostring(sound) .. "'")
        end
      else
        engine.Warning(tostring(emitter) .. " cannot play '" .. tostring(sound) .. "' because it is not a string")
      end
    else
      SoundDebug(tostring(emitter) .. " cannot play sound that is nil")
    end
  else
    SoundDebug("Emitter is nil or empty")
  end
end
local StopSound = function(emitter, sound, ignoreLoopCheck)
  if emitter ~= nil then
    if sound ~= nil and sound ~= "" then
      if type(sound) == "string" then
        if not ignoreLoopCheck and not emitter:IsPlaying(sound) then
          SoundDebug(tostring(emitter) .. " is not playing '" .. tostring(sound) .. "' so it cannot be stopped")
        else
          emitter:Stop(sound)
          SoundDebug(tostring(emitter) .. " stopping " .. "'" .. tostring(sound) .. "'")
        end
      else
        engine.Warning(tostring(emitter) .. " cannot stop sound '" .. tostring(sound) .. "'' because it is not a string")
      end
    else
      SoundDebug(tostring(emitter) .. " cannot stop a sound that is nil")
    end
  else
    SoundDebug("Emitter is nil or empty")
  end
end
local StopRestartableSoundLoop = function(emitter, sound)
  if emitter ~= nil then
    if sound ~= nil and sound ~= "" then
      if type(sound) == "string" then
        if not emitter:IsPlaying(sound) then
          SoundDebug(tostring(emitter) .. " is not playing '" .. tostring(sound) .. "' so it cannot be stopped")
        else
          emitter:StopLoop(sound)
          SoundDebug(tostring(emitter) .. " stopping " .. "'" .. tostring(sound) .. "'")
        end
      else
        engine.Warning(tostring(emitter) .. " cannot stop sound '" .. tostring(sound) .. "'' because it is not a string")
      end
    else
      SoundDebug(tostring(emitter) .. " cannot stop a sound that is nil")
    end
  else
    SoundDebug("Emitter is nil or empty")
  end
end
local PlaySoundAfterDelay = function(emitter, sound, delay)
  if emitter ~= nil and sound ~= nil then
    if delay ~= nil then
      timers_LoadLibrary()
      timers.StartLevelTimer(delay, function()
        PlaySound(emitter, sound)
      end)
    else
      PlaySound(emitter, sound)
    end
  end
end
local StopSoundAfterDelay = function(emitter, sound, delay)
  if emitter ~= nil and sound ~= nil then
    if delay ~= nil then
      timers_LoadLibrary()
      timers.StartLevelTimer(delay, function()
        StopSound(emitter, sound)
      end)
    else
      StopSound(emitter, sound)
    end
  end
end
local PlayLoopingSoundToAnimFrame = function(emitter, anim, loopSound, animFrameToStart, animFrameToStop)
  if emitter ~= nil and anim ~= nil and loopSound ~= nil then
    if not anim.AnimFrame then
      engine.Warning("The anim object: ", anim(" does not seem to have any animation data on it. Please double check that you are referencing the correct object"))
      return
    end
    local emitterAnimMonitor = monitors.CreateAnimFrameMonitor(anim)
    if animFrameToStop == nil or type(animFrameToStop) ~= "number" or animFrameToStop > anim.AnimLengthFrames or animFrameToStop < 1 then
      animFrameToStop = anim.AnimLengthFrames
    else
    end
    if animFrameToStart == nil or animFrameToStart < 1 or type(animFrameToStart) ~= "number" then
      animFrameToStart = 1
      PlaySound(emitter, loopSound)
    else
      emitterAnimMonitor:OnFrameForward(animFrameToStart, function()
        PlaySound(emitter, loopSound)
      end)
      emitterAnimMonitor:OnFrameBackward(animFrameToStop, function()
        PlaySound(emitter, loopSound)
      end)
    end
    emitterAnimMonitor:OnFrameForward(animFrameToStop, function()
      emitterAnimMonitor:Stop()
      StopSound(emitter, loopSound)
      emitterAnimMonitor:Terminate()
      emitterAnimMonitor = nil
    end)
    emitterAnimMonitor:OnFrameBackward(animFrameToStart, function()
      emitterAnimMonitor:Stop()
      StopSound(emitter, loopSound)
      emitterAnimMonitor:Terminate()
      emitterAnimMonitor = nil
    end)
  end
end
local PlaySoundOnFrame = function(emitter, anim, mSound, animFrame, direction, ignoreLoopCheck)
  if emitter ~= nil and anim ~= nil and mSound ~= nil and mSound ~= "" then
    if animFrame ~= nil and type(animFrame) == "number" and 0 < animFrame then
      local emitterAnimMonitor = monitors.CreateAnimFrameMonitor(anim)
      if direction ~= nil and string.lower(direction) == "forward" then
        emitterAnimMonitor:OnFrameForward(animFrame, function()
          PlaySound(emitter, mSound, ignoreLoopCheck)
          emitterAnimMonitor:Stop()
          emitterAnimMonitor:Terminate()
          emitterAnimMonitor = nil
        end)
      elseif direction ~= nil and string.lower(direction) == "backward" then
        emitterAnimMonitor:OnFrameBackward(animFrame, function()
          PlaySound(emitter, mSound, ignoreLoopCheck)
          emitterAnimMonitor:Stop()
          emitterAnimMonitor:Terminate()
          emitterAnimMonitor = nil
        end)
      else
        emitterAnimMonitor:OnFrame(animFrame, function()
          PlaySound(emitter, mSound, ignoreLoopCheck)
          emitterAnimMonitor:Stop()
          emitterAnimMonitor:Terminate()
          emitterAnimMonitor = nil
        end)
      end
      return emitterAnimMonitor
    else
      PlaySound(emitter, mSound, ignoreLoopCheck)
      return nil
    end
  end
  SoundDebug("Setting up sound to play on frame " .. tostring(emitter) .. " Anim: " .. tostring(anim) .. " Sound: " .. tostring(mSound) .. " AnimFrame: " .. tostring(animFrame) .. " Direction: " .. tostring(direction))
end
local StopSoundOnFrame = function(emitter, anim, sound, animFrame, direction, ignoreLoopCheck, fn)
  if emitter ~= nil and anim ~= nil and sound ~= nil and sound ~= "" then
    if animFrame ~= nil and type(animFrame) == "number" then
      local emitterAnimMonitor = monitors.CreateAnimFrameMonitor(anim)
      if direction ~= nil and string.lower(direction) == "forward" then
        if animFrame < 2 then
          animFrame = anim.AnimLengthFrames - 1
        end
        emitterAnimMonitor:OnFrameForward(animFrame, function()
          StopSound(emitter, sound, ignoreLoopCheck)
          if fn ~= nil then
            fn()
          end
          emitterAnimMonitor:Stop()
          emitterAnimMonitor:Terminate()
          emitterAnimMonitor = nil
        end)
      elseif direction ~= nil and string.lower(direction) == "backward" then
        if animFrame < 1 then
          animFrame = 1
        end
        emitterAnimMonitor:OnFrameBackward(animFrame, function()
          StopSound(emitter, sound, ignoreLoopCheck)
          if fn ~= nil then
            fn()
          end
          emitterAnimMonitor:Stop()
          emitterAnimMonitor:Terminate()
          emitterAnimMonitor = nil
        end)
      else
        emitterAnimMonitor:OnFrame(animFrame, function()
          StopSound(emitter, sound, ignoreLoopCheck)
          if fn ~= nil then
            fn()
          end
          emitterAnimMonitor:Stop()
          emitterAnimMonitor:Terminate()
          emitterAnimMonitor = nil
        end)
      end
      return emitterAnimMonitor
    else
      StopSound(emitter, sound, ignoreLoopCheck)
      if fn ~= nil then
        fn()
      end
      return nil
    end
  end
  SoundDebug("Stop Emitter: " .. tostring(emitter) .. " Anim: " .. tostring(anim) .. " Sound: " .. tostring(sound) .. " AnimFrame: " .. tostring(animFrame) .. " Direction: " .. tostring(direction))
end
local TrackEmitterOnLine = function(vectorTable, goToTrack, goWithEmitter, maxDistance)
  if vectorTable ~= nil and type(vectorTable) == "table" then
    if goToTrack ~= nil and goToTrack:GetWorldPosition() ~= nil then
      if goWithEmitter ~= nil and goWithEmitter:GetWorldPosition() ~= nil then
        maxDistance = maxDistance or 20
        local shortestDistance, shortestPoint
        if 0 < #vectorTable then
          local position = goToTrack:GetWorldPosition()
          for i = 1, #vectorTable do
            local distance = (vectorTable[i] - position):Length()
            if maxDistance > distance then
              if shortestDistance ~= nil then
                if shortestDistance > distance then
                  shortestDistance = distance
                  shortestPoint = vectorTable[i]
                end
              else
                shortestDistance = distance
                shortestPoint = vectorTable[i]
              end
            end
          end
          if shortestPoint ~= nil and tostring(shortestPoint) ~= tostring(goWithEmitter:GetWorldPosition()) then
            goWithEmitter:SetWorldPosition(shortestPoint)
            SoundDebug("Closest point in vector table to " .. tostring(goToTrack) .. " is " .. tostring(shortestPoint) .. " | " .. tostring(goWithEmitter) .. " position is: " .. tostring(goWithEmitter:GetWorldPosition()))
          end
        end
      else
        engine.Warning("Game Object with Emitter is either nil or not a GameObject")
      end
    else
      engine.Warning("Game Object to be followed by SoundEmitter is either nil or not a Game Object")
    end
  else
    engine.Warning("Vector Table in TrackEmitterOnLine function is either nil or not a table")
  end
end
local StartCombatMusic = function(startMusic, endMusic)
  if startMusic == nil or endMusic == nil then
    engine.Warning("You must pass both 'StartMusic' and 'EndMusic' into AudioLibrary.StartCombatMusic( startMusic, endMusic )")
  end
  game.Player.FindPlayer():CallScript("EvaluateCombatMusicStart", startMusic, endMusic)
end
local StopCombatMusic = function()
  game.Player.FindPlayer():CallScript("EvaluateCombatMusicEnd")
end
local DisableCombatBanter = function()
  game.Audio.SetBanterFact("DisableSpawnCallouts", true)
end
local EnableCombatBanter = function()
  game.Audio.SetBanterFact("DisableSpawnCallouts", false)
end
return profile.WrapLibrary({
  PlaySound = PlaySound,
  StopSound = StopSound,
  ReplaySound = ReplaySound,
  PlayLoopingSoundToAnimFrame = PlayLoopingSoundToAnimFrame,
  PlaySoundAfterDelay = PlaySoundAfterDelay,
  StopSoundAfterDelay = StopSoundAfterDelay,
  PlaySoundOnFrame = PlaySoundOnFrame,
  StopSoundOnFrame = StopSoundOnFrame,
  TrackEmitterOnLine = TrackEmitterOnLine,
  SoundDebug = SoundDebug,
  IsLoopingSound = IsLoopingSound,
  StartCombatMusic = StartCombatMusic,
  StopCombatMusic = StopCombatMusic,
  DisableCombatBanter = DisableCombatBanter,
  EnableCombatBanter = EnableCombatBanter,
  PlayRestartableSoundLoop = PlayRestartableSoundLoop,
  StopRestartableSoundLoop = StopRestartableSoundLoop,
  PlaySoundRingOut = PlaySoundRingOut
})
