function varargout=docblock(varargin)
%DOCBLOCK  Simulink documentation block
%   DOCBLOCK manages the Simulink documentation block
%
%   The DocBlock can edit HTML and RTF files as well as plain text.  Do
%   SET_PARAM(BLKH,'DocumentType','HTML') or use the "Mask Parameters"
%   dialog to change the type of document associated with the block.
%
%   On PC systems, RTF and HTML files will be opened in Microsoft (TM)
%   Word.  Otherwise, the document content will be opened in the MATLAB
%   editor.  To change this behavior, use the commands:
%
%   DOCBLOCK('setEditorHTML',EDITCMD)
%   DOCBLOCK('setEditorDOC',EDITCMD)
%   DOCBLOCK('setEditorTXT',EDITCMD)
%
%   where EDITCMD is a string to be evaluated at the command line to launch
%   a custom application.  The special token %<FileName> will be replaced
%   with the full path to the file.  For example, to use Mozilla Composer
%   as your HTML editor, use the command:
%
%   docblock('setEditorHTML','system(''/usr/local/bin/mozilla -edit "%<FileName>" &'');')
%
%   To return to the default behavior, use an empty string ('') as EDITCMD
%
%   To get the current edit command settings, type:
%
%   EDITCMD = DOCBLOCK('getEditorHTML')
%   EDITCMD = DOCBLOCK('getEditorDOC')
%   EDITCMD = DOCBLOCK('getEditorTXT')
%
%   Custom editor commands persist between MATLAB sessions.
%

%   Copyright 1990-2020 The MathWorks, Inc.

%DocBlock parameters:
%      DeleteFcn          "docblock('close_document',gcb);"
%      PreSaveFcn         "docblock('save_document',gcb);"
%      OpenFcn            "docblock('edit_document',gcb);"
%      InitFcn            "docblock('edit_document',gcb);"

%Enhancements:
% Install listeners on the editor so that when document is saved, changes are pushed to model
% Put inports on block to allow it to be hooked to a signal

[varargin{:}] = convertStringsToChars(varargin{:});

if nargin==0
    varargout{1} = addToSystem;
elseif nargout==0
    try
        feval(varargin{:});
    catch ME
        warning(ME.identifier,'%s',ME.message);
    end
else
    try
        [varargout{1:nargout}]=feval(varargin{:});
    catch ME
        warning(ME.identifier,'%s',ME.message);
    end
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function fName=getBlockFileName(inBlkName)
%Every block has a unique file name associated with it.
%This function returns that file.
%
% PRIVATE: This function is not guaranteed to remain stable

% get original docblock, if incoming block is a part of harness.
inBlkObj = get_param(inBlkName, 'Object');

if (isprop(inBlkObj, 'DocBlockDocument') && ~isempty(inBlkObj.DocBlockDocument.SrcBlk))
    blkName = inBlkObj.DocBlockDocument.SrcBlk;
else
    % if block is a harness block, use system block
    blkName = harnessBlkToSystemBlk(inBlkName);
    if isempty(blkName)
        blkName = inBlkName;
    end
end

% Retrieve filename, if empty create new one
blkHandle = get_param(blkName,'handle');
blkObj = get_param(blkName, 'Object');
if ~isprop(blkObj, 'DocBlockDocument')
    if slf_feature('get','BigSwitch_DA_U2M')
        p = addprop(blkObj,'DocBlockDocument');
        p.Transient = 1;
        p.NonCopyable= 1;
        p.Hidden = 1;
    else
        p = schema.prop(blkObj, 'DocBlockDocument', 'mxArray');
        p.AccessFlags.Serialize = 'off';
        p.AccessFlags.Copy = 'off';
        p.Visible = 'off';
    end
    blkObj.DocBlockDocument = struct('Name', '', 'Type', blkObj.DocumentType, 'SrcBlk', '');
end

if (~isempty(blkObj.DocBlockDocument.Name) && strcmp(blkObj.DocBlockDocument.Type, blkObj.DocumentType))
    fName = blkObj.DocBlockDocument.Name;
else
    try
        docExt = lower(get_param(blkName,'DocumentType'));
    catch ME1 %#ok
        %This probably means that we have an old-style block which needs to be
        %upgraded.
        try
            convert_legacy_block(blkName);
        catch ME2
            warning(message('SimulinkBlocks:docblock:ConvertLegacyBlock',ME2.message));
        end
        docExt = 'txt';
    end
    
    if strcmp(docExt,'text')
        docExt = 'txt';
    end
    
    fName = [tempname '.' docExt];
    
    % Update the table of names every time we generate a name
    blkObj.DocBlockDocument.Name = fName;
    blkObj.DocBlockDocument.Type = blkObj.DocumentType;
    internalMapping('setFile', fName, blkHandle);
end

