function [outputs, errorInfo, elapsed] = RunFunction(parentFolder, funcName, inputs, shouldStepIn)
% RunFunction  Run a function from a folder.  parentFolder must be an 
% absolute path or empty.  When parentFolder is empty, MATLAB searchs 
% funcName in its search path.
%
% When executing a function, inputs is a cell array of input arguments that are
% passed to the function and outputs is a cell array of outputs returned by the
% function.  numOuts must be a number of output variables to request from the
% function.
%
% Returns in outputs, a cell array of output arguments.  If the function
% returns no outpupts, then outputs is an empty cell array.  The errorInfo
% output returns an error if the function produced an error or an empty cell
% array if there were no errors.  The errorInfo cell array contains 2 char
% vectors.  The first is the error message.  The second cell is the error
% message ID string.  The elapsed is a numeric array containing performance
% metrics.
%
% Copyright (c) 1984-2018, The MathWorks, Inc.
% All rights reserved.
% Redistribution and use in source and binary forms, with or without 
% modification, are permitted provided that the following conditions are 
% met:
% 1. Redistributions of source code must retain the above copyright notice,
%    this list of conditions and the following disclaimer.
% 2. Redistributions in binary form must reproduce the above copyright 
%    notice, this list of conditions and the following disclaimer in the
%    documentation and/or other materials provided with the distribution.
% 3. In all cases, the software is, and all modifications and derivatives 
%    of the software shall be, licensed to you solely for use in 
%    conjunction with MathWorks products and service offerings. 
% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
% "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
% TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 
% PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER 
% OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
% EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
% PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
% PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
% LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

    outputs = {};
    errorInfo = {};
    elapsed = zeros(1, 3);

    if isstring(parentFolder)
        parentFolder = char(parentFolder);
    end
    if isstring(funcName)
        funcName = char(funcName);
    end

    try
        % If the input had a path, CD to that path if it exists
        if ~isempty(parentFolder)
            cd(parentFolder);
        end
        if (shouldStepIn)
            % Add a breakpoint as we are stepping in.
            filePath = fullfile(parentFolder, funcName);
            dbstop(filePath);
        end

        [outputs, elapsed] = localCallFunction(funcName, inputs);
    catch ME
        errorInfo{1} = ME.message;
        if strcmp(ME.identifier, 'NI:')
            errorInfo{2} = ME.identifier;
        elseif length(ME.stack) >= 3 && strcmp(ME.stack(end-2).name, 'UnescapeForCom')
            errorInfo{2} = 'NI:SetVariable';
        elseif length(ME.stack) >= 3 && strcmp(ME.stack(end-2).name, 'EscapeForCom')
            errorInfo{2} = 'NI:GetVariable';
        elseif length(ME.stack) >= 2 && strcmp(ME.stack(end-1).name, 'localCallFunction')
            % Error from user file
            errorInfo{2} = ME.identifier;
        else
            errorInfo{2} = 'NI:Unexpected';
        end
    end
end

%Call a Function
function [outputs__, elapsed__] = localCallFunction(funcName__, inputs__)
    elapsed__ = zeros(1, 3);
    t0 = tic;
    for k = 1:numel(inputs__)
        inputs__{k} = niifm.UnescapeForCom(inputs__{k});
    end
    elapsed__(1) = toc(t0);
    nargout__ = getOutputArgumentsCount(funcName__);
    if nargout__ > 0
        t0 = tic;
        [outputs__{1:nargout__}] = feval(funcName__, inputs__{:});
        elapsed__(2) = toc(t0);
        t0 = tic;
        for k = 1:numel(outputs__)
            outputs__{k} = niifm.EscapeForCom(outputs__{k});
        end
        elapsed__(3) = toc(t0);
    else
        outputs__ = {};
        t0 = tic;
        feval(funcName__, inputs__{:});
        elapsed__(2) = toc(t0);
    end
end

% Returns the number of the output arguments for the function.
function n = getOutputArgumentsCount(funcName__)
    try
        n = nargout(funcName__);
    catch ME
% It is observed that after executing MATLAB source from a specific 
% directory,new sources added to the same directory during the same
% session are failing to execute with an error "Not a valid Mfile" error
% in nargout function call. A 'hack' to address this issue is to change
% the directory to an alternative location and then back to the source
% directory.
        switch(ME.identifier)
            case 'MATLAB:narginout:notValidMfile'
                currentDirectory = pwd;
                altDirectory = matlabroot;                
                if strcmp(currentDirectory, matlabroot)
                    altDirectory = fullfile(matlabroot, '..');
                end
                cd(altDirectory);
                cd(currentDirectory);
                n = nargout(funcName__);
        end
    end
end

