## Copyright (C) 2019 Emmanuel Farhi
##
## This file is part of Octave.
##
## Octave is free software; you can redistribute it and/or modify it
## under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 3 of the License, or (at
## your option) any later version.
##
## Octave is distributed in the hope that it will be useful, but
## WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
## General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with Octave; see the file COPYING.  If not, see
## <http://www.gnu.org/licenses/>.

classdef timer < handle
## -*- texinfo -*-
## @deftypefn  {Function File} timer
## Timer Object Properties and Methods.
##
## Use a timer to schedule one or multiple executions of tasks comprised of  
## callback functions. If a timer is scheduled to execute multiple times, you can
## specify the time between executions.
##
## @multitable @columnfractions 0.25 0.65
## @headitem Timer properties @tab Description
## @item AveragePeriod  @tab Average number of seconds between TimerFcn executions.
## @item BusyMode       @tab Action taken when TimerFcn executions are in progress.
## @item ErrorFcn       @tab Callback function executed when an error occurs.
## @item ExecutionMode  @tab Mode used to schedule timer events.
## @item InstantPeriod  @tab Elapsed time between the last two TimerFcn executions. 
## @item Name           @tab Descriptive name of the timer object.
## @item Period         @tab Seconds between TimerFcn executions.
## @item Running        @tab Timer object running status.
## @item StartDelay     @tab Delay between START and the first scheduled TimerFcn execution.
## @item StartFcn       @tab Callback function executed when timer object starts.
## @item StopFcn        @tab Callback function executed after timer object stops.
## @item Tag            @tab Label for object.
## @item TasksExecuted  @tab Number of TimerFcn executions that have occurred.
## @item TasksToExecute @tab Number of times to execute the TimerFcn callback.
## @item time           @tab Time when the timer was last updated.
## @item TimerFcn       @tab Callback function executed when a timer event occurs.
## @item UserData       @tab User data for timer object.
## @end multitable
## 
## @multitable @columnfractions 0.15 0.8
## @headitem Timer methods @tab Description
## @item timer          @tab Construct timer object.
## @item delete         @tab Remove timer object from memory.
## @item display        @tab Display method for timer objects.
## @item start          @tab Start timer object running.
## @item startat        @tab Start timer object running at a specified time.
## @item stop           @tab Stop timer object running.
## @item waitfor        @tab Wait for timer object to stop running.
## @end multitable
##
## Example:
##   @code{t=timer('TimerFcn','disp OK','Period',3,'ExecutionMode','fixedDelay'); start(t);}
##
## See: https://gitlab.com/farhi/octave-timer
## @seealso{handle}
## @end deftypefn

  properties
    ErrorFcn      =[];            # Callback function executed when an error occurs.
    ExecutionMode ='singleShot';  # Mode used to schedule timer events (fixedDelay, singleShot)
    Name          ='';            # Descriptive name of the timer object.
    Period        =1;             # Seconds between TimerFcn executions.
    StartDelay    =0;             # Delay between START and the first scheduled TimerFcn execution.
    StartFcn      =[];            # Callback function executed when timer object starts. StartFcn(obj, evt)
    StopFcn       =[];            # Callback function executed after timer object stops. StopFcn(obj, evt)
    Tag           ='';            # Label for object.
    TasksToExecute=Inf;           # Number of times to execute the TimerFcn callback.
    TimerFcn      =[];            # Callback function executed when a timer event occurs.
    UserData      =[];            # User data for timer object.
  endproperties # properties
  
  properties(SetAccess=protected)
    AveragePeriod =nan;     # Average number of seconds between TimerFcn executions.
    BusyMode      ='drop';  # Action taken when TimerFcn executions are in progress.
    InstantPeriod =nan;     # Elapsed time between the last two TimerFcn executions. 
    Running       ='off';   # Timer object running status.
    TasksExecuted =0;       # Number of TimerFcn executions that have occurred.
    time          =[];      # Time when the timer was last updated.
  endproperties # read-only properties
  
  properties(Access=protected)
    SumPeriod=0;
    Hook_ID
  endproperties
  
  methods
    function self = timer(varargin)
    ## TIMER Construct timer object.
    ## T = timer constructs a timer object with default attributes.
    ##
    ## T = timer('PropertyName1',PropertyValue1, 'PropertyName2', PropertyValue2,...)
    ## constructs a timer object in which the given Property name/value pairs are
    ## set on the object.
    ##
    ## Example: To construct a timer object with a timer callback mycallback and a 10s interval:
    ## t = timer('TimerFcn',@mycallback, 'Period', 10.0);
      
      # fill properties from input arguments
      [reg, prop] = parseparams({ varargin{:} });
      for index=1:2:numel(prop)
        if ischar(prop{index}) && index < numel(prop)
          try
          self.(prop{index}) = prop{index+1};
          end
        endif
      endfor
      if isempty(self.Name), 
        [p,f] = fileparts(tempname);
        self.Name=[ 'timer-' f ];
      endif
      
      # add the timer callback to the input loop
      self.Hook_ID = add_input_event_hook(@(s)event(s), self);
    endfunction
    
    function delete(self)
    ## DELETE Remove timer object from memory.
      if ~isempty(self.Hook_ID)
        remove_input_event_hook(self.Hook_ID);
      endif
    endfunction
    
    function display(self)
    ## DISPLAY Display method for timer objects.
      disp([ 'Timer Object: ' self.Name ' ' self.Tag])
      disp([ 'Timer Settings' ]);
      disp([ '  ExecutionMode: ' self.ExecutionMode ]);
      disp([ '         Period: ' num2str(self.Period) ]);
      disp([ '       BusyMode: ' self.BusyMode ]);
      disp([ '        Running: ' self.Running ]);
      disp([ '  Callbacks' ]);
      disp([ '       TimerFcn:' char(self.TimerFcn) ]);
      if ~isempty(self.ErrorFcn)
        disp([ '       ErrorFcn:' char(self.ErrorFcn) ]);
      endif
      if ~isempty(self.StartFcn)
        disp([ '       StartFcn:' char(self.StartFcn) ]);
      endif
      if ~isempty(self.StopFcn)
        disp([ '        StopFcn:' char(self.StopFcn)  ]);
      endif
    endfunction
    
    function start(self)
    ## START Start timer(s) running.
    ##  START sets the Running property of the timer object, OBJ, to 'On',
    ##  initiates TimerFcn callbacks, and executes the StartFcn callback.
      self.time = time(); # since Jan 1 1970
      try
        timer_feval(self,'StartFcn');
      catch
        timer_feval(self,'ErrorFcn');
      end
      self.Running = 'on';
    endfunction
    
    function startat(self, varargin)
    ## STARTAT Start timer(s) running at the specified time.
    ##   STARTAT(S, firingTime) uses firingTime (e.g. as char of datevec)
      if nargin < 2, start(self); endif
      # time=since 1 Jan 1970; now and datenum=since 1 Jan 0000
      self.StartDelay = datenum(varargin{:}) - now; 
    endfunction
    
    function stop(self)
    ## STOP Stop timer(s).
    ##  STOP sets the Running property of the timer object, OBJ,
    ##  to 'Off', halts further TimerFcn callbacks, and executes the 
    ##  StopFcn callback.
      self.Running = 'off';
      try
        timer_feval(self,'StopFcn');
      catch
        timer_feval(self,'ErrorFcn');
      end
    endfunction
    
    function waitfor(self)
    ## WAITFOR Block command prompt until timer stops running.
      while ~strcmpi(self.Running,'off')
        pause(self.Period);
        event(self);
      endwhile
    endfunction
    
    function event(self)
    ## EVENT Timer callback event (e.g. calls TimerFcn).
      t = time();
      if strcmpi(self.Running,'on') && (t > self.time + self.Period)
        if self.TasksExecuted == 0 && t < self.time + self.StartDelay
          # StartDelay not yet passed
          return
        endif
        try
          timer_feval(self, 'TimerFcn');
        catch
          timer_feval(self,'ErrorFcn');
        end
        self.TasksExecuted = self.TasksExecuted+1;
        self.InstantPeriod = t - self.time;
        self.SumPeriod     = self.SumPeriod + self.InstantPeriod;
        self.AveragePeriod = self.SumPeriod / self.TasksExecuted;
        self.time          = t; # since Jan 1 1970
        if self.TasksExecuted >= self.TasksToExecute || strcmpi(self.ExecutionMode,'singleShot')
          stop(self);
        endif
      endif
    endfunction
    
  endmethods # methods
  
  methods(Access=protected)
    function timer_feval(self, f, varargin)
    ## TIMER_FEVAL Evalate timer callback.
    ##   Callbacks should be f(obj, evt, ...)
      if isempty(f), return; endif
      fun = self.(f);
      if isempty(fun), return; endif
      evt.Data = self; # time since 1 Jan 0000
      evt.Type = f;
      if ischar(fun) || isa(fun, 'function_handle')
        feval(fun, self, evt, varargin{:})
      elseif iscell(fun)
        feval(fun{1}, self, evt, fun{2:end}, varargin{:})
      else
        disp([ 'Unkown callback ' f ' type. Must be char, function_handle or cell, but is: ' class(fun) ])
      endif
    endfunction
  endmethods
  
endclassdef # timer

## Test for timer
## check normal use
%!test
%!  obj = timer('TimerFcn', @(o,e)disp(o.TasksExecuted), 'ExecutionMode','fixedDelay');
%!  obj.TasksToExecute = 2;
%!  start(obj)
%!  waitfor(obj);
%!  assert (obj.TasksExecuted, 2)

# ------------------------------------------------------------------------------
# the idea to build a 'timer' class a-la Matlab came from
#   https://savannah.gnu.org/bugs/?32316
# In this implementation, we have omitted:
#   timerfind
#   timerfindall
#   get
#   set
#   BusyMode is fixed as "drop"
