function [are_equal,errorstr] = objcmp(S1,S2,tol,ignored)
% function [are_equal] = objcmp(S1,S2,tol,ignored)
% compares two objects 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
% use cell array ignored to exclude certain fields from the comparison
%
% F. Felici SPC 2018
%
% [+GenLib General Purpose Library+] Swiss Plasma Center EPFL Lausanne 2022. All rights reserved.

if nargin<4
  ignored = {};
end
if nargin<3
  tol = 100*eps;
end

myfieldnames1 = setdiff(fieldnames(S1),ignored,'stable');
myfieldnames2 = setdiff(fieldnames(S2),ignored,'stable');

are_equal=true; % init

%% check class
assert(isequal(class(S1),class(S2)),...
  'Class of inputs is different %s vs %s',class(S1),class(S2));

errorstr = '';

%% check existence of all fields in each object
[~,i1,i2] = setxor(myfieldnames1,myfieldnames2);
if ~isempty(i1)
  newerrorstr = sprintf('Fields {%s} do not exist in the second object/structure\n',strjoin(myfieldnames1(i1),','));
  errorstr = [errorstr,newerrorstr]; %#ok<*AGROW>
  are_equal = false;
end
if ~isempty(i2)
  newerrorstr = sprintf('Fields {%s} do not exist in the first  object/structure\n',strjoin(myfieldnames2(i2),','));
  errorstr = [errorstr,newerrorstr];
  are_equal = false;
end

%% check values
for ifield = 1:numel(myfieldnames1)
  myfieldname = myfieldnames1{ifield};
  
  if ~any(i1 == ifield)
    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]; %#ok<*AGROW>
    end
  end
end

%% Display error string
if ~are_equal
  fprintf(errorstr);
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

has_strings = ~verLessThan('matlab', '9.1'); % strings introduced in 9.1

if isstruct(fieldval1) || (isobject(fieldval1) && ~(has_strings && isstring(fieldval1))) % Strings are MATLAB objects
  % recursive call
  isequal = objcmp(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 [areequal,errorstr] = compare_value(val1,val2,tol)
errorstr = '\n';

has_strings = ~verLessThan('matlab', '9.1'); % strings introduced in 9.1

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

if isnumeric(val1)
  if isequaln(val1,val2) % matlab built-in
    areequal = true;
  else
    err = relerr(val1,val2);
    areequal = err<tol; % small numerical errors are accepted on different platforms
    if ~areequal, errorstr = sprintf('\n  different numerical values, relative error: %3.3g\n',err);end
  end
elseif islogical(val1)
  if isequaln(val1,val2)
    areequal = true;
  else
    areequal = false;
    errorstr = sprintf('\n  different logical values in indices: %d',val1(:)~=val2(:));
  end
elseif (ischar(val1) || (has_strings && isstring(val1)))
  if strcmp(val1,val2)
    areequal = true;
  else
    areequal = false;
    errorstr = sprintf('\n  different strings: %s , %s\n',val1,val2);
  end
elseif isa(val1,'function_handle')
  areequal = strcmp(func2str(val1),func2str(val2));
  if ~areequal
    err1 = sprintf('\n  different function handles: %s , %s\n',func2str(val1),func2str(val2));
    func1 = functions(val1);
    func2 = functions(val2);
    if ~isequal(func1.type,func2.type)
      err2 = sprintf('different function handle types: %s vs %\n',func1.type,func2.type);
    else
      if ~isequal(func1.function,func2.function)
        err2 = sprintf('different function handle functions: %s vs %s \n',func1.function,func2.function);
      else
        switch func1.type
          case 'simple'
            if ~isequal(func1.file,func2.file)
              if isempty(func1.file); f1.file='[empty]'; end
              if isempty(func2.file); f2.file='[empty]'; end
              err2 = sprintf('\n  function handles pointing to different files: %s , %s\n',f1.file,f2.file);
            else
              error('uncaught difference between simple function handles - this should not happen')
            end
          case 'anonymous'
            if ~objcmp(func1.workspace,func2.workspace)
              err2 = sprintf('different function handle workspaces\n');
            else
              areequal = true; % assume that if the function and workspace are the same, the function handle is the same
            end
          case 'scopedfunctions'
            error('scoped functions errors not handled yet')
          otherwise
            error('don''t know how to treat function handles of type %s',func1.type)
        end
      end
    end
    errorstr = [err1,err2];
  end
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)
% Check that NaN values match
if ~isequal(isnan(val1),isnan(val2)),             err=NaN; return; end
% Check that Inf values match
if ~isequal(isinf(val1),isinf(val2)),             err=NaN; return; end
if ~isequal(val1(isinf(val1)),val2(isinf(val2))), err=NaN; return; end
% Compare the finite values
val1 = val1(isfinite(val1)); val2 = val2(isfinite(val2));
val1 = double(val1); val2=double(val2);
dv = val1(:)-val2(:);
err =  sqrt(sum(dv.^2))/(max(norm(val1(:)),norm(val2(:)))+eps);
end
