% Settings - Singleton for accessing settings and profiles for Parallel Computing Toolbox

%   Copyright 2011-2023 The MathWorks, Inc.



% NB This class is hidden for now as the Profiles API is not yet documented
classdef (Hidden, Sealed) Settings < parallel.internal.settings.CustomSettingsGetSet & ...
                                     parallel.internal.settings.CustomSettingsPropDisp

    %---------------------------------------------------
    % Properties stored in Settings
    %---------------------------------------------------
    properties (PCTGetSet, Transient, PCTConstraint = 'nonemptystring')
        DefaultProfile
    end
    
    % This stuff for pctconfig - leave for later.
    % properties (PCTGetSet, Transient, PCTConstraint = 'nonemptystring')
        % Hostname
    % end
    % properties (PCTGetSet, Transient, PCTConstraint = 'portrange')
        % PortRange
    % end

    properties (Access = private, PCTGetSet, Transient, PCTConstraint = 'numericscalar')
        ConfigurationsLastUpgradeTime
    end
    
    %---------------------------------------------------
    % Normal class properties
    %---------------------------------------------------
    properties (Hidden, Transient)
        % The settings related to pools.  
        Pool
    end
    
    properties (Transient, Access = protected) % Implementation of CustomSettingsGetSet Abstract properties
        NodeFacade
    end
    
    properties (Constant, GetAccess = private)
        LocalProfileName = 'Processes';
        FactoryDefaultProfileName = iGetFactoryDefaultProfileName();
        LastUpgradeSettingsLevel = parallel.internal.types.SettingsLevel.User;
        
        ProfilesMcrUserDataKey = 'ParallelProfile';
        ConfigsMcrUserDataKey = 'ParallelConfigurationFile';
        McrUserDataNameSuffix = '_mcruserdata';
        ImportNameSuffix = '_Import';

        % This is required to ensure a validator is registered for profiles.
        DummyEnforceInitClient = initclient();
    end

    properties (Dependent, Transient, SetAccess = private)
        % The array of Profiles
        Profiles
        % The array of Scheduler Components
        SchedulerComponents
        % The array of Project Components
        ProjectComponents
    end
    
    properties (Transient, Access = private)
        % The NamedNodesCollection that stores the profiles
        ProfilesCollection
        % The SchedulerComponentsCollection that stores the scheduler components
        SchedulerComponentsCollection
        % The NamedNodesCollection that stores the project components
        ProjectComponentsCollection
        UsingDeployedCollections = false;
        % The actual Profile object that corresponds to the DefaultProfile
        DefaultProfileObject
        % Listeners for the DefaultProfileObject
        DefaultProfileListeners
        % Listener for the DefaultProfileChanged
        DefaultProfileChangedListener       
    end
    
    events (Hidden)
        DefaultProfileChanged
        ProfileAdded
        SchedulerComponentAdded
        ProjectComponentAdded
    end
    
    methods % Property get/set methods
        function value = get.Profiles(obj)
            value = obj.ProfilesCollection.getAll();
        end
        
        function value = get.SchedulerComponents(obj) 
            value = obj.SchedulerComponentsCollection.getAll();
        end

        function value = get.ProjectComponents(obj) 
            value = obj.ProjectComponentsCollection.getAll();
        end
    end
    
    methods (Hidden)
        function hSetProperty(obj, props, values, varargin)
        % Override of CustomSettingsGetSet hSetProperty method because
        % the collapsed value of DefaultProfile needs to be the name 
        % of an existing profile.  If the profile doesn't exist, error.
            if ~iscell( props )
                props  = { props };
                values = { values };
            end
            defaultProfileIdx = find(strcmp(props, 'DefaultProfile'), 1, 'last');
            if ~isempty(defaultProfileIdx)
                desiredProfileName = values{defaultProfileIdx};
                if ~obj.ProfilesCollection.nameExists(desiredProfileName)
                    validNames = obj.ProfilesCollection.getAllNames();
                    error(message('parallel:settings:CannotSetDefaultProfile', ...
                        desiredProfileName, desiredProfileName, ...
                        iFormatProfileNamesForMessages(validNames)));
                end
            end
            hSetProperty@parallel.internal.settings.CustomSettingsGetSet(obj, props, values, varargin{:});
        end
        
        function values = hGetProperty(obj, props, varargin)
        % Override of CustomSettingsGetSet hGetProperty method because the
        % collapsed value of DefaultProfile needs to be the name of an
        % existing profile.  If the profile doesn't exist, we set 
        % DefaultProfile to the factory default profile (i.e. local in 
        % Desktop MATLAB)
            import parallel.internal.types.SettingsLevel
            import parallel.Settings
            
            values = hGetProperty@parallel.internal.settings.CustomSettingsGetSet(obj, props, varargin{:});
            isDefaultProfileIdx = strcmp(props, 'DefaultProfile');
            if ~any(isDefaultProfileIdx)
                return;
            end

            returnCell = true;
            if ~iscell(props)
                props = {props};
                values = {values};
                returnCell = false;
            end
            if nargin < 3
                % default is to get collapsed value when no level is supplied.
                levels = repmat(SettingsLevel.Collapsed, size(props));
            else
                levels = varargin{1};
            end

            [values(isDefaultProfileIdx), setUserDefaultToFactoryDefaultProfile] = iReplaceBadDefaultProfile( ...
                values(isDefaultProfileIdx), levels(isDefaultProfileIdx), ...
                obj.ProfilesCollection);
            
            if setUserDefaultToFactoryDefaultProfile
                % As long as PCT is not "taught" how to work with no profiles at all
                % this will be fine, there will be at least one good profile returned
                newDefaultGoodProfile = values{1};
                % Use the no-check option to set the Default Profile at the user level
                obj.hSetPropertyNoCheck('DefaultProfile', newDefaultGoodProfile, SettingsLevel.User);
            end
            if ~returnCell
                values = values{1};
            end
        end

        function names = hGetAllProfileNames(obj)
        % names = hGetAllProfileNames(obj) - get the names of all the profiles.
        % This is more efficient than accessing the Name property of all of the
        % profiles directly.
            names = obj.ProfilesCollection.getAllNames();
        end
        
        function names = hGetAllSchedulerComponentNames(obj)
        % names = hGetAllSchedulerComponentNames(obj) - get the names of all the 
        % scheduler components.  This is more efficient than accessing the Name
        % property of all of the scheduler components directly.
            names = obj.SchedulerComponentsCollection.getAllNames();
        end
        
        function names = hGetAllProjectComponentNames(obj)
        % names = hGetAllProjectComponentNames(obj) - get the names of all the 
        % project components.  This is more efficient than accessing the Name
        % property of all of the project components directly.
            names = obj.ProjectComponentsCollection.getAllNames();
        end
        
        function name = hGetUnusedProfileName(obj, proposedName)
        % name = hGetUnusedProfileName(obj, proposedName) - get an unused
        % profile name inspired by proposedName.  If no proposedName is 
        % supplied, then a sensible proposedName is used.
            if nargin < 2
                proposedName = 'profile';
            end
            name = iGetUnusedName(proposedName, obj.hGetAllProfileNames());
        end

        function name = hGetUnusedSchedulerComponentName(obj, proposedName)
        % name = hGetUnusedSchedulerComponentName(obj, proposedName) - get an unused
        % scheduler component name inspired by proposedName.  If no proposedName is 
        % supplied, then a sensible proposedName is used.
            if nargin < 2
                proposedName = 'schedulerComponent';
            end
            name = iGetUnusedName(proposedName, obj.hGetAllSchedulerComponentNames());
        end
        
        function name = hGetUnusedProjectComponentName(obj, proposedName)
        % name = hGetUnusedProjectComponentName(obj, proposedName) - get an unused
        % project component name inspired by proposedName.  If no proposedName is 
        % supplied, then a sensible proposedName is used.
            if nargin < 2
                proposedName = 'projectComponent';
            end
            name = iGetUnusedName(proposedName, obj.hGetAllProjectComponentNames());
        end
        
        function tf = hIsValidName(~, name)
            tf = isvarname(name);
        end

        function tf = hProfileExists(obj, name)
        % tf = hProfileExists(obj, name) - does a profile with the 
        % specified name exist?
            tf = obj.ProfilesCollection.nameExists(name);
        end
        
        function tf = hSchedulerComponentExists(obj, name)
        % tf = hSchedulerComponentExists(obj, name) - does a scheduler 
        % component with the specified name exist?
            tf = obj.SchedulerComponentsCollection.nameExists(name);
        end
        
        function tf = hProjectComponentExists(obj, name)
        % tf = hProjectComponentExists(obj, name) - does a project 
        % component with the specified name exist?
            tf = obj.ProjectComponentsCollection.nameExists(name);
        end

        function hThrowNonExistentProfile(obj, nonExistentName)
            allNames = obj.hGetAllProfileNames();
            allNames = allNames(parallel.internal.settings.isProfileVisible(allNames));
            throwAsCaller(MException(message('parallel:settings:NonExistentProfile', nonExistentName, iFormatProfileNamesForMessages(allNames))));
        end
        
        function localProfile = hGetLocalProfile(obj)
            import parallel.Settings
            localProfile = obj.findProfile('Name', Settings.LocalProfileName);
            if isempty(localProfile)
                parallel.internal.warningNoBackTrace( ...
                    message('parallel:settings:FactoryProfileNotFound', Settings.LocalProfileName));
            end
        end
        
        function [profileNames, schedCompNames, projCompNames, profNamesInFile] = ...
            hImportProfilesFromFile(obj, filename)
            
            import parallel.internal.settings.NamedNodesCollection
            import parallel.internal.settings.ParallelSettingsTree
            import parallel.settings.Profile
            import parallel.internal.types.SettingsLevel
            
            profileNames   = {};
            schedCompNames = {};
            projCompNames  = {};
            profNamesInFile = {};
            [profCollection, scCollection, pcCollection] = iLoadSettings(filename);
            if isempty(profCollection) && isempty(scCollection) && isempty(pcCollection)
                parallel.internal.warningNoBackTrace( ...
                    message('parallel:settings:ImportProfileNoProfilesInFile', filename));
                return;
            end
            
            [profileNames, schedCompNames, projCompNames, profNamesInFile] = ...
                obj.addAllCollections(profCollection, scCollection, pcCollection, ...
                parallel.Settings.ImportNameSuffix, SettingsLevel.User, filename);
        end
        
        function hResetLastConfigurationUpgrade(obj)
        % hResetLastConfigurationUpgrade(obj) - reset the flag that indicates
        % when the last configuration upgrade occurred.
            obj.NodeFacade.unsetKey('ConfigurationsLastUpgradeTime', ...
                parallel.Settings.LastUpgradeSettingsLevel);
        end
        
        function tf = hNeedsConfigurationsUpgrade(obj)
        % tf = hNeedsConfigurationsUpgrade(obj) - do the configurations need
        % upgrading?
            tf = ~obj.NodeFacade.isKeySet('ConfigurationsLastUpgradeTime', ...
                parallel.Settings.LastUpgradeSettingsLevel);
        end
        
        function hSetConfigurationsUpgraded(obj)
        % hSetConfigurationsUpgraded(obj) - indicate that configurations
        % have been upgraded
            obj.NodeFacade.setKey('ConfigurationsLastUpgradeTime', now, ...
                parallel.Settings.LastUpgradeSettingsLevel); %#ok<TNOW1> required for backwards compatibility
        end
        
        function val = hGetConfigurationsLastUpgradeTime(obj)
        % val = hGetConfigurationsLastUpgradeTime(obj) - get the time
        % when configurations were last upgraded
            val = obj.NodeFacade.getKey('ConfigurationsLastUpgradeTime', ...
                parallel.Settings.LastUpgradeSettingsLevel);
        end
    end
    
    methods (Hidden, Access = ?matlab.unittest.TestCase)
        function [profCollection, scCollection, pcCollection] = hGetCollections(obj)
        % Used only for testing
            profCollection = obj.ProfilesCollection;
            scCollection = obj.SchedulerComponentsCollection;
            pcCollection = obj.ProjectComponentsCollection;
        end
    end
    
    methods % Constructor
        function obj = Settings(varargin)
        % Settings is a singleton.  Always return the same one.
            % mlock this file so that we always return the same handle from the constructor
            mlock;
            
            % This will error in a deployed application if
            % this is the second MCR component in the application.
            parallel.internal.apishared.mcrShutdownHandler( 'initialize' );

            persistent sInstance
            if isempty(sInstance) || ~isvalid(sInstance)
                % Ensure that we don't end up with a poorly built Settings object 
                % e.g. in the case of a CTRL+C
                try
                    obj.buildProperties();
                catch err
                    obj.delete();
                    throw(err);
                end
               
                sInstance = obj;
                cleanupObj = parallel.internal.general.DisarmableOncleanup(@() sInstance.delete());

                % NB It is important not to do the configurations upgrade until 
                % AFTER the various collections have been build because it assumes
                % that the collections exist. 
                iUpgradeConfigurationsIfRequired();
                
                cleanupObj.disarm();
            else
                % If we are just returning the existing instance, then we allow no argsin
                narginchk(0, 0);
            end

            obj = sInstance;
        end
    end

    methods (Hidden)
        function hChangeStorage(obj, mode, varargin)
        % Hidden backdoor to change the storage mechanism used for the settings.  
        % This allows QE to simulate a deployed application.
        
        % Note that currently this only changes the storage for the profiles
        % and components and not for the Client specific settings (e.g. DefaultProfile)
            obj.buildProperties(mode, varargin{:});
        end
    end
    
    methods (Access = private)
        function delete(obj)
        % Private delete to prevent Settings from being deleted via 
        % the normal channels
            obj.clearProperties();
        end
    end
    
    methods % methods defined in other files
        profile = createProfile(obj, profileName, schedulerComponentName, varargin) 
        component = createSchedulerComponent(obj, type, name, varargin)
        component = createProjectComponent(obj, name, varargin)
        profiles = findProfile(obj, varargin)
        components = findSchedulerComponent(obj, varargin) 
        components = findProjectComponent(obj, varargin)
    end
    
    methods (Static, Access = private)
        function [nameIsSpecified, name, pvPairs] = amalgamateAndExtractName(pvPairs)
        % Amalgamate the P-V pairs and extract the value from the
        % 'Name' P-V pair, if it exists
            import parallel.internal.customattr.PropSet
            
            [nameIsSpecified, name, pvPairs] = PropSet.extractOneProperty('Name', pvPairs{:});
            [props, vals] = PropSet.amalgamate({}, pvPairs{:});
            pvPairs = PropSet.namesValuesToPv(props, vals);
        end
    end

    methods (Static)
        function tf = isFactoryProfile(profileName)
            tf = strcmp(profileName, parallel.Settings.LocalProfileName);
        end
    end

    methods (Static, Hidden, Access = ?matlab.unittest.TestCase)
        function settings = hImportXMLCloudClusterProfile(filename)
            settings = importXMLCloudClusterProfile(filename);
        end
    end

    methods (Access = private)
        function setDefaultProfileObjectAndListeners(obj, profileName)
            import parallel.Settings
            
            if ~isempty(obj.DefaultProfileObject) && isvalid(obj.DefaultProfileObject) && ...
                strcmp(obj.DefaultProfileObject.Name, profileName)
                % Do nothing if we've already got the right DefaultProfileObject
                return;
            end
            
            obj.DefaultProfileObject = obj.findProfile('Name', profileName);
            if isempty(obj.DefaultProfileObject)
                % It's pretty unlikely we'll get in here because we check that the name is 
                % a valid profile before calling setDefaultProfileObjectAndListeners.
                % If local can't be found, then the factory settings have gone.
                if Settings.isFactoryProfile(profileName)
                    parallel.internal.warningNoBackTrace( ...
                        message('parallel:settings:FactoryProfileNotFound', profileName));
                else
                    parallel.internal.warningNoBackTrace( ...
                        message('parallel:settings:DefaultProfileObjectNotFound', profileName));
                end
                % Make sure we let go of any old listeners
                obj.DefaultProfileListeners = [];
            else
                destroyedListener = event.listener(obj.DefaultProfileObject, ...
                    'ObjectBeingDestroyed', @obj.handleDefaultProfileDestroyed);
                nameChangedListener = event.listener(obj.DefaultProfileObject, ...
                    'NameChanged', @obj.handleDefaultProfileNameChanged);
                obj.DefaultProfileListeners = [destroyedListener, nameChangedListener];
            end
        end
        
        function handleDefaultProfileDestroyed(obj, ~, ~)
            import parallel.internal.types.SettingsLevel
            import parallel.settings.Profile

            % Set the DefaultProfileObject to empty and let go of old listeners
            obj.DefaultProfileObject = Profile.empty();
            obj.DefaultProfileListeners = [];
            
            % Get the collapsed default profile - this will trigger all the checking about
            % whether the collapsed default is a valid name and set the 
            % new DefaultProfileObject if necessary
            obj.hGetProperty('DefaultProfile', SettingsLevel.Collapsed);
        end
        
        function handleDefaultProfileNameChanged(obj, ~, evtData)
            validateattributes(evtData, {'parallel.internal.settings.NameChangedEventData'}, {'scalar'});
            oldName = evtData.OldName;
            newName = evtData.NewName;
            if strcmp(oldName, newName)
                % Nothing to do if the name hasn't actually changed
                return;
            end
            
            % Use full public API for settings Default Profile in case
            % there is some oddness with setting stuff from the UI.
            obj.DefaultProfile = newName;
            parallel.internal.warningNoBackTrace( ...
                message('parallel:settings:NameOfDefaultProfileChanged', oldName, newName, oldName, newName));
        end
        
        function handleDefaultProfileChanged(obj)         
            obj.setDefaultProfileObjectAndListeners(obj.DefaultProfile);
            obj.notify('DefaultProfileChanged');   
        end
        
        function [profNames, scNames, pcNames, origProfNames] = ...
            addAllCollections(obj, profCollection, scCollection, pcCollection, nameSuffix, ...
            level, filename)
            
            % Creating the collections from the nodes will also validate that the
            % profiles/components are defined correctly in the supplied node.
            try
                [scNames, origScNames, haveScNamesChanged] = iAddObjectsFromCollection(...
                    scCollection, obj.SchedulerComponentsCollection, nameSuffix, level);
            catch err
                ex = MException(message('parallel:settings:ImportProfileFailedToAddSchedulerComponents', filename));
                ex = ex.addCause(err);
                throw(ex)
            end

            try
                [pcNames, origPcNames, havePcNamesChanged] = iAddObjectsFromCollection(...
                    pcCollection, obj.ProjectComponentsCollection, nameSuffix, level);
            catch err
                % Remove the scheduler components that were added
                iDeleteNamesFromCollection(obj.SchedulerComponentsCollection, scNames);
                ex = MException(message('parallel:settings:ImportProfileFailedToAddProjectComponents', filename));
                ex = ex.addCause(err);
                throw(ex)
            end
            
            try
                % Ensure profiles that reference old names are changed before merging
                iChangeComponentNamesInProfile(profCollection, ...
                    origScNames(haveScNamesChanged), scNames(haveScNamesChanged), ...
                    origPcNames(havePcNamesChanged), pcNames(havePcNamesChanged));
                [profNames, origProfNames] = iAddObjectsFromCollection(...
                    profCollection, obj.ProfilesCollection, nameSuffix, level);
            catch err
                % Remove the components that were added
                iDeleteNamesFromCollection(obj.SchedulerComponentsCollection, scNames);
                iDeleteNamesFromCollection(obj.ProjectComponentsCollection, pcNames);
                ex = MException(message('parallel:settings:ImportProfileFailedToAddProfiles', filename));
                ex = ex.addCause(err);
                throw(ex)
            end
        end
        
        function addTransientProfilesFromFile(obj, filename)
            import parallel.internal.types.SettingsLevel
            assert(obj.UsingDeployedCollections);
            
            if isempty(filename)
                return;
            end
            
            % Ensure that profiles etc that are created here are always transient
            nSetCreateTransientNodes(obj, true);
            cleanupObj = onCleanup(@() nSetCreateTransientNodes(obj, false));
            
            % Load the settings from the file
            [~, ~, ext] = fileparts(filename);
            if strcmpi(ext, '.mat')
                [profNames, ~, ~, origProfNames] = ...
                    parallel.internal.settings.importProfileOrConfiguration(filename);
            else
                [profCollection, scCollection, pcCollection] = iLoadSettings(filename);
                % Nothing to do if the file contains no profiles
                if isempty(profCollection) && isempty(scCollection) && isempty(pcCollection)
                    return;
                end
                [profNames, ~, ~, origProfNames] = ...
                    obj.addAllCollections(profCollection, scCollection, pcCollection, ...
                    parallel.Settings.McrUserDataNameSuffix, SettingsLevel.Session, filename);
            end

            if ~isempty(profNames)
                % Warn if we changed any names in the imported file or if
                % we imported more than 1 profile.  If both have occurred, 
                % then only warn about the name changes (since the purpose
                % of the > 1 profile imported message is simply to tell the
                % user what the default has been changed to).
                defaultToSet = profNames{1};
                haveProfNamesChanged = ~strcmp(profNames, origProfNames);
                if any(haveProfNamesChanged)
                    origNames = origProfNames(haveProfNamesChanged);
                    newNames  = profNames(haveProfNamesChanged);
                    namesChanged = {origNames{:}; newNames{:}};
                    parallel.internal.warningNoBackTrace( ...
                        message('parallel:settings:ImportMcruserdataProfileNamesChanged', filename, ...
                                    sprintf( '\t''%s'' --> ''%s''\n', namesChanged{ : } ), defaultToSet));
                elseif numel(profNames) > 1
                    parallel.internal.warningNoBackTrace( ...
                        message('parallel:settings:ImportMcruserdataMultipleProfiles', filename, defaultToSet));
                end
                obj.set('DefaultProfile', defaultToSet, SettingsLevel.Session.Name);
            end

            function nSetCreateTransientNodes(obj, value)
                obj.ProfilesCollection.CreateTransientNodes            = value;
                obj.SchedulerComponentsCollection.CreateTransientNodes = value;
                obj.ProjectComponentsCollection.CreateTransientNodes   = value;
            end
        end
        
        function clearProperties(obj)
            import parallel.internal.types.SettingsLevel
            % Tidy up any existing state
            % 1. explicitly clear listeners to prevent them from firing as we
            % rebuild the collections
            % 2. Ensure that any old QE overrides are reverted
            % 3. Clear all collections to ensure that the profiles/component
            % handles are invalidated
            obj.DefaultProfileListeners = [];
            obj.DefaultProfileObject = [];

            obj.DefaultProfileChangedListener = [];
            
            if obj.UsingDeployedCollections
                % Ensure that we unset the DefaultProfile and then disallow 
                % the session level
                obj.unset('DefaultProfile', SettingsLevel.Session.Name);
                parallel.internal.settings.qeDeployedOverride(false);
            end
            obj.SchedulerComponentsCollection = [];
            obj.ProfilesCollection = [];
            obj.ProjectComponentsCollection = [];
        end
        
        function buildProperties(obj, mode, varargin)
            import parallel.internal.settings.ParallelSettingsTree
            import parallel.internal.settings.SettingsNodeFacade

            if nargin == 1
                mode = 'normal';
            else
                validatestring(mode, {'normal', 'QETestDeployed'});
            end
            
            obj.clearProperties();
            switch mode
                case 'QETestDeployed'
                    validateattributes(varargin{1}, {'char'}, {'row'});
                    obj.UsingDeployedCollections = true;
                    profileFilename = varargin{1};
                    parallel.internal.settings.qeDeployedOverride(true);
                    [profCollection, scCollection, pcCollection] = iBuildDeployedCollections();
                case 'normal'
                    obj.UsingDeployedCollections = isdeployed();
                    if obj.UsingDeployedCollections
                        profileFilename = iGetDeployedProfileFilename();
                        [profCollection, scCollection, pcCollection] = iBuildDeployedCollections();
                    else
                        [profCollection, scCollection, pcCollection] = iBuildCollectionsFromSettings();
                    end
                otherwise
                    % should never get here because of validatestring check
            end
            obj.SchedulerComponentsCollection = scCollection;
            obj.ProfilesCollection = profCollection;
            obj.ProjectComponentsCollection = pcCollection;
            
            % Use addlistener to ensure that the listener has the same lifetime as the collection.
            obj.ProfilesCollection.addlistener('NodeAdded', ...
                @(~, evtData) obj.notify('ProfileAdded', evtData));
            
            obj.SchedulerComponentsCollection.addlistener('NodeAdded', ...
                @(~, evtData) obj.notify('SchedulerComponentAdded', evtData));
            
            obj.ProjectComponentsCollection.addlistener('NodeAdded', ...
                @(~, evtData) obj.notify('ProjectComponentAdded', evtData));            

            % Currently use the same Settings Node for normal and
            % deployed modes.
            obj.NodeFacade = SettingsNodeFacade(ParallelSettingsTree.ClientSettingsNode, ...
                ParallelSettingsTree.ClientSettingsNodeName, ParallelSettingsTree.ParallelNode);
                
            obj.setDefaultProfileObjectAndListeners(obj.DefaultProfile);
            % The default profile will definitely have changed because
            % we have re-built the collections and have new Profiles handles.
            % Ensure we do this after setting the appropriate listeners on it.
            obj.notify('DefaultProfileChanged');
            
            % Add a listener to the NodeFacade to support changes to the
            % DefaultProfile from java
            obj.DefaultProfileChangedListener = obj.NodeFacade.addChangeListenerToKeys({'DefaultProfile'}, @(~, ~) obj.handleDefaultProfileChanged());

            % NB It is important not to import the profiles from file until AFTER 
            % the collections have been assigned because it assumes that the various 
            % collections exist.
            if obj.UsingDeployedCollections
                try
                    obj.addTransientProfilesFromFile(profileFilename);
                catch err
                    ex = MException(message('parallel:settings:FailedToAddMcrUserDataProfile', profileFilename));
                    ex = ex.addCause(err);
                    throw(ex);
                end
            end
            % Remove old stale profiles if required.
            parallel.internal.settings.removeStaleProfiles(obj.ProfilesCollection, ...
                obj.SchedulerComponentsCollection, obj.ProjectComponentsCollection);
            
            obj.Pool = parallel.internal.settings.PoolSettings();
        end
    end
