classdef validator
    % The validator for the monitor object

    %   Copyright 2022-2023 The MathWorks, Inc.

    methods(Static)
        function mustBeAMetric(monitor, values)
            nonExistentMetrics = string(setdiff(values, monitor.Metrics));
            if ~isempty(nonExistentMetrics)
                ME = MException(message('shared_experimentmonitor:validator:MetricNotDefined', nonExistentMetrics(1)));
                throwAsCaller(ME);
            end
        end

        function mustHaveSameYScales(monitor, metricsToGroup)
            yscalesMap = monitor.Model.YScalesMap;
            subPlotMap = monitor.Model.SubPlotMap;

            axNames = [subPlotMap(metricsToGroup).Title];
            yscales = yscalesMap(axNames);
            yscales = unique(yscales);
            
            if ~isscalar(yscales)
                ME = MException(message('shared_experimentmonitor:validator:YScaleMismatchOnGrouping'));
				throwAsCaller(ME);
            end
        end

        function mustBeAnAxesName(userSpecifiedAxis, monitor, methodName)
            subPlotMap = monitor.Model.SubPlotMap;
            if numEntries(subPlotMap) == 0
                ME = MException(message('shared_experimentmonitor:validator:NoMetricsAdded', userSpecifiedAxis, methodName));
                throwAsCaller(ME);
            else
                existingAxesNames = [values(subPlotMap).Title];
                existingAxesNames = unique(existingAxesNames, "stable");

                if ~ismember(userSpecifiedAxis,existingAxesNames)
                    validAxesNames = strjoin(existingAxesNames, '", "');
                    ME = MException(message('shared_experimentmonitor:validator:AxesNotDefined',userSpecifiedAxis, validAxesNames));
                    throwAsCaller(ME);
                end
            end
        end

        function mustHaveUniqueValues(value)
            if numel(unique(value)) ~= numel(value)
                ME = MException(message('shared_experimentmonitor:validator:MustHaveUniqueValues'));
                throwAsCaller(ME);
            end
        end

        function mustNotReplace(oldMetrics, updatedMetrics, name)
            tf = all(ismember(oldMetrics, updatedMetrics));
            if ~tf
                msgFriendlyStringArray = """" + strjoin(updatedMetrics, {'","'}) + """";
                ME = MException(message('shared_experimentmonitor:validator:MustNotReplace', name, msgFriendlyStringArray));
                throwAsCaller(ME);
            end
        end

        function [step, metricNames, metricValues] = parseMetrics(existingMetricNames, metricData, step, varargin)
            p = experiment.internal.validator.createInputParser();
            addRequired(p,'step', @experiment.internal.validator.mustBeNumericScalarRealAndNonSparse);
            for field = existingMetricNames
                p.addParameter(field, '',  @experiment.internal.validator.mustBeNumericScalarRealAndNonSparse);
            end
            try
                p.parse(step, varargin{:});
            catch ME
                throwAsCaller(ME);
            end

            [tf, fnames] = experiment.internal.validator.inputHasUndefinedName(p);
            if tf
                ME = MException(message('shared_experimentmonitor:validator:UndefinedName', fnames{1}, 'Metrics'));
                throwAsCaller(ME);
            end

            step = experiment.internal.validator.convertNumbersToDouble(step);
            metricNames = setdiff(existingMetricNames, p.UsingDefaults);
            numMetricNames = length(metricNames);
            metricValues = zeros(1,numMetricNames);

            % Check that for all metricNames that are being recorded, if
            % any metric's last step value is greater than or equal to the
            % current step value, then error out to ensure that all metrics'
            % step values are strictly increasing.
            for i = 1:numMetricNames
                thisMetricName = metricNames(i);
                if ~isempty(metricData.(thisMetricName))
                    lastStep = metricData.(thisMetricName)(end,1);
                    if step <= lastStep
                        ME = MException(message('shared_experimentmonitor:validator:IndexCannotDecrease', thisMetricName, num2str(lastStep)));
                        throwAsCaller(ME);
                    end
                end
            end

            for i = 1:numMetricNames
                metricValues(i) = experiment.internal.validator.convertNumbersToDouble(p.Results.(metricNames(i)));
            end
        end

        function val = convertNumbersToDouble(val)
            if isnumeric(val)
                val = double(gather(val));
                if isa(val, 'dlarray')
                    val = extractdata(val);
                end
            end
        end

        function val = validateProgress(val)
            val = full(val);
            val = experiment.internal.validator.convertNumbersToDouble(val);
        end

        function [infoNames, infoValues] = parseInfo(existingInfo, infoData, varargin)
            p = experiment.internal.validator.createInputParser();
            for field = existingInfo
                p.addParameter(field, '',  @(val)experiment.internal.validator.validateInfo(infoData, val, char(field)));
            end
            try
                p.parse(varargin{:});
            catch ME
                throwAsCaller(ME);
            end

            [tf, fnames] = experiment.internal.validator.inputHasUndefinedName(p);
            if tf
                ME = MException(message('shared_experimentmonitor:validator:UndefinedName', fnames{1}, 'Info'));
                throwAsCaller(ME);
            end

            infoNames = setdiff(existingInfo, p.UsingDefaults);
            numInfoNames = length(infoNames);
            infoValues = cell(1,numInfoNames);
            for i = 1:numInfoNames
                infoValues{i} = convertCharsToStrings(experiment.internal.validator.convertNumbersToDouble(p.Results.(infoNames(i))));
            end
        end

        function mustBeNumericScalarRealAndNonSparse(val)
            mustBeNonempty(val);
            mustBeNumeric(val);
            mustBeScalarOrEmpty(val);
            mustBeNonsparse(val);
            mustBeReal(val);
        end

        function TF = validateInfo(infoData, val, infoName)
            % Info accepts numeric (except for sparse and complex), logical,
            % or string.
            TF = true;
            val = experiment.internal.validator.convertNumbersToDouble(val);
            val = convertCharsToStrings(val);
            
            mustBeNonempty(val);
            mustBeScalarOrEmpty(val);

            if ~isnumeric(val) && ~islogical(val) && ~isstring(val)
                ME = MException(message('shared_experimentmonitor:validator:InfoErrorType', infoName));
                throwAsCaller(ME);
            end

            if ~isstring(val)
                mustBeReal(val);
                mustBeNonsparse(val);
            end

            outputClass = class(val);
            isEmptyData = isempty(infoData.(infoName));
            infoClassName = class(infoData.(infoName));
            if ~strcmp(infoClassName, outputClass) && ~isEmptyData
                % Current Info value must match the previous info values.
                ME = MException(message('shared_experimentmonitor:validator:InfoErrorTypeMismatch', ...
                    infoName, infoClassName));
                throwAsCaller(ME);
            end
        end

        function [tf, fnames] = inputHasUndefinedName(p)
            fnames = fieldnames(p.Unmatched);
            tf = false;
            if ~isempty(fnames)
                tf = true;
            end
        end

        function p = createInputParser()
            p = inputParser();
            p.CaseSensitive = true;
            p.PartialMatching = false;
            p.KeepUnmatched = true;
        end
    end
end