classdef incrementalRobustRandomCutForest < incremental.anomaly.AnomalyDetector
    %

    %   Copyright 2023 The MathWorks, Inc.

    properties(GetAccess=public,SetAccess=protected)
        CollusiveDisplacement {mustBeMember(CollusiveDisplacement,{'maximal','average'})} = 'maximal'
        NumLearners(1,1) {mustBeNumeric,mustBePositive,mustBeInteger,mustBeNonsparse} = 100
        NumObservationsPerLearner(1,1) {mustBeNumeric, mustBeGreaterThan(NumObservationsPerLearner,2),mustBeInteger,mustBeNonsparse} = 256
        ObservationRemoval
        NumObservationsToKeep(1,1) {mustBeNumeric,mustBeNonnegative,mustBeInteger,mustBeNonsparse} = 256
    end

    properties (GetAccess=public, SetAccess=protected, Dependent)
        Mu
        Sigma
        CategoricalPredictors
        EstimationPeriod
    end

    properties(Access=protected)
        NumTreesToUpdate
    end

    properties (SetAccess = protected,Hidden)
        Impl(:,1) {mustBeA(Impl,'incremental.anomaly.impl.RRCForestImpl')} = incremental.anomaly.impl.RRCForestImpl.empty
    end

    properties (Constant, Access = protected)
        CanSupportCategoricalNatively = true
        CanSupportTables = true
    end

    methods
        function obj = incrementalRobustRandomCutForest(varargin)
            obj = obj@incremental.anomaly.AnomalyDetector(mfilename,varargin{:});
        end

        function [varargout] = fit(obj,X,varargin)
            varargin = incremental.utils.parseUnsupportedParameters('ObservationsIn', varargin{:});
            internal.stats.parseArgs({},{},varargin{:});
            [varargout{1:nargout}] = fit@incremental.anomaly.AnomalyDetector(obj,X,mfilename,varargin{:});
        end

        function catpreds = get.CategoricalPredictors(obj)
            catpreds = obj.PrivCategoricalPredictors;
        end

        function estimationperiod = get.EstimationPeriod(obj)
            estimationperiod = obj.Impl.EstimationPeriod;
        end

        function mu = get.Mu(obj)
            mu = anomaly.utils.getMu(obj.Impl.Mu,obj.CategoricalPredictors);
        end

        function sigma = get.Sigma(obj)
            sigma = anomaly.utils.getSigma(obj.Impl.Sigma,obj.CategoricalPredictors);
        end

        function [isAnomaly,scores] = isanomaly(obj,X,varargin)
            varargin = incremental.utils.parseUnsupportedParameters('ObservationsIn', varargin{:});
            [isAnomaly,scores] = obj.isanomalyHelper(X,'incrementalRobustRandomCutForest',varargin{:});
        end

    end

    methods (Access = protected)
        function [obj,X,removedIndices,nobs,iscat] = prepareForFit(obj,X,obsInRows,functionname, varargin)
            % prepare data for training and update internal states
            [obj,X,removedIndices,nobs,iscat] = prepareData(obj,X,obsInRows,functionname, varargin{:});
            obj.Impl = allocateEstimationBuffers(obj.Impl,obj.NumPredictors,obj.PredictorClass);
            [obj.Impl,X,~,~,removedIndices] = prepareDataAndUpdateEstimationState(obj.Impl,X,[],[],iscat,obsInRows,nobs,removedIndices,varargin{:});
            obj = synchronizePredictorInformation(obj,X,obsInRows);
            nobs = size(X,1);
        end

        function [obj,remainingArgs] = makeAnomalyDetector(obj,parametersAreFromOffline,remainingArgs)

            [modelParams,implParams,remainingArgs] = obj.extractRobustRandomCutForestParameters(parametersAreFromOffline,remainingArgs);

            obj.NumLearners               = modelParams.NumLearners;
            obj.NumLearners = double(obj.NumLearners);
            obj.NumObservationsPerLearner = modelParams.NumObservationsPerLearner;
            obj.NumObservationsPerLearner = double(obj.NumObservationsPerLearner);
            obj.NumObservationsToKeep = modelParams.NumObservationsToKeep;
            obj.NumObservationsToKeep = double(obj.NumObservationsToKeep);
            nt = max(1,floor(obj.NumLearners*obj.NumObservationsPerLearner/obj.NumObservationsToKeep));
            obj.NumTreesToUpdate = cast(min(obj.NumLearners,nt),'like',obj.NumLearners);
            cls = anomaly.utils.inttype((obj.NumObservationsPerLearner-1)*2+1);
            maximumsize = cast(obj.NumObservationsPerLearner*2-1,cls);

            implParams.MaximumSize =  maximumsize;
            implParams.NumPredictors =  obj.NumPredictors;
            implParams.NumLearners = obj.NumLearners;
            iscat = false(1,obj.NumPredictors);
            iscat(obj.CategoricalPredictors) = true;
            implParams.IsCategoricalPredictor = iscat;

            obj.Impl = incremental.anomaly.impl.RRCForestImpl.make(implParams,parametersAreFromOffline);

            [obj.Impl,remainingArgs] = configureForEstimation(obj.Impl,parametersAreFromOffline,obj.NumPredictors,obj.PredictorClass,remainingArgs);
            obj.CollusiveDisplacement = implParams.CollusiveDisplacement;
            [~,obj.ObservationRemoval] = incremental.anomaly.utils.validateObservationRemoval(implParams);
        end

        function cantrain = canTrain(obj)
            % can model train
            cantrain = canTrain(obj.Impl) && canTrain@incremental.anomaly.AnomalyDetector(obj);
        end

        function [obj] = fitWithoutValidation(obj, X,obsInRows,iscat,varargin)

            nobs = size(X,1);
            obj.Impl = fit(obj.Impl,X,[],[],[],obsInRows,nobs,iscat,obj.NumTreesToUpdate);
        end
    end

    methods (Access = protected)

        function statesdefined = statesDefined(obj)
            statesdefined = statesDefined(obj.Impl);
        end

        function obj = updateStateParameters(obj,numPredictors,predictorClass,categoricalPredictors)
            obj.Impl = updateStateParameters(obj.Impl,numPredictors,predictorClass,categoricalPredictors);
        end

        function [numPredictors,predictorClass] = predictorAttributes(obj)
            [numPredictors,predictorClass] = predictorAttributes(obj.Impl);
        end

        function scores = score(obj, X,~,iscat,varargin)
            internal.stats.parseArgs({},{},varargin{:});
            scores = score(obj.Impl,X,iscat);
        end

        function obj = resetAnomalyDetector(obj,varargin)
            obj.Impl = reset(obj.Impl,varargin{:});
        end

        function propList = propsForDisplay(obj)
            % common properties
            propList = propsForDisplay@incremental.anomaly.AnomalyDetector(obj);
            propList.NumLearners = obj.NumLearners;
            propList.NumObservationsPerLearner = obj.NumObservationsPerLearner;
        end
    end

    methods (Access = {?incremental.anomaly.AnomalyDetector,?matlab.unittest.TestCase})
        function [obj,X,removedIndices,N,iscategoric] = prepareData(obj,X,obsInRows, varargin)
            % prepareData Prepare data for creating
            % incrementalRobustRandomCutForest

            % prepareData from base class does not modify number of
            % observation and predictors.
            [obj,X,removedIndices,N,iscategoric] = prepareData@incremental.anomaly.AnomalyDetector(obj,X,obsInRows,varargin{:},'OrdinalIsCategorical',false); %,'OrdinalIsCategorical',false

            allnanrows = all(isnan(X),2);
            if any(allnanrows)
                removedIndices = allnanrows;
                N = N-sum(removedIndices);
                X(removedIndices,:) = [];
            end
        end
    end


    methods (Access=protected,Static=true)
        function validateScoreThreshold(thresh)
            validateattributes(thresh,{'single','double'},{'scalar','real','>=',0},'isanomaly','ScoreThreshold');
        end

        function [modelParams,implParams,remainingArgs] = extractRobustRandomCutForestParameters(parametersAreFromOffline,params)
            % extract parameters specific to RobustRandomCutForest

            dfltNumLearners                 = 100;
            dfltNumObservationsPerLearner   = 256;
            dfltNumObservationsToKeep             = dfltNumObservationsPerLearner;
            dfltCollusiveDisplacement               = 'maximal';
            dfltObservationRemoval               = 'oldest';
            dfltStandardizeData =         false;
            dfltTrees =         [];
            dfltUseParallel = false;
            dfltOptions = [];

            p = inputParser;
            p.StructExpand = true;
            p.KeepUnmatched = true;
            p.PartialMatching = true;

            addParameter(p,'NumLearners',dfltNumLearners);
            addParameter(p,'NumObservationsPerLearner',dfltNumObservationsPerLearner);
            addParameter(p,'NumObservationsToKeep',dfltNumObservationsToKeep);
            addParameter(p,'CollusiveDisplacement',dfltCollusiveDisplacement);
            addParameter(p,'ObservationRemoval',dfltObservationRemoval);
            addParameter(p,'StandardizeData',dfltStandardizeData);
            addParameter(p,'Trees',dfltTrees);
            addParameter(p,'UseParallel',dfltUseParallel);
            addParameter(p,'Options',dfltOptions);

            params = incremental.utils.convertStructToCell(params);

            parse(p,params{:});
            args = p.Results;
            usingDefaults = p.UsingDefaults;
            remainingArgs = p.Unmatched;

            if ~ismember('UseParallel',usingDefaults)
                error(message("stats:incremental:anomaly:RobustRandomCutForest:UseParallelNotAllowed"));
            end
            
            usepar = false;
            opts = args.Options;

            % use opts fields if it's not empty
            rngScheme = [];
            if ~isempty(opts)
                multipleStreams = true;
                [usepar,rngScheme] = internal.stats.parallel.validateParallelOptions(opts,multipleStreams);
            end
            
            standardize = internal.stats.parseOnOff(args.StandardizeData,'StandardizeData');
            if ~parametersAreFromOffline
                assert(isempty(args.Trees));
            else
                implParams.Mu = remainingArgs.Mu;
                implParams.Sigma = remainingArgs.Sigma;
                remainingArgs = rmfield(remainingArgs,{'Mu','Sigma'});
            end

            if isfield(remainingArgs,'SplitAtHalf')
                splitathalf = remainingArgs.SplitAtHalf;
                remainingArgs = rmfield(remainingArgs,{'SplitAtHalf'});
            else
                splitathalf = false;
            end

            % create modelParams structure
            modelParams.NumLearners               = args.NumLearners;
            modelParams.NumObservationsToKeep  = args.NumObservationsToKeep;
            modelParams.NumObservationsPerLearner          = args.NumObservationsPerLearner;
            implParams.CollusiveDisplacement             = args.CollusiveDisplacement;
            implParams.ObservationRemoval             = args.ObservationRemoval;
            implParams.StandardizeData = standardize;
            implParams.Trees = args.Trees;
            implParams.SplitAtHalf = splitathalf;
            implParams.RNGScheme = rngScheme;
            implParams.UseParallel = usepar;

        end

    end
end
