classdef RecallMetric < deep.internal.ClassificationMetric & deep.internal.metric.BuiltIn & deep.internal.metric.RequiresMatchingOutputsAndTargets
    % RecallMetric  Recall metric for deep learning

    %   Copyright 2022-2023 The MathWorks, Inc.

    properties
        % Name   (string scalar) A name for the metric.
        Name
    end

    properties (SetAccess = private)
        % AverageType  (string scalar) Type of averaging when calculating
        % the metric. Can be either "micro", "macro", or "weighted".
        AverageType

        % ClassificationMode (string) Type of classification task. 
        % Can be either "single-label" or "multilabel".
        ClassificationMode
    end

    properties (Access = private)
        % True Positives and False Negatives.
        % These variables are vectors and store values per class.
        TP
        FN
    end

    properties (Constant, Access = private)
        % Version   Number to identify the current version of this object
        %   This is used to ensure that objects from older versions are
        %   loaded correctly.
        Version = 1
    end

    methods(Static)
        function in = loadobj(in)
            % Set Maximize on load as a RecallMetric loaded from 2023b will 
            % not have the Maximize property set.
            in.Maximize = true;
        end
    end

    methods (Hidden)
        function this = RecallMetric(name, networkOutput, averageType, classificationMode)
            this.Name = name;
            this.NetworkOutput = networkOutput;
            this.AverageType = averageType;
            this.ClassificationMode = classificationMode;
            this.Maximize = true;
        end           
        
        function this = initialize(this,Y,T,varargin)
            if nargin > 3
                error(message('deep:networkMetrics:MultiOutputWithBuiltinMetric', this.Name, "recallMetric"));
            end

            this = initialize@deep.internal.metric.RequiresMatchingOutputsAndTargets(this,Y,T);
            
            this = initialize@deep.internal.ClassificationMetric(this,Y,T,varargin{:});
        end

        function this = reset(this)
            % This method never gets called internally as the reset gets
            % shared with other classification metrics. This method is for
            % external purposes only.
            this.TP = 0;
            this.FN = 0;
        end

        function this = update(this,Y, T,varargin)
            % This method never gets called internally as the update gets
            % shared with other classification metrics. This method is for
            % external purposes only.
            
            % Calculate TP and FN for the current minibatch
            [currentTP,~,currentFN] = deep.internal.util.calculateConfusionMatrixValues(Y,T,this.ClassificationMode,this.Threshold,this.ChannelDim,this.NumClasses);

            this.TP = this.TP + currentTP;
            this.FN = this.FN + currentFN;
        end

        function val = evaluate(this)
            val = iEvaluateMetric(this.TP,this.FN,this.AverageType,this.Weights);
        end

        function this = aggregate(this, otherMetric)
            this.TP = this.TP + otherMetric.TP;
            this.FN = this.FN + otherMetric.FN;
        end
    end

    methods
        function this = set.Name(this, name)
            name = deep.internal.util.validateName(name);
            this.Name = name;
        end
    end

    methods (Access = {?deep.metric.AccuracyMetric, ?deep.metric.FScoreMetric, ?deep.metric.PrecisionMetric, ?deep.metric.RecallMetric, ?deep.internal.metric.SharedClassificationMetric})
        function val = evaluateMetricWithSharedData(this,tp,~,fn,~)
            val = iEvaluateMetric(tp,fn,this.AverageType,this.Weights);
        end
    end
end

function  val = iEvaluateMetric(tp,fn,avgType,weights)
if avgType == "micro"
    tp = sum(tp, 'all');
    fn = sum(fn, 'all');
elseif avgType == "weighted"
    numObsPerClass = tp + fn;
    weights = numObsPerClass./sum(numObsPerClass+eps);
end

val = sum(weights .* (tp./(tp+fn+eps)));
end