% Profile - Define parameters and properties for connecting to clusters and creating jobs
 
%   Copyright 2011-2023 The MathWorks, Inc.

% TODO:doc
% NB This class is hidden for now as the Profiles API is not yet documented
classdef (Hidden, Sealed) Profile < parallel.internal.settings.NamedNode
    %---------------------------------------------------
    % Properties stored in Settings
    %---------------------------------------------------
    properties (PCTGetSet, Transient, PCTConstraint = 'string')
        % The description of this profile
        Description
        % The name of the project component used by this profile
        ProjectComponent
    end
    
    properties (PCTGetSet, Transient, PCTConstraint = 'nonemptystring')
        % The name of the scheduler component used by this profile.
        % This cannot be an empty string
        SchedulerComponent
    end

    %---------------------------------------------------
    % Normal class properties
    %---------------------------------------------------
    properties (Dependent)
        % The validation status of this profile.
        ValidationStatus 
    end
    
    properties (GetAccess = private, SetAccess = immutable)
        IsSchedulerComponentReadOnly = false
    end
    
    properties (Access = private)
        SettingsNodeDestroyedListener
    end
    
    methods % Property get/set methods
        function status = get.ValidationStatus(obj)
            import parallel.internal.settings.AbstractValidator
            import parallel.settings.Profile

            status = AbstractValidator.getDefaultStatus();
            validator = Profile.currentValidator();
            if ~isempty(validator) && obj.NodeFacade.IsValid
                status = validator.getStatus(obj.Name);
            end
            status = status.Name;
        end
    end

    methods (Hidden, Static) % Implementation of NamedNode Hidden, Static methods
        function obj = hBuildFromSettings(name, settingsNode, parentSettingsNode, varargin) 
            obj = parallel.settings.Profile(name, settingsNode, parentSettingsNode, varargin{:});
        end
    end
    
    methods (Hidden) %Override of methods defined in CustomSettingsGetSet
        function hSetProperty(obj, props, values, varargin)
            if obj.IsSchedulerComponentReadOnly && ~isempty(props) && any(strcmp(props, 'SchedulerComponent'))
                throwAsCaller(MException(message('parallel:settings:ProfileCannotChangeSchedulerComponent', obj.Name)));
            end
            hSetProperty@parallel.internal.settings.NamedNode(...
                obj, props, values, varargin{:});
        end
    end
    
    methods (Access = private) % Constructor
        function obj = Profile(name, settingsNode, parentSettingsNode, readOnlyAliases, readOnlySchedNames) 
            import parallel.internal.types.SettingsLevel
            import parallel.settings.Profile

            if nargin < 4
                readOnlyAliases ={};
            end
            
            obj@parallel.internal.settings.NamedNode(name, settingsNode, parentSettingsNode, readOnlyAliases);
            obj.addlistener('NameChanged', @obj.handleNameChanged);
            obj.SettingsNodeDestroyedListener = event.listener(obj, 'SettingsNodeDestroyed', ...
                @(~, ~) obj.handleNodeDestroyed(name));
            
            if nargin > 4
                obj.IsSchedulerComponentReadOnly = any(strcmp(name, readOnlySchedNames));
            end
        end
    end
    
    methods  % destructor
        function delete(objOrObjs, varargin)
        % NB delete must define more than 1 arg to distinguish it from 
        % "normal" handle delete
            import parallel.settings.Profile
            
            % TODO:later once Profiles API is documented, work out how
            % to ensure that the status is not removed from the validator
            % if delete() is called with invalid syntax.
            validator = Profile.currentValidator();
            if ~isempty(validator)
                for ii = 1:length(objOrObjs)
                    % Remove the validation status if we have a valid name
                    if objOrObjs(ii).HasName
                        validator.deleteStatus(objOrObjs(ii).Name);
                    end
                end
            end
            % Call the superclass delete.
            delete@parallel.internal.settings.NamedNode(objOrObjs, varargin{:});
        end
    end
       
    methods (Hidden, Static)
        function oldValidator = hRegisterValidator(validator)
            import parallel.settings.Profile
            if ~isempty(validator)
                validateattributes(validator, {'parallel.internal.settings.AbstractValidator'}, {'scalar'});
            end
            oldValidator = Profile.currentValidator(validator);
        end
        
        function validator = hGetRegisteredValidator()
            import parallel.settings.Profile
            validator = Profile.currentValidator();
        end
    end
       
    methods
        function validate(obj, suite)
        % validate(obj, suite) - run validation suite on the current profile
            import parallel.internal.settings.ValidationDisplayer
            import parallel.internal.validator.ValidationSuite
            import parallel.settings.Profile
            
            % Use the default (full) validation suite if none is specified
            if nargin < 2
                % Assume a scheduler with validation matching 'Processes'
                % by default.
                schedulerType = 'Processes';
                if ~parallel.settings.DefaultValue.isDefault(obj.SchedulerComponent)
                    p = parallel.Settings;
                    schedulerComponent = p.findSchedulerComponent('Name', obj.SchedulerComponent);
                    if ~isempty(schedulerComponent)
                        schedulerType = schedulerComponent.Type;
                    end
                end
                suite = ValidationSuite.createForSchedulerType(schedulerType);
            else
                validateattributes(suite, {'parallel.internal.validator.ValidationSuite'}, {'scalar'});
            end

            validator = Profile.currentValidator();
            if isempty(validator)
                warning(message('parallel:settings:NoRegisteredProfileValidator'));
            else
                % Ensure that validation results are displayed to the command 
                % windows when validation is launched through the API.
                displayer = ValidationDisplayer(validator); %#ok<NASGU> - hold onto displayer
                validator.validate(obj.Name, suite);
            end
        end
        
        function val = getSchedulerComponent(obj)
        % component = getSchedulerComponent(obj) - get the SchedulerComponent
        % object whose name is the same as the SchedulerComponent property
        % of the current profile.  This method throws an error if there is 
        % no scheduler component with that name.
        
            if parallel.settings.DefaultValue.isDefault(obj.SchedulerComponent)
                error(message('parallel:settings:ProfileSchedulerComponentNotDefined', obj.Name));
            end
            
            p = parallel.Settings;
            val = p.findSchedulerComponent('Name', obj.SchedulerComponent);
            if isempty(val)
                error(message('parallel:settings:ProfileCannotFindSchedulerComponent', obj.Name, obj.SchedulerComponent));
            end
        end
        
        function val = getProjectComponent(obj)
        % component = getProjectComponent(obj) - get the ProjectComponent
        % object whose name is the same as the ProjectComponent property
        % of the current profile.  If the project component is unset,
        % then an empty project component is returned.  If the project 
        % component is not unset and there is no project component with that
        % name, then this errors.
        
            if parallel.settings.DefaultValue.isDefault(obj.ProjectComponent)
                % It is OK not to have a project component
                val = parallel.settings.ProjectComponent.empty();
                return;
            end
            
            p = parallel.Settings;
            val = p.findProjectComponent('Name', obj.ProjectComponent);
            if isempty(val)
                error(message('parallel:settings:ProfileCannotFindProjectComponent', obj.Name, obj.ProjectComponent));
            end
        end

        function export(objOrObjs, filename)
        % export(obj, filename) - export the collapsed profile and 
        % associated scheduler components to the specified file.
            validateattributes(filename, {'char'}, {'row'}, 'export', 'filename');
            import parallel.internal.settings.NamedNode
            
            try
                objOrObjs = unique(objOrObjs);
                objOrObjsToExport = NamedNode.removeUnexportableNodes(objOrObjs);
            
                % Get the components for all the profiles
                allSchedComps = arrayfun(@(x) x.getSchedulerComponent(), ...
                    objOrObjsToExport, 'UniformOutput', false);
                allProjComps  = arrayfun(@(x) x.getProjectComponent(), ...
                    objOrObjsToExport, 'UniformOutput', false);
                schedCompsToExport = NamedNode.removeUnexportableNodes([allSchedComps{:}]);
                projCompsToExport  = NamedNode.removeUnexportableNodes([allProjComps{:}]);
                
                profileNodes = iGetNodeFacadeOrEmpty(objOrObjsToExport);
                schedCompNodes = iGetNodeFacadeOrEmpty(schedCompsToExport);
                projCompNodes = iGetNodeFacadeOrEmpty(projCompsToExport);
                
                nodeFacadesToExport = [profileNodes, schedCompNodes, projCompNodes];
                nodeFacadesToExport.exportNode(filename);
            catch err
                throw(err);
            end
        end
    end

    methods (Access = protected) % overrides of NamedNode Abstract methods
        function errorIfInvalidName(~, newName)
            p = parallel.Settings;
            if p.hProfileExists(newName)
                error(message('parallel:settings:ProfileNameAlreadyExists', ... 
                    newName));
            end
        end
    end
    
    methods (Static, Access = private) % methods defined in other files    
        currValidator = currentValidator(newValidator)
    end
    
    methods (Access = private)
        function handleNameChanged(obj, ~, namedChangedEventData)
            import parallel.settings.Profile
            validateattributes(namedChangedEventData, ...
                {'parallel.internal.settings.NameChangedEventData'}, {'scalar'})
                
            % Update the validator with our new name
            validator = Profile.currentValidator();
            if ~isempty(validator)
                oldStatus = validator.getStatus(namedChangedEventData.OldName);
                validator.deleteStatus(namedChangedEventData.OldName);
                validator.setStatus(namedChangedEventData.NewName, oldStatus);
            end

            % Change the SettingsNodeDestroyed listener so that we can remove
            % the correct profile from the validator when the profile is deleted.
            obj.SettingsNodeDestroyedListener = event.listener(obj, 'SettingsNodeDestroyed', ...
                @(~, ~) obj.handleNodeDestroyed(namedChangedEventData.NewName));
        end

        function handleNodeDestroyed(~, name)
            import parallel.settings.Profile
            % Remove our status from the validator since the underlying Settings Node
            % has disappeared and the validation status is no longer valid
            validator = Profile.currentValidator();
            if ~isempty(validator)
                validator.deleteStatus(name);
            end
        end
    end
end
%#ok<*ATUNK> custom attributes

function nodes = iGetNodeFacadeOrEmpty(namedNodes)
if isempty(namedNodes)
    nodes = parallel.internal.settings.SettingsNodeFacade.empty;
else
    nodes = [namedNodes.NodeFacade];
end
end
