--- **Ops** - AWACS
--
-- ===
--
-- ## Main Features:
--
--  * TBD
--
-- ===
--
-- ## Example Missions:
-- 
-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/).
--       
-- ===
-- 
-- ### Author: **applevangelist**
-- Last Update April 2022
--
-- ===
-- @module Ops.AWACS
-- @image MOOSE.JPG

do
--- Ops AWACS Class
-- @type AWACS
-- @field #string ClassName Name of this class.
-- @field #string version Versioning.
-- @field #string lid LID for log entries.
-- @field #number coalition Colition side.
-- @field #string coalitiontxt = "blue"
-- @field Core.Zone#ZONE OpsZone,
-- @field Core.Zone#ZONE AnchorZone,
-- @field #number Frequency
-- @field #number Modulation
-- @field Wrapper.Airbase#AIRBASE Airbase
-- @field Ops.AirWing#AIRWING AirWing
-- @field #number AwacsAngels
-- @field Core.Zone#ZONE OrbitZone
-- @field #number CallSign
-- @field #number CallSignNo
-- @field #boolean debug
-- @field #number verbose
-- @field #table ManagedGrps
-- @field #number ManagedGrpID
-- @field #number ManagedTaskID
-- @field Utilities.FiFo#FIFO AnchorStacks
-- @field Utilities.FiFo#FIFO CAPIdleAI
-- @field Utilities.FiFo#FIFO CAPIdleHuman
-- @field Utilities.FiFo#FIFO TaskedCAPAI
-- @field Utilities.FiFo#FIFO TaskedCAPHuman
-- @field Utilities.FiFo#FIFO OpenTasks
-- @field Utilities.FiFo#FIFO AssignedTasks
-- @field Utilities.FiFo#FIFO PictureAO
-- @field Utilities.FiFo#FIFO PictureEWR
-- @field Utilities.FiFo#FIFO Contacts
-- @field Utilities.FiFo#FIFO ContactsAO
-- @field Utilities.FiFo#FIFO RadioQueue
-- @field Utilities.FiFo#FIFO CAPAirwings
-- @field #number AwacsTimeOnStation
-- @field #number AwacsTimeStamp
-- @field #number EscortsTimeOnStation
-- @field #number EscortsTimeStamp
-- @field #string AwacsROE
-- @field #string AwacsROT
-- @field Ops.Auftrag#AUFTRAG AwacsMission
-- @field Ops.Auftrag#AUFTRAG EscortMission
-- @field Ops.Auftrag#AUFTRAG AwacsMissionReplacement
-- @field Ops.Auftrag#AUFTRAG EscortMissionReplacement
-- @field Utilities.FiFo#FIFO AICAPMissions FIFO for Ops.Auftrag#AUFTRAG for AI CAP
-- @field #boolean MenuStrict
-- @field #number MaxAIonCAP
-- @field #number AIonCAP
-- @field #boolean ShiftChangeAwacsFlag
-- @field #boolean ShiftChangeEscortsFlag
-- @field #boolean ShiftChangeAwacsRequested
-- @field #boolean ShiftChangeEscortsRequested
-- @extends Core.Fsm#FSM

---
--
-- @field #AWACS
AWACS = {
  ClassName = "AWACS", -- #string
  version = "alpha 0.0.3", -- #string
  lid = "", -- #string
  coalition = coalition.side.BLUE, -- #number
  coalitiontxt = "blue", -- #string
  OpsZone = nil,
  AnchorZone = nil,
  AirWing = nil,
  Frequency = 271, -- #number
  Modulation = radio.modulation.AM, -- #number
  Airbase = nil,
  AwacsAngels = 25, -- orbit at 25'000 ft
  OrbitZone = nil,
  CallSign = CALLSIGN.AWACS.Magic, -- #number
  CallSignNo = 1, -- #number
  debug = true,
  verbose = true,
  ManagedGrps = {},
  ManagedGrpID = 0, -- #number
  ManagedTaskID = 0, -- #number
  AnchorStacks = {}, -- Utilities.FiFo#FIFO
  CAPIdleAI = {},
  CAPIdleHuman = {},
  TaskedCAPAI = {},
  TaskedCAPHuman = {},
  OpenTasks = {}, -- Utilities.FiFo#FIFO
  AssignedTasks = {}, -- Utilities.FiFo#FIFO
  PictureAO = {}, -- Utilities.FiFo#FIFO
  PictureEWR = {}, -- Utilities.FiFo#FIFO
  Contacts = {}, -- Utilities.FiFo#FIFO
  ContactsAO = {}, -- Utilities.FiFo#FIFO
  RadioQueue = {}, -- Utilities.FiFo#FIFO
  AwacsTimeOnStation = 1,
  AwacsTimeStamp = 0,
  EscortsTimeOnStation = 0.5,
  EscortsTimeStamp = 0,
  AwacsROE = "",
  AwacsROT = "",
  MenuStrict = true,
  MaxAIonCAP = 4,
  AIonCAP = 0,
  AICAPMissions = {}, -- Utilities.FiFo#FIFO
  ShiftChangeAwacsFlag = false,
  ShiftChangeEscortsFlag = false,
  ShiftChangeAwacsRequested = false,
  ShiftChangeEscortsRequested = false,
  CAPAirwings = {},  -- Utilities.FiFo#FIFO
}

---
--@field CallSignClear
AWACS.CallSignClear = {
    [1]="Overlord",
    [2]="Magic",
    [3]="Wizard",
    [4]="Focus",
    [5]="Darkstar",
}

---
-- @field AnchorNames
AWACS.AnchorNames = {
  [1] = "One",
  [2] = "Two",
  [3] = "Three",
  [4] = "Four",
  [5] = "Five",
  [6] = "Six",
  [7] = "Seven",
  [8] = "Eight",
  [9] = "Nine",
  [10] = "Ten",
}

---
-- @field ROE
AWACS.ROE = {
  POLICE = "Police",
  VID = "Visual ID",
  IFF = "IFF",
  BVR = "Beyond Visual Range",
}

---
-- @field AWACS.ROT
AWACS.ROT = {
    PASSIVE = "Passive Defense",
    ACTIVE = "Active Defense",
    LOCK = "Lock",
    RETURNFIRE = "Return Fire",
    OPENFIRE = "Open Fire",
 }
 
---
--@field THREATLEVEL -- can be 1-10, thresholds
AWACS.THREATLEVEL = {
  GREEN = 3,
  AMBER = 7,
  RED = 10,
}

---
-- @type AWACS.MenuStructure
-- @field #boolean menuset
-- @field #string groupname
-- @field Core.Menu#MENU_GROUP basemenu
-- @field Core.Menu#MENU_GROUP_COMMAND checkin
-- @field Core.Menu#MENU_GROUP_COMMAND checkout
-- @field Core.Menu#MENU_GROUP_COMMAND picture
-- @field Core.Menu#MENU_GROUP_COMMAND bogeydope
-- @field Core.Menu#MENU_GROUP_COMMAND declare
-- @field Core.Menu#MENU_GROUP_COMMAND showtask

---
-- @type AWACS.ManagedGroup
-- @field Wrapper.Group#GROUP Group
-- @field #string GroupName
-- @field Ops.FlightGroup#FLIGHTGROUP FlightGroup for AI
-- @field #boolean IsPlayer
-- @field #boolean IsAI
-- @field #string CallSign
-- @field #number CurrentTask
-- @field #boolean HasAssignedTask
-- @field #number GID
-- @field #number AnchorStackNo
-- @field #number AnchorStackAngels

---
-- @type AWACS.TaskDescription
AWACS.TaskDescription = {
  ANCHOR = "Anchor",
  REANCHOR = "Re-Anchor",
  INTERCEPT = "Intercept",
  SWEEP = "Sweep",
  RTB = "RTB",
}

---
-- @type AWACS.TaskStatus
AWACS.TaskStatus = {
  IDLE = "Idle",
  ASSIGNED = "Assigned",
  EXECUTING = "Executing",
  SUCCESS = "Success",
  FAILED = "Failed",
}

---
-- @type AWACS.ManagedTask
-- @field #number TID
-- @field #number AssignedGroupID
-- @field #boolean IsPlayerTask
-- @field #boolean IsUnassigned
-- @field Ops.Target#TARGET Target
-- @field Ops.Auftrag#AUFTRAG Auftrag
-- @field #AWACS.TaskStatus Status
-- @field #AWACS.TaskDescription ToDo
-- @field #string ScreenText Long descrition
-- @field Ops.Intelligence#INTEL.Contact Contact
-- @field Ops.Intelligence#INTEL.Cluster Cluster

---
-- @type AWACS.AnchorAssignedEntry
-- @field #number ID
-- @field #number Angels

---
-- @type AWACS.AnchorData
-- @field #number AnchorBaseAngels
-- @field #boolean AnchorZone
-- @field Core.Point#COORDINATE AnchorZoneCoordinate
-- @field #boolean AnchorZoneCoordinateText
-- @field Utilities.FiFo#FIFO AnchorAssignedID FiFo of #AWACS.AnchorAssignedEntry
-- @field Utilities.FiFo#FIFO Anchors FiFo of available stacks

---
--@type RadioEntry
--@field #string TextTTS
--@field #string TextScreen
--@field #boolean IsNew
--@field #boolean IsGroup
--@field #boolean GroupID
--@field #number Duration
--@field #boolean ToScreen
--@field #boolean FromAI

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO-List
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- DEBUG - Escorts via AirWing not staying on
-- TODO - Link (multiple) AWs to the AWACS Controller
-- DONE - Use AO as Anchor of Bulls, AO as default
-- DONE - SRS TTS output
-- DONE - Check-In/Out Humans
-- DONE - Check-In/Out AI
-- DONE - Picture
-- TODO - TripWire
-- DONE - Radio Menu
-- DONE - Intel Detection
-- TODO - CHIEF / COMMANDER / AIRWING connection?
-- TODO - LotATC / IFF
-- DONE - ROE
-- TODO - Player & AI tasking
-- DONE - Anchor Stack Management
-- TODO - Reporting
-- TODO - Missile launch callout
-- TODO - Localization
-- DONE - Shift Length AWACS/AI
-- DEBUG - Shift Change, Change on asset RTB or dead or mission done
-- TODO - Borders for INTEL
-- TODO - FIFO for checkin/checkout and tasking
-- TODO - Event detection
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO Constructor

