--- **Sound** - Queues Radio Transmissions.
-- 
-- ===
-- 
-- ## Features:
-- 
--   * Manage Radio Transmissions
--
-- ===
--
-- ### Authors: funkyfranky
--
-- @module Sound.RadioQueue
-- @image Core_Radio.JPG

--- Manages radio transmissions.
-- 
-- The main goal of the RADIOQUEUE class is to string together multiple sound files to play a complete sentence.
-- The underlying problem is that radio transmissions in DCS are not queued but played "on top" of each other.
-- Therefore, to achive the goal, it is vital to know the precise duration how long it takes to play the sound file.
-- 
-- @type RADIOQUEUE
-- @field #string ClassName Name of the class "RADIOQUEUE".
-- @field #boolean Debugmode Debug mode. More info.
-- @field #string lid ID for dcs.log.
-- @field #number frequency The radio frequency in Hz.
-- @field #number modulation The radio modulation. Either radio.modulation.AM or radio.modulation.FM.
-- @field Core.Scheduler#SCHEDULER scheduler The scheduler.
-- @field #string RQid The radio queue scheduler ID.
-- @field #table queue The queue of transmissions.
-- @field #string alias Name of the radio.
-- @field #number dt Time interval in seconds for checking the radio queue.
-- @field #number delay Time delay before starting the radio queue. 
-- @field #number Tlast Time (abs) when the last transmission finished.
-- @field Core.Point#COORDINATE sendercoord Coordinate from where transmissions are broadcasted.
-- @field #number sendername Name of the sending unit or static.
-- @field #boolean senderinit Set frequency was initialized.
-- @field #number power Power of radio station in Watts. Default 100 W.
-- @field #table numbers Table of number transmission parameters.
-- @field #boolean checking Scheduler is checking the radio queue. 
-- @field #boolean schedonce Call ScheduleOnce instead of normal scheduler.
-- @field Sound.SRS#MSRS msrs Moose SRS class.
-- @extends Core.Base#BASE
RADIOQUEUE = {
  ClassName   = "RADIOQUEUE",
  Debugmode   = nil,
  lid         = nil,
  frequency   = nil,
  modulation  = nil,
  scheduler   = nil,
  RQid        = nil,
  queue       =  {},
  alias       = nil,
  dt          = nil,
  delay       = nil,
  Tlast       = nil,
  sendercoord = nil,
  sendername  = nil,
  senderinit  = nil,
  power       = nil,
  numbers     =  {},
  checking    = nil,
  schedonce   = false,
}

--- Radio queue transmission data.
-- @type RADIOQUEUE.Transmission
-- @field #string filename Name of the file to be transmitted.
-- @field #string path Path in miz file where the file is located.
-- @field #number duration Duration in seconds.
-- @field #string subtitle Subtitle of the transmission.
-- @field #number subduration Duration of the subtitle being displayed.
-- @field #number Tstarted Mission time (abs) in seconds when the transmission started.
-- @field #boolean isplaying If true, transmission is currently playing.
-- @field #number Tplay Mission time (abs) in seconds when the transmission should be played.
-- @field #number interval Interval in seconds before next transmission.
-- @field Sound.SoundOutput#SOUNDFILE soundfile Sound file object to play via SRS.
-- @field Sound.SoundOutput#SOUNDTEXT soundtext Sound TTS object to play via SRS.


--- Create a new RADIOQUEUE object for a given radio frequency/modulation.
-- @param #RADIOQUEUE self
-- @param #number frequency The radio frequency in MHz.
-- @param #number modulation (Optional) The radio modulation. Default `radio.modulation.AM` (=0).
-- @param #string alias (Optional) Name of the radio queue.
-- @return #RADIOQUEUE self The RADIOQUEUE object.
function RADIOQUEUE:New(frequency, modulation, alias)

  -- Inherit base
  local self=BASE:Inherit(self, BASE:New()) -- #RADIOQUEUE
  
  self.alias=alias or "My Radio"
  
  self.lid=string.format("RADIOQUEUE %s | ", self.alias)
  
  if frequency==nil then
    self:E(self.lid.."ERROR: No frequency specified as first parameter!")
    return nil
  end
  
  -- Frequency in Hz.
  self.frequency=frequency*1000000
  
  -- Modulation.
  self.modulation=modulation or radio.modulation.AM
  
  -- Set radio power.
  self:SetRadioPower()
  
  -- Scheduler.
  self.scheduler=SCHEDULER:New()
  self.scheduler:NoTrace()
  
  return self
