%

% Copyright 2018-2020 The MathWorks, Inc.

classdef SaveVariablesToScript < handle
    properties (SetAccess = immutable)
        T table;
        parameters (1,:) Simulink.Simulation.Variable;
        varNames (1,:) string;
        varValues (1,:) cell;
    end

    properties (SetAccess = private)
        filenames (1,:) string;
        blockPaths (1,:) string;
        tempDir (1,1) string;
        rv (1,1) xls.internal.RestoreVariable;
        isMask (1,:) logical;
        needScript (1,:) logical;
    end

    methods
        function this = SaveVariablesToScript(parameters)
            this.parameters = parameters;
            this.varNames = {this.parameters.Name};
            this.varValues = {this.parameters.Value};
            this.setMaskBlockPaths;
            this.setupEnvironment;
            this.initFilenames;
            this.validateInputs;
            this.assignVars;
            this.evalExpressions;
            this.T = table(this.varNames', this.getValueColumn, this.getMaskBlockPaths{:});
        end

        function delete(this)
            rmdir(this.tempDir, 's');
        end
    end

    methods (Static)
        valueStruct = toValue(params, needsEval);
        maskIdx = getMaskIdx(workspace);
    end

    methods (Access = private)
        function setupEnvironment(this)
            % create temp folder and cache base workspace variables
            this.tempDir = tempname; % create different folder each time
            mkdir(this.tempDir);
            this.rv = xls.internal.RestoreVariable(this.varNames);
        end

        function assignVars(this)
            cellfun(@(var, val) assignin('base', var, val), ...
                this.varNames(this.needScript), this.varValues(this.needScript));
        end

        function expr = getSaveExpression(this)
            % saveVariablesToScript works best with vars in base workspace
            expr = "matlab.io.saveVariablesToScript('" + ...
                this.filenames(this.needScript) + "','" + ...
                this.varNames(this.needScript) + "');";
        end

        function valueColumn = getValueColumn(this)
            valueColumn = strings(numel(this.filenames), 1);
            texts = arrayfun(@(file) string(fileread(file)), this.filenames(this.needScript).');
            valueColumn(this.needScript) = arrayfun(@removeCommentsAndWhitespace, texts);
            valueColumn(this.isMask) = string(this.varValues(this.isMask));
            retainValue = ~(this.needScript | this.isMask);
            valueColumn = this.getRetainValueColumn(valueColumn, retainValue);
        end

        function evalExpressions(this)
            arrayfun(@evalExpression, this.getSaveExpression);
        end

        function setMaskBlockPaths(this)
            this.blockPaths = {this.parameters.Workspace};
            this.isMask = xls.internal.SaveVariablesToScript.getMaskIdx(this.blockPaths);
            this.needScript = cellfun(@doesValueNeedScript, this.varValues) & ~this.isMask;
            this.blockPaths(this.blockPaths == "global-workspace") = "";
        end

        function bp = getMaskBlockPaths(this)
            if all(this.blockPaths.strlength == 0)
                bp = {};
            else
                bp = {this.blockPaths.'};
            end
        end

        function validateInputs(this)
            % mask parameters must be scalar text
            cellfun(@(x) validateattributes(x,["char","string"],{'scalartext'}), ...
                this.varValues(this.isMask));
        end

        function initFilenames(this)
            this.filenames = strings(1, numel(this.parameters));
            this.filenames(this.needScript) = this.tempDir + filesep + "d" + ...
                (1:nnz(this.needScript)) + ".m";
        end

        function valueColumn = getRetainValueColumn(this, valueColumn, retainValue)
            if writeAsNumeric(this.varValues)
                assert(all(retainValue));
                valueColumn = cell2mat(this.varValues).';
            else
                fcns = this.getFcnsToConvertToString(retainValue);
                valueColumn(retainValue) = cellfun(@(value, fcn) fcn(value), ...
                    this.varValues(retainValue), fcns, 'Uniform', false);
            end
        end

        function fcn = getFcnsToConvertToString(this, retainValue)
            fcn = cellfun(@getFcnToConvertToString, ...
                this.varValues(retainValue), 'Uniform', false);
        end
    end
end

function evalExpression(expr)
    [~, r2] = evalin('base', expr);
    if ~isempty(r2)
        error(message('MATLAB:savevars:SaveVarsUnsupportedClassAt', r2{1}));
    end
end

function lines = removeCommentsAndWhitespace(text)
    % (mtree also works but reformats and does not wrap data. Its output
    % diverges from saveVariablesToScript, ex: mtree(file, '-file').tree2str)
    lines = text.splitlines;
    lines = lines(~lines.startsWith('%')).strip;
    lines = lines(lines.strlength ~= 0).join(newline);
end

function bool = doesValueNeedScript(value)
    % these values are simple enough not to need saveVariablesToScript
    reader = xls.internal.reader.getReader(value);
    bool = reader.doesValueNeedSaveVarsToScript;
end

function bool = writeAsNumeric(values)
    % writing as text is not necessary if all values are scalar double
    bool = all(cellfun(@(value) isscalar(value) && isa(value, 'double'), values));
end

function fcn = getFcnToConvertToString(value)
    reader = xls.internal.reader.getReader(value);
    fcn = reader.getFcnToConvertToString;
end
