--[[---------------------------------------------------------------------------------------

	Wraith ARS 2X
	Created by WolfKnight

	For discussions, information on future updates, and more, join
	my Discord: https://discord.gg/fD4e6WD

	MIT License

	Copyright (c) 2020-2021 WolfKnight

	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all
	copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
	SOFTWARE.

---------------------------------------------------------------------------------------]]--

-- Cache some of the main Lua functions and libraries
local next = next
local dot = dot
local table = table
local type = type
local tostring = tostring
local math = math
local pairs = pairs


--[[----------------------------------------------------------------------------------
	UI loading and key binds registering
----------------------------------------------------------------------------------]]--
local function RegisterKeyBinds()
	if ( UTIL:IsResourceNameValid() ) then
		UTIL:Log( "Registering radar commands and key binds." )

		-- Opens the remote control
		RegisterCommand( "radar_remote", function()
			if ( not RADAR:GetKeyLockState() ) then
				RADAR:OpenRemote()
			end
		end )
		RegisterKeyMapping( "radar_remote", "Open Remote Control", "keyboard", CONFIG.keyDefaults.remote_control )

		-- Locks speed from front antenna
		RegisterCommand( "radar_fr_ant", function()
			if ( not RADAR:GetKeyLockState() and PLY:CanControlRadar() ) then
				RADAR:LockAntennaSpeed( "front", nil )

				SYNC:LockAntennaSpeed( "front", RADAR:GetAntennaDataPacket( "front" ) )
			end
		end )
		RegisterKeyMapping( "radar_fr_ant", "Front Antenna Lock/Unlock", "keyboard", CONFIG.keyDefaults.front_lock )

		-- Locks speed from rear antenna
		RegisterCommand( "radar_bk_ant", function()
			if ( not RADAR:GetKeyLockState() and PLY:CanControlRadar() ) then
				RADAR:LockAntennaSpeed( "rear", nil )

				SYNC:LockAntennaSpeed( "rear", RADAR:GetAntennaDataPacket( "rear" ) )
			end
		end )
		RegisterKeyMapping( "radar_bk_ant", "Rear Antenna Lock/Unlock", "keyboard", CONFIG.keyDefaults.rear_lock )

		-- Locks front plate reader
		RegisterCommand( "radar_fr_cam", function()
			if ( not RADAR:GetKeyLockState() and PLY:CanControlRadar() ) then
				READER:LockCam( "front", true, false )

				SYNC:LockReaderCam( "front", READER:GetCameraDataPacket( "front" ) )
			end
		end )
		RegisterKeyMapping( "radar_fr_cam", "Front Plate Reader Lock/Unlock", "keyboard", CONFIG.keyDefaults.plate_front_lock )

		-- Locks rear plate reader
		RegisterCommand( "radar_bk_cam", function()
			if ( not RADAR:GetKeyLockState() and PLY:CanControlRadar() ) then
				READER:LockCam( "rear", true, false )

				SYNC:LockReaderCam( "rear", READER:GetCameraDataPacket( "rear" ) )
			end
		end )
		RegisterKeyMapping( "radar_bk_cam", "Rear Plate Reader Lock/Unlock", "keyboard", CONFIG.keyDefaults.plate_rear_lock )

		-- Toggles the key lock state
		RegisterCommand( "radar_key_lock", function()
			RADAR:ToggleKeyLock()
		end )
		RegisterKeyMapping( "radar_key_lock", "Toggle Keybind Lock", "keyboard", CONFIG.keyDefaults.key_lock )

		-- Deletes all of the KVPs
		RegisterCommand( "reset_radar_data", function()
			DeleteResourceKvp( "wk_wars2x_ui_data" )
			DeleteResourceKvp( "wk_wars2x_om_data" )
			DeleteResourceKvp( "wk_wars2x_new_user" )

			UTIL:Notify( "Radar data deleted, please immediately restart your game without opening the radar's remote." )
		end, false )
		TriggerEvent( "chat:addSuggestion", "/reset_radar_data", "Resets the KVP data stored for the wk_wars2x resource." )
	else
		UTIL:Log( "ERROR: Resource name is not wk_wars2x. Key binds will not be registered for compatibility reasons. Contact the server owner and ask them to change the resource name back to wk_wars2x" )
	end
end

local function LoadUISettings()
	UTIL:Log( "Attempting to load saved UI settings data." )

	-- Try and get the saved UI data
	local uiData = GetResourceKvpString( "wk_wars2x_ui_data" )

	-- If the data exists, then we send it off!
	if ( uiData ~= nil ) then
		SendNUIMessage( { _type = "loadUiSettings", data = json.decode( uiData ) } )

		UTIL:Log( "Saved UI settings data loaded!" )
	-- If the data doesn't exist, then we send the defaults
	else
		SendNUIMessage( { _type = "setUiDefaults", data = CONFIG.uiDefaults } )

		UTIL:Log( "Could not find any saved UI settings data." )
	end
end


--[[----------------------------------------------------------------------------------
	Radar variables

	NOTE - This is not a config, do not touch anything unless you know what
	you are actually doing.
----------------------------------------------------------------------------------]]--
RADAR = {}
RADAR.vars =
{
	-- Whether or not the radar's UI is visible
	displayed = false,

	-- The radar's power, the system simulates the radar unit powering up when the user clicks the
	-- power button on the interface
	power = false,
	poweringUp = false,

	-- Whether or not the radar should be hidden, e.g. the display is active but the player then steps
	-- out of their vehicle
	hidden = false,

	-- These are the settings that are used in the operator menu
	settings = {
		-- Should the system calculate and display faster targets
		["fastDisplay"] = CONFIG.menuDefaults["fastDisplay"],

		-- Sensitivity for each radar mode, this changes how far the antennas will detect vehicles
		["same"] = CONFIG.menuDefaults["same"],
		["opp"] = CONFIG.menuDefaults["opp"],

		-- The volume of the audible beep
		["beep"] = CONFIG.menuDefaults["beep"],

		-- The volume of the verbal lock confirmation
		["voice"] = CONFIG.menuDefaults["voice"],

		-- The volume of the plate reader audio
		["plateAudio"] = CONFIG.menuDefaults["plateAudio"],

		-- The speed unit used in conversions
		["speedType"] = CONFIG.menuDefaults["speedType"],

		-- The state of automatic speed locking
		["fastLock"] = CONFIG.menuDefaults["fastLock"],

		-- The speed limit for automatic speed locking
		["fastLimit"] = CONFIG.menuDefaults["fastLimit"]
	},

	-- These 3 variables are for the in-radar menu that can be accessed through the remote control, the menuOptions table
	-- stores all of the information about each of the settings the user can change
	menuActive = false,
	currentOptionIndex = 1,
	menuOptions = {
		{ displayText = { "¦¦¦", "FAS" }, optionsText = { "On¦", "Off" }, options = { true, false }, optionIndex = -1, settingText = "fastDisplay" },
		{ displayText = { "¦SL", "SEn" }, optionsText = { "¦1¦", "¦2¦", "¦3¦", "¦4¦", "¦5¦" }, options = { 0.2, 0.4, 0.6, 0.8, 1.0 }, optionIndex = -1, settingText = "same" },
		{ displayText = { "¦OP", "SEn" }, optionsText = { "¦1¦", "¦2¦", "¦3¦", "¦4¦", "¦5¦" }, options = { 0.2, 0.4, 0.6, 0.8, 1.0 }, optionIndex = -1, settingText = "opp" },
		{ displayText = { "bEE", "P¦¦" }, optionsText = { "Off", "¦1¦", "¦2¦", "¦3¦", "¦4¦", "¦5¦" }, options = { 0.0, 0.2, 0.4, 0.6, 0.8, 1.0 }, optionIndex = -1, settingText = "beep" },
		{ displayText = { "VOI", "CE¦" }, optionsText = { "Off", "¦1¦", "¦2¦", "¦3¦", "¦4¦", "¦5¦" }, options = { 0.0, 0.2, 0.4, 0.6, 0.8, 1.0 }, optionIndex = -1, settingText = "voice" },
		{ displayText = { "PLt", "AUd" }, optionsText = { "Off", "¦1¦", "¦2¦", "¦3¦", "¦4¦", "¦5¦" }, options = { 0.0, 0.2, 0.4, 0.6, 0.8, 1.0 }, optionIndex = -1, settingText = "plateAudio" },
		{ displayText = { "Uni", "tS¦" }, optionsText = { "USA", "INT" }, options = { "mph", "kmh" }, optionIndex = -1, settingText = "speedType" }
	},

	-- Player's vehicle speed, mainly used in the dynamic thread wait update
	patrolSpeed = 0,

	-- Antennas, this table contains all of the data needed for operation of the front and rear antennas
	antennas = {
		-- Variables for the front antenna
		[ "front" ] = {
			xmit = false,			-- Whether the antenna is transmitting or in hold
			mode = 0,				-- Current antenna mode, 0 = none, 1 = same, 2 = opp, 3 = same and opp
			speed = 0,				-- Speed of the vehicle caught by the front antenna
			dir = nil, 				-- Direction the caught vehicle is going, 0 = towards, 1 = away
			fastSpeed = 0, 			-- Speed of the fastest vehicle caught by the front antenna
			fastDir = nil, 			-- Direction the fastest vehicle is going
			speedLocked = false, 	-- A speed has been locked for this antenna
			lockedSpeed = nil, 		-- The locked speed
			lockedDir = nil, 			-- The direction of the vehicle that was locked
			lockedType = nil        -- The locked type, 1 = strongest, 2 = fastest
		},

		-- Variables for the rear antenna
		[ "rear" ] = {
			xmit = false,			-- Whether the antenna is transmitting or in hold
			mode = 0,				-- Current antenna mode, 0 = none, 1 = same, 2 = opp, 3 = same and opp
			speed = 0,				-- Speed of the vehicle caught by the front antenna
			dir = nil, 				-- Direction the caught vehicle is going, 0 = towards, 1 = away
			fastSpeed = 0, 			-- Speed of the fastest vehicle caught by the front antenna
			fastDir = nil, 			-- Direction the fastest vehicle is going
			speedLocked = false,	-- A speed has been locked for this antenna
			lockedSpeed = nil,		-- The locked speed
			lockedDir = nil,			-- The direction of the vehicle that was locked
			lockedType = nil        -- The locked type, 1 = strongest, 2 = fastest
		}
	},

	-- The maximum distance that the radar system's ray traces can go, changing this will change the max
	-- distance in-game, but I wouldn't really put it more than 500.0
	maxCheckDist = 350.0,

	-- Cached dynamic vehicle sphere sizes, automatically populated when the system is running
	sphereSizes = {},

	-- Table to store tables for hit entities of captured vehicles
	capturedVehicles = {},

	-- Table to store the valid vehicle models
	validVehicles = {},

	-- The current vehicle data for display
	activeVehicles = {},

	-- Vehicle pool, automatically populated when the system is running, holds all of the current
	-- vehicle IDs for the player using entity enumeration (see cl_utils.lua)
	vehiclePool = {},

	-- Ray trace state, this is used so the radar system doesn't initiate another set of ray traces until
	-- the current set has finished
	rayTraceState = 0,

	-- Number of ray traces, automatically cached when the system first runs
	numberOfRays = 0,

	-- The wait time for the ray trace system, this changes dynamically based on if the player's vehicle is stationary
	-- or not
	threadWaitTime = 500,

	-- Key lock, when true, prevents any of the radar's key events from working, like the ELS key lock
	keyLock = false
}