end
%#ok<*ATUNK> custom attributes

%------------------------------------------------------------
function [values, needsReplace] = iReplaceBadDefaultProfile(values, levels, profilesCollection)
% If the collapsed value is not a valid profile name or is equal to 
% parallel.settings.DefaultValue then set the user level to
% FactoryDefaultProfileName.  If the user value is not DefaultValue, then
% check that it is a valid profile name and set it to FactoryDefaultProfileName
% if it is invalid.
%
% Note that we technically shouldn't get a collapsed value of
% parallel.settings.DefaultValue because DefaultProfile is
% defined in the factory, but the user may have a corrupt installation
% or may have deleted the factory settings file, so check for
% DefaultValue anyway.
import parallel.internal.types.SettingsLevel
import parallel.Settings
needsReplace = false;

allProfileNames = profilesCollection.getAllNames();

isUserIdx = levels == SettingsLevel.User;
isCollapsedIdx = levels == SettingsLevel.Collapsed;
userDefaultProfile = values(isUserIdx);
collapsedDefaultProfile = values(isCollapsedIdx);
if ~isempty(userDefaultProfile)
    userDefaultProfile = userDefaultProfile{1};
    if ~parallel.settings.DefaultValue.isDefault(userDefaultProfile) && ...
            ~profilesCollection.nameExists(userDefaultProfile)
        parallel.internal.warningNoBackTrace( ...
            message('parallel:settings:UserDefaultProfileNoLongerExists', ...
                userDefaultProfile, Settings.FactoryDefaultProfileName, ...
                iFormatProfileNamesForMessages(allProfileNames)));
        needsReplace = true;
    end
