function htmlOut = matdiff(source1, source2, reportID, mergingEnabled)
% Compares two MAT-files and returns an HTML report showing the differences.
% The inputs must be ComparisonSources and must each have a "readable location"
% property.

%   Copyright 2007-2017 The MathWorks, Inc.

    if nargin < 4
        mergingEnabled = 'true';
    end

    [source1,fullname1,shortname1,readable1] = i_resolve(source1);
    [source2,fullname2,shortname2,readable2] = i_resolve(source2);

    % Make sure that our "short" names are actually different.  If not, use
    % the file name from the readable location, and add directory names
    % until they are different.  This isn't perfect, but is better
    % confusing the user with identical names.
    if strcmp(shortname1,shortname2)
        [pt1,n,e] = fileparts(readable1);
        shortname1 = [n e];
        [pt2,n,e] = fileparts(readable2);
        shortname2 = [n e];
        while strcmp(shortname1,shortname2)
            [pt1,dn1] = fileparts(pt1);
            [pt2,dn2] = fileparts(pt2);
            if isempty(dn1) && isempty(dn2)
                % No more directory names to add.
                % Presumably we're comparing the file with itself.
                break;
            end
            % It doesn't matter if one of dn1 and dn2 is empty, because
            % fullfile won't insert unnecessary separators.
            shortname1 = fullfile(dn1,shortname1);
            shortname2 = fullfile(dn2,shortname2);
        end
    end

    [names1,size1,backup1] = i_info(readable1);
    [names2,size2,backup2] = i_info(readable2);
    
    % Performance trade-off: if either file is bigger than a gigabyte then
    % we'll probably be better off loading variables one at a time rather
    % than all at once.
    import com.mathworks.comparisons.mat.MatComparisonUtils;
    filesize_threshold = MatComparisonUtils.MAX_MAT_FILE_SIZE.get();
    one_at_a_time = size1 > filesize_threshold || size2 > filesize_threshold;
    
    if ~one_at_a_time
        % Load the entire file contents at once.  If we run out of memory,
        % fall back to loading the variables one at a time.
        try
            % Load the file contents, temporarily suppressing all warnings.
            variables1 = i_load(readable1);
            variables2 = i_load(readable2);
        catch E
            if ~strcmp(E.identifier,'MATLAB:nomem')
                % Something other than out-of-memory.
                rethrow(E);
            end
            one_at_a_time = true;
        end
    end
    
    if one_at_a_time
        % Create empty structure to make things easy later.  If we ran out
        % of memory while loading the file, this clears the data from the
        % first file too, making it more likely that loading individual
        % variables will succeed.
        variables1 = struct;
        variables2 = struct;
    end

    allnames = unique(vertcat(names1(:),names2(:)));

    doc = com.mathworks.xml.XMLUtils.createDocument('MatFileEditScript');
    root = doc.getDocumentElement;
    root.setAttribute('id',reportID);
    root.setAttribute('mergingEnabled', mergingEnabled);
    node = i_xmltextnode(doc,root,'LeftLocation',fullname1);
    node.setAttribute('Readable', readable1);
    node.setAttribute('ShortName',shortname1);
    if ~isempty(backup1)
        node.setAttribute('Backup',backup1);
    end
    node = i_xmltextnode(doc,root,'RightLocation',fullname2);
    node.setAttribute('Readable', readable2);
    node.setAttribute('ShortName',shortname2);
    if ~isempty(backup2)
        node.setAttribute('Backup',backup2);
    end
    
    found_diff = false;

    for i=1:numel(allnames)
        varname = allnames{i};
        if ~ismember(varname,names2)
            % Left only
            node = i_xmltextnode(doc,root,'LeftVariable',varname);
            var = i_getvar(readable1,variables1,varname);
            node.setAttribute('size',i_getsize(var));
            node.setAttribute('class', comparisons.internal.variableClass(var));
            found_diff = true;
        elseif ~ismember(varname,names1)
            % Right only
            node = i_xmltextnode(doc,root,'RightVariable',varname);
            var = i_getvar(readable2,variables2,varname);
            node.setAttribute('size',i_getsize(var));
            node.setAttribute('class', comparisons.internal.variableClass(var));
            found_diff = true;
        else
            % Both sides
            node = i_xmltextnode(doc,root,'Variable',varname);
            var1 = i_getvar(readable1,variables1,varname);
            node.setAttribute('leftsize',i_getsize(var1));
            node.setAttribute('leftclass', comparisons.internal.variableClass(var1));
            var2 = i_getvar(readable2,variables2,varname);
            node.setAttribute('rightsize',i_getsize(var2));
            node.setAttribute('rightclass', comparisons.internal.variableClass(var2));
            match_type = comparisons.internal.variablesEqual(var1, var2);
            node.setAttribute('contentsMatch',match_type);
            if ~strcmp(match_type,'yes')
                found_diff = true;
            end
        end
    end
    
    % Free the memory now that we no longer need it.
    clear variables1;
    clear variables2;
    
    if ~found_diff
        % No differences between the variables in these files.  Check
        % whether the files themselves are identical, byte-for-byte.
        identical = com.mathworks.comparisons.compare.concr.BinaryComparison.compare(source1,source2);
        if ~identical
            % The files are not identical, even though the data they store
            % is.  
            root.setAttribute('difftype','container');
        else
            % The files are identical.
            root.setAttribute('difftype','identical');
        end
    else
        root.setAttribute('difftype','contents');
    end
    
    % If required for debugging purposes, save the edit script to a file.
    schemafile = fullfile(fileparts(mfilename('fullpath')),'matdiff.xsd');
    com.mathworks.comparisons.compare.concr.ListComparisonUtilities.debugSaveXML(doc,schemafile);
    
    stylesheet = fullfile(fileparts(mfilename('fullpath')),'matdiff.xsl');
    xmlsource = com.mathworks.xml.XMLUtils.transformSourceFactory(doc);
    
    htmlOut = char(...
        com.mathworks.comparisons.compare.concr.ListComparisonUtilities.doTransform(xmlsource,stylesheet));

