% SettingsNodeFacade - Facade around Settings to allow set/get etc of multiple keys 
% and operations on entire nodes

% NB Most calls to validateattributes are commented out in the interests of speed
% (particularly for auto-creation of pools with parallel language).  In the 
% parallel.Settings workflow, all the necessary checks are done further up the chain,
% mostly in CustomSettingsGetSet.
 
% Copyright 2011-2020 The MathWorks, Inc.

classdef (Hidden, Sealed) SettingsNodeFacade < handle
    properties (GetAccess = public, SetAccess = immutable)
        % The name of the current node
        NodeName
    end
    
    properties (Dependent)
        % Is the current node valid?
        IsValid
    end
    
    properties (Transient, GetAccess = private, SetAccess = immutable)
        % Settings Handle for the current node
        SettingsNode
        % Settings Handle for the current node's parent
        ParentNode
        % Listener for the ObjectBeingDestroyed event on the SettingsNode
        SettingsNodeDestroyedListener
    end

    events 
        % Event to signal that the underlying node was destroyed manually
        % i.e. the node was not destroyed using the delete() method of this
        % class, but was removed from the Settings tree in some other way.
        % This is triggered when the SettingsNode is destroyed.
        NodeHasBeenDestroyed
    end
    
    methods % Property accessors
        function value = get.IsValid(obj)
        % get.IsValid - the node is valid if the root node is a valid
        % handle and the parent node has a child node with the current
        % node name
            value = isvalid(obj.SettingsNode) && isvalid(obj.ParentNode) && ...              
                obj.ParentNode.hasGroup(obj.NodeName);        
        end
    end
    
    methods (Static)
        function [keys, nodes] = getChildNamesFromNode(node)
            validateattributes(node, {'matlab.settings.SettingsGroup'}, {'scalar'});
            children    = properties(node);
            childKeyIdx = cellfun(@(x) node.hasSetting(x), children);
            keys        = children(childKeyIdx);
            nodes       = children(~childKeyIdx);
        end
    end
    
    methods (Sealed) % destructor
        function delete(obj, varargin)
        % delete(obj) - delete the current node.  This removes the node from the 
        % Settings tree.
            % NB delete must define more than 1 arg to distinguish it from 
            % "normal" handle delete
            narginchk(1, 1);
            % remove the settings node - ensure we remove the ObjectBeingDestroyed
            % listener first to stop the NodeHasBeenDestroyed event from firing.  
            obj.SettingsNodeDestroyedListener.delete();
            if obj.IsValid 
                obj.ParentNode.removeGroup(obj.NodeName);
            end
            delete@handle(obj);
        end
        
        function names = getSiblingNodeNames(obj)
        % Gets the name of all sibling nodes of the current node
            [~, names] = parallel.internal.settings.SettingsNodeFacade.getChildNamesFromNode(obj.ParentNode);
        end
    end

    methods
        function obj = SettingsNodeFacade(node, nodeName, parentNode)
            % validateattributes(node, {'matlab.settings.SettingsGroup'}, {'scalar'});
            % validateattributes(nodeName, {'char'}, {'row'});
            % validateattributes(parentNode, {'matlab.settings.SettingsGroup'}, {'scalar'});
            if ~isvalid(node) || ~isvalid(parentNode)
                error(message('parallel:settings:NodeAndParentNotValid'))
            end
            obj.SettingsNode = node;
            obj.NodeName = nodeName;
            obj.ParentNode = parentNode;
            % When the settings node is destroyed, fire the NodeHasBeenDestroyed event
            obj.SettingsNodeDestroyedListener = event.listener(obj.SettingsNode, ...
                'ObjectBeingDestroyed', @obj.handleSettingsNodeDestroyed);
        end
        
        function tf = isequal(obj1, obj2)
        % isequal(obj1, obj2) - node facades are equal if the underlying settings nodes 
        % themselves are equal
            sizesMatch = isequal(size(obj1), size(obj2));
            classAndSizeMatch = sizesMatch && ...
                isequal(class(obj1), class(obj2)) && ...
                isa(obj1, 'parallel.internal.settings.SettingsNodeFacade');
                
            if classAndSizeMatch
                tf = true;
                for ii = 1:numel(obj1)
                    currObj1 = obj1(ii);
                    currObj2 = obj2(ii);
                    currObj1.errorIfNodeNotValid();
                    currObj2.errorIfNodeNotValid();
                    tf = tf && strcmp(currObj1.NodeName, currObj2.NodeName) && ...
                        isequal(currObj1.SettingsNode, currObj2.SettingsNode);
                end
            else 
                tf = false;
            end
        end

        function addKeys(obj, names)
        % addKeys(obj, names) - add sub keys to the current node, but only if those
        % sub keys don't already exist.  
        % names must be a cell array of strings.
            validateattributes(names, {'cell'}, {});
            assert(iscellstr(names), ...
                'parallel:settings:KeyOrNodeNameString', 'Names must be a cell array of strings.'); %#ok<ISCLSTR> only support cellstr
            obj.errorIfNodeNotValid();

            iAddChild(obj.SettingsNode, names, 'key');
        end
        
        function removeKeys(obj, names)
            validateattributes(names, {'cell'}, {});
            obj.errorIfNodeNotValid();
            iRestoreNodeRemoveChild(obj.SettingsNode, names, 'key');
        end

        function eventListener = addChangeListenerToKeys(obj, names, listenerFcn)
        % eventListener = addChangeListenerToKeys(obj, names, listenerFcn) - Add
        % PostSet listeners to the specified keys of the node.  The
        % listeners are added using addlistener and are tied to the 
        % lifetime of the node.
        % Returns an event.listener.
            % validateattributes(names, {'cell'}, {});
            % validateattributes(listenerFcn, {'function_handle'}, {'scalar'})
            % assert(iscellstr(names), ...
                % 'parallel:settings:KeyOrNodeNameString', 'Names must be a cell array of strings.');
            obj.errorIfNodeNotValid();

            % NB we must use findprop in order to get the meta properties associated
            % with the names because Settings uses dynamic properties and the meta class
            % for the Settings handle will not have the dynamic props
            metaPropsToListen = cellfun(@(x) findprop(obj.SettingsNode, x), names, 'UniformOutput', false);
            eventListener = event.proplistener(obj.SettingsNode, metaPropsToListen, 'PostSet', listenerFcn);
        end
        
        function tf = existKeys(obj, names)
        % existKeys(names) - Determine if the supplied names exist 
        % as keys under the current node
            % validateattributes(names, {'cell'}, {});
            % assert(iscellstr(names), ...
            %    'parallel:settings:KeyOrNodeNameString', 'Names must be a cell array of strings.');
            obj.errorIfNodeNotValid();
            tf = false(size(names));
            
            for ii = 1:numel(names)
                tf(ii) = obj.SettingsNode.hasSetting(names{ii});
            end
        end
        
        function setKey(obj, name, value, level)
        % setKey(obj, name, value, level) - set key to the specified value
        % at the specified level.
        % name, value must be non-empty strings.
        % level must be a scalar SettingsLevel
            validateattributes(name, {'char'}, {'row'});
            validateattributes(level, {'parallel.internal.types.SettingsLevel'}, {'scalar'});
            obj.setKeys({name}, {value}, level);
        end

        function setKeys(obj, names, values, levels)
        % setKeys(obj, names, values, levels) - set keys to the specified values 
        % at the specified levels.
        % names, values must be cells with the same numel.
        % levels must be an array of SettingsLevel with the same numel
        % as names and values.
            import parallel.internal.types.SettingsLevel
            % iValidateNameLevel(names, levels);
            SettingsLevel.errorIfLevelsNotSettable(levels);
            validateattributes(values, {'cell'}, {'numel', numel(names)});
            obj.errorIfNodeNotValid();
            
            oldKeyValues = cell(size(names));
            haveChangedKey = false(size(names));
            try
                for ii = 1:numel(names)
                    currName = names{ii};
                    currValue = values{ii};
                    currLevel = levels(ii);
                    obj.errorIfKeyNotExist(currName);
                    
                    oldKeyValues{ii} = iGetSingleKeyFromLevel(obj.SettingsNode, currName, currLevel);
                    iSetSingleKeyAtLevel(obj.SettingsNode, currName, currValue, currLevel);
                    haveChangedKey(ii) = true;
                end
            catch err
                % bail on the first key that we can't set, but restore the ones that we have already
                % set
                iRestoreKeyValuesOnNode(obj.SettingsNode, names(haveChangedKey), ...
                    oldKeyValues(haveChangedKey), levels(haveChangedKey));

                ex = MException(message('parallel:settings:SetKeyOnLevelFailed', currName, currLevel.Name, obj.NodeName));
                ex = ex.addCause(err);
                throw(ex);
            end
        end

        function value = getKey(obj, name, level)
        % getKey(obj, name, level) - get key from the specified level.
        % name must be a non-empty string.
        % level must be a scalar SettingsLevel
            validateattributes(name, {'char'}, {'row'});
            validateattributes(level, {'parallel.internal.types.SettingsLevel'}, {'scalar'});
            value = obj.getKeys({name}, level);
            value = value{1};
        end
 
        function values = getKeys(obj, names, levels)
        % getKeys(obj, names, levels) - get keys from the specified levels.
        % names must be a cell array of strings.
        % levels must be an array of SettingsLevel with the same numel
        % as names.
        % Always returns a cell.
            import parallel.internal.types.SettingsLevel
            % iValidateNameLevel(names, levels);
            SettingsLevel.errorIfLevelsNotGettable(levels);
            obj.errorIfNodeNotValid();
            
            % NB get usually returns a row
            values = cell(1, numel(names));
            try
                for ii = 1:numel(names)
                    currName = names{ii};
                    currLevel = levels(ii);
                    obj.errorIfKeyNotExist(currName);
                    if currLevel == SettingsLevel.Collapsed
                        values{ii} = iGetSingleCollapsedKey(obj.SettingsNode, currName);
                    else
                        values{ii} = iGetSingleKeyFromLevel(obj.SettingsNode, currName, currLevel);
                    end
                end
            catch err
                ex = MException(message('parallel:settings:GetKeyOnLevelFailed', currName, currLevel.Name, obj.NodeName));
                ex = ex.addCause(err);
                throw(ex);
            end
        end
        
        function unsetKey(obj, name, level)
        % unsetKey(obj, name, level) - unset key at the specified level.
        % name must be a non-empty string.
        % level must be a scalar SettingsLevel
            validateattributes(name, {'char'}, {'row'});
            validateattributes(level, {'parallel.internal.types.SettingsLevel'}, {'scalar'});
            obj.unsetKeys({name}, level);
        end
        
        function unsetKeys(obj, names, levels)
        % unsetKeys(obj, names, levels) - unset keys at the specified levels
        % names  must be a cell array of strings.
        % levels must be an array of SettingsLevel with the same numel
        % as names.
            import parallel.internal.types.SettingsLevel
            % iValidateNameLevel(names, levels);
            SettingsLevel.errorIfLevelsNotSettable(levels);
            obj.errorIfNodeNotValid();
            haveUnsetKey = false(size(names));
            oldKeyValues = cell(size(names));
            try
                for ii = 1:numel(names)
                    currName = names{ii};
                    currLevel = levels(ii);
                    obj.errorIfKeyNotExist(currName);
                        
                    oldKeyValues{ii} = iGetSingleKeyFromLevel(obj.SettingsNode, currName, currLevel);
                    iUnsetKeyAtLevel(obj.SettingsNode, currName, currLevel);
                    haveUnsetKey(ii) = true;
                end
            catch err
                % bail on the first key that we can't unset, but restore the ones that we have already
                % set
                iRestoreKeyValuesOnNode(obj.SettingsNode, names(haveUnsetKey), ...
                    oldKeyValues(haveUnsetKey), levels(haveUnsetKey));
                
                ex = MException(message('parallel:settings:UnsetKeyOnLevelFailed', currName, currLevel.Name, obj.NodeName));
                ex = ex.addCause(err);
                throw(ex);
            end
        end
        
        function tf = isKeySet(obj, name, level)
        % isKeySet(obj, name, level) - determine if the key is set at 
        % the specified level.
        % name must be a non-empty string.
        % level must be a scalar SettingsLevel
            validateattributes(name, {'char'}, {'row'});
            validateattributes(level, {'parallel.internal.types.SettingsLevel'}, {'scalar'});
            tf = obj.areKeysSet({name}, level);
        end
        
        function tf = areKeysSet(obj, names, levels)
        % areKeysSet(obj, names, levels) - determine if the keys are set at 
        % the specified levels.
        % names must be a cell array of strings.
        % levels must be an array of SettingsLevel with the same numel
        % as names.
        % Always returns a cell
            import parallel.internal.types.SettingsLevel
            % iValidateNameLevel(names, levels);
            SettingsLevel.errorIfCannotIsSetLevels(levels);
            obj.errorIfNodeNotValid();
            tf = false(1, numel(names));
            try
                for ii = 1:numel(names)
                    currName = names{ii};
                    currLevel = levels(ii);
                    obj.errorIfKeyNotExist(currName);
                    tf(ii) = currLevel.hasValue(obj.SettingsNode.(currName));
                end
            catch err
                ex = MException(message('parallel:settings:AreKeysSetLevelFailed', currName, currLevel.Name, obj.NodeName));
                ex = ex.addCause(err);
                throw(ex);
            end
        end
        
        function exportNode(objOrObjs, filename)
        % exportNode(objOrObjs, filename) - export the current node and all children
        % to the specified file.  Exports the collapsed values.
        % If an array of SettingsNodeFacade are supplied, then all the 
        % nodes are exported to the same file.  If the filename does not have the 
        % .mlsettings extension, then .mlsettings is assumed.
            import parallel.internal.apishared.FilenameUtils
            import parallel.internal.settings.ParallelSettingsTree
   
            validateattributes(filename, {'char'}, {'row'});
            
            % Check all the objs are valid.
            arrayfun(@(x) x.errorIfNodeNotValid(), objOrObjs);
            
            % Make sure the directory exists before calling exportSettings
            % because exportSettings will error otherwise.
            [path, fname, ext] = fileparts(filename);
            if isempty(fname)
                error(message('parallel:settings:ExportNoFilename', filename));
            end
            if ~isempty(path) && exist(path, 'dir') ~= 7
                [~, msg, messageID] = mkdir(path);
                if exist(path, 'dir') ~= 7
                    ex = MException(message('parallel:settings:SettingsNodeExportCannotMakeFolder', path));
                    ex = ex.addCause(MException(messageID, '%s', msg));
                    throw(ex);
                end
            end
            allowedExt = '.mlsettings';
            % Assume .mlsettings extension if none is supplied or if .mlsettings
            % wasn't the one supplied.
            if isempty(ext) || ~strcmpi(ext, allowedExt)
                filename = sprintf('%s%s', filename, allowedExt);
            end
            
            absFile = FilenameUtils.getAbsolutePath(filename);
            % Export version node to identify upgrade requirements on
            % imported profiles.
            versionNode = ParallelSettingsTree.VersionNode;
            nodesToExport = [versionNode objOrObjs.SettingsNode];
            matlab.internal.exportSettings(nodesToExport, absFile);
        end
        
        function copyCollapsedValuesToNode(obj, destinationNodeFacade)
        % copyCollapsedValuesToNode(obj, destinationNodeFacade) - copy the 
        % collapsed values to the user level of the destination
            import parallel.internal.types.SettingsLevel
            validateattributes(destinationNodeFacade, ...
                {'parallel.internal.settings.SettingsNodeFacade'}, {'scalar'});
            obj.errorIfNodeNotValid();
            try
                iCopyNode(obj.SettingsNode, SettingsLevel.Collapsed, ...
                    destinationNodeFacade.SettingsNode, SettingsLevel.User);
            catch err
                ex = MException(message('parallel:settings:CopyCollapsedNodeFailed', obj.NodeName, destinationNodeFacade.NodeName));
                ex = ex.addCause(err);
                throw(ex);
            end
        end
        
        function copyAllLevelsToNode(obj, destinationNodeFacade)
        % copyAllLevelsToNode(obj, destinationNodeFacade) - copy the 
        % workgroup and user levels to the destination
            import parallel.internal.types.SettingsLevel
            validateattributes(destinationNodeFacade, ...
                {'parallel.internal.settings.SettingsNodeFacade'}, {'scalar'});
            obj.errorIfNodeNotValid();
            
            if obj.hasAnyFactoryValues()
                error(message('parallel:settings:NodeFacadeCopyAllLevelsHasFactoryValues', obj.NodeName));
            end

            % Copy the user level first - we ought to be able to do this
            % TODO:OneDay session support? Ignore the session level for now.
            try
                iCopyNode(obj.SettingsNode, SettingsLevel.User, ...
                    destinationNodeFacade.SettingsNode, SettingsLevel.User);
            catch err
                ex = MException(message('parallel:settings:CopyUserNodeFailed', obj.NodeName, destinationNodeFacade.NodeName));
                ex = ex.addCause(err);
                throw(ex);
            end

            % NB workgroup level is not supported in 12b for Settings.  See g796418
            % Now try to do the workgroup level - this may fail if we don't have access
            % to the workgroup level, so warn rather than error if it fails
            % try
                % iCopyNode(obj.SettingsNode, SettingsLevel.Workgroup, ...
                    % destinationNodeFacade.SettingsNode, SettingsLevel.Workgroup);
            % catch dummyErr %#ok<NASGU>
                % warning(message('parallel:settings:CopyWorkgroupNodeFailed', obj.NodeName, destinationNodeFacade.NodeName));
            % end
        end

        function saveUserToLevel(obj, level)
        % saveUserToLevel(obj, level) - save all the user values to the specified level
            import parallel.internal.types.SettingsLevel
            validateattributes(level, {'parallel.internal.types.SettingsLevel'}, {'scalar'});
            SettingsLevel.errorIfLevelsNotSettable(level);
            obj.errorIfNodeNotValid();

            try
                iSaveUserNodeToLevel(obj.SettingsNode, level);
            catch err
                ex = MException(message('parallel:settings:SaveUserToLevelFailed', obj.NodeName, level.Name));
                ex = ex.addCause(err);
                throw(ex);
            end
        end
   
        function unsetNode(obj, level, excludeKeys)
        % unsetNode(obj, level, excludeKeys) - unset all keys in this node recursively, optionally 
        % excluding the supplied exclusions
            import parallel.internal.types.SettingsLevel
            validateattributes(level, {'parallel.internal.types.SettingsLevel'}, {'scalar'});
            obj.errorIfNodeNotValid();
            SettingsLevel.errorIfLevelsNotSettable(level);
            
            if nargin < 3
                excludeKeys = {};
            end
            
            try
                iUnsetAllBelowNode(obj.SettingsNode, level, excludeKeys);
            catch err
                ex = MException(message('parallel:settings:UnsetNodeFailed', obj.NodeName, level.Name));
                ex = ex.addCause(err);
                throw(ex);
            end
        end
        
        function siblingNodeFacade = createSiblingNode(obj, name)
        % createSiblingNode(obj, name) - create a single sibling node to this node.
        % Returns the SettingsNodeFacade for the sibling.
            import parallel.internal.settings.SettingsNodeFacade
            validateattributes(name, {'char'}, {'row'});
            if ~isvalid(obj.ParentNode)
                error(message('parallel:settings:ParentNodeInvalid'));
            end
            try
                obj.ParentNode.addGroup(name);                
                siblingNode = obj.ParentNode.(name);
                siblingNodeFacade = SettingsNodeFacade(siblingNode, name, obj.ParentNode);
            catch err
                ex = MException(message('parallel:settings:CreateSiblingNodeFailed', name, obj.NodeName));
                ex = ex.addCause(err);
                throw(ex);
            end
        end
        
        function tf = hasAnyFactoryValues(obj)
            obj.errorIfNodeNotValid();
            tf = iHasAnyFactoryValues(obj.SettingsNode);
        end
    end
    
    methods (Access = private)
        function errorIfKeyNotExist(obj, name)
            if ~obj.SettingsNode.hasSetting(name)
                throwAsCaller(MException(message('parallel:settings:KeyNotExist', name, obj.NodeName)));
            end
        end
        
        function errorIfNodeNotValid(obj)
            if ~obj.IsValid
                error(message('parallel:settings:NodeNotValid', obj.NodeName));
            end
        end
        
        function handleSettingsNodeDestroyed(obj, ~, ~)
            obj.notify('NodeHasBeenDestroyed');
        end
    end
