local version = "{{OLYMPUS_VERSION_NUMBER}}.{{OLYMPUS_COMMIT_HASH}}"

local debug = false				-- True enables debug printing using DCS messages

-- .dll related variables
Olympus.OlympusDLL = nil
Olympus.DLLsloaded = false

-- Logger reference
Olympus.log = mist.Logger:new("Olympus", 'info')

-- Data structures for transfer to .dll
Olympus.missionData = {}
Olympus.unitsData = {}
Olympus.weaponsData = {}
Olympus.drawingsByLayer = {}
Olympus.executionResults = {}

-- Units data structures
Olympus.unitCounter = 1			-- Counter to generate unique names
Olympus.cloneDatabase = {}		-- Database of spawn options, used for units cloning
Olympus.unitIndex = 0			-- Counter used to spread the computational load of data retrievial from DCS
Olympus.unitStep = 50			-- Max number of units that get updated each cycle
Olympus.units = {}				-- Table holding references to all the currently existing units
Olympus.unitsInitialLife = {}	-- getLife0 returns 0 for ships, so we need to store the initial life of units
Olympus.drawArguments = {}      -- Table that sets what drawArguments to read for each unit

Olympus.weaponIndex = 0			-- Counter used to spread the computational load of data retrievial from DCS			
Olympus.weaponStep = 50			-- Max number of weapons that get updated each cycle
Olympus.weapons = {}			-- Table holding references to all the currently existing weapons

-- Spots (laser/IR) data
Olympus.spots = {}
Olympus.spotsCounter = 1

-- Miscellaneous initializations
Olympus.missionStartTime = DCS.getRealTime()
Olympus.napalmCounter = 1
Olympus.fireCounter = 1

-- Load the lua file system
local lfs = require('lfs')

------------------------------------------------------------------------------------------------------
-- Olympus functions
------------------------------------------------------------------------------------------------------
-- Print a debug message if the debug option is true
function Olympus.debug(message, displayFor)
	if debug == true then
		Olympus.log:info(message)
    	trigger.action.outText(message, displayFor)
	end
end

-- Print a notify message
function Olympus.notify(message, displayFor)
	Olympus.log:info(message)
    trigger.action.outText(message, displayFor)
end

-- Loads the olympus .dll
function Olympus.loadDLLs()
	-- Add the .dll paths
	package.cpath = package.cpath..';'..Olympus.instancePath..'?.dll;'
	
	local status
	status, Olympus.OlympusDLL = pcall(require, 'olympus')
	if status then
		return true
	else
		return false
	end	
end

-- Gets a unit class reference from a given ObjectID (the ID used by Olympus for unit referencing)
function Olympus.getUnitByID(ID)
	return Olympus.units[ID];
end

-- Gets the ID of the first country that belongs to a coalition
function Olympus.getCountryIDByCoalition(coalitionString)
	for countryName, countryId in pairs(country.id) do
		if coalition.getCountryCoalition(countryId) == Olympus.getCoalitionIDByCoalition(coalitionString) then
			return countryId
		end
	end
	return 0
end

-- Gets the coalition ID of a coalition
function Olympus.getCoalitionIDByCoalition(coalitionString)
	local coalitionID = 0
	if coalitionString == "red" then 
		coalitionID = 1
	elseif coalitionString == "blue" then
		coalitionID = 2
	end
	return coalitionID
end

-- Gets the coalition name from the coalition ID
function Olympus.getCoalitionByCoalitionID(coalitionID)
	local coalitionString = "neutral"
	if coalitionID == 1 then 
		coalitionString = "red"
	elseif coalitionID == 2 then
		coalitionString = "blue"
	end
	return coalitionString
end

-- Builds a valid enroute task depending on the provided options
function Olympus.buildEnrouteTask(options)
	local task = nil
	-- Engage specific target by ID. Checks if target exists.
	if options['id'] == 'EngageUnit' and options['targetID'] then
		local target = Olympus.getUnitByID(options['targetID'])
		if target and target:isExist() then
			task = { 
				id = 'EngageUnit', 
				params = { 
					unitId = options['targetID'],
				} 
			}
		end
	-- Start being an active tanker
	elseif options['id'] == 'Tanker' then
		task = { 
			id = 'Tanker', 
			params = {},
		}
	-- Start being an active AWACS
	elseif options['id'] == 'AWACS' then
		task = { 
			id = 'AWACS', 
			params = {},
		}
	end
	return task
end

-- Builds a valid main task depending on the provided options
function Olympus.buildTask(groupName, options)
	local task = nil

	local group = Group.getByName(groupName)

	-- Combo tasks require nested tables
	if (Olympus.isArray(options)) then
		local tasks = {}
		for idx, subOptions in pairs(options) do
			tasks[idx] = Olympus.buildTask(groupName, subOptions) or Olympus.buildEnrouteTask(subOptions)
		end
		task = { 
			id = 'ComboTask', 
			params = { 
				tasks = tasks
			} 
		} 
		Olympus.debug(Olympus.serializeTable(task), 30)
	else 
		-- Follow a unit in formation with a given offset
		if options['id'] == 'FollowUnit' and options['leaderID'] and options['offset'] then
			local leader = Olympus.getUnitByID(options['leaderID'])
			if leader and leader:isExist() then
				task = {
					id = 'Follow',
					params = {
						groupId = leader:getGroup():getID(),
						pos = options['offset'],
						lastWptIndexFlag = false,
						lastWptIndex = 1
					}    
				}
			end
		-- Go refuel to the nearest tanker. If the unit can't refuel it will RTB
		elseif options['id'] == 'Refuel' then
			task = {
				id = 'Refueling', 
				params = {}   
			}
		-- Orbit in place at a given altitude and with a given pattern
		elseif options['id'] == 'Orbit' then
			task = { 
				id = 'Orbit', 
				params = { 
					pattern = options['pattern'] or "Circle"
				} 
			}

			if options['pattern'] == 'Race-Track' then
				local heading = options['heading'] or 0
				local length = options['length'] or 20000
				if group ~= nil then
					local groupPos = mist.getLeadPos(group)
					task['params']['point'] = {x = groupPos.x, y = groupPos.z}
					task['params']['point2'] = {x = groupPos.x + math.cos(heading) * length, y = groupPos.z + math.sin(heading) * length}
				end
			end

			-- Compute the altitude depending on the altitude type 
			if options['altitude'] then
				if options ['altitudeType'] then
					if options ['altitudeType'] == "AGL" then
						local groundHeight = 0
						if group ~= nil then
							local groupPos = mist.getLeadPos(group)
							groundHeight = land.getHeight({x = groupPos.x, y = groupPos.z})
						end
						task['params']['altitude'] = groundHeight + options['altitude']
					else
						task['params']['altitude'] = options['altitude']
					end
				else
					task['params']['altitude'] = options['altitude']
				end
			end

			-- Compute the speed depending on the speed type. CAS calculation is only available if the altitude is also available 
			if options['speed'] then
				-- Simplified formula to compute CAS from GS
				local speed = options['speed']
				if options['speedType'] and options['speedType'] == "CAS" and task['params']['altitude'] then
					speed = speed * (1 + 0.02 * task['params']['altitude'] / 0.3048 / 1000)
				end
				task['params']['speed'] = speed
			end
		-- Bomb a specific location
		elseif options['id'] == 'Bombing' and options['lat'] and options['lng'] then
			local point = coord.LLtoLO(options['lat'], options['lng'], 0)
			task = {
				id = 'Bombing', 
				params = {
					point = {x = point.x, y = point.z},
					attackQty = 1
				}   
			}
		-- Perform carpet bombing at a specific location
		elseif options['id'] == 'CarpetBombing' and options['lat'] and options['lng'] then
			local point = coord.LLtoLO(options['lat'], options['lng'], 0)
			task = {
				id = 'CarpetBombing', 
				params = {
					x = point.x,
					y = point.z,
					carpetLength = 1000,
					attackType = 'Carpet',
					expend = "All",
					attackQty = 1,
					attackQtyLimit = true 
				}   
			}
		-- Fire at a specific point
		elseif options['id'] == 'FireAtPoint' and options['lat'] and options['lng'] and options['radius'] then
			local point = coord.LLtoLO(options['lat'], options['lng'], 0)
			local expendQtyEnabled = false
			local expendQty = 0

			if options['expendQty'] then
				expendQtyEnabled = true
				expendQty = options['expendQty']
			end

			if options['alt'] then
				task = {
					id = 'FireAtPoint', 
					params = {
						point = {x = point.x, y = point.z},
						radius = options['radius'],
						altitude = options['alt'],
						alt_type = 0, -- ASL
						expendQtyEnabled = expendQtyEnabled,
						expendQty = expendQty
					}   
				}
			else
				task = {
					id = 'FireAtPoint', 
					params = {
						point = {x = point.x, y = point.z},
						radius = options['radius'],
						expendQtyEnabled = expendQtyEnabled,
						expendQty = expendQty
					}   
				}
			end
		-- Land at a specific point
		elseif options['id'] == 'LandAtPoint' then
			local point = coord.LLtoLO(options['lat'], options['lng'], 0)
			task = {
				id = 'Land', 
				params = {
					point = {x = point.x, y = point.z},
				}   
			}
		-- Attack unit
		elseif options['id'] == 'AttackUnit' and options['unitID']  then
			task = {
				id = 'AttackUnit', 
				params = {
					unitId = options['unitID'],
				}   
			}
		end
	end
	return task