end

--- Start the radio queue.
-- @param #RADIOQUEUE self
-- @param #number delay (Optional) Delay in seconds, before the radio queue is started. Default 1 sec.
-- @param #number dt (Optional) Time step in seconds for checking the queue. Default 0.01 sec.
-- @return #RADIOQUEUE self The RADIOQUEUE object.
function RADIOQUEUE:Start(delay, dt)
  
  -- Delay before start.
  self.delay=delay or 1
  
  -- Time interval for queue check.
  self.dt=dt or 0.01
  
  -- Debug message.
  self:I(self.lid..string.format("Starting RADIOQUEUE %s on Frequency %.2f MHz [modulation=%d] in %.1f seconds (dt=%.3f sec)", self.alias, self.frequency/1000000, self.modulation, self.delay, self.dt))

  -- Start Scheduler.
  if self.schedonce then
    self:_CheckRadioQueueDelayed(self.delay)
  else
    self.RQid=self.scheduler:Schedule(nil, RADIOQUEUE._CheckRadioQueue, {self}, self.delay, self.dt)
  end
  
  return self
end

--- Stop the radio queue. Stop scheduler and delete queue.
-- @param #RADIOQUEUE self
-- @return #RADIOQUEUE self The RADIOQUEUE object.
function RADIOQUEUE:Stop()
  self:I(self.lid.."Stopping RADIOQUEUE.")
  self.scheduler:Stop(self.RQid)
  self.queue={}
  return self
end

--- Set coordinate from where the transmission is broadcasted.
-- @param #RADIOQUEUE self
-- @param Core.Point#COORDINATE coordinate Coordinate of the sender.
-- @return #RADIOQUEUE self The RADIOQUEUE object.
function RADIOQUEUE:SetSenderCoordinate(coordinate)
  self.sendercoord=coordinate
  return self
end

--- Set name of unit or static from which transmissions are made.
-- @param #RADIOQUEUE self
-- @param #string name Name of the unit or static used for transmissions.
-- @return #RADIOQUEUE self The RADIOQUEUE object.
function RADIOQUEUE:SetSenderUnitName(name)
  self.sendername=name
  return self
end

--- Set radio power. Note that this only applies if no relay unit is used.
-- @param #RADIOQUEUE self
-- @param #number power Radio power in Watts. Default 100 W.
-- @return #RADIOQUEUE self The RADIOQUEUE object.
function RADIOQUEUE:SetRadioPower(power)
  self.power=power or 100
  return self
end

--- Set SRS.
-- @param #RADIOQUEUE self
-- @param #string PathToSRS Path to SRS.
-- @param #number Port SRS port. Default 5002.
-- @return #RADIOQUEUE self The RADIOQUEUE object.
function RADIOQUEUE:SetSRS(PathToSRS, Port)
  local path = PathToSRS or MSRS.path
  local port = Port or MSRS.port
  self.msrs=MSRS:New(path, self.frequency/1000000, self.modulation)
  self.msrs:SetPort(port)
  return self
end

--- Set parameters of a digit.
-- @param #RADIOQUEUE self
-- @param #number digit The digit 0-9.
-- @param #string filename The name of the sound file.
-- @param #number duration The duration of the sound file in seconds.
-- @param #string path The directory within the miz file where the sound is located. Default "l10n/DEFAULT/".
-- @param #string subtitle Subtitle of the transmission.
-- @param #number subduration Duration [sec] of the subtitle being displayed. Default 5 sec.
-- @return #RADIOQUEUE self The RADIOQUEUE object.
function RADIOQUEUE:SetDigit(digit, filename, duration, path, subtitle, subduration)

  local transmission={} --#RADIOQUEUE.Transmission
  transmission.filename=filename
  transmission.duration=duration
  transmission.path=path or "l10n/DEFAULT/"
  transmission.subtitle=nil
  transmission.subduration=nil
  
  -- Convert digit to string in case it is given as a number.
  if type(digit)=="number" then
    digit=tostring(digit)
  end

  -- Set transmission.
  self.numbers[digit]=transmission
  
  return self
