function save(fileName, format, busNames, scope, varargin)
% Simulink.Bus.save saves bus objects in cell or bus object format
% in a MATLAB file.
%
% Usage: 
% ======
%   1. Save all bus objects in the MATLAB base workspace into a MATLAB file
%      in the default (i.e. cell) format:
%      >> Simulink.Bus.save(fileName);
%
%   2. Save specific bus objects in the MATLAB base workspace into a MATLAB file 
%      in a specific format:
%      >> Simulink.Bus.save(fileName, format, busNames);
%
%   3. Save all bus objects in a Simulink data dictionary into a MATLAB file 
%      in a specific format:
%      >> scope = Simulink.data.dictionary.open('myfile.sldd');
%      >> Simulink.Bus.save(fileName, format, {}, scope);
%
%   Required Inputs:
%      fileName: Name of the file to store the bus objects. This input
%      maybe specified in the form of a character vector or a string.
%
%   Optional Inputs:
%       format:  May be 'cell' or 'object' or omitted in which
%                case 'cell' is assumed. Use cell format to save the
%                objects in a compact form.  This input
%                maybe specified in the form of a character vector or a string.
%     busNames:  A cell array containing names of bus objects to be saved.
%                If the cell array is empty, all bus objects in the
%                base workspace will be saved.Each name might be specified
%                as a character vector or a string. 
%        scope:  This argument specifies a Simulink.data.Dictionary object
%                or a Simulink.data.DataAccessor object. If unspecified,
%                this argument defaults to the MATLAB base workspace.
%
%   The generated MATLAB file for the cell format will include a call to
%   Simulink.Bus.cellToObject to recreate the bus objects when it is executed. In
%   addition, the generated MATLAB file will return the cell array when executed.
%   To suppress the creation of bus objects when the generated MATLAB file is run,
%   supply an optional argument 'false' to the MATLAB file when executing it.
%
%   Example:
%       busCell = { ...
%       { ...
%           'myBusObj', ...       % Bus Object Name
%           'MyHeader.h', ...     % Header File
%           'My description', ... % Description
%           'Exported', ...       % Data Scope
%           '-1', ...             % Alignment
%           '0', ...              % PreserveElementDimensions
%           {{'a',1,'int32','complex','Sample'}; ...
%           {'b',1,'double','real','Sample'}},...
%           }, ...
%       };
%       Simulink.Bus.cellToObject(busCell);                      % Creates bus object 'myBusObj'
%       fileName = 'BusCellFile1';
%       Simulink.Bus.save(fileName);                             % Saves all bus objects in cell format
%       Simulink.Bus.save('BusObjFile', 'object');               % Saves all bus objects in bus format
%       Simulink.Bus.save('BusCellFile2', 'cell', {'myBusObj'}); % Saves myBusObj in cell format in BusCellFile2.m
%       Simulink.Bus.save("BusObjFile2", "object", {"myBusObj"});% Saves myBusObj in bus format in BusObjFile2.m
%       
%   See also Simulink.Bus.cellToObject

%   Copyright 1994-2022 The MathWorks, Inc.

% do not disp back traces when reporting warning
if nargin > 0
    if isstring(fileName)
        fileName = convertStringsToChars(fileName);
    end
end

if nargin > 1
    if isstring(format)
        format = convertStringsToChars(format);
    end
end

if nargin > 2
    if isstring(busNames)
        busNames = cellstr(busNames);
    end
end

wStates = [warning; warning('query','backtrace')];
warning off backtrace;
cleanupObj = onCleanup(@()warning(wStates));

