MAIN = {}

local departments = nil
local department = nil
local isSupervisor = false
local reservation = nil
local allowExtend = true
local trackingBlip = nil
local trackingVehicleNetId = nil
local waitingOnSpawn = false
local blockedVehicleHashes = { }
local initializedMunicipalities = false

local directionLookup = {
    [0] = 'N', [22.5] = 'N', [45] = 'NE', [67.5] = 'NE',
    [90] = 'E', [112.5] = 'E', [135] = 'SE', [157.5] = 'SE',
    [180] = 'S', [202.5] = 'S', [225] = 'SW', [247.5] = 'SW',
    [270] = 'W', [292.5] = 'W', [315] = 'NW', [337.5] = 'NW',
    [360] = 'N'
}

--[[Resource Start: get existing reservation and list of vehicles to block in vMenu.]]
CreateThread(function()
	-- Wait for JS to initialize
	Wait(2000)
	UI:SendConfigVars()
	TriggerServerEvent('vms:GetExistingReservation')
	TriggerServerEvent('vms:GetBlockedVehicles')
	TriggerServerEvent('vms:GetIsPrivileged')
	TriggerServerEvent('vms:GetCurrentTable', false)
	
	-- Remove vehicles illegally spawned by other means.
	while true do
		Citizen.Wait(30000)
		local playerPed = PlayerPedId()
		local playerVehicle = GetVehiclePedIsIn(playerPed, false)
		if playerVehicle then
			if GetPedInVehicleSeat(playerVehicle, -1) == playerPed then
				local vehicleModel = GetEntityModel(playerVehicle)
				local vehicleNetId = VehToNet(playerVehicle)
				if blockedVehicleHashes[vehicleModel] ~= nil and (reservation == nil or reservation.vehicleNetId ~= vehicleNetId) then
					TriggerServerEvent('vms:ValidateVehicleLegitimacy', vehicleNetId)
				end
			end
		end
	end
end)

--------------------------------------------------------------------------------------
--[[	COMMANDS	]]
--------------------------------------------------------------------------------------

RegisterCommand('vms', function(source, args)
	if MAIN:CheckDepartment() then
		TriggerServerEvent('vms:GetCurrentTable', true)
	end
end, false)
TriggerEvent('chat:addSuggestion', '/vms', 'Open VMS checkout or existing reservation.')

RegisterCommand('vmsmanage', function(source, args)
	if MAIN:CheckDepartment() then
		MAIN:SetTabletUpdateRequests(true)
		UI:SetTabletState(true, department, isSupervisor)
	end
end, false)
TriggerEvent('chat:addSuggestion', '/vmsmanage', 'Open VMS tablet.')

RegisterCommand('vmsextend', function(source, args)
	MAIN:ExtendReservation(false)
end, false)
TriggerEvent('chat:addSuggestion', '/vmsextend', 'Request extension on current vehicle reservation.')

RegisterCommand('vmsrecovered', function(source, args)
	MAIN:SetStolenStatus(trackingVehicleNetId, false)
	MAIN:SetTracking(trackingVehicleNetId, false)
	UI:SetTrackingOverlayState(false)
	trackingVehicleNetId = nil
end, false)
TriggerEvent('chat:addSuggestion', '/vmsrecovered', 'Mark your vehicle as no longer stolen. Supervisor use tablet unless the vehicle is your own.')

RegisterCommand('vmsstoptracking', function(source, args)
	MAIN:SetTracking(trackingVehicleNetId, false)
	UI:SetTrackingOverlayState(false)
end, false)
TriggerEvent('chat:addSuggestion', '/vmsstoptracking', 'Disable tracking overlay and remove blip. This does not mark the vehicle as recovered see vmsrecovered.')


