--- **Core** - SCHEDULEDISPATCHER dispatches the different schedules.
--
-- ===
--
-- Takes care of the creation and dispatching of scheduled functions for SCHEDULER objects.
--
-- This class is tricky and needs some thorough explanation.
-- SCHEDULE classes are used to schedule functions for objects, or as persistent objects.
-- The SCHEDULEDISPATCHER class ensures that:
--
--   - Scheduled functions are planned according the SCHEDULER object parameters.
--   - Scheduled functions are repeated when requested, according the SCHEDULER object parameters.
--   - Scheduled functions are automatically removed when the schedule is finished, according the SCHEDULER object parameters.
--
-- The SCHEDULEDISPATCHER class will manage SCHEDULER object in memory during garbage collection:
--
--   - When a SCHEDULER object is not attached to another object (that is, it's first :Schedule() parameter is nil), then the SCHEDULER object is _persistent_ within memory.
--   - When a SCHEDULER object *is* attached to another object, then the SCHEDULER object is _not persistent_ within memory after a garbage collection!
--
-- The non-persistency of SCHEDULERS attached to objects is required to allow SCHEDULER objects to be garbage collected when the parent object is destroyed, or set to nil and garbage collected.
-- Even when there are pending timer scheduled functions to be executed for the SCHEDULER object,
-- these will not be executed anymore when the SCHEDULER object has been destroyed.
--
-- The SCHEDULEDISPATCHER allows multiple scheduled functions to be planned and executed for one SCHEDULER object.
-- The SCHEDULER object therefore keeps a table of "CallID's", which are returned after each planning of a new scheduled function by the SCHEDULEDISPATCHER.
-- The SCHEDULER object plans new scheduled functions through the @{Core.Scheduler#SCHEDULER.Schedule}() method.
-- The Schedule() method returns the CallID that is the reference ID for each planned schedule.
--
-- ===
--
-- ### Contributions: -
-- ### Authors: FlightControl : Design & Programming
--
-- @module Core.ScheduleDispatcher
-- @image Core_Schedule_Dispatcher.JPG

--- SCHEDULEDISPATCHER class.
-- @type SCHEDULEDISPATCHER
-- @field #string ClassName Name of the class.
-- @field #number CallID Call ID counter.
-- @field #table PersistentSchedulers Persistent schedulers.
-- @field #table ObjectSchedulers Schedulers that only exist as long as the master object exists.
-- @field #table Schedule Meta table setmetatable( {}, { __mode = "k" } ).
-- @extends Core.Base#BASE

--- The SCHEDULEDISPATCHER structure
-- @type SCHEDULEDISPATCHER
SCHEDULEDISPATCHER = {
  ClassName = "SCHEDULEDISPATCHER",
  CallID = 0,
  PersistentSchedulers = {},
  ObjectSchedulers = {},
  Schedule = nil,
}

--- Player data table holding all important parameters of each player.
-- @type SCHEDULEDISPATCHER.ScheduleData
-- @field #function Function The schedule function to be called.
-- @field #table Arguments Schedule function arguments.
-- @field #number Start Start time in seconds.
-- @field #number Repeat Repeat time interval in seconds.
-- @field #number Randomize Randomization factor [0,1].
-- @field #number Stop Stop time in seconds.
-- @field #number StartTime Time in seconds when the scheduler is created.
-- @field #number ScheduleID Schedule ID.
-- @field #function CallHandler Function to be passed to the DCS timer.scheduleFunction().
-- @field #boolean ShowTrace If true, show tracing info.

--- Create a new schedule dispatcher object.
-- @param #SCHEDULEDISPATCHER self
-- @return #SCHEDULEDISPATCHER self
function SCHEDULEDISPATCHER:New()
  local self = BASE:Inherit( self, BASE:New() )
  self:F3()
  return self
end

--- Add a Schedule to the ScheduleDispatcher.
-- The development of this method was really tidy.
-- It is constructed as such that a garbage collection is executed on the weak tables, when the Scheduler is set to nil.
-- Nothing of this code should be modified without testing it thoroughly.
-- @param #SCHEDULEDISPATCHER self
-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object.
-- @param #function ScheduleFunction Scheduler function.
-- @param #table ScheduleArguments Table of arguments passed to the ScheduleFunction.
-- @param #number Start Start time in seconds.
-- @param #number Repeat Repeat interval in seconds.
-- @param #number Randomize Randomization factor [0,1].
-- @param #number Stop Stop time in seconds.
-- @param #number TraceLevel Trace level [0,3].
-- @param Core.Fsm#FSM Fsm Finite state model.
-- @return #string Call ID or nil.
function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop, TraceLevel, Fsm )
  self:F2( { Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop, TraceLevel, Fsm } )

  -- Increase counter.
  self.CallID = self.CallID + 1

  -- Create ID.
  local CallID = self.CallID .. "#" .. (Scheduler.MasterObject and Scheduler.MasterObject.GetClassNameAndID and Scheduler.MasterObject:GetClassNameAndID() or "") or ""

  self:T2( string.format( "Adding schedule #%d CallID=%s", self.CallID, CallID ) )

  -- Initialize PersistentSchedulers
  self.PersistentSchedulers = self.PersistentSchedulers or {}

  -- Initialize the ObjectSchedulers array, which is a weakly coupled table.
  -- If the object used as the key is nil, then the garbage collector will remove the item from the Functions array.
  self.ObjectSchedulers = self.ObjectSchedulers or setmetatable( {}, { __mode = "v" } )

  if Scheduler.MasterObject then
    --env.info("FF Object Scheduler")
    self.ObjectSchedulers[CallID] = Scheduler
    self:F3( { CallID = CallID, ObjectScheduler = tostring( self.ObjectSchedulers[CallID] ), MasterObject = tostring( Scheduler.MasterObject ) } )
  else
    --env.info("FF Persistent Scheduler")
    self.PersistentSchedulers[CallID] = Scheduler
    self:F3( { CallID = CallID, PersistentScheduler = self.PersistentSchedulers[CallID] } )
  end

  self.Schedule = self.Schedule or setmetatable( {}, { __mode = "k" } )
  self.Schedule[Scheduler] = self.Schedule[Scheduler] or {}
  self.Schedule[Scheduler][CallID] = {} -- #SCHEDULEDISPATCHER.ScheduleData
  self.Schedule[Scheduler][CallID].Function = ScheduleFunction
  self.Schedule[Scheduler][CallID].Arguments = ScheduleArguments
  self.Schedule[Scheduler][CallID].StartTime = timer.getTime() + ( Start or 0 )
  self.Schedule[Scheduler][CallID].Start = Start + 0.001
  self.Schedule[Scheduler][CallID].Repeat = Repeat or 0
  self.Schedule[Scheduler][CallID].Randomize = Randomize or 0
  self.Schedule[Scheduler][CallID].Stop = Stop

  -- This section handles the tracing of the scheduled calls.
  -- Because these calls will be executed with a delay, we inspect the place where these scheduled calls are initiated.
  -- The Info structure contains the output of the debug.getinfo() calls, which inspects the call stack for the function name, line number and source name.
  -- The call stack has many levels, and the correct semantical function call depends on where in the code AddSchedule was "used".
  --   - Using SCHEDULER:New()
  --   - Using Schedule:AddSchedule()
  --   - Using Fsm:__Func()
  --   - Using Class:ScheduleOnce()
  --   - Using Class:ScheduleRepeat()
  --   - ...
  -- So for each of these scheduled call variations, AddSchedule is the workhorse which will schedule the call.
  -- But the correct level with the correct semantical function location will differ depending on the above scheduled call invocation forms.
  -- That's where the field TraceLevel contains optionally the level in the call stack where the call information is obtained.
  -- The TraceLevel field indicates the correct level where the semantical scheduled call was invoked within the source, ensuring that function name, line number and source name are correct.
  -- There is one quick ...
  -- The FSM class models scheduled calls using the __Func syntax. However, these functions are "tailed".
  -- There aren't defined anywhere within the source code, but rather implemented as triggers within the FSM logic, 
  -- and using the onbefore, onafter, onenter, onleave prefixes. (See the FSM for details).
  -- Therefore, in the call stack, at the TraceLevel these functions are mentioned as "tail calls", and the Info.name field will be nil as a result.
  -- To obtain the correct function name for FSM object calls, the function is mentioned in the call stack at a higher stack level.
  -- So when function name stored in Info.name is nil, then I inspect the function name within the call stack one level higher.
  -- So this little piece of code does its magic wonderfully, performance overhead is negligible, as scheduled calls don't happen that often.

  local Info = {}

  if debug then
    TraceLevel = TraceLevel or 2
    Info = debug.getinfo( TraceLevel, "nlS" )
    local name_fsm = debug.getinfo( TraceLevel - 1, "n" ).name -- #string
    if name_fsm then
      Info.name = name_fsm
    end
  end

  self:T3( self.Schedule[Scheduler][CallID] )

  --- Function passed to the DCS timer.scheduleFunction()
  self.Schedule[Scheduler][CallID].CallHandler = function( Params )

    local CallID = Params.CallID
    local Info = Params.Info or {}
    local Source = Info.source or "?"
    local Line = Info.currentline or "?"
    local Name = Info.name or "?"

    local ErrorHandler = function( errmsg )
      env.info( "Error in timer function: " .. errmsg or "" )
      if BASE.Debug ~= nil then
        env.info( BASE.Debug.traceback() )
      end
      return errmsg
    end

    -- Get object or persistent scheduler object.
    local Scheduler = self.ObjectSchedulers[CallID] -- Core.Scheduler#SCHEDULER
    if not Scheduler then
      Scheduler = self.PersistentSchedulers[CallID]
    end

    -- self:T3( { Scheduler = Scheduler } )

    if Scheduler then

      local MasterObject = tostring( Scheduler.MasterObject )

      -- Schedule object.
      local Schedule = self.Schedule[Scheduler][CallID] -- #SCHEDULEDISPATCHER.ScheduleData

      -- self:T3( { Schedule = Schedule } )

      local SchedulerObject = Scheduler.MasterObject -- Scheduler.SchedulerObject Now is this the Master or Scheduler object?
      local ShowTrace       = Scheduler.ShowTrace

      local ScheduleFunction  = Schedule.Function
      local ScheduleArguments = Schedule.Arguments or {}
      local Start             = Schedule.Start
      local Repeat            = Schedule.Repeat or 0
      local Randomize         = Schedule.Randomize or 0
      local Stop              = Schedule.Stop or 0
      local ScheduleID        = Schedule.ScheduleID

      local Prefix = (Repeat == 0) and "--->" or "+++>"

      local Status, Result
      -- self:E( { SchedulerObject = SchedulerObject } )
      if SchedulerObject then
        local function Timer()
          if ShowTrace then
            SchedulerObject:T( Prefix .. Name .. ":" .. Line .. " (" .. Source .. ")" )
          end
          -- The master object is passed as first parameter. A few :Schedule() calls in MOOSE expect this currently. But in principle it should be removed.
          return ScheduleFunction( SchedulerObject, unpack( ScheduleArguments ) ) 
        end
        Status, Result = xpcall( Timer, ErrorHandler )
      else
        local function Timer()
          if ShowTrace then
            self:T( Prefix .. Name .. ":" .. Line .. " (" .. Source .. ")" )
          end
          return ScheduleFunction( unpack( ScheduleArguments ) )
        end
        Status, Result = xpcall( Timer, ErrorHandler )
      end

      local CurrentTime = timer.getTime()
      local StartTime = Schedule.StartTime

      -- Debug info.
      self:F3( { CallID = CallID, ScheduleID = ScheduleID, Master = MasterObject, CurrentTime = CurrentTime, StartTime = StartTime, Start = Start, Repeat = Repeat, Randomize = Randomize, Stop = Stop } )

      if Status and ((Result == nil) or (Result and Result ~= false)) then

        if Repeat ~= 0 and ((Stop == 0) or (Stop ~= 0 and CurrentTime <= StartTime + Stop)) then
          local ScheduleTime = CurrentTime + Repeat + math.random( -(Randomize * Repeat / 2), (Randomize * Repeat / 2) ) + 0.0001 -- Accuracy
          -- self:T3( { Repeat = CallID, CurrentTime, ScheduleTime, ScheduleArguments } )
          return ScheduleTime -- returns the next time the function needs to be called.
        else
          self:Stop( Scheduler, CallID )
        end

      else
        self:Stop( Scheduler, CallID )
      end
    else
      self:I( "<<<>" .. Name .. ":" .. Line .. " (" .. Source .. ")" )
    end

    return nil
  end

  self:Start( Scheduler, CallID, Info )

  return CallID
end

--- Remove schedule.
-- @param #SCHEDULEDISPATCHER self
-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object.
-- @param #table CallID Call ID.
function SCHEDULEDISPATCHER:RemoveSchedule( Scheduler, CallID )
  self:F2( { Remove = CallID, Scheduler = Scheduler } )

  if CallID then
    self:Stop( Scheduler, CallID )
    self.Schedule[Scheduler][CallID] = nil
  end
end

--- Start dispatcher.
-- @param #SCHEDULEDISPATCHER self
-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object.
-- @param #table CallID (Optional) Call ID.
-- @param #string Info (Optional) Debug info.
function SCHEDULEDISPATCHER:Start( Scheduler, CallID, Info )
  self:F2( { Start = CallID, Scheduler = Scheduler } )

  if CallID then

    local Schedule = self.Schedule[Scheduler][CallID] -- #SCHEDULEDISPATCHER.ScheduleData

    -- Only start when there is no ScheduleID defined!
    -- This prevents to "Start" the scheduler twice with the same CallID...
    if not Schedule.ScheduleID then

      -- Current time in seconds.
      local Tnow = timer.getTime()

      Schedule.StartTime = Tnow -- Set the StartTime field to indicate when the scheduler started.

      -- Start DCS schedule function https://wiki.hoggitworld.com/view/DCS_func_scheduleFunction
      Schedule.ScheduleID = timer.scheduleFunction( Schedule.CallHandler, { CallID = CallID, Info = Info }, Tnow + Schedule.Start )

      self:T( string.format( "Starting SCHEDULEDISPATCHER Call ID=%s ==> Schedule ID=%s", tostring( CallID ), tostring( Schedule.ScheduleID ) ) )
    end

  else

    -- Recursive.
    for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do
      self:Start( Scheduler, CallID, Info ) -- Recursive
    end

  end
end

--- Stop dispatcher.
-- @param #SCHEDULEDISPATCHER self
-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object.
-- @param #string CallID (Optional) Scheduler Call ID. If nil, all pending schedules are stopped recursively.
function SCHEDULEDISPATCHER:Stop( Scheduler, CallID )
  self:F2( { Stop = CallID, Scheduler = Scheduler } )

  if CallID then

    local Schedule = self.Schedule[Scheduler][CallID] -- #SCHEDULEDISPATCHER.ScheduleData

    -- Only stop when there is a ScheduleID defined for the CallID. So, when the scheduler was stopped before, do nothing.
    if Schedule and Schedule.ScheduleID then

      self:T( string.format( "SCHEDULEDISPATCHER stopping scheduler CallID=%s, ScheduleID=%s", tostring( CallID ), tostring( Schedule.ScheduleID ) ) )

      -- Remove schedule function https://wiki.hoggitworld.com/view/DCS_func_removeFunction
      timer.removeFunction( Schedule.ScheduleID )

      Schedule.ScheduleID = nil

    else
      self:T( string.format( "Error no ScheduleID for CallID=%s", tostring( CallID ) ) )
    end

  else

    for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do
      self:Stop( Scheduler, CallID ) -- Recursive
    end

  end
end

--- Clear all schedules by stopping all dispatchers.
-- @param #SCHEDULEDISPATCHER self
-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object.
function SCHEDULEDISPATCHER:Clear( Scheduler )
  self:F2( { Scheduler = Scheduler } )

  for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do
    self:Stop( Scheduler, CallID ) -- Recursive
  end
end

--- Show tracing info.
-- @param #SCHEDULEDISPATCHER self
-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object.
function SCHEDULEDISPATCHER:ShowTrace( Scheduler )
  self:F2( { Scheduler = Scheduler } )
  Scheduler.ShowTrace = true
end

--- No tracing info.
-- @param #SCHEDULEDISPATCHER self
-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object.
function SCHEDULEDISPATCHER:NoTrace( Scheduler )
  self:F2( { Scheduler = Scheduler } )
  Scheduler.ShowTrace = false
end