% store original docblock info, into harness docblock
if ~strcmp(inBlkName, blkName)
    inBlkObj = get_param(inBlkName, 'Object');
    if ~isprop(inBlkObj, 'DocBlockDocument')
        if slf_feature('get', 'BigSwitch_DA_U2M')
            p = addprop(inBlkObj, 'DocBlockDocument');
            p.Transient = 1;
            p.NonCopyable= 1;
            p.Hidden = 1;
        else
            p = schema.prop(inBlkObj, 'DocBlockDocument', 'mxArray');
            p.AccessFlags.Serialize = 'off';
            p.AccessFlags.Copy = 'off';
            p.Visible = 'off';
        end
        
        inBlkObj.DocBlockDocument.Name = blkObj.DocBlockDocument.Name;
        inBlkObj.DocBlockDocument.Type = blkObj.DocBlockDocument.Type;
        inBlkObj.DocBlockDocument.SrcBlk = blkName;
    end
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function result = filename2blockhandle(fName) %called from VnV code
% getBlockFileName(blkName) provides a deterministic filename for a given
% block. VnV features require the reverse procedure: given the filename get
% the block. To provide this sort of backword resolution, this manager
% function will maintain a table of blocknames.

result = internalMapping('getBlk', fName);

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function result = internalMapping(method, varargin)

persistent fileToBlockMap

mlock();
if ~isa(fileToBlockMap, 'containers.Map')
    fileToBlockMap = containers.Map;
end

result = [];

switch method
    case 'getBlk'
        file = varargin{1};
        if isKey(fileToBlockMap, file)
            blkSid = fileToBlockMap(file);
            result = get_param(blkSid, 'Handle');
        end
        
    case 'setFile'
        file = varargin{1};
        blkH = varargin{2};
        blkSid = Simulink.ID.getSID(blkH);
        fileToBlockMap(file) = blkSid;
       
    case 'removeFile'
        file = varargin{1};
        if isKey(fileToBlockMap, file)
            remove(fileToBlockMap, file);
        end
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function breaklink(blkName) %#ok called from legacy block callblock
%This is not used anymore.  Left around for legacy DocBlocks which still
%call docblock('breaklink') with their CopyFcn

%noop

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function fileName=edit_document(blkName) %called from block callback
%CALLBACK: used as OpenFcn
%Creates the text file and opens it in the user's selected editor

if nargin<1
    blkName = gcb;
end

% g1132120, Doc block can not be opened from library browser
if strcmpi(bdroot(blkName) , 'simulink')
    errordlg(getString(message('SimulinkBlocks:docblock:NeedsModelOrLibrary')), ...
        'Error', 'Modal');
    return;
end

try
    fileName = getBlockFileName(blkName);
catch ME
    fileName = '';
    warning(message('SimulinkBlocks:docblock:NoTempFile',ME.message));
    return;
end

if exist(fileName,'file')
    %the file is already open - update the block
    try
        % If harness, then block is already updated
        harnessBlk = systemBlkToHarnessBlk(blkName);
        if isempty(harnessBlk)
            file2blk(blkName,fileName);
        end
    catch ME
        warning(ME.identifier,'%s',ME.message);
    end
    dirtyModel = false;
else
    try
        blk2file(blkName,fileName);
    catch ME
        warning(ME.identifier,'%s',ME.message);
    end
    %the file does not exist - create it

    dirtyModel = true;
end

try
    open_document(fileName,blkName);
catch ME
    warning(ME.identifier,'%s',ME.message);
end

if dirtyModel
    %Dirty the model in order to encourage saving later on
    %Note: dirty the model AFTER opening it in the editor to make sure
    %that the warning dialog is not buried under the newly-opened editor.
    setDirty(blkName,getString(message('SimulinkBlocks:docblock:UnlockBlockDiagramPrompt')));
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function errMsg = open_document(fileName,varargin)

