function outputString = formattedDisplayText(ary, nvPairs)
%formattedDisplayText Capture output of displaying an array
%
% s = formattedDisplayText(A) captures the display output of array A
% in string s.  The format of the output is consistent with the output
% of the DISP function, except that the trailing newline character shown
% for "loose" line spacing is omitted.  The current numeric display format
% and line spacing are used.  
%
% s = formattedDisplayText(A,"NumericFormat",format) returns the text display 
% output for array A using the specified numeric display format.  format 
% must be a valid display format and contain no spaces.  For example, the 
% "long Eng" format must be specified as "longEng".  The current 
% numeric display format is used if no value is specified.  This option has
% no effect if A does not contain numeric data.  
%
% s = formattedDisplayText(A,...,"LineSpacing",spacing) returns the display
% output for array A using the specified line spacing.  Spacing must be one
% of "loose" or "compact".  The current line spacing value is used if no
% value is specified for this option.
%
% s = formattedDisplayText(A,...,"UseTrueFalseForLogical",tf) displays
% logical values using the names "true" and "false" for 1 and 0,
% respectively.  By default, tf is false, meaning MATLAB uses 1 and 0
% for logical values.  
%
% s = formattedDisplayText(A,...,"SuppressMarkup",tf) removes the markup
% inserted into the display output, such as hyperlinks in headers and bold
% font in table variable names.  However, hyperlinks appearing as data in A
% are still displayed as hyperlinks.  By default, tf is false, meaning
% markup is not suppressed.
%
%   Examples:
%
%      % Capture the output of a numeric matrix using shortEng format
%      ary = magic(4);
%      s1 = formattedDisplayText(ary,"NumericFormat","shortEng")
%
%      % Use true and false for logical values
%      tf = logical(randi(2,4,4) - 1);
%      s2 = formattedDisplayText(tf,"UseTrueFalseForLogical",true)
%
%      % Use true and false for logical values in a struct
%      mystruct = struct("ButtonSize",2,"Enabled",true,"Visible",false);
%      s3 = formattedDisplayText(mystruct,"UseTrueFalseForLogical",true)
%
%      % Suppress markup in the display of a table
%      mytable = struct2table(struct("ButtonSize",2,"Enabled",true,"Visible",false));
%      s4 = formattedDisplayText(mytable,"SuppressMarkup",true)
%
% See also disp, format