end

-- Move a unit. Since many tasks in DCS are Enroute tasks, this function is an important way to control the unit AI
function Olympus.move(groupName, lat, lng, altitude, altitudeType, speed, speedType, category, taskOptions)
    Olympus.debug("Olympus.move " .. groupName .. " (" .. lat .. ", " .. lng ..") " .. altitude .. "m " .. altitudeType .. " ".. speed .. "m/s " .. category .. " " .. Olympus.serializeTable(taskOptions), 2)
    local group = Group.getByName(groupName)
	if group ~= nil then
		if category == "Aircraft" then
			local startPoint = mist.getLeadPos(group)
			local endPoint = coord.LLtoLO(lat, lng, 0) 

			-- 'AGL' mode does not appear to work in the buildWP function. This is a crude approximation
			if altitudeType == "AGL" then
				altitude = land.getHeight({x = endPoint.x, y = endPoint.z}) + altitude
			end

			-- Simplified formula to compute CAS from GS
			if speedType == "CAS" then
				speed = speed * (1 + 0.02 * altitude / 0.3048 / 1000)
			end

			-- Create the path
			local path = {
				[1] = mist.fixedWing.buildWP(startPoint, turningPoint, speed, altitude, 'BARO'),
				[2] = mist.fixedWing.buildWP(endPoint, turningPoint, speed, altitude, 'BARO')
			}

			-- If a task exists assign it to the controller
			if taskOptions then
				local task = Olympus.buildEnrouteTask(taskOptions)
				if task then 
					path[1].task = task
					path[2].task = task
				end
			end
			
			-- Assign the mission task to the controller
			local missionTask = {
				id = 'Mission',
				params = {
					route = {
						points = mist.utils.deepCopy(path),
					},
				},
			}
			local groupCon = group:getController()
			if groupCon then
				groupCon:setTask(missionTask)
			end
			Olympus.debug("Olympus.move executed successfully on Aircraft", 2)
		elseif category == "Helicopter" then
			local startPoint = mist.getLeadPos(group)
			local endPoint = coord.LLtoLO(lat, lng, 0) 

			-- 'AGL' mode does not appear to work in the buildWP function. This is a crude approximation
			if altitudeType == "AGL" then
				altitude = land.getHeight({x = endPoint.x, y = endPoint.z}) + altitude
			end

			-- Simplified formula to compute CAS from GS
			if speedType == "CAS" then
				speed = speed * (1 + 0.02 * altitude / 0.3048 / 1000)
			end

			-- Create the path
			local path = {
				[1] = mist.heli.buildWP(startPoint, turningPoint, speed, altitude, 'BARO'),
				[2] = mist.heli.buildWP(endPoint, turningPoint, speed, altitude, 'BARO')
			}

			-- If a task exists assign it to the controller
			if taskOptions then
				local task = Olympus.buildEnrouteTask(taskOptions)
				if task then 
					path[1].task = task
					path[2].task = task
				end
			end
			
			-- Assign the mission task to the controller
			local missionTask = {
				id = 'Mission',
				params = {
					route = {
						points = mist.utils.deepCopy(path),
					},
				},
			}
			local groupCon = group:getController()
			if groupCon then
				groupCon:setTask(missionTask)
			end
			Olympus.debug("Olympus.move executed successfully on Helicopter", 2)
		elseif category == "GroundUnit" then
			local endPoint = coord.LLtoLO(lat, lng, 0) 

			local action = "Off Road"
			local disableRoads = true
			if taskOptions and taskOptions['id'] == 'FollowRoads' and taskOptions['value'] == true then
				action = "On Road"
				disableRoads = false
			end
			
			missionTask = { 
				id = 'Mission', 
				params = { 
					route = { 
						points = { 
							[1] = { 
								type = "Turning Point",
								action = action,
								disableRoads = disableRoads,
								x = endPoint.x, 
								y = endPoint.z, 
								speed = speed, 
								speed_locked = false, 
								ETA_locked = false, 
								name = 'Mission1', 
							},
							[2] = { 
								type = "Turning Point",
								action = action,
								disableRoads = disableRoads,
								x = endPoint.x, 
								y = endPoint.z, 
								speed = speed, 
								speed_locked = false, 
								ETA_locked = false, 
								name = 'Mission1', 
							}, 
						} 
					}, 
				} 
			} 
			local groupCon = group:getController()
			if groupCon then
				groupCon:setTask(missionTask)
			end
			Olympus.debug("Olympus.move executed successfully on GroundUnit", 2)
		elseif category == "NavyUnit" then
			local startPoint = mist.getLeadPos(group)
			local endPoint = coord.LLtoLO(lat, lng, 0) 
			local bearing = math.atan2(endPoint.z - startPoint.z, endPoint.x - startPoint.x)
			
			vars = {
				group = group, 
				point = endPoint,
				heading = bearing,
				speed = speed
			}
			mist.groupToRandomPoint(vars)
			Olympus.debug("Olympus.move executed successfully on NavyUnit", 2)
		else
			Olympus.debug("Olympus.move not implemented yet for " .. category, 2)
		end
	else
        Olympus.debug("Error in Olympus.move " .. groupName, 2)
	end
end  

-- Creates a simple smoke on the ground
function Olympus.smoke(color, lat, lng)
    Olympus.debug("Olympus.smoke " .. color .. " (" .. lat .. ", " .. lng ..")", 2)
	local colorEnum = nil
	if color == "green" then
		colorEnum = trigger.smokeColor.Green
	elseif color == "red" then
		colorEnum = trigger.smokeColor.Red
	elseif color == "white" then 
		colorEnum = trigger.smokeColor.White
	elseif color == "orange" then 
		colorEnum = trigger.smokeColor.Orange
	elseif color == "blue" then
		colorEnum = trigger.smokeColor.Blue
	end
    trigger.action.smoke(mist.utils.makeVec3GL(coord.LLtoLO(lat, lng, 0)), colorEnum)
end  

