function str = Var2Str(varargin)
% str = Var2Str(in,name)
%
% Takes variable IN and creates a string representation of it that would
% return the original variable when fed to eval(). NAME is the name of the variable
% that will be printed in this string.
% Can process any (combination of) MATLAB built-in datatype
%
% examples:
%   Var2Str({4,7,'test',@exp})
%   ans =
%       {4,7,'test',@exp}
%
%   var.field1.field2 = {logical(1),'yar',int32(43),[3 6 1 2],@isempty};
%   Var2Str(var,'var2')
%   ans =
%       var2.field1.field2 = {true,'yar',int32(43),[3,6,1,2],@isempty};

% DN 2008-01    Wrote it.
% DN 2008-07-30 Added support for function handles
% DN 2008-07-31 Added checking of MATLAB version where needed
% DN 2008-08-06 Lessened number of output lines needed to represent
%               variable in some cases
% DN 2008-08-12 Added wronginputhandler() for easy creation of specific
%               error messages on wrong input + added sparse support
% DN 2011-06-07 Simple variables while having only one input argument
%               didn't actually work....
% DN 2011-06-08 reworked 2D+ engine and added a dispatcher to remove code
%               duplication. Also put addition of LHS to expression in one
%               common place.
% DN 2011-06-09 Added handling of empty and/or fieldless structs

% TODO: make extensible by userdefined object parser for user specified
% datatypes - this will make this function complete

% make string of values in input, output is a cell per entry
strc = dispatcher(varargin{:});

if iscell(strc)
    % align equals-signs (autistic)
    indices     = strfind(strc,'=');
    indices     = cellfun(@(x)x(1),indices);
    MaxIndex    = max(indices);
    
    for p = 1:length(strc)
        i       = indices(p);
        dif     = MaxIndex - i;
        strc{p} = [strc{p}(1:i-1) repmat(' ',1,dif) strc{p}(i:end)];
    end
    
    % make string out of the stringcells
    str = [strc{:}];
else
    str = strc;
end


% dispatch string representation creation of datatype to handler for that
% datatype
function str = dispatcher(varargin)
if isnumeric(varargin{1}) || islogical(varargin{1})
    str = numeric2str(varargin{:});
elseif ischar(varargin{1})
    str = str2str(varargin{:});
elseif iscell(varargin{1})
    str = cell2str(varargin{:});
elseif isstruct(varargin{1})
    str = struct2str(varargin{:});
elseif isa(varargin{1},'function_handle')
    str = funchand2str(varargin{1});
else
    wronginputhandler(varargin{:});
end

% add LHS if needed
if ischar(str)
    if nargin==2
        str = {[varargin{2} ' = ' str ';' char(10)]};
    end
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% functions to deal with the different datatypes

% this function handles the unsupported data types
function wronginputhandler(input,name)
if nargin == 1
    at = '';
else
    at = [char(10) 'Error at ' name];
end
switch class(input)
    case 'inline'
        error('Inline functions not supported, they are deprecated.\nPlease see ''help function_handle'' for information on an alternative.%s',at)
    otherwise
        error('input of type %s is not supported.%s',class(input),at)
end

% this function handles numerical and logical data types
function str = numeric2str(input,name)

psychassert(isnumeric(input)||islogical(input),'numeric2str: Input must be numerical or logical, not %s',class(input))

if numel(input)>4 && isscalar(unique(input))
    % special case, all same value
    str = constant2str(input);
elseif isempty(input) || ndims(input)<=2
    if ismember(class(input),{'double','logical'})
        str = mat2str(input,17);
    else
        % for any non-double datatype, preserve type - double is default
        str = mat2str(input,17,'class');
    end
    str = regexprep(str,'\s+',' ');
    % check for "Infi" which is an infinity imaginary component.
    % to recreate that, we need to replace it with: complex(0, inf)
    str = strrep(str,'Infi','complex(0, inf)');
else
    psychassert(nargin==2,'input argument name must be defined if processing 2D+ matrix');
    str = mat2strhd(input,name);
end

% this function handles the char data type -> strings
function str = str2str(input,name)

psychassert(ischar(input),'str2str: Input must be char, not %s',class(input))

if numel(input)>4 && isscalar(unique(input))
    % special case, all same value
    str = constant2str(input);
elseif isempty(input) || ndims(input)<=2
    str = mat2str(input,17);
else
    psychassert(nargin==2,'input argument name must be defined if processing 2D+ string matrix');
    str = mat2strhd(input,name);
end

% this one for cells
function str = cell2str(input,name)

psychassert(iscell(input),'cell2str: Input must be cell, not %s',class(input));

qstruct     = IsACell(input,@isstruct);         % recursively check if there is any struct in the cell
q2dplus     = IsACell(input,@(x)ndims(x)>2);    % recursively check if there is any element in the cell extending over more than 2 dimensions


if isempty(input)
    if ndims(input)==2 && all(size(input)==0)
        str = '{}';
    else
        s   = size(input);
        str = ['cell(' regexprep(mat2str(s,17),'\s+',',') ')'];
    end
elseif ~qstruct && ~q2dplus
    [nrow ncol] = size(input);
    str         = '{';
    % process cell per element
    for p = 1:nrow
        for q=1:ncol
            if isstruct(input{p,q})
                error('structs should not be processed here in any circumstance');
            else
                str = [str dispatcher(input{p,q})];
            end
            
            if q~=ncol
                str = [str ','];
            end
        end
        if p~=nrow
            str = [str ';'];
        end
    end
    str = [str '}'];
