function diffs = vardiff(fcn1, fcn2, subsref1, subsref2)
%VARDIFF  Compares two variables and returns a VariableDifferences object
%
% diffs = vardiff(fcn1,fcn2,subsref1,subsref2)
%
% All inputs are strings. The "fcn" inputs are MATLAB expressions which are
% evaluated to return the value of the variable.  The "subsref" inputs
% describe how the relevant portion of the variable is to be extracted.
% This contains field names and indices, separated by "pipe" characters.

%   Copyright 2010-2017 The MathWorks, Inc.

% Get the two variables.
var1 = eval(fcn1);
var2 = eval(fcn2);

% Get the relevant portion of each variable.  The returned "subsref" is the
% user-readable version.
if nargin > 2 && ~isempty(subsref1)
    [var1,subsref1] = i_do_subsref(var1,subsref1);
end
if nargin > 3 && ~isempty(subsref2)
    [var2,subsref2] = i_do_subsref(var2,subsref2);
end

diffs = i_vardiff(var1,var2);

s1 = i_get_summaries({var1});
diffs.setValueSummary1(s1{1});
s2 = i_get_summaries({var2});
diffs.setValueSummary2(s2{1});
if nargin>2 && ~isempty(subsref1)
    diffs.setSubsref1(subsref1);
end
if nargin>3 && ~isempty(subsref2)
    diffs.setSubsref2(subsref2);
end

end

%--------------------------------------------------------------------------
function diffs = i_vardiff(var1,var2)

if isnumeric(var1) && isnumeric(var2)
    if numel(size(var1))>2 || numel(size(var2))>2
        % Multi-dimensional array.  We need to treat this as a non-numeric
        % array (i.e. a 2D array of non-scalars).
        c1 = i_multidimensional_to_cell(var1);
        c2 = i_multidimensional_to_cell(var2);
        diffs = i_do_cell(c1,c2);
    elseif ~isreal(var1) || ~isreal(var2)
        % The simplest way to handle complex numbers is to treat them
        % just like cell arrays, and use ValueSummary objects to renderer
        % the values.
        diffs = i_do_cell(num2cell(var1),num2cell(var2),true);
    else
        try
            diffs = com.mathworks.comparisons.decorator.variable.VariableDifferences.createNumericDiffs(var1,var2);
        catch E %#ok<NASGU>
            if canTreatAsStruct(var1) && canTreatAsStruct(var2)
                diffs = i_do_struct(var1, var2);
            else
                diffs = createUnrelatedDiffs();
            end
        end
    end
elseif islogical(var1) && islogical(var2)
    if numel(size(var1))>2 || numel(size(var2))>2
        % Multi-dimensional array.  We need to treat this as a non-numeric
        % array (i.e. a 2D array of non-scalars).
        c1 = i_multidimensional_to_cell(var1);
        c2 = i_multidimensional_to_cell(var2);
        diffs = i_do_cell(c1,c2);
    else
        diffs = com.mathworks.comparisons.decorator.variable.VariableDifferences.createLogicalDiffs(double(var1),double(var2));
    end
elseif (ischar(var1) || (isstring(var1) && isscalar(var1))) && ...
       (ischar(var2) || (isstring(var2) && isscalar(var2)))
    [str1,str2] = stringdiff(var1,var2);
    diffs = com.mathworks.comparisons.decorator.variable.VariableDifferences.createStringDiffs(str1,str2);    
elseif iscell(var1) && iscell(var2)
    if numel(size(var1))>2 || numel(size(var2))>2
        % Multi-dimensional cell array.
        c1 = i_multidimensional_to_cell(var1);
        c2 = i_multidimensional_to_cell(var2);
        diffs = i_do_cell(c1,c2);
    else
        diffs = i_do_cell(var1,var2);
    end
elseif numel(var1)>1 || numel(var2)>1
    % Non-numeric, non-cell array.  Convert to a cell array for the purposes
    % of analyzing its contents.
    if numel(size(var1)) > 2 || numel(size(var2)) > 2
        % Multi-dimensional array.
        diffs = i_do_cell(i_multidimensional_to_cell(var1), i_multidimensional_to_cell(var2));
    else
        diffs = i_do_cell(convertToCell(var1), convertToCell(var2));
    end