end


%-------------------------------------------------------
function iValidateNameLevel(names, levels) %#ok<DEFNU> - used in commented out validation code
% Check if the supplied names and levels are valid
validateattributes(names, {'cell'}, {});
assert(iscellstr(names), ...
   'parallel:settings:KeyOrNodeNameString', 'Names must be a cell array of strings.'); %#ok<ISCLSTR> only support cellstr
validateattributes(levels, {'parallel.internal.types.SettingsLevel'}, {'numel', numel(names)});
end

%-------------------------------------------------------
function destinationSettingsNode = iCopyNode(sourceSettingsNode, sourceLevel, destinationSettingsNode, destinationLevel)
% Copy the source node at the specified level to the specified destination node and level.
import parallel.internal.settings.SettingsNodeFacade
import parallel.internal.types.SettingsLevel
validateattributes(sourceSettingsNode, {'matlab.settings.SettingsGroup'}, {'scalar'});
validateattributes(destinationSettingsNode, {'matlab.settings.SettingsGroup'}, {'scalar'});
validateattributes(sourceLevel, {'parallel.internal.types.SettingsLevel'}, {'scalar'})
validateattributes(destinationLevel, {'parallel.internal.types.SettingsLevel'}, {'scalar'})
SettingsLevel.errorIfLevelsNotSettable(destinationLevel);