try
    fid = -1;
    
    % This function expects one to five arguments
    isOk = nargin < 1 || nargin > 5;
    if isOk
        DAStudio.error('Simulink:tools:slbusInvalidNumInputs');
    end
    
    % Fill with default values if the inputs are not specified
    if nargin < 2
        format  = 'cell';
    end
    
    if nargin < 3
        busNames = {};
    end
    
    if nargin < 4
        scope = Simulink.data.BaseWorkspace;
    end
    
    fileMode = 'wt';
    if nargin == 5
        fileMode = varargin{1};
    end
    
    % Convert to internal abstraction from user documented API for SLDD
    if isa(scope, 'Simulink.data.Dictionary')
        scope = Simulink.data.DataDictionary(scope.filepath);
    elseif isa(scope, 'Simulink.dd.Connection')
        scope = Simulink.data.DataDictionary(scope.filespec); % for Bus Editor v3
    end
    
    if ~(isa(scope, 'Simulink.data.BaseWorkspace') || isa(scope, 'Simulink.data.DataDictionary') || ...
            isa(scope, 'Simulink.data.DataAccessor'))
        DAStudio.error('Simulink:tools:slbusInvalidScope');
    end
    
    % Inputs must be string
    if ~ischar(fileName) || ~ischar(format)
        DAStudio.error('Simulink:tools:slbusSaveInvalidFirstArg');
    end
    
    % Check file name extension
    [pathStr, fcnName, ext] = fileparts(fileName); %#ok
    if isempty(ext)
        fileName = [fileName, '.m'];
    elseif ~strcmp(ext, '.m')
        DAStudio.error('Simulink:tools:slbusSaveInvalidExt');
    end
    
    % error if the file exists
    if exist(fileName, 'file') && ~strcmp(fileMode, 'at')
        DAStudio.error('Simulink:tools:slbusSaveFileAlreadyExists', fileName, fileName);
    end
    
    % Check the path
    if ~isempty(pathStr) && ~exist(pathStr, 'dir')
        DAStudio.error('Simulink:tools:slbusSaveDirDoesNotExist');
    end
    
    % format must be 'cell' or 'object'
    if ~(strcmpi(format, 'cell') || strcmpi(format,'object'))
        DAStudio.error('Simulink:tools:slbusSaveInvalidSecondArg');
    end
    
    % Check busName
    isOk = iscell(busNames);
    if isOk
        for idx = 1:length(busNames)
            if ~isvarname(busNames{idx})
                isOk = false;
                break;
            end
        end
    end
    
    if ~isOk
        DAStudio.error('Simulink:tools:slbusSaveInvalidThirdArg');
    end
    
    % Open the file name
    [fid, message] = fopen(fileName, fileMode);
    if fid == -1
        DAStudio.error('Simulink:tools:slbusSaveFileOpenError', fileName, message);
    end
    
    if strcmpi(format,'cell')
        save_bus_in_cell_format_l(fid, fileName, busNames, scope);
    else
        save_bus_in_object_format_l(fid, fileName, busNames, scope, fileMode);
    end
    
    save_nonbus_numeric_dtypes_l(fid, busNames, scope);
    
    fclose(fid);
catch me
    if fid ~= -1
        fclose(fid);
        delete(fileName);
    end
    rethrow(me);
end

%endfunction


% Function save_bus_in_cell_format_l ==========================================
% Abstract:
%    It saves either all bus objects in the base workspace or
%    the specified buses in cell format in a MATLAB file.
%
function save_bus_in_cell_format_l(fid, fileName, busNames, scope)
[pathStr, fcnName] = fileparts(fileName); %#ok

fprintf(fid,'function cellInfo = %s(varargin) \n', fcnName);
tmpStr = 'returns a cell array containing bus object information';
fprintf(fid,'%% %s %s \n', upper(fcnName), tmpStr);
fprintf(fid,'%% \n');
fprintf(fid, '%% Optional Input: ''false'' will suppress a call to Simulink.Bus.cellToObject \n');
fprintf(fid, '%%                 when the MATLAB file is executed. \n');
fprintf(fid,'%% The order of bus element attributes is as follows:\n');

tmpStr = 'ElementName, Dimensions, DataType, ';

tmpStr = [tmpStr, 'Complexity, SamplingMode, DimensionsMode, Min, Max'];

tmpStr = [tmpStr, ', DocUnits, Description'];

fprintf(fid,'%%   %s \n\n', tmpStr);

fprintf(fid, 'suppressObject = false; \n');
fprintf(fid, 'if nargin == 1 && islogical(varargin{1}) && varargin{1} == false \n');
fprintf(fid, '    suppressObject = true; \n');
fprintf(fid, 'elseif nargin > 1 \n');
fprintf(fid, '    error(''Invalid input argument(s) encountered''); \n');
fprintf(fid, 'end \n\n');

