function layer = featureInputLayer(inputSize,nameValueArgs)
% featureInputLayer   Feature Input Layer
%
%   layer = featureInputLayer(numFeatures) defines a feature input layer,
%   where numFeatures is the number of features in the input data.
%
%   layer = featureInputLayer(numFeatures, 'PARAM1', VAL1, 'PARAM2', VAL2, ...)
%   specifies optional parameter name/value pairs for creating the layer:
%
%    'Normalization'     Data normalization applied when data is forward
%                        propagated through the input layer. Valid values
%                        are:
%                           'zerocenter'        - zero-center normalization
%                           'zscore'            - z-score normalization
%                           'rescale-symmetric' - rescale to [-1 1]
%                           'rescale-zero-one'  - rescale to [0 1]
%                           'none'              - do not normalize
%                           function handle     - custom normalization
%
%                        Default: 'none'
%
%    'NormalizationDimension'
%                        Dimension over which the same normalization is
%                        applied, specified as one of the following:
%                          - 'auto'    - If the ResetInputNormalization
%                                        training option is false and you
%                                        specify normalization statistics,
%                                        then normalize over dimensions
%                                        matching the statistics.
%                                        Otherwise, recompute the
%                                        statistics and apply channel-wise
%                                        normalization.
%                          - 'channel' - Channel-wise normalization
%                          - 'all'     - Normalize all values using
%                                        scalar statistics.
%
%                        Default: 'auto'
%
%    'Mean'              The mean value used for zero centering and z-score
%                        normalization. This can be a 1-by-numFeatures array of means
%                        per feature, a scalar, or [].
%
%                        Default: []
%
%    'StandardDeviation' The standard deviation used for z-score 
%                        normalization. This can be a 1-by-numFeatures array of
%                        standard deviations per feature, a scalar, or [].
%
%                        Default: []
%
%    'Min'               Minimum value used for rescaling.
%                        This can be a 1-by-numFeatures array of minima per feature,
%                        a scalar, or [].
%
%                        Default: []
%
%    'Max'               Maximum value used for rescaling.
%                        This can be a 1-by-numFeatures array of maxima per feature,
%                        a scalar, or [].
%
%                        Default: []
%
%    'SplitComplexInputs'         
%                        Flag indicating whether the layer should split the 
%                        real and imaginary components of the data, 
%                        specified as one of the following:
%                          - false - Do not split the input data. This 
%                                    option supports real input only. 
%                          - true  - Output real and imaginary parts as
%                                    separate channels. The number of 
%                                    channels of the layer output is 2*C, 
%                                    where C is the number of channels in 
%                                    the input data. For real input, this
%                                    option outputs zeros for the imaginary 
%                                    parts.
%                        When SplitComplexInputs is 1, Normalization must 
%                        be 'zerocenter', 'zscore', 'none', or a function 
%                        handle.
%
%                        Default: false
%
%    'Name'              A name for the layer.
%
%                        Default: ''
%
%   Example 1:
%       % Create a feature input layer for data with 10 features:
%
%       layer = featureInputLayer(10);
%
%   See also nnet.cnn.layer.FeatureInputLayer, imageInputLayer,
%   image3dInputLayer, sequenceInputLayer
%
%   <a href="matlab:helpview('deeplearning','list_of_layers')">List of Deep Learning Layers</a>

%   Copyright 2020-2023 The MathWorks, Inc.

arguments
    inputSize (1,1) {mustBeNumeric, mustBeNonempty, ...
        mustBeReal, mustBeFinite, mustBeInteger, mustBePositive}
    nameValueArgs.Normalization = 'none';
    nameValueArgs.NormalizationDimension = 'auto';
    nameValueArgs.Name {iAssertValidLayerName} = '';
    nameValueArgs.Mean = [];
    nameValueArgs.StandardDeviation = [];
    nameValueArgs.Min = [];
    nameValueArgs.Max = [];
    nameValueArgs.SplitComplexInputs (1,1) {iAssertBinary} = false;
end

inputSize = nnet.internal.cnn.layer.util.gatherParametersToCPU(inputSize);
nameValueArgs = nnet.internal.cnn.layer.util.gatherParametersToCPU(nameValueArgs);
[inputSize,nameValueArgs] = iConvertToCanonicalForm(inputSize,nameValueArgs);

