classdef WorkflowController < handle
    % WORKFLOWCONTROLLER Handles server-client communication for model and proposal settings
    
    % Copyright 2015-2020 The MathWorks, Inc.
    
    properties(Access = 'private')
        UniqueId = '';
        ApplyConfigurationSubChannel = '/fpt/applyConfiguration';
        CollectRunNamePublishChannel = '/fpt/collectrunNameinfo';
        VerifyRunNamePublishChannel = '/fpt/verifyrunNameinfo';
        SimPublishChannel = '/fpt/simSettinginfo';
        SimSettingChannel = '/fpt/updateSimSetting';
        ShortcutGetCollectRunNameChannel = '/fpt/getCollectRunName';
        ShortcutGetVerifyRunNameChannel = '/fpt/getVerifyRunName';
        ProposalOptionChannel = '/fpt/updateProposalOptions';
        ProposalOptionPublishChannel = '/fpt/proposalOptionInfo';
        ToleranceSettingsPublishChannel = '/fpt/updateToleranceSettings';
        ToleranceSettingsSubscribeChannel = '/fpt/fetchToleranceSettings';
        UpdateTolerancesStoreSubscribeChannel = '/fpt/updateSignalTolerancesStore/';
        RangeModeUpdateChannel = '/fpt/updateRangeModes';
        TriggerProposalActionChannel = '/fpt/codeview/proposal';
        TriggerApplyActionChannel = '/fpt/codeview/apply';
        PublishProposalOptionsValidity = '/fpt/proposalOptionsValidation';
        EnableVerifyActionChannel = '/fpt/enableVerify';
        PublishSimulationInputs = '/fpt/updateSimulationInputs';
        ImportSimulationInputsSubChannel = '/fpt/importSimulationInputs';
        Subscriptions
        UpdateCustomShortcutsListener
        Model
        LastUsedChannel
        LastUsedData
    end
    
    events
        UpdateCustomShortcuts;
    end
    
    methods
        function this = WorkflowController(uniqueID)
            % Ensure the connector is on before subscribing
            connector.ensureServiceOn;
            
            this.UniqueId = uniqueID;
            this.initializeSubscriptions(uniqueID);
            
            this.SimPublishChannel = sprintf('%s/%s',this.SimPublishChannel, uniqueID);
            this.ProposalOptionPublishChannel = sprintf('%s/%s',this.ProposalOptionPublishChannel, uniqueID);
            this.PublishProposalOptionsValidity = sprintf('%s/%s',this.PublishProposalOptionsValidity, uniqueID);
            this.ToleranceSettingsPublishChannel = sprintf('%s/%s',this.ToleranceSettingsPublishChannel, uniqueID);
            this.CollectRunNamePublishChannel = sprintf('%s/%s',this.CollectRunNamePublishChannel, uniqueID);
            this.VerifyRunNamePublishChannel = sprintf('%s/%s',this.VerifyRunNamePublishChannel, uniqueID);
            this.TriggerProposalActionChannel = sprintf('%s/%s',this.TriggerProposalActionChannel, uniqueID);
            this.TriggerApplyActionChannel = sprintf('%s/%s',this.TriggerApplyActionChannel, uniqueID);
            this.EnableVerifyActionChannel = sprintf('%s/%s',this.EnableVerifyActionChannel, uniqueID);
            this.PublishSimulationInputs = sprintf('%s/%s',this.PublishSimulationInputs, uniqueID);
            
        end
    end
    
    methods(Hidden)
        function setModel(this, modelName)
            this.Model = modelName;
        end
        function uniqueID = getUniqueId(this)
            %% getUniqueId returns the unique id used for all channels
            uniqueID = this.UniqueId;
        end
        
        function publishData(this)
            if fixed.internal.utility.featuremanager.FeatureManager.getStatus('fxpoptInFPT') == 15
                this.resetTolerances();
            end
            message.publish(this.SimPublishChannel, get_param(this.Model,'MinMaxOverflowArchiveMode'));
            this.publishProposalOptions();
            this.publishSystemBehaviorSettings();
            this.importSimulationInputsFromWorkspace();
            this.enableVerifySection(this.canEnableVerifySection);
        end
        
        
        function publishProposalOptions(this)
            featureFlag = fixed.internal.utility.featuremanager.FeatureManager.getStatus('fxpoptInFPT');
            isIterativeConversionWorkflow = this.isIterativeConversion();

            % if 'fxpoptInFPT' is turned ON and current workflow is not iterative
            % conversion, proposal options should not be published
            if featureFlag == 15 && ~isIterativeConversionWorkflow
                return;
            end
            
            proposalOptions = this.getProposalOptions(featureFlag);
            serverProposalOptions = this.packageProposalOptions(proposalOptions);
            message.publish(this.ProposalOptionPublishChannel, serverProposalOptions);
        end
        
        function resetTolerances(~)
            fptInstance = fxptui.FixedPointTool.getExistingInstance;  
            if ~isempty(fptInstance.Converter)                          
                dataLayer = fxptds.DataLayerInterface.getInstance();
                dataLayer.clearSignalToleranceStore(fptInstance.Converter.TopModel);
            end
        end
        
        function publishSystemBehaviorSettings(this, varargin)
            %% publishSystemBehaviorSettings queries DesignStore for proposalSettings
            % and publishes to the client
            
            % Get the FPT instance on which the settings are being made
            fptInstance = fxptui.FixedPointTool.getExistingInstance;
            
            dataLayer = fxptds.DataLayerInterface.getInstance();
            
            % If the model reference is pointing to an invalid model,
            % Converter is empty
            if ~isempty(fptInstance.Converter)
                % Update the signalToleranceStore
                dataLayer.setSignalTolerancesStore(fptInstance.Converter.TopModel, fptInstance.Converter.AllSystems, 0);
            end
            
            dataStore = dataLayer.getSignalTolerancesStore(this.Model);
            this.LastUsedData = [];
            % Create the signal tolerances store. The client store is
            % updated with this signal logging data when the fixed-point tool
            % is launched. The store data is used by the client to populate
            % the tolerance settings table in the setup document.
            
            if ~isempty(dataStore)
                portIndices = [dataStore.PortIndex{:}];
                
                signalNames = {dataStore.Name};
                signalNames = [signalNames{:}];
                nonEmptySignalIndices = cellfun(@(x) ~isempty(x), signalNames, 'UniformOutput', false);
                nonEmptySignalIndices = [nonEmptySignalIndices{:}];
                
                % Stateflow local data is registerd in SDI with port
                % index 0. Currently, fxpopt supports only simulink block
                % path, port index syntax to add tolerances. Hence, the
                % following filtering has to be made to remove stateflow
                % elements with signal logging on to be used in the system
                % behavior workflow. In future, this filter will be removed
                % once fxpopt is enhanced to add tolerances to stateflow
                % data.
                signalsWithPortIndex = dataStore(portIndices > 0 & nonEmptySignalIndices, {'ID', 'Name', 'AbsTol', 'RelTol', 'TimeTol'});
                if (height(signalsWithPortIndex) > 0)
                    this.LastUsedData = table2struct(signalsWithPortIndex);
                end
            end
            if size(this.LastUsedData, 1) == 1
                this.LastUsedData = {this.LastUsedData};
            end
            message.publish(this.ToleranceSettingsPublishChannel, this.LastUsedData);
            this.LastUsedChannel = this.ToleranceSettingsPublishChannel;
        end
        function delete(this)
            for i = 1:numel(this.Subscriptions)
                message.unsubscribe(this.Subscriptions{i});
            end
            delete(this.UpdateCustomShortcutsListener);
            this.UpdateCustomShortcutsListener = [];
            this.Subscriptions = [];
        end
        
        function triggerProposalForCodeView(this)
            % Triggers the proposal action from the client when requested
            % by codeview. We do this so that the user will be given the
            % correct run selection dialog when needed.
            msgObj.message = 'propose';
            message.publish(this.TriggerProposalActionChannel,msgObj);
        end
        
        function triggerApplyForCodeView(this)
            % Triggers the apply action from the client when requested
            % by codeview. We do this so that the user will be given the
            % correct run selection dialog when needed.
            msgObj.message = 'apply';
            message.publish(this.TriggerApplyActionChannel,msgObj);
        end
        
        function publishProposalOptionsValidity(this, data)
            % Notify the client when an invalid input is specified to the
            % proposal options widgets
            
            % Manipulate the identifier in Exception object to identify the
            % settings that was changed
            % Identifier - SimulinkFixedPoint:autoscalerProposalSettings:invalidSafetyMargin
            % msgObj.identifier - invalidSafetyMargin
            identifier = strsplit(data.identifier, ':');
            msgObj.identifier = identifier{end};
            msgObj.statusText = data.message;
            message.publish(this.PublishProposalOptionsValidity, msgObj);
        end
        
        function importSimulationInputsFromWorkspace(this, ~)
            % Import the simulation input objects defined in the matlab workspace
            baseWorkspace = Simulink.data.BaseWorkspace;
            
            % Variables list from base workspace
            clientData = baseWorkspace.whos('Simulink.SimulationInput');
            
            message.publish(this.PublishSimulationInputs, clientData);
            % Used for testing
            this.LastUsedChannel = this.PublishSimulationInputs;
            this.LastUsedData = clientData;
        end
        
        function updateWorkflowProposalOptions(this, clientData)
            workflow = this.getCurrentWorkflow();
            proposalField = fxptui.message('proposalSettingsWidgetKey');
            settings = struct(proposalField, clientData);
            fxptui.internal.updateWorkflowSetting(workflow, settings);
            this.publishProposalOptions();
        end
        
        function success = updateProposalOptions(this, clientData)
            % used by Codeview WebFptFacade to update proposal settings as
            % well
            success = true;
            featureFlag = fixed.internal.utility.featuremanager.FeatureManager.getStatus('fxpoptInFPT');
            % if 'fxpoptInFPT' is turned ON, update workflow settings
            if featureFlag == 15
                this.updateWorkflowProposalOptions(clientData);
            else
                appData = SimulinkFixedPoint.getApplicationData(this.Model);
                proposalSetting = appData.AutoscalerProposalSettings;
                try
                    if isfield(clientData, 'RangeOption')
                        switch clientData.RangeOption
                            case 0
                                proposalSetting.isUsingSimMinMax = true;
                                proposalSetting.isUsingDerivedMinMax = true;
                            case 1
                                proposalSetting.isUsingSimMinMax = true;
                                proposalSetting.isUsingDerivedMinMax = false;
                            case 2
                                proposalSetting.isUsingSimMinMax = false;
                                proposalSetting.isUsingDerivedMinMax = true;
                        end
                    end
                    if isfield(clientData, 'isAutoSignedness')
                        proposalSetting.isAutoSignedness = clientData.isAutoSignedness;
                    end
                    if isfield(clientData, 'isWLSelectionPolicy')
                        proposalSetting.isWLSelectionPolicy = clientData.isWLSelectionPolicy;
                    end
                    if isfield(clientData, 'ProposeForInherited')
                        proposalSetting.ProposeForInherited = clientData.ProposeForInherited;
                    end
                    if isfield(clientData, 'ProposeForFloatingPoint')
                        proposalSetting.ProposeForFloatingPoint = clientData.ProposeForFloatingPoint;
                    end
                    if isfield(clientData, 'DefaultWordLength')
                        proposalSetting.DefaultWordLength = clientData.DefaultWordLength;
                    end
                    if isfield(clientData, 'DefaultFractionLength')
                        % Fractional/ Decimal values entered by the
                        % user are rounded and sent to the client. If an invalid/
                        % out of range value is entered, the server sends the
                        % last valid value
                        proposalSetting.DefaultFractionLength = clientData.DefaultFractionLength;
                    end
                    if isfield(clientData, 'SafetyMarginForSimMinMax')
                        % safetyMargin in (-100, Inf)
                        proposalSetting.SafetyMarginForSimMinMax = clientData.SafetyMarginForSimMinMax;
                    end
                catch err
                    success = false;
                    % Invalid Proposal Settings value. Notify the client
                    this.publishProposalOptionsValidity(err);
                    return;
                end
                this.publishProposalOptions;
            end
        end
        
        function updateSignalTolerances(~, tolerances)
            % Update the signal tolerances store with client data
            % Get the FPT instance on which the settings are being made
            fptInstance = fxptui.FixedPointTool.getExistingInstance;
            
            dataLayer = fxptds.DataLayerInterface.getInstance();
            
            % Update the tolerance field in the signal tolerance store
            dataLayer.updateTolerances(fptInstance.getModel, tolerances);
        end
        
        function shortcutName = getShortcutName(~, shortcutName)
            switch shortcutName
                case 'UseCurrent'
                    shortcutName = fxptui.message('lblOriginalSettings');
                case 'DoubleOverride'
                    shortcutName = fxptui.message('lblDblOverride');
                case 'SingleOverride'
                    shortcutName = fxptui.message('lblSglOverride');
                case 'ScaledDoubles'
                    shortcutName = fxptui.message('lblSclDblOverride');
                case 'SpecifiedType'
                    shortcutName = fxptui.message('lblFxptOverride');
            end
        end
        
    end
    
    methods(Access='private')
        function initializeSubscriptions(this, uniqueID)
            this.Subscriptions{1} = message.subscribe(sprintf('%s/%s',this.ApplyConfigurationSubChannel,uniqueID), @(config)this.applyConfiguration(config));
            this.Subscriptions{2} = message.subscribe(sprintf('%s/%s',this.SimSettingChannel,uniqueID), @(clientData)this.applySimSetting(clientData));
            this.Subscriptions{3} = message.subscribe(sprintf('%s/%s',this.ShortcutGetCollectRunNameChannel,uniqueID), @(clientData, e)this.publishRunName(clientData, 'collect'));
            this.Subscriptions{4} = message.subscribe(sprintf('%s/%s',this.ShortcutGetVerifyRunNameChannel,uniqueID), @(clientData, e)this.publishRunName(clientData, 'verify'));
            this.Subscriptions{5} = message.subscribe(sprintf('%s/%s',this.ProposalOptionChannel,uniqueID), @(clientData, e)this.updateProposalOptions(clientData));
            this.Subscriptions{6} = message.subscribe(sprintf('%s/%s',this.RangeModeUpdateChannel, uniqueID), @(clientData) this.updateRangeModeSelection(clientData));
            this.Subscriptions{7} = message.subscribe(sprintf('%s/%s',this.ImportSimulationInputsSubChannel, uniqueID), @(clientData) this.importSimulationInputsFromWorkspace(clientData));
            this.Subscriptions{8} = message.subscribe(sprintf('%s/%s',this.ToleranceSettingsSubscribeChannel, uniqueID), @(clientData) this.publishSystemBehaviorSettings(clientData));
            this.Subscriptions{9} = message.subscribe(sprintf('%s/%s',this.UpdateTolerancesStoreSubscribeChannel, uniqueID), @(clientData) this.updateSignalTolerances(clientData));
        end
        
        function applyConfiguration(this, config)
            % Get the FPT instance on which the settings are being made
            fptInstance = fxptui.FixedPointTool.getExistingInstance;
            
            % Get the shortcut to be applied
            shortcutName = this.getShortcutName(config.configSetting.label);
            fptInstance.getShortcutManager.applyShortcut(shortcutName);
            
            if ~isempty(config.runName)
                set_param(fptInstance.getModel,'FPTRunName',config.runName);
            end
        end
        
        function applySimSetting(this, clientData)
            % Initialize a callbackHandler for top model
            callbackHandler = fxptui.Web.CallbackService.CallbackHandler(this.Model);
            
            % Turn off fast restart in order to apply MMOArchiveMode settings
            callbackHandler.getFastRestartHandler.turnOffFastRestart;
            
            if clientData.mergeOption
                set_param(this.Model,'MinMaxOverflowArchiveMode','merge');
            else
                set_param(this.Model,'MinMaxOverflowArchiveMode','overwrite');
            end
            
            % Restore fast restart settings on the model
            callbackHandler.getFastRestartHandler.restoreFastRestart;
        end
        
        function publishRunName(this, clientData, phase)
            fptInstance = fxptui.FixedPointTool.getExistingInstance;
            mgr = fptInstance.getShortcutManager;
            runName = '';
            shortcutName = this.getShortcutName(clientData.name);
            if ~isempty(shortcutName)
                runName = mgr.getRunNameForShortcut(shortcutName);
            end
            if isempty(runName)
                runName = get_param(this.Model,'FPTRunName');
            end
            if strcmpi(phase, 'collect')
                message.publish(this.CollectRunNamePublishChannel,runName);
            else
                message.publish(this.VerifyRunNamePublishChannel,runName);
            end
        end
        
        function publishUpdateCustomShortcuts(this, ~, eventdata)
            msgObj = struct('customShortcut',{eventdata.CustomShortcut}, 'action',eventdata.Action);
            message.publish(this.UpdateCustomShortcutsPublishChannel, msgObj);
        end
        
        function updateRangeModeSelection(this, clientData)
            fpt = fxptui.FixedPointTool.getExistingInstance;
            if ~isempty(fpt)
                shortcutManager = fpt.getShortcutManager;
                shortcutName = this.getShortcutName(clientData.name);
                if strcmpi(clientData.stage, 'rangecollection')
                    shortcutManager.setIdealizedShortcut(shortcutName);
                else
                    shortcutManager.setVerifyShortcut(shortcutName);
                end
            end
        end
    end
    methods(Access = {?fxptui.Web.WorkflowController, ?SharedSetupForWebAppTests, ?fxptui.Web.CallbackService.ApplyHandler})
        function enableVerifySection(this, flag)
            % Enables verify section in the toolstrip
            
            msgObj = flag;
            message.publish(this.EnableVerifyActionChannel, msgObj);
            
            this.LastUsedChannel  = this.EnableVerifyActionChannel;
            this.LastUsedData = msgObj;
        end
        
        function enableSection = canEnableVerifySection(this)
            % Check if verify section can be enabled in toolstrip
            
            enableSection = false;
            dataLayer = fxptds.DataLayerInterface.getInstance();
            if ~isempty(dataLayer)
                facade = dataLayer.getWorkflowTopologyFacade(this.Model);
                allNodes = facade.query(this.Model, 'type', 'Design');
                prepareAction = facade.query(this.Model, 'type', 'Prepare', 'limit', 1);
                
                % If number of design nodes other than the root node is
                % atleast 1, then enable sim embedded (verify) button in FPT
                nonRootNodes = setdiff(allNodes, this.Model);
                if isempty(prepareAction{1})
                    enableSection = numel(nonRootNodes) > 0;
                else
                    enableSection = numel(nonRootNodes) > 1;
                end
            end
        end
        
        function proposalOptions = getProposalOptions(this, featureFlag)
            % if 'fxpoptInFPT' is turned OFF, use ApplicationData. Otherwise, read
            % proposal options from workflow settings
            if featureFlag == 10
                appData = SimulinkFixedPoint.getApplicationData(this.Model);
                proposalOptions = appData.AutoscalerProposalSettings;                
            else
                workflow = this.getCurrentWorkflow();
                proposalSetting = workflow.WorkflowSetting.getSettingValue('ProposalSetting');
                proposalOptions = proposalSetting.getSettings();
            end
        end
        
        function status = isIterativeConversion(this)
            % determine if the current workflow is iterative conversion
            workflow = this.getCurrentWorkflow();
            intent = fxptui.internal.WorkflowIntent.Null;
            if ~isempty(workflow)
                intent = workflow.WorkflowIntent;
            end
            status = isequal(intent, fxptui.internal.WorkflowIntent.IterativeConversion);
        end
        
        function workflow = getCurrentWorkflow(~)
            % get current workflow from the data layer
            dataLayer = fxptds.DataLayerInterface.getInstance();
            fptInstance = fxptui.FixedPointTool.getExistingInstance;
            model = fptInstance.getModel;
            workflow = dataLayer.getCurrentWorkflow(model);
        end
        
        function serverProposalOptions = packageProposalOptions(~, proposalOptions)
            % package the proposalOptions for client consumption
            serverProposalOptions.isAutoSignedness = proposalOptions.isAutoSignedness;
            serverProposalOptions.isWLSelectionPolicy = proposalOptions.isWLSelectionPolicy;
            serverProposalOptions.ProposeForInherited = proposalOptions.ProposeForInherited;
            serverProposalOptions.ProposeForFloatingPoint = proposalOptions.ProposeForFloatingPoint;
            serverProposalOptions.DefaultWordLength = proposalOptions.DefaultWordLength;
            serverProposalOptions.DefaultFractionLength = proposalOptions.DefaultFractionLength;
            serverProposalOptions.isUsingSimMinMax = proposalOptions.isUsingSimMinMax;
            serverProposalOptions.isUsingDerivedMinMax = proposalOptions.isUsingDerivedMinMax;
            serverProposalOptions.SafetyMarginForSimMinMax = proposalOptions.SafetyMarginForSimMinMax;
        end
    end
    methods(Hidden)
        % Test only methods
        function data = getLastUsedData(this)
            data = this.LastUsedData;
        end
        function channelInfo = getLastUsedChannel(this)
            channelInfo = this.LastUsedChannel;
        end
    end
end

% LocalWords:  fpt customshortcutinfo collectrun Nameinfo verifyrun Sgl Scl
% LocalWords:  Settinginfo lbl Fxpt DTOMMO MMO rangecollection codeview
