classdef (Abstract) Layer < nnet.cnn.layer.Layer & ...
        nnet.cnn.layer.mixin.Learnable
    % Layer   Interface for custom network layers
    
    %   Copyright 2017-2023 The MathWorks, Inc.
    
    properties
        % Name (char vector)   A name for the layer
        Name = '';
    end
    
    properties (SetAccess = protected)
        % Description (char vector or scalar string)   A one line
        %   description for the layer
        Description = ''
        
        % Type (char vector or scalar string)   The type of layer
        Type = ''
    end
    
    properties(Dependent, SetAccess = protected)
        % NumInputs (integer)   Number of inputs of this layer
        NumInputs
        
        % InputNames (cellstr,string)  Names of the inputs of this layer.
        InputNames
        
        % NumOutputs (integer)   Number of outputs of this layer
        NumOutputs
        
        % InputNames (cellstr,string)   Names of the outputs of this layer.
        OutputNames
    end
    
    properties(Access = private)
        % PrivateInputs (cellstr)   Names of the inputs to the layer
        PrivateInputs = {iDefaultInput()}
        
        % PrivateInputs (cellstr)   Names of the outputs from the layer
        PrivateOutputs = {iDefaultOutput()}
    end
    
    methods (Abstract)
        % [Z,state] = predict(layer,X)
        % Forward input data through the layer at prediction time and
        % output the result and updated state.
        %
        % Inputs:
        %         layer - Layer to forward propagate through
        %         X     - Input data
        % Outputs:
        %         Z     - Output of layer forward function
        %         state - (Optional) Updated layer state.
        %
        %  - For layers with multiple inputs, replace X with X1,...,XN,
        %    where N is the number of inputs.
        %  - For layers with multiple outputs, replace Z with
        %    Z1,...,ZM, where M is the number of outputs.
        %  - For layers with multiple state parameters, replace state
        %    with state1,...,stateK, where K is the number of state
        %    parameters.
        varargout = predict( layer, varargin )
    end
    
    methods
        function varargout = backward( this, varargin ) %#ok<STOUT>
            % [dLdX,dLdW,dLdSin] = backward(layer,X,Z,dLdZ,dLdSout,memory)
            % (Optional) Backward propagate the derivative of the loss
            % function through the layer.
            %
            % Inputs:
            %         layer   - Layer to backward propagate through
            %         X       - Layer input data
            %         Z       - Layer output data
            %         dLdZ    - Derivative of loss with respect to layer
            %                   output
            %         dLdSout - (Optional) Derivative of loss with respect
            %                   to state output
            %         memory  - Memory value from forward function
            % Outputs:
            %         dLdX   - Derivative of loss with respect to layer input
            %         dLdW   - (Optional) Derivative of loss with respect to
            %                  learnable parameter
            %         dLdSin - (Optional) Derivative of loss with respect to
            %                  state input
            %
            %  - For layers with state parameters, the backward syntax must
            %    include both dLdSout and dLdSin, or neither.
            %  - For layers with multiple inputs, replace X and dLdX with
            %    X1,...,XN and dLdX1,...,dLdXN, respectively, where N is
            %    the number of inputs.
            %  - For layers with multiple outputs, replace Z and dlZ with
            %    Z1,...,ZM and dLdZ,...,dLdZM, respectively, where M is the
            %    number of outputs.
            %  - For layers with multiple learnable parameters, replace
            %    dLdW with dLdW1,...,dLdWP, where P is the number of
            %    learnable parameters.
            %  - For layers with multiple state parameters, replace dLdSin
            %    and dLdSout with dLdSin1,...,dLdSinK and
            %    dLdSout1,...dldSoutK, respectively, where K is the number
            %    of state parameters.
            %
            % See also: forward
            error(message('nnet_cnn:internal:cnn:layer:CustomLayer:CustomlayerUnusedBackward', class(this)))
        end

        function varargout = forward( layer, varargin )
            % [Z,state,memory] = forward(layer,X)
            % (Optional) Forward input data through the layer at training
            % time and output the result, updated state, and a memory
            % value.
            %
            % Inputs:
            %         layer - Layer to forward propagate through
            %         X     - Layer input data
            % Outputs:
            %         Z      - Output of layer forward function
            %         state  - (Optional) Updated layer state
            %         memory - (Optional) Memory value for custom backward
            %                  function
            %
            %  - For layers with multiple inputs, replace X with X1,...,XN,
            %    where N is the number of inputs.
            %  - For layers with multiple outputs, replace Z with
            %    Z1,...,ZM, where M is the number of outputs.
            %  - For layers with multiple state parameters, replace state
            %    with state1,...,stateK, where K is the number of state
            %    parameters.
            [varargout{1:layer.NumOutputs+layer.PrivateNumStates}] = predict( layer, varargin{:} );
            varargout{end+1} = [];
        end
        


        function layer = resetState(layer)
            % layer = resetState(layer)
            % (Optional) Reset layer state.
        end

        function layer = initialize( layer, varargin )
            % layer = initialize(layer,X)
            % (Optional) Initialize layer.
            %
            % Inputs:
            %         layer - Layer to initialize
            %         X     - Layer input networkDataLayout object
            % Outputs:
            %         layer - Initialized layer
            %
            %  - For layers with multiple inputs, replace X with X1,...,XN,
            %    where N is the number of inputs.
        end
    end
    
    methods (Hidden, Access = {?dlnetwork,?deep.internal.graph.EditableLayerGraph,?nnet.cnn.layer.Layer})
        function nestedLayer = getLayer(layer, layerPath)
            % nestedLayer = getLayer(layer,layerPath)
            % Return nested layer, for layers that have them
            %
            % Inputs:
            %        layer     - Layer containing nested layers
            %        layerPath - String identifying the nested layer, using syntax
            %        "networkPropertyName/layerName"
            % Outputs:
            %        nestedLayer - Nested layer matching the layerPath
            arguments
                layer(1,1) nnet.layer.Layer
                layerPath(1,1) string
            end
            [net, nestedPath] = iSplitAndValidateLayerPath(layer, layerPath);
            nestedLayer = getLayer(net, nestedPath);
        end

        function layer = replaceLayer(layer, layerPath, layerArray, args)
            %replaceLayer  Substitute a nested layer
            %
            % For layers that contain nested layers, replaces the specified nested
            % layer with layerArray and reconnects inputs and outputs either by "name"
            % or by "order" of connections. layerPath identifies the layer using syntax
            % "networkPropertyName/layerName".
            arguments
                layer(1,1) nnet.layer.Layer
                layerPath(1,1) string
                layerArray(:,1) nnet.cnn.layer.Layer
                args.ReconnectBy (1,1) string {mustBeMember(args.ReconnectBy, {'name', 'order'})} = 'name';
            end
            [net, nestedPath, propertyName] = iSplitAndValidateLayerPath(layer, layerPath);
            args = [fieldnames(args) struct2cell(args)]';
            layer.(propertyName) = replaceLayer(net, nestedPath, layerArray, args{:});
        end
    end

    methods(Hidden, Access = protected)
        function [description, type] = getOneLineDisplay( layer )
            if strlength( layer.Description ) == 0
                % If Description was not defined, show the class
                description = class( layer );
            else
                description = layer.Description;
            end
            
            if strlength( layer.Type ) == 0
                % If Type was not defined, show the class
                type = class( layer );
            else
                type = layer.Type;
            end
        end
        
        function groups = getPropertyGroups( this )
            [ generalProperties, learnableParameters, stateParameters ] = iGetLayerProperties( this );
            
            groups = [
                this.propertyGroupGeneral( generalProperties )
                this.propertyGroupLearnableParameters( learnableParameters )
                this.propertyGroupDynamicParameters( stateParameters )
                ];
        end
        
        function footer = getFooter( this )
            variableName = inputname(1);
            footer = this.createShowAllPropertiesFooter( variableName );
        end
    end

    methods (Hidden, Sealed)
        function displayAllProperties(this)
            proplist = properties( this );
            proplist = iOrderPropList( proplist );
            matlab.mixin.CustomDisplay.displayPropertyGroups( ...
                this, ...
                this.propertyGroupGeneral( proplist ) );
        end
    end
    
    methods(Hidden)
        function tf = isFormattable(this)
            tf = isa(this, 'nnet.layer.Formattable');
        end
        function tf = isAcceleratable(this)
            tf = isa(this, 'nnet.internal.cnn.layer.Traceable');
        end
        function tf = isElementwise(this)
            tf = isa(this, 'nnet.layer.Elementwise');
        end
    end
    
    methods
        function layer = set.Name( layer, val )
            iEvalAndThrow(@()iAssertValidLayerName( val ));
            layer.Name = convertStringsToChars( val );
        end
        
        function layer = set.Description( layer, val )
            val = iEvalAndThrow(@()iValidateText( val, "Description" ));
            layer.Description = val;
        end
        
        function layer = set.Type( layer, val )
            val = iEvalAndThrow(@()iValidateText( val, "Type" ));
            layer.Type = val;
        end
        
        function val = get.NumInputs( layer )
            val = numel( layer.PrivateInputs );
        end
        
        function val = get.NumOutputs( layer )
            val = numel( layer.PrivateOutputs );
        end
        
        function val = get.InputNames( layer )
            val = layer.PrivateInputs;
        end
        
        function val = get.OutputNames( layer )
            val =  layer.PrivateOutputs;
        end
        
        function layer = set.NumInputs( layer, val )
            iEvalAndThrow(@()iAssertPositiveInteger(val,'NumInputs'))
            prefix = iDefaultInput();
            layer.PrivateInputs = iGenerateNames(prefix,val);
        end
        
        function layer = set.NumOutputs( layer, val )
            iEvalAndThrow(@()iAssertPositiveInteger(val,'NumOutputs'))
            prefix = iDefaultOutput();
            layer.PrivateOutputs = iGenerateNames(prefix,val);
        end
        
        function layer = set.InputNames( layer, names )
            iEvalAndThrow(@()iAssertValidPortNameList(names,'InputNames'))
            layer.PrivateInputs = cellstr(names);
        end
        
        function layer = set.OutputNames( layer, names )
            iEvalAndThrow(@()iAssertValidPortNameList(names,'OutputNames'))
            layer.PrivateOutputs = cellstr(names);
        end
    end
    
    methods (Static = true, Access = private)
        function name = matlabCodegenRedirect(~)
            % nnet.layer.Layer is not supported for codegen
            % nnet.internal.cnn.coder.Layer is the implementation of
            % nnet.layer.Layer that supports codegen for inference
            % (predict/activations/classify)
            % when generating code for any custom layer inheriting from
            % nnet.layer.Layer class, we will redirect it to the
            % codegenable implementation
            %
            % any changes made in nnet.layer.Layer that affects the predict
            % call needs to be implemented in nnet.internal.cnn.coder.Layer
            % as well
            name = 'nnet.internal.cnn.coder.Layer';
        end
    end