end
if ~isempty(collapsedDefaultProfile)
    collapsedDefaultProfile = collapsedDefaultProfile{1};
    if parallel.settings.DefaultValue.isDefault(collapsedDefaultProfile)
        parallel.internal.warningNoBackTrace( ...
            message('parallel:settings:DefaultProfileIsDefaultValue', Settings.FactoryDefaultProfileName, ...
                iFormatProfileNamesForMessages(allProfileNames)));
        needsReplace = true;
    elseif ~profilesCollection.nameExists(collapsedDefaultProfile)
        if Settings.isFactoryProfile(collapsedDefaultProfile)
            parallel.internal.warningNoBackTrace( ...
                message('parallel:settings:FactoryProfileNotFound', collapsedDefaultProfile));
            needsReplace = false;
        else
            parallel.internal.warningNoBackTrace( ...
                message('parallel:settings:CollapsedDefaultProfileNoLongerExists', ...
                    collapsedDefaultProfile, Settings.FactoryDefaultProfileName, ...
                    iFormatProfileNamesForMessages(allProfileNames)));
            needsReplace = true;
        end
    end
end

if needsReplace
    fallbackProfileName = Settings.FactoryDefaultProfileName;
    % If the local cluster is not enabled, we're trying to avoid falling back on to it,
    % or any of the other cluster profiles that are not visible ('threads', 'processes')
    if ~matlab.internal.parallel.isLocalClusterEnabled()
        % We're getting the list of visible, non-local profiles. `ismember` and `isProfileVisible`
        % will return logical arrays, so we use the '&' operator rather than the '&&' which only
        % applies to logical scalars.
        nonLocalProfiles = allProfileNames(~ismember(allProfileNames, fallbackProfileName) & ...
            parallel.internal.settings.isProfileVisible(allProfileNames));
        if ~isempty(nonLocalProfiles)
            fallbackProfileName = nonLocalProfiles{1};
        end
    end
    % And update the collapsed and user level values of the values.
    values{isUserIdx | isCollapsedIdx} = fallbackProfileName;