else
    psychassert(nargin==2,'input argument name must be defined if processing 2D+ cell or cell containing structs');
    str = cell2strhd(input,name);
end

% and this one for structs
function strc = struct2str(input,name)

if ~isstruct(input)
    error('Input is not struct')
end

%%%%%
fields  = fieldnames(input);
strc    = [];

if isempty(fields)
    % handle case of no fields
    strc = 'struct()';
    
    if ~isscalar(input)
        sizestr = regexprep(mat2str(size(input),17),'\s+',' ');
        strc    = ['repmat(' strc ',' sizestr ')'];
    end
elseif all(arrayfun(@(x) all(structfun(@(y) ndims(y)==2 && all(size(y)==0) && isa(y,'double'),x)),input))
    % handle case of all fields default-empty (0x0 double)
    nf = length(fields);
    fields = MergeCell('''',fields,''',[]');
    strc = cell2mat(['struct(' Interleave(fields,repmat(',',1,nf-1)) ')']);
    
    if ~isscalar(input)
        sizestr = regexprep(mat2str(size(input),17),'\s+',' ');
        strc    = ['repmat(' strc ',' sizestr ')'];
    end
else
    % struct has actual data, process
    psychassert(nargin==2,'input argument name must be defined if processing a struct');
    
    if isscalar(input)
        fields = fieldnames(input);
        for r=1:length(fields)
            wvar = input.(fields{r});
            namesuff = ['.' fields{r}];
            
            strc = [strc; dispatcher(wvar,[name namesuff])];
        end
    else
        strc = struct2strnonscalar(input,name);
    end
end



function str = funchand2str(input)

psychassert(isa(input,'function_handle'),'funchand2str: Input must be a function handle, not %s',class(input));

str = func2str(input);
if str(1)~='@'
    str = ['@' str];
end


%%%% function for special case of constant array
function str = constant2str(in)
item = unique(in);
s    = size(in);

sizestr = regexprep(mat2str(s,17),'\s+',' ');
itemstr = dispatcher(item);

if islogical(item)
    str = [itemstr '(' strrep(sizestr(2:end-1),' ',',') ')'];
else
    str = ['repmat(' itemstr ',' sizestr ')'];
end
return;



%%%% HD functions for variables of more than 2 non-singleton dimensions

function strc = mat2strhd(in,name)

s = size(in);
% unwrap all higher dimensions into a 2D mat, e.g., make a 4x2x3 into a
% 12x2 where every four rows contain one element from the third dimension
in=permute(in,[1,3:numel(s),2]);
in=reshape(in,[],s(2));
% wrap in cell per higher-dimension element
in=num2cellStrided(in,[s(1:2)]);

% prepare output indices
idxs    = size2idxs(s,2);
fmt     = [name '(:,:' repmat(',%d',[1,length(s)-2]) ')'];

% dispatch each 2D to correct interpreter
strc = cellfun(@(x,y)  dispatcher(x,[name sprintf(fmt,y)]),in,num2cell(idxs,2));

function strc = cell2strhd(in,name)

strc = [];
siz = size(in);

qDontProcess = cellfun(@(x) ndims(x)==2 && all(size(x)==0) && isa(x,'double'),in);
for p=1:numel(in)
    if qDontProcess(p)
        continue;
    end

    [idx{1:numel(siz)}] = ind2sub(siz,p);
    namesuff = ['{' Interleave([idx{:}],repmat(',',1,length(idx)-1)) '}'];
    
    strc = [strc; dispatcher(in{p},[name namesuff])];
end

function strc = struct2strnonscalar(in,name)

strc = [];
siz = size(in);

qDontProcess = arrayfun(@(x) all(structfun(@(y) ndims(y)==2 && all(size(y)==0) && isa(y,'double'),x)),in);
for p=1:numel(in)
    if qDontProcess(p)
        continue;
    end
    
    [idx{1:numel(siz)}] = ind2sub(siz,p);
    namesuff = ['(' Interleave([idx{:}],repmat(',',1,length(idx)-1)) ')'];
    
    strc = [strc; struct2str(in(p),[name namesuff])];
end


%%% other helpers
function res = size2idxs(siz,noff)
if nargin==1
    noff = 0;
end
narg=numel(siz);
n=narg-noff;

if n
    arg=cell(n,1);
    x=cell(n,1);
    for	i=1:n
        arg{i}=1:siz(i+noff);
    end
else
    res = [];
    return;
end

if n > 1
    [x{1:n,1}]=ndgrid(arg{1:end});
    res=reshape(cat(n+1,x{:}),[],n);
else
    res=arg{:}.';
end

function res = num2cellStrided(in,stride)
% the function is only tested 2D for now, don't want to think about higher
% dims, but it might just work equally fine
siz     = size(in);
ndim    = numel(siz);
ncell   = siz./stride;
assert(~any(mod(ncell,1)),'size needs to be a multiple of stride for each dimension');

res = reshape( in , Interleave(stride,ncell) );
res = permute( res, InterLeave(1:ndim,ndim+1:ndim*2) );

res = squeeze(num2cell(res,[1:ndim]));