classdef(Hidden,Sealed) ImportDialog < handle
    %IMPORTDIALOG Dialog for importing data in to the econModeler app
    
    %Copyright 2017-2019 The MathWorks Inc.
    
    properties(Access = private)
        Session
        DataSource
        Figure
        Table
        ImportButton
        CancelButton
        FilePath
        Listeners
    end
    
    methods(Access = ?internal.EconModeler.DialogFactory)
        function this = ImportDialog(session, datasource)
            this.Session = session;
            
            this.DataSource = datasource;
            
            try
                tableData = this.findVariableInfo();
            catch ME
                uialert(session.App,ME.message,...
                    getString(message('econ:econModeler:AppTitle')),...
                    'Icon','error','Modal',true);
                delete(this);
                return;
            end

            if isempty(tableData)
                %This indicates that the file dialog was closed with no
                %selection
                delete(this)
                return;
            end
            
            %Create and center the figure
            this.Figure = uifigure('Name', getString(message('econ:econModeler:ImportFigName')),...
                'Position', [100 100 494 287],...
                'CloseRequestFcn',@(s,e)cleanup(this),...
                'Tag','importFigure',...
                'UserData',@(x)this.qeCheckImportItem(x),...
                'WindowStyle','modal',...
                'Visible','off');

            matlab.graphics.internal.themes.figureUseDesktopTheme(this.Figure);
            
            grid = uigridlayout(this.Figure,[2 1],'RowHeight',{'1x','fit'});
            
            %Place panel
            panel = uipanel(grid,'Title',getString(message('econ:econModeler:ImportPanelName')),...
                'FontSize',12,...
                'Tag','importPanel');
            panel.Layout.Row = 1;
            panelGrid = uigridlayout(panel,[1 1]);
            %Create the table
            columnNames = {getString(message('econ:econModeler:ImportTableCheckColumnName')),...
                getString(message('econ:econModeler:ImportTableNameColumnName')),...
                getString(message('econ:econModeler:ImportTableValueColumnName'))};
            this.Table = uitable(panelGrid,'Data',tableData,...
                'ColumnName',columnNames,...
                'RowName',[],...
                'ColumnFormat',{'logical','char','char'},...
                'ColumnEditable',[true false false],...
                'Tag','importTable',...
                'CellEditCallback',@(s,e)this.checkForSelection(s,e));
              
            bottomRow = uigridlayout(grid,[1 3],'ColumnWidth',{'1x','fit','fit'});
            %Create the Import and Cancel Buttons
            this.ImportButton = uibutton(bottomRow,'push',...
                'Position',[243 23 100 22],...
                'Text',getString(message('econ:econModeler:Import')),...
                'Tag','importButton',...
                'ButtonPushedFcn',@(s,e)importSelectedData(this,datasource),...
                'Enable','off');
            this.ImportButton.Layout.Column = 2;
            
            this.CancelButton = uibutton(bottomRow,'push',...
                'Position',[383 23 100 22],...
                'Text',getString(message('econ:econModeler:Cancel')),...
                'Tag','cancelButton',...
                'ButtonPushedFcn',@(s,e)cleanup(this));
            this.CancelButton.Layout.Column = 3;
            
            %Make the figure visible
            movegui(this.Figure,'center');
            this.Figure.Visible = 'on';   
            
            this.Listeners{end+1} = event.listener(this.Session,'AppClosed',@(s,e)cleanup(this));
        end        
    end
    
    methods(Access = private)        
        function cleanup(this)
            %Close this figure and delete this object when data is
            %imported, or the user clicks "Cancel"
            this.Session.notify('DialogClosed');
            delete(this.Figure);
            delete(this);
        end
        
        function varInfo = findVariableInfo(this)
            % Obtain valid variables from either the workspace or a
            % MAT-file
            switch this.DataSource
                case 'ws'
                    validVariables = getVariablesFromBaseWorkspace();
                case 'file'
                    validVariables = getVariablesFromMatFile(this);
                otherwise
                    % Error out if incorrect datasource was specified
                    error(message('econ:econModeler:BadImportDatasource'));        
            end
            
            if islogical(validVariables)
                %This is true when the user has canceled the MAT-File
                %dialog
                varInfo = [];
                return;
            elseif isempty(validVariables)
                %If no valid variables exist in the MAT-file, do
                %not continue
                error(message('econ:econModeler:NoValidVariables'));
            end
            
            %Get variable names and descriptions (size and class)
            variableNames = arrayfun(@(x)x.name,validVariables,'UniformOutput',false);
            variableDescriptions = arrayfun(@(x)[num2str(x.size(1)) 'x' num2str(x.size(2)) ...
                ' ' x.class],validVariables,'UniformOutput',false);
            
            %All selections are false initially
            initialSelections = false(length(validVariables),1);
            
            %Create table data with selections, names, and descriptions
            varInfo = [num2cell(initialSelections),variableNames,variableDescriptions];
        end
        
        function checkForSelection(this,~,e)
            %Listener for the SelectionsChanged event. Enable import only
            %if any selections are true
            data = this.Table.Data;
            
            if e.NewData
                %If a checkbox was selected, remove any other selections
                chosenIdx = e.Indices(1);
                [rows,~] = size(data);
                idxToClear = setdiff(1:rows,chosenIdx);
                data(idxToClear,1) = {false};
                this.Table.Data = data;
            end
            
            selections = cell2mat(data(:,1));
            if any(selections)
                this.ImportButton.Enable = 'on';
            else
                this.ImportButton.Enable = 'off';
            end
        end
        
        function importSelectedData(this, datasource)
            %Callback to the Import button. Import selected data into the
            %app
            import internal.EconModeler.EventData.ImportedDataEventData
            
            %Obtain the names of the selected variables
            data = this.Table.Data;
            selections = cell2mat(data(:,1));
            names = data(:,2);
            selectedNames = names(selections);
            
            try
                %Retrieve the values of the variables from the data source
                switch datasource
                    case 'ws'
                        dataToImport = getDataFromBaseWorkspace(selectedNames);
                    case 'file'
                        dataToImport = getDataFromMatFile(this,selectedNames);
                end
            catch ME
                uialert(this.Session.App,ME.message,...
                    getString(message('econ:econModeler:AppTitle')),...
                    'Icon','error','Modal',true);
                try
                    varInfo = this.findVariableInfo();
                catch ME2
                    uialert(this.Session.App,ME2.message,...
                        getString(message('econ:econModeler:AppTitle')),...
                        'Icon','error','Modal',true);
                    cleanup(this);
                    return;
                end
                this.Table.Data = varInfo;
                this.ImportButton.Enable = 'off';
                return;
            end
            
            %Clear out the session before proceeding
            notify(this.Session,'SessionClear');
            
            %Get information about the original variable, time basis, and
            %time series limits
            this.Session.OriginalVariable = dataToImport{1};
            this.Session.OriginalClass = class(dataToImport{2});
            if istimetable(dataToImport{2})
                dataToImport{2} = sortrows(dataToImport{2});
                this.Session.TimeBasis = dataToImport{2}.Properties.RowTimes;
                this.Session.Limits = [this.Session.TimeBasis(1),this.Session.TimeBasis(end)];
            else
                this.Session.TimeBasis = datetime.empty;
                [numObs,~] = size(dataToImport{2});
                this.Session.Limits = [1, numObs];
            end
            
            %notify app of imported data
            notify(this.Session,'DataImported',ImportedDataEventData(dataToImport));
            cleanup(this);
            
        end
        
        function validVariables = getVariablesFromMatFile(this)
            %Obtain valid variables from a MAT-file
            
            %Open the system dialog to obtain a MAT-file location from the
            %user
            [FileName,PathName] = uigetfile('*.mat',getString(message('econ:econModeler:SelectMatFile')));
            if isnumeric(FileName) && FileName == 0
                validVariables = false;
                return;
            end
            this.FilePath = [PathName FileName];

            load(this.FilePath);
            allData = whos;
            
