classdef (Hidden) ModelReductionApp < handle
    % Model Reduction App
    % compatible with MATLAB Online       
    
    % Author(s): Suat Gumussoy
    % Copyright 2016 The MathWorks, Inc.
    
    properties (SetAccess = private, GetAccess = public, SetObservable)
        % Data
        Models
    end
    properties (Access = private)
        % App Container
        AppContainer
        AppContainerListener
        
        % Tabs
        ReduceSystemTab
        PlotTab
        
        % Data Browser Manager
        DataBrowserManager
        ModelBrowser
        
        % Event Manager
        EventManager
        
        % Plot Manager
        PlotManager
        
        % Dialogs
        ImportModelsDialog
        ExportModelsDialog
        
        % Tools (Cell array)
        ReductionTool
        
        % Data, Plot Manager, Status Bar, ToolGroup
        StatusBar
        
        % everything except Plot Manager and Tool status 
        IsDirty = false;
        % flag for loading a session
        Loading = false;
    end
    properties (Access = private, Transient)
        ExportCompletedListener        
    end
    methods
        %% Constructor
        function this = ModelReductionApp(ModelList)
            if nargin == 1
                importModels(this, ModelList)
            end            
                        
            % Create App Container
            if mrtool.util.MRToolstripSettings.isNewAPI
                this.AppContainer = matlab.ui.internal.desktop.ToolGroup(getAppTitle(this),'modelreducer');             
            else
                % Set ToolGroup Name by appending a temp name at the end
                tmpName = tempname;
                idx     = regexp(tmpName,filesep);
                tmpName = tmpName(idx(end)+1:end);
                this.AppContainer = toolpack.desktop.ToolGroup(...
                    sprintf('ModelReductionApp(%s)',tmpName), ...
                    getAppTitle(this));
            end
            
            % Eventmanager
            this.EventManager = controllib.app.managers.eventmanager.internal.AppEventManager(this.AppContainer);
            % this.EventManager.StatusBar.ParentTool = this.AppContainer.Name;
            % createWidgets(this.EventManager, this.AppContainer);                
            
            % Create Plot Manager
            this.PlotManager = mrtool.PlotManager(this);
            
            % Permanent Tabs
            createPermanentTabs(this)
            setContextualHelpCallback(this.AppContainer,@(es,ed) mrtool.guiutil.mrToolHelp('ModelReducerGeneral'));
            
            % DataBrowser
            addDataBrowserManager(this)
            
            % Connect Plot Tab to Data Browser
            setDataBrowserComponent(this.PlotTab,this.ModelBrowser)
                                    
            notify(this,'ModelsUpdated');

            % Install client listeners
            installListeners(this);
            
            % Show GUI
            show(this);
            
            % Approval required to close tool
            setClosingApprovalNeeded(this.AppContainer,true); 
            setDirty(this,false);
        end       
        function installListeners(this)
            % Group events
            this.AppContainerListener = addlistener(this.AppContainer,'GroupAction',@(es,ed) groupCallback(this,es,ed));
        end
        function groupCallback(this,es,ed)
            if strcmp(ed.EventData.EventType,'CLOSING') && es.isClosingApprovalNeeded  
                if isAppDirty(this)
                    askForSaveSession(this);
                else
                    this.AppContainer.approveClose;
                end
            end
            if strcmp(ed.EventData.EventType,'CLOSED')
                close(this);
            end
        end              
        function PlotManager = getPlotManager(this)
            PlotManager = this.PlotManager;
        end        
        function AppContainer = getAppContainer(this)
            AppContainer = this.AppContainer;
        end   
        function EventManager = getEventManager(this)
            EventManager = this.EventManager;
        end
        function addFigure(this, fig)
            addFigure(this.AppContainer,fig);
        end     
        
        %% Load/Save Session 
        % load
        function promptForLoadSession(this)
            [filename, pathname] = uigetfile( ...
                {'*.mat';'*.*'}, ...
                getString(message('Control:mrtool:OpenMRSession')));
            if ~isequal(filename,0) && ~isequal(pathname,0)
                % load the session file
                SessionFile = fullfile(pathname,filename);
                try
                    S = mrtool.util.validateSessionFile(SessionFile);
                catch ME
                    mrtool.util.openJavaModalErrorDialog(getToolGroup(this),ME.message);
                    return;
                end                    
             
                %% preload the session
                preLoadSession(this,S.ModelReducerSession,mrtool.util.extractFileName(filename));
            end
        end
        function preLoadSession(this,SessionData,filename)
            % if tool is clean, open the session data on the existing app
            % if not, open the session data on a new app
            if ~isAppDirty(this)
                setWaiting(this,true);
                postActionStatus(this.EventManager, 'on',mrtool.util.convertStr2Html(getString(message('Control:mrtool:StatusMessageOpeningSession',filename))));
                this.Loading = true;
                loadSession(this,SessionData);
                % set waiting bar and message
                setWaiting(this,false);
                postActionStatus(this.EventManager,'off',mrtool.util.convertStr2Html(getString(message('Control:mrtool:StatusMessageOpenedSession',filename))));
            else
                preLoadSession(modelReducer(),SessionData,filename);
            end
        end
        function loadSession(App,SessionData)
            % load models            
            App.Models=[];
            importModels(App,SessionData.Models);
            setAppDirty(App,false); 
            % load tools
            ToolSessionData = SessionData.Tools;          
            for ct=1:numel(ToolSessionData)
                Tool = openToolWithoutPostingMessage(App,ToolSessionData{ct}.ToolType,ToolSessionData{ct}.Target);
                loadSession(Tool,ToolSessionData{ct});
                setDirty(Tool,false);                
            end
            % load plot manager
            loadSession(App.PlotManager,SessionData.PlotManager);
            loadSession(App.PlotTab);
            drawnow;
            % let others events to finish
            drawnow;
            setAppDirty(App,false);            
        end
        % save
        function askForSaveSession(this)
            selection = questdlg(...
                getString(message('Control:mrtool:SaveSessionQuestion')), ...
                getString(message('Control:mrtool:SaveSessionTitle')), ...
                getString(message('Control:mrtool:Yes')),...
                getString(message('Control:mrtool:No')),...
                getString(message('Control:mrtool:Cancel')),...
                getString(message('Control:mrtool:Yes')));
            switch selection
                case getString(message('Control:mrtool:Yes'))
                    promptForSaveSession(this,true);
                case getString(message('Control:mrtool:No'))
                    this.AppContainer.approveClose;
                case getString(message('Control:mrtool:Cancel'))
                    this.AppContainer.vetoClose;
                otherwise
                    this.AppContainer.vetoClose;
            end
        end         
        function promptForSaveSession(this,close)
            [filename, pathname] = uiputfile( ...
                {'*.mat';'*.*'}, ...
                getString(message('Control:mrtool:SaveMRSession')), ...
                getString(message('Control:mrtool:SessionName')));
            if ~isequal(filename,0) && ~isequal(pathname,0)
                ModelReducerSession = saveSession(this);
                save(fullfile(pathname, filename), getString(message('Control:mrtool:SessionName')));
                postActionStatus(this.EventManager,'off',getString(message('Control:mrtool:StatusMessageSavedSession',mrtool.util.extractFileName(filename))));
                if close
                    this.AppContainer.approveClose;
                else
                    return;
                end
            else
                if close
                    this.AppContainer.vetoClose;
                else
                    return;
                end
            end
        end                   
        function SessionData = saveSession(this)
            SessionData = mrtool.data.SessionData;
            % save session models
            SessionData.Models = this.Models;
            Tools = getReductionTools(this);
            % save session for each open tool
            for ct=1:length(Tools)
                SessionData.Tools{ct,1} = saveSession(Tools{ct});
            end
            % save session for plot manager
            SessionData.PlotManager = saveSession(this.PlotManager);
            setAppDirty(this,false);
        end 
            
        %% Export/Import/Rename Models
        function showImportDialog(this,Anchor)
            if nargin == 1
                Anchor = this.getWidgets.Tabs.ReduceSystemTab.ImportExportSection.ImportButton;
            end
            if isempty(this.ImportModelsDialog)
                this.ImportModelsDialog = mrtool.dialogs.ImportModelDialog(this);
            end
            if mrtool.util.MRToolstripSettings.isNewAPI                
                showTearOffDialog(Anchor,getDialog(this.ImportModelsDialog),true);
                updateUI(this.ImportModelsDialog);
            else
                this.ImportModelsDialog.show(Anchor);
            end
        end                
        function importModels(this, data, isReducedModel)
            if nargin<3
                isReducedModel = false;
            end
            for ct = 1:length(data)
                label = data(ct).Name;
                if isReducedModel
                    % check the word at the end is "Reduced"
                    newlabel = [label 'Reduced' mat2str(order(data.System))];
                else
                    newlabel = label;
                end
                if ~isempty(this.Models)                                      
                    label1 = matlab.lang.makeUniqueStrings(newlabel, {this.Models.Label});
                    if ~strcmpi(label,label1)
                        data(ct).Label = label1;                        
                    end
                end
                this.Models = [this.Models; data(ct)];
            end   
            updateToolModelList(this);
            notify(this,'ModelsUpdated');
            if ~this.Loading % ignore while loading
                setDirty(this,true);
            end
        end        
        function removeModel(this,ModelArray)
            % ask whether to remove or cancel            
            for ix=1:length(ModelArray)
                Tools = getReductionTools(this);
                Model = ModelArray(ix);
                flag = zeros(size(Tools));
                for ct=1:length(Tools)
                    flag(ct)=isTarget(Tools{ct},Model);
                end
                DeleteModel = true;
                if any(flag)
                    % ask question to remove tab
                    selection = askRemoveModelBeforeRemoveTab(this,Model);
                    switch selection
                        case getString(message('Control:mrtool:Yes'))
                            DeleteModel = true;
                        case getString(message('Control:mrtool:No'))
                            DeleteModel = false;
                        case getString(message('Control:mrtool:Cancel'))
                            DeleteModel = false;
                    end
                end
                if DeleteModel
                    this.Models(this.Models==Model) = [];
                    delete(Model);
                    notify(this,'ModelsUpdated');
                    % no open tab, we can delete safely
                    updateToolModelList(this);
                    setDirty(this,true);
                end
            end            
        end
        function renameModel(this,newname,row)
            Model = this.Models(row);
            oldname = Model.Label;            
            Model.Label = newname;
            
            % renamed oopen tools
            for ct=1:length(this.ReductionTool)
                Tool = this.ReductionTool{ct};
                if isTarget(Tool,Model)
                    setTarget(Tool,Model);
                else
                    setTargetList(Tool,this.Models)
                end                
            end
                        
            % update plots
            eventData = ctrluis.toolstrip.dataprocessing.GenericEventData(struct('NewName',newname,'OldName',oldname,'Model',Model));
            this.notify('ModelNameChanged',eventData);            
        end
        function selection = askRemoveModelBeforeRemoveTab(this,Model)
            % Explain the user that deleteing a model removes all the
            % relevant tabs.
            selection = questdlg(...
                 getString(message('Control:mrtool:WarningRemoveModel',Model.Name,Model.Name)), ...
                 getString(message('Control:mrtool:WarningRemoveModelTitle')), ...
                 getString(message('Control:mrtool:Yes')),...
                 getString(message('Control:mrtool:No')),...
                 getString(message('Control:mrtool:Cancel')),...
                 getString(message('Control:mrtool:Yes')));
        end        
        function updateToolModelList(this)
            Tools = getReductionTools(this);
            for ct=1:length(Tools)
                updateTargetList(Tools{ct},this.Models)                
            end
        end 
        function Tools = getReductionTools(this)
            Tools = this.ReductionTool;
        end
        function showExportDialog(this,Anchor)
            if nargin == 1
                Anchor = this.getWidgets.Tabs.ReduceSystemTab.ImportExportSection.ExportButton;
            end
            if isempty(this.ExportModelsDialog)
                this.ExportModelsDialog = mrtool.dialogs.ExportModelDialog(this);
                this.ExportCompletedListener  = addlistener(this.ExportModelsDialog,'ExportCompleted',@(es,ed) cbExportCompletedPostMessage(this,es,ed));
            end
            if mrtool.util.MRToolstripSettings.isNewAPI
                showTearOffDialog(Anchor,getDialog(this.ExportModelsDialog),true);
                updateUI(this.ExportModelsDialog);
            else
                this.ExportModelsDialog.show(Anchor);
            end                        
        end
        function cbExportCompletedPostMessage(this,~,ed)
            Sentence = mrtool.util.createModelNameSentence({ed.Data.ExportedVariables});
            if length({ed.Data.ExportedVariables})>1
                Msg = getString(message('Control:mrtool:StatusMessageExportModels',Sentence));
            else
                Msg = getString(message('Control:mrtool:StatusMessageExportModel',Sentence));
            end
            postActionStatus(this.EventManager, 'off',mrtool.util.convertStr2Html(Msg));
        end       
        %% Model Reduction Tools
        function openBalancedTruncationTool(this,Model)
            openTool(this,'BalancedTruncation',Model);
        end
        function openModeSelectionTool(this,Model)        
            openTool(this,'ModeSelection',Model);
        end
        function openPoleZeroSimplificationTool(this,Model)       
            openTool(this,'PoleZeroSimplification',Model)
        end
        function openTool(this,MethodName,Model)
            setWaiting(this,true);                       
            switch MethodName
                case 'BalancedTruncation'
                    Msg = getString(message('Control:mrtool:StatusMessageOpenBTTool'));
                case 'ModeSelection'
                    Msg = getString(message('Control:mrtool:StatusMessageOpenMSTool'));
                case 'PoleZeroSimplification'
                    Msg = getString(message('Control:mrtool:StatusMessageOpenPZTool'));
            end 
            postActionStatus(this.EventManager,'on',mrtool.util.convertStr2Html(Msg));
            for ct=1:length(Model)
                openToolWithoutPostingMessage(this,MethodName,Model(ct));
            end
            clearActionStatus(this.EventManager);
            setWaiting(this,false); 
        end
        function Tool = openToolWithoutPostingMessage(this,MethodName,Model,loadingFlag)
            if nargin <4
                loadingFlag = false;
            end
            switch MethodName
                case 'BalancedTruncation'
                    Tool = mrtool.tools.BalancedTruncationTool(Model,this.Models,getToolGroup(this),getEventManager(this));
                case 'ModeSelection'
                    Tool = mrtool.tools.ModeSelectionTool(Model,this.Models,getToolGroup(this),getEventManager(this));
                case 'PoleZeroSimplification'
                    Tool = mrtool.tools.PoleZeroSimplificationTool(Model,this.Models,getToolGroup(this),getEventManager(this));
            end
            addToolListeners(this,Tool);
            addNewReductionTool(this,Tool); 
            if ~loadingFlag
                setDirty(this,true)
            end
        end
        function addNewReductionTool(this,NewTool)
            this.ReductionTool = [this.ReductionTool;{NewTool}];            
        end            
        function addToolListeners(this,NewTool)
            addlistener(NewTool,'CreateReducedModel',@(es,ed) createReducedModel(this,es,ed));    
            addlistener(NewTool,'ObjectBeingDestroyed', @(es,ed) deleteTool(this));
        end
        function deleteTool(this)
            Tools = getReductionTools(this);
            ix = cellfun(@(x) ~isvalid(x),Tools);
            this.ReductionTool(ix)=[];
        end
        function createReducedModel(this,~,ed)
            importModels(this,ed.Data.ReducedModel,true);
            postActionStatus(this.EventManager, 'off',mrtool.util.convertStr2Html(getString(message('Control:mrtool:StatusMessageReducedSystemCreated',ed.Data.ReducedModel.Name))));
        end
        function Model = getSelectedModelinDataBrowser(this)
            Model = getSelectedData(this.ModelBrowser);            
        end        
        function Model = getSelectedModel(this)
            % return either selected model on data browser or first model
            % by default
            Model = getSelectedModelinDataBrowser(this);
            % if no model selected, return first one
            if isempty(Model)
                Model = this.Models;
                if isempty(Model)
                    Model = [];
                else
                    Model = Model(1);                
                end
            end
        end     
        %% Open/Close/Show/Delete
        function show(this)
            open(this.AppContainer);
        end
        function close(this)
            delete(this);
        end
        function delete(this)
            % Delete managers
            delete(this.PlotManager);
            delete(this.DataBrowserManager);
            delete(this.EventManager);
            % Delete data browser
            delete(this.ModelBrowser);
            delete(this.ExportCompletedListener);
            % Delete tabs
            delete(this.ReduceSystemTab);
            delete(this.PlotTab);            
            % Delete toolgroup
            delete(this.AppContainerListener);
            setClosingApprovalNeeded(this.AppContainer,false);
            delete(this.AppContainer);
        end                
        function TG = getToolGroup(this)
            TG = this.AppContainer;
        end
    end
    methods (Hidden)
        function flag = isAppDirty(this)
            % Dirty actions:
            % - import or create, remove models (update in Models)
            % - opened tools
            % - create plot
            
            flagApp = getDirty(this); % import/create/remove models
            flagTool = false; % any open tool
            for ct=1:length(this.ReductionTool)
                flagTool = flagTool || getDirty(this.ReductionTool{ct});
            end
            flagPlot = getDirty(this.PlotManager); % any open plot
            
            flag = flagApp || flagTool || flagPlot;
        end
        function setDirty(this,flag)
            this.IsDirty = flag;
        end
        function Flag = getDirty(this)
            Flag = this.IsDirty;
        end
        function setAppDirty(this,flag)
            if islogical(flag)
                setDirty(this,flag);
                for ct=1:length(this.ReductionTool)
                    setDirty(this.ReductionTool{ct},flag);
                end
                setDirty(this.PlotManager,flag);
            end
        end
        function Widgets = getWidgets(this)
            %% APP CONTAINER, TOOLGROUP AND STATUS BAR
            Widgets.AppContainer = getAppContainer(this);
            Widgets.StatusBar = getStatusBar(this.EventManager);
            Widgets.ModelBrowser = this.ModelBrowser;
            %% TOOLS
            Tools = getReductionTools(this);
            for ct=1:length(Tools)
                % Each open tool has data, tab, plot
                % data has its get/set functions
                % tab has its own getWidgets function
                % plot has its own getWidgets function to access each plot inside a document
                Widgets.OpenTools{ct,1} = getWidgets(Tools{ct});                                                    
            end
            %% IMPORT/EXPORT DIALOGS
            if isempty(this.ImportModelsDialog)
                Widgets.Dialogs.Import = [];
            else
                Widgets.Dialogs.Import = getWidgets(this.ImportModelsDialog);
            end
            if isempty(this.ExportModelsDialog)
                Widgets.Dialogs.Export = [];
            else
                Widgets.Dialogs.Export = getWidgets(this.ExportModelsDialog);
            end
            %% REDUCE SYSTEM AND PLOT TABS
            Widgets.Tabs.ReduceSystemTab = getWidgets(this.ReduceSystemTab);
            Widgets.Tabs.PlotTab = getWidgets(this.PlotTab);
            %% MANAGERS 
            Widgets.Managers.DataBrowser = this.DataBrowserManager;
            Widgets.Managers.Event = this.EventManager;
            Widgets.Managers.Plot = this.PlotManager;          
        end 
        function setWaiting(this,flag,comps)
            if nargin>2
                setWaiting(this.AppContainer,flag,comps);
            else
                setWaiting(this.AppContainer,flag);
            end
        end          
    end
    methods (Access = private)
        function ToolTitle = getAppTitle(this)
            % Get App Title
            ToolTitle =  getString(message('Control:mrtool:toolTitle'));
        end        
        function createPermanentTabs(this)
            %% Create Permenant Tabs            
            if mrtool.util.MRToolstripSettings.isNewAPI
                this.ReduceSystemTab = mrtool.tabs.ReduceSystemTabMLOnline(this);
                this.PlotTab = mrtool.tabs.PlotTabMLOnline(this);
                tabgroup = matlab.ui.internal.toolstrip.TabGroup();
                tabgroup.add(this.ReduceSystemTab.getTab());
                tabgroup.add(this.PlotTab.getTab());
                this.AppContainer.addTabGroup(tabgroup);                
            else
                % Home Tab
                this.ReduceSystemTab = mrtool.tabs.ReduceSystemTab(this);
                add(this.AppContainer,getTab(this.ReduceSystemTab),1);
                
                % Plots Tab
                this.PlotTab = mrtool.tabs.PlotTab(this);
                add(this.AppContainer,getTab(this.PlotTab),2);
            end
        end        
        function addDataBrowserManager(this)
            %% Add Data Browser Manager
            
            % Create browser
            Browser = controllib.app.databrowser.internal.AppDataBrowser();
            this.DataBrowserManager = Browser;            
            comp = mrtool.ModelBrowser(this);
            this.ModelBrowser = comp;
            Browser.addComponent(comp);
            Browser.addPreviewComponent();
            
            % Add to app container
            setDataBrowser(this.AppContainer,getDataBrowserPanel(this.DataBrowserManager));
        end             
    end
    events
        ModelsUpdated
        ModelNameChanged
    end
end