--- Set up a new AI AWACS.
-- @param #AWACS self
-- @param #string Name Name of this AWACS for the radio menu.
-- @param #string AirWing The core Ops.AirWing#AIRWING managing the AWACS, Escort and (optionally) AI CAP planes for us.
-- @param #number Coalition Coalition, e.g. coalition.side.BLUE. Can also be passed as "blue", "red" or "neutral".
-- @param #string AirbaseName Name of the home airbase.
-- @param #string AwacsOrbit Name of the round, mission editor created zone where this AWACS orbits.
-- @param #string OpsZone Name of the round, mission editor created operations zone this AWACS controls. Can be passed as #ZONE_POLYGON
-- @param #string AnchorZone Name of the round, mission editor created anchor zone where CAP groups will be stacked.
-- @param #number Frequency Radio frequency, e.g. 271.
-- @param #number Modulation Radio modulation, e.g. radio.modulation.AM or radio.modulation.FM.
-- @return #AWACS self
function AWACS:New(Name,AirWing,Coalition,AirbaseName,AwacsOrbit,OpsZone,AnchorZone,Frequency,Modulation)
    -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New())
  
  --set Coalition
  if Coalition and type(Coalition)=="string" then
    if Coalition=="blue" then
      self.coalition=coalition.side.BLUE
      self.coalitiontxt = Coalition
    elseif Coalition=="red" then
      self.coalition=coalition.side.RED
      self.coalitiontxt = Coalition
    elseif Coalition=="neutral" then
      self.coalition=coalition.side.NEUTRAL
      self.coalitiontxt = Coalition
    else
      self:E("ERROR: Unknown coalition in AWACS!")
    end
  else
    self.coalition = Coalition
    self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition))
  end
  
  -- base setup
  self.Name = Name -- #string
  self.AirWing = AirWing -- Ops.AirWing#AIRWING object
  
  AirWing:SetUsingOpsAwacs(self)
  
  self.CAPAirwings = FIFO:New() -- Utilities.FiFo#FIFO
  self.CAPAirwings:Push(AirWing,1)
  
  self.AwacsFG = nil
  --self.AwacsPayload = PayLoad -- Ops.AirWing#AIRWING.Payload
  self.ModernEra = true -- use of EPLRS
  self.RadarBlur = 10 -- 10% detection precision i.e. 90-110 reported group size
  if type(OpsZone) == "string" then
    self.OpsZone = ZONE:New(OpsZone) -- Core.Zone#ZONE
  elseif type(OpsZone) == "table" and OpsZone.ClassName and OpsZone.ClassName == "ZONE_POLYGON" then
    self.OpsZone = OpsZone
  else
    self:E("AWACS - Invalid OpsZone passed!")
    return
  end
  
  self.AOCoordinate = self.OpsZone:GetCoordinate()
  self.UseBullsAO = false
  self.ControlZoneRadius = 100 -- nm
  self.AnchorZone = ZONE:New(AnchorZone) -- Core.Zone#ZONE
  self.Frequency = Frequency or 271 -- #number
  self.Modulation = Modulation or radio.modulation.AM
  self.Airbase = AIRBASE:FindByName(AirbaseName)
  self.AwacsAngels = 25 -- orbit at 25'000 ft
  self.OrbitZone = ZONE:New(AwacsOrbit) -- Core.Zone#ZONE
  self.CallSign = CALLSIGN.AWACS.Magic -- #number
  self.CallSignNo = 1 -- #number
  self.NoHelos = true
  self.MaxAIonCAP = 4
  self.AIonCAP = 0
  self.AICAPMissions = FIFO:New() -- Utilities.FiFo#FIFO
  
  local speed = 250
  self.SpeedBase = speed
  self.Speed = UTILS.KnotsToAltKIAS(speed,self.AwacsAngels*1000)
  self.CapSpeedBase = 220
  self.Heading = 0 -- north
  self.Leg = 50 -- nm
  self.invisible = true
  self.immortal = true
  self.callsigntxt = "AWACS"
  
  self.AwacsTimeOnStation = 2
  self.AwacsTimeStamp = 0
  self.EscortsTimeOnStation = 2
  self.EscortsTimeStamp = 0
  self.ShiftChangeTime = 0.25 -- 15mins
  self.ShiftChangeAwacsFlag = false
  self.ShiftChangeEscortsFlag = false
  
  self.AwacsROE = AWACS.ROE.POLICE
  self.AwacsROT = AWACS.ROT.PASSIVE
  
  self.MenuStrict = true
  
  -- Escorts
  self.HasEscorts = false
  self.EscortTemplate = ""
  
  -- SRS
  self.PathToSRS = "C:\\Program Files\\DCS-SimpleRadio-Standalone"
  self.Gender = "male"
  self.Culture = "en-US"
  self.Voice = nil
  self.Port = 5002
  self.RadioQueue = FIFO:New() -- Utilities.FiFo#FIFO
  self.maxspeakentries = 3
  
  self.CAPGender = "male"
  self.CAPCulture = "en-US"
  self.CAPVoice = nil
  
  -- Client SET  
  self.clientset = SET_GROUP:New():FilterCategoryAirplane():FilterCoalitions(self.coalitiontxt):FilterStart()
  
  -- managed groups
  self.ManagedGrps = {} -- #table of #AWACS.ManagedGroup entries
  self.ManagedGrpID = 0  
  
  self.AICAPCAllName = CALLSIGN.F16.Viper
  self.AICAPCAllNumber = 0
  
  -- Anchor stacks init
  self.AnchorStacks = FIFO:New() -- Utilities.FiFo#FIFO
  self.AnchorBaseAngels = 22
  self.AnchorStackDistance = 2
  self.AnchorMaxStacks = 4
  self.AnchorMaxAnchors = 2
  self.AnchorMaxZones = 6
  self.AnchorCurrZones = 1
  self.AnchorTurn = -(360/self.AnchorMaxZones)

  self:_CreateAnchorStack()

  -- Task lists
  self.AssignedTasks = FIFO:New() -- Utilities.FiFo#FIFO
  self.OpenTasks = FIFO:New() -- Utilities.FiFo#FIFO
  
  -- Pilot lists
  --[[ ToDo - Maybe only 2? Move to managedgroups
  self.CAPIdleAI = FIFO:New() -- Utilities.FiFo#FIFO
  self.CAPIdleHuman = FIFO:New() -- Utilities.FiFo#FIFO
  self.TaskedCAPAI = FIFO:New() -- Utilities.FiFo#FIFO
  self.TaskedCAPHuman = FIFO:New() -- Utilities.FiFo#FIFO
  --]]
  
  -- Picture, Contacts, Bogeys
  self.PictureAO = FIFO:New() -- Utilities.FiFo#FIFO
  self.PictureEWR = FIFO:New() -- Utilities.FiFo#FIFO
  self.Contacts = FIFO:New() -- Utilities.FiFo#FIFO
  self.ContactsAO = FIFO:New() -- Utilities.FiFo#FIFO

  -- SET for Intel Detection
  self.DetectionSet=SET_GROUP:New()
  
  -- Set some string id for output to DCS.log file.
  self.lid=string.format("%s (%s) | ", self.Name, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown")
  
    -- Start State.
  self:SetStartState("Stopped")
  
  -- Add FSM transitions.
  --                 From State  -->   Event        -->     To State
  self:AddTransition("Stopped",       "Start",              "Running")     -- Start FSM.
  self:AddTransition("*",             "Status",             "*")           -- Status update.
  self:AddTransition("*",             "CheckedIn",          "*") 
  self:AddTransition("*",             "CheckedOut",         "*") 
  self:AddTransition("*",             "AssignAnchor",       "*") 
  self:AddTransition("*",             "AssignedAnchor",     "*")
  self:AddTransition("*",             "NewCluster",         "*")
  self:AddTransition("*",             "NewContact",         "*")
  self:AddTransition("*",             "LostCluster",        "*")
  self:AddTransition("*",             "LostContact",        "*")
  self:AddTransition("*",             "CheckRadioQueue",    "*")
  self:AddTransition("*",             "EscortShiftChange",  "*")
  self:AddTransition("*",             "AwacsShiftChange",   "*")
  self:AddTransition("*",             "FlightOnMission",    "*")
  --
  self:AddTransition("*",             "Stop",               "Stopped")     -- Stop FSM.
  
  -- self:__Start(math.random(2,5))
  
  local text = string.format("%sAWACS Version %s Initiated",self.lid,self.version)
  
  self:I(text)
  
  -- debug zone markers
  if self.debug then
    self.OpsZone:DrawZone(-1,{1,0,0},1,{1,0,0},0.2,5,true)
    MARKER:New(self.OpsZone:GetCoordinate(),"AO Zone"):ToAll()
    self.AnchorZone:DrawZone(-1,{0,0,1},1,{0,0,1},0.2,5,true)
    MARKER:New(self.AnchorZone:GetCoordinate(),"Anchor Zone"):ToAll()
    self.OrbitZone:DrawZone(-1,{0,1,0},1,{0,1,0},0.2,5,true)
    MARKER:New(self.OrbitZone:GetCoordinate(),"Orbit Zone"):ToAll()
  end
  
  return self
end

-- TODO Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- [User] Get AWACS Name
-- @param #AWACS self
-- @return #string Name of this instance
function AWACS:GetName()
  return self.Name or "not set"
end

--- [User] Set AWACS flight details
-- @param #AWACS self
-- @param #number CallSign Defaults to CALLSIGN.AWACS.Magic
-- @param #number CallSignNo Defaults to 1
-- @param #number Angels Defaults to 25 (i.e. 25000 ft)
-- @param #number Speed Defaults to 250kn
-- @param #number Heading Defaults to 0 (North)
-- @param #number Leg Defaults to 25nm
-- @return #AWACS self
function AWACS:SetAwacsDetails(CallSign,CallSignNo,Angels,Speed,Heading,Leg)
  self:T(self.lid.."SetAwacsDetails")
  self.CallSign = CallSign or CALLSIGN.AWACS.Magic
  self.CallSignNo = CallSignNo or 1
  self.Angels = Angels or 25
  local speed = Speed or 250
  self.SpeedBase = speed
  self.Speed = UTILS.KnotsToAltKIAS(speed,self.Angels*1000)
  self.Heading = Heading or 0
  self.Leg = Leg or 25
  return self
end

--- [User] Add a radar GROUP object to the INTEL detection SET_GROUP
-- @param Wrapper.Group#GROUP Group The GROUP to be added. Can be passed as SET_GROUP.
-- @return #AWACS self
function AWACS:AddGroupToDetection(Group)
  self:T(self.lid.."AddGroupToDetection")
  if Group and Group.ClassName and Group.ClassName == "GROUP" then
    self.DetectionSet:AddGroup(Group)
  elseif Group and Group.ClassName and Group.ClassName == "SET_GROUP" then
    self.DetectionSet:AddSet(Group)
  end
  return self
end

--- [User] Set AWACS SRS TTS details - see @{#MSRS} for details
-- @param #AWACS self
-- @param #string PathToSRS Defaults to "C:\\Program Files\\DCS-SimpleRadio-Standalone"
-- @param #string Gender Defaults to "male"
-- @param #string Culture Defaults to "en-US"
-- @param #number Port Defaults to 5002
-- @param #string Voice (Optional) Use a specifc voice with the @{#MSRS.SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`.
-- Note that this must be installed on your windows system.
-- @return #AWACS self
function AWACS:SetSRS(PathToSRS,Gender,Culture,Port,Voice)
  self:T(self.lid.."SetSRS")
  self.PathToSRS = PathToSRS or "C:\\Program Files\\DCS-SimpleRadio-Standalone"
  self.Gender = Gender or "male"
  self.Culture = Culture or "en-US"
  self.Port = Port or 5002
  self.Voice = Voice 
  return self
end

--- [User] Set AWACS Escorts Template
-- @param #AWACS self
-- @param #number EscortNumber Number of fighther planes to accompany this AWACS. 0 or nil means no escorts.
-- @return #AWACS self
function AWACS:SetEscort(EscortNumber)
  self:T(self.lid.."SetEscort")
  if EscortNumber and EscortNumber > 0 then
    self.HasEscorts = true
    self.EscortNumber = EscortNumber
  end
  return self
end

--- [Internal] Start AWACS Escorts FlightGroup
-- @param #AWACS self
-- @param #boolean Shiftchange This is a shift change call
-- @return #AWACS self
function AWACS:_StartEscorts(Shiftchange)
  self:T(self.lid.."_StartEscorts")
  
  local AwacsFG = self.AwacsFG -- Ops.FlightGroup#FLIGHTGROUP
  local group = AwacsFG:GetGroup()
  local mission = AUFTRAG:NewESCORT(group,{x=-100, y=0, z=200},30,{"Air"})
  mission:SetRequiredAssets(self.EscortNumber)
  
  local timeonstation = (self.EscortsTimeOnStation + self.ShiftChangeTime) * 3600 -- hours to seconds
  mission:SetTime(nil,timeonstation)
  
  self.AirWing:AddMission(mission)
  
  if Shiftchange then
    self.EscortMissionReplacement = mission
  else
    self.EscortMission = mission
  end
  
  return self
end

--- [Internal] AWACS further Start Settings
-- @param #AWACS self
-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup
-- @param Ops.Auftrag#AUFTRAG Mission
-- @return #AWACS self
function AWACS:_StartSettings(FlightGroup,Mission)
  self:T(self.lid.."_StartSettings")
  
  -- called by AW OnafterFlightOnMission(...)
  
  local Mission = Mission -- Ops.Auftrag#AUFTRAG
  local AwacsFG = FlightGroup -- Ops.FlightGroup#FLIGHTGROUP
  
  -- Is this our Awacs mission?
  if self.AwacsMission:GetName() == Mission:GetName() then
    
    AwacsFG:SetDefaultRadio(self.Frequency,self.Modulation,false)
    AwacsFG:SwitchRadio(self.Frequency,self.Modulation)
    AwacsFG:SetDefaultAltitude(self.AwacsAngels*1000)
    AwacsFG:SetHomebase(self.Airbase)
    AwacsFG:SetDefaultCallsign(self.CallSign,self.CallSignNo)
    AwacsFG:SetDefaultROE(ENUMS.ROE.WeaponHold)
    AwacsFG:SetDefaultAlarmstate(AI.Option.Ground.val.ALARM_STATE.GREEN)
    AwacsFG:SetDefaultEPLRS(self.ModernEra)
    AwacsFG:SetDespawnAfterLanding()
    AwacsFG:SetFuelLowRTB(true)
    AwacsFG:SetFuelLowThreshold(20)
    
    local group = AwacsFG:GetGroup() -- Wrapper.Group#GROUP
    
    group:SetCommandInvisible(self.invisible)
    group:SetCommandImmortal(self.immortal)
    group:CommandSetCallsign(self.CallSign,self.CallSignNo,2)
    -- Non AWACS does not seem take AWACS CS in DCS Group
    --group:CommandSetCallsign(CALLSIGN.Aircraft.Pig,self.CallSignNo,2)
    
    AwacsFG:SetSRS(self.PathToSRS,self.Gender,self.Culture,self.Voice,self.Port,nil)
    self.callsigntxt = string.format("%s %d %d",AWACS.CallSignClear[self.CallSign],1,self.CallSignNo)
    
    local text = string.format("%s starting for AO %s control.",self.callsigntxt,self.OpsZone:GetName() or "AO")
    self:T(self.lid..text)
    
    AwacsFG:RadioTransmission(text,1,false)
    
    self.AwacsFG = AwacsFG 
    
    self:__CheckRadioQueue(10)
    
    if self.HasEscorts then
      --mission:SetRequiredEscorts(self.EscortNumber)
      self:_StartEscorts()
    end
    
    self.AwacsTimeStamp = timer.getTime()
    self.EscortsTimeStamp = timer.getTime()
    
  elseif self.ShiftChangeAwacsRequested and self.AwacsMissionReplacement and self.AwacsMissionReplacement:GetName() == Mission:GetName() then
    
    -- manage AWACS Replacement
    AwacsFG:SetDefaultRadio(self.Frequency,self.Modulation,false)
    AwacsFG:SwitchRadio(self.Frequency,self.Modulation)
    AwacsFG:SetDefaultAltitude(self.AwacsAngels*1000)
    AwacsFG:SetHomebase(self.Airbase)
    self.CallSignNo = self.CallSignNo+1
    AwacsFG:SetDefaultCallsign(self.CallSign,self.CallSignNo)
    AwacsFG:SetDefaultROE(ENUMS.ROE.WeaponHold)
    AwacsFG:SetDefaultAlarmstate(AI.Option.Ground.val.ALARM_STATE.GREEN)
    AwacsFG:SetDefaultEPLRS(self.ModernEra)
    AwacsFG:SetDespawnAfterLanding()
    AwacsFG:SetFuelLowRTB(true)
    AwacsFG:SetFuelLowThreshold(20)
    
    local group = AwacsFG:GetGroup() -- Wrapper.Group#GROUP
    
    group:SetCommandInvisible(self.invisible)
    group:SetCommandImmortal(self.immortal)
    group:CommandSetCallsign(self.CallSign,self.CallSignNo,2)
    -- Non AWACS does not seem take AWACS CS in DCS Group
    -- group:CommandSetCallsign(CALLSIGN.Aircraft.Pig,self.CallSignNo,2)
    
    AwacsFG:SetSRS(self.PathToSRS,self.Gender,self.Culture,self.Voice,self.Port,nil)
    self.callsigntxt = string.format("%s %d %d",AWACS.CallSignClear[self.CallSign],1,self.CallSignNo)
    
    local text = string.format("%s shift change for AO %s control.",self.callsigntxt,self.OpsZone:GetName() or "AO")
    self:T(self.lid..text)
    
    AwacsFG:RadioTransmission(text,1,false)
    
    self.AwacsFG = AwacsFG 
    
    --self:__CheckRadioQueue(10)
    
    if self.HasEscorts then
      --mission:SetRequiredEscorts(self.EscortNumber)
      self:_StartEscorts(true)
    end
    
    self.AwacsTimeStamp = timer.getTime()
    self.EscortsTimeStamp = timer.getTime()
    
  end
  return self
end

--- [Internal] Check if a group has checked in
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to check
-- @return #number ID
-- @return #boolean CheckedIn
function AWACS:_GetManagedGrpID(Group)
  self:T(self.lid.."_GetManagedGrpID for "..Group:GetName())
  local GID = 0
  local Outcome = false
  local nametocheck = Group:GetName()
  local managedgrps = self.ManagedGrps or {}
  for _,_managed in pairs (managedgrps) do
    local managed = _managed -- #AWACS.ManagedGroup
    if managed.GroupName == nametocheck then
      GID = managed.GID
      Outcome = true
    end
  end
  return GID, Outcome
end

--- [Internal] AWACS Get TTS compatible callsign
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to use
-- @return #string Callsign
function AWACS:_GetCallSign(Group)
  self:T(self.lid.."_GetCallSign")
  local callsign = ""
  local shortcallsign = Group:GetCallsign() -- e.g.Uzi11, but we want Uzi 1 1
  local callnumber = string.match(shortcallsign, "(%d+)$" )
  local callnumbermajor = string.char(string.byte(callnumber,1))
  local callnumberminor = string.char(string.byte(callnumber,2))
  callsign = string.gsub(shortcallsign,callnumber,"").." "..callnumbermajor.." "..callnumberminor
  self:T("Callsign for TTS = " .. callsign)
  return callsign
end

--- [Internal] AWACS Speak Picture AO/EWR entries
-- @param #AWACS self
-- @param #boolean AO If true this is for AO, else EWR
-- @param #string Callsign Callsign to address
-- @param #number GID GroupID for comms
-- @return #AWACS self
function AWACS:_CreatePicture(AO,Callsign,GID)
  self:T(self.lid.."_CreatePicture AO="..tostring(AO).." for "..Callsign.." GID "..GID)
  
  local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
  local group = managedgroup.Group -- Wrapper.Group#GROUP
  local groupcoord = group:GetCoordinate()
  
  local fifo = self.PictureAO -- Utilities.FiFo#FIFO
  local maxentries = self.maxspeakentries
  local counter = 0
  
  if not AO then 
    fifo = self.PictureEWR 
  end
  
  local entries = fifo:GetSize()
  
  if entries < maxentries then maxentries = entries end
  
  local text = "First group."
  local textScreen = text
  
  while counter < maxentries do
    counter = counter + 1
    local cluster = fifo:Pull() -- Ops.Intelligence#INTEL.Cluster
    self:T({cluster})
    if cluster and cluster.coordinate then
      local clustercoord = cluster.coordinate -- Core.Point#COORDINATE
      local refBRAA = clustercoord:ToStringBRA(groupcoord)
      text = text .. " "..refBRAA.."."
      textScreen = textScreen .." "..refBRAA..".\n"
      if counter < maxentries then
        text = text .. " Next group."
        textScreen = textScreen .. text .. "\n"
      end
    end
  end
  
  -- empty queue from leftovers
  fifo:Clear()
  
  local RadioEntry = {} -- #AWACS.RadioEntry
  RadioEntry.IsNew = true
  RadioEntry.TextTTS = text
  RadioEntry.TextScreen = textScreen
  RadioEntry.GroupID = GID
  RadioEntry.IsGroup = managedgroup.IsPlayer
  RadioEntry.ToScreen = managedgroup.IsPlayer 
  RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8
  
  self.RadioQueue:Push(RadioEntry)
  
  return self
end

--- [Internal] AWACS Speak Bogey Dope entries
-- @param #AWACS self
-- @param #string Callsign Callsign to address
-- @param #number GID GroupID for comms
-- @return #AWACS self
function AWACS:_CreateBogeyDope(Callsign,GID)
  self:T(self.lid.."_CreateBogeyDope for "..Callsign.." GID "..GID)
  
  local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
  local group = managedgroup.Group -- Wrapper.Group#GROUP
  local groupcoord = group:GetCoordinate()
  
  local fifo = self.ContactsAO -- Utilities.FiFo#FIFO
  local maxentries = self.maxspeakentries
  local counter = 0
  
  local entries = fifo:GetSize()
  
  if entries < maxentries then maxentries = entries end
  
  local sortedIDs = fifo:GetIDStackSorted() -- sort by distance
  
  while counter < maxentries do
    counter = counter + 1
    local cluster = fifo:PullByID(sortedIDs[counter]) -- Ops.Intelligence#INTEL.Contact
    self:T({cluster})
    if cluster and cluster.position then
      self:_AnnounceContact(cluster,false,group,true)
    end
  end
  
  -- empty queue from leftovers
  fifo:Clear()
  
  return self
end

--- [Internal] AWACS Menu for Picture
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to use
-- @return #AWACS self
function AWACS:_Picture(Group)
  self:T(self.lid.."_Picture")
  local text = "Picture WIP"
  local textScreen = text
  
  if not self.intel then
    -- no intel yet!
    text = string.format("%s. %s. Clear.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1")
    textScreen = text
    local RadioEntry = {} -- #AWACS.RadioEntry
    RadioEntry.IsNew = true
    RadioEntry.TextTTS = text
    RadioEntry.TextScreen = text
    RadioEntry.ToScreen = true
    RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8
    
    self.RadioQueue:Push(RadioEntry) 
    return self 
  end
  
  local GID, Outcome = self:_GetManagedGrpID(Group)

  if Outcome then
    -- Pilot is checked in
    -- get clusters from Intel
    local clustertable = self.intel:GetClusterTable()
    -- sort into buckets
    for _,_cluster in pairs(clustertable) do
      local cluster = _cluster -- Ops.Intelligence#INTEL.Cluster
      local coordVec2 = cluster.coordinate:GetVec2()
      
      if self.OpsZone:IsVec2InZone(coordVec2) then
        self.PictureAO:Push(cluster)
      elseif self.ControlZone:IsVec2InZone(coordVec2) then
        self.PictureEWR:Push(cluster)
      end
    end
    
    local clustersAO = self.PictureAO:GetSize()
    local clustersEWR = self.PictureEWR:GetSize()
    
    if clustersAO == 0 and clustersEWR == 0 then
      -- clear
      text = string.format("%s. %s. Clear.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1")
      textScreen = text
      local RadioEntry = {} -- #AWACS.RadioEntry
      RadioEntry.IsNew = true
      RadioEntry.TextTTS = text
      RadioEntry.TextScreen = textScreen
      RadioEntry.GroupID = GID
      RadioEntry.IsGroup = Outcome
      RadioEntry.ToScreen = true
      RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8
      
      self.RadioQueue:Push(RadioEntry)
    else
    
      if clustersAO > 0 then
        text = string.format("%s. %s. Picture A O. ",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1")
        textScreen = string.format("%s. %s. Picture AO. ",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1")
        if clustersAO == 1 then
          text = text .. "One group. "
          textScreen = textScreen .. "One group.\n"
        else
          text = text .. clustersAO .. " groups. "
          textScreen = textScreen .. clustersAO .. " groups.\n"
        end
        local RadioEntry = {} -- #AWACS.RadioEntry
        RadioEntry.IsNew = true
        RadioEntry.TextTTS = text
        RadioEntry.TextScreen = text
        RadioEntry.GroupID = GID
        RadioEntry.IsGroup = Outcome
        RadioEntry.ToScreen = true
        RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8      
        self.RadioQueue:Push(RadioEntry)
        
        self:_CreatePicture(true,self:_GetCallSign(Group) or "Unknown 1 1",GID)
      end
      
      if clustersEWR > 0 then
       text = string.format("%s. %s. Picture Early Warning. ",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1")
       textScreen = string.format("%s. %s. Picture EWR. ",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1")
       if clustersEWR == 1 then
          text = text .. "One group. "
          textScreen = textScreen .. "One group.\n"
        else
          text = text .. clustersEWR .. " groups. "
          textScreen = textScreen .. clustersAO .. " groups.\n"
        end
        local RadioEntry = {} -- #AWACS.RadioEntry
        RadioEntry.IsNew = true
        RadioEntry.TextTTS = text
        RadioEntry.TextScreen = textScreen
        RadioEntry.GroupID = GID
        RadioEntry.IsGroup = Outcome
        RadioEntry.ToScreen = true
        RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8     
        self.RadioQueue:Push(RadioEntry)
        
       self:_CreatePicture(false,self:_GetCallSign(Group) or "Unknown 1 1",GID)
      end
    end
    
  elseif self.AwacsFG then
    -- no, unknown
    text = string.format("%s. Negative %s. You are not checked in.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1")  
    local RadioEntry = {} -- #AWACS.RadioEntry
    RadioEntry.IsNew = true
    RadioEntry.TextTTS = text
    RadioEntry.TextScreen = text
    RadioEntry.GroupID = GID
    RadioEntry.IsGroup = Outcome
    RadioEntry.ToScreen = true
    RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8
    
    self.RadioQueue:Push(RadioEntry)
  end
  return self
end

--- [Internal] AWACS Menu for Bogey Dope
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to use
-- @return #AWACS self
function AWACS:_BogeyDope(Group)
  self:T(self.lid.."_BogeyDope")
  local text = "BogeyDope WIP"
  local textScreen = "BogeyDope WIP"
  
  if not self.intel then
    -- no intel yet!
    text = string.format("%s. %s. Clear.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1")
    textScreen = text
    local RadioEntry = {} -- #AWACS.RadioEntry
    RadioEntry.IsNew = true
    RadioEntry.TextTTS = text
    RadioEntry.TextScreen = textScreen
    RadioEntry.GroupID = 0
    RadioEntry.IsGroup = false
    RadioEntry.ToScreen = true
    RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8
    
    self.RadioQueue:Push(RadioEntry) 
    return self 
  end
  
  local GID, Outcome = self:_GetManagedGrpID(Group)

  if Outcome then
    -- Pilot is checked in
    
    local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
    local pilotgroup = managedgroup.Group
    local pilotcoord = managedgroup.Group:GetCoordinate()
    
    -- get contacts from Intel
    local contactstable = self.intel:GetContactTable()
    
    -- sort into buckets - AO only for bogey dope!
    for _,_contact in pairs(contactstable) do
      local cluster = _contact -- Ops.Intelligence#INTEL.Contact
      local coordVec2 = cluster.position:GetVec2()
      
      -- Get distance for sorting
      local dist = pilotcoord:Get2DDistance(cluster.position)
      
      if self.OpsZone:IsVec2InZone(coordVec2) then
        self.ContactsAO:Push(cluster,dist)
      end     
    end
    
    local contactsAO = self.ContactsAO:GetSize()
    
    if contactsAO == 0 then
      -- clear
      text = string.format("%s. %s. Clear.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1")
      textScreen = text
      local RadioEntry = {} -- #AWACS.RadioEntry
      RadioEntry.IsNew = true
      RadioEntry.TextTTS = text
      RadioEntry.TextScreen = textScreen
      RadioEntry.GroupID = GID
      RadioEntry.IsGroup = Outcome
      RadioEntry.ToScreen = Outcome
      RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8
      
      self.RadioQueue:Push(RadioEntry)
    else
    
      if contactsAO > 0 then
        text = string.format("%s. %s. Bogey Dope. ",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1")
        if contactsAO == 1 then
          text = text .. "One group. "
          textScreen = text .. "\n"
        else
          text = text .. contactsAO .. " groups. "
          textScreen = text .. contactsAO .. " groups.\n"
        end
        local RadioEntry = {} -- #AWACS.RadioEntry
        RadioEntry.IsNew = true
        RadioEntry.TextTTS = text
        RadioEntry.TextScreen = textScreen
        RadioEntry.GroupID = GID
        RadioEntry.IsGroup = Outcome
        RadioEntry.ToScreen = true
        RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8      
        self.RadioQueue:Push(RadioEntry)
        
        self:_CreateBogeyDope(self:_GetCallSign(Group) or "Unknown 1 1",GID)
      end
    end
    
  elseif self.AwacsFG then
    -- no, unknown
    text = string.format("%s. Negative %s. You are not checked in.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1")  
    local RadioEntry = {} -- #AWACS.RadioEntry
    RadioEntry.IsNew = true
    RadioEntry.TextTTS = text
    RadioEntry.TextScreen = text
    RadioEntry.GroupID = GID
    RadioEntry.IsGroup = Outcome
    RadioEntry.ToScreen = true
    RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8
    
    self.RadioQueue:Push(RadioEntry)
  end
  return self
end


--- [Internal] AWACS Menu for Declare
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to use
-- @return #AWACS self
function AWACS:_Declare(Group)
  self:T(self.lid.."_Declare")

  local GID, Outcome = self:_GetManagedGrpID(Group)
  local text = "Declare Not yet implemented"
  if Outcome then
    --[[ yes, known

    --]]
  elseif self.AwacsFG then
    -- no, unknown
    text = string.format("%s. Negative %s. You are not checked in.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1")
  end
  
  local RadioEntry = {} -- #AWACS.RadioEntry
  RadioEntry.IsNew = true
  RadioEntry.TextTTS = text
  RadioEntry.TextScreen = text
  RadioEntry.GroupID = GID
  RadioEntry.IsGroup = Outcome
  RadioEntry.ToScreen = true
  RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8
  
  self.RadioQueue:Push(RadioEntry)
  
  return self
end

--- [Internal] AWACS Menu for Showtask
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to use
-- @return #AWACS self
function AWACS:_Showtask(Group)
  self:T(self.lid.."_Showtask")

  local GID, Outcome = self:_GetManagedGrpID(Group)
  local text = "Showtask WIP"
  
  if Outcome then
   -- known group
   
   -- Do we have a task?
   local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
   
   if managedgroup.IsPlayer and self.TaskedCAPHuman:HasUniqueID(GID) then

    if managedgroup.CurrentTask >0 and self.AssignedTasks:HasUniqueID(managedgroup.CurrentTask) then
      -- get task structure
      local currenttask = self.AssignedTasks:ReadByID(managedgroup.CurrentTask) -- #AWACS.ManagedTask
      if currenttask then
        local status = currenttask.Status
        local targettype = currenttask.Target:GetCategory()
        local targetstatus = currenttask.Target:GetState()
        local ToDo = currenttask.ToDo
        local description = currenttask.ScreenText
        local callsign = self:_GetCallSign(Group)
        
        if self.debug then
          local taskreport = REPORT:New("AWACS Tasking Display")
          taskreport:Add("===============")
          taskreport:Add(string.format("Task for Callsign: %s",callsign))
          taskreport:Add(string.format("Task: %s with Status: %s",ToDo,status))
          taskreport:Add(string.format("Target of Type: %s",targettype))
          taskreport:Add(string.format("Target in State: %s",targetstatus))
          taskreport:Add("===============")
          self:I(taskreport:Text())
        end
        
        MESSAGE:New(description,30,"AWACS",true):ToGroup(Group)
        
      end
    end
   end
   
  elseif self.AwacsFG then
    -- no, unknown
    text = string.format("%s. Negative %s. You are not checked in.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1")
  
    local RadioEntry = {} -- #AWACS.RadioEntry
    RadioEntry.IsNew = true
    RadioEntry.TextTTS = text
    RadioEntry.TextScreen = text
    RadioEntry.GroupID = GID
    RadioEntry.IsGroup = Outcome
    RadioEntry.ToScreen = true
    RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8
    
    self.RadioQueue:Push(RadioEntry)
  end
  return self
end

--- [Internal] AWACS Menu for Check in
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to use
-- @return #AWACS self
function AWACS:_CheckIn(Group)
  self:I(self.lid.."_CheckIn "..Group:GetName())
  -- check if already known
  local GID, Outcome = self:_GetManagedGrpID(Group)
  local text = ""
  if not Outcome then
    self.ManagedGrpID = self.ManagedGrpID + 1
    local managedgroup = {} -- #AWACS.ManagedGroup
      managedgroup.Group = Group
      managedgroup.GroupName = Group:GetName()
      managedgroup.IsPlayer = true
      managedgroup.IsAI = false
      managedgroup.CallSign = self:_GetCallSign(Group) or "Unknown 1 1"
      managedgroup.CurrentTask = 0
      managedgroup.HasAssignedTask = false
      managedgroup.GID = self.ManagedGrpID
      GID = managedgroup.GID
    self.ManagedGrps[self.ManagedGrpID]=managedgroup
    text = string.format("%s. Copy %s. Await tasking.",self.callsigntxt,managedgroup.CallSign)
    self:__CheckedIn(1,managedgroup.GID)
    self:__AssignAnchor(5,managedgroup.GID)
  elseif self.AwacsFG then
    text = string.format("%s. Negative %s. You are already checked in.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1")
  end
  
  local RadioEntry = {} -- #AWACS.RadioEntry
  RadioEntry.IsNew = true
  RadioEntry.TextTTS = text
  RadioEntry.TextScreen = text
  RadioEntry.GroupID = GID
  RadioEntry.IsGroup = true
  RadioEntry.ToScreen = true
  RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8
  
  self.RadioQueue:Push(RadioEntry)
  
  return self
end

--- [Internal] AWACS Menu for CheckInAI
-- @param #AWACS self
-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup to use
-- @param Wrapper.Group#GROUP Group Group to use
-- @param #number AuftragsNr Ops.Auftrag#AUFTRAG.auftragsnummer
-- @return #AWACS self
function AWACS:_CheckInAI(FlightGroup,Group,AuftragsNr)
  self:I(self.lid.."_CheckInAI "..Group:GetName())
  -- check if already known
  local GID, Outcome = self:_GetManagedGrpID(Group)
  local text = ""
  if not Outcome then
    self.ManagedGrpID = self.ManagedGrpID + 1
    local managedgroup = {} -- #AWACS.ManagedGroup
      managedgroup.Group = Group
      managedgroup.GroupName = Group:GetName()
      managedgroup.FlightGroup = FlightGroup
      managedgroup.IsPlayer = false
      managedgroup.IsAI = true
      managedgroup.CallSign = self:_GetCallSign(Group) or "Unknown 1 1"
      managedgroup.CurrentTask = AuftragsNr
      managedgroup.HasAssignedTask = false
      managedgroup.GID = self.ManagedGrpID
    
    -- SRS voice for CAP
    --FlightGroup:SetSRS(PathToSRS,Gender,Culture,Voice,Port,PathToGoogleKey)
    
    FlightGroup:SetDefaultRadio(self.Frequency,self.Modulation,false)
    FlightGroup:SwitchRadio(self.Frequency,self.Modulation)
    
    FlightGroup:SetSRS(self.PathToSRS,self.CAPGender,self.CAPCulture,self.CAPVoice,self.Port,nil)
      
    self.ManagedGrps[self.ManagedGrpID]=managedgroup
    text = string.format("%s. Copy %s. Await tasking.",self.callsigntxt,managedgroup.CallSign)
    self:__CheckedIn(1,managedgroup.GID)
    self:__AssignAnchor(5,managedgroup.GID)
  else
    text = string.format("%s. Negative %s. You are already checked in.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1")
  end
  
  local RadioEntry = {} -- #AWACS.RadioEntry
  RadioEntry.IsNew = true
  RadioEntry.TextTTS = text
  RadioEntry.ToScreen = false
  RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8
  
  self.RadioQueue:Push(RadioEntry)
  
  return self
end

--- [Internal] AWACS Menu for Check Out
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to use
-- @return #AWACS self
function AWACS:_CheckOut(Group)
  self:I(self.lid.."_CheckOut")

  -- check if already known
  local GID, Outcome = self:_GetManagedGrpID(Group)
  local text = ""
  if Outcome then
    -- yes, known
    text = string.format("%s. Copy %s. Have a safe flight home.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1")
    -- grab some data before we nil the entry
    local AnchorAssigned = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
    local Stack = AnchorAssigned.AnchorStackNo
    local Angels = AnchorAssigned.AnchorStackAngels
    self.ManagedGrps[GID] = nil
    self:__CheckedOut(1,GID,Stack,Angels)
  else
    -- no, unknown
    text = string.format("%s. Negative %s. You are not checked in.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1")
  end
  
  local RadioEntry = {} -- #AWACS.RadioEntry
  RadioEntry.IsNew = true
  RadioEntry.TextTTS = text
  RadioEntry.ToScreen = true
  RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8
  
  self.RadioQueue:Push(RadioEntry)
  
  return self
end

--- [Internal] AWACS set client menus
-- @param #AWACS self
-- @return #AWACS self
function AWACS:_SetClientMenus()
  self:T(self.lid.."_SetClientMenus")
  local clientset = self.clientset -- Core.Set#SET_GROUP
  local aliveset = clientset:GetAliveSet() -- #table of #GROUP objects
  --local aliveobjects = aliveset:GetSetObjects() or {}
  local clientmenus = {} 
  for _,_group in pairs(aliveset) do
    -- go through set and build the menu
    local grp = _group -- Wrapper.Group#GROUP
    if self.MenuStrict then
      -- check if pilot has checked in
      if grp and grp:IsAlive() and grp:GetUnit(1):IsPlayer() then
        local GID, checkedin = self:_GetManagedGrpID(grp)
        if checkedin then
          -- full menu minus checkin
          local hasclientmenu = self.clientmenus[grp:GetName()] -- #AWACS.MenuStructure
          --local checkinmenu = hasclientmenu.checkin -- Core.Menu#MENU_GROUP_COMMAND 
          --checkinmenu:Remove(nil,grp:GetName())
          local basemenu = hasclientmenu.basemenu -- Core.Menu#MENU_GROUP
          --local basemenu = MENU_GROUP:New(grp.Name,nil)
          basemenu:RemoveSubMenus()
          local picture = MENU_GROUP_COMMAND:New(grp,"Picture",basemenu,self._Picture,self,grp)
          local bogeydope = MENU_GROUP_COMMAND:New(grp,"Bogey Dope",basemenu,self._BogeyDope,self,grp)
          local declare = MENU_GROUP_COMMAND:New(grp,"Declare",basemenu,self._Declare,self,grp)
          local showtask = MENU_GROUP_COMMAND:New(grp,"Showtask",basemenu,self._Showtask,self,grp)
          local checkout = MENU_GROUP_COMMAND:New(grp,"Check Out",basemenu,self._CheckOut,self,grp):Refresh()
          clientmenus[grp:GetName()] = { -- #AWACS.MenuStructure
            groupname =  grp:GetName(),
            menuset = true,
            basemenu = basemenu,
            --checkin = checkin,
            checkout= checkout,
            picture = picture,
            bogeydope = bogeydope,
            declare = declare,
            showtask = showtask,
          }
        elseif not clientmenus[grp:GetName()] then
          -- check in only
          local basemenu = MENU_GROUP:New(grp,self.Name,nil)
          local checkin = MENU_GROUP_COMMAND:New(grp,"Check In",basemenu,self._CheckIn,self,grp)
          checkin:SetTag(grp:GetName())
          checkin:Refresh()
          clientmenus[grp:GetName()] = { -- #AWACS.MenuStructure
            groupname =  grp:GetName(),
            menuset = true,
            basemenu = basemenu,
            checkin = checkin,
            --checkout= checkout,
            --picture = picture,
            --bogeydope = bogeydope,
            --declare = declare,
            --showtask = showtask,
          }
        end
      end
    else
      if grp and grp:IsAlive() and grp:GetUnit(1):IsPlayer() and not clientmenus[grp:GetName()] then
        local basemenu = MENU_COALITION:New(self.coalition,self.Name,nil)
        local picture = MENU_COALITION_COMMAND:New(self.coalition,"Picture",basemenu,self._Picture,self,grp)
        local bogeydope = MENU_COALITION_COMMAND:New(self.coalition,"Bogey Dope",basemenu,self._BogeyDope,self,grp)
        local declare = MENU_COALITION_COMMAND:New(self.coalition,"Declare",basemenu,self._Declare,self,grp)
        local showtask = MENU_COALITION_COMMAND:New(self.coalition,"Showtask",basemenu,self._Showtask,self,grp)
        local checkin = MENU_COALITION_COMMAND:New(self.coalition,"Check In",basemenu,self._CheckIn,self,grp)
        local checkout = MENU_COALITION_COMMAND:New(self.coalition,"Check Out",basemenu,self._CheckOut,self,grp):Refresh()
        clientmenus[grp:GetName()] = { -- #AWACS.MenuStructure
          groupname =  grp:GetName(),
          menuset = true,
          basemenu = basemenu,
          checkin = checkin,
          checkout= checkout,
          picture = picture,
          bogeydope = bogeydope,
          declare = declare,
          showtask = showtask,
        }
      end
    end
  end
  self.clientmenus = clientmenus  
  return self
end

--- [Internal] AWACS Create a new Anchor Stack
-- @param #AWACS self
-- @return #boolean success
-- @return #nunber AnchorStackNo
function AWACS:_CreateAnchorStack()
  self:T(self.lid.."_CreateAnchorStack")
  local stackscreated = self.AnchorStacks:GetSize()
  if stackscreated == self.AnchorMaxAnchors  then
    -- only create self.AnchorMaxAnchors Anchors
    return false, 0
  end
  local AnchorStackOne = {} -- #AWACS.AnchorData
  AnchorStackOne.AnchorBaseAngels = self.AnchorBaseAngels
  AnchorStackOne.Anchors = FIFO:New() -- Utilities.FiFo#FIFO
  AnchorStackOne.AnchorAssignedID = FIFO:New() -- Utilities.FiFo#FIFO
  local newname = ""
  for i=1,self.AnchorMaxStacks do
    AnchorStackOne.Anchors:Push((i-1)*self.AnchorStackDistance+self.AnchorBaseAngels)
  end
  if self.debug then
    --AnchorStackOne.Anchors:Flush()
  end
  if stackscreated == 0 then
    AnchorStackOne.AnchorZone = self.AnchorZone
    AnchorStackOne.AnchorZoneCoordinate = self.AnchorZone:GetCoordinate()
    AnchorStackOne.AnchorZoneCoordinateText = self.AnchorZone:GetCoordinate():ToStringLLDDM()
    --push to AnchorStacks
    self.AnchorStacks:Push(AnchorStackOne,"One")
  else
    local newsubname = AWACS.AnchorNames[stackscreated+1] or tostring(stackscreated+1)
    newname = self.AnchorZone:GetName() .. "-"..newsubname
    local anchorbasecoord = self.OpsZone:GetCoordinate() -- Core.Point#COORDINATE
    -- OpsZone can be Polygon, so use distance to AnchorZone as radius
    local anchorradius = anchorbasecoord:Get2DDistance(self.AnchorZone:GetCoordinate())
    --local anchorradius = self.OpsZone:GetRadius() -- #number
    --anchorradius = anchorradius + self.AnchorZone:GetRadius()
    local angel = self.AnchorZone:GetCoordinate():GetAngleDegrees(self.OpsZone:GetVec3())
    self:T("Angel Radians= " .. angel)
    local turn = math.fmod(self.AnchorTurn*stackscreated,360) -- #number
    if self.AnchorTurn < 0 then turn = -turn end
    local newanchorbasecoord = anchorbasecoord:Translate(anchorradius,turn+angel) -- Core.Point#COORDINATE
    AnchorStackOne.AnchorZone = ZONE_RADIUS:New(newname, newanchorbasecoord:GetVec2(), self.AnchorZone:GetRadius())
    AnchorStackOne.AnchorZoneCoordinate = newanchorbasecoord
    AnchorStackOne.AnchorZoneCoordinateText = newanchorbasecoord:ToStringLLDDM()
    --push to AnchorStacks
    self.AnchorStacks:Push(AnchorStackOne,newname)
  end

  if self.debug then
    --self.AnchorStacks:Flush()
    AnchorStackOne.AnchorZone:DrawZone(-1,{0,0,1},1,{0,0,1},0.2,5,true)
    MARKER:New(AnchorStackOne.AnchorZone:GetCoordinate(),"Anchor Zone: "..newname):ToAll()
  end

  return true,self.AnchorStacks:GetSize()
  
end

--- [Internal] AWACS get free anchor stack for managed groups
-- @param #AWACS self
-- @return #number AnchorStackNo
-- @return #boolean free 
function AWACS:_GetFreeAnchorStack()
  self:T(self.lid.."_GetFreeAnchorStack")
  local AnchorStackNo, Free = 0, false
  --return AnchorStackNo, Free
  local availablestacks = self.AnchorStacks:GetPointerStack() or {} -- #table
  for _id,_entry in pairs(availablestacks) do
    local entry = _entry -- Utilities.FiFo#FIFO.IDEntry
    local data = entry.data -- #AWACS.AnchorData
    if data.Anchors:IsNotEmpty() then
      AnchorStackNo = _id
      Free = true
      break
    end
  end
  -- TODO - if extension of anchor stacks to max, send AI home
  if not Free then
    -- try to create another stack
    local created, number = self:_CreateAnchorStack()
    if created then
      -- we could create a new one - phew!
      self:_GetFreeAnchorStack()
    end
  end
  return AnchorStackNo, Free
end

--- [Internal] AWACS Assign Anchor Position to a Group
-- @param #AWACS self
-- @return #number GID Managed Group ID
-- @return #AWACS self
function AWACS:_AssignAnchorToID(GID)
  self:T(self.lid.."_AssignAnchorToID")
  local AnchorStackNo, Free = self:_GetFreeAnchorStack()
  if Free then
    -- get the Anchor from the stack
    local Anchor = self.AnchorStacks:PullByPointer(AnchorStackNo) -- #AWACS.AnchorData
    -- pull one free angels
    local freeangels = Anchor.Anchors:Pull()
    -- push GID on anchor
    Anchor.AnchorAssignedID:Push(GID)
    if self.debug then
      --Anchor.AnchorAssignedID:Flush()
      --Anchor.Anchors:Flush()
    end
    -- push back to AnchorStacks
    self.AnchorStacks:Push(Anchor)
    self:T({Anchor,freeangels})
    self:__AssignedAnchor(5,GID,Anchor,AnchorStackNo,freeangels)
  else
    self:E(self.lid .. "Cannot assing free anchor stack to GID ".. GID)
    -- try again ...
    self:__AssignAnchor(10,GID)
  end
  return self
end

--- [Internal] Remove GID (group) from Anchor Stack
-- @param #AWACS self
-- @param #AWACS.ManagedGroup.GID ID
-- @param #number AnchorStackNo
-- @param #number Angels
-- @return #AWACS self
function AWACS:_RemoveIDFromAnchor(GID,AnchorStackNo,Angels)
  self:I(self.lid.."_RemoveIDFromAnchor for GID="..GID.." Stack="..AnchorStackNo.." Angels="..Angels)
  -- pull correct anchor
  local AnchorStackNo = AnchorStackNo or 1
  local Anchor = self.AnchorStacks:PullByPointer(AnchorStackNo) -- #AWACS.AnchorData
  -- pull GID from stack
  local removedID = Anchor.AnchorAssignedID:PullByID(GID)
  -- push free angels to stack
  Anchor.Anchors:Push(Angels)
  -- push back AnchorStack
  self.AnchorStacks:Push(Anchor)
  return self
end

--- [Internal] Start INTEL detection when we reach the AWACS Orbit Zone
-- @param #AWACS self
-- @param Wrapper.Group#GROUP awacs
-- @return #AWACS self
function AWACS:_StartIntel(awacs)
  self:T(self.lid.."_StartIntel")
  
  self.DetectionSet:AddGroup(awacs)
  
  local intel = INTEL:New(self.DetectionSet,self.coalition,self.callsigntxt)
  --intel:SetVerbosity(2)
  intel:SetClusterAnalysis(true,self.debug)
  if self.NoHelos then
    intel:SetFilterCategory({Unit.Category.AIRPLANE})
  else
    intel:SetFilterCategory({Unit.Category.AIRPLANE,Unit.Category.HELICOPTER})
  end
  
  -- Callbacks
  local function NewCluster(Cluster)
    self:__NewCluster(5,Cluster)
  end 
  function intel:OnAfterNewCluster(From,Event,To,Cluster)
    NewCluster(Cluster)
  end
  
  local function NewContact(Contact)
    self:__NewContact(5,Contact)
  end 
  function intel:OnAfterNewContact(From,Event,To,Contact)
   NewContact(Contact)
  end
  
  local function LostContact(Contact)
    self:__LostContact(5,Contact)
  end 
  function intel:OnAfterLostContact(From,Event,To,Contact)
    LostContact(Contact)
  end
  
  local function LostCluster(Cluster,Mission)
    self:__LostCluster(5,Cluster,Mission)
  end
  function intel:OnAfterLostCluster(From,Event,To,Cluster,Mission)
    LostCluster(Cluster,Mission)
  end
  
  intel:__Start(2)
  
  self.intel = intel -- Ops.Intelligence#INTEL
  return self
end

--- [Internal] Get blurred size of group or cluster
-- @param #AWACS self
-- @param #number size
-- @return #number adjusted size
function AWACS:_GetBlurredSize(size)
  self:T(self.lid.."_GetBlurredSize")
  local threatsize = 0
  local blur = self.RadarBlur
  local blurmin = 100 - blur
  local blurmax = 100 + blur
  local actblur = math.random(blurmin,blurmax) / 100
  threatsize = math.floor(size * actblur)
  return threatsize
end

--- [Internal] Get threat level as clear test
-- @param #AWACS self
-- @param #number threatlevel
-- @return #string threattext
function AWACS:_GetThreatLevelText(threatlevel)
  self:T(self.lid.."_GetThreatLevelText")
  local threattext = "GREEN"
  if threatlevel <= AWACS.THREATLEVEL.GREEN then
   threattext = "GREEN"
  elseif threatlevel <= AWACS.THREATLEVEL.AMBER then
   threattext = "AMBER"
  else
    threattext = "RED"
  end
  return threattext
end

--- [Internal] Get BRA text for TTS
-- @param #AWACS self
-- @param Core.Point#COORDINATE clustercoordinate
-- @return #string BRAText
function AWACS:_GetBRAfromBullsOrAO(clustercoordinate)
  self:T(self.lid.."__GetBRAfromBullsOrAO")
  local refcoord = self.AOCoordinate -- Core.Point#COORDINATE
  local BRAText = ""
  if not self.UseBullsAO then
    -- get BR from AO
    BRAText = "AO "..refcoord:ToStringBR(clustercoordinate)
  else
    -- get BR from Bulls
    BRAText = clustercoordinate:ToStringBULLS(self.coalition)
  end
  return BRAText
end

--- [Internal] Register Task for Group by GID
-- @param #AWACS self
-- @param #number GroupID ManagedGroup ID
-- @param #AWACS.TaskDescription Description Short Description Task Type
-- @param #string ScreenText Long task description for screen output
-- @param #table Object Object for Ops.Target#TARGET assignment
-- @return #AWACS self
function AWACS:_CreateTaskForGroup(GroupID,Description,ScreenText,Object)
   self:I(self.lid.."_CreateTaskForGroup "..GroupID .." Description: "..Description)
   local managedgroup = self.ManagedGrps[GroupID] -- #AWACS.ManagedGroup
   local task = {} -- #AWACS.ManagedTask
   self.ManagedTaskID = self.ManagedTaskID + 1
   task.TID = self.ManagedTaskID
   task.AssignedGroupID = GroupID
   task.Status = AWACS.TaskStatus.ASSIGNED
   task.ToDo = Description
   task.Target = TARGET:New(Object)
   task.ScreenText = ScreenText
   if Description == AWACS.TaskDescription.ANCHOR or Description == AWACS.TaskDescription.REANCHOR then
    task.Target.Type = TARGET.ObjectType.ZONE
   end
   self.AssignedTasks:Push(task,task.TID)
   --self:I({task})
   managedgroup.HasAssignedTask = true
   managedgroup.CurrentTask = task.TID
   self.ManagedGrps[GroupID] = managedgroup
   --if managedgroup.IsPlayer then
    --self.TaskedCAPHuman:Push(GroupID)
   --elseif managedgroup.IsAI then
    --self.TaskedCAPAI:Push(GroupID)
   --end
   return self
end 

--- [Internal] Read registered Task for Group by its ID
-- @param #AWACS self
-- @param #number GroupID ManagedGroup ID
-- @return #AWACS.ManagedTask Task or nil if n/e
function AWACS:_ReadAssignedTaskFromGID(GroupID)
   self:T(self.lid.."_GetAssignedTaskFromGID "..GroupID)
   local managedgroup = self.ManagedGrps[GroupID] -- #AWACS.ManagedGroup
   if managedgroup and managedgroup.HasAssignedTask then
     local TaskID = managedgroup.CurrentTask
     if self.AssignedTasks:HasUniqueID(TaskID) then
      return self.AssignedTasks:ReadByID(TaskID)
     end
   end
   return nil
end

--- [Internal] Read assigned Group from a TaskID
-- @param #AWACS self
-- @param #number TaskID ManagedTask ID
-- @return #AWACS.ManagedGroup Group structure or nil if n/e
function AWACS:_ReadAssignedGroupFromTID(TaskID)
   self:T(self.lid.."_ReadAssignedGroupFromTID "..TaskID)
   if self.AssignedTasks:HasUniqueID(TaskID) then
    local task = self.AssignedTasks:ReadByID(TaskID) -- #AWACS.ManagedTask
    if task and task.AssignedGroupID and task.AssignedGroupID > 0 then
      return self.ManagedGrps[task.AssignedGroupID]
    end
   end
   return nil
end

--- [Internal] Create new idle task from contact to pick up later
-- @param #AWACS self
-- @param #string Description Task Type
-- @param #table Object Object of TARGET
-- @param Ops.Intelligence#INTEL.Contact Contact
-- @return #AWACS self
function AWACS:_CreateIdleTaskForContact(Description,Object,Contact)
   self:T(self.lid.."_CreateIdleTaskForContact "..Description)
   local task = {} -- #AWACS.ManagedTask
   self.ManagedTaskID = self.ManagedTaskID + 1
   task.TID = self.ManagedTaskID
   task.AssignedGroupID = 0
   task.Status = AWACS.TaskStatus.IDLE
   task.ToDo = Description
   task.Target = TARGET:New(Object)
   task.Contact = Contact
   task.IsContact = true
   task.ScreenText = Description
   if Description == AWACS.TaskDescription.ANCHOR or Description == AWACS.TaskDescription.REANCHOR then
    task.Target.Type = TARGET.ObjectType.ZONE
   end
   self.OpenTasks:Push(task,task.TID)
   return self
end  

--- [Internal] Create new idle task from cluster to pick up later
-- @param #AWACS self
-- @param #string Description Task Type
-- @param #table Object Object of TARGET
-- @param Ops.Intelligence#INTEL.Cluster Cluster
-- @return #AWACS self
function AWACS:_CreateIdleTaskForCluster(Description,Object,Cluster)
   self:T(self.lid.."_CreateIdleTaskForCluster "..Description)
   local task = {} -- #AWACS.ManagedTask
   self.ManagedTaskID = self.ManagedTaskID + 1
   task.TID = self.ManagedTaskID
   task.AssignedGroupID = 0
   task.Status = AWACS.TaskStatus.IDLE
   task.ToDo = Description
   --self:T({Cluster.Contacts})
   --task.Target = TARGET:New(Cluster.Contacts[1])
   task.Target = TARGET:New(self.intel:GetClusterCoordinate(Cluster))
   task.Cluster = Cluster
   task.IsCluster = true
   task.ScreenText = Description
   if Description == AWACS.TaskDescription.ANCHOR or Description == AWACS.TaskDescription.REANCHOR then
    task.Target.Type = TARGET.ObjectType.ZONE
   end
   self.OpenTasks:Push(task,task.TID)
   return self
end 

--- [Internal] Create radio entry to tell players that CAP is on station in Anchor
-- @param #AWACS self
-- @param #number GID Group ID 
-- @return #AWACS self
function AWACS:_MessageAIReadyForTasking(GID)
  self:I(self.lid.."_MessageAIReadyForTasking")
  -- obtain group details
  if GID >0  and self.ManagedGrps[GID] then
    local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
    local GFCallsign = self:_GetCallSign(managedgroup.Group)
    local TextTTS = string.format("%s. %s. On station over anchor %d at angels  %d. Ready for tasking.",GFCallsign,self.callsigntxt,managedgroup.AnchorStackNo or 1,managedgroup.AnchorStackAngels or 25)
    local RadioEntry = {} -- #AWACS.RadioEntry
    RadioEntry.TextTTS = TextTTS
    RadioEntry.TextScreen = ""
    RadioEntry.IsNew = true
    RadioEntry.IsGroup = false
    RadioEntry.GroupID = GID
    RadioEntry.Duration = STTS.getSpeechTime(TextTTS,1.2,false)+2 or 16
    RadioEntry.ToScreen = false
    RadioEntry.FromAI = true
    
    self.RadioQueue:Push(RadioEntry)
  end
  return self
end

--- [Internal] Check available tasks and status
-- @param #AWACS self
-- @return #AWACS self
function AWACS:_CheckTaskQueue()
  self:I(self.lid.."_CheckTaskQueue")
  local opentasks = 0
  local assignedtasks = 0
  if self.OpenTasks:IsNotEmpty() then
    opentasks = self.OpenTasks:GetSize()
    self:I("Open Tasks: " .. opentasks)
    local taskstack = self.OpenTasks:GetPointerStack()
    for _id,_entry in pairs(taskstack) do
      local data = _entry -- Utilities.FiFo#FIFO.IDEntry
      local entry = data.data -- #AWACS.ManagedTask
      local target = entry.Target -- Ops.Target#TARGET
      local description = entry.ToDo
      self:I("ToDo = "..description)
      if description == AWACS.TaskDescription.ANCHOR or description == AWACS.TaskDescription.REANCHOR then
        self:I("Open Tasks ANCHOR/REANCHOR")
        -- see if we have reached the anchor zone
        local managedgroup = self.ManagedGrps[entry.AssignedGroupID] -- #AWACS.ManagedGroup
        if managedgroup then
          local group = managedgroup.Group
          local groupcoord = group:GetCoordinate()
          local zone = target:GetObject() -- Core.Zone#ZONE
          self:T({zone})
          if group:IsInZone(zone) then
            self:I("Open Tasks ANCHOR/REANCHOR success for GroupID "..entry.AssignedGroupID)
            -- made it
            target:Stop()
            -- pull task from OpenTasks
            self.OpenTasks:PullByPointer(_id)
            --[[ add group to idle stack
            if managedgroup.IsAI then
              self.TaskedCAPAI:PullByPointer(entry.AssignedGroupID)
              self.CAPIdleAI:Push(entry.AssignedGroupID)
            elseif managedgroup.IsPlayer then
              self.TaskedCAPHuman:PullByPointer(entry.AssignedGroupID)
              self.CAPIdleHuman:Push(entry.AssignedGroupID)
            end
            --]]
          else
            -- not there yet
          end
        end
      elseif description == AWACS.TaskDescription.INTERCEPT then
        -- TODO
      elseif description == AWACS.TaskDescription.RTB then
       -- TODO
      end
    end
  end
  
  if self.AssignedTasks:IsNotEmpty() then
    opentasks = self.AssignedTasks:GetSize()
    self:I("Assigned Tasks: " .. opentasks)
    local taskstack = self.AssignedTasks:GetPointerStack()
    for _id,_entry in pairs(taskstack) do
      local data = _entry -- Utilities.FiFo#FIFO.IDEntry
      local entry = data.data -- #AWACS.ManagedTask
      local target = entry.Target -- Ops.Target#TARGET
      local description = entry.ToDo
      self:I("ToDo = "..description)
      if description == AWACS.TaskDescription.ANCHOR or description == AWACS.TaskDescription.REANCHOR then
        self:I("Open Tasks ANCHOR/REANCHOR")
        -- see if we have reached the anchor zone
        local managedgroup = self.ManagedGrps[entry.AssignedGroupID] -- #AWACS.ManagedGroup
        if managedgroup then
          local group = managedgroup.Group
          local groupcoord = group:GetCoordinate()
          local zone = target:GetObject() -- Core.Zone#ZONE
          self:T({zone})
          if group:IsInZone(zone) then
            self:I("Open Tasks ANCHOR/REANCHOR success for GroupID "..entry.AssignedGroupID)
            -- made it
            target:Stop()
            -- pull task from OpenTasks
            self.AssignedTasks:PullByPointer(_id)
            -- add group to idle stack
            if managedgroup.IsAI then
              -- message AI on station
              self:_MessageAIReadyForTasking(managedgroup.GID)
            elseif managedgroup.IsPlayer then
              --self.TaskedCAPHuman:PullByPointer(entry.AssignedGroupID)
              ---self.CAPIdleHuman:Push(entry.AssignedGroupID)
            end
          else
            -- not there yet
          end
        end
      elseif description == AWACS.TaskDescription.INTERCEPT then
        -- TODO
      elseif description == AWACS.TaskDescription.RTB then
       -- TODO
      end
    end
  end
  
  return self
end

--- [Internal] Write stats to log
-- @param #AWACS self
-- @return #AWACS self
function AWACS:_LogStatistics()
  self:T(self.lid.."_LogStatistics")
  return self 
end

--- [User] Add another AirWing for CAP Flights under management
-- @param #AWACS self
-- @param Ops.AirWing#AIRWING AirWing The AirWing to (also) obtain CAP flights from
-- @return #AWACS self
function AWACS:AddCAPAirWing(AirWing)
  self:I(self.lid.."AddCAPAirWing")
  if AirWing then
    -- TODO - Test Install callback
    -- TODO - add distance to AO as UniqueID
    AirWing:SetUsingOpsAwacs(self)
    self.CAPAirwings:Push(AirWing)
  end
  return self
end

--- Recruit assets for a given TARGET.
-- @param #AWACS self
-- @param #string MissionType Mission Type.
-- @param #number NassetsMin Min number of required assets.
-- @param #number NassetsMax Max number of required assets.
-- @return #boolean If `true` enough assets could be recruited.
-- @return #table Assets that have been recruited from all legions.
-- @return #table Legions that have recruited assets.
function AWACS:RecruitAssets(MissionType, NassetsMin, NassetsMax)

  -- Cohorts.
  local Cohorts={}
  local AWFiFo = self.CAPAirwings -- Utilities.FiFo#FIFO
  local AWStack = AWFiFo:GetPointerStack()
  local AirWingList = {}
  
  for _ID,_AWID in pairs(AWStack) do
    local SubAW = self.CAPAirwings:ReadByPointer(_ID)
    if SubAW then
      table.insert(AirWingList,SubAW)
    end
  end
  
  for _,_legion in pairs(AirWingList) do
    local legion=_legion --Ops.Legion#LEGION
    
    -- Check that runway is operational
    local Runway=legion:IsAirwing() and legion:IsRunwayOperational() or true
    
    if legion:IsRunning() and Runway then    
    
      -- Loops over cohorts.
      for _,_cohort in pairs(legion.cohorts) do
        local cohort=_cohort --Ops.Cohort#COHORT
        table.insert(Cohorts, cohort)
      end
      
    end
  end  

  -- Target position.
  local TargetVec2=self.OpsZone:GetVec2()
  
  -- Recruit assets.
  local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, MissionType, nil, NassetsMin, NassetsMax, TargetVec2)

  return recruited, assets, legions
end

--- [Internal] Announce a new contact
-- @param #AWACS self
-- @param Ops.Intelligence#INTEL.Contact Contact
-- @param #boolean IsNew
-- @param Wrapper.Group#GROUP Group Announce to Group if not nil
-- @param #boolean IsBogeyDope If true, this is a bogey dope announcement
-- @return #AWACS self
function AWACS:_AnnounceContact(Contact,IsNew,Group,IsBogeyDope)
  self:T(self.lid.."_AnnounceContact")
  -- do we have a group to talk to?
  local isGroup = false
  local GID = 0
  local grpcallsign = "Unknown 1 1"
  if Group and Group:IsAlive() then
    GID, isGroup = self:_GetManagedGrpID(Group)
    self:T("GID="..GID.." CheckedIn = "..tostring(isGroup))
    grpcallsign = self:_GetCallSign(Group) or "Unknown 1 1"
  end
  local contact = Contact -- Ops.Intelligence#INTEL.Contact
  local intel = self.intel -- Ops.Intelligence#INTEL
  local size = contact.group:CountAliveUnits()
  local threatsize = self:_GetBlurredSize(size)
  local threatlevel = contact.threatlevel
  local threattext = self:_GetThreatLevelText(threatlevel)
  local clustercoordinate = contact.position

  local BRAfromBulls = self:_GetBRAfromBullsOrAO(clustercoordinate)
  if isGroup then
    BRAfromBulls = clustercoordinate:ToStringBRA(Group:GetCoordinate())
  end
  
  local Warnlevel = "Early Warning."

  if self.OpsZone:IsVec2InZone(clustercoordinate:GetVec2()) and not IsBogeyDope then
    Warnlevel = "Warning."
  elseif IsBogeyDope then
   Warnlevel = ""
  end
  
  if IsNew then
    Warnlevel = Warnlevel .. " New"
  end
  
  Warnlevel = string.format("%s %s", Warnlevel, threattext)
  
  if isGroup then
    Warnlevel = string.format("%s. %s",grpcallsign,Warnlevel)
  end
  
  -- TTS
  local TextTTS = string.format("%s. %s %d ship contact. %s",self.callsigntxt,Warnlevel,threatsize,BRAfromBulls)
  
  -- TextOutput
  local TextScreen = string.format("%s. %s %d ship contact.\n%s\nThreatlevel %s",self.callsigntxt,Warnlevel,threatsize,BRAfromBulls,threattext)
  
  local RadioEntry = {} -- #AWACS.RadioEntry
  RadioEntry.TextTTS = TextTTS
  RadioEntry.TextScreen = TextScreen
  RadioEntry.IsNew = IsNew
  RadioEntry.IsGroup = isGroup
  RadioEntry.GroupID = GID
  RadioEntry.Duration = STTS.getSpeechTime(TextTTS,1.2,false)+2 or 16
  RadioEntry.ToScreen = true
  
  self.RadioQueue:Push(RadioEntry)

  return self
end

--- [Internal] Check for alive OpsGroup from Mission OpsGroups table
-- @param #AWACS self
-- @param #table OpsGroups
-- @return Ops.OpsGroup#OPSGROUP or nil
function AWACS:_GetAliveOpsGroupFromTable(OpsGroups)
  self:T(self.lid.."_GetAliveOpsGroupFromTable")
  local handback = nil 
  for _,_OG in pairs(OpsGroups or {}) do
    local OG = _OG -- Ops.OpsGroup#OPSGROUP
    if OG and OG:IsFlightgroup() and OG:IsAlive() then
      handback = OG
      self:T("Handing back OG: " .. OG:GetName())
      break
    end
  end 
  return handback
end

--- [Internal] Check Enough AI CAP on Station
-- @param #AWACS self
-- @return #AWACS self
function AWACS:_CheckAICAPOnStation()
  self:I(self.lid.."_CheckAICAPOnStation")
  if self.MaxAIonCAP > 0 then
    local onstation = self.AICAPMissions:Count()
    -- control number of AI CAP Flights
    if onstation < self.MaxAIonCAP then
      -- not enough
      local AnchorStackNo,free = self:_GetFreeAnchorStack()
      if free then
        -- recruit assets (thanks to FF!)
        local recruited, assets, airwings = self:RecruitAssets(AUFTRAG.Type.CAP,1,1)
        if recruited then
          -- yes, there should be ONE asset and ONE AW
          
          self:I(self.lid..string.format("Recruited %d assets for mission type CAP", #assets))
          local mission = AUFTRAG:NewCAP(self.AnchorZone,20000,350,self.AnchorZone:GetCoordinate(),nil,nil,{})
          
          -- Add asset to mission.
          if mission then
            for _,_asset in pairs(assets) do
              local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
              mission:AddAsset(asset)
              asset.legion:AddMission(mission)
              -- Debug info.
              self:I(self.lid..string.format("Assigning mission \"%s\" [%s] to AW \"%s\"", mission.name, mission.type, asset.legion.alias))
            end
          end
          
          -- TODO - necessary?
          LEGION.UnRecruitAssets(assets)
          
          --self.AirWing:AddMission(mission)
          self.AICAPMissions:Push(mission,mission.auftragsnummer)
          
        else
          -- no
          self:I(self.lid.."Could NOT recruit assets for mission type CAP")
        end
      end
    elseif onstation > self.MaxAIonCAP then
      -- too many, send one home
      local mission = self.AICAPMissions:Pull() -- Ops.Auftrag#AUFTRAG
      local Groups = mission:GetOpsGroups()
      local OpsGroup = self:_GetAliveOpsGroupFromTable(Groups)
      mission:__Cancel(5)
      self:_CheckOut(OpsGroup)
    end
    -- Check CAP Mission states
    if onstation > 0 then
      local missionIDs = self.AICAPMissions:GetIDStackSorted()
      --self:_CheckInAI(FlightGroup,FlightGroup:GetGroup(),Mission.auftragsnummer)
      -- get mission type and state
      for _,_MissionID in pairs(missionIDs) do
        local mission = self.AICAPMissions:ReadByID(_MissionID) -- Ops.Auftrag#AUFTRAG
        self:T("Looking at AuftragsNr " .. mission.auftragsnummer)
        local type = mission:GetType()
        local state = mission:GetState()
        if type == AUFTRAG.Type.CAP then
          local OpsGroups = mission:GetOpsGroups()
          local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups)
          local FGstate = mission:GetGroupStatus(OpsGroup)
          -- FG ready?
          if FGstate == AUFTRAG.Status.STARTED or FGstate ==  AUFTRAG.Status.EXECUTING then
            -- has this group checked in already? Avoid double tasking
            local GID, CheckedInAlready = self:_GetManagedGrpID(OpsGroup:GetGroup())
            if not CheckedInAlready then
              self:_SetAIROE(OpsGroup,OpsGroup:GetGroup())
              self:_CheckInAI(OpsGroup,OpsGroup:GetGroup(),mission.auftragsnummer)
            end
          end
        end
      end
    end
    -- cycle mission status
    if onstation > 0 then
      local report = REPORT:New("CAP Mission Status")
      report:Add("===============")
      local missionIDs = self.AICAPMissions:GetIDStackSorted()
      local i = 1
      for _,_MissionID in pairs(missionIDs) do 
      --for i=1,self.MaxAIonCAP do
        local mission = self.AICAPMissions:ReadByID(_MissionID) -- Ops.Auftrag#AUFTRAG
        --local mission = self.AICAPMissions:ReadByPointer(i) -- Ops.Auftrag#AUFTRAG
        if mission then
          i = i + 1
          report:Add(string.format("Entry %d",i))
          report:Add(string.format("Mission No %d",mission.auftragsnummer))
          report:Add(string.format("Mission Type %s",mission:GetType()))
          report:Add(string.format("Mission State %s",mission:GetState()))
          local OpsGroups = mission:GetOpsGroups()
          local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP
          if OpsGroup then
          local OpsName = OpsGroup:GetName() or "Unknown"
          local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown"
            report:Add(string.format("Mission FG %s",OpsName))
            report:Add(string.format("Callsign %s",OpsCallSign))
            report:Add(string.format("Mission FG State %s",OpsGroup:GetState()))
          else
            report:Add("***** Cannot obtain (yet) this missions OpsGroup!")
          end
          report:Add(string.format("Target Type %s",mission:GetTargetType()))
        end
        report:Add("===============") 
      end
      if self.debug then
        self:T(report:Text())
      end    
    end
  end
  return self
end

--- [Internal] Set ROE for AI CAP
-- @param #AWACS self
-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup
-- @param Wrapper.Group#GROUP Group
-- @return #AWACS self
function AWACS:_SetAIROE(FlightGroup,Group)
  self:T(self.lid.."_SetAIROE")
  local ROE = self.AwacsROE or AWACS.ROE.POLICE
  local ROT = self.AwacsROT or AWACS.ROT.PASSIVE
  
  -- TODO adjust to AWACS set ROE
  -- for the time being set to be defensive
  Group:OptionAlarmStateGreen()
  Group:OptionECM_OnlyLockByRadar()
  Group:OptionROEHoldFire()
  Group:OptionROTEvadeFire()
  Group:OptionRTBBingoFuel(true)
  Group:OptionKeepWeaponsOnThreat()
  local callname = self.AICAPCAllName or CALLSIGN.F16.Viper
  self.AICAPCAllNumber = self.AICAPCAllNumber + 1 
  Group:CommandSetCallsign(callname,math.fmod(self.AICAPCAllNumber,9))
  -- FG level
  FlightGroup:SetDefaultAlarmstate(AI.Option.Ground.val.ALARM_STATE.GREEN)
  FlightGroup:SetDefaultROE(ENUMS.ROE.WeaponHold)
  FlightGroup:SetDefaultROT(ENUMS.ROT.EvadeFire)
  FlightGroup:SetFuelLowRTB(true)
  FlightGroup:SetFuelLowThreshold(0.2)
  FlightGroup:SetEngageDetectedOff()
  FlightGroup:SetOutOfAAMRTB(true)
  return self
end

-- TODO FSMs
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- [Internal] onafterStart
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @return #AWACS self
function AWACS:onafterStart(From, Event, To)
  self:I({From, Event, To})
  
  -- Set up control zone
  self.ControlZone = ZONE_RADIUS:New(self.OpsZone:GetName(),self.OpsZone:GetVec2(),UTILS.NMToMeters(self.ControlZoneRadius))
  if self.debug then
    self.ControlZone:DrawZone(-1,{0,1,0},1,{1,0,0},0.05,3,true)
    MARKER:New(self.ControlZone:GetCoordinate(),"Control Zone"):ToAll()
  end
  
  -- set up the AWACS and let it orbit
  local AwacsAW = self.AirWing -- Ops.AirWing#AIRWING
  local mission = AUFTRAG:NewORBIT_RACETRACK(self.OrbitZone:GetCoordinate(),self.AwacsAngels*1000,self.Speed,self.Heading,self.Leg)
  local timeonstation = (self.AwacsTimeOnStation + self.ShiftChangeTime) * 3600
  mission:SetTime(nil,timeonstation)

  AwacsAW:AddMission(mission)
  
  --[[ callback functions
  local function StartSettings(FlightGroup,Mission)
    self:_StartSettings(FlightGroup,Mission)
  end
  
  function AwacsAW:OnAfterFlightOnMission(From,Event,To,FlightGroup,Mission)
   StartSettings(FlightGroup,Mission)
  end
  --]]
  
  self.AwacsMission = mission
  self.AwacsInZone = false -- not yet arrived or gone again
  
  self:__Status(-30)
  return self
end

--- [Internal] onafterStatus
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @return #AWACS self
function AWACS:onafterStatus(From, Event, To)
  self:I({From, Event, To})
  
  self:_SetClientMenus()
  
  local awacs = nil
  if self.AwacsFG then
    awacs = self.AwacsFG:GetGroup() -- Wrapper.Group#GROUP
  end
  if awacs and awacs:IsAlive() and not self.AwacsInZone then
    -- check if we arrived
    local orbitzone = self.OrbitZone -- Core.Zone#ZONE
    if awacs:IsInZone(orbitzone) then
      -- arrived
      self.AwacsInZone = true
      self:I(self.lid.."Arrived in Orbit Zone: " .. orbitzone:GetName())
      local text = string.format("%s on station for A O %s control.",self.callsigntxt,self.OpsZone:GetName() or "A O")

      local RadioEntry = {} -- #AWACS.RadioEntry
      RadioEntry.IsNew = true
      RadioEntry.TextTTS = text
      RadioEntry.TextScreen = text
      RadioEntry.ToScreen = true
      RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8
      
      self.RadioQueue:Push(RadioEntry)
      self:_StartIntel(awacs)
    end
  end 
  
    --------------------------------
    --     AWACS
    --------------------------------
   
  if (awacs and awacs:IsAlive()) then
  
    -- Check on Awacs Mission Status
    local AWmission = self.AwacsMission -- Ops.Auftrag#AUFTRAG
    local awstatus = AWmission:GetState()
    local AWmissiontime = (timer.getTime() - self.AwacsTimeStamp)
    
    local AWTOSLeft = UTILS.Round((((self.AwacsTimeOnStation+self.ShiftChangeTime)*3600) - AWmissiontime),0) -- seconds
    
    AWTOSLeft = UTILS.Round(AWTOSLeft/60,0) -- minutes
    
    local ChangeTime = UTILS.Round(((self.ShiftChangeTime * 3600)/60),0)
    
    local Changedue = "No"
    
    if not self.ShiftChangeAwacsFlag and (AWTOSLeft <= ChangeTime or AWmission:IsOver()) then 
      Changedue = "Yes"
      self.ShiftChangeAwacsFlag = true
      self:__AwacsShiftChange(2) 
    end
    
    local report = REPORT:New("AWACS:")
    report:Add("====================")
    report:Add("AWACS:")
    report:Add(string.format("Auftrag Status: %s",awstatus))
    report:Add(string.format("TOS Left: %d min",AWTOSLeft))
    report:Add(string.format("Needs ShiftChange: %s",Changedue))
    
    local OpsGroups = AWmission:GetOpsGroups()
    local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP
    if OpsGroup then
      local OpsName = OpsGroup:GetName() or "Unknown"
      local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown"
      report:Add(string.format("Mission FG %s",OpsName))
      report:Add(string.format("Callsign %s",OpsCallSign))
      report:Add(string.format("Mission FG State %s",OpsGroup:GetState()))
    else
      report:Add("***** Cannot obtain (yet) this missions OpsGroup!")
    end
    
    -- Check for replacement mission - if any
    if self.ShiftChangeAwacsFlag and self.ShiftChangeAwacsRequested then -- Ops.Auftrag#AUFTRAG
      AWmission = self.EscortMissionReplacement
      local esstatus = AWmission:GetState()
      local ESmissiontime = (timer.getTime() - self.AwacsTimeStamp)
      local ESTOSLeft = UTILS.Round((((self.AwacsTimeOnStation+self.ShiftChangeTime)*3600) - ESmissiontime),0) -- seconds
      ESTOSLeft = UTILS.Round(ESTOSLeft/60,0) -- minutes
      local ChangeTime = UTILS.Round(((self.ShiftChangeTime * 3600)/60),0)
      --local Changedue = "No"
      
      --report:Add("====================")
      report:Add("AWACS REPLACEMENT:")
      report:Add(string.format("Auftrag Status: %s",esstatus))
      report:Add(string.format("TOS Left: %d min",ESTOSLeft))
      --report:Add(string.format("Needs ShiftChange: %s",Changedue))
      
      local OpsGroups = AWmission:GetOpsGroups()
      local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP
      if OpsGroup then
        local OpsName = OpsGroup:GetName() or "Unknown"
        local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown"
        report:Add(string.format("Mission FG %s",OpsName))
        report:Add(string.format("Callsign %s",OpsCallSign))
        report:Add(string.format("Mission FG State %s",OpsGroup:GetState()))
      else
        report:Add("***** Cannot obtain (yet) this missions OpsGroup!")
      end
      
      if AWmission:IsExecuting() then
        -- make the actual change in the queue
        self.ShiftChangeAwacsFlag = false
        self.ShiftChangeAwacsRequested = false
        -- cancel old mission
        if self.AwacsMission and self.AwacsMission:IsNotOver() then
            self.AwacsMission:Cancel()
        end
        self.AwacsMission = self.AwacsMissionReplacement
        self.AwacsMissionReplacement = nil
        self.AwacsTimeStamp = timer.getTime()
        report:Add("*** Replacement DONE ***")
      end
      report:Add("====================")
    end
    
    --------------------------------
    --     ESCORTS
    --------------------------------
                       
    if self.HasEscorts then
      local ESmission = self.EscortMission -- Ops.Auftrag#AUFTRAG
      local esstatus = ESmission:GetState()
      local ESmissiontime = (timer.getTime() - self.EscortsTimeStamp)
      local ESTOSLeft = UTILS.Round((((self.EscortsTimeOnStation+self.ShiftChangeTime)*3600) - ESmissiontime),0) -- seconds
      ESTOSLeft = UTILS.Round(ESTOSLeft/60,0) -- minutes
      local ChangeTime = UTILS.Round(((self.ShiftChangeTime * 3600)/60),0)
      local Changedue = "No"
      
      if (ESTOSLeft <= ChangeTime and not self.ShiftChangeEscortsFlag) or (ESmission:IsOver() and not self.ShiftChangeEscortsFlag) then 
        Changedue = "Yes" 
        self.ShiftChangeEscortsFlag = true -- set this back when new Escorts arrived
        self:__EscortShiftChange(2)
      end
      
      report:Add("====================")
      report:Add("ESCORTS:")
      report:Add(string.format("Auftrag Status: %s",esstatus))
      report:Add(string.format("TOS Left: %d min",ESTOSLeft))
      report:Add(string.format("Needs ShiftChange: %s",Changedue))
      
      local OpsGroups = ESmission:GetOpsGroups()
      local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP
      if OpsGroup then
        local OpsName = OpsGroup:GetName() or "Unknown"
        local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown"
        report:Add(string.format("Mission FG %s",OpsName))
        report:Add(string.format("Callsign %s",OpsCallSign))
        report:Add(string.format("Mission FG State %s",OpsGroup:GetState()))
      else
        report:Add("***** Cannot obtain (yet) this missions OpsGroup!")
      end
      
      report:Add("====================")
      
      -- Check for replacement mission - if any
      if self.ShiftChangeEscortsFlag and self.ShiftChangeEscortsRequested then -- Ops.Auftrag#AUFTRAG
        ESmission = self.EscortMissionReplacement
        local esstatus = ESmission:GetState()
        local ESmissiontime = (timer.getTime() - self.EscortsTimeStamp)
        local ESTOSLeft = UTILS.Round((((self.EscortsTimeOnStation+self.ShiftChangeTime)*3600) - ESmissiontime),0) -- seconds
        ESTOSLeft = UTILS.Round(ESTOSLeft/60,0) -- minutes
        local ChangeTime = UTILS.Round(((self.ShiftChangeTime * 3600)/60),0)
        --local Changedue = "No"
        
        --report:Add("====================")
        report:Add("ESCORTS REPLACEMENT:")
        report:Add(string.format("Auftrag Status: %s",esstatus))
        report:Add(string.format("TOS Left: %d min",ESTOSLeft))
        --report:Add(string.format("Needs ShiftChange: %s",Changedue))
        
        local OpsGroups = ESmission:GetOpsGroups()
        local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP
        if OpsGroup then
          local OpsName = OpsGroup:GetName() or "Unknown"
          local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown"
          report:Add(string.format("Mission FG %s",OpsName))
          report:Add(string.format("Callsign %s",OpsCallSign))
          report:Add(string.format("Mission FG State %s",OpsGroup:GetState()))
        else
          report:Add("***** Cannot obtain (yet) this missions OpsGroup!")
        end
        
        if ESmission:IsExecuting() then
          -- make the actual change in the queue
          self.ShiftChangeEscortsFlag = false
          self.ShiftChangeEscortsRequested = false
          -- cancel old mission
          if self.EscortMission and self.EscortMission:IsNotOver() then
              self.EscortMission:Cancel()
          end
          self.EscortMission = self.EscortMissionReplacement
          self.EscortMissionReplacement = nil
          self.EscortsTimeStamp = timer.getTime()
          report:Add("*** Replacement DONE ***")
        end
        report:Add("====================")
      end
    end
      
    if self.debug then  
      self:I(report:Text())
    end
    -- Check on AUFTRAG status for CAP AI
    self:_CheckAICAPOnStation()
  else
   -- do other stuff
  
  end
  
  -- Check task queue (both)
  self:_CheckTaskQueue()

  self:__Status(30)
  return self
end

--- [Internal] onafterStop
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @return #AWACS self
function AWACS:onafterStop(From, Event, To)
  self:T({From, Event, To})
  -- unhandle stuff, exit intel
  
  self.intel:Stop()
  
  local AWFiFo = self.CAPAirwings -- Utilities.FiFo#FIFO
  local AWStack = AWFiFo:GetPointerStack()
  for _ID,_AWID in pairs(AWStack) do
    local SubAW = self.CAPAirwings:ReadByPointer(_ID)
    if SubAW then
      SubAW:RemoveUsingOpsAwacs()
    end
  end
  
  return self
end

--- [Internal] onafterAssignAnchor
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @param #number GID Group ID
-- @return #AWACS self
function AWACS:onafterAssignAnchor(From, Event, To, GID)
  self:T({From, Event, To, "GID = " .. GID})
  self:_AssignAnchorToID(GID)
  return self
end

--- [Internal] onafterCheckedOut
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @param #AWACS.ManagedGroup.GID Group ID 
-- @param #number AnchorStackNo
-- @param #number Angels
-- @return #AWACS self
function AWACS:onafterCheckedOut(From, Event, To, GID, AnchorStackNo, Angels)
  self:T({From, Event, To, "GID = " .. GID})
  self:_RemoveIDFromAnchor(GID,AnchorStackNo,Angels)
  return self
end

--- [Internal] onafterAssignedAnchor
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @param #number GID Managed Group ID
-- @param #AWACS.AnchorData Anchor
-- @param #number AnchorStackNo
-- @return #AWACS self
function AWACS:onafterAssignedAnchor(From, Event, To, GID, Anchor, AnchorStackNo, AnchorAngels)
  self:I({From, Event, To, "GID=" .. GID, "Stack=" .. AnchorStackNo})
  -- TODO
  local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
  managedgroup.AnchorStackNo = AnchorStackNo
  managedgroup.AnchorStackAngels = AnchorAngels
  self.ManagedGrps[GID] = managedgroup
  local isPlayer = managedgroup.IsPlayer
  local isAI = managedgroup.IsAI
  local Group = managedgroup.Group
  local CallSign = managedgroup.CallSign or "unknown 1 1"
  local AnchorName = Anchor.AnchorZone:GetName() or "unknown"
  local AnchorCoordTxt = Anchor.AnchorZoneCoordinateText or "unknown"
  local Angels = AnchorAngels or 25
  local AnchorSpeed = self.CapSpeedBase or 220
  local AuftragsNr = managedgroup.CurrentTask

  local textTTS = string.format("%s. %s. Anchor at %s at angels %d doing %d knots. Wait for task assignment.",self.callsigntxt,CallSign,AnchorName,Angels,AnchorSpeed)
  local ROEROT = self.AwacsROE.." "..self.AwacsROT
  local textScreen = string.format("%s. %s.\nAnchor at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s\nWait for task assignment.",self.callsigntxt,CallSign,AnchorName,Angels,AnchorSpeed,AnchorCoordTxt,ROEROT)
  local TextTasking = string.format("%s. %s.\nAnchor at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s",self.callsigntxt,CallSign,AnchorName,Angels,AnchorSpeed,AnchorCoordTxt,ROEROT)
  
  local RadioEntry = {} -- #AWACS.RadioEntry
  RadioEntry.IsNew = true
  RadioEntry.TextTTS = textTTS
  RadioEntry.TextScreen = textScreen
  RadioEntry.GroupID = GID
  RadioEntry.IsGroup = isPlayer
  RadioEntry.Duration = STTS.getSpeechTime(textTTS,1.0,false) or 10
  RadioEntry.ToScreen = isPlayer
  
  self.RadioQueue:Push(RadioEntry)
      
  self:_CreateTaskForGroup(GID,AWACS.TaskDescription.ANCHOR,TextTasking,Anchor.AnchorZone)
  
  if isAI and AuftragsNr and AuftragsNr > 0 and self.AICAPMissions:HasUniqueID(AuftragsNr) then
    -- Change current Auftrag to Orbit at Anchor
    local capalt = Angels*1000
    local capspeed = UTILS.KnotsToAltKIAS(self.CapSpeedBase,capalt)
    local AnchorMission = AUFTRAG:NewORBIT(Anchor.AnchorZoneCoordinate,capalt,capspeed,0,15)
    managedgroup.FlightGroup:AddMission(AnchorMission)
    managedgroup.FlightGroup:GetMissionCurrent():__Cancel(5)
    self.AICAPMissions:PullByID(AuftragsNr)
    self.AICAPMissions:Push(AnchorMission,AnchorMission.auftragsnummer)
    managedgroup.CurrentTask = AnchorMission.auftragsnummer
    self.ManagedGrps[GID] = managedgroup  
  end
  
  return self
end

--- [Internal] onafterNewCluster
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @param Ops.Intelligence#INTEL.Cluster Cluster
-- @return #AWACS self
function AWACS:onafterNewCluster(From,Event,To,Cluster)
  self:T({From, Event, To, Cluster})
  return self
end
  
--- [Internal] onafterNewContact
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @param Ops.Intelligence#INTEL.Contact Contact
-- @return #AWACS self 
function AWACS:onafterNewContact(From,Event,To,Contact)
  self:T({From, Event, To, Contact})
  self.Contacts:Push(Contact)
  self:_AnnounceContact(Contact,true,nil,false)
  return self
end
  
--- [Internal] onafterLostContact
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @param Ops.Intelligence#INTEL.Contact Contact
-- @return #AWACS self
function AWACS:onafterLostContact(From,Event,To,Contact)
  self:T({From, Event, To, Contact})
  -- TODO Check Idle, Assigned Tasks for status
  return self
end
  
--- [Internal] onafterLostCluster
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @param Ops.Intelligence#INTEL.Cluster Cluster
-- @param Ops.Auftrag#AUFTRAG Mission
-- @return #AWACS self
function AWACS:onafterLostCluster(From,Event,To,Cluster,Mission)
  self:T({From, Event, To})
  -- TODO Remove Cluster from Picture
  return self
end

--- [Internal] onafterCheckRadioQueue
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @return #AWACS self
function AWACS:onafterCheckRadioQueue(From,Event,To)
 self:T({From, Event, To})
 -- do we have messages queued?
 
 local nextcall = 10
 if self.RadioQueue:IsNotEmpty() then
  local RadioEntry = self.RadioQueue:Pull() -- #AWACS.RadioEntry
  
  self:T({RadioEntry})
  
  if not RadioEntry.FromAI then
    -- AI AWACS Speaking
    self.AwacsFG:RadioTransmission(RadioEntry.TextTTS,1,false)
  else
    -- CAP AI speaking
    if RadioEntry.GroupID and RadioEntry.GroupID ~= 0 then
      local managedgroup = self.ManagedGrps[RadioEntry.GroupID] -- #AWACS.ManagedGroup
      if managedgroup and managedgroup.FlightGroup and managedgroup.FlightGroup:IsAlive() then
        managedgroup.FlightGroup:RadioTransmission(RadioEntry.TextTTS,1,false)
      end
    end
  end
  
  if RadioEntry.Duration then nextcall = RadioEntry.Duration end
  if RadioEntry.ToScreen and RadioEntry.TextScreen then
    if RadioEntry.GroupID and RadioEntry.GroupID ~= 0 then
      local managedgroup = self.ManagedGrps[RadioEntry.GroupID] -- #AWACS.ManagedGroup
      if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive() then
        MESSAGE:New(RadioEntry.TextScreen,20,"AWACS"):ToGroup(managedgroup.Group)
      end
    else
      MESSAGE:New(RadioEntry.TextScreen,20,"AWACS"):ToCoalition(self.coalition)
    end
  end
 end
 
 if self:Is("Running") then
  -- exit if stopped
  self:__CheckRadioQueue(nextcall+2)
 end
 return self
end

--- [Internal] onafterEscortShiftChange
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @return #AWACS self
function AWACS:onafterEscortShiftChange(From,Event,To)
  self:I({From, Event, To})
  -- request new Escorts, check if AWACS-FG still alive first!
  if self.AwacsFG and self.ShiftChangeEscortsFlag and not self.ShiftChangeEscortsRequested then
    local awacs = self.AwacsFG:GetGroup() -- Wrapper.Group#GROUP
    if awacs and awacs:IsAlive() then
      -- ok we're good to re-request
      self.ShiftChangeEscortsRequested = true
      self.EscortsTimeStamp = timer.getTime()
      self:_StartEscorts(true)
    else
      -- should not happen
      self:E("**** AWACS group dead at onafterEscortShiftChange!")
    end
  end
  return self
end

--- [Internal] onafterAwacsShiftChange
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @return #AWACS self
function AWACS:onafterAwacsShiftChange(From,Event,To)
  self:I({From, Event, To})
  -- request new Escorts, check if AWACS-FG still alive first!
  if self.AwacsFG and self.ShiftChangeAwacsFlag and not self.ShiftChangeAwacsRequested then
    
    -- ok we're good to re-request
    self.ShiftChangeAwacsRequested = true
    self.AwacsTimeStamp = timer.getTime()
    
    -- set up the AWACS and let it orbit
    local AwacsAW = self.AirWing -- Ops.AirWing#AIRWING
    local mission = AUFTRAG:NewORBIT_RACETRACK(self.OrbitZone:GetCoordinate(),self.AwacsAngels*1000,self.Speed,self.Heading,self.Leg)
    local timeonstation = (self.AwacsTimeOnStation + self.ShiftChangeTime) * 3600
    mission:SetTime(nil,timeonstation)
  
    AwacsAW:AddMission(mission)
    
    self.AwacsMissionReplacement = mission
    
  end
  return self
end

--- On after "FlightOnMission".
-- @param #AWACS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup on mission.
-- @param Ops.Auftrag#AUFTRAG Mission The requested mission.
-- @return #AWACS self
function AWACS:onafterFlightOnMission(From, Event, To, FlightGroup, Mission)
  self:I({From, Event, To})
  -- coming back from AW, set up the flight
  if self:Is("Running") then
    self:_StartSettings(FlightGroup,Mission)
  end
  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- END AWACS
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
end -- end do

--- Testing
_SETTINGS:SetLocale("en")
_SETTINGS:SetImperial()
_SETTINGS:SetPlayerMenuOff()

-- We need an AirWing
local AwacsAW = AIRWING:New("AirForce WH-1","AirForce One")
AwacsAW:SetReportOn()
AwacsAW:SetMarker(true)
AwacsAW:SetAirbase(AIRBASE:FindByName(AIRBASE.Caucasus.Kutaisi))
AwacsAW:SetRespawnAfterDestroyed(900)
AwacsAW:Start()

-- And a couple of Squads
-- AWACS itself
local Squad_One = SQUADRON:New("Awacs One",2,"Awacs North")
Squad_One:AddMissionCapability({AUFTRAG.Type.ORBIT},100)
Squad_One:SetFuelLowRefuel(false)
Squad_One:SetFuelLowThreshold(0.2)
Squad_One:SetTurnoverTime(10,20)
AwacsAW:AddSquadron(Squad_One)
AwacsAW:NewPayload("Awacs One One",-1,{AUFTRAG.Type.ORBIT},100)

-- Escorts
local Squad_Two = SQUADRON:New("Escorts",4,"Escorts North")
Squad_Two:AddMissionCapability({AUFTRAG.Type.ESCORT})
Squad_Two:SetFuelLowRefuel(true)
Squad_Two:SetFuelLowThreshold(0.3)
Squad_Two:SetTurnoverTime(10,20)
AwacsAW:AddSquadron(Squad_Two)
AwacsAW:NewPayload("Escorts",-1,{AUFTRAG.Type.ESCORT},100)

-- CAP
local Squad_Three = SQUADRON:New("CAP",2,"CAP North")
Squad_Three:AddMissionCapability({AUFTRAG.Type.ALERT5, AUFTRAG.Type.CAP, AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT})
Squad_Three:SetFuelLowRefuel(true)
Squad_Three:SetFuelLowThreshold(0.3)
Squad_Three:SetTurnoverTime(10,20)
AwacsAW:AddSquadron(Squad_Three)
AwacsAW:NewPayload("Aerial-1-2",-1,{AUFTRAG.Type.ALERT5,AUFTRAG.Type.CAP, AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT},100)


-- We need a secondary AirWing for testing
local AwacsAW2 = AIRWING:New("AirForce WH-2","AirForce Two")
AwacsAW2:SetReportOn()
AwacsAW2:SetMarker(true)
AwacsAW2:SetAirbase(AIRBASE:FindByName(AIRBASE.Caucasus.Beslan))
AwacsAW2:SetRespawnAfterDestroyed(900)
AwacsAW2:Start()

-- CAP2
local Squad_ThreeOne = SQUADRON:New("CAP2",4,"CAP West")
Squad_ThreeOne:AddMissionCapability({AUFTRAG.Type.ALERT5, AUFTRAG.Type.CAP, AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT})
Squad_ThreeOne:SetFuelLowRefuel(true)
Squad_ThreeOne:SetFuelLowThreshold(0.3)
Squad_ThreeOne:SetTurnoverTime(10,20)
AwacsAW2:AddSquadron(Squad_ThreeOne)
AwacsAW2:NewPayload("CAP 2-1",-1,{AUFTRAG.Type.ALERT5,AUFTRAG.Type.CAP, AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT},100)

-- Get AWACS started
local testawacs = AWACS:New("AWACS North",AwacsAW,"blue",AIRBASE.Caucasus.Kutaisi,"Awacs Orbit",ZONE:FindByName("NW Zone"),"Anchor One",255,radio.modulation.AM )
testawacs:SetEscort(2)
testawacs:SetAwacsDetails(CALLSIGN.AWACS.Darkstar,1,22,230,61,15)
testawacs:SetSRS("E:\\Program Files\\DCS-SimpleRadio-Standalone","female","en-GB",5010,nil)
testawacs:AddCAPAirWing(AwacsAW2)
testawacs:__Start(5)