end
end

%------------------------------------------------------------
function collection = iBuildSchedulerComponentCollection(settingsNode, varargin)
import parallel.internal.settings.TypedNodesCollection
import parallel.internal.settings.SchedulerComponentType
import parallel.internal.types.SchedulerType

% The TypedNodesCollection will only contain those types that
% are actually present under the settingsNode.
[~, schedTypeNames] = enumeration('parallel.internal.types.SchedulerType');
schedNamesInNode = intersect(schedTypeNames, properties(settingsNode));
schedTypes = SchedulerType.fromName(schedNamesInNode);

if isempty(schedTypes)
   collection = TypedNodesCollection.empty();
   return;
end

buildFcns = SchedulerComponentType.getBuildFunction(schedTypes);
emptyFcns = SchedulerComponentType.getEmptyFunction(schedTypes);

if numel(schedTypes) == 1
    buildFcns = {buildFcns};
    emptyFcns = {emptyFcns};
end

for ii = 1:numel(buildFcns)
    currFcn = buildFcns{ii};
    buildFcns{ii} = @(x, y, z) currFcn(x, y, z, varargin{:});
end

localEmptyFcn = SchedulerComponentType.getEmptyFunction(SchedulerType.Local);
% Use Local as the default scheduler component to return for the Scheduler
% components
collection = TypedNodesCollection({schedTypes.Name}, ...
    settingsNode, ...
    buildFcns, emptyFcns, ...
    localEmptyFcn);