% Get the keys and nodes
[keys, nodes] = SettingsNodeFacade.getChildNamesFromNode(sourceSettingsNode);

% Copy the keys first
haveAddedKey = false(numel(keys), 1);
oldKeyValues = cell(numel(keys), 1);
haveChangedKey = false(numel(keys), 1);
try
    for ii = 1:numel(keys)
        [haveAddedKey(ii), oldKeyValues{ii}] = iCopyKey(sourceSettingsNode, sourceLevel, destinationSettingsNode, destinationLevel, keys{ii});
        haveChangedKey(ii) = true;
    end
catch err
    % restore any changes that we've made to the destination node
    iRestoreNodeRemoveChild(destinationSettingsNode, keys(haveAddedKey), 'key');
    modifiedKeyValue = haveChangedKey & ~haveAddedKey;
    iRestoreKeyValuesOnNode(destinationSettingsNode, keys(modifiedKeyValue), ...
        oldKeyValues(modifiedKeyValue), destinationLevel);
    rethrow(err);
end

% Copy the nodes recursively
haveAddedNode = false(numel(nodes), 1);
try
    for ii = 1:numel(nodes)
        currNode = nodes{ii};
        if ~destinationSettingsNode.hasGroup(currNode)
            iAddHiddenNode(destinationSettingsNode, currNode);
            haveAddedNode(ii) = true;
        end
        iCopyNode(sourceSettingsNode.(currNode), sourceLevel, destinationSettingsNode.(currNode), destinationLevel);
    end
