% NamedNode - A named object that has properties stored in a Settings node 
% whose name is the same as the name of the object.
% Base class for Profiles and Associated Components.
 
%   Copyright 2011-2019 The MathWorks, Inc.

% TODO:doc public methods and properties
classdef (Hidden) NamedNode < parallel.internal.settings.CustomSettingsGetSet & ...
                              parallel.internal.settings.CustomSettingsPropDisp
    %---------------------------------------------------
    % Normal class properties
    %---------------------------------------------------
    properties (Constant, Hidden)
        AliasKeyName = 'Alias';
    end
    
    properties (Transient, Access = protected) % Implementation of CustomSettingsGetSet Abstract properties
        NodeFacade
    end

    properties (Transient, Dependent, AbortSet)
        % The name of the object.  May or may not be different from the 
        % SettingsName. Name needs to have AbortSet on so that we don't 
        % change the settings tree unnecessarily.
        Name 
    end

    properties (PCTGetSet, Transient, PCTConstraint = 'string', Hidden)
        % The alias of this node, which may be distinct from the name of the name of the
        % node in Settings.  If this key is not defined in the Settings tree, then we're dealing with
        % a profile that was created pre-15b, in which case we set Alias to be the same as the 
        % the node name.
        Alias
    end

    properties (GetAccess = private, SetAccess = immutable)
        IsAliasReadOnly = false
    end
    
    properties (Hidden, Transient, Dependent, AbortSet, SetAccess = immutable)
        % The name of the node in Settings.  
        SettingsName
    end

    properties (Hidden, Transient, Dependent, AbortSet, SetAccess = private)
        % Whether or not this node actually has a name.  It may not have a 
        % name if the underlying Settings node has actually been deleted.
        HasName
    end

    properties (Hidden, SetAccess = private, Transient)
        % Whether or not this profile can be exported.  This is to support
        % the disabling of export for the cloud profile. This is set by
        % the settings value "Exportable".  If "Exportable" is unset or
        % undefined, then IsExportable defaults to true.
        IsExportable
    end

    properties (Hidden, SetAccess = private, Transient)
        % Whether or not this profile can be copied.  This is to support
        % the disabling of duplication for the cloud profile. This is set by
        % the settings value "Copyable".  If "Copyable" is unset or
        % undefined, then IsCopyable defaults to true.
        IsCopyable
    end
    
    properties (Transient, Access = protected)
        % The listener for changes to the underlying Settings keys
        KeysChangedOnNodeListener
        % The listener for the underlying Settings node being destroyed
        % by someone else (not by us deleting it).  Note that this is 
        % NOT listening to the Settings node's ObjectBeingDestroyed event.
        NodeHasBeenDestroyedListener
    end

    events 
        % NameChanged is fired when the name of the Settings node changes 
        % (i.e. when the NamedNode is moved to a different Settings node).
        % Examples of listeners: NamedNodeCache - to ensure that the 
        % keys in the cache are kept up-to-date with the names 
        % of the Settings node
        NameChanged
        % NodeDestroyed is fired when the this node is destroyed.
        % NB Calling delete() on this NamedNode will fire
        % the NodeDestroyed event.
        NodeDestroyed
        % SettingsNodeDestroyed is fired when the underlying Settings node
        % is destroyed.
        % NB Calling delete() on this NamedNode will not fire
        % the SettingsNodeDestroyed event.
        % Examples of listeners: Profile - to ensure that the 
        % Validation Status for a destroyed profile is removed 
        % from the status cache.
        SettingsNodeDestroyed
        % SettingsNodeChanged is fired when any aspect of the Settings node is 
        % changed:
        %   * when the name of Settings node changes
        %   * when the underlying Settings node is destroyed
        %   * when any of the values of the keys are changed
        %   * when any child nodes/keys are added/removed (TODO:later
        %     waiting for Settings support)
        % NB Calling delete() on this NamedNode will not fire
        % the SettingsNodeChanged event.
        % Examples of listeners: ProfileExpander - to ensure 
        % that the cached PV Pairs from profiles/components 
        % the correct latest values
        SettingsNodeChanged
    end

    methods % Property get/set methods
        function name = get.Name(obj)
            name = obj.get(parallel.internal.settings.NamedNode.AliasKeyName);
        end
        
        function set.Name(obj, newName)
            obj.errorIfInvalidName(newName)
            oldName = obj.Name;
            try
                obj.set(parallel.internal.settings.NamedNode.AliasKeyName, newName);
                obj.notifyNameChanged(oldName, newName);
            catch err
                throw(err);
            end
        end
        
        function name = get.SettingsName(obj)
            name = obj.NodeFacade.NodeName;
        end
        
        function tf = get.HasName(obj)
            % If we have the Alias key in this node, then we have a name.
            tf = obj.NodeFacade.IsValid && ...
                obj.NodeFacade.existKeys({parallel.internal.settings.NamedNode.AliasKeyName});
        end
    end
    
    methods (Abstract, Hidden, Static)
        obj = hBuildFromSettings(name, settingsNode, parentSettingsNode, varargin);
    end
    
    methods (Hidden)
        function hSetIsExportable(obj, value)
            obj.IsExportable = value;
            obj.setLogicalValueToSettings('Exportable', value, true);
        end
        
        function hSetIsCopyable(obj, value)
            obj.IsCopyable = value;
            obj.setLogicalValueToSettings('Copyable', value, true);
        end
    end
    
    methods (Hidden) %Override of methods defined in CustomSettingsGetSet
        function hSetProperty(obj, props, values, varargin)
            if obj.IsAliasReadOnly && ~isempty(props) && ...
                any(strcmp(props, parallel.internal.settings.NamedNode.AliasKeyName))
                throwAsCaller(MException(message('parallel:settings:CannotRenameNode', obj.Alias, class(obj))));
            end
            hSetProperty@parallel.internal.settings.CustomSettingsGetSet(...
                obj, props, values, varargin{:});
        end
    end

    methods (Access = protected) % Constructor
        function obj = NamedNode(settingsName, settingsNode, parentSettingsNode, readOnlyAliases) 
            import parallel.internal.settings.CustomSettingsGetSet
            import parallel.internal.settings.SettingsNodeFacade
            
             % validateattributes(settingsName, {'char'}, {'row'});
             % validateattributes(settingsNode, {'Settings'}, {'scalar'});
             % validateattributes(parentSettingsNode, {'Settings'}, {'scalar'});

            obj.NodeFacade = SettingsNodeFacade(settingsNode, settingsName, parentSettingsNode);

            % Keys that are unset in Settings are not persisted when MATLAB is restarted.
            % Therefore, we must ensure all the properties that are stored in Settings 
            % actually exist as keys under this Settings node before we go any further.
            [~, ~, settingsProps, constraints] = obj.hGetPublicSettingsProperties();
            CustomSettingsGetSet.addMissingKeysWithType(obj.NodeFacade, settingsProps, constraints);
            % If this node doesn't have a "Alias" key set, then make it the
            % same as the settingsName for now NB We must do this after we
            % have added missing keys to the node.
            if ~obj.hIsSetAnywhere(parallel.internal.settings.NamedNode.AliasKeyName)
                obj.set(parallel.internal.settings.NamedNode.AliasKeyName, settingsName);
            end
            % Set the IsAliasReadOnly flag AFTER we have set the alias name
            if nargin > 3
                obj.IsAliasReadOnly = any(strcmp(settingsName, readOnlyAliases));
            end
            
            obj.attachListenersToNode();
            
            obj.IsExportable = obj.getLogicalValueFromSettingsOrDefault('Exportable', true);
            obj.IsCopyable = obj.getLogicalValueFromSettingsOrDefault('Copyable', true);
        end
    end
    
    methods % Destructor
        function delete(objOrObjs, varargin)
        % delete(obj) - delete the current NamedNode.  This removes the Settings node from the 
        % Settings tree.
        % varargin is specified to ensure we get dispatched correctly; but,
        % we don't support any additional arguments other than the magic
        % string 'invalidateOnly' which allows us to invalidate the handle
        % but not destroy the underlying data.
            narginchk(1, 2);

            if nargin == 2
                if ~isequal( varargin{1}, 'invalidateOnly' )
                    error(message('parallel:settings:InvalidDeleteSyntax'));
                end
                fullDestruction = false;
            else
                fullDestruction = true;
            end
            
            for ii = 1:length(objOrObjs)
                currObj = objOrObjs(ii);
                % This must be called before calling super delete methods.
                currObj.notify('NodeDestroyed');
                if fullDestruction
                    if currObj.NodeFacade.IsValid && currObj.NodeFacade.hasAnyFactoryValues()
                        error(message('parallel:settings:DeleteNodeHasFactoryValues', currObj.Name));
                    end
                    try
                        currObj.NodeFacade.delete();
                    catch err
                        % NB Must error here because a Profile doesn't get deleted
                        % if the Settings node can't be deleted (e.g. because of factory settings)
                        ex = MException(message('parallel:settings:FailedToDeleteSettingsNode', currObj.Name));
                        ex = ex.addCause(err);
                        throw(ex);
                    end
                end
                delete@parallel.internal.settings.CustomSettingsGetSet(currObj);
            end
        end
    end

    methods
        function tf = isequal(obj1, obj2)
        % isequal(obj1, obj2) - NamedNodes 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.NamedNode');
            if classAndSizeMatch
                tf = true;
                for ii = 1:numel(obj1)
                    tf = tf && isequal(obj1(ii).NodeFacade, obj2(ii).NodeFacade);
                end
            else 
                tf = false;
            end
        end
        
        function saveAs(obj, newName)
        % saveAs(obj, newName) - save the current NamedNode as a new NamedNode
        % with a different name.  This is like "duplicate" - i.e a Settings node 
        % with the newName will be created, but the current Settings node is not 
        % deleted.  Note that the collapsed values of the current Settings node
        % are copied to the new Settings node
            validateattributes(newName, {'char'}, {'row'});
            if ~obj.IsCopyable
                error(message('parallel:settings:NamedNodeCannotDuplicate', obj.Name, class(obj)));
            end
            obj.errorIfInvalidName(newName);
            settingsName = obj.getUnusedSettingsName(newName);
            newNodeFacade = obj.createSiblingNodeForCopy(settingsName);
            try
                obj.NodeFacade.copyCollapsedValuesToNode(newNodeFacade);
                newNodeFacade.setKey(parallel.internal.settings.NamedNode.AliasKeyName, ...
                    newName, parallel.internal.types.SettingsLevel.User);
            catch err
                newNodeFacade.delete();
                throw(err);
            end
        end
        
        function export(objOrObjs, filename)
        % export(objOrObjs, filename) - export the collapsed values to the 
        % specified file.
            validateattributes(filename, {'char'}, {'row'}, 'export', 'filename');
            objOrObjs = unique(objOrObjs);
            
            try
                objOrObjsToExport = parallel.internal.settings.NamedNode.removeUnexportableNodes(objOrObjs);
                nodeFacadesToExport = [objOrObjsToExport.NodeFacade];
                nodeFacadesToExport.exportNode(filename);
            catch err
                throw(err);
            end
        end
    end
    
    methods (Sealed)
        function saveToLevel(obj, level)
        % saveToLevel(obj, level) - save the user level values to the 
        % specified level.
            import parallel.internal.types.SettingsLevel
            validateattributes(level, {'char'}, {'row'});
            try
                level = SettingsLevel.fromNameI(level);
                obj.NodeFacade.saveUserToLevel(level);
            catch err
                throw(err);
            end
        end
        
        function unsetAll(obj, level)
        % unset(obj, level) - unset all the non-hidden property values at the 
        % specified level.  If no level is specified, the 
        % values are unset at the user level
            import parallel.internal.types.SettingsLevel
            import parallel.internal.customattr.Reflection
            if nargin < 2
                level = SettingsLevel.User;
            else
                validateattributes(level, {'char'}, {'row'});
                level = SettingsLevel.fromNameI(level);
            end
            allPublicProps = Reflection.getAllPublicProperties(obj);
            publicPropsNoHidden = Reflection.getPublicProperties(obj);
            propsToExclude = setdiff(allPublicProps, publicPropsNoHidden);
            try
                obj.NodeFacade.unsetNode(level, propsToExclude);
            catch err
                throw(err);
            end
        end
        
        function val = formatDispatcher(obj, displayHelper, valDisplayLength, formatter)
        % Format objects of this type and its subtypes for display
            val = formatter(displayHelper,  obj , valDisplayLength);
        end
    end
    
    methods (Hidden)
        function restorePoint = hGetRestorePoint(obj, level)
        % restorePoint = hGetRestorePoint(obj, level) - get a structure that represents
        % the current state of the node so that it can be used for restoring
        % if necessary.  If no level is supplied, then User is assumed.
            import parallel.internal.types.SettingsLevel
            if nargin < 2
                level = SettingsLevel.User;
            else
                validateattributes(level, {'parallel.internal.types.SettingsLevel'}, {'scalar'});
            end
            
            if ~level.CanReallyGet || ~level.CanReallySet
                error(message('parallel:settings:NamedNodeRestoreInvalidLevel'));
            end
            
            gettableProps = obj.hGetPublicSettingsProperties();
            userVals = cellfun(@(x) obj.get(x, level.Name), gettableProps, ...
                'UniformOutput', false);
            restorePoint.(level.Name) = cell2struct(userVals, gettableProps, 2);
        end
        
        function hRestore(obj, restorePoint)
        % hRestore(obj, restorePoint) - restore the node from a restorePoint
        % that was generated by hGetRestorePoint
            import parallel.internal.types.SettingsLevel
            import parallel.settings.DefaultValue
            validateattributes(restorePoint, {'struct'}, {'scalar'});
            
            levels = fieldnames(restorePoint);
            try
                cellfun(@(x) SettingsLevel.fromNameI(x), levels, 'UniformOutput', false);
            catch err
                error(message('parallel:settings:NamedNodeRestoreInvalidStruct'));
            end
            
            [~, settableProps] = obj.hGetPublicSettingsProperties();
            try
                for ii = 1:numel(levels)
                    currLevel = levels{ii};
                    restoreStruct = restorePoint.(currLevel);
                    propsToRestore = fieldnames(restoreStruct);
                    % Work out which are actually settable and which should be 
                    % set and which should be unset
                    props = intersect(settableProps(:), propsToRestore);
                    vals = cellfun(@(x) restoreStruct.(x), props, 'UniformOutput', false);
                    propsToUnsetIdx = cellfun(@(x) DefaultValue.isDefault(x), vals);            
                    
                    cellfun(@(x) obj.unset(x, currLevel), props(propsToUnsetIdx));
                    cellfun(@(x, y) obj.set(x, y, currLevel), ...
                        props(~propsToUnsetIdx), vals(~propsToUnsetIdx));
                end
            catch err
                ex = MException(message('parallel:settings:NamedNodeFailedToRestore', obj.Name));
                ex = ex.addCause(err);
                throw(ex);
            end
        end
    end

    methods (Sealed, Hidden)
        function hInvalidate(objOrObjs)
        %hInvalidate Invalidate named node objects without destroying underlying data.
            delete(objOrObjs, 'invalidateOnly');
        end
    end

    methods (Abstract, Access = protected)
        errorIfInvalidName(obj, newName)
    end

    methods (Access = protected)
        function notifyNameChanged(obj, oldName, newName)
        % Fire the NameChanged and SettingsNodeChanged events
            eventData = parallel.internal.settings.NameChangedEventData(oldName, newName);
            obj.notify('NameChanged', eventData);
            obj.notifySettingsNodeChanged();
        end
        function notifySettingsNodeDestroyed(obj)
        % Fire the SettingsNodeDestroyed and SettingsNodeChanged events
            obj.notify('SettingsNodeDestroyed');
            obj.notifySettingsNodeChanged();
        end
        function notifySettingsNodeChanged(obj, ~, eventData)
        % Fire the SettingsNodeChanged event
            if nargin < 2
                obj.notify('SettingsNodeChanged');
                return
            end

            node = eventData.AffectedObject;
            setting = node.(eventData.Source.Name);
            if setting.hasPersonalValue()
                value = setting.PersonalValue;
            else
                value = parallel.settings.DefaultValue();
            end
            evt = parallel.internal.settings.SettingsChangedEventData(...
                setting, eventData.Source.Name, value);
            obj.notify('SettingsNodeChanged', evt);
        end
    end
    
    methods (Sealed, Access = protected)
        function moveNode(obj, newName)
            if obj.NodeFacade.hasAnyFactoryValues()
                error(message('parallel:settings:MoveNodeHasFactoryValues', obj.Name));
            end

            newNodeFacade = obj.createSiblingNodeForCopy(newName);
            try
                % copyAllLevelsToNode may fail if the user does not have write 
                % access to all of the Settings levels in which this NamedNode 
                % is defined (e.g. workgroup)
                obj.NodeFacade.copyAllLevelsToNode(newNodeFacade);
                % Try to delete the existing node facade first before settings
                % the new named node.  Note that this really deletes the Settings 
                % node.  Ensure that we unset the listeners before the delete to
                % prevent the node destroyed listener from firing.  
                obj.KeysChangedOnNodeListener.delete();
                obj.NodeHasBeenDestroyedListener.delete();
                obj.NodeFacade.delete();
                obj.NodeFacade = newNodeFacade;
                % Make sure we attach listeners to the new node facade
                obj.attachListenersToNode();
            catch err
                newNodeFacade.delete();
                % Make sure we attach listeners back to the original node facade
                obj.attachListenersToNode();
                rethrow(err);
            end
        end
        
        function [p, v] = getCollapsedPropertiesNoDefaultValues(obj, p)
        % get the collapsed property values, ignoring any that are 
        % the DefaultValue (i.e. unset)
            v = obj.get(p);
            hasValue = ~cellfun(@(x) parallel.settings.DefaultValue.isDefault(x), v);
            p = p(hasValue);
            v = v(hasValue);
        end

        function settingsName = getUnusedSettingsName(obj, nodeName)
            siblingSettingsNames = obj.NodeFacade.getSiblingNodeNames();
            settingsName = matlab.lang.makeValidName(nodeName);
            settingsName = matlab.lang.makeUniqueStrings(settingsName, siblingSettingsNames, namelengthmax);
        end
    end
    
    methods (Static, Access = protected)
        function nodes = removeUnexportableNodes(nodes)
            % Warn about any nodes that aren't exportable and remove them 
            % from the array.  If none of the nodes are exportable, then error
            % instead.
            isExportable = cellfun(@(x) isequal(x, true), {nodes.IsExportable});
            if all(isExportable)
                return;
            end
            
            notExportable = nodes(~isExportable);
            if numel(notExportable) == 1 && numel(nodes) == 1
                error(message('parallel:settings:NamedNodeCannotExport', ...
                    notExportable.Name, class(notExportable)));
            elseif numel(notExportable) == numel(nodes)
                names = {notExportable.Name};
                error(message('parallel:settings:MultipleNamedNodesCannotExport', ...
                    class(nodes), sprintf('\n %s', names{:})));
            else
                cellfun(@(x, y) parallel.internal.warningNoBackTrace(message('parallel:settings:NamedNodeCannotExport', x, y)), ...
                    {notExportable.Name}, {class(notExportable)});
                nodes = nodes(isExportable);            
            end
        end
    end
    
    methods (Access = private)
        function attachListenersToNode(obj)
        % Attach listeners to changes the Settings Node
            import parallel.internal.customattr.Reflection

            % Attach listeners to changes in the public-settable keys.
            % The lifetime of these listeners is the same as obj.KeysChangedOnNodeListener
            % and obj.NodeHasBeenDestroyedListener.
            [allProps, useGetSet] = Reflection.getPublicProperties(obj);
            propsUsingGetSet = allProps(useGetSet);
            obj.KeysChangedOnNodeListener = obj.NodeFacade.addChangeListenerToKeys(...
                propsUsingGetSet, @obj.notifySettingsNodeChanged);
            % When the Settings node is destroyed, fire the SettingsNodeDestroyed event
            obj.NodeHasBeenDestroyedListener = event.listener(obj.NodeFacade, ...
                'NodeHasBeenDestroyed', @(~, ~) obj.notifySettingsNodeDestroyed());
        end
        
        function newNodeFacade = createSiblingNodeForCopy(obj, newName)
            import parallel.internal.settings.CustomSettingsGetSet
            newNodeFacade = obj.NodeFacade.createSiblingNode(newName);
            try
                [~, ~, settingsProps, constraints] = obj.hGetPublicSettingsProperties();
                CustomSettingsGetSet.addMissingKeysWithType(newNodeFacade, settingsProps, constraints);
            catch err
                newNodeFacade.delete();
                rethrow(err);
            end
        end

        function tf = getLogicalValueFromSettingsOrDefault(obj, secretNodeKey, defaultValue)
            % See if the secret node key is defined.  If it is, and it's not "Use Default",
            % then use its value, otherwise use the specified default value.
            tf = defaultValue;
            if obj.NodeFacade.existKeys({secretNodeKey})
                tf = obj.NodeFacade.getKey(secretNodeKey, parallel.internal.types.SettingsLevel.User);
                if parallel.settings.DefaultValue.isDefault(tf)
                    tf = defaultValue;
                end
            end
        end

        function setLogicalValueToSettings(obj, secretNodeKey, newValue, defaultValue)
            if newValue == defaultValue
                warningState = warning('off', 'parallel:settings:FailedRestoreNodeRemoveKeys');
                obj.NodeFacade.removeKeys({secretNodeKey});
                warning(warningState);
            else
                obj.NodeFacade.addKeys({secretNodeKey});
                obj.NodeFacade.setKey(secretNodeKey, newValue, parallel.internal.types.SettingsLevel.User);
            end
        end
    end
end
%#ok<*ATUNK> custom attributes