end

%------------------------------------------------------------
function collection = iBuildProjectComponentCollection(settingsNode)
import parallel.internal.settings.NamedNodesCollection
import parallel.settings.ProjectComponent
collection = NamedNodesCollection('ProjectComponent', ...
    settingsNode, ...
    @ProjectComponent.hBuildFromSettings, @ProjectComponent.empty);
end

%------------------------------------------------------------
function collection = iBuildProfilesCollection(settingsNode, varargin)
import parallel.internal.settings.NamedNodesCollection
import parallel.settings.Profile

collection = NamedNodesCollection('Profile', ...
    settingsNode, @(x, y, z) Profile.hBuildFromSettings(x, y, z, varargin{:}), ...
    @Profile.empty);
end

%------------------------------------------------------------
function [profileCollection, schedCompsCollection, projCompCollection] = ...
    iLoadSettings(filename)
    
import parallel.internal.apishared.FilenameUtils
import parallel.internal.settings.NamedNodesCollection
import parallel.internal.settings.ParallelSettingsTree
import parallel.internal.settings.TypedNodesCollection

filename = FilenameUtils.getAbsolutePath(filename);
if exist(filename, 'file') ~= 2
    error(message('parallel:settings:ImportProfileInvalidFilename', filename));
end

profileCollection = NamedNodesCollection.empty();
schedCompsCollection = TypedNodesCollection.empty();
projCompCollection = TypedNodesCollection.empty();