catch err
    % restore any changes that we've made to the destination node
    iRestoreNodeRemoveChild(destinationSettingsNode, nodes(haveAddedNode), 'node');
    rethrow(err);
end
end

%-------------------------------------------------------
function [addedKey, oldValue] = iCopyKey(sourceSettingsNode, sourceLevel, destinationSettingsNode, destinationLevel, keyName)
% Copy a key's value from the source node and level and writes it to the destination node and level
import parallel.internal.types.SettingsLevel
validateattributes(sourceSettingsNode, {'matlab.settings.SettingsGroup'}, {'scalar'});
validateattributes(destinationSettingsNode, {'matlab.settings.SettingsGroup'}, {'scalar'});
validateattributes(sourceLevel, {'parallel.internal.types.SettingsLevel'}, {'scalar'})
validateattributes(destinationLevel, {'parallel.internal.types.SettingsLevel'}, {'scalar'})
validateattributes(keyName, {'char'}, {});
SettingsLevel.errorIfLevelsNotSettable(destinationLevel);

% Get the current value or add the key to the destination
if ~destinationSettingsNode.hasSetting(keyName)
    iAddHiddenKey(destinationSettingsNode, keyName);
    addedKey = true;
    oldValue = [];
else
    addedKey = false;
    oldValue = iGetSingleKeyFromLevel(destinationSettingsNode, keyName, destinationLevel);
