--- **Core** - Timer scheduler.
--
-- **Main Features:**
--
--    * Delay function calls
--    * Easy set up and little overhead
--    * Set start, stop and time interval
--    * Define max function calls
--
-- ===
--
-- ### Author: **funkyfranky**
-- @module Core.Timer
-- @image Core_Scheduler.JPG


--- TIMER class.
-- @type TIMER
-- @field #string ClassName Name of the class.
-- @field #string lid Class id string for output to DCS log file.
-- @field #number tid Timer ID returned by the DCS API function.
-- @field #number uid Unique ID of the timer.
-- @field #function func Timer function.
-- @field #table para Parameters passed to the timer function.
-- @field #number Tstart Relative start time in seconds.
-- @field #number Tstop Relative stop time in seconds.
-- @field #number dT Time interval between function calls in seconds.
-- @field #number ncalls Counter of function calls.
-- @field #number ncallsMax Max number of function calls. If reached, timer is stopped.
-- @field #boolean isrunning If `true`, timer is running. Else it was not started yet or was stopped.
-- @extends Core.Base#BASE

--- *Better three hours too soon than a minute too late.* - William Shakespeare
--
-- ===
--
-- # The TIMER Concept
-- 
-- The TIMER class is the little sister of the @{Core.Scheduler#SCHEDULER} class. It does the same thing but is a bit easier to use and has less overhead. It should be sufficient in many cases.
-- 
-- It provides an easy interface to the DCS [timer.scheduleFunction](https://wiki.hoggitworld.com/view/DCS_func_scheduleFunction).
-- 
-- # Construction
-- 
-- A new TIMER is created by the @{#TIMER.New}(*func*, *...*) function
-- 
--     local mytimer=TIMER:New(myfunction, a, b)
--     
-- The first parameter *func* is the function that is called followed by the necessary comma separeted parameters that are passed to that function.
-- 
-- ## Starting the Timer
-- 
-- The timer is started by the @{#TIMER.Start}(*Tstart*, *dT*, *Duration*) function
-- 
--     mytimer:Start(5, 1, 20)
--     
-- where
-- 
-- * *Tstart* is the relative start time in seconds. In the example, the first function call happens after 5 sec.
-- * *dT* is the time interval between function calls in seconds. Above, the function is called once per second.
-- * *Duration* is the duration in seconds after which the timer is stopped. This is relative to the start time. Here, the timer will run for 20 seconds.
--  
-- Note that
-- 
-- * if *Tstart* is not specified (*nil*), the first function call happens immediately, i.e. after one millisecond.
-- * if *dT* is not specified (*nil*), the function is called only once.
-- * if *Duration* is not specified (*nil*), the timer runs forever or until stopped manually or until the max function calls are reached (see below).
--
-- For example,
-- 
--     mytimer:Start(3)            -- Will call the function once after 3 seconds.
--     mytimer:Start(nil, 0.5)     -- Will call right now and then every 0.5 sec until all eternity.
--     mytimer:Start(nil, 2.0, 20) -- Will call right now and then every 2.0 sec for 20 sec.
--     mytimer:Start(1.0, nil, 10) -- Does not make sense as the function is only called once anyway. 
--
-- ## Stopping the Timer
--
-- The timer can be stopped manually by the @{#TIMER.Stop}(*Delay*) function
-- 
--     mytimer:Stop()
--
-- If the optional paramter *Delay* is specified, the timer is stopped after *delay* seconds.
-- 
-- ## Limit Function Calls
-- 
-- The timer can be stopped after a certain amount of function calles with the @{#TIMER.SetMaxFunctionCalls}(*Nmax*) function
-- 
--     mytimer:SetMaxFunctionCalls(20)
--
-- where *Nmax* is the number of calls after which the timer is stopped, here 20.
-- 
-- For example,
-- 
--     mytimer:SetMaxFunctionCalls(66):Start(1.0, 0.1)
--     
-- will start the timer after one second and call the function every 0.1 seconds. Once the function has been called 66 times, the timer is stopped.
-- 
--
-- @field #TIMER
TIMER = {
  ClassName      = "TIMER",
  lid            =   nil,
}

--- Timer ID.
_TIMERID=0

--- TIMER class version.
-- @field #string version
TIMER.version="0.2.0"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: Randomization.
-- TODO: Pause/unpause.
-- DONE: Write docs.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new TIMER object.
-- @param #TIMER self
-- @param #function Function The function to call.
-- @param ... Parameters passed to the function if any.
-- @return #TIMER self
function TIMER:New(Function, ...)

  -- Inherit BASE.
  local self=BASE:Inherit(self, BASE:New()) --#TIMER
  
  -- Function to call.
  self.func=Function
  
  -- Function arguments.
  self.para=arg or {}
  
  -- Number of function calls.
  self.ncalls=0
  
  -- Not running yet.
  self.isrunning=false
  
  -- Increase counter
  _TIMERID=_TIMERID+1
  
  -- Set UID.
  self.uid=_TIMERID
  
  -- Log id.
  self.lid=string.format("TIMER UID=%d | ", self.uid)
  
  return self
end

--- Start TIMER object.
-- @param #TIMER self
-- @param #number Tstart Relative start time in seconds.
-- @param #number dT Interval between function calls in seconds. If not specified `nil`, the function is called only once.
-- @param #number Duration Time in seconds for how long the timer is running. If not specified `nil`, the timer runs forever or until stopped manually by the `TIMER:Stop()` function.
-- @return #TIMER self
function TIMER:Start(Tstart, dT, Duration)

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

  -- Start time in sec.
  self.Tstart=Tstart and Tnow+math.max(Tstart, 0.001) or Tnow+0.001  -- one millisecond delay if Tstart=nil
  
  -- Set time interval.
  self.dT=dT
  
  -- Stop time.
  if Duration then
    self.Tstop=self.Tstart+Duration
  end
  
  -- Call DCS timer function.
  self.tid=timer.scheduleFunction(self._Function, self, self.Tstart)
  
  -- Set log id.
  self.lid=string.format("TIMER UID=%d/%d | ", self.uid, self.tid)
  
  -- Is now running.
  self.isrunning=true
  
  -- Debug info.
  self:T(self.lid..string.format("Starting Timer in %.3f sec, dT=%s, Tstop=%s", self.Tstart-Tnow, tostring(self.dT), tostring(self.Tstop)))

  return self
end

--- Start TIMER object if a condition is met. Useful for e.g. debugging.
-- @param #TIMER self
-- @param #boolean Condition Must be true for the TIMER to start
-- @param #number Tstart Relative start time in seconds.
-- @param #number dT Interval between function calls in seconds. If not specified `nil`, the function is called only once.
-- @param #number Duration Time in seconds for how long the timer is running. If not specified `nil`, the timer runs forever or until stopped manually by the `TIMER:Stop()` function.
-- @return #TIMER self
function TIMER:StartIf(Condition,Tstart, dT, Duration)
  if Condition then
    self:Start(Tstart, dT, Duration)
  end
  return self
end

--- Stop the timer by removing the timer function.
-- @param #TIMER self
-- @param #number Delay (Optional) Delay in seconds, before the timer is stopped.
-- @return #TIMER self
function TIMER:Stop(Delay)

  if Delay and Delay>0 then
  
    self.Tstop=timer.getTime()+Delay
    
  else

    if self.tid then
    
      -- Remove timer function.
      self:T(self.lid..string.format("Stopping timer by removing timer function after %d calls!", self.ncalls))

      -- We use a pcall here because if the DCS timer does not exist any more, it crashes the whole script!
      local status=pcall(
        function ()
          timer.removeFunction(self.tid)
        end 
      )
      
      -- Debug messages.
      if status then
        self:T2(self.lid..string.format("Stopped timer!"))
      else
        self:E(self.lid..string.format("WARNING: Could not remove timer function! isrunning=%s", tostring(self.isrunning)))
      end
      
      -- Not running any more.
      self.isrunning=false

    end
    
  end

  return self
end

--- Set max number of function calls. When the function has been called this many times, the TIMER is stopped.
-- @param #TIMER self
-- @param #number Nmax Set number of max function calls.
-- @return #TIMER self
function TIMER:SetMaxFunctionCalls(Nmax)
  self.ncallsMax=Nmax
  return self
end

--- Set time interval. Can also be set when the timer is already running and is applied after the next function call.
-- @param #TIMER self
-- @param #number dT Time interval in seconds.
-- @return #TIMER self
function TIMER:SetTimeInterval(dT)
  self.dT=dT
  return self
end

--- Check if the timer has been started and was not stopped.
-- @param #TIMER self
-- @return #boolean If `true`, the timer is running.
function TIMER:IsRunning()
  return self.isrunning
end

--- Call timer function.
-- @param #TIMER self
-- @param #number time DCS model time in seconds.
-- @return #number Time when the function is called again or `nil` if the timer is stopped.
function TIMER:_Function(time)

  -- Call function.
  self.func(unpack(self.para))
  
  -- Increase number of calls.
  self.ncalls=self.ncalls+1
  
  -- Next time.
  local Tnext=self.dT and time+self.dT or nil
  
  -- Check if we stop the timer.
  local stopme=false
  if Tnext==nil then
    -- No next time.
    self:T(self.lid..string.format("No next time as dT=nil ==> Stopping timer after %d function calls", self.ncalls))
    stopme=true
  elseif self.Tstop and Tnext>self.Tstop then
    -- Stop time passed.
    self:T(self.lid..string.format("Stop time passed %.3f > %.3f ==> Stopping timer after %d function calls", Tnext, self.Tstop, self.ncalls))
    stopme=true
  elseif self.ncallsMax and self.ncalls>=self.ncallsMax then
    -- Number of max function calls reached.
    self:T(self.lid..string.format("Max function calls Nmax=%d reached ==> Stopping timer after %d function calls", self.ncallsMax, self.ncalls))
    stopme=true
  end
  
  if stopme then
    -- Remove timer function.
    self:Stop()
    return nil
  else
    -- Call again in Tnext seconds.
    return Tnext
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------