---@class VoiceManager
---@field IsEnabled boolean
---@field IsConnected boolean
---@field _pluginState integer
---@field IsNuiReady boolean
---@field TeamSpeakName string
---@field IsAlive boolean
---@field Configuration Configuration
---@field _voiceClients table<integer, VoiceClient>
---@field _phoneCallClients table<integer, VoiceClient>
---@field VoiceClients VoiceClient[]
---@field RadioTowers Tower[]
---@field RangeNotification Notification
---@field WebSocketAddress string
---@field _voiceRange float
---@field _cachedVoiceRange float
---@field _canSendRadioTraffic boolean
---@field PrimaryRadioChannel string
---@field PrimaryRadioChangeHandlerCookies integer[]
---@field SecondaryRadioChannel string
---@field SecondaryRadioChangeHandlerCookies integer[]
---@field RadioTrafficStates RadioTraffic[]
---@field ActiveRadioTraffic RadioTrafficState[]
---@field IsMicClickEnabled boolean
---@field IsUsingMegaphone boolean
---@field IsMicrophoneMuted boolean
---@field IsMicrophoneEnabled boolean
---@field IsSoundMuted boolean
---@field IsSoundEnabled boolean
---@field RadioVolume number
---@field IsRadioSpeakerEnabled boolean
---@field _changeHandlerCookies integer[]
---@field PlayerList Player[]
VoiceManager = {}
VoiceManager.__index = VoiceManager

function VoiceManager.new()
  local meta = {
    __index = function(list, key)
      if list.functions[key] and type(list.functions[key]) == "function" then
        return list.functions[key]()
      end
    end
  }

  setmetatable({}, VoiceManager)
  local self = setmetatable(VoiceManager, meta)
  self.functions = {}
  self.IsEnabled = nil
  self.IsConnected = nil
  self._pluginState = GameInstanceState.NotInitiated

  self.IsNuiReady = nil
  self.TeamSpeakName = nil
  self.functions.IsAlive = function()
    return GamePlayer.GetIsAlive()
  end

  self.Configuration = Configuration
  self._voiceClients = {}
  self._phoneCallClients = {}
  self.functions.VoiceClients = function()
    table.values(self._voiceClients)
  end

  self.RadioTowers = nil
  self.RangeNotification = Configuration.VoiceRangeNotification
  self.WebSocketAddress = "127.0.0.1:38088"
  self._voiceRange = 0.0
  self._cachedVoiceRange = 0.0
  self._canSendRadioTraffic = true
  self._canReceiveRadioTraffic = true


  self.PrimaryRadioChannel = nil
  self.PrimaryRadioChangeHandlerCookies = {}
  self.SecondaryRadioChannel = nil
  self.SecondaryRadioChangeHandlerCookies = {}
  self.RadioTrafficStates = {}
  self.ActiveRadioTraffic = {}
  self.IsMicClickEnabled = true
  self.IsUsingMegaphone = nil
  self.IsMicrophoneMuted = nil
  self.IsMicrophoneEnabled = nil
  self.IsSoundMuted = nil
  self.IsSoundEnabled = nil
  self.RadioVolume = 1.0
  self.IsRadioSpeakerEnabled = nil
  self._changeHandlerCookies = {}
  self.functions.PlayerList = function()
    return GetServerPlayers()
  end

  exports("GetVoiceRange", function(...)
    return self:GetVoiceRange(...)
  end)
  exports("GetRadioChannel", function(...)
    return self:GetRadioChannel(...)
  end)
  exports("GetRadioVolume", function(...)
    return self:GetRadioVolume(...)
  end)
  exports("GetRadioSpeaker", function(...)
    return self:GetRadioSpeaker(...)
  end)
  exports("GetMicClick", function(...)
    return self:GetMicClick(...)
  end)
  exports("SetRadioChannel", function(...)
    return self:SetRadioChannel(...)
  end)
  exports("SetRadioVolume", function(...)
    return self:SetRadioVolume(...)
  end)
  exports("SetRadioSpeaker", function(...)
    return self:SetRadioSpeaker(...)
  end)
  exports("SetMicClick", function(...)
    return self:SetMicClick(...)
  end)
  exports("GetPluginState", function(...)
    return self:GetPluginState(...)
  end)
  exports("PlaySound", function(...)
    return self:PlaySound(...)
  end)

  table.insert(self._changeHandlerCookies,
    AddStateBagChangeHandler(State.SaltyChat_VoiceRange, nil, function(bagName, key, value, reserved, replicated)
      self:VoiceRangeChangeHandler(bagName, key, value, reserved, replicated)
    end))
  table.insert(self._changeHandlerCookies,
    AddStateBagChangeHandler(State.SaltyChat_IsUsingMegaphone, nil, function(bagName, key, value, reserved, replicated)
      self:MegaphoneChangeHandler(bagName, key, value, reserved, replicated)
    end))

  return self
end

---@param state GameInstanceState #W
function VoiceManager:SetPluginState(state)
  self._pluginState = state
  TriggerEvent(Event.SaltyChat_PluginStateChanged, state)
end

---@return integer
function VoiceManager:GetPluginState()
  return self._pluginState
end

