function layer = image3dInputLayer(inputSize,nameValueArgs)
% image3dInputLayer   3-D Image input layer
%
%   layer = image3dInputLayer(inputSize) defines a 3-D image input layer.
%   inputSize is the size of the 3-D input image for the layer. It must be
%   a row vector of three or four numbers.
%
%   layer = image3dInputLayer(inputSize, '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: 'zerocenter'
%
%    '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
%                          - 'element' - Element-wise normalization
%                          - 'all'     - Normalize all values using
%                                        scalar statistics
%
%                        Default: 'auto'
%
%    'Mean'              The mean image used for zero centering and z-score
%                        normalization. This can be a H-by-W-by-D-by-C
%                        array, a 1-by-1-by-1-by-C array of means per
%                        channel, a scalar, or [], where H is the height, W
%                        the width, D the depth, and C the number of channels.
%
%                        Default: []
%
%    'StandardDeviation' The standard deviation used for z-score normalization.
%                        This can be a H-by-W-by-D-by-C array, a
%                        1-by-1-by-1-by-C array of standard deviations per
%                        channel, a scalar, or [], where H is the height, W
%                        the width, D the depth, and C the number of channels.
%
%                        Default: []
%
%    'Min'               Minimum value used for rescaling.
%                        This can be a H-by-W-by-D-by-C array, a
%                        1-by-1-by-1-by-C array of minima per channel, a
%                        scalar, or [], where H is the height, W the width,
%                        D the depth, and C the number of channels.
%
%                        Default: []
%
%    'Max'               Maximum value used for rescaling.
%                        This can be a H-by-W-by-D-by-C array, a
%                        1-by-1-by-1-by-C array of maxima per channel, a
%                        scalar or [], where H is the height, W the width,
%                        D the depth, and C the number of channels.
%
%                        Default: []
%
%    'Name'              A name for the layer.
%
%                        Default: ''
%
%   Example 1:
%       % Create a 3-D input layer for 132-by-132-by-116 RGB images.
%
%       layer = image3dInputLayer([132 132 116 3]);
%
%   Example 2:
%       % Create a 3-D image input layer for 132-by-132-by-116 grayscale images.
%       % Set the mean image for zero-center normalization to an array of
%       % ones.
%
%       layer = image3dInputLayer([132 132 116], 'Mean', ones([132 132 116]));
%
%   See also nnet.cnn.layer.Image3DInputLayer, imageInputLayer,
%   convolution3dLayer, fullyConnectedLayer, maxPooling3dLayer.
%
%   <a href="matlab:helpview('deeplearning','list_of_layers')">List of Deep Learning Layers</a>

%   Copyright 2018-2023 The MathWorks, Inc.

arguments
    inputSize {mustBeNumeric, mustBeNonempty, mustBeValidRowVector(inputSize), ...
        mustBeReal, mustBeFinite, mustBeInteger, mustBePositive}
    nameValueArgs.Normalization {iValidNormalization} = 'zerocenter';
    nameValueArgs.NormalizationDimension = 'auto';
    nameValueArgs.Name {iAssertValidLayerName} = '';
    nameValueArgs.Mean = [];
    nameValueArgs.StandardDeviation = [];
    nameValueArgs.Min = [];
    nameValueArgs.Max = [];
    nameValueArgs.AverageImage = [];
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 3-D image input layer.
internalLayer = nnet.internal.cnn.layer.Image3DInput(...
    nameValueArgs.Name, ...
    inputSize, ...
    normalization);

% 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 3-D
% image input layer.
layer = nnet.cnn.layer.Image3DInputLayer(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(iMakeIntoRowVectorOfFour(inputSize));
    args.Normalization = iCheckAndReturnValidNormalization(params.Normalization);
    args.NormalizationDimension = iCheckAndReturnValidDimension( ...
        params.NormalizationDimension, args.Normalization );
    args.Mean = iCheckAndReturnSingleMean( params, ...
        args.Normalization, args.NormalizationDimension, inputSize );
    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.Name = convertStringsToChars(params.Name); % make sure strings get converted to char vectors
catch e
    % Reduce the stack trace of the error message by throwing as caller
    throwAsCaller(e)
end
end

function meanVal = iCheckAndReturnSingleMean(params, normalization, normalizationDimension, inputSize)
if isempty(params.Mean)
    % Use value of AverageImage NVP as the Mean if Mean not specified
    meanVal = params.AverageImage;
else
    % Ignore AverageImage NVP if Mean was specified
    meanVal = params.Mean;
end
meanVal = iCheckAndReturnSingleStatistics( meanVal, 'Mean', ...
    normalization, normalizationDimension, inputSize);
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)
    nnet.internal.cnn.layer.paramvalidation.validateNormalizationStatistics(...
        userValue, name, normalization, normalizationDimension, inputSize);
end
statsValue = single(userValue);
end

function validSize = iMakeIntoRowVectorOfFour(inputSize)
if numel(inputSize) == 3
    validSize = [inputSize 1];
else
    validSize = inputSize;
end
end

function x = iValidNormalization(x)
x = ~isempty(iCheckAndReturnValidNormalization(x));
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);
end

function mustBeValidRowVector(sz)
isValidSize = iIsRowVectorOfThreeOrFour(sz);
if ~isValidSize
    error(message('nnet_cnn:layer:Image3dInputLayer:InvalidDataSize'));
end
end

function tf = iIsRowVectorOfThreeOrFour(x)
tf = isrow(x) && (numel(x)==3 || numel(x)==4);
end

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

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