% ParallelSettingsUpgrader - helper class for upgrading parallel.Settings

% Copyright 2016-2020 The MathWorks, Inc.
classdef (Sealed, Hidden) ParallelSettingsUpgrader
    
    properties (Constant)
        VersionSettingName = 'PCTVersionNumber';
        OldLicensingName = 'RequiresMathWorksHostedLicensing';
        NewLicensingName = 'RequiresOnlineLicensing';
        OldGenericScriptsLocationName = 'IntegrationScriptsLocation';
        NewGenericScriptsLocationName = 'PluginScriptsLocation';
    end
        
    methods (Static)
        function parallelRoot = upgrade(parallelRoot)
            % Execute updates which depend on the version difference between the
            % settings and the current PCT version
            startVersion = iGetSettingsVersion(parallelRoot);
            currentVersion = iGetCurrentPCTVersion();
            versionUpdates = startVersion:currentVersion;
            updateFunctionFormat = 'iUpdateVersion_%g_%g';
            for ii = 1:numel(versionUpdates)-1
                upgradeFrom = versionUpdates(ii);
                upgradeTo = versionUpdates(ii+1);
                updateHandleString = sprintf(updateFunctionFormat, upgradeFrom, upgradeTo);
                if exist(updateHandleString) == 2 %#ok<EXIST>
                    updateHandle = str2func(updateHandleString);
                    parallelRoot = updateHandle(parallelRoot);
                end

                % Update settings version
                iSetSettingsVersion(parallelRoot, upgradeTo);
            end
        end
    end    
end

function removeSetting(grandParentNode, parentNodeName, settingNameToRemove)
    % Remove a setting from a settings tree.
    try
        grandParentNode.(parentNodeName).removeSetting(settingNameToRemove);
    catch
    end
end

function removeSettingsGroup(grandParentNode, parentNodeName, groupNameToRemove)
% Remove a settings group from a settings tree.
    try
        grandParentNode.(parentNodeName).removeGroup(groupNameToRemove);
    catch
    end
end

function iReplacePropertyIfExists(node, oldProp, newProp)
% Check if the node has a setting for oldProp and replace it with the
% value from newProp. Then delete the oldProp setting.
    try
        if hasSetting(node, oldProp)
            addSetting(node, newProp);
            if hasPersonalValue(node.(oldProp))
                node.(newProp).PersonalValue = node.(oldProp).PersonalValue;
            end
            removeSetting(node, oldProp);
        end
    catch
    end
end

function parallelRoot = iUpdateVersion_29_30(parallelRoot)
    % Update parallel settings from PCT version 29 to 30
    % IntegrationScriptsLocation is renamed to PluginScriptsLocation for
    % Generic clusters in 19b.
    import parallel.internal.settings.ParallelSettingsTree
    import parallel.internal.settings.ParallelSettingsUpgrader
    import parallel.internal.types.SchedulerType
    
    oldSettingName = ParallelSettingsUpgrader.OldGenericScriptsLocationName;
    newSettingName = ParallelSettingsUpgrader.NewGenericScriptsLocationName;

    if ~parallelRoot.hasGroup(ParallelSettingsTree.SchedulerComponentsNodeName)
        return
    end
    schedCompsNode = parallelRoot.(ParallelSettingsTree.SchedulerComponentsNodeName);
    
    genericNodeName = SchedulerType.Generic.Name;
    if ~schedCompsNode.hasGroup(genericNodeName)
        return
    end
    genericNode = schedCompsNode.(genericNodeName);
    
    % Get the names of all the Generic Scheduler Components
    genericSchedCompNames = properties(genericNode);
    cellfun(@(x) iReplacePropertyIfExists(genericNode.(x), oldSettingName, newSettingName), genericSchedCompNames);
end

function parallelRoot = iUpdateVersion_28_29(parallelRoot)
    % Update parallel settings from PCT version 28 to 29
    % MATLAB Parallel Cloud is no longer available from version 29.
    parallelRoot = iRemoveMATLABParallelCloud(parallelRoot);
    parallelRoot = iUpdateLicensingSetting(parallelRoot);
end

function parallelRoot = iRemoveMATLABParallelCloud(parallelRoot)
    import parallel.internal.settings.ParallelSettingsTree
    import parallel.internal.settings.ParallelSettingsUpgrader
    try
        internalNode = parallelRoot.(ParallelSettingsTree.InternalSettingsNodeName);
        removeSetting(internalNode, ParallelSettingsTree.ProfilesReadOnlyAliasNodeName, 'MATLABParallelCloud');
        removeSetting(internalNode, ParallelSettingsTree.ProfilesReadOnlySchedNodeName, 'MATLABParallelCloud');
        removeSetting(internalNode, ParallelSettingsTree.SchedulerComponentReadOnlyAliasNodeName, 'MATLABParallelCloudSchedulerComponent');
    catch
    end
    removeSettingsGroup(parallelRoot, ParallelSettingsTree.ProfilesNodeName, 'MATLABParallelCloud');
    removeSettingsGroup(parallelRoot, ParallelSettingsTree.SchedulerComponentsNodeName, 'Cloud');
end

