% ProfilesConfigSerializer - Serializer for configurations that are stored as Profiles
% Used to provide backwards compatibility of Profiles.
%
% Use pctconfig('useprofiles', <true/false>) to switch between using (API2) 
% Profiles-backed configurations and (API1) preferences-backed configurations

% Copyright 2011-2012 The MathWorks, Inc.

classdef (Hidden, Sealed) ProfilesConfigSerializer < parallel.internal.settings.AbstractConfigSerializer
    properties (Dependent, GetAccess = private)
        ParallelSettings
    end
    properties (Access = private)
        % Keep track of how many times the configurations have changed.  
        % This is required to support configurations-as-profiles from 
        % distcompConfigSection in API1.
        % The CacheCounter is incremented whenever saveConfigurationStruct
        % is called, or when the last-retrieved profile in 
        % getConfigurationStruct is changed
        CacheCounter
    end

    methods (Static)
        function instance = getInstance()
            persistent sInstance
            if isempty(sInstance)
                sInstance = parallel.internal.settings.ProfilesConfigSerializer;
            end
            instance = sInstance;
        end
    end
    
    methods
        function val = get.ParallelSettings(~)
            val = parallel.Settings;
        end
    end

    methods % implementation of AbstractConfigSerializer Abstract methods
        function newName = clone(obj, name, proposedName)
        % clone - clone a configuration.  Returns the actual name of
        % the new configuration
        % This is required for interim support of configurations-as-profiles from 
        % the config UI.
            import parallel.internal.settings.ProfilesConverter
            validateattributes(name, {'char'}, {'row'});
            validateattributes(proposedName, {'char'}, {'row'});

            % Get the profile with that name and its associated components
            profile = obj.findProfileOrErrorIfEmpty(name);
            schedComp = profile.getSchedulerComponent();
            projComp = profile.getProjectComponent();
            
            % Get the appropriate names for the new profile and components
            newName = obj.ParallelSettings.hGetUnusedProfileName(proposedName);
            [newScName, newPcName] = ProfilesConverter.deriveComponentNamesForProfile(newName);
            % Clone the components and the profile, ensuring that the new
            % profile uses the new component names
            try
                schedComp.saveAs(newScName);
                if ~isempty(projComp)
                    projComp.saveAs(newPcName);
                end
                profile.saveAs(newName);
                newProf = obj.findProfileOrErrorIfEmpty(newName);
                newProf.SchedulerComponent = newScName;
                if ~isempty(projComp)
                    newProf.ProjectComponent = newPcName;
                end
            catch err
                iDeleteProfileAndComponentsIfExist(newName, newScName, newPcName);
                rethrow(err);
            end
        end
        
        function newName = createNew(obj, proposedName, api1Type)
        % createNew - create a new configuration of the specified type.  
        % Returns the actual name of the new configuration.
        % This is required for interim support of configurations-as-profiles from 
        % the config UI.
            import parallel.internal.apishared.ConversionContext
            import parallel.internal.settings.ProfilesConverter
            validateattributes(proposedName, {'char'}, {'row'});
            validateattributes(api1Type, {'char'}, {'row'});
            
            % Get the appropriate names for the new profile and components
            newName = obj.ParallelSettings.hGetUnusedProfileName(proposedName);
            [newScName, newPcName] = ProfilesConverter.deriveComponentNamesForProfile(newName);
            
            api2Type = ConversionContext.ClusterType.convertToApi2(api1Type);
            try
                obj.ParallelSettings.createSchedulerComponent(api2Type, newScName);
                obj.ParallelSettings.createProjectComponent(newPcName);
                obj.ParallelSettings.createProfile(newName, newScName, 'ProjectComponent', newPcName);
            catch err
                iDeleteProfileAndComponentsIfExist(newName, newScName, newPcName);
                rethrow(err);
            end
        end
        
        function [name, nameInFile, values] = createNewFromFile(~, filename)
        % createNewFromFile - import the configuration from the supplied file
        % Returns the actual name of the new configuration, the name of the 
        % configuration/profile in the file and a structure containing the 
        % configuration values.  
        % The file can be an exported configuration or profile (with associated
        % components)
        % This is required to support configurations-as-profiles from 
        % importParallelConfig in API1
            import parallel.internal.apishared.ConfigurationsUtils
            import parallel.internal.settings.ProfilesConverter
            validateattributes(filename, {'char'}, {'row'});
            % Actually load the contents of the .mat file
            [nameInFile, values] = ConfigurationsUtils.loadConfigurationFile(filename);
            % Create the profile from it
            fullConfigStruct = iGetFullConfigStruct(nameInFile, values);
            profile = ProfilesConverter.createProfileFromConfigurationStruct(fullConfigStruct);
            name = profile.Name;
        end
        
        function deleteConfig(obj, configName)
        % deleteConfig - delete the config with the specified name
        % This is required for interim support of configurations-as-profiles from 
        % the config UI.
            validateattributes(configName, {'char'}, {'row'});
            profile = obj.findProfileOrErrorIfEmpty(configName);
            schedComp = profile.getSchedulerComponent();
            projComp = profile.getProjectComponent();
            
            profile.delete();
            schedComp.delete();
            if ~isempty(projComp)
                projComp.delete();
            end
        end

        function rename(obj, oldName, newName)
        % rename - rename the specified config.  Errors if the newName is already
        % in use.  NB This renames only the profile.  The scheduler component
        % and project component names are not changed.
        % This is required for interim support of configurations-as-profiles from 
        % the config UI.
            import parallel.internal.settings.ProfilesConverter
            validateattributes(oldName, {'char'}, {'row'});
            validateattributes(newName, {'char'}, {'row'});
            
            if isequal(oldName, newName)
                return;
            end
            if obj.ParallelSettings.hProfileExists(newName)
                error(message('parallel:settings:RenameConfigurationNameAlreadyInUse', ...
                    oldName, newName, newName));
            end
            profile = obj.findProfileOrErrorIfEmpty(oldName);
            profile.Name = newName;
        end
        
        function names = getAllNames(obj)
        % getAllNames - get the names of all configurations
        % This is required to support configurations-as-profiles from 
        % defaultParallelConfig in API1
            names = obj.ParallelSettings.hGetAllProfileNames();
        end
        
        function name = getDefaultName(obj)
        % getDefaultName - get the name of the default configuration
        % This is required to support configurations-as-profiles from 
        % defaultParallelConfig in API1
            name = obj.ParallelSettings.DefaultProfile;
        end
        
        function setDefaultName(obj, configName)
        % setDefaultName - set the name of the default configuration
        % This is required to support configurations-as-profiles from 
        % defaultParallelConfig in API1
            validateattributes(configName, {'char'}, {'row'});
            obj.ParallelSettings.DefaultProfile = configName;
        end
        
        function configStruct = getConfigurationStruct(obj, configName)
        % getConfigurationStruct - get the configuration structure for the 
        % configuration with the specified name.  The returned structure
        % has fields 'findResource', 'scheduler', 'job', 'paralleljob', 'task'.
        % (i.e. it is the "Values" section of the configuration.
        % This is required to support configurations-as-profiles from API1
            import parallel.internal.settings.ProfilesConverter
            persistent cachedConfigName
            persistent cachedConfigStruct
            persistent cachedListeners %#ok<PUSE> - hold on to listeners
            
            validateattributes(configName, {'char'}, {'row'});
            if isequal(cachedConfigName, configName)
                configStruct = cachedConfigStruct;
                return;
            end
            
            % Get the configuration structure from the profile
            profile = obj.findProfileOrErrorIfEmpty(configName);
            fullConfigStruct = ProfilesConverter.getConfigurationStructureFromProfile(profile);
            configStruct = fullConfigStruct.Values;
            
            % Attach listeners to the profile and components so we can clear our cache
            % and increment the cache counter if any of them change or are deleted.
            % SettingsNodeChanged event will be fired if the underlying settings node is deleted
            % (e.g. from the UI) and ObjectBeingDestroyed will be fired if the delete() method
            % is called on the profile/component.
            schedComp = profile.getSchedulerComponent();
            projComp  = profile.getProjectComponent();
            eventsToListen = {'SettingsNodeChanged', 'ObjectBeingDestroyed'};
            listenerFcn = @(~, ~) nResetCachedValues;
            profileListeners      = cellfun(@(x) event.listener(profile, x, listenerFcn), eventsToListen, ...
                                           'UniformOutput', false);
            schedCompListeners    = cellfun(@(x) event.listener(schedComp, x, listenerFcn), eventsToListen, ...
                                           'UniformOutput', false);
            if isempty(projComp)
                projCompListeners = {event.listener.empty()};
            else
                projCompListeners = cellfun(@(x) event.listener(projComp,  x, listenerFcn), eventsToListen, ...
                                           'UniformOutput', false);
            end
            
            % Stash the values in the cache.  Need to cache the listeners so that they
            % actually get fired.
            cachedConfigName   = configName;
            cachedConfigStruct = configStruct;
            cachedListeners    = [profileListeners{:}, schedCompListeners{:}, projCompListeners{:}];

            function nResetCachedValues
            % Reset the cached values and clear the listeners
                cachedConfigName   = '';
                cachedConfigStruct = struct([]);
                cachedListeners    = {};
                obj.incrementCacheCounter();
            end
        end
        
        function saveConfigurationStruct(obj, configName, newValues)
        % saveConfigurationStruct - save the new values for the specified
        % configuration.  The supplied structure is the "Values" section of the 
        % configuration and should have fields 'findResource', 'scheduler', 'job',
        % 'paralleljob', 'task'.
        % This is required for interim support of configurations-as-profiles from 
        % the config UI.
            import parallel.internal.settings.ProfilesConverter
            validateattributes(configName, {'char'}, {'row'});
            validateattributes(newValues, {'struct'}, {'scalar'});
            
            profile = obj.findProfileOrErrorIfEmpty(configName);
            fullConfigStruct = iGetFullConfigStruct(configName, newValues);
            ProfilesConverter.setConfigurationStructOnProfile(profile, fullConfigStruct);  
            % Increase the cache counter every time we save a new configuration struct.
            % Note that this isn't really necessary because getConfigurationStruct 
            % already deals with the CacheCounter, and the only place that cares 
            % (distcompConfigSection) only ever accesses profiles via getConfigurationStruct.
            obj.incrementCacheCounter();
        end
        
        function cacheCounter = getCacheCounter(obj)
        % getCacheCounter - get the cache counter for the configurations.  
        % The counter is increased every time a configuration is saved
        % or when the last accessed profile is changed.
        % This is required to support configurations-as-profiles from 
        % distcompConfigSection in API1.
            cacheCounter = obj.CacheCounter;
        end
    end

    methods (Access = private)
        function obj = ProfilesConfigSerializer()
            obj.CacheCounter = 0;
        end
        
        function incrementCacheCounter(obj)
            obj.CacheCounter = obj.CacheCounter + 1;
        end
        
        function profile = findProfileOrErrorIfEmpty(obj, profileName)
            profile = obj.ParallelSettings.findProfile('Name', profileName);
            if ~isempty(profile)
                return;
            end
            obj.ParallelSettings.hThrowNonExistentProfile(profileName);
        end
    end
end


%----------------------------------------------------------
function iDeleteProfileAndComponentsIfExist(profName, scName, pcName)
p = parallel.Settings;
schedComp = p.findSchedulerComponent('Name', scName);
iDeleteIfNotEmpty(schedComp);
projComp = p.findProjectComponent('Name', pcName);
iDeleteIfNotEmpty(projComp);
prof = p.findProfile('Name', profName);
iDeleteIfNotEmpty(prof);
end

%----------------------------------------------------------
function iDeleteIfNotEmpty(obj)
if ~isempty(obj)
    obj.delete();
end
end

%----------------------------------------------------------
function fullConfigStruct = iGetFullConfigStruct(configName, valuesStruct)
fullConfigStruct = struct('Name', configName, 'Values', valuesStruct);
end