--- **Utilities** - Derived utilities taken from the MIST framework, added helpers from the MOOSE community.
--
-- ### Authors:
--
--   * Grimes : Design & Programming of the MIST framework.
--
-- ### Contributions:
--
--   * FlightControl : Rework to OO framework.
--   * And many more
--
-- @module Utilities.Utils
-- @image MOOSE.JPG

--- Smoke color enum `trigger.smokeColor`.
-- @type SMOKECOLOR
-- @field #number Green Green smoke (0)
-- @field #number Red Red smoke (1)
-- @field #number White White smoke (2)
-- @field #number Orange Orange smoke (3)
-- @field #number Blue Blue smoke (4)

SMOKECOLOR = trigger.smokeColor -- #SMOKECOLOR

--- Flare colur enum `trigger.flareColor`.
-- @type FLARECOLOR
-- @field #number Green (0)
-- @field #number Red Red flare (1)
-- @field #number White White flare (2)
-- @field #number Yellow Yellow flare (3)

FLARECOLOR = trigger.flareColor -- #FLARECOLOR

--- Big smoke preset enum.
-- @type BIGSMOKEPRESET
-- @field #number SmallSmokeAndFire Small moke and fire (1)
-- @field #number MediumSmokeAndFire Medium smoke and fire (2)
-- @field #number LargeSmokeAndFire Large smoke and fire (3)
-- @field #number HugeSmokeAndFire Huge smoke and fire (4)
-- @field #number SmallSmoke Small smoke (5)
-- @field #number MediumSmoke Medium smoke (6)
-- @field #number LargeSmoke Large smoke (7)
-- @field #number HugeSmoke Huge smoke (8)
BIGSMOKEPRESET = {
  SmallSmokeAndFire=1,
  MediumSmokeAndFire=2,
  LargeSmokeAndFire=3,
  HugeSmokeAndFire=4,
  SmallSmoke=5,
  MediumSmoke=6,
  LargeSmoke=7,
  HugeSmoke=8,
}

--- DCS map as returned by `env.mission.theatre`.
-- @type DCSMAP
-- @field #string Caucasus Caucasus map.
-- @field #string Normandy Normandy map.
-- @field #string NTTR Nevada Test and Training Range map.
-- @field #string PersianGulf Persian Gulf map.
-- @field #string TheChannel The Channel map.
-- @field #string Syria Syria map.
-- @field #string MarianaIslands Mariana Islands map.
-- @field #string Falklands South Atlantic map.
-- @field #string Sinai Sinai map.
-- @field #string Kola Kola map.
-- @field #string Afghanistan Afghanistan map
-- @field #string Iraq Iraq map
-- @field #string GermanyCW Germany Cold War map
DCSMAP = {
  Caucasus="Caucasus",
  NTTR="Nevada",
  Normandy="Normandy",
  PersianGulf="PersianGulf",
  TheChannel="TheChannel",
  Syria="Syria",
  MarianaIslands="MarianaIslands",
  Falklands="Falklands",
  Sinai="SinaiMap",
  Kola="Kola",
  Afghanistan="Afghanistan",
  Iraq="Iraq",
  GermanyCW="GermanyCW",
}