%             %Get all variables in the MAT-file
%             allData = whos('-file',this.FilePath);

            %Only return variables which are compatible with the app
            validVariables = filterVariables(allData);
        end
        
        function dataToImport = getDataFromMatFile(this,selectedNames) %#ok<INUSL>
            %Get selected data from the MAT-file
            
            %Store the selected names
            dataToImport(1) = selectedNames;
            
            %Create a struct with the data from each selected variable
            varString = strcat('''',selectedNames{1},'''');
            loadedData = eval(['load(this.FilePath,',varString,');']);
            
            %Store the values in the output variable
            dataToImport{2} = loadedData.(cell2mat(selectedNames));
        end  
    end
    
    methods(Hidden,Access = public)
        function qeCheckImportItem(this,selection)
            this.Table.Data{selection,1} = ~this.Table.Data{selection,1};
            event.NewData = this.Table.Data{selection,1};
            event.Indices = [selection,1];
            this.checkForSelection([],event);
        end
    end
end

function validVariables = getVariablesFromBaseWorkspace()
    %Obtain valid variables from the base workspace

    
    %Get all variables in the workspace
    allData = evalin('base','whos');
    
    %Only return variables which are compatible with the app
    validVariables = filterVariables(allData);
    
end

function validVariables = filterVariables(allData)
    %Given all of the variables in a workspace/file, return only the
    %variables which are compatible with the app
    
    % Valid variables are non-empty 1-D or 2-D double matrices
    validIdx = arrayfun(@(x)(any(strcmp({'double','table','timetable'},x.class)) && length(x.size)<=2  && sum(x.size)~=0 && x.size(1) > 1 && ~x.complex),allData);
    validVariables = allData(validIdx);

end

function dataToImport = getDataFromBaseWorkspace(selectedNames)
    %Get selected data from the MAT-file

    %Store the variable names in the output variable
    dataToImport(1) = selectedNames;

    %Obtain the values from the base workspace
    datacmd = ['{' selectedNames{1} '}'];
    
    %Store the corresponding values in the output variable
    dataToImport(2) = evalin("base",datacmd);
end
