% NamedNodesCollection - A collection of named nodes

% Copyright 2011-2020 The MathWorks, Inc.

classdef (Hidden, Sealed) NamedNodesCollection < parallel.internal.settings.AbstractNodesCollection
    properties (GetAccess = private, SetAccess = immutable)
        % The type of named node stored in this collection
        Type
        % The top level Settings node for this collection.
        % All nodes in the collection are children of this 
        % root node.
        TopLevelRawSettingsNode

        % Function handles for creating nodes
        EmptyNodeFcn
        NodeBuilderFcn
    end
    
    properties (Access = private)
        % Array of nodes
        CachedNodes
        % Cell array of listeners
        CachedNodeListeners
    end
    
    properties (Dependent, GetAccess = private)
        CachedNodeNames
        CachedSettingsNames
    end
    
    methods
        function names = get.CachedNodeNames(obj)
            names = {obj.CachedNodes.Name};
        end
        
        function names = get.CachedSettingsNames(obj)
            names = {obj.CachedNodes.SettingsName};
        end
    end
    
    methods
        function obj = NamedNodesCollection(type, topLevelSettingsNode, nodeBuilderFcn, emptyFcn)
            % validateattributes(type, {'char'}, {'row'});
            % validateattributes(topLevelSettingsNode, {'Settings'}, {'scalar'});
            % validateattributes(nodeBuilderFcn, {'function_handle'}, {'scalar'});
            % validateattributes(emptyFcn, {'function_handle'}, {'scalar'});
            obj.Type = type;
            obj.TopLevelRawSettingsNode = topLevelSettingsNode;
            obj.EmptyNodeFcn = emptyFcn;
            obj.NodeBuilderFcn = nodeBuilderFcn;
            obj.CachedNodes = emptyFcn();
            obj.CachedNodeListeners = {};
        end
        
        function delete(obj)
            % Ensure that we invalidate the handles of all the nodes
            % in the cache
            obj.CachedNodes.hInvalidate();
        end

        function nodes = getAll(obj)
        % getAll - Get the NamedNodes that are defined under the TopLevelRawSettingsNode
            obj.refreshCacheIfRequired();
            nodes = obj.CachedNodes;
            if ~isempty(nodes)
                % Return a sorted list in alphabetical order.
                [~, sortOrder] = sort(lower(obj.CachedNodeNames));
                nodes = nodes(sortOrder);
            end
        end

        function namedNode = createNew(obj, name, pvPairs)
        % createNew - Build a new NamedNode
            validateattributes(pvPairs, {'cell'}, {});
            
            [propNames, propVals, level] = iAmalgamateAndExtractLevel(pvPairs);
            % TODO:later make this node hidden
            settingsName = obj.getUnusedSettingsNameNoCache(name);
            obj.TopLevelRawSettingsNode.addGroup(settingsName);
            try
                childNode = obj.TopLevelRawSettingsNode.(settingsName);
                namedNode = obj.NodeBuilderFcn(settingsName, childNode, obj.TopLevelRawSettingsNode);
                % Set the name without going through the full checks as we
                % don't want to trigger any listeners until we add the node
                % to the cache
                namedNode.set(parallel.internal.settings.NamedNode.AliasKeyName, name);
            catch err
                % Remove the node if we couldn't create the object
                obj.TopLevelRawSettingsNode.removeGroup(settingsName);
                dctSchedulerMessage(6, 'Failed to create %s with name %s and settings name %s', obj.Type, name, settingsName);
                ex = MException(message('parallel:settings:NamedNodesBuildNewFailed', obj.Type, name));
                ex = ex.addCause(err);
                throwAsCaller(ex);
            end
            
            % Set the P-V pairs
            try
                cellfun(@(x, y) namedNode.set(x, y, level.Name), propNames, propVals);
            catch err
                % delete the profile to remove it from the settings tree
                % if we couldn't set the P-V pairs
                namedNode.delete();
                dctSchedulerMessage(6, 'Failed to create %s with name %s and settings name %s', obj.Type, name, settingsName);
                ex = MException(message('parallel:settings:NamedNodesBuildNewFailed', obj.Type, name));
                ex = ex.addCause(err);
                throwAsCaller(ex);
            end
            
            obj.addNodeToCache(namedNode);
        end
        
        function tf = nameExists(obj, nodeName)
        % tf = nameExists(obj, nodeName) - does the name exist?
        % Refreshes the cache if it is not up-to-date.
            validateattributes(nodeName, {'char'}, {'row'});
            obj.refreshCacheIfRequired();
            allNames = obj.CachedNodeNames;
            tf = any(strcmp(nodeName, allNames));
        end

        function nodeNames = getAllNames(obj)
        % [nodeNames, settingsNames] = getAllNames - get the names of all the NamedNodes, 
        % including the names of Settings node.
        % Refreshes the cache if it is not up-to-date.
            obj.refreshCacheIfRequired();
            nodes = obj.getAll();
            nodeNames = {nodes.Name};
        end
        
        function node = getFromName(obj, nodeName)
        % getFromName - get a specific node from its name.  
        % Returns an empty node if no node with that name can be found.
        % Does not refresh the cache, but instead loads only the specified 
        % name to facilitate faster retrieval of nodes when using 
        % >> p = parallel.Settings
        % >> p.findProfile('Name', 'foo');
            [nodeExistsInCache, node] = obj.getNodeFromCacheNoRefresh(nodeName);
            if nodeExistsInCache
                return;
            end

            % If the node isn't in the cache, then check to 
            % see if we can find the corresponding node as a child
            % of the current root.  If it does, then try 
            % construct the node, otherwise we'll just return 
            % empty.  Don't try to do a refreshCache here to avoid
            % loading all children.
            % Note that since the nodeName actually corresponds to the "Alias" key
            % under the child nodes, we have to search to find the correct
            % node to load.  As we're not refreshing the cache, it's also possible
            % that the Alias key doesn't yet exist on the settings node (this would
            % get added in the NamedNode constructor), so if the Alias key doesn't 
            % exist yet, we have to just use the node name.
            children = properties(obj.TopLevelRawSettingsNode);
            nodes = cellfun(@(x) obj.TopLevelRawSettingsNode.(x), children, 'UniformOutput', false);
            aliases = cellfun(@(x, y) iGetAliasFromSettingsNode(x,y), nodes, children, ...
                'UniformOutput', false);
            
            hasAliasIdx = strcmp(nodeName, aliases);
            if any(hasAliasIdx)
                settingsName = children{hasAliasIdx};
                nodeWasRemoved = obj.removeNodeOrEnsureInCache(settingsName);
                if nodeWasRemoved
                    warning(message('parallel:settings:NamedNodesDeletedInvalidChild', settingsName, obj.TopLevelRawSettingsNode.SettingsName, obj.Type));
                else
                    % Node must exist in the cache, so no need to 
                    % check again.
                    [~, node] = obj.getNodeFromCacheNoRefresh(nodeName);
                end
            else
                node = obj.EmptyNodeFcn();
            end
        end
        
        function deleteNodes(obj, nodeNames)
        % deleteNodes(obj, nodeNames) - removes the specified named
        % nodes from the cache and deletes the actual node (i.e. removes 
        % from Settings).  Does not throw errors if the names do not
        % exist.
            if isempty(nodeNames)
                return;
            end
            if ~iscell(nodeNames)
                nodeNames = {nodeNames};
            end
            if ~iscellstr(nodeNames)
                error(message('parallel:settings:DeleteNamedNodesNameNotString'));
            end
            
            for ii = 1:numel(nodeNames)
                currName = nodeNames{ii};
                node = obj.getFromName(currName);
                if ~isempty(node)
                    node.delete();
                end
            end
        end
        
        function deleteAllNodes(obj)
        % deleteAllNodes(obj) - deletes all the nodes from the cache
        % and removes the actual Settings nodes.
            obj.refreshCacheIfRequired();
            nodes = obj.CachedNodes;
            if ~isempty(nodes)
                nodes.delete();
            end
        end
        
        function [addedNames, origNodeNames, haveNamesChanged] = addFromCollection(obj, otherCollection, ...
            nameSuffix, level, usedNames)
        % [addedNames, origNames, haveNamesChanged] = addFromCollection(obj, otherCollection, ...
        %   nameSuffix, level, usedNames)
        % Add the nodes from the otherCollection to this collection at the specified level.   
        % Any nodes with the whose name clashes with one in this collection or with any of the 
        % names in usedNames will be given an new name inspired by the original name and the 
        % nameSuffix.  Returns the names of all the nodes that were added, their original names 
        % and a boolean flag indicating if the name has changed.
        % WARNING: This method will rename nodes in the otherCollection (the source collection
        % to add nodes from). Normally, the otherCollection is not used for anthing else.
            validateattributes(otherCollection, {'parallel.internal.settings.NamedNodesCollection'}, {'scalar'});
            validateattributes(nameSuffix, {'char'}, {});
            validateattributes(level, {'parallel.internal.types.SettingsLevel'}, {'scalar'});
            if nargin < 5
                usedNames = {};
            elseif ~iscellstr(usedNames) %#ok<ISCLSTR> only support cellstr
                error(message('parallel:settings:NamedNodesCollectionAddUsedNames'));
            end
            if isequal(obj, otherCollection)
                error(message('parallel:settings:NamedNodesCollectionAddFromSame'));
            end
            if ~isequal(obj.Type, otherCollection.Type)
                error(message('parallel:settings:NamedNodesCollectionAddFromWrongType', otherCollection.Type, obj.Type));
            end
            if isequal(obj.TopLevelRawSettingsNode, otherCollection.TopLevelRawSettingsNode)
                error(message('parallel:settings:NamedNodesCollectionAddFromSameSettingsNode'));
            end
            
            aliasKeyName = parallel.internal.settings.NamedNode.AliasKeyName;
            
            origNodeNames = otherCollection.getAllNames();
            origSettingsNames = otherCollection.getAllSettingsNames();
            addedNames       = cell(size(origNodeNames));
            haveNamesChanged = false(size(origNodeNames));
            haveAddedIdx     = false(size(origNodeNames));
            for ii = 1:numel(origNodeNames)
                currNodeName = origNodeNames{ii};
                currSettingsName = origSettingsNames{ii};
                % The source settings node that we need to add to our TopLevelRawSettingsNode
                otherSettingsNode = otherCollection.TopLevelRawSettingsNode.(currSettingsName);
                % Check for name clashes with our names.  
                [currNodeName, haveNamesChanged(ii)] = obj.getUnusedName(currNodeName, nameSuffix, usedNames);
                currSettingsName = obj.getUnusedSettingsNameNoCache(currNodeName);
                
                % Add the new node to our TopLevelRawSettingsNode
                obj.TopLevelRawSettingsNode.addGroup(currSettingsName);
                addedNames{ii} = currNodeName;
                destSettingsNode = obj.TopLevelRawSettingsNode.(currSettingsName);
                try
                    if haveNamesChanged(ii) 
                        % Ensure that we fix the Alias for all the levels
                        % at which it is currently set.
                        settingsLevels = enumeration('parallel.internal.types.SettingsLevel');
                        for jj = 1:numel(settingsLevels)
                            currLevel = settingsLevels(jj);
                            if currLevel.CanReallyCallIsSet() ...
                                    && currLevel.CanReallySet() ...
                                    && currLevel.hasValue(otherSettingsNode.(aliasKeyName))
                                currLevel.setValue(otherSettingsNode.(aliasKeyName), currNodeName);
                            end
                        end
                    end

                    % Merge the contents of the other node onto ours.
                    matlab.internal.mergeSettingsGroup(destSettingsNode, otherSettingsNode, level.Name);

                    haveAddedIdx = true;
                catch err
                    % Remove the settings node that was just added to the TopLevelRawSettingsNode, 
                    % but has not yet had its values merged from the otherCollection
                    obj.TopLevelRawSettingsNode.removeGroup(currSettingsName);
                    % Remove the nodes that have already been added
                    cellfun(@(x) obj.TopLevelRawSettingsNode.removeGroup(x), addedNames(haveAddedIdx));
                    ex = MException(message('parallel:settings:NamedNodesMergeSettingsFailed', currSettingsName));
                    ex = ex.addCause(err);
                    throw(ex);
                end
            end
            % Now refresh the cache so that we get the newly added nodes.
            obj.refreshCacheIfRequired();
        end
    end
    
    methods (Access = protected)
        function settingsNames = getAllSettingsNames(obj)
        % getAllSettingsNames - get the SettingsNames of all the nodes in the cache
        % Refreshes the cache if it is not up-to-date.
            obj.refreshCacheIfRequired();
            nodes = obj.getAll();
            settingsNames = {nodes.SettingsName};
        end
        
        function settingsName = getUnusedSettingsNameNoCache(obj, nodeName)
        % settingName = getUnusedSettingsNameNoCache - get an unused SettingsName
        % directly from the Settings tree rather than by looking in the Cache
        % which may not be up-to-date
            [~, usedSettingsNames] = parallel.internal.settings.SettingsNodeFacade.getChildNamesFromNode(obj.TopLevelRawSettingsNode);
            settingsName = matlab.lang.makeValidName(nodeName);
            settingsName = matlab.lang.makeUniqueStrings(settingsName, usedSettingsNames, namelengthmax);
        end
    end
    
    methods (Access = private)
        function [nodeExists, node] = getNodeFromCacheNoRefresh(obj, nodeName)
            obj.removeInvalidCachedNodes();
            nodeIdx = strcmp(nodeName, obj.CachedNodeNames);
            nodeExists = any(nodeIdx) && sum(nodeIdx) == 1;
            if nodeExists
                node = obj.CachedNodes(nodeIdx);
            else
                node = obj.EmptyNodeFcn();
            end
        end

        function refreshCacheIfRequired(obj)
            % Cache is up to date if the settings names match with the child nodes
            obj.removeInvalidCachedNodes();
            settingsNames = properties(obj.TopLevelRawSettingsNode);
            needsRefresh = ~isequal(sort(settingsNames(:)), sort(obj.CachedSettingsNames(:)));

            if needsRefresh
                obj.refreshCache();
            end
        end
        
        function refreshCache(obj)
        % Refresh the cache with the current child nodes of the root node.
        % Note that nodes are automatically removed from the cache
        % when the underlying Settings node is deleted.
            children = properties(obj.TopLevelRawSettingsNode);

            % Remove stale nodes from the cache
            % NB Invalid handles have already been removed from the cache.
            staleNodes = setdiff(obj.CachedSettingsNames, children);
            cellfun(@(x) obj.removeNodeFromCache(x), staleNodes);

            % Then add any missing children to the cache.
            % Keep track of any children that aren't valid profiles/components
            invalidChildren = {};
            deletedChildren = {};
            for ii = 1:numel(children)
                currSettingsNames = children{ii};
                try
                    nodeWasRemoved = obj.removeNodeOrEnsureInCache(currSettingsNames);
                    if nodeWasRemoved
                        deletedChildren{end+1} = currSettingsNames; %#ok<AGROW>
                    end
                catch err %#ok<NASGU>
                    invalidChildren{end+1} = currSettingsNames; %#ok<AGROW>
                end
            end
            % Warn about any invalid/deleted nodes
            if ~isempty(deletedChildren)
                warning(message('parallel:settings:NamedNodesDeletedInvalidChildren', obj.TopLevelRawSettingsNode.SettingsName, strtrim( sprintf( '%s ', deletedChildren{ : } ) )));
            end
            if ~isempty(invalidChildren)
                warning(message('parallel:settings:NamedNodesFailedToRemoveInvalidChildren', obj.TopLevelRawSettingsNode.SettingsName, strtrim( sprintf( '%s ', invalidChildren{ : } ) )));
            end   
        end
        
        function wasRemoved = removeNodeOrEnsureInCache(obj, settingsName)
        % See if the supplied settingsName is a valid named node and ensure it is in 
        % the cache.  If the settingsName is not a valid named node, then remove it
        % from the TopLevelRawSettingsNode.
            if obj.TopLevelRawSettingsNode.hasSetting(settingsName)
                % Can't be a valid profile/component if it is a key, so remove it
                obj.TopLevelRawSettingsNode.removeSetting(settingsName);
                wasRemoved = true;
                return;
            end

            existingNames = obj.CachedSettingsNames;
            wasRemoved = false;
            if ~any(strcmp(settingsName, existingNames))
                try
                    % Construct the namedNode and store it in the cache.
                    namedNode = obj.NodeBuilderFcn(settingsName, obj.TopLevelRawSettingsNode.(settingsName), obj.TopLevelRawSettingsNode);
                    obj.addNodeToCache(namedNode);
                catch err %#ok<NASGU>
                    % wasn't a valid named node
                    obj.TopLevelRawSettingsNode.removeGroup(settingsName);
                    wasRemoved = true;
                end
            end
        end

        function addNodeToCache(obj, namedNode)
            import parallel.internal.settings.NamedNodeEventData
            
            if any(arrayfun(@(x) isequal(namedNode, x), obj.CachedNodes))
                return;
            end
            
            obj.CachedNodeListeners{end + 1} = obj.addListenersForCachedNode(namedNode);
            obj.CachedNodes(end + 1) = namedNode;
            
            obj.notify('NodeAdded', NamedNodeEventData(namedNode));
        end

        function listeners = addListenersForCachedNode(obj, namedNode)
            % Make sure we remove the namedNode from the cache if the user 
            % deletes it so that we don't have stale NamedNodes in the cache.
            % Also make sure we change the cache if the user renames the 
            % namedNode.
            settingsName = namedNode.SettingsName;
            destroyListener = event.listener(namedNode, 'ObjectBeingDestroyed', ...
                @(~, ~) obj.removeNodeFromCache(settingsName));
            nodeDestroyedListener = event.listener(namedNode, 'SettingsNodeDestroyed', ...
                @(~, ~)  obj.removeNodeFromCache(settingsName));
            nameChangedListener = event.listener(namedNode, 'NameChanged', ...
                @(namedNodeSrc, ~) obj.reattachListenersForCachedNode(namedNodeSrc));
            listeners = [destroyListener, nodeDestroyedListener, nameChangedListener];
        end

        function removeNodeFromCache(obj, settingsName)
            % Remove the node and the listeners from the arrays
            obj.removeInvalidCachedNodes();
            nodeIdx = strcmp(settingsName, obj.CachedSettingsNames);
            obj.removeNodesAndEnsureConsistentCache(nodeIdx);
        end
        
        function reattachListenersForCachedNode(obj, namedNode)
            % Find the existing node and make sure we re-attach listeners 
            % based on the new name
            obj.removeInvalidCachedNodes();
            nodeIdx = strcmp(namedNode.Name, obj.CachedNodeNames);
            obj.CachedNodeListeners{nodeIdx} = obj.addListenersForCachedNode(namedNode);
        end
        
        function removeInvalidCachedNodes(obj)
            % remove any invalid nodes first
            invalidNodes = ~arrayfun(@isvalid, obj.CachedNodes);
            obj.removeNodesAndEnsureConsistentCache(invalidNodes);
        end
        
        function removeNodesAndEnsureConsistentCache(obj, idxToRemove)
        % Remove nodes from the cache and ensure that an empty cache is 
        % always 0x0
            obj.CachedNodes(idxToRemove) = [];
            obj.CachedNodeListeners(idxToRemove) = [];
            if isempty(obj.CachedNodes)
                obj.CachedNodes = obj.EmptyNodeFcn();
                obj.CachedNodeListeners = {};
            end
        end
    end
end

function [props, vals, level] = iAmalgamateAndExtractLevel(pvPairs)
% Amalgamate the PV pairs and extract the level
import parallel.internal.types.SettingsLevel
import parallel.internal.customattr.PropSet

[levelIsSpecified, level, pvPairs] = PropSet.extractOneProperty('Level', pvPairs{:});
% Default to user level if none supplied
if ~levelIsSpecified
    level = SettingsLevel.User;
else
    level = SettingsLevel.fromNameI(level);
    SettingsLevel.errorIfLevelsNotSettable(level);
end

[props, vals] = parallel.internal.customattr.PropSet.amalgamate({}, pvPairs{:});
end

function alias = iGetAliasFromSettingsNode(node, nodeName)
% If the Alias key doesn't exist yet, just defer to the node name.  Don't
% try to add the alias key as that will get added in the NamedNode constructor
keyName = parallel.internal.settings.NamedNode.AliasKeyName;
if node.hasSetting(keyName)
    alias = node.(keyName).ActiveValue;
else
    alias = nodeName;
end
end