end

% Get the value from the source
if sourceLevel == SettingsLevel.Collapsed
    sourceValue = iGetSingleCollapsedKey(sourceSettingsNode, keyName);
else
    sourceValue = iGetSingleKeyFromLevel(sourceSettingsNode, keyName, sourceLevel);
end

% Set the value on the destination
try
    iSetSingleKeyAtLevel(destinationSettingsNode, keyName, sourceValue, destinationLevel);
catch err
    % Couldn't set the value on the destination, try remove the key if we 
    % added it
    if addedKey
        iRestoreNodeRemoveChild(destinationSettingsNode, {keyName}, 'key');
    end
    rethrow(err);
end
end

%-------------------------------------------------------
function settingsNode = iSaveUserNodeToLevel(settingsNode, level)
% Save user level values to the specified level recursively
import parallel.internal.settings.SettingsNodeFacade
import parallel.internal.types.SettingsLevel
validateattributes(settingsNode, {'matlab.settings.SettingsGroup'}, {'scalar'});
validateattributes(level, {'parallel.internal.types.SettingsLevel'}, {'scalar'})

% Don't need to worry about adding keys/nodes here because
% we're just copying between different levels in the same node.
[keys, nodes] = SettingsNodeFacade.getChildNamesFromNode(settingsNode);
haveChangedKey = false(numel(keys), 1);
oldKeyValues = cell(numel(keys), 1);
try
    for ii = 1:numel(keys)
        key = keys{ii};
        oldKeyValues{ii} = iGetSingleKeyFromLevel(settingsNode, key, level);
        valueToSet = iGetSingleKeyFromLevel(settingsNode, key, SettingsLevel.User);
        iSetSingleKeyAtLevel(settingsNode, key, valueToSet, level);
        haveChangedKey(ii) = true;
    end
    
    for ii = 1:numel(nodes)
        iSaveUserNodeToLevel(settingsNode.(nodes{ii}), level);
    end
