function layer = functionLayer(fun, nameValueArgs)
% functionLayer   Function layer
%
%   LAYER = functionLayer(FUN) creates a function layer for function handle
%   or differentiable function (subclass of deep.DifferentiableFunction) 
%   FUN. The layer operation is given by FUN(X1, ..., Xn), where X1, ... Xn 
%   are the layer inputs.
%
%   LAYER = functionLayer(FUN, 'PARAM1', VAL1, 'PARAM2', VAL2, ...)
%   specifies optional parameter name/value pairs for creating the layer:
%
%       'NumInputs'       - Number of layer inputs. If the value is
%                           empty the number is automatically computed.
%                           The default is [].
%       'NumOutputs'      - Number of layer outputs. If the value is
%                           empty the number is automatically computed.
%                           The default is [].
%       'InputNames'      - Names of the layer inputs. If the value is
%                           empty the names are automatically computed.
%                           The default is an empty string array.
%       'OutputNames'     - Names of the layer outputs. If the value is
%                           empty the names are automatically computed.
%                           The default is an empty string array.
%       'Formattable'     - Logical scalar that indicates if FUN operates
%                           on formatted dlarray objects. The values are
%                           false (default) or true.
%       'Acceleratable'   - Logical scalar that indicates if FUN supports
%                           acceleration using dlaccelerate. The values
%                           are false (default) or true.
%       'Name'            - Name for the layer. The default is ''.
%       'Description'     - Description for the layer. If empty the string
%                           form of FUN is used. The default is ''.
%
%   Example:
%       Create a function layer.
%
%       fun = @(x)x./(1+exp(-x));
%       layer = functionLayer(fun);
%
%   See also nnet.cnn.layer.FunctionLayer, deep.DifferentiableFunction
%
%   <a href="matlab:helpview('deeplearning','list_of_layers')">List of Deep Learning Layers</a>

%   Copyright 2020-2023 The MathWorks, Inc.

arguments
    fun(1,1) {iAssertValidFunction}
end

arguments
    nameValueArgs.Name {iAssertValidLayerName} = ''
    nameValueArgs.Description {iAssertValidText} = ''
    nameValueArgs.NumInputs {iAssertValidNumInputsOutputs} = []
    nameValueArgs.NumOutputs {iAssertValidNumInputsOutputs} = []
    nameValueArgs.InputNames = strings(1,0)
    nameValueArgs.OutputNames = strings(1,0)
    nameValueArgs.Formattable(1,1) {iAssertBinary} = false
    nameValueArgs.Acceleratable(1,1) {iAssertBinary} = false
end

% prepare inputs to the layer constructor
ctorArgs = nameValueArgs;
try
    [funNargin, funNargout] = iNarginNargout(fun);
    [ctorArgs.NumInputs, requestedNumInputs] = iCheckNumberAndNames(nameValueArgs.NumInputs,...
                                                                    nameValueArgs.InputNames, funNargin, 'Input', fun);
    iCheckMaxNumInputs(fun, requestedNumInputs);
    nout = iComputeDefaultNumOutputs(fun, funNargout);
    ctorArgs.NumOutputs = iCheckNumberAndNames(nameValueArgs.NumOutputs,...
                                               nameValueArgs.OutputNames, nout, 'Output', fun);
    ctorArgs.PredictFcn = fun;
    ctorArgs.Description = iComputeDescription(nameValueArgs.Description, fun);
catch err
    throw(err);
end
layer = nnet.cnn.layer.FunctionLayer(ctorArgs);
end

function [num, requestedNum] = iCheckNumberAndNames(num, names, defaultNum, paramRoot, fun)
if isempty(num) && isempty(names)
    if isnan(defaultNum)
        % Function not found, hence requires explicit input/output information.
        funName = func2str(fun);
        error(message('nnet_cnn:layer:FunctionLayer:Explicit',funName));
    elseif defaultNum < 0 % varargin or varargout
        msgID = ['nnet_cnn:layer:FunctionLayer:Ambiguous' paramRoot];
        error(message(msgID));
    end
    requestedNum = 0;
    num = defaultNum;
elseif ~isempty(num) && ~isempty(names)
    if num ~= length(names)
        numParam = ['Num' paramRoot 's'];
        namesParam = [paramRoot 'Names'];
        error(message('nnet_cnn:layer:FunctionLayer:NumberMustMatch', numParam, namesParam))
    end
    requestedNum = num;
    num = [];  % Setting names is enough to set number by side effect.
elseif isempty(num)
    requestedNum = length(names);
else
    requestedNum = num;
end
end

function iAssertValidLayerName(name)
nnet.internal.cnn.layer.paramvalidation.validateLayerName(name);
end

function iAssertValidNumInputsOutputs(value)
if ~isequal(value, [])
    validateattributes(value, {'numeric'}, ...
        {'real','finite','positive', 'integer', 'nonempty', 'scalar'});
end
end

function description = iComputeDescription(description, fun)
if strlength(description) == 0
    if isa(fun,'function_handle')        
        description = func2str(fun);
        if description(1) ~= '@'
            description = ['@' description];
        end
    else
        description = class(fun);
    end
end
end

function iAssertValidText(description)
isValidText = nnet.internal.cnn.layer.paramvalidation.isValidStringOrCharArray(description);
if ~isValidText
    error(message('nnet_cnn:layer:Layer:ParamMustBeStringOrChar','Description'))
end
end

function nout = iComputeDefaultNumOutputs(fun, nout)
% Return nout unless FUN is an anonymous function and in that case return 1.
if nout < 0
    info = functions(fun);
    if isscalar(info) && strcmp(info.type, 'anonymous')
        nout = 1;
    end
end
end

function [funNargin, funNargout] = iNarginNargout(fun)
% Detect the number of inputs and outputs to fun. If fun
% is unknown then return NaNs.
try
    funNargin = nargin(fun);
    funNargout = nargout(fun);
catch err
    if err.identifier == "MATLAB:nargin:isScript"
        error(message('nnet_cnn:layer:FunctionLayer:Script',char(fun)));
    else
        info = functions(fun);
        if (info.type == "simple" || info.type == "classsimple") && ...
                ismethod(dlarray([]), info.function)
            functionPath = which(info.function);
            funNargin = nargin(functionPath);
            funNargout = nargout(functionPath);
        else
            funNargin = NaN;
            funNargout = NaN;
        end
    end
end
end

function iCheckMaxNumInputs(fun, requestedNumInputs)
if isa(fun,'function_handle')
    info = functions(fun);
    if info.type == "anonymous" && nargin(fun) > 0 && requestedNumInputs > nargin(fun)
        error(message('nnet_cnn:layer:FunctionLayer:TooManyInputs', nargin(fun), requestedNumInputs));
    end
end
end

function iAssertBinary(value)
nnet.internal.cnn.options.OptionsValidator.assertBinary(value);
end

function iAssertValidFunction(value)
if ~isa(value,'function_handle') && ~isa(value,'deep.DifferentiableFunction')
    error(message('nnet_cnn:layer:FunctionLayer:FunctionInput'));
end
end