try
    settingsNode = matlab.internal.importSettings(filename);
catch err
    ex = MException(message('parallel:settings:ImportProfileFailedToImportSettings', filename));
    ex = ex.addCause(err);
    throw(ex)
end

% Try loading it as an XML MJSComputeCloud profile. This is to support
% cloud clusters' profiles downloaded from CloudCenter.
if isempty(settingsNode)
    settingsNode = importXMLCloudClusterProfile(filename);
end

if isempty(settingsNode)
    % Can be empty if the file is empty
    ex = MException(message('parallel:settings:ImportProfileFailedToImportSettings', filename));
    throw(ex)
end

% Make necessary adjustments to the settings tree.
settingsNode = ParallelSettingsTree.upgradeSettingsIfRequired(settingsNode);

if settingsNode.hasGroup(ParallelSettingsTree.SchedulerComponentsNodeName)
    schedCompsCollection = iBuildSchedulerComponentCollection(...
        settingsNode.(ParallelSettingsTree.SchedulerComponentsNodeName));
end

if settingsNode.hasGroup(ParallelSettingsTree.ProjectComponentsNodeName)
    projCompCollection = iBuildProjectComponentCollection(...
        settingsNode.(ParallelSettingsTree.ProjectComponentsNodeName));
end

if settingsNode.hasGroup(ParallelSettingsTree.ProfilesNodeName)
    profileCollection = iBuildProfilesCollection(...
        settingsNode.(ParallelSettingsTree.ProfilesNodeName));
