function layer = featureInputLayer(varargin)
% 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: []
%
%    '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 The MathWorks, Inc.

% Parse the input arguments.
inputArguments = iParseInputArguments(varargin{:});

normalization = iCreateTransforms(...
    inputArguments.Normalization, inputArguments.InputSize);

% Create an internal representation of a feature input layer.
internalLayer = nnet.internal.cnn.layer.GenericInput(...
    inputArguments.Name, ...
    inputArguments.InputSize, ...
    normalization);

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

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

function inputArguments = iParseInputArguments(varargin)
varargin = nnet.internal.cnn.layer.util.gatherParametersToCPU(varargin);
parser = iCreateParser();
parser.parse(varargin{:});
inputArguments = iConvertToCanonicalForm(parser.Results);
end

function p = iCreateParser(varargin)
p = inputParser;

defaultName = '';
defaultTransform = 'none';
defaultDimension = 'auto';

addRequired(p,  'InputSize', @iAssertValidInputSize);
addParameter(p, 'Normalization', defaultTransform, @(x)~isempty(iCheckAndReturnValidNormalization(x)));
addParameter(p, 'NormalizationDimension', defaultDimension);
addParameter(p, 'Name', defaultName, @iAssertValidLayerName);
addParameter(p, 'Mean', []);
addParameter(p, 'StandardDeviation', []);
addParameter(p, 'Min', []);
addParameter(p, 'Max', []);
end

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

function iAssertValidInputSize(sz)
validateattributes(sz,{'numeric'},{'nonempty','real','finite','integer','positive'})
if ~isscalar(sz)
    error(message('nnet_cnn:layer:FeatureInputLayer:InvalidDataSize'));
end
end

function args = iConvertToCanonicalForm(params)
try    
    args = struct;
    % Make sure integral values are converted to double
    args.InputSize = double(params.InputSize);
    args.Normalization = iCheckAndReturnValidNormalization(params.Normalization);
    args.NormalizationDimension = iCheckAndReturnValidDimension( ...
        params.NormalizationDimension, args.Normalization );
    args.Mean = iCheckAndReturnSingleStatistics( params.Mean, 'Mean', ...
        args.Normalization, args.NormalizationDimension, args.InputSize);
    args.StandardDeviation = iCheckAndReturnSingleStatistics( params.StandardDeviation, 'StandardDeviation', ...
        args.Normalization, args.NormalizationDimension, args.InputSize);
    args.Min = iCheckAndReturnSingleStatistics( params.Min, 'Min', ...
        args.Normalization, args.NormalizationDimension, args.InputSize);
    args.Max = iCheckAndReturnSingleStatistics( params.Max, 'Max', ...
        args.Normalization, args.NormalizationDimension, args.InputSize);
    % 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)
% Validate user-provided statistics with respect to normalization method 
% and input size. Return the statistic in single precision.
if ~isempty(userValue)
    hasRowVectorStats = true;
    nnet.internal.cnn.layer.paramvalidation.validateNormalizationStatistics(...
        userValue, name, normalization, normalizationDimension, ...
        inputSize, hasRowVectorStats);
end
% Permute user value from row to column vector for internal use
statsValue = single(userValue');
end

function x = iCheckAndReturnValidNormalization(x)
validateattributes(x,{'string','char','function_handle'},{})
if isa(x,'function_handle')
    % Checks are performed at train/inference time when normalization is
    % applied
else
    validTransforms = {'zerocenter', 'zscore', 'rescale-symmetric', ...
        'rescale-zero-one', 'none'};
    x = validatestring(x, validTransforms);
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