end

%------------------------------------------------------------
function node = i_xmltextnode(docNode,parentNode,tag,content)
% Creates a simple text tag with the specified contents
    node = docNode.createElement(tag);
    node.appendChild(docNode.createTextNode(content));
    parentNode.appendChild(node);
end

%------------------------------------------------------------
function str = i_getsize(var)
  sz = size(var);
  % We show the number of elements in the same format as the Workspace Browser.
  % See com.mathworks.mlwidgets.workspace.ClassicWhosInformation.getSize
  if numel(sz)==2
      str = sprintf('%dx%d',sz(1),sz(2));
  elseif numel(sz)==3
      str = sprintf('%dx%dx%d',sz(1),sz(2),sz(3));
  else
      % More than 3 dimensions.  Just show number of dimensions.
      str = sprintf('%d-D',numel(sz));
  end
  %info = whos('var');
  %str = sprintf('%s (%d bytes)', str, info.bytes);
end

%------------------------------------------------------------
function [source,fullname,shortname,readable] = i_resolve(source)

    if ischar(source) || (isstring(source) && isscalar(source))
        % String supplied.  Treat it as a file name.
        source = char(source);
        source = comparisons.internal.resolvePath(source);
        source = com.mathworks.comparisons.source.impl.LocalFileSource(java.io.File(source),source);
    end
    
    absnameprop = com.mathworks.comparisons.source.property.CSPropertyAbsoluteName.getInstance();
    if source.hasProperty(absnameprop)
        fullname = char(source.getPropertyValue(absnameprop,[]));
    else
        % No "absolute name" property.  Just use the "name".
        nameprop = com.mathworks.comparisons.source.property.CSPropertyName.getInstance();
        % All sources have a name
        assert(source.hasProperty(nameprop));
        fullname = char(source.getPropertyValue(nameprop,[]));
    end
    
    shorttitleprop = com.mathworks.comparisons.source.property.CSPropertyShortTitle.getInstance();
    if source.hasProperty(shorttitleprop)
        shortname = char(source.getPropertyValue(shorttitleprop,[]));
    else
        % No "absolute name" property.  Just use the "name".
        nameprop = com.mathworks.comparisons.source.property.CSPropertyName.getInstance();
        % All sources have a name
        assert(source.hasProperty(nameprop));
        shortname = char(source.getPropertyValue(nameprop,[]));
    end
    
    readableprop = com.mathworks.comparisons.source.property.CSPropertyReadableLocation.getInstance();
    % We need a "readable location" property.
    assert(source.hasProperty(readableprop));
    readable = char(source.getPropertyValue(readableprop,[]));
end


%------------------------------------------------------------
% If the supplied variables structure contains a variable with the
% specified name, its value is returned.  Otherwise the named variable is
% loaded from the file.
function val = i_getvar(filename,variables,name)
    if isfield(variables,name)
        val = variables.(name);
    else
        x = i_load(filename,name);
        val = x.(name);
    end
end

%------------------------------------------------------------
% Loads the specified variable from the file, or all variables if no
% name is specified.  Temporarily suppress all warnings during loading.
function vars = i_load(filename,varname)
    cleanup = suppressWarnings(); %#ok<NASGU>
    if nargin<2
        vars = load(filename,'-mat');
    else
        vars = load(filename,'-mat',varname);
    end
end

%------------------------------------------------------------
% Returns the names of all variables in the specified MAT-file.  Also
% returns their total size, in bytes.
function [names,totalsize,backup_file] = i_info(filename)
    cleanup = suppressWarnings(); %#ok<NASGU>
    w = whos('-file',filename);
    names = {w.name};
    totalsize = sum([w.bytes]);
    backup_file = [filename '~']; % needs to be the same as in varmerge.m
    backup_exists = exist(backup_file,'file')~=0;
    if ~backup_exists
        backup_file = [];
    end
end

function cleanup = suppressWarnings()
    w = warning('off');
    cleanup = onCleanup(@() warning(w));
end