--------------------------------------------------------------------------------------
--[[	EVENTS		]]
--------------------------------------------------------------------------------------
--[[Update department data, open reservation if found, update checkout page if open, or open display.]]
RegisterNetEvent("vms:ReturnCurrentTable")
AddEventHandler("vms:ReturnCurrentTable", function(departmentsData, openDisplay, districtCoverageData)
	departments = departmentsData
	
	-- Drop unused data for non-MP departments.
	if department ~= "MP" then
		districtCoverageData = { }
	end
	
	if MAIN:CheckDepartment() and openDisplay then 
		local isCheckoutOpen = UI.isCheckoutOpen
		local isNearStation = MAIN:CheckProximityToStations()

		--	Has reservation and is not viewing allotment.
		if reservation ~= nil and not UI.isCheckoutOpen then
			-- Clunky approach to sending municipality data for selector gen after reservation recovery.
			if not initializedMunicipalities then
				UI:OpenDisplay(departmentsData[department], department, districtCoverageData)
				UI:CloseDisplay()
				initializedMunicipalities = true
			end
			
			UI:OpenReservation(reservation, department, isNearStation, districtCoverageData)
			return
		end
	
		if UI.isCheckoutOpen then
			-- Update department / vehicle counters.
			UI:SetDepartmentData(departmentsData[department])
		else
			if isNearStation then
				-- First open, near station.
				UI:OpenDisplay(departmentsData[department], department, districtCoverageData)
				initializedMunicipalities = true
			else
				UI:DisplayHelpMessage("~b~VMS~w~: no reservation found, please try again near a station.", 10)
			end
		end
	end
end)

--[[Validation Return]]
RegisterNetEvent("vms:ReturnValidationResult")
AddEventHandler("vms:ReturnValidationResult", function(result, vehicleData)
	if result == 1 then
		UI:SetLoadingState(true, 'Spawning Vehicle...')
		CreateThread(function()
			TriggerEvent("vMenu:SpawnVmsVehicle", vehicleData.fleetId, vehicleData.model, vehicleData.initLivery)
			waitingOnSpawn = true
			
			local timeout = GetGameTimer() + 10000
			while timeout > GetGameTimer() and waitingOnSpawn do
				Wait(100)
			end
			
			if waitingOnSpawn then
				TriggerEvent('vms:ReturnValidationResult', -2, nil)
			end
		end)
	elseif result == 0 then
		UI:SetLoadingState(false, '')
		UI:SetDialogActive("We're Sorry...", 'The vehicle you were reserving</br>is no longer available 😞')
	elseif result == -1 then
		UI:SetLoadingState(false, '')
		UI:SetDialogActive('Vehicle Occupied...', 'The vehicle cannot be occupied by another player.')
	elseif result == -2 then
		UI:SetLoadingState(false, '')
		UI:SetDialogActive('An Error Occurred...', 'The vehicle you requested could not be located. Please make a bug report.')
	elseif result == -3 then
		UI:SetLoadingState(false, '')
		UI:SetDialogActive('Out-of-Service Vehicle...', 'The vehicle you were respawning is marked</br>out-of-service and cannot be respawned.')
	end
end)

--[[vMenu return vehicle handle.]]
RegisterNetEvent("vms:VehicleSpawnedClient")
AddEventHandler("vms:VehicleSpawnedClient", function(reservationData)
	waitingOnSpawn = false
	Wait(1000)
	reservation = reservationData
	SetNetworkIdExistsOnAllMachines(NetToVeh(reservationData.vehicleNetId), true)
	UI:SetLoadingState(false, '')
	UI:CloseDisplay()
end)

--[[Check-In Vehicle Result.]]
RegisterNetEvent("vms:CheckInVehicleResult")
AddEventHandler("vms:CheckInVehicleResult", function(result)
	if result == 1 then
		UI:SetLoadingState(false, '')
		UI:SetDialogActive('Success', 'Vehicle checked in. Thank you.')
		reservation = nil;
		allowExtend = true
	elseif result == 0 then
		UI:SetLoadingState(false, '')
		UI:SetDialogActive('An Error Occurred...', 'We could not locate a reservation for that vehicle or you. Please make a bug report.')
	elseif result == -1 then
		UI:SetLoadingState(false, '')
		UI:SetDialogActive('An Error Occurred...', 'Reservation found, but failed to check-in the vehicle. Please make a bug report.')
	end
end)

--[[Recover current reservation.]]
RegisterNetEvent("vms:GetExistingReservation")
AddEventHandler("vms:GetExistingReservation", function(departmentData, reservationData)
	departments = departmentData
	department = reservationData.department
	reservation = reservationData
	UI:SetDepartmentData(departments[department])
	--	Create thread to recover reservation by entering
	CreateThread(MAIN.RecoveryVehicleCheck)
end)