-- Speed conversion values
RADAR.speedConversions = { ["mph"] = 2.236936, ["kmh"] = 3.6 }

-- These vectors are used in the custom ray tracing system
RADAR.rayTraces = {
	{ startVec = { x = 0.0 }, endVec = { x = 0.0, y = 0.0 }, rayType = "same" },
	{ startVec = { x = -5.0 }, endVec = { x = -5.0, y = 0.0 }, rayType = "same" },
	{ startVec = { x = 5.0 }, endVec = { x = 5.0, y = 0.0 }, rayType = "same" },
	{ startVec = { x = -10.0 }, endVec = { x = -10.0, y = 0.0 }, rayType = "opp" },
	{ startVec = { x = -17.0 }, endVec = { x = -17.0, y = 0.0 }, rayType = "opp" }
}

-- Each of these are used for sorting the captured vehicle data, the 'strongest' filter is used for the main
-- target window of each antenna, whereas the 'fastest' filter is used for the fast target window of each antenna
RADAR.sorting = {
	strongest = function( a, b ) return a.size > b.size end,
	fastest = function( a, b ) return a.speed > b.speed end
}


--[[----------------------------------------------------------------------------------
	Radar essentials functions
----------------------------------------------------------------------------------]]--
-- Returns if the radar's power is on or off
function RADAR:IsPowerOn()
	return self.vars.power
end

-- Returns if the radar system is powering up, the powering up stage only takes 2 seconds
function RADAR:IsPoweringUp()
	return self.vars.poweringUp
end

-- Allows the powering up state variable to be set
function RADAR:SetPoweringUpState( state )
	self.vars.poweringUp = state
end

-- Toggles the radar power
function RADAR:SetPowerState( state, instantOverride )
	local currentState = self:IsPowerOn()

	-- Only power up if the system is not already powering up
	if ( not self:IsPoweringUp() and currentState ~= state ) then
		-- Toggle the power variable
		self.vars.power = state

		-- Send the NUI message to toggle the power
		SendNUIMessage( { _type = "radarPower", state = state, override = instantOverride, fast = self:IsFastDisplayEnabled() } )

		-- Power is now turned on
		if ( self:IsPowerOn() ) then
			-- Also make sure the operator menu is inactive
			self:SetMenuState( false )

			-- Only do the power up simulation if allowed
			if ( not instantOverride ) then
				-- Tell the system the radar is 'powering up'
				self:SetPoweringUpState( true )

				-- Set a 2 second countdown
				Citizen.SetTimeout( 2000, function()
					-- Tell the system the radar has 'powered up'
					self:SetPoweringUpState( false )

					-- Let the UI side know the system has loaded
					SendNUIMessage( { _type = "poweredUp", fast = self:IsFastDisplayEnabled() } )
				end )
			end
		else
			-- If the system is being turned off, then we reset the antennas
			self:ResetAntenna( "front" )
			self:ResetAntenna( "rear" )
		end
	end
end

-- Toggles the display state of the radar system
function RADAR:ToggleDisplayState()
	-- Toggle the display variable
	self.vars.displayed = not self.vars.displayed

	-- Send the toggle message to the NUI side
	SendNUIMessage( { _type = "setRadarDisplayState", state = self:GetDisplayState() } )
end

-- Gets the display state
function RADAR:GetDisplayState()
	return self.vars.displayed
end

-- Return the state of the fastDisplay setting, short hand direct way to check if the fast system is enabled
function RADAR:IsFastDisplayEnabled()
	return self:GetSettingValue( "fastDisplay" )
end

-- Returns if either of the antennas are transmitting
function RADAR:IsEitherAntennaOn()
	return self:IsAntennaTransmitting( "front" ) or self:IsAntennaTransmitting( "rear" )
end

-- Sends an update to the NUI side with the current state of the antennas and if the fast system is enabled
function RADAR:SendSettingUpdate()
	-- Create a table to store the setting information for the antennas
	local antennas = {}

	-- Iterate through each antenna and grab the relevant information
	for ant in UTIL:Values( { "front", "rear" } ) do
		antennas[ant] = {}
		antennas[ant].xmit = self:IsAntennaTransmitting( ant )
		antennas[ant].mode = self:GetAntennaMode( ant )
		antennas[ant].speedLocked = self:IsAntennaSpeedLocked( ant )
		antennas[ant].fast = self:ShouldFastBeDisplayed( ant )
	end

	-- Send a message to the NUI side with the current state of the antennas
	SendNUIMessage( { _type = "settingUpdate", antennaData = antennas } )
end

-- Returns if a main task can be performed
-- A main task such as the ray trace thread should only run if the radar's power is on, the system is not in the
-- process of powering up, and the operator menu is not open
function RADAR:CanPerformMainTask()
	return self:IsPowerOn() and not self:IsPoweringUp() and not self:IsMenuOpen()
end

-- Returns/sets what the dynamic thread wait time is
function RADAR:GetThreadWaitTime() return self.vars.threadWaitTime end
function RADAR:SetThreadWaitTime( time ) self.vars.threadWaitTime = time end

-- Returns/sets the radr's display hidden state
function RADAR:GetDisplayHidden() return self.vars.hidden end
function RADAR:SetDisplayHidden( state ) self.vars.hidden = state end

-- Opens the remote only if the pause menu is not open and the player's vehicle state is valid, as the
-- passenger can also open the remote, we check the config variable as well.
function RADAR:OpenRemote()
	if ( not IsPauseMenuActive() and PLY:CanViewRadar() ) then
		-- Get the remote open state from the other player
		local openByOtherPly = SYNC:IsRemoteAlreadyOpen( PLY:GetOtherPed() )

		-- Check that the remote can be opened
		if ( not openByOtherPly ) then
			-- Tell the NUI side to open the remote
			SendNUIMessage( { _type = "openRemote" } )

			SYNC:SetRemoteOpenState( true )

			if ( CONFIG.allow_quick_start_video ) then
				-- Display the new user popup if we can
				local show = GetResourceKvpInt( "wk_wars2x_new_user" )

				if ( show == 0 ) then
					SendNUIMessage( { _type = "showNewUser" } )
				end
			end

			-- Bring focus to the NUI side
			SetNuiFocus( true, true )
		else
			UTIL:Notify( "Another player already has the remote open." )
		end
	end
end

-- Event to open the remote
RegisterNetEvent( "wk:openRemote" )
AddEventHandler( "wk:openRemote", function()
	RADAR:OpenRemote()
end )

-- Returns if the passenger can view the radar too
function RADAR:IsPassengerViewAllowed()
	return CONFIG.allow_passenger_view
end

-- Returns if the passenger can control the radar and plate reader, reliant on the passenger being
-- able to view the radar and plate reader too
function RADAR:IsPassengerControlAllowed()
	return CONFIG.allow_passenger_view and CONFIG.allow_passenger_control
end