function parallelRoot = iUpdateLicensingSetting(parallelRoot)
    import parallel.internal.settings.ParallelSettingsTree
    import parallel.internal.settings.ParallelSettingsUpgrader

    oldProp = ParallelSettingsUpgrader.OldLicensingName;
    newProp = ParallelSettingsUpgrader.NewLicensingName;

    % There are SettingsGroups for each SchedulerComponent type, e.g. Local,
    % MJS, Slurm etc. First loop over them.
    if ~parallelRoot.hasGroup(ParallelSettingsTree.SchedulerComponentsNodeName)
        return
    end
    schedCompsNode = parallelRoot.(ParallelSettingsTree.SchedulerComponentsNodeName);
    schedProps = properties(schedCompsNode);
    for ii = 1:length(schedProps)
        % For each scheduler component type, the settings node has SettingsGroups
        % for each loaded scheduler component. Loop over those and replace the value
        % of the old setting in the new setting, and delete the old setting.
        thisProp = schedProps{ii};
        thisNode = schedCompsNode.(thisProp);
        nodeProps = properties(thisNode);
        cellfun(@(x) iReplacePropertyIfExists(thisNode.(x), oldProp, newProp), nodeProps);
    end
end

%---------------------------------------------------
function parallelRoot = iUpdateVersion_24_25(parallelRoot)
    % Update parallel settings from PCT version 24 to 25
    % Generic scheduler was deprecated to GenericR2016b. Any scheduler
    % components under Generic need to be transferred to GenericR2016b.
    import parallel.internal.settings.ParallelSettingsTree
    import parallel.internal.types.SchedulerType
    import parallel.internal.settings.SettingsNodeFacade
    
    if ~parallelRoot.hasGroup(ParallelSettingsTree.SchedulerComponentsNodeName)
        return
    end
    schedCompsNode = parallelRoot.(ParallelSettingsTree.SchedulerComponentsNodeName);
    
    sourceNodeName = SchedulerType.Generic.Name;
    destinationNodeName = SchedulerType.GenericR2016b.Name;
    
    if ~schedCompsNode.hasGroup(sourceNodeName)
        return
    end
    sourceNode = schedCompsNode.(sourceNodeName);

    if ~schedCompsNode.hasGroup(destinationNodeName)
        schedCompsNode.addGroup(destinationNodeName);
    end
    destinationNode = schedCompsNode.(destinationNodeName);
  
    destinationMetaclass = ?parallel.settings.schedulerComponent.GenericR2016b;
    [~, childrenNodeNames] = SettingsNodeFacade.getChildNamesFromNode(sourceNode);
    
    cellfun(@(childNodeName) iMoveSchedulerComponent(sourceNode, destinationNode, ...
        destinationMetaclass, childNodeName), childrenNodeNames);
end

%---------------------------------------------------
function iMoveSchedulerComponent(sourceSchedCompsNode, destinationSchedCompsNode, destinationMetaclass, childNodeName)
    % Move a scheduler component called childNodeName from sourceSchedCompsNode to
    % destinationSchedCompsNode.
    % destinationMetaclass is the metaclass of the destination scheduler
    % component. This is required to copy over the key types.
    
    import parallel.internal.settings.CustomSettingsGetSet
    import parallel.internal.settings.SettingsNodeFacade

    [~, ~, keys, types] = ...
        parallel.internal.settings.getPropertiesAndConstraints(destinationMetaclass);
    
    sourceChildNodeFacade = SettingsNodeFacade(sourceSchedCompsNode.(childNodeName), childNodeName, sourceSchedCompsNode);
    try
        % Add scheduler component to destination
        destinationSchedCompsNode.addGroup(childNodeName);
        % Copy scheduler component node with constraints and values
        destinationChildNodeFacade = SettingsNodeFacade(destinationSchedCompsNode.(childNodeName), ...
            childNodeName, destinationSchedCompsNode);
        CustomSettingsGetSet.addMissingKeysWithType(destinationChildNodeFacade, keys, types);
        sourceChildNodeFacade.copyCollapsedValuesToNode(destinationChildNodeFacade);
    catch err
        % On failure we delete the destination node
        destinationChildNodeFacade.delete();
        dctSchedulerMessage(2, ['Failed to move scheduler component %s.' ...
            'Scheduler component %s will be deleted. Reason:\n%s'], ...
            childNodeName, childNodeName, err.getReport());
        parallel.internal.warningNoBackTrace(message('parallel:settings:FailedToUpgradeSchedulerComponent', childNodeName));
    end
    % Delete source node which has been moved.
    sourceChildNodeFacade.delete();
end

%---------------------------------------------------
function versionSetting = iGetVersionSetting(parallelRoot)
% Return the parallel.Settings version node. If it does not exists create
% it.
    import parallel.internal.settings.ParallelSettingsTree
    import parallel.internal.settings.ParallelSettingsUpgrader
    
    if ~parallelRoot.hasGroup(ParallelSettingsTree.VersionNodeName)
        parallelRoot.addGroup(ParallelSettingsTree.VersionNodeName);
    end
    versionNode = parallelRoot.(ParallelSettingsTree.VersionNodeName);
    
    if ~versionNode.hasSetting(ParallelSettingsUpgrader.VersionSettingName)
        versionNode.addSetting(ParallelSettingsUpgrader.VersionSettingName);
        versionSetting = versionNode.(ParallelSettingsUpgrader.VersionSettingName);
        versionSetting.PersonalValue = 0;
    else
        versionSetting = versionNode.(ParallelSettingsUpgrader.VersionSettingName);
    end
end

%---------------------------------------------------
function iSetSettingsVersion(parallelRoot, versionNumber)
% Set the PCT version number stored in parallel.Settings
    versionSetting = iGetVersionSetting(parallelRoot);
    versionSetting.PersonalValue = versionNumber;
end

%---------------------------------------------------
function versionNumber = iGetSettingsVersion(parallelRoot)
% Get the PCT version number stored in parallel.Settings
    versionSetting = iGetVersionSetting(parallelRoot);
    versionNumber = versionSetting.ActiveValue;
end

%---------------------------------------------------
function currentVersion = iGetCurrentPCTVersion()
% Get the current PCT version number
    currentVersion = parallel.internal.version.Version.Current.Num;
end