-- Creates an explosion on the ground
function Olympus.explosion(intensity, explosionType, lat, lng, alt)
    Olympus.debug("Olympus.explosion " .. explosionType .. " " .. intensity .. " (" .. lat .. ", " .. lng .. ")", 2)

	local vec3 = nil
	if alt ~= nil then
		vec3 = coord.LLtoLO(lat, lng, alt)
	else
		vec3 = mist.utils.makeVec3GL(coord.LLtoLO(lat, lng))
	end

	if explosionType == "normal" then
		trigger.action.explosion(vec3, intensity)
	elseif explosionType == "phosphorous" then
		Olympus.phosphorous(vec3)
	elseif explosionType == "napalm" then
		Olympus.napalm(vec3)
	elseif explosionType == "secondary" then
		Olympus.secondaries(vec3)
	elseif explosionType == "fire" then
		Olympus.createFire(vec3)
	elseif explosionType == "depthCharge" then

	end
end  

function Olympus.phosphorous(vec3) 
	trigger.action.explosion(vec3, 1)
	for i =	1,math.random(3, 10) do 
		angle = mist.utils.toRadian((math.random(1, 360)))
		local randVec = mist.utils.makeVec3GL((mist.getRandPointInCircle(vec3, 5, 1, 0, 360)))
		trigger.action.signalFlare(randVec, 2, angle)
	end
end

function Olympus.napalm(vec3)
	local napeName = "napalmStrike" .. Olympus.napalmCounter
		Olympus.napalmCounter = Olympus.napalmCounter + 1
		mist.dynAddStatic(
							{
								country = 20,
								category = 'Fortifications',
								hidden = true,
								name = napeName,
								type ="Fuel tank",
								x = vec3.x,
								y = vec3.z,
								heading = 0,
							} -- end of function
						)
		timer.scheduleFunction(Olympus.explodeNapalm, vec3, timer.getTime() + 0.1)
		timer.scheduleFunction(Olympus.removeNapalm, napeName, timer.getTime() + 0.12)
end

function Olympus.explodeNapalm(vec3)
	trigger.action.explosion(vec3, 10)
end

function Olympus.removeNapalm(staticName) 
	StaticObject.getByName(staticName):destroy()
end

function Olympus.createFire(vec3)
	local smokeName = "smokeName" .. Olympus.fireCounter
	Olympus.fireCounter = Olympus.fireCounter + 1
	trigger.action.effectSmokeBig(vec3, 2 , 1, smokeName)
	trigger.action.explosion(vec3, 1) -- looks wierd to spawn in on flat land without this
	timer.scheduleFunction(Olympus.removeFire, smokeName, timer.getTime() + 20)
end

function Olympus.removeFire (smokeName)
	trigger.action.effectSmokeStop(smokeName)
end

function Olympus.secondaries(vec3) 
	Olympus.randomDebries(vec3)
	--trigger.action.explosion(vec3, 1)
	--for i =	1, 10 do 
	--	timer.scheduleFunction(Olympus.randomDebries, vec3, timer.getTime() + math.random(0, 180))
	--end
end

function Olympus.randomDebries(vec3) 
	trigger.action.explosion(vec3, 1)
	for i =	1,math.random(3, 10) do
		angle = mist.utils.toRadian((math.random(1, 360)))
		local randVec = mist.utils.makeVec3GL((mist.getRandPointInCircle(vec3, 5, 1, 0, 360)))
		trigger.action.signalFlare(randVec, 3, angle)
	end
end

-- Shines a laser from a unit to a point
function Olympus.fireLaser(ID, code, lat, lng)
    Olympus.debug("Olympus.fireLaser " .. ID .. " -> (" .. lat .. ", " .. lng .. ") code " .. code, 2)

	local vec3 = mist.utils.makeVec3GL(coord.LLtoLO(lat, lng))

	local unit = Olympus.getUnitByID(ID)
	if unit ~= nil and unit:isExist() then
		local spot = Spot.createLaser(unit, {x = 0, y = 1, z = 0}, vec3, code)
		Olympus.spotsCounter = Olympus.spotsCounter + 1
		Olympus.spots[Olympus.spotsCounter] = {
			type = "laser",
			object = spot,
			sourceUnitID = ID,
			targetPosition = {
				lat = lat,
				lng = lng
			},
			active = true,
			code = code
		}
	end
end  

-- Shines a infrared light from a unit to a point
function Olympus.fireInfrared(ID, lat, lng)
    Olympus.debug("Olympus.fireInfrared " .. ID .. " -> (" .. lat .. ", " .. lng .. ")", 2)

	local vec3 = mist.utils.makeVec3GL(coord.LLtoLO(lat, lng))

	local unit = Olympus.getUnitByID(ID)
	if unit ~= nil and unit:isExist() then
		local spot = Spot.createInfraRed(unit, {x = 0, y = 1, z = 0}, vec3)
		Olympus.spotsCounter = Olympus.spotsCounter + 1
		Olympus.spots[Olympus.spotsCounter] = {
			type = "infrared",
			object = spot,
			sourceUnitID = ID,
			targetPosition = {
				lat = lat,
				lng = lng
			},
			active = true
		}
	end
end 

-- Set new laser code
function Olympus.setLaserCode(spotID, code) 
	Olympus.debug("Olympus.setLaserCode " .. spotID .. " -> " .. code, 2)
	local spot = Olympus.spots[spotID]
	if spot ~= nil and spot.type == "laser" then
		spot.object:setCode(code)
		spot.code = code
	end
end

-- Move spot to a new location
function Olympus.moveSpot(spotID, lat, lng)
	Olympus.debug("Olympus.moveSpot " .. spotID .. " -> (" .. lat .. ", " .. lng .. ")", 2)
	local spot = Olympus.spots[spotID]
	if spot ~= nil then
		spot.object:setPoint(mist.utils.makeVec3GL(coord.LLtoLO(lat, lng, 0)))
		spot.targetPosition = {lat = lat, lng = lng}
	end
end

-- Remove the spot
function Olympus.deleteSpot(spotID)
	Olympus.debug("Olympus.deleteSpot " .. spotID, 2)
	local spot = Olympus.spots[spotID]
	if spot ~= nil then
		spot.object:destroy()
		Olympus.spots[spotID]["active"] = false
	end
end

-- Spawns a new unit or group
-- Spawn table contains the following parameters
-- category: (string), either Aircraft, Helicopter, GroundUnit or NavyUnit
-- coalition: (string)
-- country: (string)
-- airbaseName: (string, optional) only for air units
-- units: (array) Array of units to spawn. All units will be in the same group. Each unit element must contain:
	-- unitType: (string) DCS Name of the unit
	-- lat: (number)
	-- lng: (number)
	-- alt: (number, optional) only for air units
	-- loadout: (string, optional) only for air units, must be one of the loadouts defined in unitPayloads.lua or mods.lua
	-- payload: (table, optional) overrides loadout, specifies directly the loadout of the unit
	-- liveryID: (string, optional)