end

% Remove old Stale profiles if required.
parallel.internal.settings.removeStaleProfiles(profileCollection, ...
    schedCompsCollection, projCompCollection);

% Validate profiles and their components
if ~isempty(profileCollection)
    profileCollection.getAll().verify();
end
if ~isempty(schedCompsCollection)
    schedCompsCollection.getAll().verify();
end
if ~isempty(projCompCollection)
    projCompCollection.getAll().verify();
end
end

%------------------------------------------------------------
function [namesAdded, origNames, haveNamesChanged] = iAddObjectsFromCollection(...
        source, destination, nameSuffix, level)
% Add objects from one collection to another.  Returns
% namesAdded - the names of the things that were added to the destination
% origNames  - the original names
% haveNamesChanged - logical array indicating if the names have changed
validateattributes(source, {'parallel.internal.settings.AbstractNodesCollection'}, {});
validateattributes(destination, {'parallel.internal.settings.AbstractNodesCollection'}, {});
validateattributes(level, {'parallel.internal.types.SettingsLevel'}, {'scalar'});

namesAdded = {};
origNames = {};
haveNamesChanged  = [];
if isempty(source)
    return;
end

[namesAdded, origNames, haveNamesChanged] = ...
    destination.addFromCollection(source, nameSuffix, level);
end

%------------------------------------------------------------
function iChangeComponentNamesInProfile(profileCollection, ...
        origScNames, newScNames, origPcNames, newPcNames)
% Change the names of the components in the profile collection from
% the original names to the specified new names.
validateattributes(profileCollection, {'parallel.internal.settings.NamedNodesCollection'}, {});
validateattributes(origScNames, {'cell'}, {});
validateattributes(newScNames,  {'cell'}, {'numel', numel(origScNames)});
validateattributes(origPcNames, {'cell'}, {});
validateattributes(newPcNames,  {'cell'}, {'numel', numel(origPcNames)});

if isempty(profileCollection) || (isempty(origScNames) && isempty(origPcNames))
    return;
end

profiles = profileCollection.getAll();
for ii = 1:numel(profiles)
    currProfile = profiles(ii);
    if ~isempty(origScNames)
        newScNameIdx = strcmp(currProfile.SchedulerComponent, origScNames);
        if any(newScNameIdx)
            currProfile.SchedulerComponent = newScNames{newScNameIdx};
        end
    end
    if ~isempty(origPcNames)
        newPcNameIdx = strcmp(currProfile.ProjectComponent, origPcNames);
        if any(newPcNameIdx)
            currProfile.ProjectComponent = newPcNames{newPcNameIdx};
        end
    end
end
end

