function save(fileName, busNames, scope, varargin)
% Simulink.ConnectionBus.save saves bus objects in cell or bus object format
% in a MATLAB file.
%
% Usage: 
% ======
%   1. Save all connection bus objects in the MATLAB base workspace into a MATLAB file:
%      >> Simulink.ConnectionBus.save(fileName);
%
%   2. Save specific connection bus objects in the MATLAB base workspace into a MATLAB file:
%      >> Simulink.ConnectionBus.save(fileName, busNames);
%
%   3. Save all connection bus objects in a Simulink data dictionary into a MATLAB file:
%      >> scope = Simulink.data.dictionary.open('myfile.sldd');
%      >> Simulink.ConnectionBus.save(fileName, {}, scope);
%
%   Required Inputs:
%      fileName: Name of the file to store the connection bus objects. This input
%      maybe specified in the form of a character vector or a string.
%
%   Optional Inputs:
%     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.
%
%   Example:
%       Simulink.ConnectionBus.save('BusObjFile', 'object');               % Saves all connection bus objects in bus format
%       Simulink.ConnectionBus.save("BusObjFile2", "object", {"myBusObj"});% Saves myBusObj in bus format in BusObjFile2.m
%       
%   See also Simulink.Bus.save

%   Copyright 2020 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(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 four arguments
    isOk = nargin < 1 || nargin > 4;
    if isOk
        DAStudio.error('Simulink:tools:slbusInvalidNumInputs');
    end
    
    if nargin < 2
        busNames = {};
    end
    
    if nargin < 3
        scope = Simulink.data.BaseWorkspace;
    end
    
    fileMode = 'wt';
    if nargin == 4
        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)
        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
    
    % 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:slconnectionBusSaveInvalidSecondArg');
    end
    
    % Open the file name
    [fid, message] = fopen(fileName, fileMode);
    if fid == -1
        DAStudio.error('Simulink:tools:slbusSaveFileOpenError', fileName, message);
    end
    
    % Save objects
    save_bus_in_object_format_l(fid, fileName, busNames, scope, fileMode);
    
    fclose(fid);
catch me
    if fid ~= -1
        fclose(fid);
        delete(fileName);
    end
    rethrow(me);
end

%endfunction

% 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.ConnectionBus');
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, '%% Connection 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 regular bus
if isa(busObj, 'Simulink.Bus')
    DAStudio.error('Simulink:utility:slUtilityBusObjectNotFoundInDataSources', busName);
end

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

elems = busObj.Elements;

for eIdx = 1:length(elems)
    thisElm = elems(eIdx);    
    % Check subclass of Simulink.ConnectionElement
    if checkSubCls.busEle && ~strcmp(class(thisElm), 'Simulink.ConnectionElement')%#ok
        % Find a Simulink.ConnectionElement subclass object, issue warning
        % No further check on bus element subclass needed
        DAStudio.warning('Simulink:tools:slbusNotPreserveAdditionalProps', 'Simulink.ConnectionElement');
        checkSubCls.busEle = false;
    end
    
    fprintf(fid,'elems(%d) = Simulink.ConnectionElement;\n',eIdx);
    fprintf(fid,'elems(%d).Name = ''%s'';\n', eIdx, thisElm.Name);
    
    % Handle types with quotes: 'Connection:
    % foundation.hydraulic.hydraulic' or 'Bus: myBus'
    typeStr = strrep(thisElm.Type,'''','''''');
    fprintf(fid,'elems(%d).Type = ''%s'';\n', eIdx, typeStr);
    elemDesc = escapeForSprintf(thisElm.Description);
    fprintf(fid,'elems(%d).Description = %s;\n',eIdx, elemDesc);
    
    fprintf(fid,'\n');
end

fprintf(fid,'%s = Simulink.ConnectionBus;\n',busName);
% 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);
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 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, newline, '\n'), ''')'];
end

% LocalWords:  cplx elems nonbus dtypes slnum myfile sldd
