function api=iterate_timed(varargin)
% iterate_timed - Iterate function under control of timer
%
% iterate_timed :: 
%    (A=>A)          ~'state transformer action',
%    A               ~'initial state',
%    real            ~'timer between updates in seconds',
%    options iterate_timed_options ~'options (see below)'
% -> sched(A) ~'scheduler API'.
%
% iterate_timed :: 
%    cell {
%       (A=>A)          ~'state transformer action',
%       A               ~'initial state'
%    } ~'iterated system as a cell array',
%    real            ~'timer between updates in seconds',
%    options iterate_timed_options ~'options (see below)'
% -> sched(A) ~'scheduler API'.
%
% iterate_timed_options = {
%       drawnow   :: {0,1} /0 ~'call drawnow after each iteration';
%       busy_mode :: {'queue','drop'} /'queue'  ~'See TIMER';
%       exec_mode :: {'fixedRate','fixedDelay','fixedSpacing'} /'fixedRate' ~'See TIMER';
%       its       :: natural / inf ~'iteration limit';
%       onstart   :: A->void / @nop ~'do this when timer starts';
%       onstop    :: A->void / @nop ~'do this when timer stops';
%       onfinish  :: A->void / @nop ~'do this when iterator terminates';
%       pre       :: A->void / []  ~'something to do before each iteration';
%       post      :: A->void / []  ~'something to do after each iteration';
%       drawnow   :: bool / 0      ~'whether to update graphics every it';
%       defer     :: bool / 0      ~'if true then don't start the timer';
%       props     :: {[[_]]} / {}  ~'extra name/value pairs for timer'
% }
%
% sched(A) ::= struct {
%		dispose   :: void => void;
%		isrunning :: void => bool;
%		startat	 :: real => void;
%		start		 :: void => void;
%		stop		 :: void => void;
%		rewind	 :: void => void;
%		getstate	 :: void => A;
%		setstate	 :: A    => void
% }.
%	
% NB: Unlike ITERATE, this does NOT respect the id, save, and recover properties.
% Neither does it respect the OPTPAUSE property 'pause'.

	warning('off','MATLAB:TIMER:STARTDELAYPRECISION');
	warning('off','MATLAB:TIMER:RATEPRECISION');

	if iscell(varargin{1})
		[nextfn,X0]=varargin{1}{:};
		T=varargin{2};
		options=varargin(3:end);
	else
		nextfn=varargin{1};
		X0=varargin{2};
		T=varargin{3};
		options=varargin(4:end);
	end

	opts=prefs('its',inf,'drawnow',0, 'quiet', 0, 'defer', 0, ...
		'busy_mode','queue','exec_mode','fixedRate', ...
		'onfinish',@nop,'onstart',@nop,'onstop',@nop,options{:});
	
	X=X0; %% !!! NB: this is a mutable shared local variable used by timercb

	if ~isfield(opts,'pre') && ~isfield(opts,'post') && ~opts.drawnow
		stfn=nextfn; % streamlined version with no checks
	else % version with calls to pre,post, and drawnow
		prefn=getparam(opts,'pre',@nop);
		postfn=getparam(opts,'post',@nop);
		stfn=@bracket;
	end
	
	tm=timer('ExecutionMode',opts.exec_mode,'BusyMode',opts.busy_mode, ...
			'TimerFcn',@timercb,'StartFcn',@startfn,'StopFcn',@stopfn, ...
			'TasksToExecute',opts.its,'Period',T);

	api=struct( ...
		'dispose',  @()delete(tm), ...
		'isrunning',@()isrunning(tm), ...
		'startat',	@(t)startat(tm,t/86400), ...
		'start',		@()start(tm), ...
		'stop',		@()stop(tm), ...
		'rewind',	@()setstate(X0), ...
		'getstate',	@getstate, ...
		'setstate',	@setstate ...
	);

	if ~opts.defer, start(tm); end

	function setstate(x), X=x; end
	function x=getstate,  x=X; end

	function X=bracket(X), 
		prefn(X); X=nextfn(X); postfn(X); 
		if opts.drawnow, drawnow; end
	end

	function startfn(tm,a)
		if ~opts.quiet, status(tm,'starting',X,a.Data.time); end 
		opts.onstart(X); 
	end

	function stopfn(tm,a)
		if ~opts.quiet, status(tm,'stopping',X,a.Data.time); end 
		opts.onstop(X); 
		if isempty(X), 
			if ~opts.quiet, status(tm,'finished',X,a.Data.time); end 
			opts.onfinish(X); 
		end
	end

	% well, we have mutable variables captured by closure so we may 
	% as well use them...
	function timercb(tm,ev)
		if isempty(X), stop(tm); else, X=stfn(X); end
	end
end

function status(tm,msg,x,t),
	fprintf('| %s %s at %s with %s\n',get(tm,'name'),msg,mat2str(t),tostring(x)); 
end