-- Returns if we only auto lock vehicle speeds if said vehicle is a player
function RADAR:OnlyLockFastPlayers()
	return CONFIG.only_lock_players
end

-- Returns if the fast limit option should be available for the radar
function RADAR:IsFastLimitAllowed()
	return CONFIG.allow_fast_limit
end

-- Only create the functions if the fast limit config option is enabled
if ( RADAR:IsFastLimitAllowed() ) then
	-- Adds settings into the radar's variables for when the allow_fast_limit variable is true
	function RADAR:CreateFastLimitConfig()
		-- Create the options for the menu
		local fastOptions =
		{
			{ displayText = { "FAS", "Loc" }, optionsText = { "On¦", "Off" }, options = { true, false }, optionIndex = 2, settingText = "fastLock" },
			{ displayText = { "FAS", "SPd" }, optionsText = {}, options = {}, optionIndex = 12, settingText = "fastLimit" }
		}

		-- Iterate from 5 to 200 in steps of 5 and insert into the fast limit option
		for i = 5, 200, 5 do
			local text = UTIL:FormatSpeed( i )

			table.insert( fastOptions[2].optionsText, text )
			table.insert( fastOptions[2].options, i )
		end

		-- Add the fast options to the main menu options table
		if ( CONFIG.fast_limit_first_in_menu ) then
			table.insert( self.vars.menuOptions, 1, fastOptions[2] )	--FasSpd
			table.insert( self.vars.menuOptions, 2, fastOptions[1] )	--FasLoc
		else
			table.insert( self.vars.menuOptions, fastOptions[1] )	--FasLoc
			table.insert( self.vars.menuOptions, fastOptions[2] )	--FasSpd
		end
	end

	-- Returns the numerical fast limit
	function RADAR:GetFastLimit()
		return self:GetSettingValue( "fastLimit" )
	end

	-- Returns if the fast lock menu option is on or off
	function RADAR:IsFastLockEnabled()
		return self:GetSettingValue( "fastLock" )
	end
end

-- Toggles the internal key lock state, which stops any of the radar's key binds from working
function RADAR:ToggleKeyLock()
	-- Check the player state is valid
	if ( PLY:CanViewRadar() ) then
		-- Toggle the key lock variable
		self.vars.keyLock = not self.vars.keyLock

		-- Tell the NUI side to display the key lock message
		SendNUIMessage( { _type = "displayKeyLock", state = self:GetKeyLockState() } )
	end
end

-- Returns the key lock state
function RADAR:GetKeyLockState()
	return self.vars.keyLock
end


--[[----------------------------------------------------------------------------------
	Radar menu functions
----------------------------------------------------------------------------------]]--
-- Sets the menu state to the given state
function RADAR:SetMenuState( state )
	-- Make sure that the radar's power is on
	if ( self:IsPowerOn() ) then
		-- Set the menuActive variable to the given state
		self.vars.menuActive = state

		-- If we are opening the menu, make sure the first item is displayed
		if ( state ) then
			self.vars.currentOptionIndex = 1
		end
	end
end

-- Closes the operator menu
function RADAR:CloseMenu( playAudio )
	-- Set the internal menu state to be closed (false)
	RADAR:SetMenuState( false )

	-- Send a setting update to the NUI side
	RADAR:SendSettingUpdate()

	-- Play a menu done beep
	if ( playAudio or playAudio == nil ) then
		SendNUIMessage( { _type = "audio", name = "done", vol = RADAR:GetSettingValue( "beep" ) } )
	end

	-- Save the operator menu values
	local omData = json.encode( RADAR.vars.settings )
	SetResourceKvp( "wk_wars2x_om_data", omData )

	-- Send the operator menu to the passenger if allowed
	if ( self:IsPassengerViewAllowed() ) then
		local updatedOMData = self:GetOMTableData()
		SYNC:SendUpdatedOMData( updatedOMData )
	end
end

-- Returns if the operator menu is open
function RADAR:IsMenuOpen()
	return self.vars.menuActive
end