elseif canTreatAsStruct(var1) && canTreatAsStruct(var2)
    % Structs or objects
    diffs = i_do_struct(var1,var2);
else
    % Unrelated types.
    diffs = createUnrelatedDiffs();
end
end

%--------------------------------------------------------------------------
function result = convertToCell(var)
if istable(var) || istimetable(var)
    result = table2cell(var);
else
    result = num2cell(var);
end
end

%--------------------------------------------------------------------------
function result = canTreatAsStruct(var)
result = isstruct(var) || isobject(var) || isa(var, 'handle.handle');
end

%--------------------------------------------------------------------------
function diff = createUnrelatedDiffs()
diff = com.mathworks.comparisons.decorator.variable.VariableDifferences.createUnrelatedDiffs();
end

%--------------------------------------------------------------------------
% Compares two cell arrays
function diffs = i_do_cell(var1,var2,complex_numbers)
    if nargin<3
        complex_numbers = false;
    end
    s1 = size(var1);
    s2 = size(var2);
    h = false(min(s1(1),s2(1)), min(s1(2),s2(2))); % the smaller size in each direction
    d1 = i_get_summaries(var1);
    d2 = i_get_summaries(var2);
    % Convert cell arrays to Java arrays
    rd1 = i_summaries_to_java_2d(d1);
    rd2 = i_summaries_to_java_2d(d2);
    for i=1:size(h,1)
        for j=1:size(h,2)
            h(i,j) = ~strcmp(comparisons.internal.variablesEqual(var1{i,j},var2{i,j}),'yes');
        end
    end
    if ~complex_numbers
        diffs = com.mathworks.comparisons.decorator.variable.VariableDifferences.createNonNumericDiffs(rd1,rd2,h);
    else
        diffs = com.mathworks.comparisons.decorator.variable.VariableDifferences.createComplexNumericDiffs(rd1,rd2,h);
    end
end

%--------------------------------------------------------------------------
% Compares two structures or objects.
function diffs = i_do_struct(var1,var2)
    is_structs = isstruct(var1) && isstruct(var2);
    if ~is_structs
        % To get access to private fields, convert the objects to
        % structures.  This gives a warning about exposing private fields,
        % so we suppress it. It's possible that the "struct" method will
        % throw an error, in which case we'll just look at the public
        % fields.
        w = warning('off','MATLAB:structOnObject');
        reset_warnings = onCleanup(@() warning(w));
        try
           var1 = struct(var1);
        catch E %#ok<NASGU>
        end
        try
           var2 = struct(var2);
        catch E %#ok<NASGU>
        end
        delete(reset_warnings); % restore original warning settings
    end
    % Now get the field names (which will include the private fields of
    % objects if the calls to "struct" succeeded).
    fieldnames1 = fieldnames(var1);
    fieldnames2 = fieldnames(var2);
    % Get the values of these fields.  If we can't access the value,
    % isvalid will be false for that entry.
    [fieldvalues1,isvalid1] = i_get_field_values(var1,fieldnames1);
    [fieldvalues2,isvalid2] = i_get_field_values(var2,fieldnames2);
    % Get the value summaries for the fields we were able to access.  For
    % fields we couldn't access, show "<0x0 ???>".
    descriptions1 = cell(size(fieldvalues1));
    descriptions1(isvalid1) = i_get_summaries(fieldvalues1(isvalid1));
    descriptions1(~isvalid1) = i_invalid;
    descriptions2 = cell(size(fieldvalues2));
    descriptions2(isvalid2) = i_get_summaries(fieldvalues2(isvalid2));
    descriptions2(~isvalid2) = i_invalid;
    % Now get the unique list of field names for both objects.
    allfields = unique([fieldnames1(:) ; fieldnames2(:)]);
    % Find the fields of the first object in this list.
    [~,i] = ismember(fieldnames1,allfields);
    d1 = cell(size(allfields));
    d1(i) = descriptions1;
    % Find the fields of the second object in this list.
    [~,j] = ismember(fieldnames2,allfields);
    d2 = cell(size(allfields));
    d2(j) = descriptions2;
    % Determine which of the fields are present in both objects and with a
    % different value in each.
    diffs = false(size(allfields));
    added_or_removed = false(size(allfields));
    for k=1:numel(allfields)
        both_sides = true;
        if isempty(d1{k})
            % MATLAB doesn't allow us to pass the array to Java if it has
            % empty entries in it.  Use an empty class name to indicate
            % an absent field.
            d1{k} = i_empty;
            both_sides = false;
        end
        if isempty(d2{k})
            % MATLAB doesn't allow us to pass the array to Java if it has
            % empty entries in it.  Use an empty class name to indicate
            % an absent field.
            d2{k} = i_empty;
            both_sides = false;
        end
        if both_sides
            comp = comparisons.internal.variablesEqual(var1.(allfields{k}),var2.(allfields{k}));
            diffs(k) = ~strcmp(comp,'yes');
        else
            added_or_removed(k) = true;
        end
    end
    % Handle the special case where the objects are different but we
    % haven't found any reason for it.  That's probably to do with private
    % data which we couldn't access.
    if ~any(diffs) && ~any(added_or_removed) && ...
            ~strcmp(comparisons.internal.variablesEqual(var1,var2),'yes')
        allfields{end+1} = comparisons.internal.message('message', 'comparisons:comparisons:VarDiffPrivateData');
        d1{end+1} = i_invalid;
        d2{end+1} = i_invalid;
        diffs(end+1) = true;
    end    
    
    d1 = i_summaries_to_java_1d(d1);
    d2 = i_summaries_to_java_1d(d2);
    if is_structs
        diffs = com.mathworks.comparisons.decorator.variable.VariableDifferences.createStructDiffs(allfields,d1,d2,diffs);
    else
        diffs = com.mathworks.comparisons.decorator.variable.VariableDifferences.createObjectDiffs(allfields,d1,d2,diffs);
    end