end

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

function iAssertValidPortName( name, prop )
nnet.internal.cnn.layer.paramvalidation.validateLayerName( name );
if isempty(name)
    error(message( 'nnet_cnn:layer:Layer:InputOutputsCannotBeEmpty', prop));
end
end

function paramValue = iValidateText( paramValue, paramName )
isValidText = nnet.internal.cnn.layer.paramvalidation.isValidStringOrCharArray(paramValue);
if isempty(paramValue)
    paramValue = '';
elseif ~isValidText
    error(message('nnet_cnn:layer:Layer:ParamMustBeStringOrChar',paramName))
end
end

function iAssertPositiveInteger( val, prop )
validateattributes( val, {'numeric'}, {'integer','real','positive','scalar'},'',prop);
end

function iAssertValidPortNameList( names, prop )
validateattributes( names, {'cell','string','char'}, {'nonempty','vector'},'',prop)
cellfun(@(s)iAssertValidPortName(s,prop),cellstr(names),'UniformOutput',false)
if numel(unique(names)) < numel(names)
    error( message( 'nnet_cnn:layer:Layer:DuplicatePortNames', prop) );
end
end

function [ generalProperties, learnableParameters, stateParameters ] = iGetLayerProperties( layer )
% iGetLayerProperties   Return layer's properties divided into general
% properties and learnable parameters
learnableParameters = {};
stateParameters = {};
generalProperties = {};
propertyList = iPropertyList(layer);
for ii=1:numel(propertyList)
    if ~iIsPublic( propertyList(ii).GetAccess )
        continue
    end
    isLearnable = iIsLearnable( propertyList(ii) );
    isStateful = iIsStateful( propertyList(ii) );
    if isLearnable && isStateful
        learnableParameters{end+1} = propertyList(ii).Name; %#ok<AGROW>
        stateParameters{end+1} = propertyList(ii).Name; %#ok<AGROW>
    elseif isLearnable
        learnableParameters{end+1} = propertyList(ii).Name; %#ok<AGROW>
    elseif isStateful
        stateParameters{end+1} = propertyList(ii).Name; %#ok<AGROW>
    elseif iCanBeShown( propertyList(ii) )
        generalProperties{end+1} = propertyList(ii).Name; %#ok<AGROW>
    end
