function [are_equal,errorstr] = structcmp(S1,S2,tol)
% function [are_equal] = structcmp(S1,S2,tol)
% compares two structures and returns flag saying if they are equal
% Numerical values are considered equal if their relative error norm
% is within tol (default tol=100*eps)
% use tol = -1 to ignore values and size of fields, only compare structure and classes
%
% to test: call structcmp()
% F. Felici SPC 2018

if nargin==0
  % test mode
  are_equal = test_structcmp;
  return
end

if isempty(S1) && isempty(S2)
  are_equal = true;
  return
end

if nargin==2
  tol = 100*eps;
end

myfieldnames1 = fieldnames(S1);

are_equal=true; % init

%% check values
errorstr = '';
for ifield = 1:numel(myfieldnames1)
  myfieldname = myfieldnames1{ifield};
  
  fieldexists = (isfield(S2,myfieldname) || isprop(S2,myfieldname));
  if fieldexists
    fieldval1 = S1.(myfieldname);
    fieldval2 = S2.(myfieldname);
    [field_is_equal,compare_errorstr] = compare_field(fieldval1,fieldval2,tol);
    
    if ~field_is_equal
      newerrorstr = sprintf('Field ''%s'' is not equal. %s\n',myfieldname,compare_errorstr);
      are_equal = false;
      errorstr = [errorstr,newerrorstr];
    end
  else
    newerrorstr = sprintf('Field ''%s'' does not exist in the second structure\n',myfieldname);
    are_equal = false;
    errorstr = [errorstr,newerrorstr];
  end
end

% also check existence of all field names in S2 in S1
[allfieldsexist2,errorstr2] = check_fieldnames_exist(S2,S1); 
if ~allfieldsexist2
  are_equal = false;
  errorstr = [errorstr,errorstr2,' in the first structure\n'];
end

if ~are_equal
  warning('structcmp:NotEqual',errorstr);
end

end

function [allfieldsexist,errorstr] = check_fieldnames_exist(S1,S2)

errorstr = ''; % init
allfieldsexist = true; % init
myfieldnames = fieldnames(S1);
for ifield = 1:numel(myfieldnames)
  myfieldname = myfieldnames{ifield};
  if ~isfield(S2,myfieldname) && ~isprop(S2,myfieldname)
    newerrorstr = sprintf('Field ''%s'' does not exist\n',myfieldname);
    errorstr=strcat(errorstr,newerrorstr);
    allfieldsexist = false;
  end
end

end

function [isequal,errorstr] = compare_field(fieldval1,fieldval2,tol)
errorstr = ''; % init
if ~strcmp(class(fieldval1),class(fieldval2))
  isequal = false;
  errorstr = sprintf('fields have a different class: %s vs %s\n',class(fieldval1),class(fieldval2));
  return
end

if isstruct(fieldval1)
  % recursive call
  isequal = structcmp(fieldval1,fieldval2,tol);
elseif (tol == -1)
  isequal = true; % do not compare values at all
elseif isempty(fieldval1)&&isempty(fieldval2)
  isequal = true;
elseif iscell(fieldval1)
  [isequal,errorstr] = compare_cell(fieldval1,fieldval2,tol);
else
  [isequal,errorstr] = compare_value(fieldval1,fieldval2,tol);
end
end

function [isequal,errorstr] = compare_value(val1,val2,tol)
errorstr = '\n';

if ~all(size(val1)==size(val2))
  isequal = false;
  errorstr = sprintf('\ndifferent size: %d vs %d\n',size(val1),size(val2));
  return
end

if isnumeric(val1) || islogical(val1)
  if isequaln(val1,val2) % matlab built-in
    isequal = true;
  elseif relerr(val1,val2)<tol
    isequal = true; % small numerical errors are accepted on different platforms
  else
    isequal=false;
    errorstr = sprintf('\n  different numerical values, relative error: %3.3g\n',relerr(val1,val2));
  end
elseif ischar(val1)
  if strcmp(val1,val2)
    isequal = true;
  else
    isequal = false;
    errorstr = sprintf('\n  different strings: %s , %s\n',val1,val2);
  end
else
  warning('value comparison not implemented yet for type ''%s''',class(val1));
end
end

function [isequal,errorstr] = compare_cell(cell1,cell2,tol)
if ~all(size(cell1)==size(cell2))
  isequal = false;
  errorstr = sprintf('\n  different cell size: [%d,%d] vs [%d,%d]\n',size(cell1),size(cell2));
  return
end

for icell = 1:numel(cell1)
  mycell1 = cell1{icell};
  mycell2 = cell2{icell};
  [isequal,errorstr] = compare_field(mycell1,mycell2,tol);
end
end

function err = relerr(val1,val2)
err =  norm(val1-val2)/(max(norm(val1),norm(val2))+eps);
end

function passed = test_structcmp
% test suite
fprintf('\nRunning structcmp tests\n\n')
S1 = struct('a',1,'b',[1 2],'S',struct('s1',1),'t',true,'C',{{1,2}},'char','mychar');

%% Check equal case
S2 = S1;
areequal=structcmp(S1,S2);
pass(1) = (areequal==true);

%% Change something
S3 = S1; S3.a = 2;
areequal=structcmp(S1,S3);
pass(2) = (areequal==false);

%%
S4 = S1; S4.c = 2;
areequal=structcmp(S1,S4);
pass(3) = (areequal==false);

%%
S5 = S1; S5.C = {1,3,3};
areequal=structcmp(S1,S5);
pass(4) = (areequal==false);

%%
S5 = S1; S5.S.s1 = 2;
areequal=structcmp(S1,S5);
pass(5) = (areequal==false);

%%
S6 = S1; S6.char = 'mychab';
areequal=structcmp(S1,S6);
pass(6) = (areequal==false);

%%
S7 = S1; tol=1e-5; S7.a = S1.a-0.1*tol; % change value less than tolerance
areequal=structcmp(S1,S7,tol);
pass(7) = (areequal==true);

%%
S8 = S1; S8.a = S1.a-1; % change value a lot
areequal=structcmp(S1,S8,inf);
pass(8) = (areequal==true);

%%
S9.A = S1; S10 = S9; S10.A.new=[];
areequal=structcmp(S9,S10);
pass(9) = ~areequal;

%%
S9.A.another = [];
areequal=structcmp(S9,S10);
pass(10) = ~areequal;
%%
passed = all(pass);
if ~passed
  error('failed stuctcmp tests')
else
  fprintf('\nPassed all tests\n')
end
end