fprintf(fid, 'cellInfo = { ... \n');

% Call objectToCell to convert the bus objects to a cell array. 
% Note that if busNames is empty, objectToCell will convert all 
% bus objects in the SLDD or base workspace automatically.
busCell = Simulink.Bus.objectToCell(busNames, scope);

for i=1:length(busCell)
    bus = busCell{i};
    fprintf(fid, '  { ... \n');
    fprintf(fid, '    ''%s'', ... \n', bus{1});   % Name
    fprintf(fid, '    ''%s'', ... \n', bus{2});   % HeaderFile
    % Multi-line descriptions containing newline does not enable creation
    % of valid cell array in the generated MATLAB file.
    % So, use sprintf to assign descriptions to these.
    busDesc = escapeForSprintf(bus{3});
    fprintf(fid, '    %s, ... \n', busDesc); % Description
    fprintf(fid, '    ''%s'', ... \n', bus{4});   % DataScope
    if sl('busUtils','NDIdxBusUI') > 0
        fprintf(fid, '    ''%s'', ... \n', bus{5});   % Alignment
        fprintf(fid, '    ''%s'', {... \n', bus{6});   % PreserveElementDimensions
        busElementsIndex = 7;
    else
        fprintf(fid, '    ''%s'', {... \n', bus{5});   % Alignment
        busElementsIndex = 6;
    end 
    for j=1:length(bus{busElementsIndex})
        el = bus{busElementsIndex}{j};
        dimsStr = sl('busUtils', 'GetDimsStr', el{2});     
      
        % Handle data types with quotes: fixdt('double', 'DataTypeOverride', 'Off')
        dataTypeStr = strrep(el{3},'''','''''');
        
        % name, dims, dataType, sampleTime, cplx, frames, dimsMode        
        if (isnumeric(el{4}) && (el{4}(1) ~= -1))
            sampleTimeFormatStr = ', %s';
            tsStr = sl('busUtils', 'GetSampleTimeStr', el{4});
            cplxStr = el{5};
            frameStr = el{6};
            dimsModeStr = el{7};
            minStr = sl('busUtils', 'getMinMaxStr', el{8});
            maxStr = sl('busUtils', 'getMinMaxStr', el{9});
            elemDocUnits = escapeForSprintf(el{10});
            elemDesc = escapeForSprintf(el{11});
        else
            sampleTimeFormatStr = [];
            tsStr = [];
            cplxStr = el{4};
            frameStr = el{5};
            dimsModeStr = el{6};
            minStr = sl('busUtils', 'getMinMaxStr', el{7});
            maxStr = sl('busUtils', 'getMinMaxStr', el{8});
            elemDocUnits = escapeForSprintf(el{9});
            elemDesc = escapeForSprintf(el{10});
        end
        sFormatStr = ['{''%s'', %s, ''%s''', sampleTimeFormatStr, ', ''%s'', ', ...
            '''%s'', ''%s'', %s, %s'];
        sArgStrCell = [{el{1}, dimsStr, dataTypeStr}, tsStr, ...
            {cplxStr, frameStr, dimsModeStr, minStr, maxStr}];
        
        sFormatStr = [sFormatStr, ', %s, %s']; %#ok
        sArgStrCell{end + 1} = elemDocUnits;  %#ok
        sArgStrCell{end + 1} = elemDesc;  %#ok
        sFormatStr = [sFormatStr, '}; ...\n'];  %#ok
        fprintf(fid, sFormatStr, sArgStrCell{:});
    end
    fprintf(fid, '    } ...\n');
    fprintf(fid, '  } ...\n');
end

fprintf(fid,  '}''; \n\n');

fprintf(fid, 'if ~suppressObject \n');
fprintf(fid, '    %% Create bus objects in the MATLAB base workspace \n');
fprintf(fid, '    Simulink.Bus.cellToObject(cellInfo) \n');
fprintf(fid, 'end \n');


%endfunction save_bus_in_cell_format_l


% Function save_bus_in_object_format_l ========================================
% Abstract:
%    It saves either all bus objects in the base workspace or
%    the specified buses in object format in a MATLAB file.
%
function save_bus_in_object_format_l(fid, fileName, busNames, scope, fileMode)
if ~strcmpi(fileMode, 'at')
    [pathStr, fcnName] = fileparts(fileName); %#ok

    fprintf(fid,'function %s() \n', fcnName);
    tmpStr = 'initializes a set of bus objects in the MATLAB base workspace';
    fprintf(fid,'%% %s %s \n\n', upper(fcnName), tmpStr);
end

if isempty(busNames)
    busNames = scope.whos('Simulink.Bus');
end

% One-time warning for each class
checkSubCls.bus    = true;
checkSubCls.busEle = true;

for idx = 1:length(busNames)
    checkSubCls = save_one_bus_in_object_format_l(fid, busNames{idx}, checkSubCls, scope);
end

%endfunction save_bus_in_object_format_l

% Function save_one_bus_in_object_format_l ====================================
% Abstract:
%   Save one bus object in the object format in the file.
%
function checkSubCls = save_one_bus_in_object_format_l(fid, busName, checkSubCls, scope)

fprintf(fid, '%% Bus object: %s \n', busName);
fprintf(fid,'clear elems;\n');

if isa(scope, 'Simulink.data.DataAccessor')
    busObj = sl('slbus_get_object_from_name_withDataAccessor', busName, true, scope);
else
    busObj = sl('slbus_get_object_from_name', busName, true, scope);
end

% g2463891 - report error similar to
% 'slbus_get_object_from_name_withDataAccessor for connection bus
if isa(busObj, 'Simulink.ConnectionBus')
    DAStudio.error('Simulink:utility:slUtilityBusObjectNotFoundInDataSources', busName);
end

% Check subclass of Simulink.Bus
if checkSubCls.bus && ~strcmp(class(busObj),'Simulink.Bus') %#ok
    % Find a Simulink.Bus subclass object, issue warning
    % No further check on bus subclass needed
    MSLDiagnostic('Simulink:tools:slbusNotPreserveAdditionalProps', 'Simulink.Bus').reportAsWarning;
    checkSubCls.bus = false;
end

elems = busObj.Elements;

for eIdx = 1:length(elems)
    thisElm = elems(eIdx);    
    % Check subclass of Simulink.BusElement
    if checkSubCls.busEle && ~strcmp(class(thisElm), 'Simulink.BusElement')%#ok
        % Find a Simulink.BusElement subclass object, issue warning
        % No further check on bus element subclass needed
        MSLDiagnostic('Simulink:tools:slbusNotPreserveAdditionalProps', 'Simulink.BusElement').reportAsWarning;
        checkSubCls.busEle = false;
    end
    
    fprintf(fid,'elems(%d) = Simulink.BusElement;\n',eIdx);
    fprintf(fid,'elems(%d).Name = ''%s'';\n', eIdx, thisElm.Name);    
    
    dimsStr = sl('busUtils', 'GetDimsStr', thisElm.Dimensions);
    fprintf(fid,'elems(%d).Dimensions = %s;\n', eIdx, dimsStr);
    
    fprintf(fid,'elems(%d).DimensionsMode = ''%s'';\n', eIdx, thisElm.DimensionsMode);
    
    % Handle data types with quotes: fixdt('double', 'DataTypeOverride', 'Off')
    dataTypeStr = strrep(thisElm.DataType,'''','''''');
    fprintf(fid,'elems(%d).DataType = ''%s'';\n', eIdx, dataTypeStr);    
   
    sampleTimeVal = thisElm.SampleTime;
    if (sampleTimeVal(1) ~= -1)
        warning(message('Simulink:DataType:BusElementNonInheritSampleTimeSpecifiedNoBlock', num2str(sampleTimeVal(1)), thisElm.Name, busName));
    end
    if ~(sampleTimeVal(1) == -1)
        tsStr = sl('busUtils', 'GetSampleTimeStr', sampleTimeVal);
        fprintf(fid,'elems(%d).SampleTime = %s;\n', eIdx, tsStr);
    end

    fprintf(fid,'elems(%d).Complexity = ''%s'';\n', eIdx, thisElm.Complexity);
    
    minStr = sl('busUtils', 'getMinMaxStr', thisElm.Min);
    fprintf(fid,'elems(%d).Min = %s;\n', eIdx, minStr);
        
    maxStr = sl('busUtils', 'getMinMaxStr', thisElm.Max);
    fprintf(fid,'elems(%d).Max = %s;\n', eIdx, maxStr);
    
    elemDocUnits = escapeForSprintf(thisElm.DocUnits);
        fprintf(fid,'elems(%d).DocUnits = %s;\n',eIdx, elemDocUnits);
        
    elemDesc = escapeForSprintf(thisElm.Description);
    fprintf(fid,'elems(%d).Description = %s;\n',eIdx, elemDesc);
    
    fprintf(fid,'\n');
end

dataScope = busObj.DataScope;
headerFile = busObj.HeaderFile;
alignment = busObj.Alignment;
if sl('busUtils','NDIdxBusUI')
    preserveElementDimensions = busObj.PreserveElementDimensions;
end 

fprintf(fid,'%s = Simulink.Bus;\n',busName);
fprintf(fid,'%s.HeaderFile = ''%s'';\n', busName, headerFile);
% Multi-line descriptions containing newline does not enable creation
% of valid cell array in the generated MATLAB file.
% So, use sprintf to assign descriptions to these.
description = escapeForSprintf(busObj.Description);
fprintf(fid, '%s.Description = %s;\n', busName, description);
fprintf(fid, '%s.DataScope = ''%s'';\n', busName, dataScope);
fprintf(fid, '%s.Alignment = %d;\n', busName, alignment);
if sl('busUtils','NDIdxBusUI')
    fprintf(fid, '%s.PreserveElementDimensions = %d;\n', busName, preserveElementDimensions);
end 
if ~isempty(elems)
    fprintf(fid,'%s.Elements = elems;\n',busName);
    fprintf(fid,'clear elems;\n');
end
fprintf(fid, 'assignin(''base'',''%s'', %s);\n', busName, busName);
fprintf(fid,'\n');

%endfunction save_one_bus_in_object_format_l

% Function: save_nonbus_numeric_dtypes_l ======================================
% Abstract:
%    Also add the numeric data types to the file.
%
function busDataTypes = save_nonbus_numeric_dtypes_l(fid, busNames, scope)

busDataTypes  ={};
if isempty(busNames)
    if isa(scope, 'Simulink.data.DataAccessor')
        varIds = scope.identifyVisibleVariablesDerivedFromClass('Simulink.Bus');
        for idx = 1:length(varIds)
            busNames{end+1} = varIds(idx).Name; %#ok
        end
    else
        var = scope.evalin('whos');
        for idx = 1:length(var)
            if (strcmp(var(idx).class, 'Simulink.Bus'))
                busNames{end+1} = var(idx).name; %#ok
            end
        end
    end
end
% We have a list of bus names

for idx = 1:length(busNames)
    if isa(scope, 'Simulink.data.DataAccessor')
        varId = scope.name2UniqueIdWithCheck(busNames{idx});
        bus = scope.getVariable(varId);
    else
        bus = scope.evalin(busNames{idx});
    end
    for yidx = 1:length( bus.Elements)
        elem = bus.Elements(yidx);
        % This can be generalized in future
        if strncmp(elem.DataType, 'slnum_', 6)
            % Numeric data types are generated using slnum_ prefix
            % See slbus_gen_object
            dtypeName  = elem.DataType;
            fixptdtype = elem.DataType(7:end);
            
            tmpStr = [dtypeName, ' = fixdt(''', fixptdtype, ''');\n'];
            fprintf(fid,tmpStr);
            
            tmpStr = ['assignin(''base'',''',dtypeName,''', ',dtypeName,');\n'];
            fprintf(fid,tmpStr);
        end
    end
end
%endfunction

function outStr = escapeForSprintf( str )
% Escape the input string where required for use with sprintf

if(isempty(str))
    outStr = '''''';
else
    outStr = strrep(str, '\', '\\');
    outStr = strrep(outStr, '''', '''''');
    outStr = strrep(outStr, '%', '%%');
    outStr = ['sprintf(''', strrep(outStr, sprintf('\n'), '\n'), ''')'];
end

% LocalWords:  cplx elems nonbus dtypes slnum myfile sldd