function Olympus.spawnUnits(spawnTable, requestHash) 
	Olympus.debug("Olympus.spawnUnits " .. Olympus.serializeTable(spawnTable), 2)

	local unitsTable = nil
	local route = nil
	local category = nil

	-- Generate the units table and route as per DCS requirements
	if spawnTable.category == 'Aircraft' then
		unitsTable = Olympus.generateAirUnitsTable(spawnTable.units)
		route = Olympus.generateAirUnitsRoute(spawnTable)
		category = 'plane'
	elseif spawnTable.category == 'Helicopter' then
		unitsTable = Olympus.generateAirUnitsTable(spawnTable.units)
		route = Olympus.generateAirUnitsRoute(spawnTable)
		category = 'helicopter'
	elseif spawnTable.category == 'GroundUnit' then
		unitsTable = Olympus.generateGroundUnitsTable(spawnTable.units)
		category = 'vehicle'
	elseif spawnTable.category == 'NavyUnit' then
		unitsTable = Olympus.generateNavyUnitsTable(spawnTable.units)
		category = 'ship'
	end

	-- It the unit country is not specified, get a country that belongs to the coalition
	local countryID = 0
	if spawnTable.country == nil or spawnTable.country == "" then
		countryID = Olympus.getCountryIDByCoalition(spawnTable.coalition)
	else
		countryID = country.id[spawnTable.country]
	end

	-- Save the units in the database, for cloning
	for idx, unitTable in pairs(unitsTable) do
		Olympus.addToDatabase(unitTable)
	end

	-- Spawn the new group
	local vars = 
	{
		units = unitsTable, 
		country = countryID, 
		category = category,
		route = route,
		name = "Olympus-" .. Olympus.unitCounter,
		task = 'CAP'
	}
	Olympus.debug(Olympus.serializeTable(vars), 2)
	local newGroup = mist.dynAdd(vars)

	Olympus.unitCounter = Olympus.unitCounter + 1
	Olympus.debug("Olympus.spawnUnits completed succesfully", 2)

	if newGroup == nil then
		Olympus.notify("Olympus.spawnUnits failed to spawn group: " .. Olympus.serializeTable(spawnTable), 30)
		return nil
	end

	Olympus.executionResults[requestHash] = newGroup.groupId
end

