function statsfun = statsfunhelper(inp1, inp2)
% Helper tool to create a statsfun for the options structure of solvers.
%
% function statsfun = statsfunhelper(name, fun)
% function statsfun = statsfunhelper(S)
%
% Usage with (name, fun):
%
% Input 1: name is a string which is a valid field name (no spaces, starts
% with a letter or an underscore, only alphanumeric characters and
% underscores).
% 
% Input2: fun is a function handle with one output and 1 to 4 inputs, as
% follows (your choice):
% 
%  fun(x)  or  fun(problem, x)  or  
%  fun(problem, x, stats)  or  fun(problem, x, stats, store)
% 
% where the inputs are the ones that would be given to options.statsfun, as
% described in the help of the solver used. Typically, x is the point on
% the manifold at the current iterate, problem is the Manopt problem
% structure, stats is all the current statistics recorded for that iterate
% and store is the cache structure at the current iterate.
%
% When calling a Manopt solver with the options structure, such as for
% example with:
%
%  [x, xcost, info] = steepestdescent(problem, [], options);
%
% you may set a field of the options structure as follows:
%
%  options.statsfun = statsfunhelper('nameofthefield', fun);
%
% As a result, at each iteration, the stats structure will contain a field
% stats.nameofthefield with the value returned by the call to fun at that
% iterate. The stats structures are stored in the struct-array info.
% As an example, if the value returned by fun is a scalar, then
% [info.nameofthefield] is a vector containing all returned values.
%
%
% Usage with S:
%
% The input S is a structure. For each field of S, say S.field, the stats
% structure will be augmented with stats.field = fun(..), where fun is the
% function handle stored in S.field, and with the same conventions as
% above. This version allows to record more than one bit of information at
% each iteration. Example:
% 
%  metrics.nameofthefield = fun;
%  metrics.othername = otherfun;
%  options.statsfun = statsfunhelper(metrics);
%
% The different function handles (here, fun and otherfun) can take 1 to 4
% inputs too, and they do not have to take the same number of inputs.
%
% Special field name:
%
% If a function with fieldname 'norecord' is specified, then the
% corresponding function is called without outputs, and no field is created
% in the stats structure for that function.
%
% This is especially helpful to display text or graphical information only.
%
% Example:
%
%  metrics.norecord = @(x) my_plotting_function_without_outputs(x);

% This file is part of Manopt: www.manopt.org.
% Original author: Nicolas Boumal, Dec. 17, 2014.
% Contributors: 
% Change log: 
%     Jan 2, 2021 (NB):
%         Passing S to thestatsfun explicitly for compatibility with Octave 6.
%     Apr 4, 2025 (NB):
%         Added special treatment for field name 'norecord', giving no output.

    if (nargin == 1) && isstruct(inp1)
        S = inp1;
    elseif (nargin == 2)
        S = struct(inp1, inp2);
    else
        error('statsfunhelper takes 1 or 2 inputs. If 1 input, it must be a structure.');
    end

    statsfun = @(problem, x, stats, store) thestatsfun(S, problem, x, stats, store);

    
    function stats = thestatsfun(S, problem, x, stats, store)
        names = fieldnames(S);
        for it = 1 : length(names)
            name = names{it};
            fun = S.(name);
            if ~strcmp('norecord', name)
                switch nargin(fun)
                    case 1
                        stats.(name) = fun(x);
                    case 2
                        stats.(name) = fun(problem, x);
                    case 3
                        stats.(name) = fun(problem, x, stats);
                    case 4
                        stats.(name) = fun(problem, x, stats, store);
                    otherwise
                        error('The functions passed to statsfunhelper must take 1 to 4 inputs.');
                end
            else
                switch nargin(fun)
                    case 1
                        fun(x);
                    case 2
                        fun(problem, x);
                    case 3
                        fun(problem, x, stats);
                    case 4
                        fun(problem, x, stats, store);
                    otherwise
                        error('The functions passed to statsfunhelper must take 1 to 4 inputs.');
                end
            end
        end
    end

end