end

--- Add a transmission to the radio queue.
-- @param #RADIOQUEUE self
-- @param #RADIOQUEUE.Transmission transmission The transmission data table. 
-- @return #RADIOQUEUE self
function RADIOQUEUE:AddTransmission(transmission)
  self:F({transmission=transmission})
  
  -- Init.
  transmission.isplaying=false
  transmission.Tstarted=nil

  -- Add to queue.
  table.insert(self.queue, transmission)
  
  -- Start checking.
  if self.schedonce and not self.checking then
    self:_CheckRadioQueueDelayed()
  end

  return self
end

--- Create a new transmission and add it to the radio queue.
-- @param #RADIOQUEUE self
-- @param #string filename Name of the sound file. Usually an ogg or wav file type.
-- @param #number duration Duration in seconds the file lasts.
-- @param #number path Directory path inside the miz file where the sound file is located. Default "l10n/DEFAULT/".
-- @param #number tstart Start time (abs) seconds. Default now.
-- @param #number interval Interval in seconds after the last transmission finished.
-- @param #string subtitle Subtitle of the transmission.
-- @param #number subduration Duration [sec] of the subtitle being displayed. Default 5 sec.
-- @return #RADIOQUEUE.Transmission Radio transmission table.
function RADIOQUEUE:NewTransmission(filename, duration, path, tstart, interval, subtitle, subduration)

  -- Sanity checks.
  if not filename then
    self:E(self.lid.."ERROR: No filename specified.")
    return nil
  end
  if type(filename)~="string" then
    self:E(self.lid.."ERROR: Filename specified is NOT a string.")
    return nil    
  end

  if not duration then
    self:E(self.lid.."ERROR: No duration of transmission specified.")
    return nil
  end
  if type(duration)~="number" then
    self:E(self.lid..string.format("ERROR: Duration specified is NOT a number but type=%s. Filename=%s, duration=%s", type(duration), tostring(filename), tostring(duration)))
    return nil    
  end
  

  local transmission={} --#RADIOQUEUE.Transmission
  transmission.filename=filename
  transmission.duration=duration
  transmission.path=path or "l10n/DEFAULT/"
  transmission.Tplay=tstart or timer.getAbsTime()
  transmission.subtitle=subtitle
  transmission.interval=interval or 0
  if transmission.subtitle then
    transmission.subduration=subduration or 5
  else
    transmission.subduration=nil
  end
  
  -- Add transmission to queue.  
  self:AddTransmission(transmission)
  
  return transmission
end

--- Add a SOUNDFILE to the radio queue.
-- @param #RADIOQUEUE self
-- @param Sound.SoundOutput#SOUNDFILE soundfile Sound file object to be added.
-- @param #number tstart Start time (abs) seconds. Default now.
-- @param #number interval Interval in seconds after the last transmission finished.
-- @return #RADIOQUEUE self
function RADIOQUEUE:AddSoundFile(soundfile, tstart, interval)
  --env.info(string.format("FF add soundfile: name=%s%s", soundfile:GetPath(), soundfile:GetFileName()))
  local transmission=self:NewTransmission(soundfile:GetFileName(), soundfile.duration, soundfile:GetPath(), tstart, interval, soundfile.subtitle, soundfile.subduration)
  transmission.soundfile=soundfile
  return self
end

--- Add a SOUNDTEXT to the radio queue.
-- @param #RADIOQUEUE self
-- @param Sound.SoundOutput#SOUNDTEXT soundtext Text-to-speech text.
-- @param #number tstart Start time (abs) seconds. Default now.
-- @param #number interval Interval in seconds after the last transmission finished.
-- @return #RADIOQUEUE self
function RADIOQUEUE:AddSoundText(soundtext, tstart, interval)

  local transmission=self:NewTransmission("SoundText.ogg", soundtext.duration, nil, tstart, interval, soundtext.subtitle, soundtext.subduration)
  transmission.soundtext=soundtext
  return self
end


