function str = toString(var, varargin)
% TOSTRING    produce string representation of any datatype
%
% S = TOSTRING(A) produces a string representation of A, where 
% class(A) can be one of 
%
%      double,   single                          
%      logical,       
%      char,          
%      int8,     uint8       
%      int16,    uint16 
%      int32,    uint32 
%      int64,    uint64  
%      cell,          
%      struct,  
%      function_handle, 
%      (user-implemented class name)   
%
% The default string represenation is as verbose as possible.
% That means the contents of structure fields, cell array 
% entries, etc. are representated in fully expanded form.
%
% S = TOSTRING(A, 'disp') produces a string representaion that 
% is identical to what the command 'disp(A)' would produce. 
%
% S = TOSTRING(A, 'compact') or S = TOSTRING(A, N) (with N a positive
% integer) limits the number of digits displayed in numerical arrays 
% to either 4 ('compact') or N. 
% 
%
% EXAMPLE 1:
%
%   >> a = struct('someField', uint32(10*rand(2)), 'otherField', {{[]}});
%   >> S = toString(a)
%
%   S =
%
%   1x1 struct:       
%    someField: [9  7]
%               [2  2]
%   otherField: { [] }
%
%
%   >> S = toString(a, 'disp')
%
%   S =
%
%        someField: [2x2 uint32]
%       otherField: []
%
%
%   EXAMPLE 2: 
%
%   >> a = rand(2,2,2,2);
%   >> S = toString(rand(2,2,3,2)
%
%     S =
% 
%       (:,:,1,1) =                                  
%       [5.501563428984222e-01	5.870447045314168e-01]
%       [6.224750860012275e-01	2.077422927330285e-01]
% 
%       (:,:,1,2) =                                  
%       [4.356986841038991e-01	9.233796421032439e-01]
%       [3.111022866504128e-01	4.302073913295840e-01]
% 
% 
%       (:,:,2,1) =                                  
%       [3.012463302794907e-01	2.304881602115585e-01]
%       [4.709233485175907e-01	8.443087926953891e-01]
% 
%       (:,:,2,2) =                                  
%       [1.848163201241361e-01	9.797483783560852e-01]
%       [9.048809686798929e-01	4.388699731261032e-01]
% 
% 
% EXAMPLE 3: 
%
%   >> a = cellfun(@(~)rand(3), cell(2), 'UniformOutput',false);
%   >> a{end} = cell(2);
%   >> a{end}{end} = @sin;
%   >> S = toString(a, 2)
%
%   S = 
%
%      {  [0.01   0.92   0.42]  [0.61   0.24   0.77]  }
%      {  [0.60   0.00   0.46]  [0.19   0.92   0.19]  }
%      {  [0.39   0.46   0.77]  [0.74   0.27   0.29]  }
%      {                                              }
%      {  [0.32   0.04   0.47]     {  []   []   }     }
%      {  [0.78   0.18   0.15]     {  []  @sin  }     }
%      {  [0.47   0.72   0.34]                        }
%
%
% See also disp, num2str, func2str, sprintf.
% source: http://www.mathworks.com/matlabcentral/fileexchange/38566-string-representation-of-any-data-type


% If you find this work useful and want to make a donation:
% https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6G3S5UYM7HJ3N



% Please report bugs and inquiries to: 
%
% Name       : Rody P.S. Oldenhuis
% E-mail     : oldenhuis@gmail.com    (personal)
%              oldenhuis@luxspace.lu  (professional)
% Affiliation: LuxSpace s�rl
% Licence    : BSD


% If you find this work useful and want to show your appreciation:
% https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6G3S5UYM7HJ3N


% Authors
%{
Rody Oldenhuis   (oldenhuis@gmail.com)
Clark Williams   (rich.dick.clark@gmail.com)
%}


% Changelog
%{
2014/February/14 (Rody Oldenhuis)
- CHANGED: Included "Authors" field, donation link, etc.
- CHANGED: Removed constructor from enum types, as it cannot be called directly
- CHANGED: Made enum types respond differently to "compact" notation
- FIXED: Integer types with user-defined accuracy never not assigned
- FIXED: warning about integers with user-defined accuracy was issued also on
         recursive call (relevant for user-defined classes). 
- FIXED: missing space in class header

2014/February/13 (Clark Williams / Rody Oldenhuis)
- NEW: support for enum types

2012/October/12 (Rody Oldenhuis)
- NEW: support for sparse matrices 

2012/October/11 (Rody Oldenhuis)
- First version
 
%}



% If you find this work useful and want to make a donation:
% https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6G3S5UYM7HJ3N


    
    %% Initialize
          
    multiD    = false;
    numDigits = inf; % maximum precision
    if nargin >= 2
        if ischar(varargin{1})
            
            switch lower(varargin{1})
                
                % return same as disp would print
                case 'disp'
                    str = evalc('disp(var)');                    
                    % instead of char(10) as \n, output multi-row char array
                    C = textscan(str, '%s',...
                        'delimiter', '\r\n',...
                        'MultipleDelimsAsOne', true);
                    str = char(C{1});                    
                    return;  
                    
                % return same as disp would print
                case 'compact'
                    numDigits = 4; %                    
                    
                % RECURSIVE CALL (FOR MULTI-D ARRAYS)
                % LEAVE UNDOCUMENTED
                case 'recursive_call'
                    multiD   = true;
                    indexString = varargin{2};
                    numDigits = varargin{3};
                    
                otherwise
                    error(...
                        'toString:unknown_option',...
                        'Unknown option: %s.', varargin{1});
            end
        
        % Manually pass number of digits
        elseif isscalar(varargin{1}) && isnumeric(varargin{1})
            numDigits = max(0, round(varargin{1}));
            
        else
            error(...
                'toString:invalid_second_argument',...
                'Second argument to toString must be a string.');
        end
    end
    
    
    %% Generate strings
    
    % handle multi-D variables 
    if ~isstruct(var) % NOT for structures
        if ndims(var)>=3
            
            a = repmat({':'}, 1,ndims(var)-3);
            
            str = [];
            for ii = 1:size(var,3)
                if ~multiD % first call
                    str = char(...
                        str, ...
                        toString( ...
                            squeeze(var(:,:,ii,a{:})), ...
                            'recursive_call', ['(:,:,' num2str(ii)],...
                            numDigits)...
                        );
                    
                else % subsequent calls
                    str = char(...
                        str, ...
                        toString( ...
                            squeeze(var(:,:,ii,a{:})), ...
                            'recursive_call', [indexString ',', num2str(ii)],...
                            numDigits), ...
                        '');
                end
            end
            
            return
            
        elseif multiD % last call
            str = char(...
                [indexString ') = '],...
                toString(var, numDigits));
            return
            
        end
    end
        
    % Empties first
    if isempty(var)
        
        if ischar(var)
            str = '''''';
            
        elseif iscell(var)
            str = '{}';
            
        % FIXME: delegate this somehow to where structs are handled
        elseif isstruct(var)
            fn = fieldnames(var);
            if ~isempty(fn)
                str = char(...
                    'empty struct with fields:',...
                    fn{:});
            else
                str = 'empty struct';
            end
            
        else
            str = '[]';
            
        end
        
        return
    end
    
    % simplest case: char
    if ischar(var)
        quote = repmat('''', size(var,1),1);
        str = [quote var quote];
        return
    end
    
    % ordinary numeric or logical array can be handled by num2str
    if isnumeric(var) || islogical(var)
        
        % integers
        if isinteger(var) || islogical(var)            
            
            str = num2str(var);
            if isfinite(numDigits)
                warning(...
                    'toString:numdigits_on_int',...
                    'The number of digits only applies to non-integer data. Ignoring...');
            end
            
        else            
            if ~isfinite(numDigits)
                if issparse(var)
                    % just use the disp version
                    str = toString(var, 'disp');                    
                    
                else
                    if isa(var, 'double')
                        str = num2str(var, '%+17.15e   ');
                    elseif isa(var, 'single')
                        str = num2str(var, '%+9.7e   ');
                    else
                        error(...
                            'toString:unknown_class',...
                            ['Unsupported numeric class: ''', class(var), '''.']);
                    end
                end
            else
                frmt = ['%+' num2str(numDigits+2) '.' num2str(numDigits), 'f   '];
                if issparse(var)
                    % just use the disp version
                    str = evalc('disp(var)');
                    
                    % apply the correct number of digits                    
                    str = textscan(str, '%s %f', ...
                        'delimiter', ' \r\n',...
                        'MultipleDelimsAsOne', true);                    
                    str{2} = num2str(str{2}, frmt);
                    str = [char(str{1}) repmat('  ', size(str{1},1),1)  str{2}];
                    
                else
                    str = num2str(var, frmt);                    
                end
                
            end
        end
            
        if numel(var) > 1            
          % modified by JSS on Jan 24, 2016:
          if 0
            % original version
            brO = repmat('[',size(str,1),1);
            brC = brO; brC(:) = ']';
            str = [brO  str  brC];
          else
            % modified version
            % purpose: make matrix retrievable from string using eval()
            brO = repmat('[',size(str,1),1);
            brC = brO; brC(:) = ']';
            brO(2:end)=' ';
            brC(1:end-1)=';';
            str = [brO  str  brC];
            str = reshape(str',[1 numel(str)]);
          end
        end
        
        return;
        
    end
    
    % Cell arrays
    if iscell(var)
        
        strreps = cellfun(@(x)toString(x,numDigits), var, 'UniformOutput', false);
        
        rows = max(cellfun(@(x)size(x,1), strreps),[],2);
        cols = max(cellfun(@(x)size(x,2), strreps),[],1);
        
        str = [];
        for ii = 1:size(strreps,1)            
            
            space  = repmat(' ', rows(ii),2);
            braceO = repmat('{', rows(ii),1);
            braceC = braceO; braceC(:) = '}';
            
            newentry = braceO;
            for jj = 1:size(strreps,2)
                newentry = [...
                    newentry,...
                    space,...
                    center(strreps{ii,jj}, rows(ii), cols(jj))]; %#ok FIXME: growing array
            end
            newentry = [newentry space braceC]; %#ok FIXME: growing array
            
            emptyline = ['{' repmat(' ', 1,size(newentry,2)-2) '}'];
            if ii == 1                
                str = char(newentry);
                
            else
                if rows(ii) == 1 
                    str = char(str, newentry);
                else
                    str = char(str, emptyline, newentry);
                end
            end
            
        end
        
        return
    end
    
    % function handles
    if isa(var, 'function_handle')
        str = func2str(var);
        if str(1) ~= '@'
            str = ['@' str]; end
        return
    end
    
    % structures
    if isstruct(var)
        
        fn = fieldnames(var);
        
        sz = num2str(size(var).');
        sz(:,2) = 'x';  sz = sz.';
        sz = sz(:).';   sz(end) = [];
        
        if isempty(fn)
            if numel(var) == 0
                str = 'empty struct';
            else
                str = [sz ' struct with no fields.'];
            end
            
        elseif numel(var) == 1
            str = [sz ' struct:'];
            str = append_list(var, str,fn,numDigits);
            
        else
            str = char(...
                [sz ' struct array with fields:'],...
                fn{:});
        end
        
        return;
    end
    
    
    % If we end up here, we've been given a classdef'ed object
    % --------------------------------------------------------
           
    name   = class(var);
    
    supers = superclasses(var);
    methds = methods(var);
    props  = properties(var);
    evnts  = events(var);
    enums  = enumeration(var);
    
    % We'll be calling toString recursively; kill this warning, because it
    % doesn't apply to this case
    warnState = warning('off', 'toString:numdigits_on_int');
    
    % Compact display for enums
    if isfinite(numDigits) && ~isempty(enums)
        str = [name '.' char(var)];
        % Don't forget to reinstate this warning
        warnint(warnState);
        return 
    end    
    
    % Class header
    if numel(supers) > 1
        supers = [
            cellfun(@(x) [x ' -> '], supers(1:end-1), 'UniformOutput', false)
            supers(end)];
        supers = [supers{:}];
    elseif ~isempty(supers)
        supers = supers{:};
    end    
    if (isempty(supers)) % BUGFIX: (Clark) empties were not handled nicely
        str = ['class ' name ', no subclasses.'];
    else        
        str = ['class ' name ', subclass of ' supers];
    end
    
    % Properties
    if ~isempty(props)
        str = char(str, '', 'Properties:', '------------');
        str = append_list(var, str,props, numDigits);
    else
        str = char(str, '','', '<< No public properties >>');
    end
    
    % Methods
    if ~isempty(methds)
        str = char(str, '', '', 'Methods:', '------------');
        
        % NOTE: remove constructur for enums
        if ~isempty(enums)
            methds = methds(~strcmpi(methds, name)); end
        
        methds = append_string(right_align(methds), '()');
        str = char(str, methds{:});
    else
        str = char(str, '','', '<< No public methods >>');
    end
    
    % Enumerations
    if ~isempty(enums)
        str = char(str, '', '', 'Enumerations:', '------------');
        
        enumlabels = cell(size(enums));
        for k = 1 : numel(enums)
            % BUGFIX: (Clark) highlight current value
            if var == enums(k)
                enumlabels{k} = ['<<' char(enums(k)) '>>'];
            else
                
                enumlabels{k} = ['  ' char(enums(k)) '  '];
            end
        end
        enumlabels = right_align(enumlabels);
        
        str = char(str, enumlabels{:});
    else
        str = char(str, '','', '<< No public enumerations >>');
    end
    
    % Events
    if ~isempty(evnts)
        str = char(str, '','', 'Events:', '------------');
        evnts = right_align(evnts);
        str = char(str, evnts{:});
    else
        str = char(str, '','', '<< No public events >>');
    end
    
    % Don't forget to reinstate this warning
    warning(warnState);
    
end



% STRING MANIPULATION
% --------------------------------------------------


% pad (cell) string with spaces according to required field width
function str = prepend_space(fw, str)    
    if iscell(str)
        str = cellfun(@(x) prepend_space(fw,x), str, 'UniformOutput', false);        
    else
        str = [repmat(' ', size(str,1),fw-length(str)) str];        
    end    
end


% make a displayable "block" of a single key and possibly many values
function str = make_block(key, value)
    
    if size(value,1) > 1
        key = [key; repmat(' ', size(value,1)-1, size(key,2))]; end
    
    str = [key value];
end


% right-align all entries in a cell string
function list = right_align(list)
    list = prepend_space(max(cellfun(@length,list)), list);
end


% center-align (horizontal and vertical) a character array 
% according to given block size
function str = center(str, rows,cols)        
    
    [sz1, sz2] = size(str);
    
    if sz2 < cols || sz1 < rows
        
        ctr = max(1, ceil([rows/2-(sz1-1)/2  cols/2-(sz2-1)/2]));
        newstr = repmat(' ', rows,cols);        

        for ii = 1:sz1
            newstr(ctr(1)+ii-1, (0:length(str(ii,:))-1)+ctr(2) ) = str(ii,:); end        
        
        str = newstr;
    end  
    
end


% append a string to every entry in a cell string
function list = append_string(list, string)    
    if iscell(list)
        list = cellfun(@(x) [x string], list, 'UniformOutput', false);
    else
        list = [list string];
    end    
end


% append a set of keys and their evaluated values to a list
function str = append_list(var, str,list,numDigits)
    
    for ii = 1:size(list,1)
        list{ii,2} = toString(var.(list{ii,1}),numDigits); end
    
    list(:,1) = append_string(right_align(list(:,1)), ': ');
    
    for ii = 1:size(list,1)
        str = char(str, make_block(list{ii,:})); end
    
end