normalization = iCreateTransforms(...
    nameValueArgs.Normalization, inputSize);

% Create an internal representation of a feature input layer.
internalLayer = nnet.internal.cnn.layer.FeatureInput(...
    nameValueArgs.Name, ...
    inputSize, ...
    normalization, ...
    nameValueArgs.SplitComplexInputs);

% Assign Statistics
internalLayer.Mean = nameValueArgs.Mean;
internalLayer.Std = nameValueArgs.StandardDeviation;
internalLayer.Min = nameValueArgs.Min;
internalLayer.Max = nameValueArgs.Max;
internalLayer.NormalizationDimension = nameValueArgs.NormalizationDimension;

% Pass the internal layer to a function to construct a user visible feature
% input layer.
layer = nnet.cnn.layer.FeatureInputLayer(internalLayer);
end

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

function [inputSize,args] = iConvertToCanonicalForm(inputSize,params)
try    
    args = struct;
    % Make sure integral values are converted to double
    inputSize = iConvertToDouble(inputSize);
    args.Normalization = iCheckAndReturnValidNormalization(params.Normalization, ...
        params.SplitComplexInputs);
    args.NormalizationDimension = iCheckAndReturnValidDimension( ...
        params.NormalizationDimension, args.Normalization );
    args.Mean = iCheckAndReturnSingleStatistics( params.Mean, 'Mean', args.Normalization, ...
        args.NormalizationDimension, inputSize, true, params.SplitComplexInputs);
    args.StandardDeviation = iCheckAndReturnSingleStatistics( params.StandardDeviation, 'StandardDeviation', ...
        args.Normalization, args.NormalizationDimension, inputSize);
    args.Min = iCheckAndReturnSingleStatistics( params.Min, 'Min', ...
        args.Normalization, args.NormalizationDimension, inputSize);
    args.Max = iCheckAndReturnSingleStatistics( params.Max, 'Max', ...
        args.Normalization, args.NormalizationDimension, inputSize);
     args.SplitComplexInputs = logical(params.SplitComplexInputs);
    % Make sure strings get converted to char vectors
    args.Name = convertStringsToChars(params.Name); 
catch e
    % Reduce the stack trace of the error message by throwing as caller
    throwAsCaller(e)
end
end

function statsValue = iCheckAndReturnSingleStatistics( userValue, name, ...
    normalization, normalizationDimension, inputSize, allowComplexStatistic, ...
    splitComplexInputs)

if nargin < 6
    allowComplexStatistic = false;
end

if nargin < 7
    splitComplexInputs = false;
end

% Validate user-provided statistics with respect to normalization method 
% and input size. Return the statistic in single precision.
if ~isempty(userValue)
    nnet.internal.cnn.layer.paramvalidation.validateNormalizationStatistics(...
        userValue, name, normalization, normalizationDimension, inputSize, ...
        'HasRowVectorStats', true, ...
        'AllowComplexStatistic', allowComplexStatistic, ...
        'SplitComplexInputs', splitComplexInputs);
end
% Permute user value from row to column vector for internal use
statsValue = single(userValue');
end

function x = iCheckAndReturnValidNormalization(x, splitComplexInputs)
validateattributes(x,{'string','char','function_handle'},{})
if isa(x,'function_handle')
    % Checks are performed at train/inference time when normalization is
    % applied
else
    if splitComplexInputs
        % Rescaling is not supported for complex inputs
        validTransforms = {'zerocenter', 'zscore', 'none'};
        try
            x = validatestring(x, validTransforms);
        catch e
            error(message('nnet_cnn:layer:InputLayer:InvalidNormalizationForComplex'));
        end
    else
        validTransforms = {'zerocenter', 'zscore', 'rescale-symmetric', ...
            'rescale-zero-one', 'none'};
        x = validatestring(x, validTransforms, '', 'Normalization');
    end
end
end

function dimValue = iCheckAndReturnValidDimension(dimValue,normalization)
dimValue = nnet.internal.cnn.layer.paramvalidation.validateNormalizationDimension(dimValue,normalization, {'auto', 'channel', 'all' });
end

function transform = iCreateTransforms(type, dataSize)
transform = nnet.internal.cnn.layer.InputTransformFactory.create(type, dataSize);
end

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

function value = iConvertToDouble(value)
value = nnet.internal.cnn.layer.util.convertToDouble(value);
end