--- Convert a number (as string) into a radio transmission.
-- E.g. for board number or headings.
-- @param #RADIOQUEUE self
-- @param #string number Number string, e.g. "032" or "183".
-- @param #number delay Delay before transmission in seconds.
-- @param #number interval Interval between the next call.
-- @return #number Duration of the call in seconds.
function RADIOQUEUE:Number2Transmission(number, delay, interval)
  
  -- Split string into characters.
  local numbers=UTILS.GetCharacters(number)

  local wait=0    
  for i=1,#numbers do
  
    -- Current number
    local n=numbers[i]
        
    -- Radio call.
    local transmission=UTILS.DeepCopy(self.numbers[n]) --#RADIOQUEUE.Transmission
    
    transmission.Tplay=timer.getAbsTime()+(delay or 0)
    
    if interval and i==1 then
      transmission.interval=interval
    end
    
    self:AddTransmission(transmission)
    
    -- Add up duration of the number.
    wait=wait+transmission.duration
  end
  
  -- Return the total duration of the call.
  return wait
end


--- Broadcast radio message.
-- @param #RADIOQUEUE self
-- @param #RADIOQUEUE.Transmission transmission The transmission.
function RADIOQUEUE:Broadcast(transmission)
  self:T("Broadcast")

  if ((transmission.soundfile and transmission.soundfile.useSRS) or transmission.soundtext) and self.msrs then
    self:_BroadcastSRS(transmission)
    return
  end

  -- Get unit sending the transmission.
  local sender=self:_GetRadioSender()
  
  -- Construct file name.
  local filename=string.format("%s%s", transmission.path, transmission.filename)
  
  if sender then
    
    -- Broadcasting from aircraft. Only players tuned in to the right frequency will see the message.
    self:T(self.lid..string.format("Broadcasting from aircraft %s", sender:GetName()))
    
    
    --if not self.senderinit then
    -- TODO Seems to be a DCS bug - if I explode ANY unit in a group the BC assignment gets lost
    
      -- Command to set the Frequency for the transmission.
      local commandFrequency={
        id="SetFrequency",
        params={
          frequency=self.frequency,  -- Frequency in Hz.
          modulation=self.modulation,
        }}
          
      -- Set commend for frequency
      sender:SetCommand(commandFrequency)
      
      self.senderinit=true
    --end
    
    -- Set subtitle only if duration>0 sec.
    local subtitle=nil
    local duration=nil
    if transmission.subtitle and transmission.subduration and transmission.subduration>0 then
      subtitle=transmission.subtitle
      duration=transmission.subduration
    end
    
    -- Command to tranmit the call.
    local commandTransmit={
      id = "TransmitMessage",
      params = {
        file=filename,
        duration=duration,
        subtitle=subtitle,
        loop=false,
      }}    
    
    -- Set command for radio transmission. 
    sender:SetCommand(commandTransmit)
    
    -- Debug message.
    if self.Debugmode then
      local text=string.format("file=%s, freq=%.2f MHz, duration=%.2f sec, subtitle=%s", filename, self.frequency/1000000, transmission.duration, transmission.subtitle or "")
      MESSAGE:New(text, 2, "RADIOQUEUE "..self.alias):ToAll()
    end
      
  else
    
    -- Broadcasting from carrier. No subtitle possible. Need to send messages to players.
    self:T(self.lid..string.format("Broadcasting via trigger.action.radioTransmission()"))
  
    -- Position from where to transmit.
    local vec3=nil
    
    -- Try to get positon from sender unit/static.
    if self.sendername then
      vec3=self:_GetRadioSenderCoord()
    end
    
    -- Try to get fixed positon.
    if self.sendercoord and not vec3 then
      vec3=self.sendercoord:GetVec3()
    end
    
    -- Transmit via trigger.
    if vec3 then
      self:T("Sending")
      self:T( { filename = filename, vec3 = vec3, modulation = self.modulation, frequency = self.frequency, power = self.power } )
      
      -- Trigger transmission.
      trigger.action.radioTransmission(filename, vec3, self.modulation, false, self.frequency, self.power)
      
      -- Debug message.
      if self.Debugmode then
        local text=string.format("file=%s, freq=%.2f MHz, duration=%.2f sec, subtitle=%s", filename, self.frequency/1000000, transmission.duration, transmission.subtitle or "")
        MESSAGE:New(string.format(text, filename, transmission.duration, transmission.subtitle or ""), 5, "RADIOQUEUE "..self.alias):ToAll()
      end
    else
      self:E("ERROR: Could not get vec3 to determine transmission origin! Did you specify a sender and is it still alive?")
    end

  end
