classdef SimulationHelper < handle
    %SIMULATIONHELPER App-authoring utilities for genapp-generated apps.
    %
    %   When an app is generated using the genapp command, one of a set of
    %   predefined templates is used to generate the main scaffold for the
    %   app's code and the associated user interface. App maintainer may
    %   still want to customize its functionality by adding more controls
    %   and performing some tasks programmatically before being ready for
    %   final deployment. SimulationHelper is a convenience class for
    %   performing tasks related to:
    %
    %   - user interface
    %   - input/output
    %   - app's workspace variables
    %   - binding user interface components to SimulationInput
    %
    %   SimulationHelper makes it easy to access utility functions in the
    %   above categories by providing the following four public properties,
    %   respectively:
    %
    %   - UserInterface
    %   - InputOutput
    %   - Workspace
    %   - Binder
    %
    %   Usage:
    %   
    %   The generated .mlapp file from a genapp command call can be edited
    %   in App Designer. In Code View, App maintainer can use the
    %   predefined SimulationHelper property on the app object to access 
    %   SimulationHelper.
    %
    %   Examples:
    %
    %   The following are some examples which show how to use the various
    %   utilities available in every category: 
    %
    %   Example 1 - InputOutput - load a MAT-file into the app's workspace:
    %       app.SimulationHelper.InputOutput.loadMATFile("input.mat");
    %
    %   Example 2 - Binder - Bind a control to loaded external input sets:
    %       app.SimulationHelper.Binder.bindToExternalInputSets(...
    %           app.SelectExtInpsSetDropDown);
    %
    %   Example 3 - Workspace - Get external input set names:
    %       setNames = ...
    %           app.SimulationHelper.Workspace.externalInputsDataSetNames();
    %
    %   Example 4 - UserInterface - Plot selected external input signals:
    %       app.SimulationHelper.UserInterface.plotSelectedExternalInputSignals(...
    %            app.SelectExtInpSignalsToPlotListBox, app.UIAxes);
    %
    %   SimulationHelper itself provides two important methods:
    %
    %    - initializeApp - set model name, input MAT-file, image file, etc.
    %    - simulate - run the simulation (with auto-created or provided
    %    SimulationInput)
    %
    %   Example 5 - Initializing the app and running the simulation:
    %       app.SimulationHelper.initializeApp(varargin{:});
    %       ...
    %       app.simulate();
    %
    %   See also Binder, InputOutput, Workspace, UserInterface,
    %   simulink.compiler.genapp, Simulink.SimulationInput 
    
    %#function Simulink.standalone_start_simulink
    
    %   Copyright 2020 The MathWorks, Inc.
    
    properties (SetAccess = private)
        App
        ModelName
        
        InputOutput
        UserInterface
        Workspace
        Binder
    end
    
    methods
        function obj = SimulationHelper(app)
            %SIMULATIONHELPER Create a SimulationHelper object.
            %
            %   SimulationHelper takes an app object (a child of
            %   matlab.apps.AppBase) to act on. The individual utility
            %   classes, that SimulationHelper makes available, use this
            %   object to perform their respective tasks.
            
            arguments
                app (1,1) matlab.apps.AppBase
            end
            
            obj.App = app; 
        end
        
        function initializeApp(obj, options)
            %INITIALIZEAPP Set model name, input MAT-file, image file, and
            %load data.
            %
            %   initializeApp accepts the following options which are
            %   needed for app initialization:
            %
            %   ModelName (string) - name of the model to use in simulation
            %   InputMATFileName (string) - MAT-file with inputs to load
            %   ModelImage (string) - name of the model image file
            %
            %   If none of these options are provided, initializeApp uses
            %   default values which are auto-generated during app
            %   generation.
            %
            %   initializeApp also loads the input MAT-file into the app's
            %   workspace. Loaded variables become available to other
            %   utility classes provided by SimulationHelper through the
            %   SimulationHelper.Workspace object.
            %
            %   See also Workspace, InputOutput
            arguments
                obj
                options.ModelName (1,1) string {mustBeValidVarName} = obj.App.defaultModelName()
                options.InputMATFileName (1,1) string {mustBeInAppDir(obj, options.InputMATFileName)} = ...
                    obj.App.defaultInputMATFileName()
                options.ModelImage (1,1) string {mustBeInAppDir(obj, options.ModelImage)} = ...
                    obj.App.defaultModelImage()
            end
            
            obj.initialize();
            
            app = obj.App;
            
            load_simulink;
            
            % Initialize app data
            obj.ModelName = convertStringsToChars(options.ModelName);
            app.SimApp.Name = metaclass(app).Name;
            
            % Set the top-left image (model thumbnail)
            app.Image.ImageSource = options.ModelImage;
            
            % Load the input data (ICs, extInpsSets, mdlPrmsSet and refWkspPrmsSets)
            if ~isempty(options.InputMATFileName) && exist(options.InputMATFileName, 'file')
                obj.InputOutput.loadMATFile(options.InputMATFileName);
            end
        end
        
        function simulate(obj, simulationInput)
            %SIMULATE Run the simulation for the underlying model.
            %
            %   SIMULATE optionally accepts a SimulationInput object as an
            %   argument. If one is provided, no bindings are applied. If a
            %   SimulationInput is not provided, it creates one using the 
            %   data loaded from the input MAT-file and applies any
            %   bindings defined by the app maintainer in the app's .mlapp
            %   file.
            %
            %   If there is a bound component for capturing app status
            %   updates, SIMULATE will update it with an appropriate
            %   message at each stage.
            %
            %   SIMULATE uses the SimulationInput to run the simulation and
            %   will either display a success message at the end or report
            %   an error if anything goes wrong with the simulation.
            %
            %   See also Binder, Simulink.SimulationInput
            arguments
                obj
                simulationInput Simulink.SimulationInput = Simulink.SimulationInput.empty()
            end
            
            if isempty(simulationInput)
                obj.InputOutput.createSimulationInput();
            else
                obj.InputOutput.SimulationInput = simulationInput;
            end

            simulatingMessage = message(...
                'simulinkcompiler:genapp:Simulating').getString;

            obj.UserInterface.setStatusMessage(simulatingMessage);
            
            try
                obj.InputOutput.SimulationOutput = sim(obj.InputOutput.SimulationInput);

                [~, ~, sigNames] = obj.InputOutput.extractTimeSeriesFromLoggedDatasets();

                if isempty(sigNames)
                    msg = message('simulinkcompiler:genapp:NoLoggedSignalsFound').string;
                else
                    msg = message('simulinkcompiler:genapp:SelectLoggedSignalsToPlot').string;
                end

                doneSimulatingMessage = message(...
                    'simulinkcompiler:genapp:DoneSimulating').string;

                obj.UserInterface.setStatusMessage(doneSimulatingMessage + " " + msg);
            catch ME
                simulationFailedMessage = message(...
                    'simulinkcompiler:genapp:SimulationFailed').getString;
         
                obj.UserInterface.setStatusMessage(simulationFailedMessage);
                obj.UserInterface.reportError(ME);
            end
        end
    end
    
    methods (Hidden)
        function initialize(obj)
            obj.InputOutput = simulink.compiler.app.InputOutput(obj);
            obj.UserInterface = simulink.compiler.app.UserInterface(obj);
            obj.Workspace = simulink.compiler.app.Workspace(obj);
            obj.Binder = simulink.compiler.app.Binder(obj);
        end
    end
    
    methods (Access = private)
        function mustBeInAppDir(obj, fileName)
            filePathInAppDir = obj.pathInAppDir(fileName);

            if ~isfile(filePathInAppDir)
                error(message('simulinkcompiler:genapp:FileNotFoundInAppDirectory'));
            end
        end
        
        function path = pathInAppDir(obj, fileName)
            appName = metaclass(obj.App).Name;
            appDir = fileparts(which(appName));
            path = fullfile(appDir, fileName);
        end
    end
end

function mustBeValidVarName(nameToCheck)
    if ~isvarname(nameToCheck)
        error(message('simulinkcompiler:genapp:ModelNameInvalid'));
    end
end

% LocalWords:  ICs Inps Prms Wksp genapp app's mlapp Inp INITIALIZEAPP