catch err
    % restore any keys that we have changed
    iRestoreKeyValuesOnNode(settingsNode, keys(haveChangedKey), ...
        oldKeyValues(haveChangedKey), level);
    rethrow(err)
end
end

%-------------------------------------------------------
function iUnsetAllBelowNode(settingsNode, level, excludeKeys)
import parallel.internal.settings.SettingsNodeFacade
% Unset all the keys that are below the supplied node
validateattributes(settingsNode, {'matlab.settings.SettingsGroup'}, {'scalar'});
validateattributes(level, {'parallel.internal.types.SettingsLevel'}, {'scalar'})

[keys, nodes] = SettingsNodeFacade.getChildNamesFromNode(settingsNode);
haveChangedKey = false(numel(keys), 1);
oldKeyValues = cell(numel(keys), 1);
try
    for ii = 1:numel(keys)
        key = keys{ii};
        if ~ismember(key, excludeKeys)
            oldKeyValues{ii} = iGetSingleKeyFromLevel(settingsNode, key, level);
            iUnsetKeyAtLevel(settingsNode, key, level);
            haveChangedKey(ii) = true;
        end
    end
    for ii = 1:numel(nodes)
        iUnsetAllBelowNode(settingsNode.(nodes{ii}), level, excludeKeys);
    end