end

%--------------------------------------------------------------------------
function [fieldvalues,isvalid] = i_get_field_values(var,fieldnames)
    if isobject(var)
        % Casting a MATLAB object to a struct exposes hidden fields and
        % provokes warnings.  Getting field names and then querying them
        % individually does not.
        fieldvalues = cell(size(fieldnames));
        isvalid = true(size(fieldvalues));
        for i=1:numel(fieldnames)
            try
                fieldvalues{i} = var.(fieldnames{i});
            catch E
                fieldvalues{i} = ['??? ' E.message];
                isvalid(i) = false;
            end
        end
    else
        fieldvalues = struct2cell(struct(var));
        isvalid = true(size(fieldvalues));
    end
end

%--------------------------------------------------------------------------
function cells = i_multidimensional_to_cell(var)
    % We want to handle e.g. a 3*4*5*6 matrix as a 3*4 matrix of
    % 5*6 matrices.
    sizes = size(var);
    cells = cell(sizes(1),sizes(2));
    substr = repmat({':'},1,numel(sizes)-2);
    cellsize = sizes(3:end);
    for i=1:size(cells,1)
        for j=1:size(cells,2)
            subcell = [{i,j},substr{:}];
            % Calling subrefs in this form returns, e.g. a 1*1*5*6 double,
            % so reshape it to 5*6 for easier handling later.
            cells{i,j} = i_reshape(subsref(var,struct('type','()','subs',{subcell})),cellsize);
        end
    end
end