errMsg = '';
switch fileName(end-2:end)
    case {'rtf','doc'}
        docViewer = getEditorDOC;

        if ~isempty(docViewer)
            docViewer = strrep(docViewer,'%<FileName>',strrep(fileName,'''',''''''));
            try
                evalin('base',docViewer);
            catch ME
                errMsg = getString(message('SimulinkBlocks:docblock:UnableToLaunchDocViewer',...
                                           docViewer,...
                                           ME.message));
            end
        elseif ispc
            errMsg = open_word(fileName);
        else
            open_editor(fileName,varargin{:});
        end
    case {'tml','htm'}
        htmlViewer = getEditorHTML;

        if ~isempty(htmlViewer)
            htmlViewer = strrep(htmlViewer,'%<FileName>',strrep(fileName,'''',''''''));
            try
                evalin('base',htmlViewer);
            catch ME
                errMsg = getString(message('SimulinkBlocks:docblock:UnableToLaunchHTMLViewer',...
                                           htmlViewer,...
                                           ME.message));
            end
        elseif ispc
            errMsg = open_word(fileName);
        else
            open_editor(fileName,varargin{:});
        end
    otherwise
        txtViewer = getEditorTXT;

        if ~isempty(txtViewer)
            txtViewer = strrep(txtViewer,'%<FileName>',strrep(fileName,'''',''''''));
            try
                evalin('base',txtViewer);
            catch ME
                errMsg = getString(message('SimulinkBlocks:docblock:UnableToLaunchTextViewer',...
                                           txtViewer,...
                                           ME.message));
            end
        else
            open_editor(fileName,varargin{:});
        end
end

if ~isempty(errMsg)
    open_editor(fileName,varargin{:});
    error(message('SimulinkBlocks:docblock:UnableToEditDocument',errMsg));
end


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function open_editor(fileName,varargin)
%This will open the MATLAB text editor and establish a file listener

try
    % For TXT files, open MATLAB editor 
    if strcmpi(fileName(end-2:end), 'txt')
        matlab.desktop.editor.openDocument(fileName);
    else
        edit(fileName);
    end
    
catch ME
    warning(message('SimulinkBlocks:docblock:TextEditorError',ME.message));
end

if ~isempty(varargin)
    blkName = varargin{1};
    try
        blkHandle = get_param(blkName,'handle');
        blkObj = get_param(blkName,'Object');

        propName = 'DocBlockEditorFileListener';
        p = findprop(blkObj,propName);
        if isempty(p)
            if slf_feature('get', 'BigSwitch_DA_U2M')
                p = addprop(blkObj, propName);
                p.Transient = 1;
                p.NonCopyable= 1;
            else
                p = schema.prop(blkObj,propName,...
                'com.mathworks.toolbox.simulink.docblock.EditorFileListener');
                p.AccessFlags.Serialize = 'off';
                p.AccessFlags.Copy = 'off';
            end
        end

        fListen = get(blkObj,propName);
        if isempty(fListen)
            fListen = com.mathworks.toolbox.simulink.docblock.EditorFileListener(blkHandle,fileName); 
            set(blkObj,propName,fListen);
        end
    catch ME
        warning(message('SimulinkBlocks:docblock:FileListenerAddError',ME.message));
    end
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
function errMsg = open_word(fileName)
% Open MS Word document.  If opened, then refocus.

errMsg = '';
try
    hWord = getWordObject();
catch ME
    errMsg = getString(message('SimulinkBlocks:docblock:WordActiveXServer',ME.message));
    return;
end

% Make Word window visible
try
    hWord.Visible = true;
catch ME
    errMsg = getString(message('SimulinkBlocks:docblock:CannotOpenWord',ME.message));
    return;
end

% See if the document is already open
hDocs = hWord.documents;
openCount = hDocs.count;
    
found = false;
i = 0;
while (~found && (i < openCount))
    i = i + 1;

    hDoc = hDocs.Item(i);
    if strcmpi(hDoc.FullName, fileName)
        found = true;
        % Bring window to the front
        try
            if (strcmpi(hDocs.Parent.WindowState, 'wdWindowStateMinimize'))
                hDocs.Parent.WindowState = 'wdWindowStateNormal';
            end
            hWord.Activate;
            hDocs.Item(i).Activate;
        catch ME
            errMsg = getString(message('SimulinkBlocks:docblock:WindowFocus',ME.message));
            return;
        end
    end

end

if ~found 
    try
        hDoc = hDocs.Open(fileName, [], 0);
        hWord.Activate;
    catch ME
        errMsg = getString(message('SimulinkBlocks:docblock:CannotOpenFile', ...
                                   fileName,...
                                   ME.message));
    end
end


%Turn dirty flag off so Word doesn't query when closing
try
    hDoc.Saved = 1;
catch ME %#ok
    %noop, this is not a critical error.  don't report it.
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function errMsg = close_word(fileName)

errMsg = '';
try
    hWord = getWordObject('get_only');
catch ME
    errMsg = getString(message('SimulinkBlocks:docblock:WordActiveXServer',ME.message));
    return;
end

if isempty(hWord)
    % No running word application, nothing to save
    return
end

hDocs = hWord.documents;
openCount = hDocs.count;
for i = 1:openCount
    hDoc = hDocs.Item(i);
    if strcmpi(hDoc.FullName, fileName)
        try
            invoke(hDoc,'Close',false);
        catch ME
            warning(message('SimulinkBlocks:docblock:CloseFile',ME.message));
        end
        break;
    end
end

if (hDocs.count == 0)
    %we don't want to quit if we are background printing or saving
    while get(hWord,'BackgroundPrintingStatus') || ...
            get(hWord,'BackgroundSavingStatus')
        pause(.25);
    end
    
    try
        invoke(hWord,'Quit',false);
        getWordObject('delete');
    catch ME %#ok
        %noop, this is not a critical error.  don't report it
    end
    
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function errMsg = save_word(fileName)

errMsg = '';
try
    hWord = getWordObject('get_only');
catch ME
    errMsg = getString(message('SimulinkBlocks:docblock:WordActiveXServer',ME.message));
    return;
end

if isempty(hWord)
    % No running word application, nothing to save
    return;
end

hDocs = hWord.documents;
openCount = hDocs.count;
for i = 1:openCount
    hDoc = hDocs.Item(i);
    if strcmpi(hDoc.FullName, fileName)
        try
            invoke(hDoc,'Save');
        catch ME
            warning(message('SimulinkBlocks:docblock:UnableToSaveFile',ME.message));
        end
        break;
    end
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function out = getWordObject(varargin)

persistent hWord

mlock();
option = '';
if (nargin > 0)
    option = varargin{1};
end

switch option
    case 'get_only'
        try 
            % Make sure word object is still valid
            hWord.Version;
        catch
            % Word object is invalid, clear it
            delete(hWord);
            hWord = [];
        end
   case 'delete'
        try
            hWord.Quit();
        catch
        end
        delete(hWord);
        hWord = [];
    otherwise
        try
            % Make sure word object is still valid
            hWord.Version;
        catch
            try
                % Try if Word already running
                hWord = actxGetRunningServer('word.application'); 
            catch
                % Create new Word object
                hWord = actxserver('word.application');
            end
        end
end
out = hWord;

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function setEditorTXT(editorCmd)  %called from command line
%Sets the command used to edit text pages.
%The substring "%<FileName>" will be replaced with the full path name

s = getSetting();
s.editor.TXT.PersonalValue = char(editorCmd);

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function editorCmd = getEditorTXT
%Returns the command used to edit text files.  See also getEditorTXT

s = getSetting();
editorCmd = s.editor.TXT.ActiveValue;

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
function setEditorHTML(editorCmd)  %called from command line
%Sets the command used to edit HTML pages.
%The substring "%<FileName>" will be replaced with the full path name

s = getSetting();
s.editor.HTML.PersonalValue = char(editorCmd);

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
function editorCmd = getEditorHTML
%Returns the command used to edit HTML files.  See also getEditorHTML

s = getSetting();
editorCmd = s.editor.HTML.ActiveValue;

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
function setEditorDOC(editorCmd)  %called from command line
%Sets the command used to edit .doc and .rtf pages.
%The substring "%<FileName>" will be replaced with the full path name

s = getSetting();
s.editor.DOC.PersonalValue = char(editorCmd);

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
function editorCmd = getEditorDOC
%Returns the command used to edit .doc and .rtf files.
%See also getEditorDOC

s = getSetting();
editorCmd = s.editor.DOC.ActiveValue;

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function out = getSetting()
   
s = settings;
if ~s.hasGroup("Simulink")
    s.addGroup("Simulink");
end

ssimulink = s.Simulink;

if ~ssimulink.hasGroup("docblock")
    sdocblock = ssimulink.addGroup("docblock");
    seditor = sdocblock.addGroup("editor");
    
    for sfileType = ["HTML" "DOC" "TXT"]
        seditor.addSetting(sfileType);
        seditor.(sfileType).PersonalValue = '';
    end
end

out = s.Simulink.docblock;

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function save_documents(mdlRoot) %called from command line
%Saves all documents in a model
%Utility function - not called by the block

%disp('Saving docblock documents!');
if nargin<1
    mdlRoot = bdroot(gcs);
end

%find all subsystems
if Simulink.internal.useFindSystemVariantsMatchFilter('DEFAULT_ALLVARIANTS')
    list = find_system( mdlRoot, 'LookUnderMasks', 'all', 'IncludeCommented', 'on', ...
    'MaskType', 'DocBlock' );
else
    list = find_system( mdlRoot, 'LookUnderMasks', 'all', 'Variants', 'AllVariants', ...
        'IncludeCommented', 'on', 'MaskType', 'DocBlock' );
end

for i=1:length(list)
    save_document(list{i});   %perform potential save operation
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function fileName = save_document(blkName) %called from block callback
%CALLBACK: used as PreSaveFcn
% Must not error, or else model can not be saved

try
    fileName = getBlockFileName(blkName);
catch ME
    fileName = '';
    warning(message('SimulinkBlocks:docblock:NoTempFile',ME.message));
    return;
end

if exist(fileName,'file')
    try
        saveDirtyEditorFile(fileName);
        if ispc() && ~strcmpi(fileName(end-2:end),'TXT')
            save_word(fileName);
        end
    catch ME
        warning(ME.identifier,'%s',ME.message);
    end

    try
        file2blk(blkName,fileName);
    catch ME
        warning(ME.identifier,'%s',ME.message);
    end

end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function close_documents(mdlRoot) %called from command line
%Closes all documents in the model
%Utility function - not called by the block

if nargin<1
    mdlRoot = bdroot(gcs);
end

%find all subsystems
if Simulink.internal.useFindSystemVariantsMatchFilter('DEFAULT_ALLVARIANTS')
    list = find_system(mdlRoot, 'LookUnderMasks', 'all', 'IncludeCommented', 'on', 'MaskType', 'DocBlock');
else
    list = find_system(mdlRoot, 'LookUnderMasks', 'all', 'Variants', 'AllVariants',...
        'IncludeCommented', 'on', 'MaskType', 'DocBlock');
end

modelDirtied = false;
for i=1:length(list)
    modelDirtied = max(modelDirtied,close_document(list{i}));
end

if modelDirtied
    %@ENHANCEMENT: should turn off save listeners
    save_system(mdlRoot);
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function modelDirtied = close_document(blkName)
%closes documents if they are open in the editor
%deletes documents
%called by the block's DeleteFcn (called at model close time)
% Must not error, or else model can not be closed

modelDirtied = false;

if strcmpi(bdroot(blkName), 'simulink')
    return
end

if (nargin < 1)
    try
        blkName = gcb;
    catch
        warning(message('SimulinkBlocks:docblock:NoCurrentBlock'));
        return;
    end
end

save_document(blkName);
try
    fileName = getBlockFileName(blkName);
catch ME
    warning(message('SimulinkBlocks:docblock:NoTempFile',ME.message));
    return;
end

if exist(fileName,'file')
    try
        closeEditorFile(fileName);
        if ispc && ~strcmpi(fileName(end-2:end),'TXT')
            close_word(fileName);
        end
        delete(fileName);
    catch ME
        warning(message('SimulinkBlocks:docblock:CloseFile',ME.message));
    end
end

%Manage the life cycle of the file listener
try
    blkObj = get_param(blkName,'Object');
    if isprop(blkObj, 'DocBlockEditorFileListener')
        fListen = blkObj.DocBlockEditorFileListener;
        if ~isempty(fListen)
            fListen.dispose();
            blkObj.DocBlockEditorFileListener = [];
        end
    end
catch ME
    warning(message('SimulinkBlocks:docblock:FileListenerRemoveError',ME.message));
end

% remove from map
blkObj = get_param(blkName, 'Object');
if isprop(blkObj, 'DocBlockDocument')
    blkObj.DocBlockDocument.Name = '';
    blkObj.DocBlockDocument.Type = '';
    blkObj.DocBlockDocument.SrcBlk = '';
end
internalMapping('removeFile', fileName);

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function uncompress_rtf_document(blk)
%Uncompress rtf document for a given block

close_document(blk);
[content,format] = getContent(blk);
if strcmpi(format,'RTF_ZIP')
    content = uncompressRTFData(content);
    setContent(blk,content,'RTF');
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function uncompress_rtf_documents(mdlRoot) %called from command line
%Uncompress all rtf documents in a model
%Utility function - not called by the block

if (nargin<1)
    mdlRoot = bdroot(gcs);
end

%find all subsystems
if Simulink.internal.useFindSystemVariantsMatchFilter('DEFAULT_ALLVARIANTS')
    list = find_system(mdlRoot, 'LookUnderMasks', 'all', 'IncludeCommented', 'on',...
        'MaskType', 'DocBlock');
else
    list = find_system(mdlRoot, 'LookUnderMasks', 'all', 'Variants', 'AllVariants', ...
        'IncludeCommented', 'on', 'MaskType', 'DocBlock');
end

for i=1:length(list)
    uncompress_rtf_document(list{i});
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function compress_rtf_document(blk)
%Compress rtf document for a given block

close_document(blk);
[content,format] = getContent(blk);

if strcmpi(format,'RTF')
    content = compressRTFData(content);
    setContent(blk, content, 'RTF_ZIP');
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function compress_rtf_documents(mdlRoot) %called from command line
%Compress all rtf documents in a model
%Utility function - not called by the block

if (nargin<1)
    mdlRoot = bdroot(gcs);
end

%find all subsystems
if Simulink.internal.useFindSystemVariantsMatchFilter('DEFAULT_ALLVARIANTS')
    list = find_system(mdlRoot, 'LookUnderMasks', 'all', 'IncludeCommented', 'on', ...
        'MaskType', 'DocBlock');
else
    list = find_system(mdlRoot, 'LookUnderMasks', 'all', 'Variants', 'AllVariants',...
        'IncludeCommented', 'on', 'MaskType', 'DocBlock');
end

for i=1:length(list)
    compress_rtf_document(list{i});
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function fileName = blk2file(blkName,fileName,encoding)
%   DOCBLOCK('blk2file',BLKNAME)
%   Writes block contents to a file without creating listeners.
%  DOCBLOCK('blk2file',BLKNAME,FILENAME,ENCODING)
%   File name can optionally be specified.
%%

if nargin<2
    try
        fileName = getBlockFileName(blkName);
    catch ME
        fileName = '';
        warning(message('SimulinkBlocks:docblock:NoTempFile',ME.message));
        return;
    end
end

if nargin<3
    if strcmpi(fileName(end-2:end), 'txt')
        % TXT files must have UTF-8 encoding
        encoding = 'utf-8';
    else
        encoding = ''; %system default
    end
end

[content,format] = getContent(blkName);

if isempty(content)
    %Support legacy RTWData DocBlock
    try
        rd = get_param(blkName,'RTWdata');
        if ~isempty(rd) && isstruct(rd)
            %struct2string - LEGACY SUPPORT
            sFields = fieldnames(rd);
            content = '';
            for i=1:length(sFields)
                if strncmp(sFields{i},'document_text',13)
                    theText = unicode2native(rd.(sFields{i}));
                    content = [content,theText]; %#ok expanding, no way to 
                                                 %determine final size
                end
            end
        else
            content = getString(message('SimulinkBlocks:docblock:TypeDocumentationHere'));
        end
    catch ME
        content = ME.message;
        warning(message('SimulinkBlocks:docblock:CannotGetRTWdata',ME.message));
    end
else
    if strcmpi(format,'RTF_ZIP')
        content = uncompressRTFData(content);
    end
end

try
    [wfid,errMsg] = fopen(fileName,'w','n',encoding);
catch ME
    wfid = -1;
    errMsg = ME.message;
end

if wfid<0
    warning(message('SimulinkBlocks:docblock:CannotCreateTempFile',errMsg));
    return;
end

footerType = 0;
switch fileName(end-2:end)
    case {'rtf'}
        if ~contains(content(1:min(128,length(content))),'{\rtf')
            fwrite(wfid,['{\rtf\ansi\deff0',newline,...
                '{\fonttbl{\f0\froman Tms Rmn;}}',newline,...
                '{\stylesheet{\fs20 \snext0Normal;}}',newline,...
                '\widoctrl\ftnbj \sectd\linex0\endnhere \pard\plain \fs20 '],...
                'char*1');
            footerType = 2;
        end
    case {'tml','htm'}
        %Word expects an HTML header and will treat the file as plain text
        %if it doesn't have one
        if ~contains(content(1:min(128,length(content))),'<html')
            fwrite(wfid,['<html><head></head><body>',newline],'char*1');
            footerType = 1;
        end
        %otherwise
        %Text file - no special formatting needed
end

%deliver the payload
try
    fwrite(wfid,content,'char*1');
catch ME
    warning(message('SimulinkBlocks:docblock:WriteBlockToTempFile',ME.message));
end

%write a special footer for the doctype if necessary
switch footerType
    case 2 %RTF
        fwrite(wfid,'}','char*1');
    case 1 %HTML
        fwrite(wfid,'</body></html>','char*1');
end

fclose(wfid);


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function file2blk(blkName,fileName)
%Writes the contents of a file to the block
%Should never cause an error.


if nargin<2
    try
        fileName = getBlockFileName(blkName);
    catch ME
        warning(message('SimulinkBlocks:docblock:NoTempFile',ME.message));
        return;
    end
end

if strcmpi(fileName(end-2:end), 'txt')
    % TXT files must have UTF-8 encoding
    encoding = 'utf-8';
else
    encoding = '';
end

try
    [rfid, msg] = fopen(fileName,'r', 'n', encoding);
catch ME
    rfid = -1;
    msg = ME.message;
end

if rfid<0
    warning(message('SimulinkBlocks:docblock:CannotCreateTempFile',msg));
    return;
else
    try
        content = fread(rfid,inf,'char*1=>char')';
    catch ME
        warning(message('SimulinkBlocks:docblock:WriteBlockToTempFile',ME.message));
        fclose(rfid);
        return;
    end

    fclose(rfid);
end

switch fileName(end-2:end)
    case {'rtf'}
        content = compressRTFData(content);
        format = 'RTF_ZIP';
    case {'tml','htm'}
        format = 'HTML';
    otherwise
        format = 'TXT';
end
setContent(blkName,content,format);

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function closeEditorFile(fileName)


try
    editorDoc = matlab.desktop.editor.findOpenDocument(fileName);
    if ~isempty(editorDoc)
        editorDoc.closeNoPrompt;
    end
    
catch ME
    warning(message('SimulinkBlocks:docblock:UnableToCloseFile',ME.message));
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function saveDirtyEditorFile(fileName)
%SAVEDIRTYEDITORFILE(FILENAME)

try
    editorDoc = matlab.desktop.editor.findOpenDocument(fileName);
    if ~isempty(editorDoc)
        editorDoc.save;
    end
    
catch ME
    warning(message('SimulinkBlocks:docblock:UnableToSaveFile',ME.message));
end


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function convert_legacy(mdlName) %called from command line, obsolete
%Convert classic docblocks to new-style
%Utility function - not called by the block

if nargin<1
    mdlName = bdroot(gcb);
end

if Simulink.internal.useFindSystemVariantsMatchFilter('DEFAULT_ALLVARIANTS')
    sysList = find_system(mdlName, 'LookUnderMasks', 'all', 'IncludeCommented', 'on',...
        'BlockType', 'SubSystem');
else
    sysList = find_system(mdlName, 'LookUnderMasks', 'all', 'Variants', 'AllVariants', ...
        'IncludeCommented', 'on', 'BlockType', 'SubSystem');
end

% from the subsystem list, find the subsystems that have a pspec_txt field
% associated with it. If present, determine if a save operation is needed.
for i=1:length(sysList)
    s = get_param(sysList{i},'RTWDATA');
    if isfield(s,'document_text')
        %@BUG: DO THIS RIGHT!
        convert_legacy_block(sysList{i});
    end
end


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function convert_legacy_block(blkName)

if nargin<1
    blkName = gcb;
end

try
    oldECoderFlag = get_param(blkName,'ECoderFlag');
catch ME %#ok
    oldECoderFlag = '[]';
end

try
    oldDocumentType = get_param(blkName,'DocumentType');
catch ME %#ok
    oldDocumentType = 'Text';
end

set_param(blkName,...
    'MaskType','DocBlock',...
    'ShowName','off',...
    'CopyFcn','',...
    'OpenFcn','docblock(''edit_document'',gcb);',...
    'PreSaveFcn','docblock(''save_document'',gcb);',...
    'DeleteFcn','docblock(''close_document'',gcb);',...
    'CopyFcn','docblock(''copy_document'',gcb);',...
    'UserDataPersistent','on',...
    'MaskDescription','Use this block to save long descriptive text with the model.  Double-clicking the block will open an editor.',...
    'MaskPromptString','RTW Embedded Coder Flag|Editor Type',...
    'MaskStyleString','edit,popup(Text|RTF|HTML)',...
    'MaskTunableValueString','off,on',...
    'MaskEnableString','on,on',...
    'MaskVisibilityString','on,on',...
    'MaskToolTipString','on,on',...
    'MaskValueString','[]|Text',...
    'MaskVariables','ECoderFlag=@1;DocumentType=&2;',...
    'MaskDisplay','plot([.8 0 0 1 1 .8 .8 1],[1 1 0 0 .8 1 .8 .8]);text(.5,.6,''DOC'',''horizontalalignment'',''center'');text(.95,.05,get_param(gcb,''DocumentType''),''verticalalignment'',''bottom'',''horizontalalignment'',''right'');',...
    'MaskIconFrame','off',...
    'MaskIconOpaque','on',...
    'MaskIconRotate','none',...
    'MaskIconUnits','autoscale');

set_param(blkName,'ECoderFlag',oldECoderFlag);
set_param(blkName,'DocumentType',oldDocumentType);


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function blkName = addToSystem(sysName)

if nargin<1
    sysName = gcs;
end

if isempty(sysName)
    sysName = new_system;
    open_system(sysName);
    sysName = get_param(sysName,'name');
end

libName = 'simulink';
libH = find_system(0,...
    'searchdepth',1,...
    'type','block_diagram',...
    'blockdiagramtype','library',...
    'Name','simulink');
if isempty(libH)
    load_system(libName);
end

dBlk = find_system(libName,...
    'MaskType','DocBlock');

if isempty(dBlk)
    blkName = '';
    warning(message('SimulinkBlocks:docblock:NoDocBlock'));
    return;
else
    dv = datevec(now);
    blkName = sprintf('%s/DocBlock-%0.4i-%0.2i-%0.2i:%0.2i-%0.2i-%0.2f',...
        sysName,...
        dv(1),dv(2),dv(3),dv(4),dv(5),dv(6));
    add_block(dBlk{1},blkName);
end

if isempty(libH)
    close_system(libH);
end
set_param(0,'CurrentSystem',sysName);

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function wasDirtySet = setDirty(blkHandle,dlgPrompt)
%Note that this function is called from a java EditorFileListener with no
%second argument.

try
    %If the DocBlock is in a linked SubSystem, we want to set the
    %reference library dirty and display the locked library
    %warning dialog.
    blkParent = get_param(blkHandle,'Parent');
    if ~strcmp(get_param(blkParent,'Type'),'block_diagram')
        parentLinkStatus = get_param(blkParent,'LinkStatus');
        % {'none'  'resolved'  'unresolved'  'implicit'}
        %"restore" and "propagate" are write-only actions for LinkStatus
        %"none","inactive", and "unresolved" will have an empty ReferenceBlock
        if any(strcmp({
                'resolved'
                'implicit'
                },parentLinkStatus))
            blkHandle = get_param(blkParent,'ReferenceBlock');
            %Switch the block handle to be the reference block, so
            %the root of the block is the library block_diagram
        end
    end
catch ME
    warning(message('SimulinkBlocks:docblock:InvalidHandle',ME.message));
    return;
end


try
    rootSys = bdroot(blkHandle);

    % if there is a harness block version, use harness model instead
    harnessBlk = systemBlkToHarnessBlk(blkHandle);
    if ~isempty(harnessBlk)
        rootSys = bdroot(harnessBlk);
    end
        
catch ME
    warning(message('SimulinkBlocks:docblock:InvalidHandle',ME.message));
    return;
end

try
    slInternal('notifySSRefAboutBlockEdit', get_param(blkHandle, 'Handle'));
    set_param(rootSys,'dirty','on');
    wasDirtySet = true;
catch ME1 %#ok
    wasDirtySet = false;

    if nargin<2
        dlgPrompt = getString(message('SimulinkBlocks:docblock:ModifyLockedLibraryPrompt'));
    end

    optUnlock = getString(message('SimulinkBlocks:docblock:UnlockButton'));
    optCancel = getString(message('SimulinkBlocks:docblock:IgnoreButton'));
    optSelected = questdlg(sprintf(dlgPrompt,rootSys),...
        getString(message('SimulinkBlocks:docblock:ReadOnlyTitle')),...
        optUnlock,optCancel,optCancel);
    switch optSelected
        case optUnlock
            try
                set_param(rootSys,'lock','off');
                set_param(rootSys,'dirty','on');
                wasDirtySet = true;
            catch ME2
                warnmsg = getString( ...
                    message('SimulinkBlocks:docblock:CannotUnlockBlockDiagram', ME2.message));

                % strip link
                warnmsg = regexprep(warnmsg, '<a\s+href\s*=\s*"[^"]*"[^>]*>(.*?)</a>', '$1');
                
                % warn dialog to prompt user
                warndlg(warnmsg, ...
                    getString(message('SimulinkBlocks:docblock:ReadOnlyTitle')), ...
                    'modal');
            end
        %case optIgnore
        %Idea: Use fileattrib to make the file read-only?
        otherwise %optCancel
            %Noop, for now.
    end
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function checkEditorFileAndMarkBlockDirty(filename, blkHandle)

if matlab.desktop.editor.isOpen(filename)
    docblock('setDirty', blkHandle);
end


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function [content,format] = getContent(blkName)
% need save as hooks?

try
    %
    % read from the file if the file is updated.
    % 
    mdlFileName = get_param(bdroot(blkName), 'FileName');
    docblockFileName = getBlockFileName(blkName);    
    if exist(docblockFileName, 'file') == 2
        if ~(exist(mdlFileName, 'file') == 2)
            % if model is new and has not been saved.
            file2blk(blkName, docblockFileName);
        else            
            d = dir(mdlFileName);
            mdlFileTime = d.datenum;
            d = dir(docblockFileName);
            if mdlFileTime < d.datenum
                file2blk(blkName, docblockFileName);
            end        
        end    
    end
    
    docBlockUserData = get_param(blkName,'UserData');
    
    if ~isstruct(docBlockUserData)
        % If we have characters outside ASCII, then we may have to convert
        % to unicode.  Note.  When we add pictures in RTF, binary data are
        % escaped to their HEX value.
        startExtendedASCII = 128;
        endExtendedASCII = 255;
        if ~isempty(find(docBlockUserData>startExtendedASCII & ...
                         docBlockUserData<endExtendedASCII,1))
            % User defined encoding
            encoding = get_param(0,'CharacterEncoding'); % best guess
            
            % Convert string to UTF16
            content = native2unicode(double(docBlockUserData), encoding);
        else
            % UTF16 encoding
            content = char(docBlockUserData);
        end
        format = 'UNDEF';
    else
        % UTF16 encoding
        content = docBlockUserData.content;
        if ~isfield(docBlockUserData,'format')
            format = 'UNDEF';
        else
            format = docBlockUserData.format;
        end
    end
    
    % force content to be always 1xN character array
    content = reshape(content,1,[]);
    
    if strcmpi(format,'UNDEF')
        if contains(content(1:min(128,length(content))),'{\rtf')
            format = 'RTF';
        end
    end
    
catch ME
    content = ME.message;
    format  = 'UNDEF';
    warning(message('SimulinkBlocks:docblock:UnableToGetContent',ME.message));
end


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function setContent(blkName,content,format)

docblockVersion = 1.2;

try
    % Save it is a structure.  Also save version info
    docBlockUserData.version = docblockVersion;
    docBlockUserData.content = content;
    docBlockUserData.format  = format;

    %If writing to a locked model, this will cause an error
    set_param(blkName,...
        'RTWdata',[],...                %provided to clear out legacy docblocks
        'UserDataPersistent','on',...
        'UserData',docBlockUserData);
catch ME
%    Don't make this block with an interactive dialog.  We don't want to
%    interrupt the "save" process with dozens (hundreds?) of warning
%    dialogs.
%    warndlg(sprintf('Unable to save DocBlock changes to model.\n\n(%s)',lasterr),...
%        'DocBlock Save Error');
    warning(message('SimulinkBlocks:docblock:UnableToSetContent',ME.message));
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function compData = compressRTFData(data)
% Compress RTF contents only (ASCII data required)

if any(data>255)
    error(message('SimulinkBlocks:docblock:RTFNonAscii'));
end

compData = mlreportgen.utils.internal.compress(data);

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function data = uncompressRTFData(compData)

data = mlreportgen.utils.internal.decompress(compData);

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function systemBlk = harnessBlkToSystemBlk(harnessBlkHandle)
    
systemBlk = '';
harnessRootSys = bdroot(harnessBlkHandle);
if strcmp(get_param(harnessRootSys, 'IsHarness'), 'on')
    systemModel = Simulink.harness.internal.getHarnessOwnerBD(harnessRootSys);
    activeHarness = Simulink.harness.internal.getActiveHarness(systemModel);
    if (~isempty(activeHarness) && strcmp(activeHarness.ownerType, 'Simulink.SubSystem'))
        
        blk = harnessBlkHandle;
        systemBlk = '';
        parentBlk = get_param(blk, 'Parent');
        grandParentBlk = get_param(parentBlk, 'Parent');
        while ~isempty(grandParentBlk)
            if ~isempty(systemBlk)
                systemBlk = [get_param(blk, 'Name') '/' systemBlk]; %#ok
            else
                systemBlk = get_param(blk, 'Name');
            end
            
            blk = parentBlk;
            parentBlk = get_param(blk, 'Parent');
            grandParentBlk = get_param(parentBlk, 'Parent');
        end
        if ~isempty(systemBlk)
            systemBlk = [activeHarness.ownerFullPath '/' systemBlk];
        end
        
        % check to make sure test harness version exists.
        r = slroot;
        if ~r.isValidSlObject(systemBlk)
            systemBlk = '';
        end
    end
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function harnessBlk = systemBlkToHarnessBlk(blkHandle)
    
rootSys = bdroot(blkHandle);
activeHarness = Simulink.harness.internal.getActiveHarness(rootSys);
harnessBlk = '';

if (~isempty(activeHarness) && strcmp(activeHarness.ownerType, 'Simulink.SubSystem'))
    blkFullName = [get_param(blkHandle, 'Parent') '/' get_param(blkHandle, 'Name')];
    harnessParentBlk = get_param(activeHarness.ownerFullPath, 'Parent');
    
    blkPartialPath = blkFullName((length(harnessParentBlk)+1):end);
    if ~isempty(blkPartialPath)
        harnessBlk = [activeHarness.name blkPartialPath];
        
        r = slroot;
        if ~r.isValidSlObject(harnessBlk)
            % make blk is valid
            harnessBlk = '';
        end
    end
end