end

--- Broadcast radio message.
-- @param #RADIOQUEUE self
-- @param #RADIOQUEUE.Transmission transmission The transmission.
function RADIOQUEUE:_BroadcastSRS(transmission)

  if transmission.soundfile and transmission.soundfile.useSRS then
    self.msrs:PlaySoundFile(transmission.soundfile)
  elseif transmission.soundtext then
    self.msrs:PlaySoundText(transmission.soundtext)
  end

end

--- Start checking the radio queue.
-- @param #RADIOQUEUE self
-- @param #number delay Delay in seconds before checking.
function RADIOQUEUE:_CheckRadioQueueDelayed(delay)
  self.checking=true
  self:ScheduleOnce(delay or self.dt, RADIOQUEUE._CheckRadioQueue, self)
end

--- Check radio queue for transmissions to be broadcasted.
-- @param #RADIOQUEUE self
function RADIOQUEUE:_CheckRadioQueue()

  -- Check if queue is empty.
  if #self.queue==0 then
    -- Queue is now empty. Nothing to else to do.
    self.checking=false
    return
  end

  -- Get current abs time.
  local time=timer.getAbsTime()
  
  local playing=false
  local next=nil  --#RADIOQUEUE.Transmission
  local remove=nil
  for i,_transmission in ipairs(self.queue) do
    local transmission=_transmission  --#RADIOQUEUE.Transmission
    
    -- Check if transmission time has passed.
    if time>=transmission.Tplay then 
      
      -- Check if transmission is currently playing.
      if transmission.isplaying then
      
        -- Check if transmission is finished.
        if time>=transmission.Tstarted+transmission.duration then
          
          -- Transmission over.
          transmission.isplaying=false
          
          -- Remove ith element in queue.
          remove=i
          
          -- Store time last transmission finished.
          self.Tlast=time
                    
        else -- still playing
        
          -- Transmission is still playing.
          playing=true
          
        end
      
      else -- not playing yet
      
        local Tlast=self.Tlast
      
        if transmission.interval==nil  then
      
          -- Not playing ==> this will be next.
          if next==nil then
            next=transmission
          end
          
        else
        
          if Tlast==nil or time-Tlast>=transmission.interval then
            next=transmission            
          else
            
          end
        end
        
        -- We got a transmission or one with an interval that is not due yet. No need for anything else.
        if next or Tlast then
          break
        end
             
      end
      
    else
      
        -- Transmission not due yet.
      
    end  
  end
  
  -- Found a new transmission.
  if next~=nil and not playing then
    self:Broadcast(next)
    next.isplaying=true
    next.Tstarted=time
  end
  
  -- Remove completed calls from queue.
  if remove then
    table.remove(self.queue, remove)
  end
  
  -- Check queue.
  if self.schedonce then
    self:_CheckRadioQueueDelayed()
  end
  
end

--- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work.
-- @param #RADIOQUEUE self
-- @return Wrapper.Unit#UNIT Sending unit or nil if was not setup, is not an aircraft or ground unit or is not alive.
function RADIOQUEUE:_GetRadioSender()

  -- Check if we have a sending aircraft.
  local sender=nil  --Wrapper.Unit#UNIT

  -- Try the general default.
  if self.sendername then
  
    -- First try to find a unit 
    sender=UNIT:FindByName(self.sendername)

    -- Check that sender is alive and an aircraft.
    if sender and sender:IsAlive() and (sender:IsAir() or sender:IsGround()) then
      return sender
    end
    
  end
    
  return nil
end

--- Get unit from which we want to transmit a radio message. This has to be an aircraft or ground unit for subtitles to work.
-- @param #RADIOQUEUE self
-- @return DCS#Vec3 Vector 3D.
function RADIOQUEUE:_GetRadioSenderCoord()

  local vec3=nil

  -- Try the general default.
  if self.sendername then
  
    -- First try to find a unit 
    local sender=UNIT:FindByName(self.sendername)

    -- Check that sender is alive and an aircraft.
    if sender and sender:IsAlive() then
      return sender:GetVec3()
    end
    
    -- Now try a static. 
    local sender=STATIC:FindByName( self.sendername, false )

    -- Check that sender is alive and an aircraft.
    if sender then
      return sender:GetVec3()
    end
    
  end
    
  return nil
end