%--------------------------------------------------------------------------
% Extracts a specified portion of the variable.  "exp" is a subsref
% expression with field names and indices separated by "pipe" characters.
% This expression is returned in a user-readable format.
function [var,exp] = i_do_subsref(var,exp)
    if any(exp=='|')
        % Multiple entries.  Process them one at a time.
        exp = textscan(exp,'%s','delimiter','|');
        exp = exp{1};
        for i=1:numel(exp)
            [var,exp{i}] = i_do_subsref(var,exp{i});
        end
        exp = [ exp{:} ];
        return;
    end
    if exp(1)=='.'
        fieldname = exp(2:end);
        if strcmp(fieldname, comparisons.internal.message('message', 'comparisons:comparisons:VarDiffPrivateData'))
            % This is the dummy "private data" field that we added above
            % because there were differences in accessible fields of the
            % objects.
            comparisons.internal.message('error', 'comparisons:comparisons:VarDiffDataInaccessible');
        end
        try
            % Field dereference in a struct or object
            var = var.(fieldname);
        catch E
            if strcmp(E.identifier,'MATLAB:class:GetProhibited')
                % Failed to access the field.  Try converting to struct.
                w = warning('off','MATLAB:structOnObject');
                reset_warnings = onCleanup(@() warning(w));
                try
                    var = struct(var);
                    var = var.(fieldname);
                catch %#ok<CTCH>
                    rethrow(E); % throw the previous error
                end
                delete(reset_warnings); % restore warning settings
            end
        end
    else
        % Indexing of some sort. The subsref format does not include
        % brackets so we need to add them ourselves, but the type of
        % brackets we need depends on the data.
        if iscell(var)
            varsize = size(var);
            if numel(varsize)==2
                % Cell subsref returning the contents of a single cell.
                exp = ['{' exp ',:}'];
                var = eval(['var' exp]);
            else
                % Cell subsref returning a portion of the array (which is
                % still a cell array, so we use normal brackets).  We need
                % to explicitly set the shape of the section.
                exp = ['(' exp ',:)'];
                var = i_reshape(eval(['var' exp]),varsize(3:end));
            end
        elseif istable(var) || istimetable(var)
            exp = ['{' exp '}'];
            var = eval(['var' exp]);
        else
            % Indexing into a (possibly multi-dimensional) array.
            % The subsref format does not include brackets, and we also need to
            % specify an additional ":" index.
            exp = ['(' exp ',:)'];
            var = eval(['var' exp]);
        end
        varsize = size(var);
        if numel(varsize)>2 && varsize(1)==1 && varsize(2)==1
            % We can simplify this.
            var = i_reshape(var,varsize(3:end));
        end
    end
end


%--------------------------------------------------------------------------
function var = i_reshape(var,newsize)
    if numel(newsize)==1
        % We can't pass a scalar to "reshape"
        newsize = [newsize 1];
    end
    var = reshape(var,newsize);
end

%-------------------------------------------------------------------------
function s = i_invalid
    s = com.mathworks.comparisons.decorator.variable.ValueInfo.getInvalid;
end

%-------------------------------------------------------------------------
function s = i_empty
    s = com.mathworks.comparisons.decorator.variable.ValueInfo.getEmpty;
end

            
%--------------------------------------------------------------------------
% Returns summaries of the values in the specified cell array, as a cell
% array of ValueInfo object.
function s = i_get_summaries(var)
    assert(iscell(var));
    if isempty(var)
        s = [];
        return;
    end
    % This function uses "length" instead of "numel", so we need to supply
    % a 1D array.
    summaries = workspacefunc('getabstractvaluesummariesj',var(:));
    strings = workspacefunc('getshortvalueobjectsj',var(:));
    % Not necessarily the same shape that we were given, so care is
    % needed.  (And "reshape" doesn't work on Java arrays.)
    infoarray = com.mathworks.comparisons.decorator.variable.ValueInfo.summariesToInfo(summaries,strings);
    s = cell(size(var));
    for i=1:numel(s)
        s{i} = infoarray(i);
    end
end

%--------------------------------------------------------------------------
% Tedious conversion from M*N cell array to ValueInfo[M][N]
function ret = i_summaries_to_java_2d(s)
    z = size(s);
    if all(z>0)
        ret = javaArray('com.mathworks.comparisons.decorator.variable.ValueInfo[]',z(1));
        for i=1:z(1)
            a = i_summaries_to_java_1d(s(i,:));
            for j=1:z(2)
                a(j) = s{i,j};
            end
            ret(i) = a;
        end
    else
        ret = [];
    end
end

%--------------------------------------------------------------------------
% Tedious conversion from 1*N cell array to ValueInfo[N]
function ret = i_summaries_to_java_1d(s)
    z = numel(s);
    if all(z>0)
        ret = javaArray('com.mathworks.comparisons.decorator.variable.ValueInfo',z);
        for i=1:z
            ret(i) = s{i};
        end
    else
        ret = [];
    end
end