catch err
    % restore any keys that we have changed
    iRestoreKeyValuesOnNode(settingsNode, keys(haveChangedKey), ...
        oldKeyValues(haveChangedKey), level);
    rethrow(err);
end
end

%-------------------------------------------------------
function iRestoreKeyValuesOnNode(settingsNode, keys, values, levelOrLevels)
% Restore key values on the specified node - this issues a warning if the
% restoration fails.
validateattributes(settingsNode, {'matlab.settings.SettingsGroup'}, {'scalar'});
validateattributes(keys, {'cell'}, {});
validateattributes(values, {'cell'}, {'numel', numel(keys)});
if numel(keys) > numel(levelOrLevels)
    validateattributes(levelOrLevels, {'parallel.internal.types.SettingsLevel'}, {'scalar'});
    levels = repmat(levelOrLevels, numel(keys), 1);
else
    validateattributes(levelOrLevels, {'parallel.internal.types.SettingsLevel'}, {'numel', numel(keys)});
    levels = levelOrLevels;
end
% Restore values on a node.  Issues a warning if any of the keys 
% cannot be restored.  Does not error.
failedToRestore = false(numel(keys), 1);
for ii = 1:numel(keys)
    try
        iSetSingleKeyAtLevel(settingsNode, keys{ii}, values{ii}, levels(ii));
    catch dummyErr %#ok<NASGU>
        failedToRestore(ii) = true;
    end
end

% Warn about stuff we couldn't restore
if any(failedToRestore)
    warning(message('parallel:settings:FailedRestoreKeyValuesOnNode'));
end
end

%-------------------------------------------------------
function iRestoreNodeRemoveChild(settingsNode, childNames, type)
% Restore a node to its original state by removing child keys or nodes
validateattributes(settingsNode, {'matlab.settings.SettingsGroup'}, {'scalar'});
validateattributes(childNames, {'cell'}, {});
validatestring(type, {'node', 'key'});

isNode = strcmp(type, 'node');
if isNode
    removeFcn = @(x, y) x.removeGroup(y);
else
    removeFcn = @(x, y) x.removeSetting(y);
end

failedToRemove = false(numel(childNames), 1);
for ii = 1:numel(childNames)
    try
        removeFcn(settingsNode, childNames{ii});
    catch dummyErr %#ok<NASGU>
        failedToRemove(ii) = true;
    end
end
% Warn about keys/nodes we couldn't delete
if any(failedToRemove)
    if isNode
        warning(message('parallel:settings:FailedRestoreNodeRemoveNodes'));
    else
        warning(message('parallel:settings:FailedRestoreNodeRemoveKeys'));
    end
end
end

%-------------------------------------------------------
function iSetSingleKeyAtLevel(settingsNode, name, value, level)
% Set a single key at a specific level
validateattributes(settingsNode, {'matlab.settings.SettingsGroup'}, {'scalar'});
validateattributes(name, {'char'}, {'row'});
validateattributes(level, {'parallel.internal.types.SettingsLevel'}, {'scalar'})
if parallel.settings.DefaultValue.isDefault(value)
    % Only unset if the key is currently set.  If the
    % key is not set, then it is already at the "Default value".
    iUnsetKeyAtLevel(settingsNode, name, level);
else
    level.setValue(settingsNode.(name), value);
end
end