-- Generates unit table for air units 
function Olympus.generateAirUnitsTable(units)
	local unitsTable = {}
	for idx, unit in pairs(units) do
		local loadout = unit.loadout			-- loadout: a string, one of the names defined in unitPayloads.lua. Must be compatible with the unitType
		local payload = unit.payload			-- payload: a table, if present the unit will receive this specific payload. Overrides loadout

		if unit.heading == nil then
			unit.heading = 0
		end

		-- Define the loadout
		if payload == nil then
			if loadout ~= nil and loadout ~= "" and Olympus.unitPayloads[unit.unitType] and Olympus.unitPayloads[unit.unitType][loadout] then
				payload = { ["pylons"] = Olympus.unitPayloads[unit.unitType][loadout], ["fuel"] = 999999, ["flare"] = 60, ["ammo_type"] = 1, ["chaff"] = 60, ["gun"] = 100 } 
			elseif loadout ~= nil and loadout ~= "" and Olympus.modsUnitPayloads ~= nil and Olympus.modsUnitPayloads[unit.unitType] and Olympus.modsUnitPayloads[unit.unitType][loadout] then
				payload = { ["pylons"] = Olympus.modsUnitPayloads[unit.unitType][loadout], ["fuel"] = 999999, ["flare"] = 60, ["ammo_type"] = 1, ["chaff"] = 60, ["gun"] = 100 } 
			else
				payload = { ["pylons"] = {}, ["fuel"] = 999999, ["flare"] = 60, ["ammo_type"] = 1, ["chaff"] = 60, ["gun"] = 100 } 
			end
		end
		
		-- Generate the unit table
		local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(unit.lat, unit.lng, 0))
		unitsTable[#unitsTable + 1] = 
		{
			["type"] = unit.unitType,
			["x"] = spawnLocation.x,
			["y"] = spawnLocation.z,
			["alt"] = unit.alt,
			["alt_type"] = "BARO",
			["skill"] = unit.skill,
			["payload"] = payload, 
			["heading"] = unit.heading,
			["callsign"] = { [1] = 1, [2] = 1, [3] = 1, ["name"] = "Olympus" .. Olympus.unitCounter.. "-" .. #unitsTable + 1 },
			["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitsTable + 1,
			["livery_id"] = unit.liveryID
		}
	end
	return unitsTable
end

function Olympus.generateAirUnitsRoute(spawnTable)
	local airbaseName = spawnTable.airbaseName	-- airbaseName: a string, if present the aircraft will spawn on the ground of the selected airbase
	local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(spawnTable.units[1].lat, spawnTable.units[1].lng, 0))

	-- If a airbase is provided the first waypoint is set as a From runway takeoff.
	local route = {}
	if airbaseName and airbaseName ~= "" then
		local airbase = Airbase.getByName(airbaseName)
		if airbase then
			local airbaseID = airbase:getID()
			route = 
			{
				["points"] = 
				{
					[1] = 
					{
						["action"] = "From Parking Area Hot",
						["tasks"] = {
							[1] = {["number"] = 1, ["auto"] = true, ["id"] = "WrappedAction", ["enabled"] = true, ["params"] = {["action"] = {["id"] = "EPLRS", ["params"] = {["value"] = true}, }, }, },
							[2] = {["number"] = 2, ["auto"] = false, ["id"] = "Orbit", ["enabled"] = true, ["params"] = {["pattern"] = "Circle"}, },
						},
						["type"] = "TakeOffParkingHot",
						["ETA"] = 0,
						["ETA_locked"] = true,
						["x"] = spawnLocation.x,
						["y"] = spawnLocation.z,
                        ["alt_type"] = "BARO",
						["formation_template"] = "",
						["airdromeId"] = airbaseID,
						["speed_locked"] = true,
					}, 
				}, 
			} 			
		end
	else
		route = {
				["points"] = 
				{
					[1] = 
					{
						["alt"] = alt,
						["alt_type"] = "BARO",
						["tasks"] = {
							[1] = {["number"] = 1, ["auto"] = true, ["id"] = "WrappedAction", ["enabled"] = true, ["params"] = {["action"] = {["id"] = "EPLRS", ["params"] = {["value"] = true}, }, }, },
							[2] = {["number"] = 2, ["auto"] = false, ["id"] = "Orbit", ["enabled"] = true, ["params"] = {["pattern"] = "Circle"}, },
						},
						["type"] = "Turning Point",
						["x"] = spawnLocation.x,
						["y"] = spawnLocation.z,
					},
				}, 
			} 
	end
	return route
end

-- Generates ground units table, either single or from template
function Olympus.generateGroundUnitsTable(units)
	local unitsTable = {}
	for idx, unit in pairs(units) do
		local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(unit.lat, unit.lng, 0))
		if Olympus.hasKey(templates, unit.unitType) then
			for idx, value in pairs(templates[unit.unitType].units) do
				unitsTable[#unitsTable + 1] = 
				{
					["type"] = value.name,
					["x"] = spawnLocation.x + value.dx,
					["y"] = spawnLocation.z + value.dy,
					["heading"] = 0,
					["skill"] = unit.skill,
					["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitsTable + 1
				}
			end 
		else
			unitsTable[#unitsTable + 1] = 
			{
				["type"] = unit.unitType,
				["x"] = spawnLocation.x,
				["y"] = spawnLocation.z,
				["heading"] = unit.heading,
				["skill"] = unit.skill,
				["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitsTable + 1,
				["livery_id"] = unit.liveryID
			}
		end
	end

	return unitsTable
end  

-- Generates navy units table, either single or from template
function Olympus.generateNavyUnitsTable(units)
	local unitsTable = {}
	for idx, unit in pairs(units) do
		local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(unit.lat, unit.lng, 0))
		if Olympus.hasKey(templates, unit.unitType) then
			for idx, value in pairs(templates[unit.unitType].units) do
				unitsTable[#unitsTable + 1] = 
				{
					["type"] = value.name,
					["x"] = spawnLocation.x + value.dx,
					["y"] = spawnLocation.z + value.dy,
					["heading"] = 0,
					["skill"] = unit.skill,
					["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitsTable + 1,
					["transportable"] = { ["randomTransportable"] = false }
				}
			end 
		else
			unitsTable[#unitsTable + 1] = 
			{
				["type"] = unit.unitType,
				["x"] = spawnLocation.x,
				["y"] = spawnLocation.z,
				["heading"] = unit.heading,
				["skill"] = unit.skill,
				["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitsTable + 1,
				["transportable"] = { ["randomTransportable"] = false },
				["livery_id"] = unit.liveryID
			}
		end
	end

	return unitsTable
end  

-- Add the unit data to the database, used for unit cloning
function Olympus.addToDatabase(unitTable)
	Olympus.cloneDatabase[unitTable.name] = unitTable
end

-- Find a database entry by ID
function Olympus.findInDatabase(ID)
	for idx, unitRecord in pairs(Olympus.cloneDatabase) do
		if unitRecord ~= nil and unitRecord["ID"] == ID then
			return unitRecord
		end
	end
	return nil
end

-- Clones a unit by ID. Will clone the unit with the same original payload as the source unit. TODO: only works on Olympus unit not ME units (TO BE VERIFIED).
-- cloneTable is an array of element, each of which contains
	-- ID: (number) ID of the unit to clone
	-- lat: (number)
	-- lng: (number)
function Olympus.clone(cloneTable, deleteOriginal)
	Olympus.debug("Olympus.clone " .. Olympus.serializeTable(cloneTable), 2)

	local unitsTable = {}
	local countryID = nil
	local category = nil
	local route = {}

	-- All the units in the table will be cloned in a single group
	for idx, cloneData in pairs(cloneTable) do
		local ID = cloneData.ID
		local unitRecord = Olympus.findInDatabase(ID)

		if unitRecord ~= nil then
			-- Update the data of the cloned unit
			local unitTable = mist.utils.deepCopy(unitRecord)

			local point = coord.LLtoLO(cloneData['lat'], cloneData['lng'], 0)
			if unitTable then
				unitTable["x"] = point.x
				unitTable["y"] = point.z
				unitTable["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitsTable + 1
			end

			if countryID == nil and category == nil then
				countryID = unitRecord["country"]
				if unitRecord["category"] == Unit.Category.AIRPLANE then
					category = 'plane'
					route = {
						["points"] = 
						{
							[1] = 
							{
								["alt"] = alt,
								["alt_type"] = "BARO",
								["tasks"] = {
									[1] = {["number"] = 1, ["auto"] = true, ["id"] = "WrappedAction", ["enabled"] = true, ["params"] = {["action"] = {["id"] = "EPLRS", ["params"] = {["value"] = true}, }, }, },
									[2] = {["number"] = 2, ["auto"] = false, ["id"] = "Orbit", ["enabled"] = true, ["params"] = {["pattern"] = "Circle"}, },
								},
								["type"] = "Turning Point",
								["x"] = point.x,
								["y"] = point.z,
							},
						}, 
					}
				elseif unitRecord["category"] == Unit.Category.HELICOPTER then
					category = 'helicopter'
					route = {
						["points"] = 
						{
							[1] = 
							{
								["alt"] = alt,
								["alt_type"] = "BARO",
								["tasks"] = {
									[1] = {["number"] = 1, ["auto"] = true, ["id"] = "WrappedAction", ["enabled"] = true, ["params"] = {["action"] = {["id"] = "EPLRS", ["params"] = {["value"] = true}, }, }, },
									[2] = {["number"] = 2, ["auto"] = false, ["id"] = "Orbit", ["enabled"] = true, ["params"] = {["pattern"] = "Circle"}, },
								},
								["type"] = "Turning Point",
								["x"] = point.x,
								["y"] = point.z,
							},
						}, 
					}
				elseif unitRecord["category"] == Unit.Category.GROUND_UNIT then
					category = 'vehicle'
				elseif unitRecord["category"] == Unit.Category.SHIP then
					category = 'ship'
				end
			end
			unitsTable[#unitsTable + 1] = mist.utils.deepCopy(unitTable)
		end

		if deleteOriginal then
			Olympus.delete(ID, false)
		end
	end

	local vars = 
	{
		units = unitsTable, 
		country = countryID, 
		category = category,
		route = route,
		name = "Olympus-" .. Olympus.unitCounter,
		task = 'CAP'
	}

	Olympus.debug(Olympus.serializeTable(vars), 1)

	-- Save the units in the database, for cloning
	for idx, unitTable in pairs(unitsTable) do
		Olympus.addToDatabase(unitTable)
	end

	Olympus.debug(Olympus.serializeTable(vars), 2)
	mist.dynAdd(vars)
	Olympus.unitCounter = Olympus.unitCounter + 1

	Olympus.debug("Olympus.clone completed successfully", 2)
end

-- Delete a unit by ID, optionally use an explosion
function Olympus.delete(ID, explosion, explosionType)
	Olympus.debug("Olympus.delete " .. ID .. " " .. tostring(explosion), 2)
	local unit = Olympus.getUnitByID(ID)
	if unit ~= nil and unit:isExist() then
		if unit:getPlayerName() or explosion then
			if explosionType == nil then
				explosionType = "normal"
			end
			local lat, lng, alt = coord.LOtoLL(unit:getPoint())
			Olympus.explosion(250, explosionType, lat, lng, alt)
			Olympus.debug("Olympus.delete completed successfully", 2)
		else
			unit:destroy(); --works for AI units not players
			Olympus.debug("Olympus.delete completed successfully", 2)
		end
	end
end

-- Set a DCS main task to a group
function Olympus.setTask(groupName, taskOptions)
	Olympus.debug("Olympus.setTask " .. groupName .. " " .. Olympus.serializeTable(taskOptions), 2)
	local group = Group.getByName(groupName)
	if group ~= nil then
		local task = Olympus.buildTask(groupName, taskOptions);
		Olympus.debug("Olympus.setTask " .. Olympus.serializeTable(task), 20)
		if task then
			group:getController():setTask(task)
			Olympus.debug("Olympus.setTask completed successfully", 2)
		end
	end
end

-- Reset the task of a group
function Olympus.resetTask(groupName)
	Olympus.debug("Olympus.resetTask " .. groupName, 2)
	local group = Group.getByName(groupName)
	if group ~= nil then
		group:getController():resetTask()
		Olympus.debug("Olympus.resetTask completed successfully", 2)
	end
end

-- Give a group a command
function Olympus.setCommand(groupName, command)
	Olympus.debug("Olympus.setCommand " .. groupName .. " " .. Olympus.serializeTable(command), 2)
	local group = Group.getByName(groupName)
	if group ~= nil then
		group:getController():setCommand(command)
		Olympus.debug("Olympus.setCommand completed successfully", 2)
	end
end

-- Set an option of a group
function Olympus.setOption(groupName, optionID, optionValue)
	Olympus.debug("Olympus.setOption " .. groupName .. " " .. optionID .. " " .. tostring(optionValue), 2)
	local group = Group.getByName(groupName)
	if group ~= nil then
		group:getController():setOption(optionID, optionValue)
		Olympus.debug("Olympus.setOption completed successfully", 2)
	end
end

-- Disable the AI of a group on or off entirely
function Olympus.setOnOff(groupName, onOff)
	Olympus.debug("Olympus.setOnOff " .. groupName .. " " .. tostring(onOff), 2)
	local group = Group.getByName(groupName)
	if group ~= nil then
		group:getController():setOnOff(onOff)
		Olympus.debug("Olympus.setOnOff completed successfully", 2)
	end
end

-- Get the unit description
function getUnitDescription(unit) 
	return unit:getDescr()
end

-- Set the unit cargo weight
function Olympus.setCargoWeight(ID, weight)
	Olympus.debug("Olympus.setCargoWeight " .. ID .. " " .. tostring(weight), 2)

	local unit = Olympus.getUnitByID(ID)
	if unit ~= nil and unit:isExist() then
		trigger.action.setUnitInternalCargo(unit:getName(), weight)
	end
end

-- Register a drawArgument to be read for a unit
function Olympus.registerDrawArgument(ID, argument, active)
	Olympus.debug("Olympus.registerDrawArgument " .. ID .. " " .. tostring(argument) .. " " .. tostring(active), 2)

	-- Create the table if it does not exist
	if Olympus.drawArguments[ID] == nil then
		Olympus.drawArguments[ID] = {}
	end

	-- Set the draw argument to true or false
	if active then
		Olympus.drawArguments[ID][argument] = true
	else
		Olympus.drawArguments[ID][argument] = false
	end
end

-- This function gets the navpoints from the DCS mission
function Olympus.getNavPoints() 	
	local function extract_tag(str)
		return str:match("^%[(.-)%]")
	end

	local navpoints = {}
	if mist.DBs.navPoints ~= nil then
		for coalitionName, coalitionNavpoints in pairs(mist.DBs.navPoints) do
			if navpoints[coalitionName] == nil then
				navpoints[coalitionName] = {}
			end

			for index, navpointDrawingData in pairs(coalitionNavpoints) do
				local navpointCustomLayer = extract_tag(navpointDrawingData['callsignStr']);

				-- Let's convert DCS coords to lat lon
				local vec3 = { x = navpointDrawingData['x'], y = 0, z = navpointDrawingData['y'] }
				local lat, lng = coord.LOtoLL(vec3)
				navpointDrawingData['lat'] = lat
				navpointDrawingData['lng'] = lng
				navpointDrawingData['coalition'] = coalitionName
				navpointDrawingData['tag'] = navpointCustomLayer

				if navpointCustomLayer ~= nil then
					if navpoints[coalitionName][navpointCustomLayer] == nil then
						navpoints[coalitionName][navpointCustomLayer] = {}
					end
					navpoints[coalitionName][navpointCustomLayer][navpointDrawingData['callsignStr']] = navpointDrawingData
				else
					navpoints[coalitionName][navpointDrawingData['callsignStr']] = navpointDrawingData
				end
			end
		end
	end

	return navpoints
end

-- This function is periodically called to collect the data of all the existing drawings in the mission to be transmitted to the olympus.dll
function Olympus.initializeDrawings()
	local function extract_custom_layer_name(str)
		if str:match("^%[LYR:(.-)%]") then
			return str:match("^%[LYR:(.-)%]")
		elseif str:match("^%[(.-)%]") then
			return str:match("^%[(.-)%]")
		end
		return nil
	end

	local drawings = {}
	if mist.DBs.drawingByName ~= nil then
		for drawingName, drawingData in pairs(mist.DBs.drawingByName) do
			local customLayer = extract_custom_layer_name(drawingName)

			-- Let's convert DCS coords to lat lon
			local vec3 = { x = drawingData['mapX'], y = 0, z = drawingData['mapY'] }
			local lat, lng = coord.LOtoLL(vec3)
			drawingData['lat'] = lat
			drawingData['lng'] = lng

			-- If the drawing has points, we have to convert those too
			if drawingData['points'] ~= nil then
				if drawingData['points']['x'] ~= nil then
					-- In this case we have only one point
					local point = { x = drawingData['points']['x'], y = 0, z = drawingData['points']['y'] }
					local pointLat, pointLng = coord.LOtoLL(point)
					drawingData['points'][0] = { lat = pointLat, lng = pointLng }
				else
					-- In this case we have multiple points indexed by number
					for pointNumber, pointLOCoords in pairs(drawingData['points']) do
						local point = { x = pointLOCoords['x'], y = 0, z = pointLOCoords['y'] }
						local pointLat, pointLng = coord.LOtoLL(point)

						drawingData['points'][pointNumber] = { lat = pointLat, lng = pointLng }
					end
				end
			end

			-- Let's initialize the layers
			if drawings[drawingData.layerName] == nil then
				drawings[drawingData.layerName] = {}
			end

			if customLayer and drawings[drawingData.layerName][customLayer] == nil then
				drawings[drawingData.layerName][customLayer] = {}
			end

			-- Let's put the drawing in the correct layer
			if customLayer then
				-- Let's remove the m from the drawing name
				local cleanDrawingName = string.match(drawingName, "%] (.+)")
				drawingData.name = cleanDrawingName
				-- The drawing has the custom layer m
				drawings[drawingData.layerName][customLayer][cleanDrawingName] = drawingData
			else
				-- The drawing is a standard drawing
				drawings[drawingData.layerName][drawingName] = drawingData
			end
		end

		local navpoints = Olympus.getNavPoints()

		drawings['navpoints'] = navpoints

		Olympus.drawingsByLayer["drawings"] = drawings

		-- Send the drawings to the DLL
		Olympus.OlympusDLL.setDrawingsData()

		Olympus.notify("Olympus drawings initialized", 2)
	else
		Olympus.debug("MIST DBs not ready", 2)
		timer.scheduleFunction(Olympus.initializeDrawings, {}, timer.getTime() + 1)
	end
end

-- This function is periodically called to collect the data of all the existing units in the mission to be transmitted to the olympus.dll
function Olympus.setUnitsData(arg, time)
	-- Units data
	local units = {}
	
	local startIndex = Olympus.unitIndex
	local endIndex = startIndex + Olympus.unitStep
	local index = 0
	for ID, unit in pairs(Olympus.units) do
		index = index + 1
		-- Only the indexes between startIndex and endIndex are handled. This is a simple way to spread the update load over many cycles
		if index > startIndex then
			if unit ~= nil and unit:isExist() then
				local table = {}	

				-- Get the object category in Olympus name
				local objectCategory = Object.getCategory(unit)
				if objectCategory == Object.Category.UNIT then
					if unit:getDesc().category == Unit.Category.AIRPLANE then
						table["category"] = "Aircraft"
					elseif unit:getDesc().category == Unit.Category.HELICOPTER then
						table["category"] = "Helicopter"
					elseif unit:getDesc().category == Unit.Category.GROUND_UNIT then
						table["category"] = "GroundUnit"
					elseif unit:getDesc().category == Unit.Category.SHIP then
						table["category"] = "NavyUnit"
					elseif Olympus.modsList ~= nil and Olympus.modsList[unit:getDesc().typeName] ~= nil then
						table["category"] = Olympus.modsList[unit:getDesc().typeName]
					end
				else
					local status, description = pcall(getUnitDescription, unit)
					if status and Olympus.modsList ~= nil and Olympus.modsList[description.typeName] ~= nil then
						table["category"] = Olympus.modsList[description.typeName]
					else
						units[ID] = {isAlive = false}
						Olympus.units[ID] = nil
					end
				end

				-- If the category is handled by Olympus, get the data
				if table["category"] ~= nil then
					-- Compute unit position and heading
					local lat, lng, alt = coord.LOtoLL(unit:getPoint())
					local position = unit:getPosition()
					local heading = math.atan2( position.x.z, position.x.x )
					local velocity = unit:getVelocity();
					local airborne = unit:inAir()
					
					-- Fill the data table
					table["unitID"] = unit:getID()
					table["name"] = unit:getTypeName()
					table["coalitionID"] = unit:getCoalition()
					table["position"] = {}
					table["position"]["lat"] = lat 
					table["position"]["lng"] = lng 
					table["position"]["alt"] = alt
					table["speed"] = mist.vec.mag(velocity)
					table["horizontalVelocity"] =  math.sqrt(velocity.x * velocity.x + velocity.z * velocity.z)
					table["verticalVelocity"] = velocity.y
					table["heading"] = heading 
					table["airborne"] = airborne

					-- Track angles are wrong because of weird reference systems, approximate it using latitude and longitude differences
					if (table["horizontalVelocity"] > 1) then
						if Olympus.unitsData["units"] ~= nil and Olympus.unitsData["units"][ID] ~= nil and Olympus.unitsData["units"][ID]["position"] ~= nil and Olympus.unitsData["units"][ID]["position"]["lat"] ~= nil and Olympus.unitsData["units"][ID]["position"]["lng"] ~= nil then
							local latDifference = lat - Olympus.unitsData["units"][ID]["position"]["lat"]
							local lngDifference = lng - Olympus.unitsData["units"][ID]["position"]["lng"]
							table["track"] = math.atan2(lngDifference * math.cos(lat / 57.29577), latDifference)
						else
							table["track"] = math.atan2(velocity.z, velocity.x)
						end
					else
						table["track"] = table["heading"]
					end

					table["isAlive"] = unit:isExist() and unit:isActive() and unit:getLife() >= 1

					--[[ COMMENTING OUT BECAUSE OF CRASHES -- TO BE INVESTIGATED LATER ON ]]--
					--[[ if unit:isActive() and unit:hasSensors(Unit.SensorType.RADAR) then
						if unit:getRadar() then
							table["radarState"] = true
						else
							table["radarState"] = false
						end	
					end ]]

					-- Read the draw arguments
					local drawArguments = {}
					if Olympus.drawArguments[ID] ~= nil then
						for argument, active in pairs(Olympus.drawArguments[ID]) do
							if active then
								drawArguments[#drawArguments + 1] = {
									argument = argument,
									value = unit:getDrawArgumentValue(argument)
								}
							end
						end
					end
					table["drawArguments"] = drawArguments
					
					local group = unit:getGroup()
					if group ~= nil then
						local controller = group:getController()
						if controller ~= nil then
							-- Get the targets detected by the unit controller
							local contacts = {}
							local unitController = unit:getController()
							if unitController ~= nil then
								for det, enum in pairs(Controller.Detection) do
									local controllerTargets = unitController:getDetectedTargets(enum)
									for i, target in ipairs(controllerTargets) do
										if target ~= nil and target.object ~= nil and target.visible then
											target["detectionMethod"] = det
											contacts[#contacts + 1] = target
										end
									end
								end
							end
							
							-- getLife0 does not seem to work for ships, so we need to keep a reference to the initial life of the unit
							if Olympus.unitsInitialLife[ID] == nil then
								Olympus.unitsInitialLife[ID] = unit:getLife()
							end

							-- Get the initial life of the unit to compute the current health
							local initialLife = 1
							if Olympus.unitsInitialLife[ID] ~= nil then
								initialLife = Olympus.unitsInitialLife[ID]
							end
							
							table["country"] = unit:getCountry()
							if unit:getPlayerName() ~= nil then
								table["unitName"] = unit:getPlayerName()
							else
								table["unitName"] = unit:getName()
							end
							-- In case of AI units the callSign and the unitName will be the same
							table["callsign"] = unit:getName()
							table["groupID"] = group:getID()
							table["groupName"] = group:getName()
							table["isHuman"] = (unit:getPlayerName() ~= nil)
							table["hasTask"] = controller:hasTask()
							table["ammo"] = unit:getAmmo() --TODO remove a lot of stuff we don't really need
							table["fuel"] = unit:getFuel()
							table["health"] = unit:getLife() / initialLife * 100
							table["contacts"] = contacts

							local name = unit:getName()

							-- If the unit is not in the clone database it means it was not spawned by Olympus. Let's try and recover it using mist
							if Olympus.cloneDatabase[name] == nil then
								if mist.DBs ~= nil and mist.DBs.unitsByName ~= nil and mist.DBs.unitsByName[name] ~= nil then
									-- Payloads can be copied from ME units only TODO: can we fix this?
									local payload = {}
									if mist.DBs.MEunitsByName[name] then
										payload = mist.getPayload(name)
									end

									-- Create a mock spawn table to generate the database
									local unitsTable = nil
									local spawnTable = {}
									spawnTable.units = {
										[1] = {
											["unitType"] = table["name"],
											["lat"] = table["position"]["lat"],
											["lng"] = table["position"]["lng"],
											["alt"] = table["position"]["alt"],
											["payload"] = payload,
											["liveryID"] = mist.DBs.unitsByName[name]["livery_id"]
										}
									}

									-- Generate the units table as per DCS requirements
									if table["category"] == 'Aircraft' then
										unitsTable = Olympus.generateAirUnitsTable(spawnTable.units)
									elseif table["category"] == 'Helicopter' then
										unitsTable = Olympus.generateAirUnitsTable(spawnTable.units)
									elseif table["category"] == 'GroundUnit' then
										unitsTable = Olympus.generateGroundUnitsTable(spawnTable.units)
									elseif table["category"] == 'NavyUnit' then
										unitsTable = Olympus.generateNavyUnitsTable(spawnTable.units)
									end

									-- Save the units in the database, for cloning
									for idx, unitTable in pairs(unitsTable) do
										-- Force the name of the unit to be equal to the original name
										unitTable["name"] = name
										Olympus.addToDatabase(mist.utils.deepCopy(unitTable))
									end
								end
							end

							-- Update the database used for unit cloning
							if Olympus.cloneDatabase[name] ~= nil then
								Olympus.cloneDatabase[name]["ID"] = ID
								Olympus.cloneDatabase[name]["category"] = unit:getDesc().category
								Olympus.cloneDatabase[name]["heading"] = table["heading"]
								Olympus.cloneDatabase[name]["alt"] = alt
								Olympus.cloneDatabase[name]["country"] = unit:getCountry()
							end

							units[ID] = table
						end
					else
						-- If the unit reference is nil it means the unit no longer exits
						units[ID] = {isAlive = false}
						Olympus.units[ID] = nil
					end
				end
			else
				-- If the unit reference is nil it means the unit no longer exits
				units[ID] = {isAlive = false}
				Olympus.units[ID] = nil
			end
		end
		if index >= endIndex then
			break
		end
	end

	-- Reset the counter 
	if index ~= endIndex then 
		Olympus.unitIndex = 0
	else
		Olympus.unitIndex = endIndex
	end
	
	-- Assemble unitsData table
	Olympus.unitsData["units"] = units

	Olympus.OlympusDLL.setUnitsData()
	return time + 0.05
end

-- This function is periodically called to collect the data of all the existing weapons in the mission to be transmitted to the olympus.dll
function Olympus.setWeaponsData(arg, time)
	-- Weapons data
	local weapons = {}
	
	local startIndex = Olympus.weaponIndex
	local endIndex = startIndex + Olympus.weaponStep
	local index = 0
	for ID, weapon in pairs(Olympus.weapons) do
		index = index + 1

		-- Only the indexes between startIndex and endIndex are handled. This is a simple way to spread the update load over many cycles
		if index > startIndex then
			if weapon ~= nil and weapon:isExist() then
				local table = {}

				-- Get the object category in Olympus name
				local objectCategory = Object.getCategory(weapon)
				if objectCategory == Object.Category.WEAPON then
					if weapon:getDesc().category == Weapon.Category.MISSILE then
						table["category"] = "Missile"
					elseif weapon:getDesc().category == Weapon.Category.ROCKET then
						table["category"] = "Missile"
					elseif weapon:getDesc().category == Weapon.Category.BOMB then
						table["category"] = "Bomb"
					--elseif weapon:getDesc().category == Weapon.Category.SHELL then
					--	table["category"] = "Shell"	-- Useful for debugging but has no real use and has big impact on performance
					end
				else
					weapons[ID] = {isAlive = false}
					Olympus.weapons[ID] = nil
				end

				-- If the category is handled by Olympus, get the data
				if table["category"] ~= nil then
					-- Compute weapon position and heading
					local lat, lng, alt = coord.LOtoLL(weapon:getPoint())
					local position = weapon:getPosition()
					local heading = math.atan2( position.x.z, position.x.x )

					-- Fill the data table
					table["name"] = weapon:getTypeName()
					table["coalitionID"] = weapon:getCoalition()
					table["position"] = {}
					table["position"]["lat"] = lat 
					table["position"]["lng"] = lng 
					table["position"]["alt"] = alt
					table["speed"] = mist.vec.mag(weapon:getVelocity())
					table["heading"] = heading 
					table["isAlive"] = weapon:isExist()
					
					weapons[ID] = table
				end
			else
				-- If the weapon reference is nil it means the unit no longer exits
				weapons[ID] = {isAlive = false}
				Olympus.weapons[ID] = nil
			end
		end
		if index >= endIndex then
			break
		end
	end

	-- Reset the counter 
	if index ~= endIndex then 
		Olympus.weaponIndex = 0
	else
		Olympus.weaponIndex = endIndex
	end
	
	-- Assemble weaponsData table
	Olympus.weaponsData["weapons"] = weapons

	Olympus.OlympusDLL.setWeaponsData()
	return time + 0.25
end

function Olympus.setExecutionResults()
	Olympus.OlympusDLL.setExecutionResults()
	return timer.getTime() + 1
end

function Olympus.setMissionData(arg, time)
	-- Bullseye data
	local bullseyes = {}
	for i = 0, 2 do
		local bullseyeVec3 = coalition.getMainRefPoint(i)
		local bullseyeLatitude, bullseyeLongitude, bullseyeAltitude = coord.LOtoLL(bullseyeVec3)
		bullseyes[i] = {}
		bullseyes[i]["latitude"] = bullseyeLatitude
		bullseyes[i]["longitude"] = bullseyeLongitude
		bullseyes[i]["coalition"] = Olympus.getCoalitionByCoalitionID(i) 
	end

	-- Airbases data
	local base = world.getAirbases()
	local airbases = {}
	for i = 1, #base do
		local info = {}
		local latitude, longitude, altitude = coord.LOtoLL(Airbase.getPoint(base[i]))
		info["callsign"] = Airbase.getCallsign(base[i])
		info["coalition"] = Olympus.getCoalitionByCoalitionID(Airbase.getCoalition(base[i])) 
		info["latitude"] =  latitude
		info["longitude"] =  longitude
		if Airbase.getUnit(base[i]) then
			info["unitId"] = Airbase.getUnit(base[i]):getID()
		end
		airbases[i] = info
	end

	-- Mission
	local mission = {}
	mission.theatre = env.mission.theatre
	mission.dateAndTime = {
		["elapsedTime"] = DCS.getRealTime() - Olympus.missionStartTime,
		["time"] = mist.time.getDHMS(timer.getAbsTime()),
		["startTime"] = env.mission.start_time,
		["date"] = env.mission.date
	}

	mission.coalitions = {
		["red"] = {},
		["blue"] = {},
		["neutral"] = {}
	}
	for countryName, countryId in pairs(country["id"]) do
		local coalitionName = Olympus.getCoalitionByCoalitionID(coalition.getCountryCoalition(countryId))
		mission.coalitions[coalitionName][#mission.coalitions[coalitionName] + 1] = countryName 
	end

	-- Spots 
	-- Initialize an empty table to store spots
	local spots = {}

	-- Iterate over each spot in Olympus.spots
	for ID, spot in pairs(Olympus.spots) do 
		-- Create a new entry in the spots table with the same ID
		spots[ID] = {
			type = spot.type,
			sourceUnitID = spot.sourceUnitID,
			targetPosition = spot.targetPosition,
			active = spot.active,
		}

		-- If the spot type is "laser", add the code to the spot entry
		if spot.type == "laser" then
			spots[ID]["code"] = spot.code 
		end
	end

	-- Assemble table
	Olympus.missionData["bullseyes"] = bullseyes
	Olympus.missionData["airbases"] = airbases
	Olympus.missionData["mission"] = mission
	Olympus.missionData["spots"] = spots

	Olympus.OlympusDLL.setMissionData()
	return time + 1	-- For perfomance reasons mission data is updated once every second
end

-- Initializes the units table with all the existing ME units
function Olympus.initializeUnits() 
	if mist and mist.DBs and mist.DBs.MEunitsById then
		for id, unitsTable in pairs(mist.DBs.MEunitsById) do
			local unit = Unit.getByName(unitsTable["unitName"])
			if unit ~= nil and unit:isExist() then
				Olympus.units[unit["id_"]] = unit
			end
		end
		Olympus.notify("Olympus units table initialized", 2)
	else
		Olympus.debug("MIST DBs not ready", 2)
		timer.scheduleFunction(Olympus.initializeUnits, {}, timer.getTime() + 1)
	end
end

------------------------------------------------------------------------------------------------------
-- Olympus utility functions
------------------------------------------------------------------------------------------------------
function Olympus.serializeTable(val, name, skipnewlines, depth)
    skipnewlines = skipnewlines or false
    depth = depth or 0

    local tmp = string.rep(" ", depth)
    if name then 
		if type(name) == "number" then
			tmp = tmp .. "[" .. name .. "]" .. " = " 
		else
			tmp = tmp .. name  .. " = " 
		end
	end

    if type(val) == "table" then
        tmp = tmp .. "{" .. (not skipnewlines and "\n" or "")
        for k, v in pairs(val) do
            tmp =  tmp .. Olympus.serializeTable(v, k, skipnewlines, depth + 1) .. "," .. (not skipnewlines and "\n" or "")
        end
        tmp = tmp .. string.rep(" ", depth) .. "}"
    elseif type(val) == "number" then
        tmp = tmp .. tostring(val)
    elseif type(val) == "string" then
        tmp = tmp .. string.format("%q", val)
    elseif type(val) == "boolean" then
        tmp = tmp .. (val and "true" or "false")
    else
        tmp = tmp .. "\"[inserializeable datatype:" .. type(val) .. "]\""
    end

    return tmp
end

function Olympus.isArray(t)
	local i = 0
	for _ in pairs(t) do
		i = i + 1
		if t[i] == nil then return false end
	end
	return true
end

function Olympus.hasValue(tab, val)
    for index, value in ipairs(tab) do
        if value == val then
            return true
        end
    end

    return false
end

function Olympus.hasKey(tab, key)
    for k, value in pairs(tab) do
        if k == key then
            return true
        end
    end

    return false
end

------------------------------------------------------------------------------------------------------
-- Olympus startup script
------------------------------------------------------------------------------------------------------
Olympus.instancePath = lfs.writedir().."Mods\\Services\\Olympus\\bin\\"
Olympus.notify("Starting DCS Olympus backend session in "..Olympus.instancePath, 2)

local OlympusName = 'Olympus ' .. version .. ' C++ module';
Olympus.DLLsloaded = Olympus.loadDLLs()
if Olympus.DLLsloaded then
	Olympus.notify(OlympusName..' successfully loaded.', 20)
else
	Olympus.notify('Failed to load '..OlympusName, 20)
end

-- Create the handler to detect new units
if handler ~= nil then
	world.removeEventHandler(handler)
	Olympus.debug("Olympus handler removed" , 2)
end
handler = {}
function handler:onEvent(event)
	if event.id == 1 then
		local weapon = event.weapon
		if Olympus ~= nil and Olympus.weapons ~= nil then
			Olympus.weapons[weapon["id_"]] = weapon
			Olympus.debug("New weapon created " .. weapon["id_"], 2)
		end
	elseif event.id == 15 then
		local unit = event.initiator
		if Olympus ~= nil and Olympus.units ~= nil then
			Olympus.units[unit["id_"]] = unit
			Olympus.debug("New unit created " .. unit["id_"], 2)
		end
	end
end
world.addEventHandler(handler)

-- Start the periodic functions
timer.scheduleFunction(Olympus.setUnitsData, {}, timer.getTime() + 0.05)
timer.scheduleFunction(Olympus.setWeaponsData, {}, timer.getTime() + 0.25)
timer.scheduleFunction(Olympus.setMissionData, {}, timer.getTime() + 1)
timer.scheduleFunction(Olympus.setExecutionResults, {}, timer.getTime() + 1)

-- Initialize the ME units
Olympus.initializeUnits()

-- Initialize the Drawings
Olympus.initializeDrawings()

Olympus.notify("OlympusCommand script " .. version .. " loaded successfully", 2, true)