--[[Update reservation timer/status.]]
-- CASE:
--		Recover Vehicle --> Update reservation timer display (false, nil)
--		Expiring Timer > 0 --> Display warning update reservation timer display (timer, nil)
--		Expiring Timer == 0 --> Clear reservation, close display, notify user via dialog or notification.
--		Extend Timer --> Update reservation timer display (timer=30, nil)
--		Set Stolen Status --> Display stolen status update notification, clear reservation, reset extend, close UI if reporting stolen.
RegisterNetEvent("vms:UpdateReservationStatus")
AddEventHandler("vms:UpdateReservationStatus", function(timer, clearReservation, stolen)
	clearReservation = clearReservation or false
	stolen = stolen or false

	if clearReservation and timer == 0 then
		reservation = nil
		allowExtend = true
		if not stolen then
			if UI.isReservationOpen then
				if MAIN:CheckProximityToStations() then
					-- clear this screen to prepare for the incoming OpenDisplay, otherwise we're left with a dangling dialog
					UI:CloseDisplay()
				end
				TriggerServerEvent('vms:GetCurrentTable', true)
				UI:SetDialogActive('Reservation Expired', 'Your vehicle was checked-in.')
			else
				UI:DisplayHelpMessage("~b~VMS~w~: your vehicle was ~o~checked-in~w~.", 10)
			end
		else
			if UI.isReservationOpen then
				UI:CloseDisplay()
			end
			UI:DisplayHelpMessage("~b~VMS~w~: your vehicle was ~o~reported as stolen~w~.", 10)
		end
	else
		-- If has reservation then store time for reopening.
		if reservation then
			reservation.timer = timer
		end

		-- Display notification message, for both reservation holders and occupants when blocked.
		if UI.isReservationOpen then
			UI:UpdateReservationStatus(timer)
		else
			if timer == 14 then
				UI:DisplayHelpMessage("~b~VMS~w~: your vehicle reservation is set to expire.", 10)
			elseif timer == 5 then
				UI:DisplayHelpMessage("~b~VMS~w~: your vehicle reservation will ~o~expire soon~w~.", 10)
			elseif timer == 1 then
				UI:DisplayHelpMessage("~b~VMS~w~: your vehicle reservation will ~o~expire soon~w~.", 30)
			elseif timer == 0 then
				UI:DisplayHelpMessage("~b~VMS~w~: your vehicle will be deleted upon exit, please check it in.", 10)
			end
		end
	end
end)

--[[Get list of vehicle models to block spawning.]]
RegisterNetEvent("vms:ReceiveBlockedVehicles")
AddEventHandler("vms:ReceiveBlockedVehicles", function(blockedVehicles, blockedHashes)
	TriggerEvent("vMenu:SetVmsBlockedVehicles", blockedVehicles)
	blockedVehicleHashes = blockedHashes
end)

--[[Gets checkedOutData for tablet.]]
RegisterNetEvent("vms:TabletDataReturn")
AddEventHandler("vms:TabletDataReturn", function(tabletData)
	local enumCheckedOut = { }
	
	-- If tracking modify data to represent the state for datatable building.
	if trackingVehicleNetId and tabletData.checkedOutData[trackingVehicleNetId] then
		tabletData.checkedOutData[trackingVehicleNetId].tracking = true
	end
	
	-- Remove Keys
	for vehicleNetId, reservationData in pairs(tabletData.checkedOutData) do
		reservationData.vehicleNetId = vehicleNetId
		table.insert(enumCheckedOut, reservationData)
	end
	for index, reservationData in ipairs(enumCheckedOut) do
		local position = reservationData.location

		if position.z ~= 0.000000 then
			local streetHash1, _ = GetStreetNameAtCoord(position.x, position.y, position.z)
			local streetName1 = GetStreetNameFromHashKey(streetHash1)
			-- CHANGEME export resource
			local postal = exports.postals:getNearestPostalFromCoordinates(position.x, position.y, position.z)
			
			enumCheckedOut[index].location = string.format('%s %s', postal, streetName1)
		else
			enumCheckedOut[index].location = "INITIALIZING..."
			enumCheckedOut[index].locationAge = 0
			enumCheckedOut[index].speed = '---'
			enumCheckedOut[index].direction = '---'
		end
	end
	
	tabletData.checkedOutData = enumCheckedOut
	UI:SendTabletRecords(tabletData)
end)