end
if ~isempty(layer)
    % Show input related properties if they have non-default values
    if ~isequal( layer.InputNames, {iDefaultInput} )
        generalProperties{end+1} = 'NumInputs';
        generalProperties{end+1} = 'InputNames';
    end
    % Show output related properties if they have non-default values
    if ~isequal( layer.OutputNames' , {iDefaultOutput} )
        generalProperties{end+1} = 'NumOutputs';
        generalProperties{end+1} = 'OutputNames';
    end
end
generalProperties = iOrderPropList( generalProperties' )';
end

function propertyList = iPropertyList( layer )
metaLayer = metaclass(layer);
propertyList = metaLayer.PropertyList;
end

function tf = iIsLearnable( prop )
tf = isprop(prop, 'Learnable') && prop.Learnable;
end

function tf = iIsStateful( prop )
tf = isprop(prop, 'State') && prop.State;
end

function tf = iIsPublic( access )
% An access in a metaclass can be public if it's either 'public' or 'none'
tf = strcmp( access, 'public' ) || strcmp( access, 'none' );
end

function tf = iCanBeShown( prop )
% iCanBeShown   Return true if the property can be shown. Properties that
% cannot be shown are Hidden or they belong to a list of properties that
% are not supposed to be shown in the short display (e.g. Type,
% Description, and MIMO properties if they have the default value)
notShowList = {'Description', 'Type', 'NumInputs', 'NumOutputs', 'InputNames', 'OutputNames'};

tf = ~prop.Hidden && ~ismember( prop.Name, notShowList );
end

function p = iOrderPropList( p )
% iOrderPropList   Reorder property list p to make sure Name, Description,
% Type and MIMO properties are reported first
idx = ismember(p, {'Name', 'Description', 'Type', 'NumInputs', 'InputNames', 'NumOutputs', 'OutputNames'});
p = [p(idx); p(~idx)];
end

function varargout = iEvalAndThrow(func)
% Omit the stack containing internal functions by throwing as caller
try
    [varargout{1:nargout}] = func();
catch exception
    throwAsCaller(exception)
end
end

function val = iDefaultInput()
val = 'in';
end

function val = iDefaultOutput()
val = 'out';
end

function names = iGenerateNames(prefix,numPorts)
if numPorts == 1
    names = {prefix};
else
    strNames = string(prefix) + (1:numPorts);
    names = cellstr(strNames);
end
end

function [net, nestedPath, propertyName] = iSplitAndValidateLayerPath(layer, layerPath)
tokens = split(layerPath, "/");
layerName = layer.Name;
if numel(tokens) < 2
    error(message("nnet_cnn:layer:Layer:InvalidPathCustomLayer",layerName));
end
propertyName = tokens(1);
[~, learnableParameters, stateParameters] = iGetLayerProperties(layer);
parameters = [learnableParameters, stateParameters];
if ~ismember(propertyName, parameters) || ~isa(layer.(propertyName),'dlnetwork')
    % Try to be helpful and find a dlnetwork property
    for i = 1:numel(parameters)
        exampleValidPropertyName = parameters{i};
        prop = layer.(exampleValidPropertyName);
        if isa(prop,'dlnetwork')
            error(message("nnet_cnn:layer:Layer:InvalidPathInvalidProperty",layerName,propertyName,exampleValidPropertyName));
        end
    end
    error(message("nnet_cnn:layer:Layer:InvalidPathNotADlnetwork",layerName,propertyName));
end
net = layer.(propertyName);
nestedPath = join(tokens(2:end),"/");
end