%------------------------------------------------------------
function iDeleteNamesFromCollection(collection, names)
validateattributes(collection, {'parallel.internal.settings.NamedNodesCollection', ...
    'parallel.internal.settings.TypedNodesCollection'}, {});
try
    collection.deleteNodes(names);
catch err %#ok<NASGU>
    parallel.internal.warningNoBackTrace( ...
        message('parallel:settings:ImportProfileFailedToRemove', collection.Type));
end
end

%------------------------------------------------------------
function [profCollection, scCollection, pcCollection] = iBuildCollectionsFromSettings()
import parallel.internal.settings.ParallelSettingsTree
import parallel.internal.settings.SettingsNodeFacade

pcCollection   = iBuildProjectComponentCollection( ...
    ParallelSettingsTree.ProjectComponentsNode);

% The "real" Settings tree has a secret node that contains all the names
% of profiles whose scheduler components and aliases cannot be modified.  The 
% presence of a profile or component name as a key under this node indicates that its 
% scheduler component and alias is read only.  The value and type of the 
% key is inconsequential.
readOnlySCAliases = SettingsNodeFacade.getChildNamesFromNode(...
    ParallelSettingsTree.SchedulerComponentReadOnlyAliasNode);
scCollection   = iBuildSchedulerComponentCollection( ...
    ParallelSettingsTree.SchedulerComponentsNode, readOnlySCAliases);
readOnlyScheds = SettingsNodeFacade.getChildNamesFromNode(...
    ParallelSettingsTree.ProfilesReadOnlySchedNode);
readOnlyAliases = SettingsNodeFacade.getChildNamesFromNode(...
    ParallelSettingsTree.ProfilesReadOnlyAliasNode);
profCollection = iBuildProfilesCollection( ...
    ParallelSettingsTree.ProfilesNode, readOnlyAliases, readOnlyScheds);
end

%------------------------------------------------------------
function [profCollection, scCollection, pcCollection] = iBuildDeployedCollections()
import parallel.internal.settings.DeployedNodesCollection
import parallel.internal.settings.ParallelSettingsTree

% Build the collections for the deployed part of the settings tree
% and ensure that all the collections are empty to begin with.
depPcColl   = iBuildProjectComponentCollection(...
    ParallelSettingsTree.getDeployedProjectComponentsNode());

readOnlySCAliases = {''};
readOnlySCAliasNode = ParallelSettingsTree.getDeployedSchedulerComponentsReadOnlyAliasNode();
if ~isempty(readOnlySCAliasNode)
    readOnlySCAliases = SettingsNodeFacade.getChildNamesFromNode(readOnlySCAliasNode);
end
depScColl = iBuildSchedulerComponentCollection(...
    ParallelSettingsTree.getDeployedSchedulerComponentsNode(), readOnlySCAliases);

readOnlyScheds = {''};
readOnlySchedNode = ParallelSettingsTree.getDeployedProfilesReadOnlySchedNode();
if ~isempty(readOnlySchedNode)
    readOnlyScheds = SettingsNodeFacade.getChildNamesFromNode(readOnlySchedNode);
end
readOnlyAliases = {''};
readOnlyAliasNode = ParallelSettingsTree.getDeployedProfilesReadOnlyAliasNode();
if ~isempty(readOnlyAliasNode)
    readOnlyAliases = SettingsNodeFacade.getChildNamesFromNode(readOnlyAliasNode);
end
depProfColl = iBuildProfilesCollection(...
    ParallelSettingsTree.getDeployedProfilesNode(), readOnlyAliases, readOnlyScheds);

depScColl.deleteAllNodes();
depPcColl.deleteAllNodes();
depProfColl.deleteAllNodes();

[profCollection, scCollection, pcCollection] = iBuildCollectionsFromSettings();

scCollection   = DeployedNodesCollection(scCollection,   depScColl);
pcCollection   = DeployedNodesCollection(pcCollection,   depPcColl);
profCollection = DeployedNodesCollection(profCollection, depProfColl);
end

%------------------------------------------------------------
function filename = iGetDeployedProfileFilename()
% Get the filename specified in the MCR User Data.  The Profiles version
% of the string always wins over the configurations
filename = getmcruserdata(parallel.Settings.ProfilesMcrUserDataKey);
if isempty(filename)
    filename = getmcruserdata(parallel.Settings.ConfigsMcrUserDataKey);
end
end

%------------------------------------------------------------
function iUpgradeConfigurationsIfRequired()
[~, undoc] = pctconfig();
if undoc.useprofiles
    % NB upgradeConfigurationsToProfiles already issues appropriate warnings, 
    % so if something goes wrong here it was utterly unexpected.
    try
        parallel.internal.settings.upgradeConfigurationsToProfiles();
    catch err
        parallel.internal.warningNoBackTrace( ...
            message('parallel:settings:UpgradeConfigurationsUnexpectedError', err.getReport()));
    end
end
end

%------------------------------------------------------------
function newName = iGetUnusedName(proposedName, usedNames)
newName = matlab.lang.makeUniqueStrings(proposedName, usedNames, namelengthmax);
end

%------------------------------------------------------------
function formattedNames = iFormatProfileNamesForMessages(names)
% Given a list of profile names, format them into a string that puts 
% quotes around the names and spaces between each one.
formattedNames = strtrim(sprintf('''%s'' ', names{:}));
end

function factoryDefaultProfileName = iGetFactoryDefaultProfileName()
    factoryDefaultProfileName = parallel.Settings.LocalProfileName;
end