-- This function changes the menu index variable so the user can iterate through the options in the operator menu
function RADAR:ChangeMenuIndex()
	-- Create a temporary variable of the current menu index plus 1
	local temp = self.vars.currentOptionIndex + 1

	-- If the temporary value is larger than how many options there are, set it to 1, this way the menu
	-- loops back round to the start of the menu
	if ( temp > #self.vars.menuOptions ) then
		temp = 1
	end

	-- Set the menu index variable to the temporary value we created
	self.vars.currentOptionIndex = temp

	-- Call the function to send an update to the NUI side
	self:SendMenuUpdate()
end

-- Returns the option table of the current menu index
function RADAR:GetMenuOptionTable()
	return self.vars.menuOptions[self.vars.currentOptionIndex]
end

-- Changes the index for an individual option
-- E.g. { "On" "Off" }, index = 2 would be "Off"
function RADAR:SetMenuOptionIndex( index )
	self.vars.menuOptions[self.vars.currentOptionIndex].optionIndex = index
end

-- Returns the option value for the current option
function RADAR:GetMenuOptionValue()
	local opt = self:GetMenuOptionTable()
	local index = opt.optionIndex

	return opt.options[index]
end

-- This function is similar to RADAR:ChangeMenuIndex() but allows for iterating forward and backward through options
function RADAR:ChangeMenuOption( dir )
	-- Get the option table of the currently selected option
	local opt = self:GetMenuOptionTable()

	-- Get the current option index of the selected option
	local index = opt.optionIndex

	-- Cache the size of this setting's options table
	local size = #opt.options

	-- As the XMIT/HOLD buttons are used for changing the option values, we have to check which button is being pressed
	if ( dir == "front" ) then
		index = index + 1
		if ( index > size ) then index = 1 end
	elseif ( dir == "rear" ) then
		index = index - 1
		if ( index < 1 ) then index = size end
	end

	-- Update the option's index
	self:SetMenuOptionIndex( index )

	-- Change the value of the setting in the main RADAR.vars.settings table
	self:SetSettingValue( opt.settingText, self:GetMenuOptionValue() )

	-- Call the function to send an update to the NUI side
	self:SendMenuUpdate()
end

-- Returns what text should be displayed in the boxes for the current option
-- E.g. "¦SL" "SEN"
function RADAR:GetMenuOptionDisplayText()
	return self:GetMenuOptionTable().displayText
end

-- Returns the option text of the currently selected setting
function RADAR:GetMenuOptionText()
	local opt = self:GetMenuOptionTable()

	return opt.optionsText[opt.optionIndex]
end

-- Sends a message to the NUI side with updated information on what should be displayed for the menu
function RADAR:SendMenuUpdate()
	SendNUIMessage( { _type = "menu", text = self:GetMenuOptionDisplayText(), option = self:GetMenuOptionText() } )
end

-- Used to set individual settings within RADAR.vars.settings, as all of the settings use string keys, using this
-- function makes updating settings easier
function RADAR:SetSettingValue( setting, value )
	-- Make sure that we're not trying to set a nil value for the setting
	if ( value ~= nil ) then
		-- Set the setting's value
		self.vars.settings[setting] = value

		-- If the setting that's being updated is same or opp, then we update the end coordinates for the ray tracer
		if ( setting == "same" or setting == "opp" ) then
			self:UpdateRayEndCoords()
		end
	end
end

-- Returns the value of the given setting
function RADAR:GetSettingValue( setting )
	return self.vars.settings[setting]
end

-- Attempts to load the saved operator menu data
function RADAR:LoadOMData()
	UTIL:Log( "Attempting to load saved operator menu data." )

	-- Try and get the data
	local rawData = GetResourceKvpString( "wk_wars2x_om_data" )

	-- If the data exists, decode it and replace the operator menu table
	if ( rawData ~= nil ) then
		local omData = json.decode( rawData )
		self.vars.settings = omData

		UTIL:Log( "Saved operator menu data loaded!" )
	else
		UTIL:Log( "Could not find any saved operator menu data." )
	end
end

-- Updates the operator menu option indexes, as the default menu values can be changed in the config, we
-- need to update the indexes otherwise the menu will display the wrong values
function RADAR:UpdateOptionIndexes( loadSaved )
	if ( loadSaved ) then
		self:LoadOMData()
	end

	-- Iterate through each of the internal settings
	for k, v in pairs( self.vars.settings ) do
		-- Iterate through all of the menu options
		for i, t in pairs( self.vars.menuOptions ) do
			-- If the current menu option is the same as the current setting
			if ( t.settingText == k ) then
				-- Iterate through the option values of the current menu option
				for oi, ov in pairs( t.options ) do
					-- If the value of the current option set in the config matches the current value of
					-- the option value, then we update the option index variable
					if ( v == ov ) then
						t.optionIndex = oi
					end
				end
			end
		end
	end
end


--[[----------------------------------------------------------------------------------
	Radar basics functions
----------------------------------------------------------------------------------]]--
-- Returns the patrol speed value stored
function RADAR:GetPatrolSpeed()	return self.vars.patrolSpeed end

-- Returns the current vehicle pool
function RADAR:GetVehiclePool()	return self.vars.vehiclePool end

-- Returns the maximum distance a ray trace can go
function RADAR:GetMaxCheckDist() return self.vars.maxCheckDist end

-- Returns the table sorting function 'strongest'
function RADAR:GetStrongestSortFunc() return self.sorting.strongest end

-- Returns the table sorting function 'fastest'
function RADAR:GetFastestSortFunc() return self.sorting.fastest end

-- Sets the patrol speed to a formatted version of the given number
function RADAR:SetPatrolSpeed( speed )
	if ( type( speed ) == "number" ) then
		self.vars.patrolSpeed = self:GetVehSpeedConverted( speed )
	end
end

-- Sets the vehicle pool to the given value if it's a table
function RADAR:SetVehiclePool( pool )
	if ( type( pool ) == "table" ) then
		self.vars.vehiclePool = pool
	end
end


--[[----------------------------------------------------------------------------------
	Radar ray trace functions
----------------------------------------------------------------------------------]]--
-- Returns what the current ray trace state is
function RADAR:GetRayTraceState() return self.vars.rayTraceState end

-- Caches the number of ray traces in RADAR.rayTraces
function RADAR:CacheNumRays() self.vars.numberOfRays = #self.rayTraces end

-- Returns the number of ray traces the system has
function RADAR:GetNumOfRays() return self.vars.numberOfRays end

-- Increases the system's ray trace state ny 1
function RADAR:IncreaseRayTraceState() self.vars.rayTraceState = self.vars.rayTraceState + 1 end

-- Resets the ray trace state to 0
function RADAR:ResetRayTraceState() self.vars.rayTraceState = 0 end

-- This function is used to determine if a sphere intersect is in front or behind the player's vehicle, the
-- sphere intersect calculation has a 'tProj' value that is a line from the centre of the sphere that goes onto
-- the line being traced. This value will either be positive or negative and can be used to work out the
-- relative position of a point.
function RADAR:GetIntersectedVehIsFrontOrRear( t )
	if ( t > 8.0 ) then
		return 1 -- vehicle is in front
	elseif ( t < -8.0 ) then
		return -1 -- vehicle is behind
	end

	return 0 -- vehicle is next to self
end

-- This function is used to check if a line going from point A to B intersects with a given sphere, it's used in
-- the radar system to check if the patrol vehicle can detect any vehicles. As the default ray trace system in GTA
-- cannot detect vehicles beyond 40~ units, my system acts as a replacement that allows the detection of vehicles
-- much further away (400+ units). Also, as my system uses sphere intersections, each sphere can have a different
-- radius, which means that larger vehicles can have larger spheres, and smaller vehicles can have smaller spheres.
function RADAR:GetLineHitsSphereAndDir( c, radius, rs, re )
	-- Take the vector3's and turn them into vector2's, this way all of the calculations below are for an
	-- infinite cylinder rather than a sphere, which also means that vehicles can be detected even when on
	-- an incline!
	local rayStart = vector2( rs.x, rs.y )
	local rayEnd = vector2( re.x, re.y )
	local centre = vector2( c.x, c.y )

	-- First we get the normalised ray, this way we then know the direction the ray is going
	local rayNorm = norm( rayEnd - rayStart )

	-- Then we calculate the ray from the start point to the centre position of the sphere
	local rayToCentre = centre - rayStart

	-- Now that we have the ray to the centre of the sphere, and the normalised ray direction, we
	-- can calculate the shortest point from the centre of the sphere onto the ray itself. This
	-- would then give us the opposite side of the right angled triangle. All of the resulting
	-- values are also in squared form, as performing square root functions is slower.
	local tProj = dot( rayToCentre, rayNorm )
	local oppLenSqr = dot( rayToCentre, rayToCentre ) - ( tProj * tProj )

	-- Square the radius
	local radiusSqr = radius * radius

	-- Calculate the distance of the ray trace to make sure we only return valid results if the trace
	-- is actually within the distance
	local rayDist = #( rayEnd - rayStart )
	local distToCentre = #( rayStart - centre ) - ( radius * 2 )

	-- Now all we have to do is compare the squared opposite length and the radius squared, this
	-- will then tell us if the ray intersects with the sphere.
	if ( oppLenSqr < radiusSqr and not ( distToCentre > rayDist ) ) then
		return true, self:GetIntersectedVehIsFrontOrRear( tProj )
	end

	return false, nil
end

-- This function is used to check if the target vehicle is in the same general traffic flow as the player's vehicle
-- is sitting. If the angle is too great, then the radar would have an incorrect return for the speed.
function RADAR:IsVehicleInTraffic( tgtVeh, relPos )
	local tgtHdg = GetEntityHeading( tgtVeh )
	local plyHdg = GetEntityHeading( PLY.veh )

	-- Work out the heading difference, but also take into account extreme opposites (e.g. 5deg and 350deg)
	local hdgDiff = math.abs( ( plyHdg - tgtHdg + 180 ) % 360 - 180 )

	if ( relPos == 1 and hdgDiff > 45 and hdgDiff < 135 ) then
		return false
	elseif ( relPos == -1 and hdgDiff > 45 and ( hdgDiff < 135 or hdgDiff > 215 ) ) then
		return false
	end

	return true
end

-- This function is the main custom ray trace function, it performs most of the major tasks for checking a vehicle
-- is valid and should be tested. It also makes use of the LOS native to make sure that we can only trace a vehicle
-- if actually nas a direct line of sight with the player's vehicle, this way we don't pick up vehicles behind walls
-- for example. It then creates a dynamic sphere for the vehicle based on the actual model dimensions of it, adds a
-- small bit of realism, as real radars usually return the strongest target speed.
function RADAR:ShootCustomRay( plyVeh, veh, s, e )
	-- Get the world coordinates of the target vehicle
	local pos = GetEntityCoords( veh )

	-- Calculate the distance between the target vehicle and the start point of the ray trace, note how we don't
	-- use GetDistanceBetweenCoords or Vdist, the method below still returns the same result with less cpu time
	local dist = #( pos - s )

	-- We only perform a trace on the target vehicle if it exists, isn't the player's vehicle, and the distance is
	-- less than the max distance defined by the system
	if ( DoesEntityExist( veh ) and veh ~= plyVeh and dist < self:GetMaxCheckDist() ) then
		-- Get the speed of the target vehicle
		local entSpeed = GetEntitySpeed( veh )

		-- Check that the target vehicle is within the line of sight of the player's vehicle
		local visible = HasEntityClearLosToEntity( plyVeh, veh, 15 ) -- 13 seems okay, 15 too (doesn't grab ents through ents)

		-- Get the pitch of the player's vehicle
		local pitch = GetEntityPitch( plyVeh )

		-- Now we check that the target vehicle is moving and is visible
		if ( entSpeed > 0.1 and ( pitch > -35 and pitch < 35 ) and visible ) then
			-- Get the dynamic radius as well as the size of the target vehicle
			local radius, size = self:GetDynamicRadius( veh )

			-- Check that the trace line intersects with the target vehicle's sphere
			local hit, relPos = self:GetLineHitsSphereAndDir( pos, radius, s, e )

			-- Return all of the information if the vehicle was hit and is in the flow of traffic
			if ( hit and self:IsVehicleInTraffic( veh, relPos ) ) then
				return true, relPos, dist, entSpeed, size
			end
		end
	end

	-- Return a whole lot of nothing
	return false, nil, nil, nil, nil
end

-- This function is used to gather all of the data on vehicles that have been hit by the given trace line, when
-- a vehicle is hit, all of the information about that vehicle is put into a keyless table which is then inserted
-- into a main table. When the loop has finished, the function then returns the table with all of the data.
function RADAR:GetVehsHitByRay( ownVeh, vehs, s, e )
	-- Create the table that will be used to store all of the results
	local caughtVehs = {}

	-- Set the variable to say if there has been data collected
	local hasData = false

	-- Iterate through all of the vehicles
	for _, veh in pairs( vehs ) do
		-- Shoot a custom ray trace to see if the vehicle gets hit
		local hit, relativePos, distance, speed, size = self:ShootCustomRay( ownVeh, veh, s, e )

		-- If the vehicle is hit, then we create a table containing all of the information
		if ( hit ) then
			-- Create the table to store the data
			local vehData = {}
			vehData.veh = veh
			vehData.relPos = relativePos
			vehData.dist = distance
			vehData.speed = speed
			vehData.size = size

			-- Insert the table into the caught vehicles table
			table.insert( caughtVehs, vehData )

			-- Change the has data variable to true, this way the table will be returned
			hasData = true
		end
	end

	-- If the caughtVehs table actually has data, then return it
	if ( hasData ) then return caughtVehs end
end

-- This function is used to gather all of the vehicles hit by a given line trace, and then insert it into the
-- internal captured vehicles table.
function RADAR:CreateRayThread( vehs, from, startX, endX, endY, rayType )
	-- Get the start and end points for the ray trace based on the given start and end coordinates
	local startPoint = GetOffsetFromEntityInWorldCoords( from, startX, 0.0, 0.0 )
	local endPoint = GetOffsetFromEntityInWorldCoords( from, endX, endY, 0.0 )

	-- Get all of the vehicles hit by the ray
	local hitVehs = self:GetVehsHitByRay( from, vehs, startPoint, endPoint )

	-- Insert the captured vehicle data and pass the ray type too
	self:InsertCapturedVehicleData( hitVehs, rayType )

	-- Increase the ray trace state
	self:IncreaseRayTraceState()
end

-- This function iterates through each of the traces defined in RADAR.rayTraces and creates a 'thread' for
-- them, passing along all of the vehicle pool data and the player's vehicle
function RADAR:CreateRayThreads( ownVeh, vehicles )
	for _, v in pairs( self.rayTraces ) do
		self:CreateRayThread( vehicles, ownVeh, v.startVec.x, v.endVec.x, v.endVec.y, v.rayType )
	end
end

-- When the user changes either the same lane or opp lane sensitivity from within the operator menu, this function
-- is then called to update the end coordinates for all of the traces
function RADAR:UpdateRayEndCoords()
	for _, v in pairs( self.rayTraces ) do
		-- Calculate what the new end coordinate should be
		local endY = self:GetSettingValue( v.rayType ) * self:GetMaxCheckDist()

		-- Update the end Y coordinate in the traces table
		v.endVec.y = endY
	end
end


--[[----------------------------------------------------------------------------------
	Radar antenna functions
----------------------------------------------------------------------------------]]--
-- Toggles the state of the given antenna between hold and transmitting, only works if the radar's power is
-- on. Also runs a callback function when present.
function RADAR:ToggleAntenna( ant )
	-- Check power is on
	if ( self:IsPowerOn() ) then
		-- Toggle the given antennas state
		self.vars.antennas[ant].xmit = not self.vars.antennas[ant].xmit

		-- Update the interface with the new antenna transmit state
		SendNUIMessage( { _type = "antennaXmit", ant = ant, on = self:IsAntennaTransmitting( ant ) } )

		-- Play some audio specific to the transmit state
		SendNUIMessage( { _type = "audio", name = self:IsAntennaTransmitting( ant ) and "xmit_on" or "xmit_off", vol = self:GetSettingValue( "beep" ) } )
	end
end

-- Returns if the given antenna is transmitting
function RADAR:IsAntennaTransmitting( ant ) return self.vars.antennas[ant].xmit end

-- Returns if the given relative position value is for the front or rear antenna
function RADAR:GetAntennaTextFromNum( relPos )
	if ( relPos == 1 ) then
		return "front"
	elseif ( relPos == -1 ) then
		return "rear"
	end
end

-- Returns the mode of the given antenna
function RADAR:GetAntennaMode( ant ) return self.vars.antennas[ant].mode end

-- Sets the mode of the given antenna if the mode is valid and the power is on. Also runs a callback function
-- when present.
function RADAR:SetAntennaMode( ant, mode )
	-- Check the mode is actually a number, this is needed as the radar system relies on the mode to be
	-- a number to work
	if ( type( mode ) == "number" ) then
		-- Check the mode is in the valid range for modes, and that the power is on
		if ( mode >= 0 and mode <= 3 and self:IsPowerOn() ) then
			-- Update the mode for the antenna
			self.vars.antennas[ant].mode = mode

			-- Update the interface with the new mode
			SendNUIMessage( { _type = "antennaMode", ant = ant, mode = mode } )

			-- Play a beep
			SendNUIMessage( { _type = "audio", name = "beep", vol = self:GetSettingValue( "beep" ) } )
		end
	end
end

-- Returns/sets the speed for the given antenna
function RADAR:GetAntennaSpeed( ant ) return self.vars.antennas[ant].speed end
function RADAR:SetAntennaSpeed( ant, speed ) self.vars.antennas[ant].speed = speed end

-- Returns/sets the direction for the given antenna
function RADAR:GetAntennaDir( ant ) return self.vars.antennas[ant].dir end
function RADAR:SetAntennaDir( ant, dir ) self.vars.antennas[ant].dir = dir end

-- Sets the speed and direction in one go
function RADAR:SetAntennaData( ant, speed, dir )
	self:SetAntennaSpeed( ant, speed )
	self:SetAntennaDir( ant, dir )
end

-- Returns/sets the fast speed for the given antenna
function RADAR:GetAntennaFastSpeed( ant ) return self.vars.antennas[ant].fastSpeed end
function RADAR:SetAntennaFastSpeed( ant, speed ) self.vars.antennas[ant].fastSpeed = speed end

-- Returns/sets the fast direction for the given antenna
function RADAR:GetAntennaFastDir( ant ) return self.vars.antennas[ant].fastDir end
function RADAR:SetAntennaFastDir( ant, dir ) self.vars.antennas[ant].fastDir = dir end

-- Sets the fast speed and direction in one go
function RADAR:SetAntennaFastData( ant, speed, dir )
	self:SetAntennaFastSpeed( ant, speed )
	self:SetAntennaFastDir( ant, dir )
end

-- Returns if the stored speed for the given antenna is valid
function RADAR:DoesAntennaHaveValidData( ant )
	return self:GetAntennaSpeed( ant ) ~= nil
end

-- Returns if the stored fast speed for the given antenna is valid
function RADAR:DoesAntennaHaveValidFastData( ant )
	return self:GetAntennaFastSpeed( ant ) ~= nil
end

-- Returns if the fast label should be displayed
function RADAR:ShouldFastBeDisplayed( ant )
	if ( self:IsAntennaSpeedLocked( ant ) ) then
		return self:GetAntennaLockedType( ant ) == 2
	else
		return self:IsFastDisplayEnabled()
	end
end

-- Returns if the given antenna has a locked speed
function RADAR:IsAntennaSpeedLocked( ant )
	return self.vars.antennas[ant].speedLocked
end

-- Sets the state of speed lock for the given antenna to the given state
function RADAR:SetAntennaSpeedIsLocked( ant, state )
	self.vars.antennas[ant].speedLocked = state
end

-- Sets a speed and direction to be locked in for the given antenna
function RADAR:SetAntennaSpeedLock( ant, speed, dir, lockType, playAudio )
	-- Check that the passed speed and direction are actually valid
	if ( speed ~= nil and dir ~= nil and lockType ~= nil ) then
		-- Set the locked speed and direction to the passed values
		self.vars.antennas[ant].lockedSpeed = speed
		self.vars.antennas[ant].lockedDir = dir
		self.vars.antennas[ant].lockedType = lockType

		-- Tell the system that a speed has been locked for the given antenna
		self:SetAntennaSpeedIsLocked( ant, true )

		if ( playAudio ) then
			-- Send a message to the NUI side to play the beep sound with the current volume setting
			SendNUIMessage( { _type = "audio", name = "beep", vol = self:GetSettingValue( "beep" ) } )

			-- Send a message to the NUI side to play the lock audio with the current voice volume setting
			SendNUIMessage( { _type = "lockAudio", ant = ant, dir = dir, vol = self:GetSettingValue( "voice" ) } )
		end

		-- Great Scott!
		if ( speed == "¦88" and self:GetSettingValue( "speedType" ) == "mph" ) then
			math.randomseed( GetGameTimer() )

			local chance = math.random()

			-- 15% chance
			if ( chance <= 0.15 ) then
				SendNUIMessage( { _type = "audio", name = "speed_alert", vol = self:GetSettingValue( "beep" ) } )
			end
		end
	end
end

-- Returns the locked speed for the given antenna
function RADAR:GetAntennaLockedSpeed( ant )
	return self.vars.antennas[ant].lockedSpeed
end

-- Returns the locked direction for the given antenna
function RADAR:GetAntennaLockedDir( ant )
	return self.vars.antennas[ant].lockedDir
end

-- Returns the lock type for the given antenna
function RADAR:GetAntennaLockedType( ant )
	return self.vars.antennas[ant].lockedType
end

-- Resets the speed lock info to do with the given antenna
function RADAR:ResetAntennaSpeedLock( ant )
	-- Blank the locked speed and direction
	self.vars.antennas[ant].lockedSpeed = nil
	self.vars.antennas[ant].lockedDir = nil
	self.vars.antennas[ant].lockedType = nil

	-- Set the locked state to false
	self:SetAntennaSpeedIsLocked( ant, false )
end

-- When the user presses the speed lock key for either antenna, this function is called to get the
-- necessary information from the antenna, and then lock it into the display
function RADAR:LockAntennaSpeed( ant, override, lockRegardless )
	-- Only lock a speed if the antenna is on and the UI is displayed
	if ( self:IsPowerOn() and ( ( self:GetDisplayState() and not self:GetDisplayHidden() ) or lockRegardless ) and self:IsAntennaTransmitting( ant ) ) then
		-- Used to determine whether or not to play the audio and update the display. This is mainly for the passenger
		-- control system, as in theory one player could be in the operator menu, and the other player could lock a speed.
		local isMenuOpen = self:IsMenuOpen()

		-- Check if the antenna doesn't have a locked speed, if it doesn't then we lock in the speed, otherwise we
		-- reset the lock
		if ( not self:IsAntennaSpeedLocked( ant ) ) then
			-- Here we check if the override parameter is valid, if so then we set the radar's speed data to the
			-- speed data provided in the override table.
			if ( override ~= nil ) then
				self:SetAntennaData( ant, override[1], override[2] )
				self:SetAntennaFastData( ant, override[3], override[4] )
			end

			-- This override parameter is used for the passenger control system, as the speeds displayed on the
			-- recipients display can't be trusted. When the player who locks the speed triggers the sync, their
			-- speed data is collected and sent to the other player so that their speed data is overriden to be the same.
			override = override or { nil, nil, nil, nil }

			-- Set up a temporary table with 3 nil values, this way if the system isn't able to get a speed or
			-- direction, the speed lock function won't work
			local data = { nil, nil, nil }

			-- As the lock system is based on which speed is displayed, we have to check if there is a speed in the
			-- fast box, if there is then we lock in the fast speed, otherwise we lock in the strongest speed
			if ( self:IsFastDisplayEnabled() and self:DoesAntennaHaveValidFastData( ant ) ) then
				data[1] = self:GetAntennaFastSpeed( ant )
				data[2] = self:GetAntennaFastDir( ant )
				data[3] = 2
			else
				data[1] = self:GetAntennaSpeed( ant )
				data[2] = self:GetAntennaDir( ant )
				data[3] = 1
			end

			-- Lock in the speed data for the antenna
			self:SetAntennaSpeedLock( ant, data[1], data[2], data[3], not isMenuOpen )
		else
			self:ResetAntennaSpeedLock( ant )
		end

		if ( not isMenuOpen ) then
			-- Send an NUI message to change the lock label, otherwise we'd have to wait until the next main loop
			SendNUIMessage( { _type = "antennaLock", ant = ant, state = self:IsAntennaSpeedLocked( ant ) } )
			SendNUIMessage( { _type = "antennaFast", ant = ant, state = self:ShouldFastBeDisplayed( ant ) } )
		end
	end
end

-- Resets an antenna, used when the system is turned off
function RADAR:ResetAntenna( ant )
	-- Overwrite default behaviour, this is because when the system is turned off, the temporary memory is
	-- technically reset, as the setter functions require either the radar power to be on or the antenna to
	-- be transmitting, this is the only way to reset the values
	self.vars.antennas[ant].xmit = false
	self.vars.antennas[ant].mode = 0

	self:ResetAntennaSpeedLock( ant )
end

-- Returns a table with the given antenna's speed data and directions
function RADAR:GetAntennaDataPacket( ant )
	return {
		self:GetAntennaSpeed( ant ),
		self:GetAntennaDir( ant ),
		self:GetAntennaFastSpeed( ant ),
		self:GetAntennaFastDir( ant )
	}
end


--[[----------------------------------------------------------------------------------
	Radar captured vehicle functions
----------------------------------------------------------------------------------]]--
-- Returns the captured vehicles table
function RADAR:GetCapturedVehicles()
	return self.vars.capturedVehicles
end

-- Resets the captured vehicles table to an empty table
function RADAR:ResetCapturedVehicles()
	self.vars.capturedVehicles = {}
end

-- Takes the vehicle data from RADAR:CreateRayThread() and puts it into the main captured vehicles table, along
-- with the ray type for that vehicle data set (e.g. same or opp)
function RADAR:InsertCapturedVehicleData( t, rt )
	-- Make sure the table being passed is valid and not empty
	if ( type( t ) == "table" and not UTIL:IsTableEmpty( t ) ) then
		-- Iterate through the given table
		for _, v in pairs( t ) do
			-- Add the ray type to the current row
			v.rayType = rt

			-- Insert it into the main captured vehicles table
			table.insert( self.vars.capturedVehicles, v )
		end
	end
end


--[[----------------------------------------------------------------------------------
	Radar dynamic sphere radius functions
----------------------------------------------------------------------------------]]--
-- Returns the dynamic sphere data for the given key if there is any
function RADAR:GetDynamicDataValue( key )
	return self.vars.sphereSizes[key]
end

-- Returns if dynamic sphere data exists for the given key
function RADAR:DoesDynamicRadiusDataExist( key )
	return self:GetDynamicDataValue( key ) ~= nil
end

-- Sets the dynamic sohere data for the given key to the given table
function RADAR:SetDynamicRadiusKey( key, t )
	self.vars.sphereSizes[key] = t
end

-- Inserts the given data into the dynamic spheres table, stores the radius and the actual summed up
-- vehicle size. The key is just the model of a vehicle put into string form
function RADAR:InsertDynamicRadiusData( key, radius, actualSize )
	-- Check to make sure there is no data for the vehicle
	if ( self:GetDynamicDataValue( key ) == nil ) then
		-- Create a table to store the data in
		local data = {}

		-- Put the data into the temporary table
		data.radius = radius
		data.actualSize = actualSize

		-- Set the dynamic sphere data for the vehicle
		self:SetDynamicRadiusKey( key, data )
	end
end

-- Returns the dynamic sphere data for the given vehicle
function RADAR:GetRadiusData( key )
	return self.vars.sphereSizes[key].radius, self.vars.sphereSizes[key].actualSize
end

-- This function is used to get the dynamic sphere data for a vehicle, if data already exists for the
-- given vehicle, then the system just returns the already made data, otherwise the data gets created
function RADAR:GetDynamicRadius( veh )
	-- Get the model of the vehicle
	local mdl = GetEntityModel( veh )

	-- Create a key based on the model
	local key = tostring( mdl )

	-- Check to see if data already exists
	local dataExists = self:DoesDynamicRadiusDataExist( key )

	-- If the data doesn't already exist, then we create it
	if ( not dataExists ) then
		-- Get the min and max points of the vehicle model
		local min, max = GetModelDimensions( mdl )

		-- Calculate the size, as the min value is negative
		local size = max - min

		-- Get a numeric size which composes of the x, y, and z size combined
		local numericSize = size.x + size.y + size.z

		-- Get a dynamic radius for the given vehicle model that fits into the world of GTA
		local dynamicRadius = UTIL:Clamp( ( numericSize * numericSize ) / 12, 5.0, 11.0 )

		-- Insert the newly created sphere data into the sphere data table
		self:InsertDynamicRadiusData( key, dynamicRadius, numericSize )

		-- Return the data
		return dynamicRadius, numericSize
	end

	-- Return the stored data
	return self:GetRadiusData( key )
end


--[[----------------------------------------------------------------------------------
	Radar functions
----------------------------------------------------------------------------------]]--
-- Takes a GTA speed and converts it into the type defined by the user in the operator menu
function RADAR:GetVehSpeedConverted( speed )
	-- Get the speed unit from the settings
	local unit = self:GetSettingValue( "speedType" )

	-- Return the coverted speed rounded to a whole number
	return UTIL:Round( speed * self.speedConversions[unit], 0 )
end

-- Returns/sets the validity of the given vehicle model
function RADAR:GetVehicleValidity( key ) return self.vars.validVehicles[key] end
function RADAR:SetVehicleValidity( key, validity ) self.vars.validVehicles[key] = validity end

-- Returns if vehicle validity data exists for the given vehicle model
function RADAR:DoesVehicleValidityExist( key )
	return self:GetVehicleValidity( key ) ~= nil
end

-- Returns if the given vehicle is valid, as we don't want the radar to detect boats, helicopters, or planes!
function RADAR:IsVehicleValid( veh )
	-- Get the model of the vehicle
	local mdl = GetEntityModel( veh )

	-- Create a key based on the model
	local key = tostring( mdl )

	-- Check if the vehicle model is valid
	local valid = self:GetVehicleValidity( key )

	-- If the validity value hasn't been set for the vehicle model, then we do it now
	if ( valid == nil ) then
		-- If the model is not what we want, then set the validity to false
		if ( IsThisModelABoat( mdl ) or IsThisModelAHeli( mdl ) or IsThisModelAPlane( mdl ) ) then
			self:SetVehicleValidity( key, false )
			return false
		else
			self:SetVehicleValidity( key, true )
			return true
		end
	end

	return valid
end

-- Gathers all of the vehicles in the local area of the player
function RADAR:GetAllVehicles()
	-- Create a temporary table
	local t = {}

	-- Iterate through vehicles
	for v in UTIL:EnumerateVehicles() do
		if ( self:IsVehicleValid( v ) ) then
			-- Insert the vehicle id into the temporary table
			table.insert( t, v )
		end
	end

	-- Return the table
	return t
end

-- Used to check if an antennas mode fits with a ray type from the ray trace system
function RADAR:CheckVehicleDataFitsMode( ant, rt )
	-- Get the current mode value for the given antenna
	local mode = self:GetAntennaMode( ant )

	-- Check that the given ray type matches up with the antenna's current mode
	if ( ( mode == 3 ) or ( mode == 1 and rt == "same" ) or ( mode == 2 and rt == "opp" ) ) then return true end

	-- Otherwise, return false as a last resort
	return false
end

-- This function is used to filter through the captured vehicles and work out what vehicles should be used for display
-- on the radar interface
function RADAR:GetVehiclesForAntenna()
	-- Create the vehs table to store the split up captured vehicle data
	local vehs = { ["front"] = {}, ["rear"] = {} }

	-- Create the results table to store the vehicle results, the first index is for the 'strongest' vehicle and the
	-- second index is for the 'fastest' vehicle
	local results = { ["front"] = { nil, nil }, ["rear"] = { nil, nil } }

	-- Loop through and split up the vehicles based on front and rear, this is simply because the actual system
	-- that gets all of the vehicles hit by the radar only has a relative position of either 1 or -1, which we
	-- then convert below into an antenna string!
	for ant in UTIL:Values( { "front", "rear" } ) do
		-- Check that the antenna is actually transmitting
		if ( self:IsAntennaTransmitting( ant ) ) then
			-- Iterate through the captured vehicles
			for k, v in pairs( self:GetCapturedVehicles() ) do
				-- Convert the relative position to antenna text
				local antText = self:GetAntennaTextFromNum( v.relPos )

				-- Check the current vehicle's relative position is the same as the current antenna
				if ( ant == antText ) then
					-- Insert the vehicle into the table for the current antenna
					table.insert( vehs[ant], v )
				end
			end

			-- As the radar is based on how the real Stalker DSR 2X works, we now sort the dataset by
			-- the 'strongest' (largest) target, this way the first result for the front and rear data
			-- will be the one that gets displayed in the target boxes.
			table.sort( vehs[ant], self:GetStrongestSortFunc() )
		end
	end

	-- Now that we have all of the vehicles split into front and rear, we can iterate through both sets and get
	-- the strongest and fastest vehicle for display
	for ant in UTIL:Values( { "front", "rear" } ) do
		-- Check that the table for the current antenna is not empty
		if ( not UTIL:IsTableEmpty( vehs[ant] ) ) then
			-- Get the 'strongest' vehicle for the antenna
			for k, v in pairs( vehs[ant] ) do
				-- Check if the current vehicle item fits the mode set by the user
				if ( self:CheckVehicleDataFitsMode( ant, v.rayType ) ) then
					-- Set the result for the current antenna
					results[ant][1] = v
					break
				end
			end

			-- Here we get the vehicle for the fastest section, but only if the user has the fast mode enabled
			-- in the operator menu
			if ( self:IsFastDisplayEnabled() ) then
				-- Get the 'fastest' vehicle for the antenna
				table.sort( vehs[ant], self:GetFastestSortFunc() )

				-- Create a temporary variable for the first result, reduces line length
				local temp = results[ant][1]

				-- Iterate through the vehicles for the current antenna
				for k, v in pairs( vehs[ant] ) do
					-- When we grab a vehicle for the fastest section, as it is like how the real system works, there are a few
					-- additional checks that have to be made
					if ( self:CheckVehicleDataFitsMode( ant, v.rayType ) and v.veh ~= temp.veh and v.size < temp.size and v.speed > temp.speed + 1.0 ) then
						-- Set the result for the current antenna
						results[ant][2] = v
						break
					end
				end
			end
		end
	end

	-- Return the results
	return { ["front"] = { results["front"][1], results["front"][2] }, ["rear"] = { results["rear"][1], results["rear"][2] } }
end


--[[----------------------------------------------------------------------------------
	NUI callback
----------------------------------------------------------------------------------]]--
-- Runs when the "Toggle Display" button is pressed on the remote control
RegisterNUICallback( "toggleRadarDisplay", function( data, cb )
	-- Toggle the display state
	RADAR:ToggleDisplayState()
	cb( "ok" )
end )

-- Runs when the user presses the power button on the radar ui
RegisterNUICallback( "togglePower", function( data, cb )
	if ( PLY:CanControlRadar() ) then
		if ( not RADAR:IsPoweringUp() ) then
			-- Toggle the radar's power
			RADAR:SetPowerState( not RADAR:IsPowerOn(), false )

			SYNC:SendPowerState( RADAR:IsPowerOn() )
		end
	end

	cb( "ok" )
end )

-- Runs when the user presses the ESC or RMB when the remote is open
RegisterNUICallback( "closeRemote", function( data, cb )
	-- Remove focus to the NUI side
	SetNuiFocus( false, false )

	if ( RADAR:IsMenuOpen() ) then
		RADAR:CloseMenu( false )
	end

	SYNC:SetRemoteOpenState( false )

	cb( "ok" )
end )

-- Runs when the user presses any of the antenna mode buttons on the remote
RegisterNUICallback( "setAntennaMode", function( data, cb )
	if ( PLY:CanControlRadar() ) then
		-- Only run the codw if the radar has power and is not powering up
		if ( RADAR:IsPowerOn() and not RADAR:IsPoweringUp() ) then
			-- As the mode buttons are used to exit the menu, we check for that
			if ( RADAR:IsMenuOpen() ) then
				RADAR:CloseMenu()
			else
				-- Change the mode for the designated antenna, pass along a callback which contains data from this NUI callback
				RADAR:SetAntennaMode( data.value, tonumber( data.mode ) )

				-- Sync
				SYNC:SendAntennaMode( data.value, tonumber( data.mode ) )
			end
		end
	end

	cb( "ok" )
end )

-- Runs when the user presses either of the XMIT/HOLD buttons on the remote
RegisterNUICallback( "toggleAntenna", function( data, cb )
	if ( PLY:CanControlRadar() ) then
		-- Only run the codw if the radar has power and is not powering up
		if ( RADAR:IsPowerOn() and not RADAR:IsPoweringUp() ) then
			-- As the xmit/hold buttons are used to change settings in the menu, we check for that
			if ( RADAR:IsMenuOpen() ) then
				-- Change the menu option based on which button is pressed
				RADAR:ChangeMenuOption( data.value )

				-- Play a beep noise
				SendNUIMessage( { _type = "audio", name = "beep", vol = RADAR:GetSettingValue( "beep" ) } )
			else
				-- Toggle the transmit state for the designated antenna, pass along a callback which contains data from this NUI callback
				RADAR:ToggleAntenna( data.value )

				-- Sync
				SYNC:SendAntennaPowerState( RADAR:IsAntennaTransmitting( data.value ), data.value )
			end
		end
	end

	cb( "ok" )
end )

-- Runs when the user presses the menu button on the remote control
RegisterNUICallback( "menu", function( data, cb )
	if ( PLY:CanControlRadar() ) then
		-- Only run the codw if the radar has power and is not powering up
		if ( RADAR:IsPowerOn() and not RADAR:IsPoweringUp() ) then
			-- As the menu button is a multipurpose button, we first check to see if the menu is already open
			if ( RADAR:IsMenuOpen() ) then
				-- As the menu is already open, we then iterate to the next option in the settings list
				RADAR:ChangeMenuIndex()
			else
				-- Set the menu state to open, which will prevent anything else within the radar from working
				RADAR:SetMenuState( true )

				-- Send an update to the NUI side
				RADAR:SendMenuUpdate()
			end

			-- Play the standard audio beep
			SendNUIMessage( { _type = "audio", name = "beep", vol = RADAR:GetSettingValue( "beep" ) } )
		end
	end

	cb( "ok" )
end )

-- Runs when the JavaScript side sends the UI data for saving
RegisterNUICallback( "saveUiData", function( data, cb )
	UTIL:Log( "Saving updated UI settings data." )
	SetResourceKvp( "wk_wars2x_ui_data", json.encode( data ) )
	cb( "ok" )
end )

-- Runs when the JavaScript side sends the quick start video has been watched
RegisterNUICallback( "qsvWatched", function( data, cb )
	SetResourceKvpInt( "wk_wars2x_new_user", 1 )
	cb( "ok" )
end )


--[[----------------------------------------------------------------------------------
	Main threads
----------------------------------------------------------------------------------]]--
-- Some people might not like the idea of the resource having a CPU MSEC over 0.10, but due to the functions
-- and the way the whole radar system works, it will use over 0.10 a decent amount. In this function, we
-- dynamically adjust the wait time in the main thread, so that when the player is driving their vehicle and
-- moving, the system doesn't run as fast so as to use less CPU time. When they have their vehicle
-- stationary, the system runs more often, which means that if a situation occurs such as a vehicle flying
-- past them at a high rate of speed, the system will be able to pick it up as it is running faster. Also, as
-- the user is stationary, if the system takes up an additional one or two frames per second, it won't really
-- be noticeable.
function RADAR:RunDynamicThreadWaitCheck()
	-- Get the speed of the local players vehicle
	local speed = self:GetPatrolSpeed()

	-- Check that the vehicle speed is less than 0.1
	if ( speed < 0.1 ) then
		-- Change the thread wait time to 200 ms, the trace system will now run five times per second
		self:SetThreadWaitTime( 200 )
	else
		-- Change the thread wait time to 500 ms, the trace system will now run two times a second
		self:SetThreadWaitTime( 500 )
	end
end

-- Create the thread that will run the dynamic thread wait check, this check only runs every two seconds
Citizen.CreateThread( function()
	while ( true ) do
		-- Run the function
		RADAR:RunDynamicThreadWaitCheck()

		-- Make the thread wait two seconds
		Citizen.Wait( 2000 )
	end
end )

-- This function handles the custom ray trace system that is used to gather all of the vehicles hit by
-- the ray traces defined in RADAR.rayTraces.
function RADAR:RunThreads()
	-- For the system to even run, the player needs to be sat in the driver's seat of a class 18 vehicle, the
	-- radar has to be visible and the power must be on, and either one of the antennas must be enabled.
	if ( PLY:CanViewRadar() and self:CanPerformMainTask() and self:IsEitherAntennaOn() ) then
		-- Before we create any of the custom ray trace threads, we need to make sure that the ray trace state
		-- is at zero, if it is not at zero, then it means the system is still currently tracing
		if ( self:GetRayTraceState() == 0 ) then
			-- Grab a copy of the vehicle pool
			local vehs = self:GetVehiclePool()

			-- Reset the main captured vehicles table
			self:ResetCapturedVehicles()

			-- Here we run the function that creates all of the main ray threads
			self:CreateRayThreads( PLY.veh, vehs )

			-- Make the thread this function runs in wait the dynamic time defined by the system
			Citizen.Wait( self:GetThreadWaitTime() )

		-- If the current ray trace state is the same as the total number of rays, then we reset the ray trace
		-- state back to 0 so the thread system can run again
		elseif ( self:GetRayTraceState() == self:GetNumOfRays() ) then
			-- Reset the ray trace state to 0
			self:ResetRayTraceState()
		end
	end
end

-- Create the main thread that will run the threads function, the function itself is run every frame as the
-- dynamic wait time is ran inside the function
Citizen.CreateThread( function()
	while ( true ) do
		-- Run the function
		RADAR:RunThreads()

		-- Make the thread wait 0 ms
		Citizen.Wait( 0 )
	end
end )

-- This is the main function that runs and handles all information that is sent to the NUI side for display, all
-- speed values are converted on the Lua side into a format that is displayable using the custom font on the NUI side
function RADAR:Main()
	-- Only run any of the main code if all of the states are met, player in the driver's seat of a class 18 vehicle, and
	-- the system has to be able to perform main tasks
	if ( PLY:CanViewRadar() and self:CanPerformMainTask() ) then
		-- Create a table that will be used to store all of the data to be sent to the NUI side
		local data = {}

		-- Get the player's vehicle speed
		local entSpeed = GetEntitySpeed( PLY.veh )

		-- Set the internal patrol speed to the speed obtained above, this is then used in the dynamic thread wait calculation
		self:SetPatrolSpeed( entSpeed )

		-- Change what is displayed in the patrol speed box on the radar interface depending on if the players vehicle is
		-- stationary or moving
		if ( entSpeed == 0 ) then
			data.patrolSpeed = "¦[]"
		else
			data.patrolSpeed = math.floor(entSpeed * 3.6 + 0.5)
		end

		-- Get the vehicles to be displayed for the antenna, then we take the results from that and send the relevant
		-- information to the NUI side
		local av = self:GetVehiclesForAntenna()
		data.antennas = { ["front"] = nil, ["rear"] = nil }

		-- Iterate through the front and rear data and obtain the information to be displayed
		for ant in UTIL:Values( { "front", "rear" } ) do
			-- Check that the antenna is actually transmitting, no point in running all the checks below if the antenna is off
			if ( self:IsAntennaTransmitting( ant ) ) then
				-- Create a table for the current antenna to store the information
				data.antennas[ant] = {}

				-- When the system works out what vehicles to be used, both the "front" and "rear" keys have two items located
				-- at index 1 and 2. Index 1 stores the vehicle data for the antenna's 'strongest' vehicle, and index 2 stores
				-- the vehicle data for the 'fastest' vehicle. Here we iterate through both the indexes and just run checks to
				-- see if it is a particular type (e.g. if i % 2 == 0 then it's the 'fastest' vehicle)
				for i = 1, 2 do
					-- Create the table to store the speed and direction for this vehicle data
					data.antennas[ant][i] = { speed = "¦¦¦", dir = 0 }

					-- If the current iteration is the number 2 ('fastest') and there's a speed locked, grab the locked speed
					-- and direction
					if ( i == 2 and self:IsAntennaSpeedLocked( ant ) ) then
						data.antennas[ant][i].speed = self:GetAntennaLockedSpeed( ant )
						data.antennas[ant][i].dir = self:GetAntennaLockedDir( ant )

					-- Otherwise, continue with getting speed and direction data
					else
						-- The vehicle data exists for this slot
						if ( av[ant][i] ~= nil ) then
							-- Here we get the entity speed of the vehicle, the speed for this vehicle would've been obtained
							-- and stored in the trace stage, but the speed would've only been obtained and stored once, which
							-- means that it woulsn't be the current speed
							local vehSpeed = GetEntitySpeed( av[ant][i].veh )
							local convertedSpeed = self:GetVehSpeedConverted( vehSpeed )
							data.antennas[ant][i].speed = UTIL:FormatSpeed( convertedSpeed )

							-- Work out if the vehicle is closing or away
							local ownH = UTIL:Round( GetEntityHeading( PLY.veh ), 0 )
							local tarH = UTIL:Round( GetEntityHeading( av[ant][i].veh ), 0 )
							data.antennas[ant][i].dir = UTIL:GetEntityRelativeDirection( ownH, tarH )

							-- Set the internal antenna data as this actual dataset is valid
							if ( i % 2 == 0 ) then
								self:SetAntennaFastData( ant, data.antennas[ant][i].speed, data.antennas[ant][i].dir )
							else
								self:SetAntennaData( ant, data.antennas[ant][i].speed, data.antennas[ant][i].dir )
							end

							-- Lock the speed automatically if the fast limit system is allowed
							if ( self:IsFastLimitAllowed() ) then
								-- Make sure the speed is larger than the limit, and that there isn't already a locked speed
								if ( self:IsFastLockEnabled() and convertedSpeed > self:GetFastLimit() and not self:IsAntennaSpeedLocked( ant ) ) then
									if ( ( self:OnlyLockFastPlayers() and UTIL:IsPlayerInVeh( av[ant][i].veh ) ) or not self:OnlyLockFastPlayers() ) then
										if ( PLY:IsDriver() ) then
											self:LockAntennaSpeed( ant, nil, false )
											SYNC:LockAntennaSpeed( ant, RADAR:GetAntennaDataPacket( ant ) )
										end
									end
								end
							end
						else
							-- If the active vehicle is not valid, we reset the internal data
							if ( i % 2 == 0 ) then
								self:SetAntennaFastData( ant, nil, nil )
							else
								self:SetAntennaData( ant, nil, nil )
							end
						end
					end
				end
			end
		end

		-- Send the update to the NUI side
		SendNUIMessage( { _type = "update", speed = data.patrolSpeed, antennas = data.antennas } )
	end
end

-- Main thread
Citizen.CreateThread( function()
	-- Remove the NUI focus just in case
	SetNuiFocus( false, false )

	-- Run the function to cache the number of rays, this way a hard coded number is never needed
	RADAR:CacheNumRays()

	-- Update the end coordinates for the ray traces based on the config, again, reduced hard coding
	RADAR:UpdateRayEndCoords()

	-- If the fast limit feature is allowed, create the config in the radar variables
	if ( RADAR:IsFastLimitAllowed() ) then
		RADAR:CreateFastLimitConfig()
	end

	-- Register the key binds
	RegisterKeyBinds()

	-- Wait a short period of time
	Citizen.Wait( 1000 )

	-- Load the saved UI settings (if available)
	LoadUISettings()

	-- Set the player's remoteOpen decorator to false
	DecorSetBool( PlayerPedId(), "wk_wars2x_sync_remoteOpen", false )

	-- Update the operator menu positions
	RADAR:UpdateOptionIndexes( true )

	-- Run the main radar function
	while ( true ) do
		RADAR:Main()

		Citizen.Wait( 100 )
	end
end )

-- This function is pretty much straight from WraithRS, it does the job so I didn't see the point in not
-- using it. Hides the radar UI when certain criteria is met, e.g. in pause menu or stepped out ot the
-- patrol vehicle
function RADAR:RunDisplayValidationCheck()
	if ( ( ( PLY.veh == 0 or ( PLY.veh > 0 and not PLY.vehClassValid ) ) and self:GetDisplayState() and not self:GetDisplayHidden() ) or IsPauseMenuActive() and self:GetDisplayState() ) then
		self:SetDisplayHidden( true )
		SendNUIMessage( { _type = "setRadarDisplayState", state = false } )
	elseif ( PLY:CanViewRadar() and self:GetDisplayState() and self:GetDisplayHidden() ) then
		self:SetDisplayHidden( false )
		SendNUIMessage( { _type = "setRadarDisplayState", state = true } )
	end
end

-- Runs the display validation check for the radar
Citizen.CreateThread( function()
	Citizen.Wait( 100 )

	while ( true ) do
		-- Run the check
		RADAR:RunDisplayValidationCheck()

		-- Wait half a second
		Citizen.Wait( 500 )
	end
end )

-- Update the vehicle pool every 3 seconds
function RADAR:UpdateVehiclePool()
	-- Only update the vehicle pool if we need to
	if ( PLY:CanViewRadar() and self:CanPerformMainTask() and self:IsEitherAntennaOn() ) then
		-- Get the active vehicle set
		local vehs = self:GetAllVehicles()

		-- Update the vehicle pool
		self:SetVehiclePool( vehs )
	end
end

-- Runs the vehicle pool updater
Citizen.CreateThread( function()
	while ( true ) do
		-- Update the vehicle pool
		RADAR:UpdateVehiclePool()

		-- Wait 3 seconds
		Citizen.Wait( 3000 )
	end
end )