--[[Return results from a passed vmsextend]]
RegisterNetEvent("vms:ExtendReservationReturn")
AddEventHandler("vms:ExtendReservationReturn", function(result, isFromNUI)
	if result == 1 then
		if isFromNUI then
			UI:SetDialogActive("Success", 'Your reservation has been extended to 30 minutes.')
		else
			UI:DisplayHelpMessage("~b~VMS~w~: your reservation has been extended to 30 minutes.", 10)
		end
		allowExtend = false
	elseif result == 0 then
		if isFromNUI then
			UI:SetDialogActive("We're Sorry...", 'You can not extend your reservation yet.')
		else
			UI:DisplayHelpMessage("~b~VMS~w~: you cannot extend a reservation yet.", 10)
		end
	end
end)

--[[If supervisor, this sets a variable, otherwise no return.]]
RegisterNetEvent("vms:IsPrivilegedReturn")
AddEventHandler("vms:IsPrivilegedReturn", function(isSupervisorReturn, isMediaReturn, rewardsModels)
	-- Access to management buttons of tablet.
	isSupervisor = isSupervisorReturn
	
	-- Set media override in JS, so that vehicle buttons are not disabled when at limit.
	if isMediaReturn or rewardsModels then
		UI:SetBypassInfo(isMediaReturn, rewardsModels)
	end
end)

--[[Updated tracking data.]]
RegisterNetEvent("vms:TrackingDataReturn")
AddEventHandler("vms:TrackingDataReturn", function(trackingData)
	local position = trackingData.location
	if position.z ~= 0.000000 then
		local streetHash1, _ = GetStreetNameAtCoord(position.x, position.y, position.z)
		local streetName1 = GetStreetNameFromHashKey(streetHash1)
		local postal = exports.postals:getNearestPostalFromCoordinates(position.x, position.y, position.z)
		trackingData.location = string.format('%s %s', postal, streetName1)

		-- Route GPS and add blip
		if not trackingBlip then
			trackingBlip = AddBlipForCoord(position.x, position.y, 0.0)
			SetBlipRoute(trackingBlip, true)
			SetBlipSprite(trackingBlip, 672)
			SetBlipColour(trackingBlip, 1)
			SetBlipRouteColour(trackingBlip, 1)
			BeginTextCommandSetBlipName('STRING')
			AddTextComponentSubstringPlayerName(string.format('STOLEN - %s', trackingData.model))
			EndTextCommandSetBlipName(trackingBlip)
		else
			SetBlipCoords(trackingBlip, position.x, position.y, 0.0)
			SetBlipRoute(trackingBlip, true)
		end
	else
		trackingData.location = "ATTEMPTING TO LOCATE..."
		trackingData.locationAge = 0
		trackingData.speed = '---'
		trackingData.direction = '---'
	end
		
	UI:SetTrackingOverlayData(trackingData)
end)

--[[Server side terminate tracking such as reservation lost.]]
RegisterNetEvent("vms:TerminateTracking")
AddEventHandler("vms:TerminateTracking", function(vehicleNetId)
	MAIN:SetTracking(nil, false, true)
	UI:SetTrackingOverlayState(false)
	trackingVehicleNetId = nil
	UI:DisplayHelpMessage("~b~VMS~w~: The vehicle you were tracking no longer exists.", 10)
end)

--[[Server side vehicle validation return.]]
RegisterNetEvent("vms:FailVehicleLegitimacy")
AddEventHandler("vms:FailVehicleLegitimacy", function()
	print('^3Unregistered Vehicle: Your vehicle has been deleted it was not spawned using VMS as required.')
	UI:ShowNotification("Your vehicle has been deleted it was not spawned using VMS as required.")
end)

--------------------------------------------------------------------------------------
--[[	FUNCTIONS		]]
--------------------------------------------------------------------------------------

--[[Get Department and Ensure we have a department config]]
function MAIN:CheckDepartment()
	department = string.upper(exports["framework"]:getCharacterInfo(5))

	if departments == nil then
		print('^3VMS: department data missing. Please file a bug report.')
		UI:ShowNotification("~b~VMS:~s~ something went wrong, department data missing.")
		return false
	end

	if department == nil then
		print('^3VMS: failed to retrieve department')
		UI:ShowNotification("~b~VMS:~s~ something went wrong, failed to retrieve current department.")
		return false
	end
	
	if departments[department] == nil then
		print('^3VMS: department table not found', department)
		UI:ShowNotification(string.format("~b~VMS:~s~ %s does not have any vehicles.", department))
		return false
	end
	
	return true
