function varargout = targets_hyperlink_manager(action, varargin)
% TARGETS_HYPERLINK_MANAGER - Constructs hyperlink text that calls into
% this function to evaluate matlab commands. This keeps the hyperlink text
% short which avoids rendering and memory issues in the MATLAB 
% command window.
%
% This function uses a persistent variable to maintain hyperlink commands,
% each of which is associated with a commandIndex. The size of this
% variable is controlled such that eventually new hyperlink commands will overwrite
% old commands and commandIndex will wrap around to 1. The persistent
% variable is protected by mlock and will not be cleared during normal
% clear operations.
%
% Duplicate commands always reuse the same storage in the persistent
% variable and resolve to the same commandIndex.
%
% varargout = targets_hyperlink_manager(action, varargin)
%
% action: 'new' | 'run' | 'get' | 'clear'
% varargin: see below for details
% varargout: see below for details
%
% [link, commandIndex] = targets_hyperlink_manager('new', linkText, commandText)
%
% Returns hyperlink text in output argument 'link' for the link text
% 'linkText' and command 'commandText'.   The 'commandIndex' output
% provides the index of the command in the persistent variable.
% If the MATLAB desktop is not in use then 'link" contains only the 'linkText'.
%
% [link, commandIndex] = targets_hyperlink_manager('new', linkText, fcnHandle, varargin)
%
% As above but command is provided in the form of a function_handle and
% variable size list of function arguments.
%
% targets_hyperlink_manager('run', commandIndex)
%
% Evaluates the command associated with 'commandIndex'.
%
% [commandText, command] = targets_hyperlink_manager('get', commandIndex)
%
% Returns: 'commandText' associated with 'commandIndex', if applicable.
%          'command' structure associated with 'commandIndex', where
%           command.fcn is the command function_handle and command.args is
%           the cell array of function arguments to be applied.
%
% targets_hyperlink_manager('clear')
%
% clear the persistent hyperlinkCommandStore variable
%

%   Copyright 2006-2021 The MathWorks, Inc.

% persistent cell array to store hyperlink commands
% for later evaluation
persistent hyperlinkCommandStore;
% persistent scalar to index hyperlinkCommandStore for adding new entries
persistent hyperlinkNextIndex;
if isempty(hyperlinkCommandStore)
    % initialize empty cache
    hyperlinkCommandStore = [];
end
if isempty(hyperlinkNextIndex)
   % initialize the pointer to the next storage location
   hyperlinkNextIndex = 1; 
end

% mlock this file so that the hyperlinkCommandStore honors any recent hyperlinks
% that may be in the command window after "clear all" is done
mlock;

switch action
    case 'new'
        % at least 2 additional args
        narginchk(3, inf)        
        % create new hyperlink
        [link, commandIndex] = i_newHyperlink(varargin{:});
        varargout{1} = link;
        varargout{2} = commandIndex;
    case 'run'
        % 1 additional arg
        narginchk(2, 2)
        commandIndex = varargin{1};
        % get command
        command = i_getCommand(commandIndex);
        % run it
        feval(command.fcn, command.args{:});
    case 'get' 
        % 1 additional arg
        narginchk(2, 2)
        commandIndex = varargin{1};
        command = i_getCommand(commandIndex);
        % make sure get API is backwards compatible for the 
        % commandString case
        if isequal(command.fcn, @i_evalCommandString)
           commandString = command.args{1}; 
        else
           % return a null command string for other types of function
           % the user can use the "command" output instead
           commandString = '';
        end        
        varargout{1} = commandString;
        % also return the full command
        varargout{2} = command;
    case 'clear'
        % no additional args
        narginchk(1, 1)
        % clear the cache of hyperlinks
        hyperlinkCommandStore = [];
        % reset ptr
        hyperlinkNextIndex = 1;
  otherwise
    rtw.pil.ProductInfo.error('pil', 'UnsupportedAction', action);
end

% create a new hyperlink command
function [link, commandIndex] = i_newHyperlink(linkText, fcn, varargin)                    
    if ischar(fcn)
        % map to function handle approach
        args = {fcn};
        fcn = @i_evalCommandString; 
    else
        rtw.connectivity.Utils.validateArg(fcn, 'function_handle');
        args = varargin;
    end    
    % search for command to avoid storing duplicates
    commandIndex = [];
    for idx = 1:length(hyperlinkCommandStore)
        command = hyperlinkCommandStore(idx);
        % match fcn & args
        if isequal(command.fcn, fcn) && isequal(command.args, args)
            commandIndex = idx;
            break;
        end        
    end
    if isempty(commandIndex)                
        % store commandText in cache at next available location
        hyperlinkCommandStore(hyperlinkNextIndex).fcn = fcn;
        hyperlinkCommandStore(hyperlinkNextIndex).args = args;
        % provide index as output
        commandIndex = hyperlinkNextIndex;
        % increment internal next index ptr
        hyperlinkNextIndex = hyperlinkNextIndex + 1;
        % limit # entries to avoid using too much memory
        % reset to start of cache and overwrite early entries with
        % later entries remaining valid
        %
        % max_size is big enough for plenty of builds, by which time the
        % user will not be evaluating really old hyperlinks
        max_size = 500;
        if hyperlinkNextIndex > max_size
            hyperlinkNextIndex = 1;
        end
    end
    hyperLink = " targets_hyperlink_manager('run'," + num2str(commandIndex) + ");";
    link = getString(message('Connectivity:target:TargetsHyperlink', {hyperLink, linkText}));
end

% get a hyperlink command
function command = i_getCommand(commandIndex)
    % check for reset hyperlinkCommand Store
    if isempty(hyperlinkCommandStore)
        rtw.pil.ProductInfo.error('pil', 'HyperlinkOutofDate');
    end
    % check for non-existent index
    if (floor(commandIndex) == commandIndex) && ...
            (commandIndex > 0) && ...
            (commandIndex <= length(hyperlinkCommandStore))
        command =  hyperlinkCommandStore(commandIndex);
    else
        rtw.pil.ProductInfo.error('pil', 'HyperlinkIndexInvalid', num2str(commandIndex));
    end
end

end

% this function is deliberately not nested in order
% to make sure it doesn't have access to a parent workspace and 
% therefore function_handles to it compare as being equal.
function i_evalCommandString(commandString)
    % eval command in base workspace        
    if ~isempty(commandString)
        evalin('base', commandString);
    end
end