% Copyright 2020-2021 The MathWorks, Inc.

   arguments
       ary
       nvPairs.NumericFormat string {mustBeScalarOrEmpty, validateNumericFormat(nvPairs.NumericFormat)} =  string.empty
       nvPairs.LineSpacing string {mustBeScalarOrEmpty, validateLineSpacing(nvPairs.LineSpacing)} = string.empty;
       nvPairs.UseTrueFalseForLogical (1,1) logical = false
       nvPairs.SuppressMarkup (1,1) logical = false
   end
   
   %% Process the inputs that are controlled by settings 
   
   % NumericFormat and LineSpacing are stored as settings.  If any of these
   % options are provided and the provided value differs from the current 
   % setting, then use the specified value.  Otherwise display will use 
   % the current (active) value.
   
   persistent cwnode;
   if isempty(cwnode)
         s = settings;
         cwnode = s.matlab.commandwindow;
   end
   
   if ~isempty(nvPairs.NumericFormat)
       currentFormat = cwnode.NumericFormat.ActiveValue;
       nvPairs.NumericFormat = cleanFormatValue(nvPairs.NumericFormat);
       if currentFormat ~= nvPairs.NumericFormat
           oc1 = updateSettingAndCreateCleanupFunction(cwnode,'NumericFormat',nvPairs.NumericFormat); %#ok<NASGU>
       end
   end
   
   if ~isempty(nvPairs.LineSpacing)
       currentSpacing = cwnode.DisplayLineSpacing.ActiveValue;
       nvPairs.LineSpacing = lower(nvPairs.LineSpacing);
       if currentSpacing ~= nvPairs.LineSpacing
           oc2 = updateSettingAndCreateCleanupFunction(cwnode,'DisplayLineSpacing',nvPairs.LineSpacing); %#ok<NASGU>
       end
   end
   
   %% If using true and false for logicals, convert to correct format
   if nvPairs.UseTrueFalseForLogical
       currentUseLogical = cwnode.UseTrueFalseForLogical.ActiveValue;
       if currentUseLogical ~= nvPairs.UseTrueFalseForLogical
           oc3 = updateSettingAndCreateCleanupFunction(cwnode, 'UseTrueFalseForLogical',nvPairs.UseTrueFalseForLogical); %#ok<NASGU>
       end
       if isa(ary, "logical")
           ary = matlab.display.internal.Logical(ary); %#ok<NASGU>
       end
   end
   
   %% Display the array
   
   % evalc has its own context for feature values.  If SuppressMarkup is true,
   % turn off the hotlinks flag inside the evalc call.  Changing the hotlinks
   % flag inside the context of evalc only impacts the evalc context, so we 
   % don't have to worry about the current value.  
   %
   % Turning off hotlinks tells display code not to inject hyperlinks and 
   % markup, but this does not disable the command window's ability to 
   % display markup and hyperlinks.  It is therefore possible that some 
   % hyperlinks will still be shown, for example if they appear in data.
   
   
   if nvPairs.SuppressMarkup
       outputString = string(evalc('feature(''hotlinks'', ''off'');disp(ary)'));
   else
       outputString = string(evalc('disp(ary)'));
   end
   
   %% If format is loose, strip off last newline
   if cwnode.DisplayLineSpacing.ActiveValue == "loose"
       tmp = char(outputString);
       outputString = string(tmp(1:end-1));
   end
   
end  %[formattedDisplayText]

%% Validation Functions
function validateNumericFormat(str)
% Validate that the numeric display format is valid
   if ~isempty(str)
       % Format argument is case-insensitive, but cannot have spaces
       mustBeMember(lower(str), [...
           "short", ...
           "long", ...
           "shorte", ...
           "longe", ...
           "shortg", ...
           "longg", ...
           "shorteng", ...
           "longeng", ...
           "+", ...
           "bank", ...
           "hex", ...
           "rational"]);
   end
end

function validateLineSpacing(str)
% Make sure that the line spacing option is valid.  
    if ~isempty(str)
        mustBeMember(lower(str),["loose","compact"]);
    end
end
        
%% Ensure that the state of the settings tree is property restored

function oc = updateSettingAndCreateCleanupFunction(cwnode, settingName, requestedValue)
 % The function obtains the active value for the setting specified by
 % settingName and creates the correct cleanup function depending on whether
 % the active value comes from the Temporary level or not. It then updates
 % the Temporary level in the settings tree with the specified value  
 % provided by the caller.
 %
 % NOTE that this function makes two assumptions:
 %    1.  The caller has explicitly provided a value for this setting, and
 %    2.  The specified value differs from the current active value.
 
    settingnode = cwnode.(settingName);
    currentValue = settingnode.ActiveValue;
    
    if hasTemporaryValue(settingnode)
        oc = onCleanup(@()resetTemporaryValue(settingnode, currentValue));
    else
        oc = onCleanup(@()clearTemporaryValue(settingnode));
    end
    settingnode.TemporaryValue  = requestedValue;
end

function resetTemporaryValue(node, originalValue)
    node.TemporaryValue = originalValue;
end



%% Clean the format value so it can be assigned into the settings tree

function cleanedFormat = cleanFormatValue(specifiedFormat)
    cleanedFormat = lower(specifiedFormat);
    switch cleanedFormat
        case "shorte"
            cleanedFormat = "shortE";
        case "longe"
            cleanedFormat = "longE";
        case "shortg"
            cleanedFormat = "shortG";
        case "longg"
            cleanedFormat = "longG";
        case "shorteng"
            cleanedFormat = "shortEng";
        case "longeng"
            cleanedFormat = "longEng";
    end
end