--- See [DCS_enum_callsigns](https://wiki.hoggitworld.com/view/DCS_enum_callsigns)
-- @type CALLSIGN
CALLSIGN={
  -- Aircraft
  Aircraft={
    Enfield=1,
    Springfield=2,
    Uzi=3,
    Colt=4,
    Dodge=5,
    Ford=6,
    Chevy=7,
    Pontiac=8,
    -- A-10A or A-10C
    Hawg=9,
    Boar=10,
    Pig=11,
    Tusk=12,
  },
  -- AWACS
  AWACS={
    Overlord=1,
    Magic=2,
    Wizard=3,
    Focus=4,
    Darkstar=5,
  },
  -- Tanker
  Tanker={
    Texaco=1,
    Arco=2,
    Shell=3,
    Navy_One=4,
    Mauler=5,
    Bloodhound=6,
    },
  -- JTAC
  JTAC={
    Axeman=1,
    Darknight=2,
    Warrior=3,
    Pointer=4,
    Eyeball=5,
    Moonbeam=6,
    Whiplash=7,
    Finger=8,
    Pinpoint=9,
    Ferret=10,
    Shaba=11,
    Playboy=12,
    Hammer=13,
    Jaguar=14,
    Deathstar=15,
    Anvil=16,
    Firefly=17,
    Mantis=18,
    Badger=19,
  },
  -- FARP
  FARP={
    London=1,
    Dallas=2,
    Paris=3,
    Moscow=4,
    Berlin=5,
    Rome=6,
    Madrid=7,
    Warsaw=8,
    Dublin=9,
    Perth=10,
  },
  F16={
    Viper=9,
    Venom=10,
    Lobo=11,
    Cowboy=12,
    Python=13,
    Rattler=14,
    Panther=15,
    Wolf=16,
    Weasel=17,
    Wild=18,
    Ninja=19,
    Jedi=20,
  },
  F18={
    Hornet=9,
    Squid=10,
    Ragin=11,
    Roman=12,
    Sting=13,
    Jury=14,
    Jokey=15,
    Ram=16,
    Hawk=17,
    Devil=18,
    Check=19,
    Snake=20,
  },
  F15E={
    Dude=9,
    Thud=10,
    Gunny=11,
    Trek=12,
    Sniper=13,
    Sled=14,
    Best=15,
    Jazz=16,
    Rage=17,
    Tahoe=18,
  },
  B1B={
    Bone=9,
    Dark=10,
    Vader=11
  },
  B52={
    Buff=9,
    Dump=10,
    Kenworth=11,
  },
  TransportAircraft={
    Heavy=9,
    Trash=10,
    Cargo=11,
    Ascot=12,
  },
  AH64={
    Army_Air = 9,
    Apache = 10,
    Crow = 11,
    Sioux = 12,
    Gatling = 13,
    Gunslinger = 14,
    Hammerhead = 15,
    Bootleg = 16,
    Palehorse = 17,
    Carnivor = 18,
    Saber = 19,
  },
  Kiowa = {
    Anvil = 1,
    Azrael = 2,
    BamBam = 3,
    Blackjack = 4,
    Bootleg = 5,
    BurninStogie = 6,
    Chaos = 7,
    CrazyHorse = 8,
    Crusader = 9,
    Darkhorse = 10,
    Eagle = 11,
    Lighthorse = 12,
    Mustang = 13,
    Outcast = 14,
    Palehorse = 15,
    Pegasus = 16,
    Pistol = 17,
    Roughneck = 18,
    Saber = 19,
    Shamus = 20,
    Spur = 21,
    Stetson = 22,
    Wrath = 23,
  },
  
} --#CALLSIGN

--- Utilities static class.
-- @type UTILS
-- @field #number _MarkID Marker index counter. Running number when marker is added.
UTILS = {
  _MarkID = 1
}

--- Function to infer instance of an object
--
-- ### Examples:
--
--    * UTILS.IsInstanceOf( 'some text', 'string' ) will return true
--    * UTILS.IsInstanceOf( some_function, 'function' ) will return true
--    * UTILS.IsInstanceOf( 10, 'number' ) will return true
--    * UTILS.IsInstanceOf( false, 'boolean' ) will return true
--    * UTILS.IsInstanceOf( nil, 'nil' ) will return true
--
--    * UTILS.IsInstanceOf( ZONE:New( 'some zone', ZONE ) will return true
--    * UTILS.IsInstanceOf( ZONE:New( 'some zone', 'ZONE' ) will return true
--    * UTILS.IsInstanceOf( ZONE:New( 'some zone', 'zone' ) will return true
--    * UTILS.IsInstanceOf( ZONE:New( 'some zone', 'BASE' ) will return true
--
--    * UTILS.IsInstanceOf( ZONE:New( 'some zone', 'GROUP' ) will return false
--
--
-- @param object is the object to be evaluated
-- @param className is the name of the class to evaluate (can be either a string or a Moose class)
-- @return #boolean
UTILS.IsInstanceOf = function( object, className )
  -- Is className NOT a string ?
  if type( className ) ~= 'string' then

    -- Is className a Moose class ?
    if type( className ) == 'table' and className.IsInstanceOf ~= nil then

      -- Get the name of the Moose class as a string
      className = className.ClassName

    -- className is neither a string nor a Moose class, throw an error
    else

      -- I'm not sure if this should take advantage of MOOSE logging function, or throw an error for pcall
      local err_str = 'className parameter should be a string; parameter received: '..type( className )
      return false
      -- error( err_str )

    end
  end

  -- Is the object a Moose class instance ?
  if type( object ) == 'table' and object.IsInstanceOf ~= nil then

    -- Use the IsInstanceOf method of the BASE class
    return object:IsInstanceOf( className )
  else

    -- If the object is not an instance of a Moose class, evaluate against lua basic data types
    local basicDataTypes = { 'string', 'number', 'function', 'boolean', 'nil', 'table' }
    for _, basicDataType in ipairs( basicDataTypes ) do
      if className == basicDataType then
        return type( object ) == basicDataType
      end
    end
  end

  -- Check failed
  return false
end


--- Deep copy a table. See http://lua-users.org/wiki/CopyTable
-- @param #table object The input table.
-- @return #table Copy of the input table.
UTILS.DeepCopy = function(object)

  local lookup_table = {}

  -- Copy function.
  local function _copy(object)
    if type(object) ~= "table" then
      return object
    elseif lookup_table[object] then
      return lookup_table[object]
    end

    local new_table = {}

    lookup_table[object] = new_table

    for index, value in pairs(object) do
      new_table[_copy(index)] = _copy(value)
    end

    return setmetatable(new_table, getmetatable(object))
  end

  local objectreturn = _copy(object)

  return objectreturn
end


--- Serialize a given table.
-- @param #table tbl Input table.
-- @return #string Table as a string.
UTILS.OneLineSerialize = function( tbl )  -- serialization of a table all on a single line, no comments, made to replace old get_table_string function

  local lookup_table = {}

  local function _Serialize( tbl )

    if type(tbl) == 'table' then --function only works for tables!

      if lookup_table[tbl] then
        return lookup_table[object]
      end

      local tbl_str = {}

      lookup_table[tbl] = tbl_str

      tbl_str[#tbl_str + 1] = '{'

      for ind,val in pairs(tbl) do -- serialize its fields
        local ind_str = {}
        if type(ind) == "number" then
          ind_str[#ind_str + 1] = '['
          ind_str[#ind_str + 1] = tostring(ind)
          ind_str[#ind_str + 1] = ']='
        else --must be a string
          ind_str[#ind_str + 1] = '['
          ind_str[#ind_str + 1] = UTILS.BasicSerialize(ind)
          ind_str[#ind_str + 1] = ']='
        end

        local val_str = {}
        if ((type(val) == 'number') or (type(val) == 'boolean')) then
          val_str[#val_str + 1] = tostring(val)
          val_str[#val_str + 1] = ','
          tbl_str[#tbl_str + 1] = table.concat(ind_str)
          tbl_str[#tbl_str + 1] = table.concat(val_str)
      elseif type(val) == 'string' then
          val_str[#val_str + 1] = UTILS.BasicSerialize(val)
          val_str[#val_str + 1] = ','
          tbl_str[#tbl_str + 1] = table.concat(ind_str)
          tbl_str[#tbl_str + 1] = table.concat(val_str)
        elseif type(val) == 'nil' then -- won't ever happen, right?
          val_str[#val_str + 1] = 'nil,'
          tbl_str[#tbl_str + 1] = table.concat(ind_str)
          tbl_str[#tbl_str + 1] = table.concat(val_str)
        elseif type(val) == 'table' then
          if ind == "__index" then
          --  tbl_str[#tbl_str + 1] = "__index"
          --  tbl_str[#tbl_str + 1] = ','   --I think this is right, I just added it
          else

            val_str[#val_str + 1] = _Serialize(val)
            val_str[#val_str + 1] = ','   --I think this is right, I just added it
            tbl_str[#tbl_str + 1] = table.concat(ind_str)
            tbl_str[#tbl_str + 1] = table.concat(val_str)
          end
        elseif type(val) == 'function' then
          tbl_str[#tbl_str + 1] = "f() " .. tostring(ind)
          tbl_str[#tbl_str + 1] = ','   --I think this is right, I just added it
        else
          env.info('unable to serialize value type ' .. UTILS.BasicSerialize(type(val)) .. ' at index ' .. tostring(ind))
          env.info( debug.traceback() )
        end

      end
      tbl_str[#tbl_str + 1] = '}'
      return table.concat(tbl_str)
    else
      return tostring(tbl)
    end
  end

  local objectreturn = _Serialize(tbl)
  return objectreturn
end

--- Serialize a table to a single line string.
-- @param #table tbl table to serialize.
-- @return #string string containing serialized table.
function UTILS._OneLineSerialize(tbl)

  if type(tbl) == 'table' then --function only works for tables!

    local tbl_str = {}

    tbl_str[#tbl_str + 1] = '{ '

    for ind,val in pairs(tbl) do -- serialize its fields
      if type(ind) == "number" then
        tbl_str[#tbl_str + 1] = '['
        tbl_str[#tbl_str + 1] = tostring(ind)
        tbl_str[#tbl_str + 1] = '] = '
      else --must be a string
        tbl_str[#tbl_str + 1] = '['
        tbl_str[#tbl_str + 1] = UTILS.BasicSerialize(ind)
        tbl_str[#tbl_str + 1] = '] = '
      end

      if ((type(val) == 'number') or (type(val) == 'boolean')) then
        tbl_str[#tbl_str + 1] = tostring(val)
        tbl_str[#tbl_str + 1] = ', '
      elseif type(val) == 'string' then
        tbl_str[#tbl_str + 1] = UTILS.BasicSerialize(val)
        tbl_str[#tbl_str + 1] = ', '
      elseif type(val) == 'nil' then -- won't ever happen, right?
        tbl_str[#tbl_str + 1] = 'nil, '
      elseif type(val) == 'table' then
        --tbl_str[#tbl_str + 1] = UTILS.TableShow(tbl,loc,indent,tableshow_tbls)
        --tbl_str[#tbl_str + 1] = ', '   --I think this is right, I just added it
      else
        --log:warn('Unable to serialize value type $1 at index $2', mist.utils.basicSerialize(type(val)), tostring(ind))
      end

    end

      tbl_str[#tbl_str + 1] = '}'
      return table.concat(tbl_str)
    else
      return  UTILS.BasicSerialize(tbl)
  end
end

--- Basic serialize (porting in Slmod's "safestring" basic serialize).
-- @param #string s Table to serialize.
UTILS.BasicSerialize = function(s)
  if s == nil then
    return "\"\""
  else
    if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'userdata') ) then
      return tostring(s)
    elseif type(s) == "table" then
      return UTILS._OneLineSerialize(s)
    elseif type(s) == 'string' then
      s = string.format('(%s)', s)
      return s
    end
  end
end

--- Counts the number of elements in a table.
-- @param #table T Table to count
-- @return #number Number of elements in the table
function UTILS.TableLength(T)
  local count = 0
  for _ in pairs(T or {}) do count = count + 1 end
  return count
end

--- Print a table to log in a nice format
-- @param #table table The table to print
-- @param #number indent Number of indents
-- @param #boolean noprint Don't log but return text
-- @return #string text Text created on the fly of the log output
function UTILS.PrintTableToLog(table, indent, noprint)
  local text = "\n"
  if not table or type(table) ~= "table" then
    env.warning("No table passed!")
    return nil
  end
  if not indent then indent = 0 end
  for k, v in pairs(table) do
    if string.find(k," ") then k='"'..k..'"'end
    if type(v) == "table" and UTILS.TableLength(v) > 0 then
      if not noprint then
        env.info(string.rep("  ", indent) .. tostring(k) .. " = {")
      end
      text = text ..string.rep("  ", indent) .. tostring(k) .. " = {\n"
      text = text .. tostring(UTILS.PrintTableToLog(v, indent + 1), noprint).."\n"
      if not noprint then
        env.info(string.rep("  ", indent) .. "},")
      end
      text = text .. string.rep("  ", indent) .. "},\n"
    elseif type(v) == "function" then
    else
      local value
      if tostring(v) == "true" or tostring(v) == "false" or tonumber(v) ~= nil then
        value=v
      else
        value = '"'..tostring(v)..'"'
      end
      if not noprint then
        env.info(string.rep("  ", indent) .. tostring(k) .. " = " .. tostring(value)..",\n")
      end
      text = text .. string.rep("  ", indent) .. tostring(k) .. " = " .. tostring(value)..",\n"
    end
  end
  return text
end

--- Returns table in a easy readable string representation.
-- @param tbl table to show
-- @param loc
-- @param indent
-- @param tableshow_tbls
-- @return Human readable string representation of given table.
function UTILS.TableShow(tbl, loc, indent, tableshow_tbls)
  tableshow_tbls = tableshow_tbls or {} --create table of tables
  loc = loc or ""
  indent = indent or ""
  if type(tbl) == 'table' then --function only works for tables!
    tableshow_tbls[tbl] = loc

    local tbl_str = {}

    tbl_str[#tbl_str + 1] = indent .. '{\n'

    for ind,val in pairs(tbl) do -- serialize its fields
      if type(ind) == "number" then
        tbl_str[#tbl_str + 1] = indent
        tbl_str[#tbl_str + 1] = loc .. '['
        tbl_str[#tbl_str + 1] = tostring(ind)
        tbl_str[#tbl_str + 1] = '] = '
      else
        tbl_str[#tbl_str + 1] = indent
        tbl_str[#tbl_str + 1] = loc .. '['
        tbl_str[#tbl_str + 1] = UTILS.BasicSerialize(ind)
        tbl_str[#tbl_str + 1] = '] = '
      end

      if ((type(val) == 'number') or (type(val) == 'boolean')) then
        tbl_str[#tbl_str + 1] = tostring(val)
        tbl_str[#tbl_str + 1] = ',\n'
      elseif type(val) == 'string' then
        tbl_str[#tbl_str + 1] = UTILS.BasicSerialize(val)
        tbl_str[#tbl_str + 1] = ',\n'
      elseif type(val) == 'nil' then -- won't ever happen, right?
        tbl_str[#tbl_str + 1] = 'nil,\n'
      elseif type(val) == 'table' then
        if tableshow_tbls[val] then
          tbl_str[#tbl_str + 1] = tostring(val) .. ' already defined: ' .. tableshow_tbls[val] .. ',\n'
        else
          tableshow_tbls[val] = loc ..  '[' .. UTILS.BasicSerialize(ind) .. ']'
          tbl_str[#tbl_str + 1] = tostring(val) .. ' '
          tbl_str[#tbl_str + 1] = UTILS.TableShow(val, loc .. '[' .. UTILS.BasicSerialize(ind).. ']', indent .. '    ', tableshow_tbls)
          tbl_str[#tbl_str + 1] = ',\n'
        end
      elseif type(val) == 'function' then
        if debug and debug.getinfo then
          local fcnname = tostring(val)
          local info = debug.getinfo(val, "S")
          if info.what == "C" then
            tbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', C function') .. ',\n'
          else
            if (string.sub(info.source, 1, 2) == [[./]]) then
              tbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', defined in (' .. info.linedefined .. '-' .. info.lastlinedefined .. ')' .. info.source) ..',\n'
            else
              tbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', defined in (' .. info.linedefined .. '-' .. info.lastlinedefined .. ')') ..',\n'
            end
          end

        else
          tbl_str[#tbl_str + 1] = 'a function,\n'
        end
      else
        tbl_str[#tbl_str + 1] = 'unable to serialize value type ' .. UTILS.BasicSerialize(type(val)) .. ' at index ' .. tostring(ind)
      end
    end

    tbl_str[#tbl_str + 1] = indent .. '}'
    return table.concat(tbl_str)
  end
end

--- Dumps the global table _G.
-- This dumps the global table _G to a file in the DCS\Logs directory.
-- This function requires you to disable script sanitization in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io libraries.
-- @param #string fname File name.
function UTILS.Gdump(fname)
  if lfs and io then

    local fdir = lfs.writedir() .. [[Logs\]] .. fname

    local f = io.open(fdir, 'w')

    f:write(UTILS.TableShow(_G))

    f:close()

    env.info(string.format('Wrote debug data to $1', fdir))
  else
    env.error("WARNING: lfs and/or io not de-sanitized - cannot dump _G!")
  end
end

--- Executes the given string.
-- borrowed from Slmod
-- @param #string s string containing LUA code.
-- @return #boolean `true` if successfully executed, `false` otherwise.
function UTILS.DoString(s)
  local f, err = loadstring(s)
  if f then
    return true, f()
  else
    return false, err
  end
end


UTILS.ToDegree = function(angle)
  return angle*180/math.pi
end

UTILS.ToRadian = function(angle)
  return angle*math.pi/180
end

UTILS.MetersToNM = function(meters)
  return meters/1852
end

UTILS.KiloMetersToNM = function(kilometers)
  return kilometers/1852*1000
end

UTILS.MetersToSM = function(meters)
  return meters/1609.34
end

UTILS.KiloMetersToSM = function(kilometers)
  return kilometers/1609.34*1000
end

UTILS.MetersToFeet = function(meters)
  return meters/0.3048
end

UTILS.KiloMetersToFeet = function(kilometers)
  return kilometers/0.3048*1000
end

UTILS.NMToMeters = function(NM)
  return NM*1852
end

UTILS.NMToKiloMeters = function(NM)
  return NM*1852/1000
end

UTILS.FeetToMeters = function(feet)
  return feet*0.3048
end

UTILS.KnotsToKmph = function(knots)
  return knots * 1.852
end

UTILS.KmphToKnots = function(knots)
  return knots / 1.852
end

UTILS.KmphToMps = function( kmph )
  return kmph / 3.6
end

UTILS.MpsToKmph = function( mps )
  return mps * 3.6
end

UTILS.MiphToMps = function( miph )
  return miph * 0.44704
end

--- Convert meters per second to miles per hour.
-- @param #number mps Speed in m/s.
-- @return #number Speed in miles per hour.
UTILS.MpsToMiph = function( mps )
  return mps / 0.44704
end

--- Convert meters per second to knots.
-- @param #number mps Speed in m/s.
-- @return #number Speed in knots.
UTILS.MpsToKnots = function( mps )
  return mps * 1.94384 --3600 / 1852
end

--- Convert knots to meters per second.
-- @param #number knots Speed in knots.
-- @return #number Speed in m/s.
UTILS.KnotsToMps = function( knots )
  if type(knots) == "number" then
    return knots / 1.94384 --* 1852 / 3600
  else
   return 0
  end
end

--- Convert temperature from Celsius to Fahrenheit.
-- @param #number Celcius Temperature in degrees Celsius.
-- @return #number Temperature in degrees Fahrenheit.
UTILS.CelsiusToFahrenheit = function( Celcius )
  return Celcius * 9/5 + 32
end

--- Convert pressure from hecto Pascal (hPa) to inches of mercury (inHg).
-- @param #number hPa Pressure in hPa.
-- @return #number Pressure in inHg.
UTILS.hPa2inHg = function( hPa )
  return hPa * 0.0295299830714
end

--- Convert indicated airspeed (IAS) to true airspeed (TAS) for a given altitude above main sea level.
-- The conversion is based on the approximation that TAS is ~2% higher than IAS with every 1000 ft altitude above sea level.
-- @param #number ias Indicated air speed in any unit (m/s, km/h, knots, ...)
-- @param #number altitude Altitude above main sea level in meters.
-- @param #number oatcorr (Optional) Outside air temperature correction factor. Default 0.017.
-- @return #number True airspeed in the same unit the IAS has been given.
UTILS.IasToTas = function( ias, altitude, oatcorr )
  oatcorr=oatcorr or 0.017
  local tas=ias + (ias * oatcorr * UTILS.MetersToFeet(altitude) / 1000)
  return tas
end

--- Convert true airspeed (TAS) to indicated airspeed (IAS) for a given altitude above main sea level.
-- The conversion is based on the approximation that TAS is ~2% higher than IAS with every 1000 ft altitude above sea level.
-- @param #number tas True air speed in any unit (m/s, km/h, knots, ...)
-- @param #number altitude Altitude above main sea level in meters.
-- @param #number oatcorr (Optional) Outside air temperature correction factor. Default 0.017.
-- @return #number Indicated airspeed in the same unit the TAS has been given.
UTILS.TasToIas = function( tas, altitude, oatcorr )
  oatcorr=oatcorr or 0.017
  local ias=tas/(1+oatcorr*UTILS.MetersToFeet(altitude)/1000)
  return ias
end


--- Convert knots to altitude corrected KIAS, e.g. for tankers.
-- @param #number knots Speed in knots.
-- @param #number altitude Altitude in feet
-- @return #number Corrected KIAS
UTILS.KnotsToAltKIAS = function( knots, altitude )
  return (knots * 0.018 * (altitude / 1000)) + knots
end

--- Convert pressure from hecto Pascal (hPa) to millimeters of mercury (mmHg).
-- @param #number hPa Pressure in hPa.
-- @return #number Pressure in mmHg.
UTILS.hPa2mmHg = function( hPa )
  return hPa * 0.7500615613030
end

--- Convert kilo gramms (kg) to pounds (lbs).
-- @param #number kg Mass in kg.
-- @return #number Mass in lbs.
UTILS.kg2lbs = function( kg )
  return kg * 2.20462
end

--[[acc:
in DM: decimal point of minutes.
In DMS: decimal point of seconds.
position after the decimal of the least significant digit:
So:
42.32 - acc of 2.
]]
UTILS.tostringLL = function( lat, lon, acc, DMS)

  local latHemi, lonHemi
  if lat > 0 then
    latHemi = 'N'
  else
    latHemi = 'S'
  end

  if lon > 0 then
    lonHemi = 'E'
  else
    lonHemi = 'W'
  end

  lat = math.abs(lat)
  lon = math.abs(lon)

  local latDeg = math.floor(lat)
  local latMin = (lat - latDeg)*60

  local lonDeg = math.floor(lon)
  local lonMin = (lon - lonDeg)*60

  if DMS then  -- degrees, minutes, and seconds.
    local oldLatMin = latMin
    latMin = math.floor(latMin)
    local latSec = UTILS.Round((oldLatMin - latMin)*60, acc)

    local oldLonMin = lonMin
    lonMin = math.floor(lonMin)
    local lonSec = UTILS.Round((oldLonMin - lonMin)*60, acc)

    if latSec == 60 then
      latSec = 0
      latMin = latMin + 1
    end

    if lonSec == 60 then
      lonSec = 0
      lonMin = lonMin + 1
    end

    local secFrmtStr -- create the formatting string for the seconds place
    secFrmtStr = '%02d'
    if acc <= 0 then  -- no decimal place.
      secFrmtStr = '%02d'
    else
      local width = 3 + acc  -- 01.310 - that's a width of 6, for example. Acc is limited to 2 for DMS!
      secFrmtStr = '%0' .. width .. '.' .. acc .. 'f'
    end

    -- 024° 23' 12"N or 024° 23' 12.03"N
    return string.format('%03d°', latDeg)..string.format('%02d', latMin)..'\''..string.format(secFrmtStr, latSec)..'"'..latHemi..' '
        .. string.format('%03d°', lonDeg)..string.format('%02d', lonMin)..'\''..string.format(secFrmtStr, lonSec)..'"'..lonHemi

  else  -- degrees, decimal minutes.
    latMin = UTILS.Round(latMin, acc)
    lonMin = UTILS.Round(lonMin, acc)

    if latMin == 60 then
      latMin = 0
      latDeg = latDeg + 1
    end

    if lonMin == 60 then
      lonMin = 0
      lonDeg = lonDeg + 1
    end

    local minFrmtStr -- create the formatting string for the minutes place
    if acc <= 0 then  -- no decimal place.
      minFrmtStr = '%02d'
    else
      local width = 3 + acc  -- 01.310 - that's a width of 6, for example.
      minFrmtStr = '%0' .. width .. '.' .. acc .. 'f'
    end

    -- 024 23'N or 024 23.123'N
    return string.format('%03d°', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. '   '
        .. string.format('%03d°', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi

  end
end

--[[acc:
in DM: decimal point of minutes.
In DMS: decimal point of seconds.
position after the decimal of the least significant digit:
So:
42.32 - acc of 2.
]]
UTILS.tostringLLM2KData = function( lat, lon, acc)

  local latHemi, lonHemi
  if lat > 0 then
    latHemi = 'N'
  else
    latHemi = 'S'
  end

  if lon > 0 then
    lonHemi = 'E'
  else
    lonHemi = 'W'
  end

  lat = math.abs(lat)
  lon = math.abs(lon)

  local latDeg = math.floor(lat)
  local latMin = (lat - latDeg)*60

  local lonDeg = math.floor(lon)
  local lonMin = (lon - lonDeg)*60

  -- degrees, decimal minutes.
  latMin = UTILS.Round(latMin, acc)
  lonMin = UTILS.Round(lonMin, acc)

  if latMin == 60 then
    latMin = 0
    latDeg = latDeg + 1
  end

  if lonMin == 60 then
    lonMin = 0
    lonDeg = lonDeg + 1
  end

  local minFrmtStr -- create the formatting string for the minutes place
  if acc <= 0 then  -- no decimal place.
    minFrmtStr = '%02d'
  else
    local width = 3 + acc  -- 01.310 - that's a width of 6, for example.
    minFrmtStr = '%0' .. width .. '.' .. acc .. 'f'
  end

  -- 024 23'N or 024 23.123'N
  return latHemi..string.format('%02d:', latDeg) .. string.format(minFrmtStr, latMin), lonHemi..string.format('%02d:', lonDeg) .. string.format(minFrmtStr, lonMin)

end

-- acc- the accuracy of each easting/northing.  0, 1, 2, 3, 4, or 5.
UTILS.tostringMGRS = function(MGRS, acc) --R2.1

  if acc <= 0 then
    return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph
  else

    if acc > 5 then acc = 5 end

    -- Test if Easting/Northing have less than 4 digits.
    --MGRS.Easting=123    -- should be 00123
    --MGRS.Northing=5432  -- should be 05432

    -- Truncate rather than round MGRS grid!
    local Easting=tostring(MGRS.Easting)
    local Northing=tostring(MGRS.Northing)

    -- Count number of missing digits. Easting/Northing should have 5 digits. However, it is passed as a number. Therefore, any leading zeros would not be displayed by lua.
    local nE=5-string.len(Easting)
    local nN=5-string.len(Northing)

    -- Get leading zeros (if any).
    for i=1,nE do Easting="0"..Easting end
    for i=1,nN do Northing="0"..Northing end

    -- Return MGRS string.
    return string.format("%s %s %s %s", MGRS.UTMZone, MGRS.MGRSDigraph, string.sub(Easting, 1, acc), string.sub(Northing, 1, acc))
  end

end


--- From http://lua-users.org/wiki/SimpleRound
-- use negative idp for rounding ahead of decimal place, positive for rounding after decimal place
function UTILS.Round( num, idp )
  local mult = 10 ^ ( idp or 0 )
  return math.floor( num * mult + 0.5 ) / mult
end

--- Porting in Slmod's dostring - execute a string as LUA code with error handling.
-- @param #string s The code as string to be executed
-- @return #boolean success If true, code was successfully executed, else false
-- @return #string Outcome Code outcome if successful or error string if not successful
function UTILS.DoString( s )
  local f, err = loadstring( s )
  if f then
    return true, f()
  else
    return false, err
  end
end

--- Here is a customized version of pairs, which I called spairs because it iterates over the table in a sorted order.
-- @param #table t The table
-- @param #string order (Optional) The sorting function
-- @return #string key The index key
-- @return #string value The value at the indexed key
-- @usage
--            for key,value in UTILS.spairs(mytable) do
--                -- your code here
--            end
function UTILS.spairs( t, order )
    -- collect the keys
    local keys = {}
    for k in pairs(t) do keys[#keys+1] = k end

    -- if order function given, sort by it by passing the table and keys a, b,
    -- otherwise just sort the keys
    if order then
        table.sort(keys, function(a,b) return order(t, a, b) end)
    else
        table.sort(keys)
    end

    -- return the iterator function
    local i = 0
    return function()
        i = i + 1
        if keys[i] then
            return keys[i], t[keys[i]]
        end
    end
end


--- Here is a customized version of pairs, which I called kpairs because it iterates over the table in a sorted order, based on a function that will determine the keys as reference first.
-- @param #table t The table
-- @param #string getkey The function to determine the keys for sorting
-- @param #string order (Optional) The sorting function itself
-- @return #string key The index key
-- @return #string value The value at the indexed key
-- @usage
--            for key,value in UTILS.kpairs(mytable, getkeyfunc) do
--                -- your code here
--            end
function UTILS.kpairs( t, getkey, order )
    -- collect the keys
    local keys = {}
    local keyso = {}
    for k, o in pairs(t) do keys[#keys+1] = k keyso[#keyso+1] = getkey( o ) end

    -- if order function given, sort by it by passing the table and keys a, b,
    -- otherwise just sort the keys
    if order then
        table.sort(keys, function(a,b) return order(t, a, b) end)
    else
        table.sort(keys)
    end

    -- return the iterator function
    local i = 0
    return function()
        i = i + 1
        if keys[i] then
            return keyso[i], t[keys[i]]
        end
    end
end

--- Here is a customized version of pairs, which I called rpairs because it iterates over the table in a random order.
-- @param #table t The table
-- @return #string key The index key
-- @return #string value The value at the indexed key
-- @usage
--            for key,value in UTILS.rpairs(mytable) do
--                -- your code here
--            end
function UTILS.rpairs( t )
    -- collect the keys

    local keys = {}
    for k in pairs(t) do keys[#keys+1] = k end

    local random = {}
    local j = #keys
    for i = 1, j do
      local k = math.random( 1, #keys )
      random[i] = keys[k]
      table.remove( keys, k )
    end

    -- return the iterator function
    local i = 0
    return function()
        i = i + 1
        if random[i] then
            return random[i], t[random[i]]
        end
    end
end

-- get a new mark ID for markings
function UTILS.GetMarkID()

  UTILS._MarkID = UTILS._MarkID + 1
  return UTILS._MarkID

end

--- Remove an object (marker, circle, arrow, text, quad, ...) on the F10 map.
-- @param #number MarkID Unique ID of the object.
-- @param #number Delay (Optional) Delay in seconds before the mark is removed.
function UTILS.RemoveMark(MarkID, Delay)
  if Delay and Delay>0 then
    TIMER:New(UTILS.RemoveMark, MarkID):Start(Delay)
  else
    if MarkID then
      trigger.action.removeMark(MarkID)
    end
  end
end


-- Test if a Vec2 is in a radius of another Vec2
function UTILS.IsInRadius( InVec2, Vec2, Radius )

  local InRadius = ( ( InVec2.x - Vec2.x ) ^2 + ( InVec2.y - Vec2.y ) ^2 ) ^ 0.5 <= Radius

  return InRadius
end

-- Test if a Vec3 is in the sphere of another Vec3
function UTILS.IsInSphere( InVec3, Vec3, Radius )

  local InSphere = ( ( InVec3.x - Vec3.x ) ^2 + ( InVec3.y - Vec3.y ) ^2 + ( InVec3.z - Vec3.z ) ^2 ) ^ 0.5 <= Radius

  return InSphere
end

--- Beaufort scale: returns Beaufort number and wind description as a function of wind speed in m/s.
-- @param #number speed Wind speed in m/s.
-- @return #number Beaufort number.
-- @return #string Beauford wind description.
function UTILS.BeaufortScale(speed)
  local bn=nil
  local bd=nil
  if speed<0.51 then
    bn=0
    bd="Calm"
  elseif speed<2.06 then
    bn=1
    bd="Light Air"
  elseif speed<3.60 then
    bn=2
    bd="Light Breeze"
  elseif speed<5.66 then
    bn=3
    bd="Gentle Breeze"
  elseif speed<8.23 then
    bn=4
    bd="Moderate Breeze"
  elseif speed<11.32 then
    bn=5
    bd="Fresh Breeze"
  elseif speed<14.40 then
    bn=6
    bd="Strong Breeze"
  elseif speed<17.49 then
    bn=7
    bd="Moderate Gale"
  elseif speed<21.09 then
    bn=8
    bd="Fresh Gale"
  elseif speed<24.69 then
    bn=9
    bd="Strong Gale"
  elseif speed<28.81 then
    bn=10
    bd="Storm"
  elseif speed<32.92 then
    bn=11
    bd="Violent Storm"
  else
    bn=12
    bd="Hurricane"
  end
  return bn,bd
end

--- Split string at separators. C.f. [split-string-in-lua](http://stackoverflow.com/questions/1426954/split-string-in-lua).
-- @param #string str Sting to split.
-- @param #string sep Separator for split.
-- @return #table Split text.
function UTILS.Split(str, sep)
  local result = {}
  local regex = ("([^%s]+)"):format(sep)
  for each in str:gmatch(regex) do
    table.insert(result, each)
  end
  return result
end

--- Get a table of all characters in a string.
-- @param #string str Sting.
-- @return #table Individual characters.
function UTILS.GetCharacters(str)

  local chars={}

  for i=1,#str do
    local c=str:sub(i,i)
    table.insert(chars, c)
  end

  return chars
end

--- Convert time in seconds to hours, minutes and seconds.
-- @param #number seconds Time in seconds, e.g. from timer.getAbsTime() function.
-- @param #boolean short (Optional) If true, use short output, i.e. (HH:)MM:SS without day.
-- @return #string Time in format Hours:Minutes:Seconds+Days (HH:MM:SS+D).
function UTILS.SecondsToClock(seconds, short)

  -- Nil check.
  if seconds==nil then
    return nil
  end

  -- Seconds
  local seconds = tonumber(seconds) or 0

  -- Seconds of this day.
  local _seconds=seconds%(60*60*24)

  if seconds<0 then
    return nil
  else
    local hours = string.format("%02.f", math.floor(_seconds/3600))
    local mins  = string.format("%02.f", math.floor(_seconds/60 - (hours*60)))
    local secs  = string.format("%02.f", math.floor(_seconds - hours*3600 - mins *60))
    local days  = string.format("%d", seconds/(60*60*24))
    local clock=hours..":"..mins..":"..secs.."+"..days
    if short then
      if hours=="00" then
        --clock=mins..":"..secs
        clock=hours..":"..mins..":"..secs
      else
        clock=hours..":"..mins..":"..secs
      end
    end
    return clock
  end
end

--- Seconds of today.
-- @return #number Seconds passed since last midnight.
function UTILS.SecondsOfToday()

    -- Time in seconds.
    local time=timer.getAbsTime()

    -- Short format without days since mission start.
    local clock=UTILS.SecondsToClock(time, true)

    -- Time is now the seconds passed since last midnight.
    return UTILS.ClockToSeconds(clock)
end

--- Cound seconds until next midnight.
-- @return #number Seconds to midnight.
function UTILS.SecondsToMidnight()
  return 24*60*60-UTILS.SecondsOfToday()
end

--- Convert clock time from hours, minutes and seconds to seconds.
-- @param #string clock String of clock time. E.g., "06:12:35" or "5:1:30+1". Format is (H)H:(M)M:((S)S)(+D) H=Hours, M=Minutes, S=Seconds, D=Days.
-- @return #number Seconds. Corresponds to what you cet from timer.getAbsTime() function.
function UTILS.ClockToSeconds(clock)

  -- Nil check.
  if clock==nil then
    return nil
  end

  -- Seconds init.
  local seconds=0

  -- Split additional days.
  local dsplit=UTILS.Split(clock, "+")

  -- Convert days to seconds.
  if #dsplit>1 then
    seconds=seconds+tonumber(dsplit[2])*60*60*24
  end

  -- Split hours, minutes, seconds
  local tsplit=UTILS.Split(dsplit[1], ":")

  -- Get time in seconds
  local i=1
  for _,time in ipairs(tsplit) do
    if i==1 then
      -- Hours
      seconds=seconds+tonumber(time)*60*60
    elseif i==2 then
      -- Minutes
      seconds=seconds+tonumber(time)*60
    elseif i==3 then
      -- Seconds
      seconds=seconds+tonumber(time)
    end
    i=i+1
  end

  return seconds
end

--- Display clock and mission time on screen as a message to all.
-- @param #number duration Duration in seconds how long the time is displayed. Default is 5 seconds.
function UTILS.DisplayMissionTime(duration)
  duration=duration or 5
  local Tnow=timer.getAbsTime()
  local mission_time=Tnow-timer.getTime0()
  local mission_time_minutes=mission_time/60
  local mission_time_seconds=mission_time%60
  local local_time=UTILS.SecondsToClock(Tnow)
  local text=string.format("Time: %s - %02d:%02d", local_time, mission_time_minutes, mission_time_seconds)
  MESSAGE:New(text, duration):ToAll()
end

--- Replace illegal characters [<>|/?*:\\] in a string.
-- @param #string Text Input text.
-- @param #string ReplaceBy Replace illegal characters by this character or string. Default underscore "_".
-- @return #string The input text with illegal chars replaced.
function UTILS.ReplaceIllegalCharacters(Text, ReplaceBy)
  ReplaceBy=ReplaceBy or "_"
  local text=Text:gsub("[<>|/?*:\\]", ReplaceBy)
  return text
end

--- Generate a Gaussian pseudo-random number.
-- @param #number x0 Expectation value of distribution.
-- @param #number sigma (Optional) Standard deviation. Default 10.
-- @param #number xmin (Optional) Lower cut-off value.
-- @param #number xmax (Optional) Upper cut-off value.
-- @param #number imax (Optional) Max number of tries to get a value between xmin and xmax (if specified). Default 100.
-- @return #number Gaussian random number.
function UTILS.RandomGaussian(x0, sigma, xmin, xmax, imax)

  -- Standard deviation. Default 10 if not given.
  sigma=sigma or 10

  -- Max attempts.
  imax=imax or 100

  local r
  local gotit=false
  local i=0
  while not gotit do

    -- Uniform numbers in [0,1). We need two.
    local x1=math.random()
    local x2=math.random()

    -- Transform to Gaussian exp(-(x-x0)°/(2*sigma°).
    r = math.sqrt(-2*sigma*sigma * math.log(x1)) * math.cos(2*math.pi * x2) + x0

    i=i+1
    if (r>=xmin and r<=xmax) or i>imax then
      gotit=true
    end
  end

  return r
end

--- Randomize a value by a certain amount.
-- @param #number value The value which should be randomized
-- @param #number fac Randomization factor.
-- @param #number lower (Optional) Lower limit of the returned value.
-- @param #number upper (Optional) Upper limit of the returned value.
-- @return #number Randomized value.
-- @usage UTILS.Randomize(100, 0.1) returns a value between 90 and 110, i.e. a plus/minus ten percent variation.
-- @usage UTILS.Randomize(100, 0.5, nil, 120) returns a value between 50 and 120, i.e. a plus/minus fivty percent variation with upper bound 120.
function UTILS.Randomize(value, fac, lower, upper)
  local min
  if lower then
    min=math.max(value-value*fac, lower)
  else
    min=value-value*fac
  end
  local max
  if upper then
    max=math.min(value+value*fac, upper)
  else
    max=value+value*fac
  end

  local r=math.random(min, max)

  return r
end

--- Calculate the [dot product](https://en.wikipedia.org/wiki/Dot_product) of two vectors. The result is a number.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param DCS#Vec3 b Vector in 3D with x, y, z components.
-- @return #number Scalar product of the two vectors a*b.
function UTILS.VecDot(a, b)
  return a.x*b.x + a.y*b.y + a.z*b.z
end

--- Calculate the [dot product](https://en.wikipedia.org/wiki/Dot_product) of two 2D vectors. The result is a number.
-- @param DCS#Vec2 a Vector in 2D with x, y components.
-- @param DCS#Vec2 b Vector in 2D with x, y components.
-- @return #number Scalar product of the two vectors a*b.
function UTILS.Vec2Dot(a, b)
  return a.x*b.x + a.y*b.y
end


--- Calculate the [euclidean norm](https://en.wikipedia.org/wiki/Euclidean_distance) (length) of a 3D vector.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @return #number Norm of the vector.
function UTILS.VecNorm(a)
  return math.sqrt(UTILS.VecDot(a, a))
end

--- Calculate the [euclidean norm](https://en.wikipedia.org/wiki/Euclidean_distance) (length) of a 2D vector.
-- @param DCS#Vec2 a Vector in 2D with x, y components.
-- @return #number Norm of the vector.
function UTILS.Vec2Norm(a)
  return math.sqrt(UTILS.Vec2Dot(a, a))
end

--- Calculate the distance between two 2D vectors.
-- @param DCS#Vec2 a Vector in 2D with x, y components.
-- @param DCS#Vec2 b Vector in 2D with x, y components.
-- @return #number Distance between the vectors.
function UTILS.VecDist2D(a, b)

  local d = math.huge

  if (not a) or (not b) then return d end

  local c={x=b.x-a.x, y=b.y-a.y}

  d=math.sqrt(c.x*c.x+c.y*c.y)

  return d
end


--- Calculate the distance between two 3D vectors.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param DCS#Vec3 b Vector in 3D with x, y, z components.
-- @return #number Distance between the vectors.
function UTILS.VecDist3D(a, b)


  local d = math.huge

  if (not a) or (not b) then return d end

  local c={x=b.x-a.x, y=b.y-a.y, z=b.z-a.z}

  d=math.sqrt(UTILS.VecDot(c, c))

  return d
end

--- Calculate the [cross product](https://en.wikipedia.org/wiki/Cross_product) of two 3D vectors. The result is a 3D vector.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param DCS#Vec3 b Vector in 3D with x, y, z components.
-- @return DCS#Vec3 Vector
function UTILS.VecCross(a, b)
  return {x=a.y*b.z - a.z*b.y, y=a.z*b.x - a.x*b.z, z=a.x*b.y - a.y*b.x}
end

--- Calculate the difference between two 3D vectors by substracting the x,y,z components from each other.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param DCS#Vec3 b Vector in 3D with x, y, z components.
-- @return DCS#Vec3 Vector c=a-b with c(i)=a(i)-b(i), i=x,y,z.
function UTILS.VecSubstract(a, b)
  return {x=a.x-b.x, y=a.y-b.y, z=a.z-b.z}
end

--- Substract is not a word, don't want to rename the original function because it's been around since forever
function UTILS.VecSubtract(a, b)
  return UTILS.VecSubstract(a, b)
end

--- Calculate the difference between two 2D vectors by substracting the x,y components from each other.
-- @param DCS#Vec2 a Vector in 2D with x, y components.
-- @param DCS#Vec2 b Vector in 2D with x, y components.
-- @return DCS#Vec2 Vector c=a-b with c(i)=a(i)-b(i), i=x,y.
function UTILS.Vec2Substract(a, b)
  return {x=a.x-b.x, y=a.y-b.y}
end

--- Substract is not a word, don't want to rename the original function because it's been around since forever
function UTILS.Vec2Subtract(a, b)
  return UTILS.Vec2Substract(a, b)
end

--- Calculate the total vector of two 3D vectors by adding the x,y,z components of each other.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param DCS#Vec3 b Vector in 3D with x, y, z components.
-- @return DCS#Vec3 Vector c=a+b with c(i)=a(i)+b(i), i=x,y,z.
function UTILS.VecAdd(a, b)
  return {x=a.x+b.x, y=a.y+b.y, z=a.z+b.z}
end

--- Calculate the total vector of two 2D vectors by adding the x,y components of each other.
-- @param DCS#Vec2 a Vector in 2D with x, y components.
-- @param DCS#Vec2 b Vector in 2D with x, y components.
-- @return DCS#Vec2 Vector c=a+b with c(i)=a(i)+b(i), i=x,y.
function UTILS.Vec2Add(a, b)
  return {x=a.x+b.x, y=a.y+b.y}
end

--- Calculate the angle between two 3D vectors.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param DCS#Vec3 b Vector in 3D with x, y, z components.
-- @return #number Angle alpha between and b in degrees. alpha=acos(a*b)/(|a||b|), (* denotes the dot product).
function UTILS.VecAngle(a, b)

  local cosalpha=UTILS.VecDot(a,b)/(UTILS.VecNorm(a)*UTILS.VecNorm(b))

  local alpha=0
  if cosalpha>=0.9999999999 then  --acos(1) is not defined.
    alpha=0
  elseif cosalpha<=-0.999999999 then --acos(-1) is not defined.
    alpha=math.pi
  else
    alpha=math.acos(cosalpha)
  end

  return math.deg(alpha)
end

--- Calculate "heading" of a 3D vector in the X-Z plane.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @return #number Heading in degrees in [0,360).
function UTILS.VecHdg(a)
  local h=math.deg(math.atan2(a.z, a.x))
  if h<0 then
    h=h+360
  end
  return h
end

--- Calculate "heading" of a 2D vector in the X-Y plane.
-- @param DCS#Vec2 a Vector in 2D with x, y components.
-- @return #number Heading in degrees in [0,360).
function UTILS.Vec2Hdg(a)
  local h=math.deg(math.atan2(a.y, a.x))
  if h<0 then
    h=h+360
  end
  return h
end

--- Calculate the difference between two "heading", i.e. angles in [0,360) deg.
-- @param #number h1 Heading one.
-- @param #number h2 Heading two.
-- @return #number Heading difference in degrees.
function UTILS.HdgDiff(h1, h2)

  -- Angle in rad.
  local alpha= math.rad(tonumber(h1))
  local beta = math.rad(tonumber(h2))

  -- Runway vector.
  local v1={x=math.cos(alpha), y=0, z=math.sin(alpha)}
  local v2={x=math.cos(beta),  y=0, z=math.sin(beta)}

  local delta=UTILS.VecAngle(v1, v2)

  return math.abs(delta)
end

--- Returns the heading from one vec2/vec3 to another vec2/vec3.
-- @param DCS#Vec3 a From Vec2 or Vec3.
-- @param DCS#Vec3 b To Vec2 or Vec3.
-- @return #number Heading in degrees.
function UTILS.HdgTo(a, b)
  local dz=(b.z or b.y) - (a.z or a.y)
  local dx=b.x-a.x
  local heading=math.deg(math.atan2(dz, dx))
  if heading < 0 then
    heading = 360 + heading
  end
  return heading
end


--- Translate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param #number distance The distance to translate.
-- @param #number angle Rotation angle in degrees.
-- @return DCS#Vec3 Vector rotated in the (x,z) plane.
function UTILS.VecTranslate(a, distance, angle)

  local SX = a.x
  local SY = a.z
  local Radians=math.rad(angle or 0)
  local TX=distance*math.cos(Radians)+SX
  local TY=distance*math.sin(Radians)+SY

  return {x=TX, y=a.y, z=TY}
end

--- Translate 2D vector in the 2D (x,z) plane.
-- @param DCS#Vec2 a Vector in 2D with x, y components.
-- @param #number distance The distance to translate.
-- @param #number angle Rotation angle in degrees.
-- @return DCS#Vec2 Translated vector.
function UTILS.Vec2Translate(a, distance, angle)

  local SX = a.x
  local SY = a.y
  local Radians=math.rad(angle or 0)
  local TX=distance*math.cos(Radians)+SX
  local TY=distance*math.sin(Radians)+SY

  return {x=TX, y=TY}
end

--- Rotate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param #number angle Rotation angle in degrees.
-- @return DCS#Vec3 Vector rotated in the (x,z) plane.
function UTILS.Rotate2D(a, angle)

  local phi=math.rad(angle)

  local x=a.z
  local y=a.x

  local Z=x*math.cos(phi)-y*math.sin(phi)
  local X=x*math.sin(phi)+y*math.cos(phi)
  local Y=a.y

  local A={x=X, y=Y, z=Z}

  return A
end

--- Rotate 2D vector in the 2D (x,z) plane.
-- @param DCS#Vec2 a Vector in 2D with x, y components.
-- @param #number angle Rotation angle in degrees.
-- @return DCS#Vec2 Vector rotated in the (x,y) plane.
function UTILS.Vec2Rotate2D(a, angle)

  local phi=math.rad(angle)

  local x=a.x
  local y=a.y

  local X=x*math.cos(phi)-y*math.sin(phi)
  local Y=x*math.sin(phi)+y*math.cos(phi)

  local A={x=X, y=Y}

  return A
end


--- Converts a TACAN Channel/Mode couple into a frequency in Hz.
-- @param #number TACANChannel The TACAN channel, i.e. the 10 in "10X".
-- @param #string TACANMode The TACAN mode, i.e. the "X" in "10X".
-- @return #number Frequency in Hz or #nil if parameters are invalid.
function UTILS.TACANToFrequency(TACANChannel, TACANMode)

  if type(TACANChannel) ~= "number" then
    return nil -- error in arguments
  end
  if TACANMode ~= "X" and TACANMode ~= "Y" then
    return nil -- error in arguments
  end

-- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137.
-- I have no idea what it does but it seems to work
  local A = 1151 -- 'X', channel >= 64
  local B = 64   -- channel >= 64

  if TACANChannel < 64 then
    B = 1
  end

  if TACANMode == 'Y' then
    A = 1025
    if TACANChannel < 64 then
      A = 1088
    end
  else -- 'X'
    if TACANChannel < 64 then
      A = 962
    end
  end

  return (A + TACANChannel - B) * 1000000
end


--- Returns the DCS map/theatre as optained by `env.mission.theatre`.
-- @return #string DCS map name.
function UTILS.GetDCSMap()
  return env.mission.theatre
end

--- Returns the mission date. This is the date the mission **started**.
-- @return #string Mission date in yyyy/mm/dd format.
-- @return #number The year anno domini.
-- @return #number The month.
-- @return #number The day.
function UTILS.GetDCSMissionDate()
  local year=tostring(env.mission.date.Year)
  local month=tostring(env.mission.date.Month)
  local day=tostring(env.mission.date.Day)
  return string.format("%s/%s/%s", year, month, day), tonumber(year), tonumber(month), tonumber(day)
end

--- Returns the day of the mission.
-- @param #number Time (Optional) Abs. time in seconds. Default now, i.e. the value return from timer.getAbsTime().
-- @return #number Day of the mission. Mission starts on day 0.
function UTILS.GetMissionDay(Time)

  Time=Time or timer.getAbsTime()

  local clock=UTILS.SecondsToClock(Time, false)

  local x=tonumber(UTILS.Split(clock, "+")[2])

  return x
end

--- Returns the current day of the year of the mission.
-- @param #number Time (Optional) Abs. time in seconds. Default now, i.e. the value return from timer.getAbsTime().
-- @return #number Current day of year of the mission. For example, January 1st returns 0, January 2nd returns 1 etc.
function UTILS.GetMissionDayOfYear(Time)

  local Date, Year, Month, Day=UTILS.GetDCSMissionDate()

  local d=UTILS.GetMissionDay(Time)

  return UTILS.GetDayOfYear(Year, Month, Day)+d

end

--- Returns the magnetic declination of the map.
-- Returned values for the current maps are:
--
-- * Caucasus +6 (East), year ~ 2011
-- * NTTR +12 (East), year ~ 2011
-- * Normandy -10 (West), year ~ 1944
-- * Persian Gulf +2 (East), year ~ 2011
-- * The Cannel Map -10 (West)
-- * Syria +5 (East)
-- * Mariana Islands +2 (East)
-- * Falklands +12 (East) - note there's a LOT of deviation across the map, as we're closer to the South Pole
-- * Sinai +4.8 (East)
-- * Kola +15 (East) - note there is a lot of deviation across the map (-1° to +24°), as we are close to the North pole
-- * Afghanistan +3 (East) - actually +3.6 (NW) to +2.3 (SE)
-- * Iraq +4.4 (East)
-- * Germany Cold War +0.1 (East) - near Fulda
-- @param #string map (Optional) Map for which the declination is returned. Default is from `env.mission.theatre`.
-- @return #number Declination in degrees.
function UTILS.GetMagneticDeclination(map)

  -- Map.
  map=map or UTILS.GetDCSMap()

  local declination=0
  if map==DCSMAP.Caucasus then
    declination=6
  elseif map==DCSMAP.NTTR then
    declination=12
  elseif map==DCSMAP.Normandy then
    declination=-10
  elseif map==DCSMAP.PersianGulf then
    declination=2
  elseif map==DCSMAP.TheChannel then
    declination=-10
  elseif map==DCSMAP.Syria then
    declination=5
  elseif map==DCSMAP.MarianaIslands then
    declination=2
  elseif map==DCSMAP.Falklands then
    declination=12
  elseif map==DCSMAP.Sinai then
    declination=4.8
  elseif map==DCSMAP.Kola then
    declination=15
  elseif map==DCSMAP.Afghanistan then
    declination=3
  elseif map==DCSMAP.Iraq then
    declination=4.4
  elseif map==DCSMAP.GermanyCW then
    declination=0.1
  else
    declination=0
  end

  return declination
end

--- Checks if a file exists or not. This requires **io** to be desanitized.
-- @param #string file File that should be checked.
-- @return #boolean True if the file exists, false if the file does not exist or nil if the io module is not available and the check could not be performed.
function UTILS.FileExists(file)
  if io then
    local f=io.open(file, "r")
    if f~=nil then
      io.close(f)
      return true
    else
      return false
    end
  else
    return nil
  end
end

--- Checks the current memory usage collectgarbage("count"). Info is printed to the DCS log file. Time stamp is the current mission runtime.
-- @param #boolean output If true, print to DCS log file.
-- @return #number Memory usage in kByte.
function UTILS.CheckMemory(output)
  local time=timer.getTime()
  local clock=UTILS.SecondsToClock(time)
  local mem=collectgarbage("count")
  if output then
    env.info(string.format("T=%s  Memory usage %d kByte = %.2f MByte", clock, mem, mem/1024))
  end
  return mem
end


--- Get the coalition name from its numerical ID, e.g. coalition.side.RED.
-- @param #number Coalition The coalition ID.
-- @return #string The coalition name, i.e. "Neutral", "Red" or "Blue" (or "Unknown").
function UTILS.GetCoalitionName(Coalition)

  if Coalition then
    if Coalition==coalition.side.BLUE then
      return "Blue"
    elseif Coalition==coalition.side.RED then
      return "Red"
    elseif Coalition==coalition.side.NEUTRAL then
      return "Neutral"
    else
      return "Unknown"
    end
  else
    return "Unknown"
  end

end

--- Get the enemy coalition for a given coalition.
-- @param #number Coalition The coalition ID.
-- @param #boolean Neutral Include neutral as enemy.
-- @return #table Enemy coalition table.
function UTILS.GetCoalitionEnemy(Coalition, Neutral)

  local Coalitions={}
  if Coalition then
    if Coalition==coalition.side.RED then
      Coalitions={coalition.side.BLUE}
    elseif Coalition==coalition.side.BLUE then
      Coalitions={coalition.side.RED}
    elseif Coalition==coalition.side.NEUTRAL then
      Coalitions={coalition.side.RED, coalition.side.BLUE}
    end
  end

  if Neutral then
    table.insert(Coalitions, coalition.side.NEUTRAL)
  end

  return Coalitions
end

--- Get the modulation name from its numerical value.
-- @param #number Modulation The modulation enumerator number. Can be either 0 or 1.
-- @return #string The modulation name, i.e. "AM"=0 or "FM"=1. Anything else will return "Unknown".
function UTILS.GetModulationName(Modulation)

  if Modulation then
    if Modulation==0  then
      return "AM"
    elseif Modulation==1  then
      return "FM"
    else
      return "Unknown"
    end
  else
    return "Unknown"
  end

end

--- Get the NATO reporting name of a unit type name
-- @param #number Typename The type name.
-- @return #string The Reporting name or "Bogey".
function UTILS.GetReportingName(Typename)

  local typename = string.lower(Typename)
  
  -- special cases - Shark and Manstay have "A-50" in the name
  if string.find(typename,"ka-50",1,true) then
    return "Shark"
  elseif string.find(typename,"a-50",1,true) then
    return "Mainstay"
  end

  for name, value in pairs(ENUMS.ReportingName.NATO) do
    local svalue = string.lower(value)
    if string.find(typename,svalue,1,true) then
      return name
    end
  end

  return "Bogey"
end

--- Get the callsign name from its enumerator value
-- @param #number Callsign The enumerator callsign.
-- @return #string The callsign name or "Ghostrider".
function UTILS.GetCallsignName(Callsign)

  for name, value in pairs(CALLSIGN.Aircraft) do
    if value==Callsign then
      return name
    end
  end

  for name, value in pairs(CALLSIGN.AWACS) do
    if value==Callsign then
      return name
    end
  end

  for name, value in pairs(CALLSIGN.JTAC) do
    if value==Callsign then
      return name
    end
  end

  for name, value in pairs(CALLSIGN.Tanker) do
    if value==Callsign then
      return name
    end
  end

  for name, value in pairs(CALLSIGN.B1B) do
    if value==Callsign then
      return name
    end
  end

  for name, value in pairs(CALLSIGN.B52) do
    if value==Callsign then
      return name
    end
  end

  for name, value in pairs(CALLSIGN.F15E) do
    if value==Callsign then
      return name
    end
  end

  for name, value in pairs(CALLSIGN.F16) do
    if value==Callsign then
      return name
    end
  end

  for name, value in pairs(CALLSIGN.F18) do
    if value==Callsign then
      return name
    end
  end

  for name, value in pairs(CALLSIGN.FARP) do
    if value==Callsign then
      return name
    end
  end

  for name, value in pairs(CALLSIGN.TransportAircraft) do
    if value==Callsign then
      return name
    end
  end
  
  for name, value in pairs(CALLSIGN.AH64) do
    if value==Callsign then
      return name
    end
  end
  
  for name, value in pairs(CALLSIGN.Kiowa) do
    if value==Callsign then
      return name
    end
  end
  
  return "Ghostrider"
end

--- Get the time difference between GMT and local time.
-- @return #number Local time difference in hours compared to GMT. E.g. Dubai is GMT+4 ==> +4 is returned.
function UTILS.GMTToLocalTimeDifference()

  local theatre=UTILS.GetDCSMap()

  if theatre==DCSMAP.Caucasus then
    return 4   -- Caucasus UTC+4 hours
  elseif theatre==DCSMAP.PersianGulf then
    return 4   -- Abu Dhabi UTC+4 hours
  elseif theatre==DCSMAP.NTTR then
    return -8  -- Las Vegas UTC-8 hours
  elseif theatre==DCSMAP.Normandy then
    return 0   -- Calais UTC+1 hour
  elseif theatre==DCSMAP.TheChannel then
    return 2   -- This map currently needs +2
  elseif theatre==DCSMAP.Syria then
    return 3   -- Damascus is UTC+3 hours
  elseif theatre==DCSMAP.MarianaIslands then
    return 10  -- Guam is UTC+10 hours.
  elseif theatre==DCSMAP.Falklands then
    return -3  -- Fireland is UTC-3 hours.
  elseif theatre==DCSMAP.Sinai then
    return 2   -- Currently map is +2 but should be +3 (DCS bug?)
  elseif theatre==DCSMAP.Kola then
    return 3   -- Currently map is +2 but should be +3 (DCS bug?)
  elseif theatre==DCSMAP.Afghanistan then
    return 4.5   -- UTC +4:30
  elseif theatre==DCSMAP.Iraq then
    return 3.0   -- UTC +3
  elseif theatre==DCSMAP.GermanyCW then
    return 1.0   -- UTC +1 Central European Time (not summer time)  
  else
    BASE:E(string.format("ERROR: Unknown Map %s in UTILS.GMTToLocal function. Returning 0", tostring(theatre)))
    return 0
  end

end


--- Get the day of the year. Counting starts on 1st of January.
-- @param #number Year The year.
-- @param #number Month The month.
-- @param #number Day The day.
-- @return #number The day of the year.
function UTILS.GetDayOfYear(Year, Month, Day)

  local floor = math.floor

   local n1 = floor(275 * Month / 9)
   local n2 = floor((Month + 9) / 12)
   local n3 = (1 + floor((Year - 4 * floor(Year / 4) + 2) / 3))

   return n1 - (n2 * n3) + Day - 30
end

--- Get sunrise or sun set of a specific day of the year at a specific location.
-- @param #number DayOfYear The day of the year.
-- @param #number Latitude Latitude.
-- @param #number Longitude Longitude.
-- @param #boolean Rising If true, calc sun rise, or sun set otherwise.
-- @param #number Tlocal Local time offset in hours. E.g. +4 for a location which has GMT+4.
-- @return #number Sun rise/set in seconds of the day.
function UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, Rising, Tlocal)

  -- Defaults
  local zenith=90.83
  local latitude=Latitude
  local longitude=Longitude
  local rising=Rising
  local n=DayOfYear
  Tlocal=Tlocal or 0


  -- Short cuts.
  local rad = math.rad
  local deg = math.deg
  local floor = math.floor
  local frac = function(n) return n - floor(n) end
  local cos = function(d) return math.cos(rad(d)) end
  local acos = function(d) return deg(math.acos(d)) end
  local sin = function(d) return math.sin(rad(d)) end
  local asin = function(d) return deg(math.asin(d)) end
  local tan = function(d) return math.tan(rad(d)) end
  local atan = function(d) return deg(math.atan(d)) end

  local function fit_into_range(val, min, max)
     local range = max - min
     local count
     if val < min then
        count = floor((min - val) / range) + 1
        return val + count * range
     elseif val >= max then
        count = floor((val - max) / range) + 1
        return val - count * range
     else
        return val
     end
  end

   -- Convert the longitude to hour value and calculate an approximate time
   local lng_hour = longitude / 15

   local t
   if rising then -- Rising time is desired
      t = n + ((6 - lng_hour) / 24)
   else -- Setting time is desired
      t = n + ((18 - lng_hour) / 24)
   end

   -- Calculate the Sun's mean anomaly
   local M = (0.9856 * t) - 3.289

   -- Calculate the Sun's true longitude
   local L = fit_into_range(M + (1.916 * sin(M)) + (0.020 * sin(2 * M)) + 282.634, 0, 360)

   -- Calculate the Sun's right ascension
   local RA = fit_into_range(atan(0.91764 * tan(L)), 0, 360)

   -- Right ascension value needs to be in the same quadrant as L
   local Lquadrant  = floor(L / 90) * 90
   local RAquadrant = floor(RA / 90) * 90
   RA = RA + Lquadrant - RAquadrant

   -- Right ascension value needs to be converted into hours
   RA = RA / 15

   -- Calculate the Sun's declination
   local sinDec = 0.39782 * sin(L)
   local cosDec = cos(asin(sinDec))

   -- Calculate the Sun's local hour angle
   local cosH = (cos(zenith) - (sinDec * sin(latitude))) / (cosDec * cos(latitude))

   if rising and cosH > 1 then
      return "N/R" -- The sun never rises on this location on the specified date
   elseif cosH < -1 then
      return "N/S" -- The sun never sets on this location on the specified date
   end

   -- Finish calculating H and convert into hours
   local H
   if rising then
      H = 360 - acos(cosH)
   else
      H = acos(cosH)
   end
   H = H / 15

   -- Calculate local mean time of rising/setting
   local T = H + RA - (0.06571 * t) - 6.622

   -- Adjust back to UTC
   local UT = fit_into_range(T - lng_hour +Tlocal, 0, 24)

   return floor(UT)*60*60+frac(UT)*60*60--+Tlocal*60*60
 end

--- Get sun rise of a specific day of the year at a specific location.
-- @param #number Day Day of the year.
-- @param #number Month Month of the year.
-- @param #number Year Year.
-- @param #number Latitude Latitude.
-- @param #number Longitude Longitude.
-- @param #boolean Rising If true, calc sun rise, or sun set otherwise.
-- @param #number Tlocal Local time offset in hours. E.g. +4 for a location which has GMT+4. Default 0.
-- @return #number Sun rise in seconds of the day.
function UTILS.GetSunrise(Day, Month, Year, Latitude, Longitude, Tlocal)

  local DayOfYear=UTILS.GetDayOfYear(Year, Month, Day)

  return UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tlocal)
end

--- Get sun set of a specific day of the year at a specific location.
-- @param #number Day Day of the year.
-- @param #number Month Month of the year.
-- @param #number Year Year.
-- @param #number Latitude Latitude.
-- @param #number Longitude Longitude.
-- @param #boolean Rising If true, calc sun rise, or sun set otherwise.
-- @param #number Tlocal Local time offset in hours. E.g. +4 for a location which has GMT+4. Default 0.
-- @return #number Sun rise in seconds of the day.
function UTILS.GetSunset(Day, Month, Year, Latitude, Longitude, Tlocal)

  local DayOfYear=UTILS.GetDayOfYear(Year, Month, Day)

  return UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, false, Tlocal)
end

--- Get OS time. Needs os to be desanitized!
-- @return #number Os time in seconds.
function UTILS.GetOSTime()
  if os then
    local ts = 0
    local t = os.date("*t")
    local s = t.sec
    local m = t.min * 60
    local h = t.hour * 3600
    ts = s+m+h
    return ts
  else
    return nil
  end
end

--- Shuffle a table accoring to Fisher Yeates algorithm
--@param #table t Table to be shuffled.
--@return #table Shuffled table.
function UTILS.ShuffleTable(t)
  if t == nil or type(t) ~= "table" then
    BASE:I("Error in ShuffleTable: Missing or wrong type of Argument")
    return
  end
  math.random()
  math.random()
  math.random()
  local TempTable = {}
  for i = 1, #t do
    local r = math.random(1,#t)
    TempTable[i] = t[r]
    table.remove(t,r)
  end
  return TempTable
end

--- Get a random element of a table.
--@param #table t Table.
--@param #boolean replace If `true`, the drawn element is replaced, i.e. not deleted.
--@return #number Table element.
function UTILS.GetRandomTableElement(t, replace)

  if t == nil or type(t) ~= "table" then
    BASE:I("Error in ShuffleTable: Missing or wrong type of Argument")
    return
  end

  math.random()
  math.random()
  math.random()

  local r=math.random(#t)

  local element=t[r]

  if not replace then
    table.remove(t, r)
  end

  return element
end

--- (Helicopter) Check if one loading door is open.
--@param #string unit_name Unit name to be checked
--@return #boolean Outcome - true if a (loading door) is open, false if not, nil if none exists.
function UTILS.IsLoadingDoorOpen( unit_name )

  local unit = Unit.getByName(unit_name)

  if unit ~= nil then
      local type_name = unit:getTypeName()
      BASE:T("TypeName = ".. type_name)

      if type_name == "Mi-8MT" and (unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1 or unit:getDrawArgumentValue(250) < 0) then
          BASE:T(unit_name .. " Cargo doors are open or cargo door not present")
          return true
      end

      if type_name == "Mi-24P" and (unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1) then
          BASE:T(unit_name .. " a side door is open")
          return true
      end

      if type_name == "UH-1H" and (unit:getDrawArgumentValue(43) == 1 or unit:getDrawArgumentValue(44) == 1) then
          BASE:T(unit_name .. " a side door is open ")
          return true
      end

      if string.find(type_name, "SA342" ) and (unit:getDrawArgumentValue(34) == 1) then
          BASE:T(unit_name .. " front door(s) are open or doors removed")
          return true
      end

      if string.find(type_name, "Hercules") and (unit:getDrawArgumentValue(1215) == 1 and unit:getDrawArgumentValue(1216) == 1) then
          BASE:T(unit_name .. " rear doors are open")
          return true
      end

      if string.find(type_name, "Hercules") and (unit:getDrawArgumentValue(1220) == 1 or unit:getDrawArgumentValue(1221) == 1) then
          BASE:T(unit_name .. " para doors are open")
          return true
      end

      if string.find(type_name, "Hercules") and (unit:getDrawArgumentValue(1217) == 1) then
          BASE:T(unit_name .. " side door is open")
          return true
      end

      if type_name == "Bell-47" then -- bell aint got no doors so always ready to load injured soldiers
          BASE:T(unit_name .. " door is open")
          return true
      end

      if type_name == "UH-60L" and (unit:getDrawArgumentValue(401) == 1 or unit:getDrawArgumentValue(402) == 1) then
          BASE:T(unit_name .. " cargo door is open")
          return true
      end

      if type_name ==  "UH-60L" and (unit:getDrawArgumentValue(38) > 0 or unit:getDrawArgumentValue(400) == 1 ) then
          BASE:T(unit_name .. " front door(s) are open")
          return true
      end

      if type_name == "UH-60L_DAP" and (unit:getDrawArgumentValue(401) == 1 or unit:getDrawArgumentValue(402) == 1) then
          BASE:T(unit_name .. " cargo door is open")
          return true
      end

      if type_name ==  "UH-60L_DAP" and (unit:getDrawArgumentValue(38) > 0 or unit:getDrawArgumentValue(400) == 1 ) then
          BASE:T(unit_name .. " front door(s) are open")
          return true
      end

      if type_name == "AH-64D_BLK_II" then
         BASE:T(unit_name .. " front door(s) are open")
         return true -- no doors on this one ;)
      end

      if type_name == "Bronco-OV-10A" then
         BASE:T(unit_name .. " front door(s) are open")
         return true -- no doors on this one ;)
      end

      if type_name == "MH-60R" and (unit:getDrawArgumentValue(403) > 0 or unit:getDrawArgumentValue(403) == -1) then
        BASE:T(unit_name .. " cargo door is open")
        return true
      end

      if type_name == "OH58D" then
        BASE:T(unit_name .. " front door(s) are open")
        return true -- no doors on this one ;)
      end

      if type_name == "CH-47Fbl1" and (unit:getDrawArgumentValue(86) > 0.5) then
        BASE:T(unit_name .. " rear cargo door is open")
        return true
      end
      
      -- ground
      local UnitDescriptor = unit:getDesc()
      local IsGroundResult = (UnitDescriptor.category == Unit.Category.GROUND_UNIT)
      
      return IsGroundResult

  end -- nil

  return nil
end

--- Function to generate valid FM frequencies in mHz for radio beacons (FM).
-- @return #table Table of frequencies.
function UTILS.GenerateFMFrequencies()
    local FreeFMFrequencies = {}
    for _first = 3, 7 do
        for _second = 0, 5 do
            for _third = 0, 9 do
                local _frequency = ((100 * _first) + (10 * _second) + _third) * 100000 --extra 0 because we didnt bother with 4th digit
                table.insert(FreeFMFrequencies, _frequency)
            end
        end
    end
    return FreeFMFrequencies
end

--- Function to generate valid VHF frequencies in kHz for radio beacons (FM).
-- @return #table VHFrequencies
function UTILS.GenerateVHFrequencies()
  
  -- known and sorted map-wise NDBs in kHz
  local _skipFrequencies = {
  214,243,264,273,274,288,291.5,295,297.5,
  300.5,304,305,307,309.5,310,311,312,312.5,316,317,
  320,323,324,325,326,328,329,330,332,335,336,337,
  340,342,343,346,348,351,352,353,358,
  360,363,364,365,368,372.5,373,374,
  380,381,384,385,387,389,391,395,396,399,
  403,404,410,412,414,418,420,423,
  430,432,435,440,445,
  450,455,462,470,485,490,
  507,515,520,525,528,540,550,560,563,570,577,580,595,
  602,625,641,662,670,680,682,690,
  705,720,722,730,735,740,745,750,770,795,
  822,830,862,866,
  905,907,920,935,942,950,995,
  1000,1025,1030,1050,1065,1116,1175,1182,1210,1215
  }

  local FreeVHFFrequencies = {}

    -- first range
  local _start = 200000
  while _start < 400000 do

      -- skip existing NDB frequencies#
      local _found = false
      for _, value in pairs(_skipFrequencies) do
          if value * 1000 == _start then
              _found = true
              break
          end
      end
      if _found == false then
          table.insert(FreeVHFFrequencies, _start)
      end
       _start = _start + 10000
  end

   -- second range
  _start = 400000
  while _start < 850000 do
       -- skip existing NDB frequencies
      local _found = false
      for _, value in pairs(_skipFrequencies) do
          if value * 1000 == _start then
              _found = true
              break
          end
      end
      if _found == false then
          table.insert(FreeVHFFrequencies, _start)
      end
      _start = _start + 10000
  end

  -- third range
  _start = 850000
  while _start <= 999000 do -- adjusted for Gazelle
      -- skip existing NDB frequencies
      local _found = false
      for _, value in pairs(_skipFrequencies) do
          if value * 1000 == _start then
              _found = true
              break
          end
      end
      if _found == false then
          table.insert(FreeVHFFrequencies, _start)
      end
       _start = _start + 50000
  end

  return FreeVHFFrequencies
end

--- Function to generate valid UHF Frequencies in mHz (AM). Can be between 220 and 399 mHz. 243 is auto-excluded.
-- @param Start (Optional) Avoid frequencies between Start and End in mHz, e.g. 244
-- @param End (Optional) Avoid frequencies between Start and End in mHz, e.g. 320
-- @return #table UHF Frequencies
function UTILS.GenerateUHFrequencies(Start,End)

    local FreeUHFFrequencies = {}
    local _start = 220000000

    if not Start then
      while _start < 399000000 do
      if _start ~= 243000000 then
        table.insert(FreeUHFFrequencies, _start)
      end
          _start = _start + 500000
      end
    else
      local myend = End*1000000 or 399000000
      local mystart = Start*1000000 or 220000000

      while _start < 399000000 do
      if _start ~= 243000000 and (_start < mystart or _start > myend) then
        print(_start)
        table.insert(FreeUHFFrequencies, _start)
      end
          _start = _start + 500000
      end

    end


    return FreeUHFFrequencies
end

--- Function to generate valid laser codes for JTAC.
-- @return #table Laser Codes.
function UTILS.GenerateLaserCodes()
    local jtacGeneratedLaserCodes = {}

    -- helper function
    local function ContainsDigit(_number, _numberToFind)
      local _thisNumber = _number
      local _thisDigit = 0
      while _thisNumber ~= 0 do
          _thisDigit = _thisNumber % 10
          _thisNumber = math.floor(_thisNumber / 10)
          if _thisDigit == _numberToFind then
              return true
          end
      end
      return false
    end

    -- generate list of laser codes
    local _code = 1111
    local _count = 1
    while _code < 1777 and _count < 30 do
        while true do
           _code = _code + 1
            if not ContainsDigit(_code, 8)
                    and not ContainsDigit(_code, 9)
                    and not ContainsDigit(_code, 0) then
                table.insert(jtacGeneratedLaserCodes, _code)
                break
            end
        end
        _count = _count + 1
    end
    return jtacGeneratedLaserCodes
end

--- Ensure the passed object is a table.
-- @param #table Object The object that should be a table.
-- @param #boolean ReturnNil If `true`, return `#nil` if `Object` is nil. Otherwise an empty table `{}` is returned.
-- @return #table The object that now certainly *is* a table.
function UTILS.EnsureTable(Object, ReturnNil)

  if Object then
    if type(Object)~="table" then
      Object={Object}
    end
  else
    if ReturnNil then
      return nil
    else
      Object={}
    end

  end

  return Object
end

--- Function to save an object to a file
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file. Existing file will be overwritten.
-- @param #string Data The data structure to save. This will be e.g. a string of text lines with an \\n at the end of each line.
-- @return #boolean outcome True if saving is possible, else false.
function UTILS.SaveToFile(Path,Filename,Data)
  -- Thanks to @FunkyFranky
  -- Check io module is available.
  if not io then
    BASE:E("ERROR: io not desanitized. Can't save current file.")
    return false
  end

  -- Check default path.
  if Path==nil and not lfs then
    BASE:E("WARNING: lfs not desanitized. File will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.")
  end

  -- Set path or default.
  local path = nil
  if lfs then
    path=Path or lfs.writedir()
  end

  -- Set file name.
  local filename=Filename
  if path~=nil then
    filename=path.."\\"..filename
  end

  -- write
  local f = assert(io.open(filename, "wb"))
  f:write(Data)
  f:close()
  return true
end

--- Function to load an object from a file.
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @return #boolean outcome True if reading is possible and successful, else false.
-- @return #table data The data read from the filesystem (table of lines of text). Each line is one single #string!
function UTILS.LoadFromFile(Path,Filename)
  -- Thanks to @FunkyFranky
  -- Check io module is available.
  if not io then
    BASE:E("ERROR: io not desanitized. Can't save current state.")
    return false
  end

  -- Check default path.
  if Path==nil and not lfs then
    BASE:E("WARNING: lfs not desanitized. Loading will look into your DCS installation root directory rather than your \"Saved Games\\DCS\" folder.")
  end

  -- Set path or default.
  local path = nil
  if lfs then
    path=Path or lfs.writedir()
  end

  -- Set file name.
  local filename=Filename
  if path~=nil then
    filename=path.."\\"..filename
  end

  -- Check if file exists.
  local exists=UTILS.CheckFileExists(Path,Filename)
  if not exists then
    BASE:I(string.format("ERROR: File %s does not exist!",filename))
    return false
  end

  -- read
  local file=assert(io.open(filename, "rb"))
  local loadeddata = {}
  for line in file:lines() do
      loadeddata[#loadeddata+1] = line
  end
  file:close()
  return true, loadeddata
end

--- Function to check if a file exists.
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @return #boolean outcome True if reading is possible, else false.
function UTILS.CheckFileExists(Path,Filename)
  -- Thanks to @FunkyFranky
  -- Function that check if a file exists.
  local function _fileexists(name)
     local f=io.open(name,"r")
     if f~=nil then
      io.close(f)
      return true
    else
      return false
    end
  end

  -- Check io module is available.
  if not io then
    BASE:E("ERROR: io not desanitized.")
    return false
  end

  -- Check default path.
  if Path==nil and not lfs then
    BASE:E("WARNING: lfs not desanitized. Loading will look into your DCS installation root directory rather than your \"Saved Games\\DCS\" folder.")
  end

  -- Set path or default.
  local path = nil
  if lfs then
    path=Path or lfs.writedir()
  end

  -- Set file name.
  local filename=Filename
  if path~=nil then
    filename=path.."\\"..filename
  end

  -- Check if file exists.
  local exists=_fileexists(filename)
  if not exists then
    BASE:E(string.format("ERROR: File %s does not exist!",filename))
    return false
  else
    return true
  end
end

--- Function to obtain a table of typenames from the group given with the number of units of the same type in the group.
-- @param Wrapper.Group#GROUP Group The group to list
-- @return #table Table of typnames and typename counts, e.g. `{["KAMAZ Truck"]=3,["ATZ-5"]=1}`
function UTILS.GetCountPerTypeName(Group)
  local units = Group:GetUnits()
  local TypeNameTable = {}
  for _,_unt in pairs (units) do
    local unit = _unt -- Wrapper.Unit#UNIT
    local typen = unit:GetTypeName()
    if not TypeNameTable[typen] then
      TypeNameTable[typen] = 1
    else
      TypeNameTable[typen] = TypeNameTable[typen] + 1
    end
  end
  return TypeNameTable
end

--- Function to save the state of a list of groups found by name
-- @param #table List Table of strings with groupnames
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @param #boolean Structured Append the data with a list of typenames in the group plus their count.
-- @return #boolean outcome True if saving is successful, else false.
-- @usage
-- We will go through the list and find the corresponding group and save the current group size (0 when dead).
-- These groups are supposed to be put on the map in the ME and have *not* moved (e.g. stationary SAM sites).
-- Position is still saved for your usage.
-- The idea is to reduce the number of units when reloading the data again to restart the saved mission.
-- The data will be a simple comma separated list of groupname and size, with one header line.
function UTILS.SaveStationaryListOfGroups(List,Path,Filename,Structured)
  local filename = Filename or "StateListofGroups"
  local data = "--Save Stationary List of Groups: "..Filename .."\n"
  for _,_group in pairs (List) do
    local group = GROUP:FindByName(_group) -- Wrapper.Group#GROUP
    if group and group:IsAlive() then
      local units = group:CountAliveUnits()
      local position = group:GetVec3()
      if Structured then
        local structure = UTILS.GetCountPerTypeName(group)
        local strucdata =  ""
        for typen,anzahl in pairs (structure) do
          strucdata = strucdata .. typen .. "=="..anzahl..";"
        end
        data = string.format("%s%s,%d,%d,%d,%d,%s\n",data,_group,units,position.x,position.y,position.z,strucdata)
      else
        data = string.format("%s%s,%d,%d,%d,%d\n",data,_group,units,position.x,position.y,position.z)
      end
    else
      data = string.format("%s%s,0,0,0,0\n",data,_group)
    end
  end
  -- save the data
  local outcome = UTILS.SaveToFile(Path,Filename,data)
  return outcome
end

--- Function to save the state of a set of Wrapper.Group#GROUP objects.
-- @param Core.Set#SET_BASE Set of objects to save
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @param #boolean Structured Append the data with a list of typenames in the group plus their count.
-- @return #boolean outcome True if saving is successful, else false.
-- @usage
-- We will go through the set and find the corresponding group and save the current group size and current position.
-- The idea is to respawn the groups **spawned during an earlier run of the mission** at the given location and reduce
-- the number of units in the group when reloading the data again to restart the saved mission. Note that *dead* groups
-- cannot be covered with this.
-- **Note** Do NOT use dashes or hashes in group template names (-,#)!
-- The data will be a simple comma separated list of groupname and size, with one header line.
-- The current task/waypoint/etc cannot be restored.
function UTILS.SaveSetOfGroups(Set,Path,Filename,Structured)
  local filename = Filename or "SetOfGroups"
  local data = "--Save SET of groups: "..Filename .."\n"
  local List = Set:GetSetObjects()
  for _,_group in pairs (List) do
    local group = _group -- Wrapper.Group#GROUP
    if group and group:IsAlive() then
      local name = group:GetName()
      local template = string.gsub(name,"-(.+)$","")
      if string.find(name,"AID") then
        template = string.gsub(name,"(.AID.%d+$","")
      end
      if string.find(template,"#") then
       template = string.gsub(name,"#(%d+)$","")
      end
      local units = group:CountAliveUnits()
      local position = group:GetVec3()
      if Structured then
        local structure = UTILS.GetCountPerTypeName(group)
        local strucdata =  ""
        for typen,anzahl in pairs (structure) do
          strucdata = strucdata .. typen .. "=="..anzahl..";"
        end
        data = string.format("%s%s,%s,%d,%d,%d,%d,%s\n",data,name,template,units,position.x,position.y,position.z,strucdata)
      else
        data = string.format("%s%s,%s,%d,%d,%d,%d\n",data,name,template,units,position.x,position.y,position.z)
      end
    end
  end
  -- save the data
  local outcome = UTILS.SaveToFile(Path,Filename,data)
  return outcome
end

--- Function to save the state of a set of Wrapper.Static#STATIC objects.
-- @param Core.Set#SET_BASE Set of objects to save
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @return #boolean outcome True if saving is successful, else false.
-- @usage
-- We will go through the set and find the corresponding static and save the current name and postion when alive.
-- The data will be a simple comma separated list of name and state etc, with one header line.
function UTILS.SaveSetOfStatics(Set,Path,Filename)
  local filename = Filename or "SetOfStatics"
  local data = "--Save SET of statics: "..Filename .."\n"
  local List = Set:GetSetObjects()
  for _,_group in pairs (List) do
    local group = _group -- Wrapper.Static#STATIC
    if group and group:IsAlive() then
      local name = group:GetName()
      local position = group:GetVec3()
      data = string.format("%s%s,%d,%d,%d\n",data,name,position.x,position.y,position.z)
    end
  end
  -- save the data
  local outcome = UTILS.SaveToFile(Path,Filename,data)
  return outcome
end

--- Function to save the state of a list of statics found by name
-- @param #table List Table of strings with statics names
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @return #boolean outcome True if saving is successful, else false.
-- @usage
-- We will go through the list and find the corresponding static and save the current alive state as 1 (0 when dead).
-- Position is saved for your usage. **Note** this works on UNIT-name level.
-- The idea is to reduce the number of units when reloading the data again to restart the saved mission.
-- The data will be a simple comma separated list of name and state etc, with one header line.
function UTILS.SaveStationaryListOfStatics(List,Path,Filename)
  local filename = Filename or "StateListofStatics"
  local data = "--Save Stationary List of Statics: "..Filename .."\n"
  for _,_group in pairs (List) do
    local group = STATIC:FindByName(_group,false) -- Wrapper.Static#STATIC
    if group and group:IsAlive() then
      local position = group:GetVec3()
      data = string.format("%s%s,1,%d,%d,%d\n",data,_group,position.x,position.y,position.z)
    else
      data = string.format("%s%s,0,0,0,0\n",data,_group)
    end
  end
  -- save the data
  local outcome = UTILS.SaveToFile(Path,Filename,data)
  return outcome
end

--- Load back a stationary list of groups from file.
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @param #boolean Reduce If false, existing loaded groups will not be reduced to fit the saved number.
-- @param #boolean Structured (Optional, needs Reduce = true) If true, and the data has been saved as structure before, remove the correct unit types as per the saved list.
-- @param #boolean Cinematic (Optional, needs Structured = true) If true, place a fire/smoke effect on the dead static position.
-- @param #number Effect (Optional for Cinematic) What effect to use. Defaults to a random effect. Smoke presets are: 1=small smoke and fire, 2=medium smoke and fire, 3=large smoke and fire, 4=huge smoke and fire, 5=small smoke, 6=medium smoke, 7=large smoke, 8=huge smoke.
-- @param #number Density (Optional for Cinematic) What smoke density to use, can be 0 to 1. Defaults to 0.5.
-- @return #table Table of data objects (tables) containing groupname, coordinate and group object. Returns nil when file cannot be read.
-- @return #table When using Cinematic: table of names of smoke and fire objects, so they can be extinguished with `COORDINATE.StopBigSmokeAndFire( name )`
function UTILS.LoadStationaryListOfGroups(Path,Filename,Reduce,Structured,Cinematic,Effect,Density)

  local fires = {}

  local function Smokers(name,coord,effect,density)
    local eff = math.random(8)
    if type(effect) == "number" then eff = effect end
    coord:BigSmokeAndFire(eff,density,name)
    table.insert(fires,name)
  end

  local function Cruncher(group,typename,anzahl)
    local units = group:GetUnits()
    local reduced = 0
    for _,_unit in pairs (units) do
      local typo = _unit:GetTypeName()
      if typename == typo then
        if Cinematic then
          local coordinate = _unit:GetCoordinate()
          local name = _unit:GetName()
          Smokers(name,coordinate,Effect,Density)
        end
        _unit:Destroy(false)
        reduced = reduced + 1
        if reduced == anzahl then break end
      end
    end
  end

  local reduce = true
  if Reduce == false then reduce = false end
  local filename = Filename or "StateListofGroups"
  local datatable = {}
  if UTILS.CheckFileExists(Path,filename) then
    local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename)
    -- remove header
    table.remove(loadeddata, 1)
    for _id,_entry in pairs (loadeddata) do
      local dataset = UTILS.Split(_entry,",")
      -- groupname,units,position.x,position.y,position.z
      local groupname = dataset[1]
      local size = tonumber(dataset[2])
      local posx = tonumber(dataset[3])
      local posy = tonumber(dataset[4])
      local posz = tonumber(dataset[5])
      local structure = dataset[6]
      --BASE:I({structure})
      local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz})
      local data = { groupname=groupname, size=size, coordinate=coordinate, group=GROUP:FindByName(groupname) }
      if reduce then
        local actualgroup = GROUP:FindByName(groupname)
        if actualgroup and actualgroup:IsAlive() and actualgroup:CountAliveUnits() > size then
          if Structured and structure then
            --BASE:I("Reducing group structure!")
            local loadedstructure = {}
            local strcset = UTILS.Split(structure,";")
            for _,_data in pairs(strcset) do
              local datasplit = UTILS.Split(_data,"==")
              loadedstructure[datasplit[1]] = tonumber(datasplit[2])
            end
            --BASE:I({loadedstructure})
            local originalstructure = UTILS.GetCountPerTypeName(actualgroup)
            --BASE:I({originalstructure})
            for _name,_number in pairs(originalstructure) do
              local loadednumber = 0
              if loadedstructure[_name] then
                loadednumber = loadedstructure[_name]
              end
              local reduce = false
              if loadednumber < _number then reduce = true end

              --BASE:I(string.format("Looking at: %s | Original number: %d | Loaded number: %d | Reduce: %s",_name,_number,loadednumber,tostring(reduce)))

              if reduce then
                Cruncher(actualgroup,_name,_number-loadednumber)
              end

            end
          else
            local reduction = actualgroup:CountAliveUnits() - size
            --BASE:I("Reducing groupsize by ".. reduction .. " units!")
            -- reduce existing group
            local units = actualgroup:GetUnits()
            local units2 = UTILS.ShuffleTable(units) -- randomize table
            for i=1,reduction do
              units2[i]:Destroy(false)
            end
          end
        end
      end
      table.insert(datatable,data)
    end
  else
    return nil
  end
  return datatable,fires
end

--- Load back a SET of groups from file.
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @param #boolean Spawn If set to false, do not re-spawn the groups loaded in location and reduce to size.
-- @param #boolean Structured (Optional, needs Spawn=true)If true, and the data has been saved as structure before, remove the correct unit types as per the saved list.
-- @param #boolean Cinematic (Optional, needs Structured=true) If true, place a fire/smoke effect on the dead static position.
-- @param #number Effect (Optional for Cinematic) What effect to use. Defaults to a random effect. Smoke presets are: 1=small smoke and fire, 2=medium smoke and fire, 3=large smoke and fire, 4=huge smoke and fire, 5=small smoke, 6=medium smoke, 7=large smoke, 8=huge smoke.
-- @param #number Density (Optional for Cinematic) What smoke density to use, can be 0 to 1. Defaults to 0.5.
-- @return Core.Set#SET_GROUP Set of GROUP objects.
-- Returns nil when file cannot be read. Returns a table of data entries if Spawn is false: `{ groupname=groupname, size=size, coordinate=coordinate, template=template }`
-- @return #table When using Cinematic: table of names of smoke and fire objects, so they can be extinguished with `COORDINATE.StopBigSmokeAndFire( name )`
function UTILS.LoadSetOfGroups(Path,Filename,Spawn,Structured,Cinematic,Effect,Density)

  local fires = {}
  local usedtemplates = {}
  local spawn = true
  if Spawn == false then spawn = false end
  local filename = Filename or "SetOfGroups"
  local setdata = SET_GROUP:New()
  local datatable = {}

  local function Smokers(name,coord,effect,density)
    local eff = math.random(8)
    if type(effect) == "number" then eff = effect end
    coord:BigSmokeAndFire(eff,density,name)
    table.insert(fires,name)
  end

  local function Cruncher(group,typename,anzahl)
    local units = group:GetUnits()
    local reduced = 0
    for _,_unit in pairs (units) do
      local typo = _unit:GetTypeName()
      if typename == typo then
        if Cinematic then
          local coordinate = _unit:GetCoordinate()
          local name = _unit:GetName()
          Smokers(name,coordinate,Effect,Density)
        end
        _unit:Destroy(false)
        reduced = reduced + 1
        if reduced == anzahl then break end
      end
    end
  end

  local function PostSpawn(args)
    local spwndgrp = args[1]
    local size = args[2]
    local structure = args[3]

    setdata:AddObject(spwndgrp)
    local actualsize = spwndgrp:CountAliveUnits()
    if actualsize > size then
      if Structured and structure then

        local loadedstructure = {}
        local strcset = UTILS.Split(structure,";")
        for _,_data in pairs(strcset) do
          local datasplit = UTILS.Split(_data,"==")
          loadedstructure[datasplit[1]] = tonumber(datasplit[2])
        end

        local originalstructure = UTILS.GetCountPerTypeName(spwndgrp)

        for _name,_number in pairs(originalstructure) do
          local loadednumber = 0
          if loadedstructure[_name] then
            loadednumber = loadedstructure[_name]
          end
          local reduce = false
          if loadednumber < _number then reduce = true end

          if reduce then
            Cruncher(spwndgrp,_name,_number-loadednumber)
          end

        end
      else
        local reduction = actualsize-size
        -- reduce existing group
        local units = spwndgrp:GetUnits()
        local units2 = UTILS.ShuffleTable(units) -- randomize table
        for i=1,reduction do
          units2[i]:Destroy(false)
        end
      end
    end
  end

  local function MultiUse(Data)
    local template = Data.template
    if template and usedtemplates[template] and usedtemplates[template].used and usedtemplates[template].used > 1 then
      -- multispawn
      if not usedtemplates[template].done then
        local spwnd = 0
        local spawngrp = SPAWN:New(template)
        spawngrp:InitLimit(0,usedtemplates[template].used)
        for _,_entry in pairs(usedtemplates[template].data) do
          spwnd = spwnd + 1
          local sgrp=spawngrp:SpawnFromCoordinate(_entry.coordinate,spwnd)
          BASE:ScheduleOnce(0.5,PostSpawn,{sgrp,_entry.size,_entry.structure})
        end
        usedtemplates[template].done = true
      end
      return true
    else
      return false
    end
  end

  --BASE:I("Spawn = "..tostring(spawn))
  if UTILS.CheckFileExists(Path,filename) then
    local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename)
    -- remove header
    table.remove(loadeddata, 1)
    for _id,_entry in pairs (loadeddata) do
      local dataset = UTILS.Split(_entry,",")
      -- groupname,template,units,position.x,position.y,position.z
      local groupname = dataset[1]
      local template = dataset[2]
      local size = tonumber(dataset[3])
      local posx = tonumber(dataset[4])
      local posy = tonumber(dataset[5])
      local posz = tonumber(dataset[6])
      local structure = dataset[7]
      local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz})
      local group=nil
      if size > 0 then
        local data = { groupname=groupname, size=size, coordinate=coordinate, template=template, structure=structure }
        table.insert(datatable,data)
        if usedtemplates[template] then
          usedtemplates[template].used = usedtemplates[template].used + 1
          table.insert(usedtemplates[template].data,data)
        else
          usedtemplates[template] = {
              data = {},
              used = 1,
              done = false,
            }
          table.insert(usedtemplates[template].data,data)
        end
      end
    end
    for _id,_entry in pairs (datatable) do
      if spawn and not MultiUse(_entry) and _entry.size > 0 then
        local group = SPAWN:New(_entry.template)
        local sgrp=group:SpawnFromCoordinate(_entry.coordinate)
        BASE:ScheduleOnce(0.5,PostSpawn,{sgrp,_entry.size,_entry.structure})
      end
    end
  else
    return nil
  end
  if spawn then
    return setdata,fires
  else
   return datatable
  end
end

--- Load back a SET of statics from file.
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @return Core.Set#SET_STATIC Set SET_STATIC containing the static objects.
function UTILS.LoadSetOfStatics(Path,Filename)
  local filename = Filename or "SetOfStatics"
  local datatable = SET_STATIC:New()
  if UTILS.CheckFileExists(Path,filename) then
    local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename)
    -- remove header
    table.remove(loadeddata, 1)
    for _id,_entry in pairs (loadeddata) do
      local dataset = UTILS.Split(_entry,",")
      local staticname = dataset[1]
      local StaticObject = STATIC:FindByName(staticname,false)
      if StaticObject then
        datatable:AddObject(StaticObject)
      end
    end
  else
    return nil
  end
  return datatable
end

--- Load back a stationary list of statics from file.
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @param #boolean Reduce If false, do not destroy the units with size=0.
-- @param #boolean Dead (Optional, needs Reduce = true) If Dead is true, re-spawn the dead object as dead and do not just delete it.
-- @param #boolean Cinematic (Optional, needs Dead = true) If true, place a fire/smoke effect on the dead static position.
-- @param #number Effect (Optional for Cinematic) What effect to use. Defaults to a random effect. Smoke presets are: 1=small smoke and fire, 2=medium smoke and fire, 3=large smoke and fire, 4=huge smoke and fire, 5=small smoke, 6=medium smoke, 7=large smoke, 8=huge smoke.
-- @param #number Density (Optional for Cinematic) What smoke density to use, can be 0 to 1. Defaults to 0.5.
-- @return #table Table of data objects (tables) containing staticname, size (0=dead else 1), coordinate and the static object. Dead objects will have coordinate points `{x=0,y=0,z=0}`
-- @return #table When using Cinematic: table of names of smoke and fire objects, so they can be extinguished with `COORDINATE.StopBigSmokeAndFire( name )`
-- Returns nil when file cannot be read.
function UTILS.LoadStationaryListOfStatics(Path,Filename,Reduce,Dead,Cinematic,Effect,Density)
  local fires = {}
  local reduce = true
  if Reduce == false then reduce = false end
  local filename = Filename or "StateListofStatics"
  local datatable = {}
  if UTILS.CheckFileExists(Path,filename) then
    local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename)
    -- remove header
    table.remove(loadeddata, 1)
    for _id,_entry in pairs (loadeddata) do
      local dataset = UTILS.Split(_entry,",")
      -- staticname,units(1/0),position.x,position.y,position.z)
      local staticname = dataset[1]
      local size = tonumber(dataset[2])
      local posx = tonumber(dataset[3])
      local posy = tonumber(dataset[4])
      local posz = tonumber(dataset[5])
      local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz})
      local data = { staticname=staticname, size=size, coordinate=coordinate, static=STATIC:FindByName(staticname,false) }
      table.insert(datatable,data)
      if size==0 and reduce then
        local static = STATIC:FindByName(staticname,false)
        if static then
          if Dead then
            local deadobject = SPAWNSTATIC:NewFromStatic(staticname,static:GetCountry())
            deadobject:InitDead(true)
            local heading = static:GetHeading()
            local coord = static:GetCoordinate()
            static:Destroy(false)
            deadobject:SpawnFromCoordinate(coord,heading,staticname)
            if Cinematic then
              local effect = math.random(8)
              if type(Effect) == "number" then
                effect = Effect
              end
              coord:BigSmokeAndFire(effect,Density,staticname)
              table.insert(fires,staticname)
            end
          else
            static:Destroy(false)
          end
        end
      end
    end
  else
    return nil
  end
  return datatable,fires
end

--- Heading Degrees (0-360) to Cardinal
-- @param #number Heading The heading
-- @return #string Cardinal, e.g. "NORTH"
function UTILS.BearingToCardinal(Heading)
  if     Heading >= 0   and Heading <= 22  then return "North"
    elseif Heading >= 23  and Heading <= 66  then return "North-East"
    elseif Heading >= 67  and Heading <= 101 then return "East"
    elseif Heading >= 102 and Heading <= 146 then return "South-East"
    elseif Heading >= 147 and Heading <= 201 then return "South"
    elseif Heading >= 202 and Heading <= 246 then return "South-West"
    elseif Heading >= 247 and Heading <= 291 then return "West"
    elseif Heading >= 292 and Heading <= 338 then return "North-West"
    elseif Heading >= 339 then return "North"
  end
end

--- Create a BRAA NATO call string BRAA between two GROUP objects
-- @param Wrapper.Group#GROUP FromGrp GROUP object
-- @param Wrapper.Group#GROUP ToGrp GROUP object
-- @return #string Formatted BRAA NATO call
function UTILS.ToStringBRAANATO(FromGrp,ToGrp)
  local BRAANATO = "Merged."
  local GroupNumber = ToGrp:GetSize()
  local GroupWords = "Singleton"
  if GroupNumber == 2 then GroupWords = "Two-Ship"
    elseif GroupNumber >= 3 then GroupWords = "Heavy"
  end
  local grpLeadUnit = ToGrp:GetUnit(1)
  local tgtCoord = grpLeadUnit:GetCoordinate()
  local currentCoord = FromGrp:GetCoordinate()
  local hdg = UTILS.Round(ToGrp:GetHeading()/100,1)*100
  local bearing = UTILS.Round(currentCoord:HeadingTo(tgtCoord),0)
  local rangeMetres = tgtCoord:Get2DDistance(currentCoord)
  local rangeNM = UTILS.Round( UTILS.MetersToNM(rangeMetres), 0)
  local aspect = tgtCoord:ToStringAspect(currentCoord)
  local alt = UTILS.Round(UTILS.MetersToFeet(grpLeadUnit:GetAltitude())/1000,0)--*1000
  local track = UTILS.BearingToCardinal(hdg)
  if rangeNM > 3 then
      if aspect == "" then
        BRAANATO = string.format("%s, BRA, %03d, %d miles, Angels %d, Track %s",GroupWords,bearing, rangeNM, alt, track)
      else
        BRAANATO = string.format("%s, BRAA, %03d, %d miles, Angels %d, %s, Track %s",GroupWords, bearing, rangeNM, alt, aspect, track)
      end
  end
  return BRAANATO
end

--- Check if an object is contained in a table.
-- @param #table Table The table.
-- @param #table Object The object to check.
-- @param #string Key (Optional) Key to check. By default, the object itself is checked.
-- @return #boolean Returns `true` if object is in table.
function UTILS.IsInTable(Table, Object, Key)

  for key, object in pairs(Table) do
    if Key then
      if Object[Key]==object[Key] then
        return true
      end
    else
      if object==Object then
        return true
      end
    end
  end

  return false
end

--- Check if any object of multiple given objects is contained in a table.
-- @param #table Table The table.
-- @param #table Objects The objects to check.
-- @param #string Key (Optional) Key to check.
-- @return #boolean Returns `true` if object is in table.
function UTILS.IsAnyInTable(Table, Objects, Key)

  for _,Object in pairs(UTILS.EnsureTable(Objects)) do

    for key, object in pairs(Table) do
      if Key then
        if Object[Key]==object[Key] then
          return true
        end
      else
        if object==Object then
          return true
        end
      end
    end

  end

  return false
end

--- Helper function to plot a racetrack on the F10 Map - curtesy of Buur.
-- @param Core.Point#COORDINATE Coordinate
-- @param #number Altitude Altitude in feet
-- @param #number Speed Speed in knots
-- @param #number Heading Heading in degrees
-- @param #number Leg Leg in NM
-- @param #number Coalition Coalition side, e.g. coaltion.side.RED or coaltion.side.BLUE
-- @param #table Color Color of the line in RGB, e.g. {1,0,0} for red
-- @param #number Alpha Transparency factor, between 0.1 and 1
-- @param #number LineType Line type to be used, line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid.
-- @param #boolean ReadOnly
function UTILS.PlotRacetrack(Coordinate, Altitude, Speed, Heading, Leg, Coalition, Color, Alpha, LineType, ReadOnly)
    local fix_coordinate = Coordinate
    local altitude = Altitude
    local speed = Speed or 350
    local heading = Heading or 270
    local leg_distance = Leg or 10

    local coalition = Coalition or -1
    local color = Color or {1,0,0}
    local alpha = Alpha or 1
    local lineType = LineType or 1


    speed = UTILS.IasToTas(speed, UTILS.FeetToMeters(altitude), oatcorr)

    local turn_radius = 0.0211 * speed -3.01

    local point_two = fix_coordinate:Translate(UTILS.NMToMeters(leg_distance), heading, true, false)
    local point_three = point_two:Translate(UTILS.NMToMeters(turn_radius)*2, heading - 90, true, false)
    local point_four = fix_coordinate:Translate(UTILS.NMToMeters(turn_radius)*2, heading - 90, true, false)
    local circle_center_fix_four = point_two:Translate(UTILS.NMToMeters(turn_radius), heading - 90, true, false)
    local circle_center_two_three = fix_coordinate:Translate(UTILS.NMToMeters(turn_radius), heading - 90, true, false)


    fix_coordinate:LineToAll(point_two, coalition, color, alpha, lineType)
    point_four:LineToAll(point_three, coalition, color, alpha, lineType)
    circle_center_fix_four:CircleToAll(UTILS.NMToMeters(turn_radius), coalition, color, alpha, nil, 0, lineType)--, ReadOnly, Text)
    circle_center_two_three:CircleToAll(UTILS.NMToMeters(turn_radius), coalition, color, alpha, nil, 0, lineType)--, ReadOnly, Text)

end

--- Get the current time in a "nice" format like 21:01:15
-- @return #string Returns string with the current time
function UTILS.TimeNow()
    return UTILS.SecondsToClock(timer.getAbsTime(), false, false)
end


--- Given 2 "nice" time string, returns the difference between the two in seconds
-- @param #string start_time Time string like "07:15:22"
-- @param #string end_time Time string like "08:11:27"
-- @return #number Seconds between start_time and end_time
function UTILS.TimeDifferenceInSeconds(start_time, end_time)
    return UTILS.ClockToSeconds(end_time) - UTILS.ClockToSeconds(start_time)
end

--- Check if the current time is later than time_string.
-- @param #string start_time Time string like "07:15:22"
-- @return #boolean True if later, False if before
function UTILS.TimeLaterThan(time_string)
    if timer.getAbsTime() > UTILS.ClockToSeconds(time_string) then
        return true
    end
    return false
end

--- Check if the current time is before time_string.
-- @param #string start_time Time string like "07:15:22"
-- @return #boolean False if later, True if before
function UTILS.TimeBefore(time_string)
    if timer.getAbsTime() < UTILS.ClockToSeconds(time_string) then
        return true
    end
    return false
end


--- Combines two time strings to give you a new time. For example "15:16:32" and "02:06:24" would return "17:22:56"
-- @param #string time_string_01 Time string like "07:15:22"
-- @param #string time_string_02 Time string like "08:11:27"
-- @return #string Result of the two time string combined
function UTILS.CombineTimeStrings(time_string_01, time_string_02)
    local hours1, minutes1, seconds1 = time_string_01:match("(%d+):(%d+):(%d+)")
    local hours2, minutes2, seconds2 = time_string_02:match("(%d+):(%d+):(%d+)")
    local total_seconds = tonumber(seconds1) + tonumber(seconds2) + tonumber(minutes1) * 60 + tonumber(minutes2) * 60 + tonumber(hours1) * 3600 + tonumber(hours2) * 3600

    total_seconds = total_seconds % (24 * 3600)
    if total_seconds < 0 then
        total_seconds = total_seconds + 24 * 3600
    end

    local hours = math.floor(total_seconds / 3600)
    total_seconds = total_seconds - hours * 3600
    local minutes = math.floor(total_seconds / 60)
    local seconds = total_seconds % 60

    return string.format("%02d:%02d:%02d", hours, minutes, seconds)
end


--- Subtracts two time string to give you a new time. For example "15:16:32" and "02:06:24" would return "13:10:08"
-- @param #string time_string_01 Time string like "07:15:22"
-- @param #string time_string_02 Time string like "08:11:27"
-- @return #string Result of the two time string subtracted
function UTILS.SubtractTimeStrings(time_string_01, time_string_02)
    local hours1, minutes1, seconds1 = time_string_01:match("(%d+):(%d+):(%d+)")
    local hours2, minutes2, seconds2 = time_string_02:match("(%d+):(%d+):(%d+)")
    local total_seconds = tonumber(seconds1) - tonumber(seconds2) + tonumber(minutes1) * 60 - tonumber(minutes2) * 60 + tonumber(hours1) * 3600 - tonumber(hours2) * 3600

    total_seconds = total_seconds % (24 * 3600)
    if total_seconds < 0 then
        total_seconds = total_seconds + 24 * 3600
    end

    local hours = math.floor(total_seconds / 3600)
    total_seconds = total_seconds - hours * 3600
    local minutes = math.floor(total_seconds / 60)
    local seconds = total_seconds % 60

    return string.format("%02d:%02d:%02d", hours, minutes, seconds)
end

--- Checks if the current time is in between start_time and end_time
-- @param #string time_string_01 Time string like "07:15:22"
-- @param #string time_string_02 Time string like "08:11:27"
-- @return #boolean True if it is, False if it's not
function UTILS.TimeBetween(start_time, end_time)
    return UTILS.TimeLaterThan(start_time) and UTILS.TimeBefore(end_time)
end

--- Easy to read one line to roll the dice on something. 1% is very unlikely to happen, 99% is very likely to happen
-- @param #number chance (optional) Percentage chance you want something to happen. Defaults to a random number if not given
-- @return #boolean True if the dice roll was within the given percentage chance of happening
function UTILS.PercentageChance(chance)
    chance = chance or math.random(0, 100)
    chance = UTILS.Clamp(chance, 0, 100)
    local percentage = math.random(0, 100)
    if percentage < chance then
        return true
    end
    return false
end

--- Easy to read one liner to clamp a value
-- @param #number value Input value
-- @param #number min Minimal value that should be respected
-- @param #number max Maximal value that should be respected
-- @return #number Clamped value
function UTILS.Clamp(value, min, max)
    if value < min then value = min end
    if value > max then value = max end

    return value
end

--- Clamp an angle so that it's always between 0 and 360 while still being correct
-- @param #number value Input value
-- @return #number Clamped value
function UTILS.ClampAngle(value)
    if value > 360 then return value - 360 end
    if value < 0 then return value + 360 end
    return value
end

--- Remap an input to a new value in a given range. For example:
--- UTILS.RemapValue(20, 10, 30, 0, 200) would return 100
--- 20 is 50% between 10 and 30
--- 50% between 0 and 200 is 100
-- @param #number value Input value
-- @param #number old_min Min value to remap from
-- @param #number old_max Max value to remap from
-- @param #number new_min Min value to remap to
-- @param #number new_max Max value to remap to
-- @return #number Remapped value
function UTILS.RemapValue(value, old_min, old_max, new_min, new_max)
    new_min = new_min or 0
    new_max = new_max or 100

    local old_range = old_max - old_min
    local new_range = new_max - new_min
    local percentage = (value - old_min) / old_range
    return (new_range * percentage) + new_min
end

--- Given a triangle made out of 3 vector 2s, return a vec2 that is a random number in this triangle
-- @param DCS#Vec2 pt1 Min value to remap from
-- @param DCS#Vec2 pt2 Max value to remap from
-- @param DCS#Vec2 pt3 Max value to remap from
-- @return DCS#Vec2 Random point in triangle
function UTILS.RandomPointInTriangle(pt1, pt2, pt3)
    local pt = {math.random(), math.random()}
    table.sort(pt)
    local s = pt[1]
    local t = pt[2] - pt[1]
    local u = 1 - pt[2]

    return {x = s * pt1.x + t * pt2.x + u * pt3.x,
            y = s * pt1.y + t * pt2.y + u * pt3.y}
end

--- Checks if a given angle (heading) is between 2 other angles. Min and max have to be given in clockwise order For example:
--- UTILS.AngleBetween(350, 270, 15) would return True
--- UTILS.AngleBetween(22, 95, 20) would return False
-- @param #number angle Min value to remap from
-- @param #number min Max value to remap from
-- @param #number max Max value to remap from
-- @return #boolean 
function UTILS.AngleBetween(angle, min, max)
    angle = (360 + (angle % 360)) % 360
    min = (360 + min % 360) % 360
    max = (360 + max % 360) % 360

    if min < max then return min <= angle and angle <= max end
    return min <= angle or angle <= max
end

--- Easy to read one liner to write a JSON file. Everything in @data should be serializable
--- json.lua exists in the DCS install Scripts folder
-- @param #table data table to write
-- @param #string file_path File path
function UTILS.WriteJSON(data, file_path)
    package.path  = package.path ..  ";.\\Scripts\\?.lua"
    local JSON = require("json")
    local pretty_json_text = JSON:encode_pretty(data)
    local write_file = io.open(file_path, "w")
    write_file:write(pretty_json_text)
    write_file:close()
end

--- Easy to read one liner to read a JSON file.
--- json.lua exists in the DCS install Scripts folder
-- @param #string file_path File path
-- @return #table
function UTILS.ReadJSON(file_path)
    package.path  = package.path ..  ";.\\Scripts\\?.lua"
    local JSON = require("json")
    local read_file = io.open(file_path, "r")
    local contents = read_file:read( "*a" )
    io.close(read_file)
    return JSON:decode(contents)
end

--- Get the properties names and values of properties set up on a Zone in the Mission Editor.
--- This doesn't work for any zones created in MOOSE
-- @param #string zone_name Name of the zone as set up in the Mission Editor
-- @return #table with all the properties on a zone
function UTILS.GetZoneProperties(zone_name)
    local return_table = {}
    for _, zone in pairs(env.mission.triggers.zones) do
        if zone["name"] == zone_name then
            if table.length(zone["properties"]) > 0 then
                for _, property in pairs(zone["properties"]) do
                    return_table[property["key"]] = property["value"]
                end
              return return_table
            else
                BASE:I(string.format("%s doesn't have any properties", zone_name))
                return {}
            end
        end
    end
end

--- Rotates a point around another point with a given angle. Useful if you're loading in groups or
--- statics but you want to rotate them all as a collection. You can get the center point of everything
--- and then rotate all the positions of every object around this center point.
-- @param DCS#Vec2 point Point that you want to rotate
-- @param DCS#Vec2 pivot Pivot point of the rotation
-- @param #number angle How many degrees the point should be rotated
-- @return DCS#Vec2 Rotated point
function UTILS.RotatePointAroundPivot(point, pivot, angle)
    local radians = math.rad(angle)

    local x = point.x - pivot.x
    local y = point.y - pivot.y

    local rotated_x = x * math.cos(radians) - y * math.sin(radians)
    local rotatex_y = x * math.sin(radians) + y * math.cos(radians)

    local original_x = rotated_x + pivot.x
    local original_y = rotatex_y + pivot.y

    return { x = original_x, y = original_y }
end

--- Makes a string semi-unique by attaching a random number between 0 and 1 million to it
-- @param #string base String you want to unique-fy
-- @return #string Unique string
function UTILS.UniqueName(base)
    base = base or ""
    local ran = tostring(math.random(0, 1000000))

    if base == "" then
        return ran
    end
    return base .. "_" .. ran
end

--- Check if a string starts with something
-- @param #string str String to check
-- @param #string value
-- @return #boolean True if str starts with value
function string.startswith(str, value)
   return string.sub(str,1,string.len(value)) == value
end


--- Check if a string ends with something
-- @param #string str String to check
-- @param #string value
-- @return #boolean True if str ends with value
function string.endswith(str, value)
    return value == "" or str:sub(-#value) == value
end

--- Splits a string on a separator. For example:
--- string.split("hello_dcs_world", "-") would return {"hello", "dcs", "world"}
-- @param #string input String to split
-- @param #string separator What to split on
-- @return #table individual strings
function string.split(input, separator)
    local parts = {}
    for part in input:gmatch("[^" .. separator .. "]+") do
        table.insert(parts, part)
    end
    return parts
end


--- Checks if a string contains a substring. Easier to remember for Python people :)
--- string.split("hello_dcs_world", "-") would return {"hello", "dcs", "world"}
-- @param #string str
-- @param #string value
-- @return #boolean True if str contains value
function string.contains(str, value)
    return string.match(str, value)
end


--- Moves an object from one table to another
-- @param #table obj object to move
-- @param #table from_table table to move from
-- @param #table to_table table to move to
function table.move_object(obj, from_table, to_table)
    local index
    for i, v in pairs(from_table) do
        if v == obj then
            index = i
        end
    end

    if index then
        local moved = table.remove(from_table, index)
        table.insert_unique(to_table, moved)
    end
end

--- Given tbl is a indexed table ({"hello", "dcs", "world"}), checks if element exists in the table.
--- The table can be made up out of complex tables or values as well
-- @param #table tbl
-- @param #string element
-- @return #boolean True if tbl contains element
function table.contains(tbl, element)
    if element == nil or tbl == nil then return false end

    local index = 1
    while tbl[index] do
        if tbl[index] == element then
            return true
        end
        index = index + 1
    end
    return false
end

--- Checks if a table contains a specific key.
-- @param #table tbl Table to check
-- @param #string key Key to look for
-- @return #boolean True if tbl contains key
function table.contains_key(tbl, key)
    if tbl[key] ~= nil then return true else return false end
end

--- Inserts a unique element into a table.
-- @param #table tbl Table to insert into
-- @param #string element Element to insert
function table.insert_unique(tbl, element)
    if element == nil or tbl == nil then return end

    if not table.contains(tbl, element) then
        table.insert(tbl, element)
    end
end

--- Removes an element from a table by its value.
-- @param #table tbl Table to remove from
-- @param #string element Element to remove
function table.remove_by_value(tbl, element)
    local indices_to_remove = {}
    local index = 1
    for _, value in pairs(tbl) do
        if value == element then
            table.insert(indices_to_remove, index)
        end
        index = index + 1
    end

    for _, idx in pairs(indices_to_remove) do
        table.remove(tbl, idx)
    end
end

--- Removes an element from a table by its key.
-- @param #table table Table to remove from
-- @param #string key Key of the element to remove
-- @return #string Removed element
function table.remove_key(table, key)
    local element = table[key]
    table[key] = nil
    return element
end

--- Finds the index of an element in a table.
-- @param #table table Table to search
-- @param #string element Element to find
-- @return #number Index of the element, or nil if not found
function table.index_of(table, element)
    for i, v in ipairs(table) do
        if v == element then
            return i
        end
    end
    return nil
end

--- Counts the number of elements in a table.
-- @param #table T Table to count
-- @return #number Number of elements in the table
function table.length(T)
  local count = 0
  for _ in pairs(T) do count = count + 1 end
  return count
end

--- Slices a table between two indices, much like Python's my_list[2:-1]
-- @param #table tbl Table to slice
-- @param #number first Starting index
-- @param #number last Ending index
-- @return #table Sliced table
function table.slice(tbl, first, last)
  local sliced = {}
  local start = first or 1
  local stop = last or table.length(tbl)
  local count = 1

  for key, value in pairs(tbl) do
      if count >= start and count <= stop then
          sliced[key] = value
      end
      count = count + 1
  end

  return sliced
end

--- Counts the number of occurrences of a value in a table.
-- @param #table tbl Table to search
-- @param #string value Value to count
-- @return #number Number of occurrences of the value
function table.count_value(tbl, value)
    local count = 0
    for _, item in pairs(tbl) do
        if item == value then count = count + 1 end
    end
    return count
end

--- Add 2 table together, t2 gets added to t1
-- @param #table t1 First table
-- @param #table t2 Second table
-- @return #table Combined table
function table.combine(t1, t2)
    if t1 == nil and t2 == nil then
        BASE:E("Both tables were empty!")
    end

    if t1 == nil then return t2 end
    if t2 == nil then return t1 end
    for i=1,#t2 do
        t1[#t1+1] = t2[i]
    end
    return t1
end

--- Merges two tables into one. If a key exists in both t1 and t2, the value of t1 with be overwritten by the value of t2
-- @param #table t1 First table
-- @param #table t2 Second table
-- @return #table Merged table
function table.merge(t1, t2)
    for k, v in pairs(t2) do
        if (type(v) == "table") and (type(t1[k] or false) == "table") then
            table.merge(t1[k], t2[k])
        else
            t1[k] = v
        end
    end
    return t1
end

--- Adds an item to the end of a table.
-- @param #table tbl Table to add to
-- @param #string item Item to add
function table.add(tbl, item)
    tbl[#tbl + 1] = item
end

--- Shuffles the elements of a table.
-- @param #table tbl Table to shuffle
-- @return #table Shuffled table
function table.shuffle(tbl)
    local new_table = {}
    for _, value in ipairs(tbl) do
        local pos = math.random(1, #new_table +1)
        table.insert(new_table, pos, value)
    end
    return new_table
end

--- Finds a key-value pair in a table.
-- @param #table tbl Table to search
-- @param #string key Key to find
-- @param #string value Value to find
-- @return #table Table containing the key-value pair, or nil if not found
function table.find_key_value_pair(tbl, key, value)
    for k, v in pairs(tbl) do
        if type(v) == "table" then
            local result = table.find_key_value_pair(v, key, value)
            if result ~= nil then
                return result
            end
        elseif k == key and v == value then
            return tbl
        end
    end
    return nil
end

--- Convert a decimal to octal
-- @param #number Number the number to convert
-- @return #number Octal
function UTILS.DecimalToOctal(Number)
  if Number < 8 then return Number end
  local number = tonumber(Number)
  local octal = ""
  local n=1
  while number > 7 do
    local number1 = number%8
    octal = string.format("%d",number1)..octal
    local number2 = math.abs(number/8)
    if number2 < 8 then
      octal = string.format("%d",number2)..octal
    end
    number = number2
    n=n+1
  end
  return tonumber(octal)
end

--- Convert an octal to decimal
-- @param #number Number the number to convert
-- @return #number Decimal
function UTILS.OctalToDecimal(Number)
  return tonumber(Number,8)
end


--- HexToRGBA
-- @param hex_string table
-- @return #table R, G, B, A
function UTILS.HexToRGBA(hex_string)
    local hexNumber = tonumber(string.sub(hex_string, 3), 16) -- convert the string to a number
    -- extract RGBA components
    local alpha = hexNumber % 256
    hexNumber = (hexNumber - alpha) / 256
    local blue = hexNumber % 256
    hexNumber = (hexNumber - blue) / 256
    local green = hexNumber % 256
    hexNumber = (hexNumber - green) / 256
    local red = hexNumber % 256

    return {R = red, G = green, B = blue, A = alpha}
end


--- Function to save the position of a set of #OPSGROUP (ARMYGROUP) objects.
-- @param Core.Set#SET_OPSGROUP Set of ops objects to save
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @param #boolean Structured Append the data with a list of typenames in the group plus their count.
-- @return #boolean outcome True if saving is successful, else false.
function UTILS.SaveSetOfOpsGroups(Set,Path,Filename,Structured)
  local filename = Filename or "SetOfGroups"
  local data = "--Save SET of groups: (name,legion,template,alttemplate,units,position.x,position.y,position.z,strucdata) "..Filename .."\n"
  local List = Set:GetSetObjects()
  for _,_group in pairs (List) do
    local group = _group:GetGroup() -- Wrapper.Group#GROUP
    if group and group:IsAlive() then
      local name = group:GetName()
      local template = string.gsub(name,"(.AID.%d+$","")
      if string.find(template,"#") then
       template = string.gsub(name,"#(%d+)$","")
      end
      local alttemplate = _group.templatename or "none"
      local legiono = _group.legion -- Ops.Legion#LEGION
      local legion = "none"
      if legiono and type(legiono) == "table" and legiono.ClassName then
        legion = legiono:GetName()
        local asset = legiono:GetAssetByName(name) -- Functional.Warehouse#WAREHOUSE.Assetitem
        alttemplate=asset.templatename
      end
      local units = group:CountAliveUnits()
      local position = group:GetVec3()
      if Structured then
        local structure = UTILS.GetCountPerTypeName(group)
        local strucdata =  ""
        for typen,anzahl in pairs (structure) do
          strucdata = strucdata .. typen .. "=="..anzahl..";"
        end
        data = string.format("%s%s,%s,%s,%s,%d,%d,%d,%d,%s\n",data,name,legion,template,alttemplate,units,position.x,position.y,position.z,strucdata)
      else
        data = string.format("%s%s,%s,%s,%s,%d,%d,%d,%d\n",data,name,legion,template,alttemplate,units,position.x,position.y,position.z)
      end
    end
  end
  -- save the data
  local outcome = UTILS.SaveToFile(Path,Filename,data)
  return outcome
end

--- Load back a #OPSGROUP (ARMYGROUP) data from file for use with @{Ops.Brigade#BRIGADE.LoadBackAssetInPosition}()
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @return #table Returns a table of data entries: `{ groupname=groupname, size=size, coordinate=coordinate, template=template, structure=structure, legion=legion, alttemplate=alttemplate }`
-- Returns nil when the file cannot be read.
function UTILS.LoadSetOfOpsGroups(Path,Filename)

  local filename = Filename or "SetOfGroups"
  local datatable = {}

  if UTILS.CheckFileExists(Path,filename) then
    local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename)
    -- remove header
    table.remove(loadeddata, 1)
    for _id,_entry in pairs (loadeddata) do
      local dataset = UTILS.Split(_entry,",")
      -- 1name,2legion,3template,4alttemplate,5units,6position.x,7position.y,8position.z,9strucdata
      local groupname = dataset[1]
      local legion = dataset[2]
      local template = dataset[3]
      local alttemplate = dataset[4]
      local size = tonumber(dataset[5])
      local posx = tonumber(dataset[6])
      local posy = tonumber(dataset[7])
      local posz = tonumber(dataset[8])
      local structure = dataset[9]
      local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz})
      if size > 0 then
        local data = { groupname=groupname, size=size, coordinate=coordinate, template=template, structure=structure, legion=legion, alttemplate=alttemplate }
        table.insert(datatable,data)
      end
    end
  else
    return nil
  end

  return datatable
end

--- Get the clock position from a relative heading
-- @param #number refHdg The heading of the reference object (such as a Wrapper.UNIT) in 0-360
-- @param #number tgtHdg The absolute heading from the reference object to the target object/point in 0-360
-- @return #string text Text in clock heading such as "4 O'CLOCK"
-- @usage Display the range and clock distance of a BTR in relation to REAPER 1-1's heading:
--
--          myUnit = UNIT:FindByName( "REAPER 1-1" )
--          myTarget = GROUP:FindByName( "BTR-1" )
--
--          coordUnit = myUnit:GetCoordinate()
--          coordTarget = myTarget:GetCoordinate()
--
--          hdgUnit = myUnit:GetHeading()
--          hdgTarget = coordUnit:HeadingTo( coordTarget )
--          distTarget = coordUnit:Get3DDistance( coordTarget )
--
--          clockString = UTILS.ClockHeadingString( hdgUnit, hdgTarget )
--
--          -- Will show this message to REAPER 1-1 in-game: Contact BTR at 3 o'clock for 1134m!
--          MESSAGE:New("Contact BTR at " .. clockString .. " for " .. distTarget  .. "m!):ToUnit( myUnit )
function UTILS.ClockHeadingString(refHdg,tgtHdg)
    local relativeAngle = tgtHdg - refHdg
    if relativeAngle < 0 then
        relativeAngle = relativeAngle + 360
    end
    local clockPos = math.ceil((relativeAngle % 360) / 30)
    return clockPos.." o'clock"
end

--- Get a NATO abbreviated MGRS text for SRS use, optionally with prosody slow tag
-- @param #string Text The input string, e.g. "MGRS 4Q FJ 12345 67890"
-- @param #boolean Slow Optional - add slow tags
-- @return #string Output for (Slow) spelling in SRS TTS e.g. "MGRS;<prosody rate="slow">4;Quebec;Foxtrot;Juliett;1;2;3;4;5;6;7;8;niner;zero;</prosody>"
function UTILS.MGRSStringToSRSFriendly(Text,Slow)
    local Text = string.gsub(Text,"MGRS ","")
    Text = string.gsub(Text,"%s+","")
    Text = string.gsub(Text,"([%a%d])","%1;") -- "0;5;1;"
    Text = string.gsub(Text,"A","Alpha")
    Text = string.gsub(Text,"B","Bravo")
    Text = string.gsub(Text,"C","Charlie")
    Text = string.gsub(Text,"D","Delta")
    Text = string.gsub(Text,"E","Echo")
    Text = string.gsub(Text,"F","Foxtrot")
    Text = string.gsub(Text,"G","Golf")
    Text = string.gsub(Text,"H","Hotel")
    Text = string.gsub(Text,"I","India")
    Text = string.gsub(Text,"J","Juliett")
    Text = string.gsub(Text,"K","Kilo")
    Text = string.gsub(Text,"L","Lima")
    Text = string.gsub(Text,"M","Mike")
    Text = string.gsub(Text,"N","November")
    Text = string.gsub(Text,"O","Oscar")
    Text = string.gsub(Text,"P","Papa")
    Text = string.gsub(Text,"Q","Quebec")
    Text = string.gsub(Text,"R","Romeo")
    Text = string.gsub(Text,"S","Sierra")
    Text = string.gsub(Text,"T","Tango")
    Text = string.gsub(Text,"U","Uniform")
    Text = string.gsub(Text,"V","Victor")
    Text = string.gsub(Text,"W","Whiskey")
    Text = string.gsub(Text,"X","Xray")
    Text = string.gsub(Text,"Y","Yankee")
    Text = string.gsub(Text,"Z","Zulu")
    Text = string.gsub(Text,"0","zero")
    Text = string.gsub(Text,"9","niner")
    if Slow then
      Text = '<prosody rate="slow">'..Text..'</prosody>'
    end
    Text = "MGRS;"..Text
    return Text
end


--- Read csv file and convert it to a lua table.
-- The csv must have a header specifing the names of the columns. The column names are used as table keys.
-- @param #string filename File name including full path on local disk.
-- @return #table The table filled with data from the csv file.
function UTILS.ReadCSV(filename)

  if not UTILS.FileExists(filename) then  
    env.error("File does not exist")
    return nil  
  end
  
  --- Function that load data from a file.
  local function _loadfile( filename )
    local f = io.open( filename, "rb" )
    if f then
      local data = f:read( "*all" )
      f:close()
      return data
    else
      BASE:E(string.format( "WARNING: Could read data from file %s!", tostring( filename ) ) )
      return nil
    end
  end
  
  -- Load asset data from file.
  local data = _loadfile( filename )
  
  local lines=UTILS.Split(data, "\n" )
  
  -- Remove carriage returns from end of lines
  for _,line in pairs(lines) do
    line=string.gsub(line, "[\n\r]","")
  end
  
  local sep=";"
  
  local columns=UTILS.Split(lines[1], sep)

  -- Remove header line.
  table.remove(lines, 1)
  
  local csvdata={}  
  for i, line in pairs(lines) do
    line=string.gsub(line, "[\n\r]","")
  
    local row={}
    for j, value in pairs(UTILS.Split(line, sep)) do
    
      local key=string.gsub(columns[j], "[\n\r]","")
      row[key]=value    
    end
    table.insert(csvdata, row)
  
  end

  return csvdata
end

--- Seed the LCG random number generator.
-- @param #number seed Seed value. Default is a random number using math.random()
function UTILS.LCGRandomSeed(seed)
  UTILS.lcg = {
    seed = seed or math.random(1, 2^32 - 1),
    a = 1664525,
    c = 1013904223,
    m = 2^32
  }
end

--- Return a pseudo-random number using the LCG algorithm.
-- @return #number Random number between 0 and 1.
function UTILS.LCGRandom()
  if UTILS.lcg == nil then
    UTILS.LCGRandomSeed()
  end
  UTILS.lcg.seed = (UTILS.lcg.a * UTILS.lcg.seed + UTILS.lcg.c) % UTILS.lcg.m
  return UTILS.lcg.seed / UTILS.lcg.m
end

--- Spawns a new FARP of a defined type and coalition and functional statics (fuel depot, ammo storage, tent, windsock) around that FARP to make it operational.
-- Adds vehicles from template if given. Fills the FARP warehouse with liquids and known materiels.
-- References: [DCS Forum Topic](https://forum.dcs.world/topic/282989-farp-equipment-to-run-it)
-- @param #string Name Name of this FARP installation. Must be unique. 
-- @param Core.Point#COORDINATE Coordinate Where to spawn the FARP.
-- @param #string FARPType Type of FARP, can be one of the known types ENUMS.FARPType.FARP, ENUMS.FARPType.INVISIBLE, ENUMS.FARPType.HELIPADSINGLE, ENUMS.FARPType.PADSINGLE. Defaults to ENUMS.FARPType.FARP.
-- @param #number Coalition Coalition of this FARP, i.e. coalition.side.BLUE or coalition.side.RED, defaults to coalition.side.BLUE.
-- @param #number Country Country of this FARP, defaults to country.id.USA (blue) or country.id.RUSSIA (red).
-- @param #number CallSign Callsign of the FARP ATC, defaults to CALLSIGN.FARP.Berlin.
-- @param #number Frequency Frequency of the FARP ATC Radio, defaults to 127.5 (MHz).
-- @param #number Modulation Modulation of the FARP ATC Radio, defaults to radio.modulation.AM.
-- @param #number ADF ADF Beacon (FM) Frequency in KHz, e.g. 428. If not nil, creates an VHF/FM ADF Beacon for this FARP. Requires a sound called "beacon.ogg" to be in the mission (trigger "sound to" ...)
-- @param #number SpawnRadius Radius of the FARP, i.e. where the FARP objects will be placed in meters, not more than 150m away. Defaults to 100.
-- @param #string VehicleTemplate, template name for additional vehicles. Can be nil for no additional vehicles.
-- @param #number Liquids Tons of fuel to be added initially to the FARP. Defaults to 10 (tons). Set to 0 for no fill.
-- @param #number Equipment Number of equipment items per known item to be added initially to the FARP. Defaults to 10 (items). Set to 0 for no fill.
-- @param #number Airframes Number of helicopter airframes per known type in Ops.CSAR#CSAR.AircraftType to be added initially to the FARP. Set to 0 for no airframes.
-- @param #string F10Text Text to display on F10 map if given. Handy to post things like the ADF beacon Frequency, Callsign and ATC Frequency.
-- @param #boolean DynamicSpawns If true, allow Dynamic Spawns from this FARP.
-- @param #boolean HotStart If true and DynamicSpawns is true, allow hot starts for Dynamic Spawns from this FARP.
-- @return #list<Wrapper.Static#STATIC> Table of spawned objects and vehicle object (if given).
-- @return #string ADFBeaconName Name of the ADF beacon, to be able to remove/stop it later.
-- @return #number MarkerID ID of the F10 Text, to be able to remove it later.
function UTILS.SpawnFARPAndFunctionalStatics(Name,Coordinate,FARPType,Coalition,Country,CallSign,Frequency,Modulation,ADF,SpawnRadius,VehicleTemplate,Liquids,Equipment,Airframes,F10Text,DynamicSpawns,HotStart)
  
  -- Set Defaults
  local farplocation = Coordinate
  local farptype = FARPType or ENUMS.FARPType.FARP
  local Coalition = Coalition or coalition.side.BLUE
  local callsign = CallSign or CALLSIGN.FARP.Berlin
  local freq = Frequency or 127.5
  local mod = Modulation or radio.modulation.AM
  local radius = SpawnRadius or 100
  if radius < 0 or radius > 150 then radius = 100 end
  local liquids = Liquids or 10
  liquids = liquids * 1000 -- tons to kg
  local equip = Equipment or 10
  local airframes = Airframes or 10
  local statictypes = ENUMS.FARPObjectTypeNamesAndShape[farptype] or {TypeName="FARP", ShapeName="FARPS"}
  local STypeName = statictypes.TypeName
  local SShapeName = statictypes.ShapeName
  local Country = Country or (Coalition == coalition.side.BLUE and country.id.USA or country.id.RUSSIA)
  local ReturnObjects = {}
  
  -- Spawn FARP
  local newfarp = SPAWNSTATIC:NewFromType(STypeName,"Heliports",Country) --  "Invisible FARP" "FARP"
  newfarp:InitShape(SShapeName) -- "invisiblefarp" "FARPS"
  newfarp:InitFARP(callsign,freq,mod,DynamicSpawns,HotStart)
  local spawnedfarp = newfarp:SpawnFromCoordinate(farplocation,0,Name)
  table.insert(ReturnObjects,spawnedfarp)
  -- Spawn Objects
  local FARPStaticObjectsNato = {
    ["FUEL"] = { TypeName = "FARP Fuel Depot", ShapeName = "GSM Rus", Category = "Fortifications"},
    ["AMMO"] = { TypeName = "FARP Ammo Dump Coating", ShapeName = "SetkaKP", Category = "Fortifications"},
    ["TENT"] = { TypeName = "FARP Tent", ShapeName = "PalatkaB", Category = "Fortifications"},
    ["WINDSOCK"]  = { TypeName = "Windsock", ShapeName = "H-Windsock_RW", Category = "Fortifications"},
  }
    
  local farpobcount = 0
  for _name,_object in pairs(FARPStaticObjectsNato) do
    local objloc = farplocation:Translate(radius,farpobcount*30)
    local heading = objloc:HeadingTo(farplocation)
    local newobject = SPAWNSTATIC:NewFromType(_object.TypeName,_object.Category,Country)
    newobject:InitShape(_object.ShapeName)
    newobject:InitHeading(heading)
    newobject:SpawnFromCoordinate(objloc,farpobcount*30,_name.." - "..Name)
    table.insert(ReturnObjects,newobject)
    farpobcount = farpobcount + 1
  end
  
  -- Vehicle if any
  if VehicleTemplate and type(VehicleTemplate) == "string" then
    local vcoordinate = farplocation:Translate(radius,farpobcount*30)
    local heading = vcoordinate:HeadingTo(farplocation)
    local vehicles = SPAWN:NewWithAlias(VehicleTemplate,"FARP Vehicles - "..Name)
    vehicles:InitGroupHeading(heading)
    vehicles:InitCountry(Country)
    vehicles:InitCoalition(Coalition)
    vehicles:InitDelayOff()
    local spawnedvehicle = vehicles:SpawnFromCoordinate(vcoordinate)
    table.insert(ReturnObjects,spawnedvehicle)
  end
  
  local newWH = STORAGE:New(Name)
  if liquids and liquids > 0 then
    -- Storage fill-up
    newWH:SetLiquid(STORAGE.Liquid.DIESEL,liquids) -- kgs to tons
    newWH:SetLiquid(STORAGE.Liquid.GASOLINE,liquids)
    newWH:SetLiquid(STORAGE.Liquid.JETFUEL,liquids)
    newWH:SetLiquid(STORAGE.Liquid.MW50,liquids)
  end
  
  if equip and equip > 0 then
    for cat,nitem in pairs(ENUMS.Storage.weapons) do
      for name,item in pairs(nitem) do
        newWH:SetItem(item,equip)
      end
    end
  end
  
  if airframes and airframes > 0 then
    for typename in pairs (CSAR.AircraftType) do
      newWH:SetItem(typename,airframes)
    end
  end
  
  local ADFName
  if ADF and type(ADF) == "number" then
    local ADFFreq = ADF*1000 -- KHz to Hz
    local Sound =  "l10n/DEFAULT/beacon.ogg"
    local vec3 = farplocation:GetVec3()
    ADFName = Name .. " ADF "..tostring(ADF).."KHz"
    --BASE:I(string.format("Adding FARP Beacon %d KHz Name %s",ADF,ADFName))
    trigger.action.radioTransmission(Sound, vec3, 0, true, ADFFreq, 250, ADFName)
  end
  
  local MarkerID = nil
  if F10Text then
    local Color = {0,0,1}
    if Coalition == coalition.side.RED then
      Color = {1,0,0}
    elseif Coalition == coalition.side.NEUTRAL then
      Color = {0,1,0}
    end
    local Alpha = 0.75
    local coordinate = Coordinate:Translate(600,0)
    MarkerID = coordinate:TextToAll(F10Text,Coalition,Color,1,{1,1,1},Alpha,14,true)
  end
  
  return ReturnObjects, ADFName, MarkerID
end

--- Spawn a MASH at a given coordinate, optionally, add an ADF Beacon.
-- @param #string Name Unique Name of the Mash.
-- @param Core.Point#COORDINATE Coordinate Coordinate where to spawn the MASH. Can be given as a Core.Zone#ZONE object, in this case we take the center coordinate.
-- @param #number Country Country ID the MASH belongs to, e.g. country.id.USA or country.id.RUSSIA.
-- @param #number ADF (Optional) ADF Frequency in kHz (Kilohertz), if given activate an ADF Beacon at the location of the MASH.
-- @param #string Livery (Optional) The livery of the static CH-47, defaults to dark green.
-- @param #boolean DeployHelo (Optional) If true, deploy the helicopter static.
-- @param #number MASHRadio MASH Radio Frequency, defaults to 127.5.
-- @param #number MASHRadioModulation MASH Radio Modulation, defaults to radio.modulation.AM.
-- @param #number MASHCallsign Defaults to CALLSIGN.FARP.Berlin.
-- @param #table Templates (Optional) You can hand in your own template table of numbered(!) entries. Each entry consist of a relative(!) x,y position and data of a 
-- static, shape_name is optional. Also, livery_id is optional, but is applied to the helicopter static only.
-- @return #table Table of Wrapper.Static#STATIC objects that were spawned.
-- @return #string ADFName Name of the ADF Beacon to remove it later.
-- @usage
--            -- MASH Template example, this one is the built in one used in the function:
--            MASHTemplates = {
--              [1]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=0.000000,y=0.000000,},
--              [2]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=0.313533,y=8.778935,},
--              [3]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=16.303737,y=20.379671,},
--              [4]={category='Helicopters',type='CH-47Fbl1',shape_name='none',heading=0,x=-20.047735,y=-63.166179,livery_id = "us army dark green",},
--              [5]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=26.650339,y=20.066138,},
--              [6]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-25.432292,y=9.077099,},
--              [7]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-12.717421,y=-3.216114,},
--              [8]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-25.439281,y=-3.216114,},
--              [9]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-12.717421,y=9.155603,},
--              [10]={category='Fortifications',type='TACAN_beacon',shape_name='none',heading=0,x=-2.329847,y=-16.579903,},
--              [11]={category='Fortifications',type='FARP Fuel Depot',shape_name='GSM Rus',heading=0,x=2.222011,y=4.487030,},
--              [12]={category='Fortifications',type='APFC fuel',shape_name='M92_APFCfuel',heading=0,x=3.614927,y=0.367838,},
--              [13]={category='Fortifications',type='Camouflage03',shape_name='M92_Camouflage03',heading=0,x=21.544148,y=21.998879,},
--              [14]={category='Fortifications',type='Container_generator',shape_name='M92_Container_generator',heading=0,x=20.989192,y=37.314334,},
--              [15]={category='Fortifications',type='FireExtinguisher02',shape_name='M92_FireExtinguisher02',heading=0,x=3.988003,y=8.362333,},
--              [16]={category='Fortifications',type='FireExtinguisher02',shape_name='M92_FireExtinguisher02',heading=0,x=-3.953195,y=12.945844,},
--              [17]={category='Fortifications',type='Windsock',shape_name='H-Windsock_RW',heading=0,x=-18.944173,y=-33.042196,},
--              [18]={category='Fortifications',type='Tent04',shape_name='M92_Tent04',heading=0,x=21.220671,y=30.247529,},
--              }
--    
function UTILS.SpawnMASHStatics(Name,Coordinate,Country,ADF,Livery,DeployHelo,MASHRadio,MASHRadioModulation,MASHCallsign,Templates)
  
  -- Basic objects table
  
  local MASHTemplates = {
    [1]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=0.000000,y=0.000000,},
    [2]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=0.313533,y=8.778935,},
    [3]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=16.303737,y=20.379671,},
    [4]={category='Helicopters',type='CH-47Fbl1',shape_name='none',heading=0,x=-20.047735,y=-63.166179,livery_id = "us army dark green",},
    [5]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=26.650339,y=20.066138,},
    [6]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-25.432292,y=9.077099,},
    [7]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-12.717421,y=-3.216114,},
    [8]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-25.439281,y=-3.216114,},
    [9]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-12.717421,y=9.155603,},
    [10]={category='Fortifications',type='TACAN_beacon',shape_name='none',heading=0,x=-2.329847,y=-16.579903,},
    [11]={category='Fortifications',type='FARP Fuel Depot',shape_name='GSM Rus',heading=0,x=2.222011,y=4.487030,},
    [12]={category='Fortifications',type='APFC fuel',shape_name='M92_APFCfuel',heading=0,x=3.614927,y=0.367838,},
    [13]={category='Fortifications',type='Camouflage03',shape_name='M92_Camouflage03',heading=0,x=21.544148,y=21.998879,},
    [14]={category='Fortifications',type='Container_generator',shape_name='M92_Container_generator',heading=0,x=20.989192,y=37.314334,},
    [15]={category='Fortifications',type='FireExtinguisher02',shape_name='M92_FireExtinguisher02',heading=0,x=3.988003,y=8.362333,},
    [16]={category='Fortifications',type='FireExtinguisher02',shape_name='M92_FireExtinguisher02',heading=0,x=-3.953195,y=12.945844,},
    [17]={category='Fortifications',type='Windsock',shape_name='H-Windsock_RW',heading=0,x=-18.944173,y=-33.042196,},
    [18]={category='Fortifications',type='Tent04',shape_name='M92_Tent04',heading=0,x=21.220671,y=30.247529,},
    }

  if Templates then MASHTemplates=Templates end
  
  -- locals
  local name = Name or "Florence Nightingale"
  local positionVec2
  local positionVec3
  local ReturnStatics = {} 
  local CountryID = Country or country.id.USA
  local livery = "us army dark green"
  local MASHRadio = MASHRadio or 127.5
  local MASHRadioModulation = MASHRadioModulation or radio.modulation.AM
  local MASHCallsign = MASHCallsign or CALLSIGN.FARP.Berlin
  
  -- check for coordinate or zone  
  if type(Coordinate) == "table" then
    if Coordinate:IsInstanceOf("COORDINATE") or Coordinate:IsInstanceOf("ZONE_BASE") then
      positionVec2 = Coordinate:GetVec2()
      positionVec3 = Coordinate:GetVec3()  
    end
  else
      BASE:E("Spawn MASH - no ZONE or COORDINATE handed!")
      return
  end
  
  -- position
  local BaseX = positionVec2.x
  local BaseY = positionVec2.y
   
  -- Statics
  for id,object in pairs(MASHTemplates) do
    local NewName = string.format("%s#%3d",name,id)
    local vec2 = {x=BaseX+object.x,y=BaseY+object.y}
    local Coordinate=COORDINATE:NewFromVec2(vec2)
    local static = SPAWNSTATIC:NewFromType(object.type,object.category,CountryID)
    if object.shape_name and object.shape_name ~= "none" then
      static:InitShape(object.shape_name)
    end
    if object.category == "Helicopters" and DeployHelo == true then 
      if object.livery_id ~= nil then
        livery = object.livery_id
      end
      static:InitLivery(livery)
      local newstatic = static:SpawnFromCoordinate(Coordinate,object.heading,NewName)
      table.insert(ReturnStatics,newstatic)
    elseif object.category == "Heliports" then
      static:InitFARP(MASHCallsign,MASHRadio,MASHRadioModulation,false,false)
      local newstatic = static:SpawnFromCoordinate(Coordinate,object.heading,NewName)
      table.insert(ReturnStatics,newstatic)
    elseif object.category ~= "Helicopters" and object.category ~= "Heliports" then
      local newstatic = static:SpawnFromCoordinate(Coordinate,object.heading,NewName)
      table.insert(ReturnStatics,newstatic)
    end

  end
  
  -- Beacon
  local ADFName
  if ADF and type(ADF) == "number" then
    local ADFFreq = ADF*1000 -- KHz to Hz
    local Sound =  "l10n/DEFAULT/beacon.ogg"
    ADFName = Name .. " ADF "..tostring(ADF).."KHz"
    --BASE:I(string.format("Adding MASH Beacon %d KHz Name %s",ADF,ADFName))
    trigger.action.radioTransmission(Sound, positionVec3, 0, true, ADFFreq, 250, ADFName)
  end
  
  return ReturnStatics, ADFName
end

--- Converts a Vec2 to a Vec3.
-- @param vec the 2D vector
-- @param y optional new y axis (altitude) value. If omitted it's 0.
function UTILS.Vec2toVec3(vec,y) 
  if not vec.z then
    if vec.alt and not y then
      y = vec.alt
    elseif not y then
      y = 0
    end
    return {x = vec.x, y = y, z = vec.y}
  else
    return {x = vec.x, y = vec.y, z = vec.z}  -- it was already Vec3, actually.
  end
end

--- Get the correction needed for true north in radians
-- @param gPoint The map point vec2 or vec3
-- @return number correction
function UTILS.GetNorthCorrection(gPoint)  
  local point = UTILS.DeepCopy(gPoint)
  if not point.z then --Vec2; convert to Vec3
    point.z = point.y
    point.y = 0
  end
  local lat, lon = coord.LOtoLL(point)
  local north_posit = coord.LLtoLO(lat + 1, lon)
  return math.atan2(north_posit.z - point.z, north_posit.x - point.x)
end

--- Convert time in seconds to a DHMS table `{d = days, h = hours, m = minutes, s = seconds}`
-- @param timeInSec Time in Seconds
-- @return #table Table with DHMS data
function UTILS.GetDHMS(timeInSec)
  if timeInSec and type(timeInSec) == 'number' then
    local tbl = {d = 0, h = 0, m = 0, s = 0}
    if timeInSec > 86400 then
      while timeInSec > 86400 do
        tbl.d = tbl.d + 1
        timeInSec = timeInSec - 86400
      end
    end
    if timeInSec > 3600 then
      while timeInSec > 3600 do
        tbl.h = tbl.h + 1
        timeInSec = timeInSec - 3600
      end
    end
    if timeInSec > 60 then
      while timeInSec > 60 do
        tbl.m = tbl.m + 1
        timeInSec = timeInSec - 60
      end
    end
    tbl.s = timeInSec
    return tbl
  else
    BASE:E("No number handed!")
    return
  end
end

--- Returns heading-error corrected direction in radians.
-- True-north corrected direction from point along vector vec.
-- @param vec Vec3 Starting point
-- @param point Vec2 Direction
-- @return direction corrected direction from point.
function UTILS.GetDirectionRadians(vec, point)
  local dir = math.atan2(vec.z, vec.x)
  if point then
    dir = dir + UTILS.GetNorthCorrection(point)
  end
  if dir < 0 then
    dir = dir + 2 * math.pi -- put dir in range of 0 to 2*pi
  end
  return dir
end

--- Raycasting a point in polygon. Code from http://softsurfer.com/Archive/algorithm_0103/algorithm_0103.htm
-- @param point Vec2 or Vec3 to test
-- @param #table poly Polygon Table of Vec2/3 point forming the Polygon
-- @param #number maxalt Altitude limit (optional)
-- @param #boolean outcome 
function UTILS.IsPointInPolygon(point, poly, maxalt) 
  point = UTILS.Vec2toVec3(point)
  local px = point.x
  local pz = point.z
  local cn = 0
  local newpoly = UTILS.DeepCopy(poly)

  if not maxalt or (point.y <= maxalt) then
    local polysize = #newpoly
    newpoly[#newpoly + 1] = newpoly[1]

    newpoly[1] = UTILS.Vec2toVec3(newpoly[1])

    for k = 1, polysize do
      newpoly[k+1] = UTILS.Vec2toVec3(newpoly[k+1])
      if ((newpoly[k].z <= pz) and (newpoly[k+1].z > pz)) or ((newpoly[k].z > pz) and (newpoly[k+1].z <= pz)) then
        local vt = (pz - newpoly[k].z) / (newpoly[k+1].z - newpoly[k].z)
        if (px < newpoly[k].x + vt*(newpoly[k+1].x - newpoly[k].x)) then
          cn = cn + 1
        end
      end
    end

    return cn%2 == 1
  else
    return false
  end
end

--- Vector scalar multiplication.
-- @param vec Vec3 vector to multiply
-- @param #number mult scalar multiplicator
-- @return Vec3 new vector multiplied with the given scalar
function UTILS.ScalarMult(vec, mult)
  return {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult}
end

--- Utilities weather class for fog mainly.
-- @type UTILS.Weather
UTILS.Weather = {}

--- Returns the current fog thickness in meters. Returns zero if fog is not present. 
function UTILS.Weather.GetFogThickness()
  return world.weather.getFogThickness()
end

--- Sets the fog to the desired thickness in meters at sea level. 
-- @param #number Thickness Thickness in meters.
-- Any fog animation will be discarded.
-- Valid range : 100 to 5000 meters
function UTILS.Weather.SetFogThickness(Thickness)
  local value = Thickness
  if value < 100 then value = 100
  elseif value > 5000 then  value = 5000 end
  return world.weather.setFogThickness(value)
end

--- Removes the fog.
function UTILS.Weather.RemoveFog()
  return world.weather.setFogThickness(0)
end

--- Gets the maximum visibility distance of the current fog setting.
-- Returns 0 if no fog is present. 
function UTILS.Weather.GetFogVisibilityDistanceMax()
  return world.weather.getFogVisibilityDistance()
end

--- Sets the maximum visibility at sea level in meters.
-- @param #number Thickness Thickness in meters.
-- Limit: 100 to 100000 
function UTILS.Weather.SetFogVisibilityDistance(Thickness)
  local value = Thickness
  if value < 100 then value = 100
  elseif value > 100000 then  value = 100000 end
  return world.weather.setFogVisibilityDistance(value)
end

---  Uses data from the passed table to change the fog visibility and thickness over a desired timeframe. This allows for a gradual increase/decrease of fog values rather than abruptly applying the values.
-- Animation Key Format: {time, visibility, thickness}
-- @param #table AnimationKeys Table of AnimationKey tables
-- @usage
-- Time: in seconds 0 to infinity
-- Time is relative to when the function was called. Time value for each key must be larger than the previous key. If time is set to 0 then the fog will be applied to the corresponding visibility and thickness values at that key. Any time value greater than 0 will result in the current fog being inherited and changed to the first key.
-- Visibility: in meters 100 to 100000
-- Thickness: in meters 100 to 5000
-- The speed at which the visibility and thickness changes is based on the time between keys and the values that visibility and thickness are being set to.
--
-- When the function is passed an empty table {} or nil the fog animation will be discarded and whatever the current thickness and visibility are set to will remain.
-- 
-- The following will set the fog in the mission to disappear in 1 minute.
--
--            UTILS.Weather.SetFogAnimation({ {60, 0, 0} })
--
-- The following will take 1 hour to get to the first fog setting, it will maintain that fog setting for another hour, then lightly removes the fog over the 2nd and 3rd hour, the completely removes the fog after 3 hours and 3 minutes from when the function was called.
--
--            UTILS.Weather.SetFogAnimation({
--              {3600, 10000, 3000},    -- one hour to get to that fog setting
--              {7200, 10000, 3000},    -- will maintain for 2 hours
--              {10800, 20000, 2000},   -- at 3 hours visibility will have been increased while thickness decreases slightly
--              {12600, 0, 0},          -- at 3:30 after the function was called the fog will be completely removed. 
--            })
--  
function UTILS.Weather.SetFogAnimation(AnimationKeys)
  return world.weather.setFogAnimation(AnimationKeys)
end

--- The fog animation will be discarded and whatever the current thickness and visibility are set to will remain
function UTILS.Weather.StopFogAnimation()
  return world.weather.setFogAnimation({})
end

--- Find a ME created zone by its name
function UTILS.GetEnvZone(name)
    for _,v in ipairs(env.mission.triggers.zones) do
        if v.name == name then
            return v
        end
    end
end

--- net.dostring_in
function UTILS.DoStringIn(State,DoString)
  return net.dostring_in(State,DoString)
end

--- Show a picture on the screen to all
-- @param #string FileName File name of the picture
-- @param #number Duration Duration in seconds, defaults to 10
-- @param #boolean ClearView If true, clears the view before showing the picture, defaults to false
-- @param #number StartDelay Delay in seconds before showing the picture, defaults to 0
-- @param #number HorizontalAlign Horizontal alignment of the picture, 0: Left, 1: Center, 2: Right
-- @param #number VerticalAlign Vertical alignment of the picture, 0: Top, 1: Center, 2: Bottom
-- @param #number Size Size of the picture in percent, defaults to 100
-- @param #number SizeUnits Size units, 0 for % of original picture size, and 1 for % of window size
function UTILS.ShowPictureToAll(FilePath, Duration, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits)
    ClearView = ClearView or false
    StartDelay = StartDelay or 0
    HorizontalAlign = HorizontalAlign or 1
    VerticalAlign = VerticalAlign or 1
    Size = Size or 100
    SizeUnits = SizeUnits or 0

    if ClearView then ClearView = "true" else ClearView = "false" end

    net.dostring_in("mission", string.format("a_out_picture(\"%s\", %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")", FilePath, Duration or 10, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits))
end

--- Show a picture on the screen to Coalition
-- @param #number Coalition Coalition ID, can be coalition.side.BLUE, coalition.side.RED or coalition.side.NEUTRAL
-- @param #string FileName File name of the picture
-- @param #number Duration Duration in seconds, defaults to 10
-- @param #boolean ClearView If true, clears the view before showing the picture, defaults to false
-- @param #number StartDelay Delay in seconds before showing the picture, defaults to 0
-- @param #number HorizontalAlign Horizontal alignment of the picture, 0: Left, 1: Center, 2: Right
-- @param #number VerticalAlign Vertical alignment of the picture, 0: Top, 1: Center, 2: Bottom
-- @param #number Size Size of the picture in percent, defaults to 100
-- @param #number SizeUnits Size units, 0 for % of original picture size, and 1 for % of window size
function UTILS.ShowPictureToCoalition(Coalition, FilePath, Duration, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits)
    ClearView = ClearView or false
    StartDelay = StartDelay or 0
    HorizontalAlign = HorizontalAlign or 1
    VerticalAlign = VerticalAlign or 1
    Size = Size or 100
    SizeUnits = SizeUnits or 0

    if ClearView then ClearView = "true" else ClearView = "false" end

    local coalName = string.lower(UTILS.GetCoalitionName(Coalition))

    net.dostring_in("mission", string.format("a_out_picture_s(\"%s\", \"%s\", %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")", coalName, FilePath, Duration or 10, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits))
end

--- Show a picture on the screen to Country
-- @param #number Country Country ID, can be country.id.USA, country.id.RUSSIA, etc.
-- @param #string FileName File name of the picture
-- @param #number Duration Duration in seconds, defaults to 10
-- @param #boolean ClearView If true, clears the view before showing the picture, defaults to false
-- @param #number StartDelay Delay in seconds before showing the picture, defaults to 0
-- @param #number HorizontalAlign Horizontal alignment of the picture, 0: Left, 1: Center, 2: Right
-- @param #number VerticalAlign Vertical alignment of the picture, 0: Top, 1: Center, 2: Bottom
-- @param #number Size Size of the picture in percent, defaults to 100
-- @param #number SizeUnits Size units, 0 for % of original picture size, and 1 for % of window size
function UTILS.ShowPictureToCountry(Country, FilePath, Duration, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits)
    ClearView = ClearView or false
    StartDelay = StartDelay or 0
    HorizontalAlign = HorizontalAlign or 1
    VerticalAlign = VerticalAlign or 1
    Size = Size or 100
    SizeUnits = SizeUnits or 0

    if ClearView then ClearView = "true" else ClearView = "false" end

    net.dostring_in("mission", string.format("a_out_picture_c(%d, \"%s\", %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")", Country, FilePath, Duration or 10, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits))
end

--- Show a picture on the screen to Group
-- @param Wrapper.Group#GROUP Group Group to show the picture to
-- @param #string FileName File name of the picture
-- @param #number Duration Duration in seconds, defaults to 10
-- @param #boolean ClearView If true, clears the view before showing the picture, defaults to false
-- @param #number StartDelay Delay in seconds before showing the picture, defaults to 0
-- @param #number HorizontalAlign Horizontal alignment of the picture, 0: Left, 1: Center, 2: Right
-- @param #number VerticalAlign Vertical alignment of the picture, 0: Top, 1: Center, 2: Bottom
-- @param #number Size Size of the picture in percent, defaults to 100
-- @param #number SizeUnits Size units, 0 for % of original picture size, and 1 for % of window size
function UTILS.ShowPictureToGroup(Group, FilePath, Duration, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits)
    ClearView = ClearView or false
    StartDelay = StartDelay or 0
    HorizontalAlign = HorizontalAlign or 1
    VerticalAlign = VerticalAlign or 1
    Size = Size or 100
    SizeUnits = SizeUnits or 0

    if ClearView then ClearView = "true" else ClearView = "false" end

    net.dostring_in("mission", string.format("a_out_picture_g(%d, \"%s\", %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")", Group:GetID(), FilePath, Duration or 10, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits))
end

--- Show a picture on the screen to Unit
-- @param Wrapper.Unit#UNIT Unit Unit to show the picture to
-- @param #string FileName File name of the picture
-- @param #number Duration Duration in seconds, defaults to 10
-- @param #boolean ClearView If true, clears the view before showing the picture, defaults to false
-- @param #number StartDelay Delay in seconds before showing the picture, defaults to 0
-- @param #number HorizontalAlign Horizontal alignment of the picture, 0: Left, 1: Center, 2: Right
-- @param #number VerticalAlign Vertical alignment of the picture, 0: Top, 1: Center, 2: Bottom
-- @param #number Size Size of the picture in percent, defaults to 100
-- @param #number SizeUnits Size units, 0 for % of original picture size, and 1 for % of window size
function UTILS.ShowPictureToUnit(Unit, FilePath, Duration, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits)
    ClearView = ClearView or false
    StartDelay = StartDelay or 0
    HorizontalAlign = HorizontalAlign or 1
    VerticalAlign = VerticalAlign or 1
    Size = Size or 100
    SizeUnits = SizeUnits or 0

    if ClearView then ClearView = "true" else ClearView = "false" end

    net.dostring_in("mission", string.format("a_out_picture_u(%d, \"%s\", %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")", Unit:GetID(), FilePath, Duration or 10, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits))
end

--- Load a mission file. This will replace the current mission with the one given carrying along the online clients.
-- @param #string FileName Mission filename
function UTILS.LoadMission(FileName)
    net.dostring_in("mission", string.format("a_load_mission(\"%s\")", FileName))
end

--- Set the mission briefing for a coalition.
-- @param #number Coalition Briefing coalition ID, can be coalition.side.BLUE, coalition.side.RED or coalition.side.NEUTRAL
-- @param #string Text Briefing text, can contain newlines, will be converted formatted properly for DCS
-- @param #string Picture Picture file path, can be a file in the DEFAULT folder inside the .miz
function UTILS.SetMissionBriefing(Coalition, Text, Picture)
    Text = Text or ""
    Text = Text:gsub("\n", "\\n")
    Picture = Picture or ""
    local coalName = string.lower(UTILS.GetCoalitionName(Coalition))
    net.dostring_in("mission", string.format("a_set_briefing(\"%s\", \"%s\", \"%s\")", coalName, Picture, Text))
end

--- Show a helper gate at a DCS#Vec3 position
-- @param DCS#Vec3 pos The position
-- @param #number heading Heading in degrees, can be 0..359 degrees
function UTILS.ShowHelperGate(pos, heading)
    net.dostring_in("mission",string.format("a_show_helper_gate(%s, %s, %s, %f)", pos.x, pos.y, pos.z, math.rad(heading)))
end

--- Show a helper gate for a unit.
-- @param Wrapper.Unit#UNIT Unit The unit to show the gate for
-- @param #number Flag Helper gate flag
function UTILS.ShowHelperGateForUnit(Unit, Flag)
    net.dostring_in("mission",string.format("a_show_route_gates_for_unit(%d, \"%d\")", Unit:GetID(), Flag))
end

--- Set the carrier illumination mode. -2: OFF, -1: AUTO, 0: NAVIGATION, 1: AC LAUNCH, 2: AC RECOVERY
-- @param #number UnitID Carrier unit ID ( UNIT:GetID() )
-- @param #number Mode Illumination mode, can be -2: OFF, -1: AUTO, 0: NAVIGATION, 1: AC LAUNCH, 2: AC RECOVERY
function UTILS.SetCarrierIlluminationMode(UnitID, Mode)
    net.dostring_in("mission",string.format("a_set_carrier_illumination_mode(%d, %d)", UnitID, Mode))
end

--- Shell a zone, zone must ME created
-- @param #string name The name of the ME created zone
-- @param #number power Equals kg of TNT, e.g. 75
-- @param #count Number of shells simulated
function UTILS.ShellZone(name, power, count)
    local z = UTILS.GetEnvZone(name)
    if z then
        net.dostring_in("mission",string.format("a_shelling_zone(%d, %d, %d)", z.zoneId, power, count))
    end
end

--- Remove objects from a zone, zone must ME created
-- @param #string name The name of the ME created zone
-- @param #number type Type of objects to remove can be 0:all, 1: trees, 2:objects
function UTILS.RemoveObjects(name, type)
    local z = UTILS.GetEnvZone(name)
    if z then
        net.dostring_in("mission",string.format("a_remove_scene_objects(%d, %d)", z.zoneId, type))
    end
end

--- Remove scenery objects from a zone, zone must ME created
-- @param #string name The name of the ME created zone
-- @param #number level Level of removal
function UTILS.DestroyScenery(name, level)
    local z = UTILS.GetEnvZone(name)
    if z then
        net.dostring_in("mission",string.format("a_scenery_destruction_zone(%d, %d)", z.zoneId, level))
    end
end

--- Search for clear zones in a given area. A powerful and efficient function using Disposition to find clear areas for spawning ground units avoiding trees, water and map scenery.
-- @param DCS##Vec3 Center position vector for the search area.
-- @param #number SearchRadius Radius of the search area.
-- @param #number PosRadius Required clear radius around each position.
-- @param #number NumPositions Number of positions to find.
-- @return #table A table of DCS#Vec2 positions that are clear of map objects within the given PosRadius.
function UTILS.GetSimpleZones(Vec3, SearchRadius, PosRadius, NumPositions)
    return Disposition.getSimpleZones(Vec3, SearchRadius, PosRadius, NumPositions)
end

--- Search for clear ground spawn zones within this zone. A powerful and efficient function using Disposition to find clear areas for spawning ground units avoiding trees, water and map scenery.
-- @param Core.Zone#ZONE Zone to search.
-- @param #number (Optional) PosRadius Required clear radius around each position. (Default is math.min(Radius/10, 200))
-- @param #number (Optional) NumPositions Number of positions to find. (Default 50)
-- @return #table A table of DCS#Vec2 positions that are clear of map objects within the given PosRadius. nil if no clear positions are found.
function UTILS.GetClearZonePositions(Zone, PosRadius, NumPositions)
    local radius = PosRadius or math.min(Zone:GetRadius()/10, 200)
    local clearPositions = UTILS.GetSimpleZones(Zone:GetVec3(), Zone:GetRadius(), radius, NumPositions or 50)
    if clearPositions and #clearPositions > 0 then
        local validZones = {}
        for _, vec2 in pairs(clearPositions) do
            if Zone:IsVec2InZone(vec2) then
                table.insert(validZones, vec2)
            end
        end
        if #validZones > 0 then
            return validZones, radius
        end
    end
    return nil
end


--- Search for a random clear ground spawn coordinate within this zone. A powerful and efficient function using Disposition to find clear areas for spawning ground units avoiding trees, water and map scenery.
-- @param Core.Zone#ZONE Zone to search.
-- @param #number PosRadius (Optional) Required clear radius around each position. (Default is math.min(Radius/10, 200))
-- @param #number NumPositions (Optional) Number of positions to find. (Default 50)
-- @return Core.Point#COORDINATE A random coordinate for a clear zone. nil if no clear positions are found.
-- @return #number Assigned radius for the found zones. nil if no clear positions are found.
function UTILS.GetRandomClearZoneCoordinate(Zone, PosRadius, NumPositions)
    local clearPositions = UTILS.GetClearZonePositions(Zone, PosRadius, NumPositions)
    if clearPositions and #clearPositions > 0 then
        local randomPosition, radius = clearPositions[math.random(1, #clearPositions)]
        return COORDINATE:NewFromVec2(randomPosition), radius
    end

    return nil
end

--- Find the point on the radius of a circle closest to a point outside of the radius.
-- @param DCS#Vec2 Vec1 Simple Vec2 marking the middle of the circle.
-- @param #number Radius The radius of the circle.
-- @param DCS#Vec2 Vec2 Simple Vec2 marking the point outside of the circle.
-- @return DCS#Vec2 Vec2 point on the radius.
function UTILS.FindNearestPointOnCircle(Vec1,Radius,Vec2)
    local r = Radius
    local cx = Vec1.x or 1
    local cy = Vec1.y or 1
    local px = Vec2.x or 1
    local py = Vec2.y or 1

    -- Berechne den Vektor vom Mittelpunkt zum externen Punkt
    local dx = px - cx
    local dy = py - cy

    -- Berechne die Länge des Vektors
    local dist = math.sqrt(dx * dx + dy * dy)

    -- Wenn der Punkt im Mittelpunkt liegt, wähle einen Punkt auf der X-Achse
    if dist == 0 then
        return {x=cx + r, y=cy}
    end

    -- Normalisiere den Vektor (richtungsweise Vektor mit Länge 1)
    local norm_dx = dx / dist
    local norm_dy = dy / dist

    -- Berechne den Punkt auf dem Rand des Kreises
    local qx = cx + r * norm_dx
    local qy = cy + r * norm_dy

    local shift_factor = 1
    qx = qx + shift_factor * norm_dx
    qy = qy + shift_factor * norm_dy

    return {x=qx, y=qy}
end

--- This function uses Disposition and other fallback logic to find better ground positions for ground units.
--- NOTE: This is not a spawn randomizer.
--- It will try to find clear ground locations avoiding trees, water, roads, runways, map scenery, statics and other units in the area and modifies the provided positions table.
--- Maintains the original layout and unit positions as close as possible by searching for the next closest valid position to each unit.
-- @param #table Positions A table of DCS#Vec2 or DCS#Vec3, can be a units table from the group template.
-- @param DCS#Vec2 Anchor (Optional) DCS#Vec2 or DCS#Vec3 as anchor point to calculate offset of the units.
-- @param #number MaxRadius (Optional) Max radius to search for valid ground locations in meters. Default is double the max radius of the units.
-- @param #number Spacing (Optional) Minimum spacing between units in meters. Default is 5% of the search radius or 5 meters, whichever is larger.
function UTILS.ValidateAndRepositionGroundUnits(Positions, Anchor, MaxRadius, Spacing)
    local units = Positions
    Anchor = Anchor or UTILS.GetCenterPoint(units)
    local gPos = { x = Anchor.x, y = Anchor.z or Anchor.y }
    local maxRadius = 0
    local unitCount = 0
    for _, unit in pairs(units) do
        local pos = { x = unit.x, y = unit.z or unit.y }
        local dist = UTILS.VecDist2D(pos, gPos)
        if dist > maxRadius then
            maxRadius = dist
        end
        unitCount = unitCount + 1
    end
    maxRadius = MaxRadius or math.max(maxRadius * 2, 10)
    local spacing = Spacing or math.max(maxRadius * 0.05, 5)
    if unitCount > 0 and maxRadius > 5 then
        local spots = UTILS.GetSimpleZones(UTILS.Vec2toVec3(gPos), maxRadius, spacing, 1000)
        if spots and #spots > 0 then
            local validSpots = {}
            for _, spot in pairs(spots) do -- Disposition sometimes returns points on roads, hence this filter.
                if land.getSurfaceType(spot) == land.SurfaceType.LAND then
                    table.insert(validSpots, spot)
                end
            end
            spots = validSpots
        end

        local step = spacing
        for _, unit in pairs(units) do
            local pos = { x = unit.x, y = unit.z or unit.y }
            local isOnLand = land.getSurfaceType(pos) == land.SurfaceType.LAND
            local isValid = false
            if spots and #spots > 0 then
                local si = 1
                local sid = 0
                local closestDist = 100000000
                local closestSpot
                for _, spot in pairs(spots) do
                    local dist = UTILS.VecDist2D(pos, spot)
                    if dist < closestDist then
                        closestDist = dist
                        closestSpot = spot
                        sid = si
                    end
                    si = si + 1
                end
                if closestSpot then
                    if closestDist >= spacing then
                        pos = closestSpot
                    end
                    isValid = true
                    table.remove(spots, sid)
                end
            end

            -- Failsafe calculation
            if not isValid and not isOnLand then

                local h = UTILS.HdgTo(pos, gPos)
                local retries = 0
                while not isValid and retries < 500 do

                    local dist = UTILS.VecDist2D(pos, gPos)
                    pos = UTILS.Vec2Translate(pos, step, h)

                    local skip = false
                    for _, unit2 in pairs(units) do
                        if unit ~= unit2 then
                            local pos2 = { x = unit2.x, y = unit2.z or unit2.y }
                            local dist2 = UTILS.VecDist2D(pos, pos2)
                            if dist2 < 12 then
                                isValid = false
                                skip = true
                                break
                            end
                        end
                    end

                    if not skip and dist > step and land.getSurfaceType(pos) == land.SurfaceType.LAND then
                        isValid = true
                        break
                    elseif dist <= step then
                        break
                    end

                    retries = retries + 1
                end
            end

            if isValid then
                unit.x = pos.x
                if unit.z then
                    unit.z = pos.y
                else
                    unit.y = pos.y
                end
            end
        end
    end
end

--- This function uses Disposition and other fallback logic to find better ground positions for statics.
--- NOTE: This is not a spawn randomizer.
--- It will try to find clear ground locations avoiding trees, water, roads, runways, map scenery, statics and other units in the area and modifies the provided positions table.
--- Maintains the original layout and unit positions as close as possible by searching for the next closest valid position to each unit.
-- @param #table Positions A table of DCS#Vec2 or DCS#Vec3, can be a units table from the group template.
-- @param DCS#Vec2 Position DCS#Vec2 or DCS#Vec3 initial spawn location.
-- @param #number MaxRadius (Optional) Max radius to search for valid ground locations in meters. Default is double the max radius of the static.
-- @return DCS#Vec2 Initial Position if it's valid, else a valid spawn position. nil if no valid position found.
function UTILS.ValidateAndRepositionStatic(Country, Category, Type, Position, ShapeName, MaxRadius)
    local coord = COORDINATE:NewFromVec2(Position)
    local st = SPAWNSTATIC:NewFromType(Type, Category, Country)
    if ShapeName then
        st:InitShape(ShapeName)
    end
    local sName = "s-"..timer.getTime().."-"..math.random(1,10000)
    local tempStatic = st:SpawnFromCoordinate(coord, 0, sName)
    if tempStatic then
        local sRadius = tempStatic:GetBoundingRadius(2) or 3
        tempStatic:Destroy()
        sRadius = sRadius * 0.5
        MaxRadius = MaxRadius or math.max(sRadius * 10, 100)
        local positions = UTILS.GetSimpleZones(coord:GetVec3(), MaxRadius, sRadius, 20)
        if positions and #positions > 0 then
            local closestSpot
            local closestDist = math.huge
            for _, spot in pairs(positions) do -- Disposition sometimes returns points on roads, hence this filter.
                if land.getSurfaceType(spot) == land.SurfaceType.LAND then
                    local dist = UTILS.VecDist2D(Position, spot)
                    if dist < closestDist then
                        closestDist = dist
                        closestSpot = spot
                    end
                end
            end

            if closestSpot then
                if closestDist >= sRadius then
                    return closestSpot
                else
                    return Position
                end
            end
        end
    end

    return nil
end