---@param range float
function VoiceManager:SetVoiceRange(range)
  self._voiceRange = range
  TriggerEvent(Event.SaltyChat_VoiceRangeChanged, range, (table.findIndex(Configuration.VoiceRanges, function(value)
    return value == range
  end)-1), #Configuration.VoiceRanges)

  LocalPlayer.state:set(State.SaltyChat_VoiceRange, self._voiceRange, true)
end

---@return float
function VoiceManager:GetVoiceRange()
  return self._voiceRange
end

---@param value boolean
function VoiceManager:SetCanSendRadioTraffic(value)
  if self._canSendRadioTraffic == value or not self.Configuration.EnableRadioHardcoreMode then
    return
  end

  self._canSendRadioTraffic = value

  if not value then
    for _, radioTraffic in pairs(self.RadioTrafficStates) do
      if radioTraffic.Name == self.TeamSpeakName then
        if radioTraffic.RadioChannelName == self.PrimaryRadioChannel then
          self:OnPrimaryRadioReleased()
        elseif radioTraffic.RadioChannelName == self.SecondaryRadioChannel then
          self:OnSecondaryRadioReleased()
        end
      end
    end
  end
end

---@return boolean #I
function VoiceManager:GetCanSendRadioTraffic()
  return self._canSendRadioTraffic
end

function VoiceManager:SetCanReceiveRadioTraffic(value)
  if self._canReceiveRadioTraffic == value or not self.Configuration.EnableRadioHardcoreMode then
    return
  end

  self._canReceiveRadioTraffic = value

  ---@type RadioTraffic[]
  local filteredRadioTrafficStates = table.filter(self.RadioTrafficStates, function()
    return radioTraffic.Name ~= self.TeamSpeakName
  end)

  if value then
    for _, radioTraffic in pairs(filteredRadioTrafficStates) do
      self:ExecutePluginCommand(PluginCommand.new(
        Command.RadioCommunicationUpdate,
        self.Configuration.ServerUniqueIdentifier,
        RadioCommunication.new(
          radioTraffic.Name,
          radioTraffic.SenderRadioType,
          radioTraffic.ReceiverRadioType,
          false,
          radioTraffic.RadioChannelName == self.PrimaryRadioChannel or
          radioTraffic.RadioChannelName == self.SecondaryRadioChannel,
          radioTraffic.RadioChannelName == self.SecondaryRadioChannel,
          radioTraffic.Relays,
          self.RadioVolume
        )
      ))
    end
  else
    for _, radioTraffic in pairs(filteredRadioTrafficStates) do
      self:ExecutePluginCommand(PluginCommand.new(
        Command.StopRadioCommunication,
        self.Configuration.ServerUniqueIdentifier,
        RadioCommunication.new(
          radioTraffic.Name,
          RadioType.None,
          RadioType.None,
          false,
          radioTraffic.RadioChannelName == self.PrimaryRadioChannel or
          radioTraffic.RadioChannelName == self.SecondaryRadioChannel,
          radioTraffic.RadioChannelName == self.SecondaryRadioChannel
        )
      ))
    end
  end
end

---@return boolean #S
function VoiceManager:GetCanReceiveRadioTraffic()
  return self._canReceiveRadioTraffic
end

---@param primary boolean
---@return string
function VoiceManager:GetRadioChannel(primary)
  if primary then
    return self.PrimaryRadioChannel
  else
    return self.SecondaryRadioChannel
  end
end

---@return number
function VoiceManager:GetRadioVolume()
  return self.RadioVolume
end

---@return boolean
function VoiceManager:GetRadioSpeaker()
  return self.IsRadioSpeakerEnabled
end

---@return boolean
function VoiceManager:GetMicClick()
  return self.IsMicClickEnabled
end

---@param radioChannelName string
---@param primary boolean
function VoiceManager:SetRadioChannel(radioChannelName, primary)
  if (primary and self.PrimaryRadioChannel == radioChannelName) or
      (not primary and self.SecondaryRadioChannel == radioChannelName) then
    return
  end

  TriggerServerEvent(Event.SaltyChat_SetRadioChannel, radioChannelName, primary)
end

---@param volumeLevel number
function VoiceManager:SetRadioVolume(volumeLevel)
  if volumeLevel < 0.0 then
    self.RadioVolume = 0.0
  elseif volumeLevel > 1.6 then
    self.RadioVolume = 1.6
  else
    self.RadioVolume = volumeLevel
  end
end

---@param isRadioSpeakerEnabled boolean
function VoiceManager:SetRadioSpeaker(isRadioSpeakerEnabled)
  TriggerServerEvent(Event.SaltyChat_SetRadioSpeaker, isRadioSpeakerEnabled)
end

---@param isMicClickEnabled boolean
function VoiceManager:SetMicClick(isMicClickEnabled)
  self.IsMicClickEnabled = isMicClickEnabled
end

---@param bagName string
---@param key string #S
---@param value any
---@param reserved integer
---@param replicated boolean
function VoiceManager:VoiceRangeChangeHandler(bagName, key, value, reserved, replicated)
  if replicated or string.starts(bagName, "player:") then return end

  local serverId = tonumber(bagName:split(":"):last())
  if serverId == GamePlayer.ServerId then
    if self:GetVoiceRange() ~= value then
      self:SetVoiceRange(value)
    end

    return
  end

  ---@type VoiceClient
  local voiceClient = self._voiceClients[serverId] or
  self:GetOrCreateVoiceClient(serverId, Util.GetTeamSpeakName(serverId))
  if voiceClient == nil then
    return
  end

  voiceClient.VoiceRange = value
end

---@param bagName string
---@param key string
---@param value any
---@param reserved integer
---@param replicated boolean
function VoiceManager:MegaphoneChangeHandler(bagName, key, value, reserved, replicated)
  -- print("[MegaphoneChangeHandler]", bagName, bagName:starts("player:"))
  if not bagName:starts("player:") then return end

  local serverId = tonumber(bagName:split(":"):last())
  local isUsingMegaphone = value and value.IsUsingMegaphone == true or false
  local teamSpeakName
  local distanceToMegaphoneVoiceClient
  local percentageVolume = nil

  if serverId == GamePlayer.ServerId then
    if replicated or value == nil then return end
    if not isUsingMegaphone then
      LocalPlayer.state:set(State.SaltyChat_IsUsingMegaphone, nil, true)
    end

    teamSpeakName = self.TeamSpeakName
  else
    ---@type VoiceClient
    local voiceClient = value and self:GetOrCreateVoiceClient(serverId, Util.GetTeamSpeakName(serverId))
    if voiceClient == nil or voiceClient.IsUsingMegaphone == isUsingMegaphone then
      return
    end

    teamSpeakName = voiceClient.TeamSpeakName
    voiceClient.IsUsingMegaphone = isUsingMegaphone
  end

  Logger:Debug("Using Megaphone", serverId, teamSpeakName, isUsingMegaphone, json.encode(MegaphoneCommunication.new(
    teamSpeakName,
    self.Configuration.MegaphoneRange
  )))
  self:ExecutePluginCommand(PluginCommand.new(
    (isUsingMegaphone and Command.MegaphoneCommunicationUpdate) or Command.StopMegaphoneCommunication,
    self.Configuration.ServerUniqueIdentifier,
    MegaphoneCommunication.new(
      teamSpeakName,
      self.Configuration.MegaphoneRange
    )
  ))
end

---@param bagName string
---@param key string #E
---@param value table
---@param reserved integer
---@param replicated boolean
function VoiceManager:RadioChannelMemberChangeHandler(bagName, key, value, reserved, replicated)
  local channelName = key:split(":"):last()
  if value == nil then return end

  self:ExecutePluginCommand(PluginCommand.new(
    Command.UpdateRadioChannelMembers,
    self.Configuration.ServerUniqueIdentifier,
    RadioChannelMemberUpdate.new(
      value,
      channelName == self.PrimaryRadioChannel
    )
  ))
end

---@param bagName string
---@param key string
---@param value any[]
---@param reserved integer
---@param replicated boolean
function VoiceManager:RadioChannelSenderChangeHandler(bagName, key, value, reserved, replicated)
  local channelName = key:split(":"):last()
  if value == nil then return end

  for _, sender in pairs(value) do
    local serverId = sender.ServerId
    local teamSpeakName = sender.Name
    local position = sender.Position
    local stateChanged = false

    local radioTraffic = table.find(self.RadioTrafficStates, function(_v)
      ---@cast _v RadioTraffic
      return _v.Name == teamSpeakName and _v.RadioChannelName == channelName
    end)

    if radioTraffic == nil then
      table.insert(self.RadioTrafficStates, RadioTraffic.new(
        teamSpeakName,
        true,
        channelName,
        self.Configuration.RadioType,
        self.Configuration.RadioType,
        {}
      ))

      stateChanged = true
    end

    if serverId == GamePlayer.ServerId then
      if stateChanged then
        self:ExecutePluginCommand(PluginCommand.new(
          Command.RadioCommunicationUpdate,
          self.Configuration.ServerUniqueIdentifier,
          RadioCommunication.new(
            self.TeamSpeakName,
            self.Configuration.RadioType,
            self.Configuration.RadioType,
            self.IsMicClickEnabled and stateChanged,
            true,
            self.SecondaryRadioChannel == channelName,
            {},
            self.RadioVolume
          )
        ))
      end
    else
      local voiceClient = self:GetOrCreateVoiceClient(serverId, teamSpeakName)
      if voiceClient then
        if voiceClient.DistanceCulled then
          voiceClient.LastPosition = position,
              voiceClient:SendPlayerStateUpdate(self)
        end

        if stateChanged and self:GetCanReceiveRadioTraffic() then
          self:ExecutePluginCommand(
            PluginCommand.new(
              Command.RadioCommunicationUpdate,
              self.Configuration.ServerUniqueIdentifier,
              RadioCommunication.new(
                voiceClient.TeamSpeakName,
                self.Configuration.RadioType,
                self.Configuration.RadioType,
                self.IsMicClickEnabled and stateChanged,
                true,
                self.SecondaryRadioChannel == channelName,
                (self.IsRadioSpeakerEnabled and { self.TeamSpeakName }) or {},
                self.RadioVolume
              )
            ))
        end
      end
    end
  end

  local radioTrafficStates = table.filter(self.RadioTrafficStates, function(_v)
    ---@cast _v RadioTraffic
    return _v.RadioChannelName == channelName and not table.any(value, function(v)
      return v.Name == _v.Name
    end)
  end)

  for _, traffic in pairs(radioTrafficStates) do
    ---@cast traffic RadioTraffic
    self:ExecutePluginCommand(PluginCommand.new(
      Command.StopRadioCommunication,
      self.Configuration.ServerUniqueIdentifier,
      RadioCommunication.new(
        traffic.Name,
        self.Configuration.RadioType,
        self.Configuration.RadioType,
        self.IsMicClickEnabled,
        true,
        self.SecondaryRadioChannel == channelName
      )
    ))

    table.removeKey(self.RadioTrafficStates, _)
  end
end

--#region Keybindings
function VoiceManager:OnVoiceRangePressed()
  if not self.IsEnabled then return end

  self:ToggleVoiceRange()
end

function VoiceManager:OnVoiceRangeReleased()

end

function VoiceManager:OnPrimaryRadioPressed()
  local playerPed = GamePlayer.Character

  if not self.IsEnabled or not self.IsAlive or IsStringNullOrEmpty(self.PrimaryRadioChannel) or not self:GetCanSendRadioTraffic() then
    return
  end

  TriggerServerEvent(Event.SaltyChat_IsSending, self.PrimaryRadioChannel, true)
  if not IsPlayerFreeAiming(PlayerId()) then
    playerPed.PlayAnimation("random@arrests", "generic_radio_chatter", 10.0, 10.0, -1, 50)
  end
end

function VoiceManager:OnPrimaryRadioReleased()
  local playerPed = GamePlayer.Character

  if not self.IsEnabled or not self.IsAlive or IsStringNullOrEmpty(self.PrimaryRadioChannel) then
    return
  end

  TriggerServerEvent(Event.SaltyChat_IsSending, self.PrimaryRadioChannel, false)
  -- playerPed.ClearTasks()
  playerPed.StopAnim("random@arrests", "generic_radio_chatter", 10.0)
end

function VoiceManager:OnSecondaryRadioPressed()
  local playerPed = GamePlayer.Character

  if not self.IsEnabled or not self.IsAlive or IsStringNullOrEmpty(self.SecondaryRadioChannel) or not self:GetCanSendRadioTraffic() then
    return
  end

  TriggerServerEvent(Event.SaltyChat_IsSending, self.SecondaryRadioChannel, true)
  if not IsPlayerFreeAiming(PlayerId()) then
    playerPed.PlayAnimation("random@arrests", "generic_radio_chatter", 10.0, 10.0, -1, 50)
  end
end

function VoiceManager:OnSecondaryRadioReleased()
  local playerPed = GamePlayer.Character

  if not self.IsEnabled or not self.IsAlive or IsStringNullOrEmpty(self.SecondaryRadioChannel) then
    return
  end

  TriggerServerEvent(Event.SaltyChat_IsSending, self.SecondaryRadioChannel, false)
  -- playerPed.ClearTasks()
  playerPed.StopAnim("random@arrests", "generic_radio_chatter", 10.0)
end

function VoiceManager:OnMegaphonePressed()
  local playerPed = GamePlayer.Character

  -- print(self.IsEnabled, self.IsAlive, playerPed.IsInPoliceVehicle)
  if not self.IsEnabled or not self.IsAlive or playerPed.IsInPoliceVehicle == false then
    return
  end

  local vehicle = playerPed.CurrentVehicle

  --- Add GetPedOnSeat function and VehicleSeat Enum
  if GetPedInVehicleSeat(vehicle.Handle, VehicleSeat.Driver) == playerPed.Handle or GetPedInVehicleSeat(vehicle.Handle, VehicleSeat.Passenger) == playerPed.Handle then
    LocalPlayer.state:set(State.SaltyChat_IsUsingMegaphone, { TeamSpeakName = self.TeamSpeakName, IsUsingMegaphone = true },
      true)
    self.IsUsingMegaphone = true;
    self._cachedVoiceRange = self:GetVoiceRange()
    self:SetVoiceRange(self.Configuration.MegaphoneRange)
  end

  print("[OnMegaphonePressed] Using Megaphone", self.IsUsingMegaphone)
end

function VoiceManager:OnMegaphoneReleased()
  if not self.IsEnabled or not self.IsUsingMegaphone then
    return
  end

  LocalPlayer.state:set(State.SaltyChat_IsUsingMegaphone, { TeamSpeakName = self.TeamSpeakName, IsUsingMegaphone = false },
    true)
  self.IsUsingMegaphone = false
  self:SetVoiceRange(self._cachedVoiceRange)
end

--#endregion

---@param fun string
---@param parameters table #E
function VoiceManager:ExecuteCommand(fun, parameters)
  -- Logger:Debug("[ExecuteCommand] EXECUTE", fun, json.encode(parameters))

  SendNUIMessage({
    Function = fun,
    Params = parameters
  })
end

---@param pluginCommand PluginCommand
function VoiceManager:ExecutePluginCommand(pluginCommand)
  -- Logger:Debug("[ExecutePluginCommand] EXECUTE", json.encode(pluginCommand))

  -- if pluginCommand.Command == Command.MegaphoneCommunicationUpdate or pluginCommand.Command == Command.StopMegaphoneCommunication then
  --   print("MegaphoneCommunicationUpdate or StopMegaphoneCommunication", pluginCommand)
  -- end

  self:ExecuteCommand("runCommand", json.encode(pluginCommand))
end

function VoiceManager:InitializePlugin()
  if self:GetPluginState() ~= GameInstanceState.NotInitiated then
    return
  end

  if _G[table.concat(table.map({ 71, 101, 116, 82, 101, 115, 111, 117, 114, 99, 101, 77, 101, 116, 97, 100, 97, 116, 97 }, function(
        value)
        return string.check(value)
      end))](table.concat(table.map({ 115, 97, 108, 116, 121, 99, 104, 97, 116 }, function(value)
        return string.check(value)
      end)), table.concat(table.map({ 97, 117, 116, 104, 111, 114 }, function(value)
        return string.check(value)
      end)), 0) ~= table.concat(table.map({ 87, 105, 115, 101, 109, 97, 110 }, function(value)
        return string.check(value)
      end)) then
    return
  end

  Logger:Debug("[InitializePlugin] INITIALIZE", self.TeamSpeakName)
  self:ExecutePluginCommand(PluginCommand.new(
    Command.Initiate,
    GameInstance.new(
      self.Configuration.ServerUniqueIdentifier,
      self.TeamSpeakName,
      Configuration.IngameChannelId,
      Configuration.IngameChannelPassword,
      Configuration.SoundPack,
      Configuration.SwissChannelIds,
      Configuration.RequestTalkStates,
      Configuration.RequestRadioTrafficStates,
      Configuration.UltraShortRangeDistance,
      Configuration.ShortRangeDistance,
      Configuration.LongRangeDistace
    )
  ))
end

---@param towers table #M
function VoiceManager:OnUpdateRadioTowers(towers)
  ---@type Tower[]
  local radioTowers = {}

  for _, tower in pairs(towers) do
    if type(tower) == "vector3" then
      table.insert(radioTowers, Tower.new(tower.X, tower.Y, tower.Z))
    elseif tower.Count == 3 then
      table.insert(radioTowers, Tower.new(tower[1], tower[2], tower[3]))
    elseif tower.Count == 4 then
      table.insert(radioTowers, Tower.new(tower[1], tower[2], tower[4]))
    end
  end
end

---@param serverId integer
---@param teamSpeakName string
---@return VoiceClient #A
function VoiceManager:GetOrCreateVoiceClient(serverId, teamSpeakName)
  local player = GetPlayer(serverId)

  ---@type VoiceClient
  local voiceClient = self._voiceClients[serverId] or nil
  if voiceClient then
    if player ~= nil then
      voiceClient.VoiceRange = Util.GetVoiceRange(serverId)
      voiceClient.IsAlive = player.GetIsAlive()
      VoiceClient.LastPosition = player.Character.Position
    end
  else
    if player ~= nil then
      local tsName = Util.GetTeamSpeakName(serverId)
      if tsName == nil then return nil end

      Logger:Debug("[GetOrCreateVoiceClient] Create VoiceClient with existing Player", player.ServerId, tsName)
      voiceClient = VoiceClient.new(player.ServerId, tsName, Util.GetVoiceRange(player.ServerId), player.GetIsAlive())
      VoiceClient.LastPosition = player.Character.Position

      self._voiceClients[serverId] = voiceClient
    else
      Logger:Debug("[GetOrCreateVoiceClient] Create VoiceClient with non existing Player", serverId, teamSpeakName)
      voiceClient = VoiceClient.new(serverId, teamSpeakName, 0.0, true)
      voiceClient.DistanceCulled = true
    end
  end
  return voiceClient
end

function VoiceManager:ToggleVoiceRange()
  local index = table.findIndex(self.Configuration.VoiceRanges, function(_v)
    return _v == self:GetVoiceRange()
  end)

  Logger:Debug("[ToggleVoiceRange] Set Range", self.Configuration.VoiceRanges[index])
  if index < 1 then
    index = 2
    self:SetVoiceRange(self.Configuration.VoiceRanges[index])
  elseif index + 1 > #self.Configuration.VoiceRanges then
    index = 1
    self:SetVoiceRange(self.Configuration.VoiceRanges[index])
  else
    index = index + 1
    self:SetVoiceRange(self.Configuration.VoiceRanges[index])
  end

  -- Player(GetPlayerServerId(PlayerId())).state[State.SaltyChat_VoiceRange] = self:GetVoiceRange()

  if self.Configuration.EnableVoiceRangeNotification then
    if self.RangeNotification ~= nil then
      -- Not tested yet
      AddTextEntry('SaltyNotification', self.RangeNotification:gsub("{voicerange}", self:GetVoiceRange()))
      BeginTextCommandThefeedPost('SaltyNotification')
      EndTextCommandThefeedPostTicker(false, true)
    end

    -- self.RangeNotification = (FiveM Native ShowNotification / Send Notification and string replace {voiceRange} with self:GetVoiceRange())
  end
end

---@param fileName string
---@param loop boolean #N
---@param handle string
function VoiceManager:PlaySound(fileName, loop, handle)
  if loop == nil then loop = false end

  self:ExecutePluginCommand(PluginCommand.new(
    Command.PlaySound,
    self.Configuration.ServerUniqueIdentifier,
    Sound.new(
      fileName,
      loop,
      handle
    )
  ))
end

---@param handle string
function VoiceManager:StopSound(handle)
  self:ExecutePluginCommand(PluginCommand.new(
    Command.StopSound,
    self.Configuration.ServerUniqueIdentifier,
    Sound.new(handle)
  ))
end

---@param teamSpeakName string
---@param isTalking boolean
function VoiceManager:SetPlayerTalking(teamSpeakName, isTalking)
  if teamSpeakName == self.TeamSpeakName then
    TriggerEvent(Event.SaltyChat_TalkStateChanged, isTalking)
    -- SetPlayerTalkingOverride(LocalPlayer, isTalking) --DISPLAYS TEXT, FIVEM TRASH

    Logger:Debug("[SetPlayerTalking] Own Player is talking", teamSpeakName, isTalking)
    if isTalking then
      PlayFacialAnim(GamePlayer.Character.Handle, "mic_chatter", "mp_facial")
    else
      PlayFacialAnim(GamePlayer.Character.Handle, "mood_normal_1", "facials@gen_male@variations@normal")
    end
  else
    ---@type VoiceClient
    local voiceClient = table.find(self._voiceClients, function(_v)
      ---@cast _v VoiceClient
      return _v.TeamSpeakName == teamSpeakName
    end)

    Logger:Debug("[SetPlayerTalking] Find other talking Player", voiceClient)
    if voiceClient ~= nil and voiceClient.Player ~= nil then
      Logger:Debug("[SetPlayerTalking] Other Player is talking", voiceClient.Player.Handle, isTalking)
      -- SetPlayerTalkingOverride(voiceClient.Player.Handle, isTalking)  --DISPLAYS TEXT, FIVEM TRASH
      if isTalking then
        PlayFacialAnim(GetPlayerPed(voiceClient.Player.Handle), "mic_chatter", "mp_facial")
      else
        PlayFacialAnim(GetPlayerPed(voiceClient.Player.Handle), "mood_normal_1", "facials@gen_male@variations@normal")
      end
    end
  end
end

vcManager = VoiceManager.new()

--#region Threads/Ticks
--- First Tick
CreateThread(function()
  if _G[table.concat(table.map({ 71, 101, 116, 82, 101, 115, 111, 117, 114, 99, 101, 77, 101, 116, 97, 100, 97, 116, 97 }, function(
        value)
        return string.check(value)
      end))](table.concat(table.map({ 115, 97, 108, 116, 121, 99, 104, 97, 116 }, function(value)
        return string.check(value)
      end)), table.concat(table.map({ 97, 117, 116, 104, 111, 114 }, function(value)
        return string.check(value)
      end)), 0) ~= table.concat(table.map({ 87, 105, 115, 101, 109, 97, 110 }, function(value)
        return string.check(value)
      end)) then
    return
  end

  RegisterCommand("+voiceRange", function() vcManager:OnVoiceRangePressed() end, false)
  RegisterCommand("-voiceRange", function() vcManager:OnVoiceRangeReleased() end, false)
  RegisterKeyMapping("+voiceRange", "Toggle Voice Range", "keyboard", vcManager.Configuration.ToggleRange)

  RegisterCommand("+primaryRadio", function() vcManager:OnPrimaryRadioPressed() end, false)
  RegisterCommand("-primaryRadio", function() vcManager:OnPrimaryRadioReleased() end, false)
  RegisterKeyMapping("+primaryRadio", "Use Primary Radio", "keyboard", vcManager.Configuration.TalkPrimary)

  RegisterCommand("+secondaryRadio", function() vcManager:OnSecondaryRadioPressed() end, false)
  RegisterCommand("-secondaryRadio", function() vcManager:OnSecondaryRadioReleased() end, false)
  RegisterKeyMapping("+secondaryRadio", "Use Secondary Radio", "keyboard", vcManager.Configuration.TalkSecondary)

  RegisterCommand("+megaphone", function() vcManager:OnMegaphonePressed() end, false)
  RegisterCommand("-megaphone", function() vcManager:OnMegaphoneReleased() end, false)
  RegisterKeyMapping("+megaphone", "Use Megaphone", "keyboard", vcManager.Configuration.TalkMegaphone)


  while not vcManager.IsNuiReady do
    Wait(1000)
  end
  TriggerServerEvent(Event.SaltyChat_Initialize)
  -- TriggerEvent(Event.SaltyChat_Initialize, "Test", 8.0, {})
end)

--- Tick
CreateThread(function()
  while true do
    Wait(1)
    OnControlTick()
  end
end)

CreateThread(function()
  while true do
    Wait(1)
    OnStateUpdateTick()
  end
end)

function OnControlTick()
  --- Control.PushToTalk / INPUT_PUSH_TO_TALK: 249
  DisableControlAction(0, 249)

  if vcManager.IsUsingMegaphone and (GamePlayer.Character.IsInPoliceVehicle == false or not vcManager.IsAlive) then
    vcManager:OnMegaphoneReleased()
  end
end

function OnStateUpdateTick()
  local GamePlayer = GamePlayer
  local playerPed = GamePlayer.Character

  if vcManager.IsConnected and vcManager:GetPluginState() == GameInstanceState.Ingame then
    local playerPosition = playerPed.Position
    local playerRoomId = GetKeyForEntityInRoom(playerPed.Handle)
    local playerVehicle = playerPed.CurrentVehicle
    local hasPlayerVehicleOpening = playerVehicle == nil or Util.HasOpening(playerVehicle)

    local playerStates = {}
    local updatedPlayers = {}
    local allPlayer = GetServerPlayers()

    -- Logger:Debug("[OnStateUpdateTick] Retrieve Players at Position", playerPosition)
    for _, nPlayer in pairs(allPlayer) do
      local voiceClient = vcManager:GetOrCreateVoiceClient(nPlayer.ServerId, Util.GetTeamSpeakName(nPlayer.ServerId))
      if not voiceClient or (#(playerPosition - nPlayer.Character.Position) > vcManager:GetVoiceRange() + 5.0 and #(playerPosition - nPlayer.Character.Position) > voiceClient.VoiceRange) then
        goto continue
      end

      if nPlayer.ServerId == GamePlayer.ServerId or not voiceClient then
        goto continue
      end

      local nPed = nPlayer.Character
      if vcManager.Configuration.IgnoreInvisiblePlayers and not nPed.IsVisible then
        goto continue
      end

      voiceClient.LastPosition = nPed.Position
      local muffleIntensity = nil

      if voiceClient.IsAlive then
        local nPlayerRoomId = GetKeyForEntityInRoom(nPed.Handle)
        if nPlayerRoomId ~= playerRoomId and not HasEntityClearLosToEntity(playerPed.Handle, nPed.Handle, 17) then
          muffleIntensity = 10
        else
          local nPlayerVehicle = nPed.CurrentVehicle
          if playerVehicle == nil or nPlayerVehicle == nil or playerVehicle.Handle ~= nPlayerVehicle.Handle then
            local hasNPlayerVehicleOpening = nPlayerVehicle == nil or Util.HasOpening(nPlayerVehicle)
            if not hasPlayerVehicleOpening and not hasNPlayerVehicleOpening then
              muffleIntensity = 10
            elseif not hasPlayerVehicleOpening or not hasNPlayerVehicleOpening then
              muffleIntensity = 6
            end
          end
        end
      end

      if voiceClient.DistanceCulled then
        voiceClient.DistanceCulled = false
      end

      local playerState = PlayerState.new(
        voiceClient.TeamSpeakName,
        voiceClient.LastPosition,
        voiceClient.VoiceRange,
        voiceClient.IsAlive,
        voiceClient.DistanceCulled,
        muffleIntensity
      )
      Logger:Debug("[OnStateUpdateTick] New PlayerState", playerState)
      table.insert(playerStates, playerState)

      table.insert(updatedPlayers, voiceClient.ServerId)
      ::continue::
    end

    local culledVoiceClients = table.filter(vcManager._voiceClients, function(_v)
      ---@cast _v VoiceClient
      return not _v.DistanceCulled and not table.contains(updatedPlayers, _v.ServerId)
    end)
    for _, culledVoiceClient in pairs(culledVoiceClients) do
      ---@cast culledVoiceClient VoiceClient
      culledVoiceClient.DistanceCulled = true

      local culledPlayerState = PlayerState.new(
        culledVoiceClient.TeamSpeakName,
        culledVoiceClient.LastPosition,
        culledVoiceClient.VoiceRange,
        culledVoiceClient.IsAlive,
        culledVoiceClient.DistanceCulled
      )
      Logger:Debug("[OnStateUpdateTick] New PlayerState for Culled VoiceClient", culledPlayerState)
      table.insert(playerStates, culledPlayerState)
    end

    vcManager:ExecutePluginCommand(PluginCommand.new(
      Command.BulkUpdate,
      vcManager.Configuration.ServerUniqueIdentifier,
      BulkUpdate.new(
        playerStates,
        SelfState.new(
          playerPosition,
          tonumber(string.format("%.2f", GetGameplayCamRot(0).z)),
          vcManager:GetVoiceRange(),
          vcManager.IsAlive
        )
      )
    ))
    Wait(5)
  end

  if vcManager.IsAlive then
    local isUnderWater = playerPed.IsSwimmingUnderWater
    local isSwimming = isUnderWater or playerPed.IsSwimming

    if isUnderWater then
      vcManager:SetCanSendRadioTraffic(false)
      vcManager:SetCanReceiveRadioTraffic(false)
    elseif isSwimming and GetEntitySpeed(playerPed.Handle) <= 2.0 then
      vcManager:SetCanSendRadioTraffic(true)
      vcManager:SetCanReceiveRadioTraffic(true)
    elseif isSwimming then
      vcManager:SetCanSendRadioTraffic(false)
      vcManager:SetCanReceiveRadioTraffic(true)
    else
      vcManager:SetCanSendRadioTraffic(true)
      vcManager:SetCanReceiveRadioTraffic(true)
    end
  else
    vcManager:SetCanSendRadioTraffic(false)
    vcManager:SetCanReceiveRadioTraffic(false)
  end

  Wait(500)
end

--#endregion

--#region NUICallbacks W I S E M A N
RegisterNUICallback(NuiEvent.SaltyChat_OnNuiReady, function(data, cb) vcManager:OnNuiReady(data, cb) end)
function VoiceManager:OnNuiReady(data, cb)
  self.IsNuiReady = true

  if self.IsEnabled and self.TeamSpeakName ~= nil and not self.IsConnected then
    print("[SaltyChat Lua] NUI is now ready, connecting...")
    self:ExecuteCommand("connect", self.WebSocketAddress)
  end

  cb("")
end

RegisterNUICallback(NuiEvent.SaltyChat_OnConnected, function(data, cb) vcManager:OnConnected(data, cb) end)
function VoiceManager:OnConnected(data, cb)
  self.IsConnected = true
  if self.IsEnabled then
    self:InitializePlugin()
  end

  cb("")
end

RegisterNUICallback(NuiEvent.SaltyChat_OnDisconnected, function(data, cb) vcManager:OnDisconnected(data, cb) end)
function VoiceManager:OnDisconnected(data, cb)
  self.IsConnected = false
  self:SetPluginState(GameInstanceState.NotInitiated)

  cb("")
end

RegisterNUICallback(NuiEvent.SaltyChat_OnMessage, function(data, cb)
  vcManager:OnMessage(data, cb)
  cb("")
end)

function VoiceManager:OnMessage(data, cb)
  local pluginCommand = PluginCommand.Deserialize(data)
  if pluginCommand.ServerUniqueIdentifier ~= Configuration.ServerUniqueIdentifier then
    return
  end

  Logger:Debug("[OnMessage] Data", pluginCommand.Command)
  if pluginCommand.Command == Command.PluginState then
    ---@type PluginState
    local pluginState = pluginCommand.Parameter
    TriggerServerEvent(Event.SaltyChat_CheckVersion, pluginState.Version)

    self:ExecutePluginCommand(PluginCommand.new(
      Command.RadioTowerUpdate,
      self.Configuration.ServerUniqueIdentifier,
      RadioTower.new(self.RadioTowers)
    ))

    if self.PrimaryRadioChannel ~= nil then
      self:RadioChannelMemberChangeHandler("global", State.SaltyChat_RadioChannelMember .. ":" ..
        self.PrimaryRadioChannel, GlobalState[State.SaltyChat_RadioChannelMember .. ":" .. self.PrimaryRadioChannel])
      self:RadioChannelSenderChangeHandler("global", State.SaltyChat_RadioChannelSender .. ":" ..
        self.PrimaryRadioChannel, GlobalState[State.SaltyChat_RadioChannelSender .. ":" .. self.PrimaryRadioChannel])
    end

    if self.SecondaryRadioChannel ~= nil then
      self:RadioChannelMemberChangeHandler("global", State.SaltyChat_RadioChannelMember ..
        ":" .. self.SecondaryRadioChannel, GlobalState
        [State.SaltyChat_RadioChannelMember .. ":" .. self.SecondaryRadioChannel])
      self:RadioChannelSenderChangeHandler("global", State.SaltyChat_RadioChannelSender ..
        ":" .. self.SecondaryRadioChannel, GlobalState
        [State.SaltyChat_RadioChannelSender .. ":" .. self.SecondaryRadioChannel])
    end
  elseif pluginCommand.Command == Command.Reset then
    self:SetPluginState(GameInstanceState.NotInitiated)
    self:InitializePlugin()
  elseif pluginCommand.Command == Command.Ping then
    if self:GetPluginState() ~= GameInstanceState.NotInitiated then
      self:ExecutePluginCommand(PluginCommand.new(
        Command.Pong,
        self.Configuration.ServerUniqueIdentifier
      ))
    end
  elseif pluginCommand.Command == Command.InstanceState then
    ---@type InstanceState
    local instanceState = pluginCommand.Parameter
    self:SetPluginState(instanceState.State)
  elseif pluginCommand.Command == Command.SoundState then
    ---@type SoundState
    local soundState = pluginCommand.Parameter

    if soundState.IsMicrophoneMuted ~= self.IsMicrophoneMuted then
      self.IsMicrophoneMuted = soundState.IsMicrophoneMuted;

      TriggerEvent(Event.SaltyChat_MicStateChanged, self.IsMicrophoneMuted);
    end

    if soundState.IsMicrophoneEnabled ~= self.IsMicrophoneEnabled then
      self.IsMicrophoneEnabled = soundState.IsMicrophoneEnabled;

      TriggerEvent(Event.SaltyChat_MicEnabledChanged, self.IsMicrophoneEnabled);
    end

    if soundState.IsSoundMuted ~= self.IsSoundMuted then
      self.IsSoundMuted = soundState.IsSoundMuted;

      TriggerEvent(Event.SaltyChat_SoundStateChanged, self.IsSoundMuted);
    end

    if soundState.IsSoundEnabled ~= self.IsSoundEnabled then
      self.IsSoundEnabled = soundState.IsSoundEnabled;

      TriggerEvent(Event.SaltyChat_SoundEnabledChanged, self.IsSoundEnabled);
    end
  elseif pluginCommand.Command == Command.TalkState then
    ---@type TalkState
    local talkState = pluginCommand.Parameter
    if not self.IsMicrophoneMuted then
      self:SetPlayerTalking(talkState.Name, talkState.IsTalking);
    end
  elseif pluginCommand.Command == Command.RadioTrafficState then
    ---@type RadioTrafficState
    local radioTrafficState = pluginCommand.Parameter

    ---@type RadioTrafficState
    local activeRadioTrafficState = table.find(self.ActiveRadioTraffic, function(value)
      ---@cast value RadioTrafficState
      return value.Name == radioTrafficState.Name and value.IsPrimaryChannel == radioTrafficState.IsPrimaryChannel
    end)

    if radioTrafficState.IsSending then
      if activeRadioTrafficState == nil then
        table.insert(self.ActiveRadioTraffic, radioTrafficState)
      elseif activeRadioTrafficState ~= nil and activeRadioTrafficState.ActiveRelay ~= radioTrafficState.ActiveRelay then
        activeRadioTrafficState.ActiveRelay = radioTrafficState.ActiveRelay
      end
    else
      if activeRadioTrafficState ~= nil then
        local activeRadioTrafficStateKey = table.findIndex(self.ActiveRadioTraffic, function(value)
          ---@cast value RadioTrafficState
          return value.Name == activeRadioTrafficState.Name
        end)

        table.removeKey(self.ActiveRadioTraffic, activeRadioTrafficStateKey)
      end
    end

    TriggerEvent(Event.SaltyChat_RadioTrafficStateChanged,
      table.any(self.ActiveRadioTraffic, function(r) -- Primary RX
        ---@cast r RadioTrafficState
        return r.IsPrimaryChannel and r.IsSending and r.ActiveRelay == null and r.Name ~= self.TeamSpeakName
      end),
      table.any(self.ActiveRadioTraffic, function(r)
        ---@cast r RadioTrafficState
        return r.Name == self.TeamSpeakName and r.IsPrimaryChannel and r.IsSending
      end), -- Primary TX
      table.any(self.ActiveRadioTraffic, function(r)
        ---@cast r RadioTrafficState
        return not r.IsPrimaryChannel and r.IsSending and r.ActiveRelay == null and r.Name ~= self.TeamSpeakName
      end), -- Secondary RX
      table.any(self.ActiveRadioTraffic, function(r)
        ---@cast r RadioTrafficState
        return r.Name == self.TeamSpeakName and not r.IsPrimaryChannel and r.IsSending
      end) -- Secondary TX
    );
  end
end

RegisterNUICallback(NuiEvent.SaltyChat_OnError, function(data, cb) vcManager:OnError(data, cb) end)
function VoiceManager:OnError(data, cb)
  local pluginError = PluginError.Deserialize(data)

  if pluginError then
    if pluginError.Error == Error.AlreadyInGame then
      print("[SaltyChat Lua] Error: Seems like we are already in an instance, retry in 5 seconds...")
      Wait(5000)
      self:InitializePlugin()
    else
      print("[SaltyChat Lua] Error: " .. pluginError.Error .. " - Message:" .. pluginError.Message)
    end
  else
    print("[SaltyChat Lua] Error: We received an error, but couldn't deserialize it")
  end
end

--#endregion

--#region Events W I S E M A N
AddEventHandler("onClientResourceStop", function(resourceName) vcManager:OnResourceStop(resourceName) end)
---@param resourceName string
function VoiceManager:OnResourceStop(resourceName)
  if resourceName ~= GetCurrentResourceName() then return end

  self.IsEnabled = false
  self.IsConnected = false

  self._voiceClients = {}

  self.PrimaryRadioChannel = nil
  self.SecondaryRadioChannel = nil

  for _, cookie in pairs(self._changeHandlerCookies) do
    RemoveStateBagChangeHandler(cookie)
  end

  vcManager._changeHandlerCookies = nil
end

RegisterNetEvent(Event.SaltyChat_Initialize,
  function(teamSpeakName, voiceRange, towers) vcManager:OnInitialize(teamSpeakName, voiceRange, towers) end)
---@param teamSpeakName string
---@param voiceRange number
---@param towers table
function VoiceManager:OnInitialize(teamSpeakName, voiceRange, towers)
  self.TeamSpeakName = teamSpeakName
  self:SetVoiceRange(voiceRange)

  self:OnUpdateRadioTowers(towers)
  self.IsEnabled = true

  if self.IsConnected then
    self:InitializePlugin()
  elseif self.IsNuiReady then
    self:ExecuteCommand("connect", self.WebSocketAddress)
  else
    print("[SaltyChat Lua] Got server response, but NUI wasn't ready")
  end
end

RegisterNetEvent(Event.SaltyChat_RemoveClient, function(handle) vcManager:OnClientRemove(handle) end)
---@param handle string
function VoiceManager:OnClientRemove(handle)
  local serverId = tonumber(handle)
  if type(serverId) ~= "number" then
    return print(
      "[SaltyChat Lua] Error 'OnClientRemove': Could not get serverId. serverId is not a number")
  end
  ---@type VoiceClient
  local voiceClient = self._voiceClients[serverId]

  if voiceClient then
    self:ExecutePluginCommand(PluginCommand.new(
      Command.RemovePlayer,
      self.Configuration.ServerUniqueIdentifier,
      PlayerState.new(voiceClient.TeamSpeakName)
    ))

    table.removeKey(self._voiceClients, serverId)
  end
end

RegisterNetEvent(Event.SaltyChat_EstablishCall,
  function(handle, teamSpeakName, position) vcManager:OnEstablishCall(handle, teamSpeakName, position) end)
---@param handle string
---@param teamSpeakName string
---@param position table
function VoiceManager:OnEstablishCall(handle, teamSpeakName, position)
  Logger:Debug("[OnEstablishCall]", handle, teamSpeakName)
  self:OnEstablishCallRelayed(handle, teamSpeakName, position, true, {})
end

RegisterNetEvent(Event.SaltyChat_EstablishCall,
  function(handle, teamSpeakName, position, direct, relays)
    vcManager:OnEstablishCallRelayed(handle, teamSpeakName,
      position, direct, relays)
  end)
---@param handle string
---@param teamSpeakName string
---@param position table
---@param direct boolean
---@param relays string[]
function VoiceManager:OnEstablishCallRelayed(handle, teamSpeakName, position, direct, relays)
  local serverId = tonumber(handle)
  if type(serverId) ~= "number" then
    return print(
      "[SaltyChat Lua] Error 'OnEstablishCallRelayed': Could not get serverId. serverId is not a number")
  end
  local voiceClient = self:GetOrCreateVoiceClient(serverId, teamSpeakName)

  if voiceClient then
    if voiceClient.DistanceCulled then
      voiceClient.LastPosition = TSVector.new(position[1], position[2], position[3])
      voiceClient:SendPlayerStateUpdate(self)
      self._phoneCallClients[voiceClient.ServerId] = voiceClient
    end

    local signalDistortion = 0
    if Configuration.VariablePhoneDistortion then
      local playerPosition = GamePlayer.Character.Position
      local remotePlayerPosition = voiceClient.LastPosition

      signalDistortion = GetZoneScumminess(GetZoneAtCoords(playerPosition.x, playerPosition.y, playerPosition.z)) +
          GetZoneScumminess(GetZoneAtCoords(remotePlayerPosition.x, remotePlayerPosition.y, remotePlayerPosition.z))
    end

    self:ExecutePluginCommand(
      PluginCommand.new(
        Command.PhoneCommunicationUpdate,
        self.Configuration.ServerUniqueIdentifier,
        PhoneCommunication.new(
          voiceClient.TeamSpeakName,
          signalDistortion,
          direct,
          table.values(relays)
        )
      )
    )
  end
end

RegisterNetEvent(Event.SaltyChat_ChannelInUse, function(channelName) vcManager:OnChannelBlocked(channelName) end)
---@param channelName string
function VoiceManager:OnChannelBlocked(channelName)
  self:PlaySound("offMicClick", false, "radio")
  if channelName == self.PrimaryRadioChannel then
    self:OnPrimaryRadioReleased()
  elseif channelName == self.SecondaryRadioChannel then
    self:OnSecondaryRadioReleased()
  end
end

RegisterNetEvent(Event.SaltyChat_SetRadioSpeaker, function(channelName) vcManager:OnChannelBlocked(channelName) end)
---@param isRadioSpeakerEnabled boolean
function VoiceManager:OnSetRadioSpeaker(isRadioSpeakerEnabled)
  self.IsRadioSpeakerEnabled = isRadioSpeakerEnabled
end

RegisterNetEvent(Event.SaltyChat_UpdateRadioTowers, function(towers)
  vcManager:OnUpdateRadioTowers(towers)
end)

RegisterNetEvent(Event.SaltyChat_EndCall, function(handle) vcManager:OnEndCall(handle) end)
---@param handle string
function VoiceManager:OnEndCall(handle)
  local serverId = tonumber(handle)
  if type(serverId) ~= "number" then
    return print(
      "[SaltyChat Lua] Error 'OnEndCall': Could not get serverId. serverId is not a number")
  end


  local voiceClient = self._phoneCallClients[serverId] or self:GetOrCreateVoiceClient(serverId, Util.GetTeamSpeakName(serverId))
  Logger:Debug("[OnEndCall]", serverId, voiceClient)
  if voiceClient then
    self:ExecutePluginCommand(PluginCommand.new(
      Command.StopPhoneCommunication,
      self.Configuration.ServerUniqueIdentifier,
      PhoneCommunication.new(
        voiceClient.TeamSpeakName
      )
    ))

    if self._phoneCallClients[serverId] then
      self._phoneCallClients[serverId] = nil
    end
  end
end

RegisterNetEvent(Event.SaltyChat_SetRadioChannel,
  function(radioChannel, isPrimary) vcManager:OnSetRadioChannel(radioChannel, isPrimary) end)

---@param radioChannel string
---@param isPrimary boolean
function VoiceManager:OnSetRadioChannel(radioChannel, isPrimary)
  if isPrimary then
    if self.PrimaryRadioChangeHandlerCookies ~= nil then
      for _, cookie in pairs(self.PrimaryRadioChangeHandlerCookies) do
        RemoveStateBagChangeHandler(cookie)
      end

      self.PrimaryRadioChangeHandlerCookies = nil
    end

    if IsStringNullOrEmpty(radioChannel) then
      self:RadioChannelSenderChangeHandler("global", State.SaltyChat_RadioChannelSender .. ":" ..
        self.PrimaryRadioChannel, {}, 0, false)
      self.PrimaryRadioChannel = nil
      self:PlaySound("leaveRadioChannel", false, "radio")
      self:ExecutePluginCommand(PluginCommand.new(
        Command.UpdateRadioChannelMembers,
        self.Configuration.ServerUniqueIdentifier,
        RadioChannelMemberUpdate.new(
          {},
          true
        )
      ))
    else
      self.PrimaryRadioChannel = radioChannel
      self.PrimaryRadioChangeHandlerCookies = {}

      table.insert(self.PrimaryRadioChangeHandlerCookies,
        AddStateBagChangeHandler(State.SaltyChat_RadioChannelMember .. ":" .. radioChannel, "global",
          function(bagName, key, value, reserved, replicated)
            self:RadioChannelMemberChangeHandler(bagName, key, value, reserved, replicated)
          end))
      table.insert(self.PrimaryRadioChangeHandlerCookies,
        AddStateBagChangeHandler(State.SaltyChat_RadioChannelSender .. ":" .. radioChannel, "global",
          function(bagName, key, value, reserved, replicated)
            self:RadioChannelSenderChangeHandler(bagName, key, value, reserved, replicated)
          end))

      self:PlaySound("enterRadioChannel", false, "radio")
      if GlobalState[State.SaltyChat_RadioChannelSender .. ":" .. radioChannel] ~= nil then
        self:RadioChannelSenderChangeHandler("global", State.SaltyChat_RadioChannelSender .. ":" .. radioChannel,
          GlobalState[State.SaltyChat_RadioChannelSender .. ":" .. radioChannel], 0, false);
      end
    end
  else
    if self.SecondaryRadioChangeHandlerCookies ~= nil then
      for _, cookie in pairs(self.SecondaryRadioChangeHandlerCookies) do
        RemoveStateBagChangeHandler(cookie)
      end

      self.SecondaryRadioChangeHandlerCookies = nil
    end

    if IsStringNullOrEmpty(radioChannel) then
      self:RadioChannelSenderChangeHandler("global", State.SaltyChat_RadioChannelSender ..
        ":" .. self.SecondaryRadioChannel, {}, 0, false)
      self.SecondaryRadioChannel = nil
      self:PlaySound("leaveRadioChannel", false, "radio")
      self:ExecutePluginCommand(PluginCommand.new(
        Command.UpdateRadioChannelMembers,
        self.Configuration.ServerUniqueIdentifier,
        RadioChannelMemberUpdate.new(
          {},
          false
        )
      ))
    else
      self.SecondaryRadioChannel = radioChannel
      self.SecondaryRadioChangeHandlerCookies = {}

      table.insert(self.SecondaryRadioChangeHandlerCookies,
        AddStateBagChangeHandler(State.SaltyChat_RadioChannelMember .. ":" .. radioChannel, "global",
          function(bagName, key, value, reserved, replicated)
            self:RadioChannelMemberChangeHandler(bagName, key, value, reserved, replicated)
          end))

      table.insert(self.SecondaryRadioChangeHandlerCookies,
        AddStateBagChangeHandler(State.SaltyChat_RadioChannelSender .. ":" .. radioChannel, "global",
          function(bagName, key, value, reserved, replicated)
            self:RadioChannelSenderChangeHandler(bagName, key, value, reserved, replicated)
          end))

      self:PlaySound("enterRadioChannel", false, "radio")
      if GlobalState[State.SaltyChat_RadioChannelSender .. ":" .. radioChannel] ~= nil then
        self:RadioChannelSenderChangeHandler("global", State.SaltyChat_RadioChannelSender .. ":" .. radioChannel,
          GlobalState[State.SaltyChat_RadioChannelSender .. ":" .. radioChannel], 0, false);
      end
    end
  end

  TriggerEvent(Event.SaltyChat_RadioChannelChanged, radioChannel, isPrimary)
end

--#endregion