end

function MAIN:ValidateCheckout(data)
	UI:SetLoadingState(true, 'Reserving Vehicle...')
	TriggerServerEvent('vms:ValidateCheckout', data.fleetId, department, data.municipality)
end

function MAIN:SetUpdateRequests(state)
	TriggerServerEvent('vms:SetUpdateRequests', state)
end

function MAIN:SetTabletUpdateRequests(state)
	TriggerServerEvent('vms:SetTabletUpdateRequests', state)
end

function MAIN:CheckInVehicle()
	UI:SetLoadingState(true, 'Attempting to check-in your vehicle...')
	TriggerServerEvent('vms:CheckInVehicle', reservation.vehicleNetId)
end

function MAIN:RespawnVehicle()
	UI:SetLoadingState(true, 'Respawning Vehicle...')
	TriggerServerEvent('vms:RespawnVehicle')
end

function MAIN:RecoveryVehicleCheck()
	local recoveryVehicle = reservation.vehicleNetId
	local remainingTime = reservation.timer
	
	local playerVehicle = nil
	local timeout = GetGameTimer() + remainingTime * 60000
	
	-- For remaining time from vms:GetExistingReservation
	--	Check if back in vehicle.
	while timeout > GetGameTimer() do
		-- reservation no longer exists kill thread
		if reservation == nil then
			print('Reservation no longer exists.')
			return
		end
		
		--status renewed exists kill thread
		if reservation.timer == false then
			return
		end

		playerVehicle = GetVehiclePedIsIn(PlayerPedId(), false)
		if playerVehicle ~= 0 then
			if playerVehicle == NetToVeh(recoveryVehicle) then
				TriggerServerEvent('vms:RecoveredReservation')
				return
			end
		end
		Wait(1000)
	end
end

-- Pass OOS data to server
function MAIN:SubmitNewOOS(oosData)
	TriggerServerEvent('vms:SubmitNewOOS', oosData)
end

-- Requests extension on reservation timer -> 30 minutes
function MAIN:ExtendReservation(isFromNUI)
	local isFromNUI = isFromNUI or false
	
	if not reservation then
		if isFromNUI then
			UI:SetDialogActive("An error occurred...", 'Your reservation was not found.')
		else
			UI:DisplayHelpMessage("~b~VMS~w~: no reservation found, that's weird.", 10)
		end
		return
	end

	if allowExtend then
		TriggerServerEvent('vms:ExtendReservation', reservation.vehicleNetId, isFromNUI)
	else
		if isFromNUI then
			UI:SetDialogActive("We're sorry...", 'We are unable to extend your reservation again.')
		else
			UI:DisplayHelpMessage("~b~VMS~w~: sorry, we are unable to extend your reservation again.", 10)
		end
	end
end

-- Toggle tracking enrollment
function MAIN:SetTracking(vehicleNetId, state, skipServerEvent)
	skipServerEvent = skipServerEvent or false
	
	if not skipServerEvent then
		TriggerServerEvent('vms:SetTracking', vehicleNetId, state)
	end
	
	if not state then
		if trackingBlip then
			RemoveBlip(trackingBlip)
			trackingBlip = nil
		end
	else
		trackingVehicleNetId = vehicleNetId
	end
end

-- Marks vehicle as stolen/recovered
function MAIN:SetStolenStatus(vehicleNetId, state)
	vehicleNetId = vehicleNetId or (reservation and reservation.vehicleNetId)
	
	if vehicleNetId then
		if state then
			trackingVehicleNetId = vehicleNetId
		end
		
		UI:SetTrackingOverlayState(state)
		TriggerServerEvent('vms:SetStolenStatus', vehicleNetId, state)
	else
		if UI.isReservationOpen then
			UI:SetDialogActive('We\'re Sorry...', 'We could not locate that vehicle.')
		else
			UI:DisplayHelpMessage("~b~VMS~w~: an error occurred, unable to report stolen.", 10)
		end
	end
end

-- Returns if the player is currently near a station and able to spawn a vehicle.
function MAIN:CheckProximityToStations()
	if not config.enforceSpawnLocations then
		return true
	end

	local playerCoords = GetEntityCoords(PlayerPedId())
	for _, location in ipairs(config.spawnLocations[department]) do
		if #(playerCoords - location.coords) < location.radius then
			return true
		end
	end
	
	return false
end