%-------------------------------------------------------
function value = iGetSingleKeyFromLevel(settingsNode, name, level)
% Get a single key at a specific level
% validateattributes(settingsNode, {'matlab.settings.SettingsGroup'}, {'scalar'});
% validateattributes(name, {'char'}, {'row'});
% validateattributes(level, {'parallel.internal.types.SettingsLevel'}, {'scalar'})

if level.hasValue(settingsNode.(name))
    value = level.getValue(settingsNode.(name));
else
    value = parallel.settings.DefaultValue;
end
end

%-------------------------------------------------------
function iUnsetKeyAtLevel(settingsNode, name, level)
% Unset a key a specific level
validateattributes(settingsNode, {'matlab.settings.SettingsGroup'}, {'scalar'});
validateattributes(name, {'char'}, {'row'});
validateattributes(level, {'parallel.internal.types.SettingsLevel'}, {'scalar'})
% Only unset if the key is currently set.  If the
% key is not set, then it is already at the "Default value".
if level.hasValue(settingsNode.(name))
    level.clearValue(settingsNode.(name));
end
end

%-------------------------------------------------------
function value = iGetSingleCollapsedKey(settingsNode, name)
% Get a single key's collapsed value
% validateattributes(settingsNode, {'matlab.settings.SettingsGroup'}, {'scalar'});
% validateattributes(name, {'char'}, {'row'});
try
    % NB get will return the collapsed value, but
    % if the key is not set at any level, then
    % get will error.  Can't use isSet here because
    % isSet requires a level and we don't want to check
    % every level individually.
    value = settingsNode.(name).ActiveValue;
catch err %#ok<NASGU>
    value = parallel.settings.DefaultValue;
end
end
    
%-------------------------------------------------------
function iAddChild(settingsNode, names, type)
% Add child nodes or keys to the specified node if it does 
% not already exist.
% validateattributes(settingsNode, {'matlab.settings.SettingsGroup'}, {'scalar'});
% validateattributes(names, {'cell'}, {});
% validatestring(type, {'node', 'key'});

if strcmp(type, 'node')
    existFcn = @(x, y) x.hasGroup(y);
    addFcn = @(x, y) iAddHiddenNode(x, y);
else
    existFcn = @(x, y) x.hasSetting(y);
    addFcn = @(x, y) iAddHiddenKey(x, y);
end
    
haveAdded = false(size(names));
try
    for ii = 1:numel(names)
        currName = names{ii};
        if ~existFcn(settingsNode, currName)
            addFcn(settingsNode, currName);
            haveAdded(ii) = true;
        end
    end
catch err 
    % bail on the first child that we can't add, and remove the ones that were added
    iRestoreNodeRemoveChild(settingsNode, names(haveAdded), type);
    rethrow(err);
end
end

%-------------------------------------------------------
function iAddHiddenKey(settingsNode, keyName)
% Add a hidden key to the specified node. The key must
% not already exist.
validateattributes(settingsNode, {'matlab.settings.SettingsGroup'}, {'scalar'});
validateattributes(keyName, {'char'}, {'row'});
try
    % TODO:later - make the key hidden
    settingsNode.addSetting(keyName);
catch err
    ex = MException(message('parallel:settings:AddHiddenKeyFailed', keyName, settingsNode.SettingsName));
    ex = ex.addCause(err);
    throw(ex);
end
end

%-------------------------------------------------------
function iAddHiddenNode(settingsNode, nodeName)
% Add a hidden node to the specified node. The node must
% not already exist.
validateattributes(settingsNode, {'matlab.settings.SettingsGroup'}, {'scalar'});
validateattributes(nodeName, {'char'}, {'row'});
try
    % TODO:later - make the node hidden
    settingsNode.addGroup(nodeName);
catch err
    ex = MException(message('parallel:settings:AddHiddenNodeFailed', nodeName, settingsNode.SettingsName));
    ex = ex.addCause(err);
    throw(ex);
end
end

%-------------------------------------------------------
function tf = iHasAnyFactoryValues(settingsNode)
import parallel.internal.settings.SettingsNodeFacade
import parallel.internal.types.SettingsLevel
validateattributes(settingsNode, {'matlab.settings.SettingsGroup'}, {'scalar'});
    
[keys, nodes] = SettingsNodeFacade.getChildNamesFromNode(settingsNode);

inFactory = cellfun(@(x) SettingsLevel.Factory.hasValue(settingsNode.(x)), keys);

tf = any(inFactory);
tf = tf || any(cellfun(@(x) iHasAnyFactoryValues(settingsNode.(x)), nodes));
end
