function htmlOut = textdiff(source1,source2,width,ignore_whitespace,show_diffs_only,showLineNumbers)
% Creates a report showing the differences between the two specified text
% files.

    %   Copyright 1984-2017 The MathWorks, Inc.

    if nargin < 3
        width = 60;
    elseif ischar(width)|| (isstring(width) && isscalar(width))
        width = str2double(width);
    end
    
    if nargin < 4
        ignore_whitespace = false;
    end

    if nargin < 5
        show_diffs_only = false; % show entire files by default
    end
    
    if nargin < 6
        showLineNumbers = true;
    end
    
    if ischar(source1) || (isstring(source1) && isscalar(source1))
        % String supplied.  Treat it as a file name.
        source1 = char(source1);
        source1 = comparisons.internal.resolvePath(source1);
        source1 = com.mathworks.comparisons.source.impl.LocalFileSource(java.io.File(source1),source1);
    end
    if ischar(source2) || (isstring(source2) && isscalar(source2))
        source2 = char(source2);
        % String supplied.  Treat it as a file name.
        source2 = comparisons.internal.resolvePath(source2);
        source2 = com.mathworks.comparisons.source.impl.LocalFileSource(java.io.File(source2),source2);
    end
    
    try
        [text1,readable1] = gettext(source1);
        [text2,readable2] = gettext(source2);
    catch e
        rethrow(e);
    end
    htmlOut = i_CreateHTML(source1,text1,readable1, ...
        source2,text2,readable2,...
        width,ignore_whitespace,show_diffs_only,showLineNumbers);

end

function [name,title,shorttitle,date,absname] = i_GetNames(source,filename)
    nameprop = com.mathworks.comparisons.source.property.CSPropertyName.getInstance();
    titleprop = com.mathworks.comparisons.source.property.CSPropertyTitle.getInstance();
    dateprop = com.mathworks.comparisons.source.property.CSPropertyLastModifiedDate.getInstance();
    shorttitleprop = com.mathworks.comparisons.source.property.CSPropertyShortTitle.getInstance();
    absnameprop = com.mathworks.comparisons.source.property.CSPropertyAbsoluteName.getInstance();
    % All sources have a name
    assert(source.hasProperty(nameprop));
    name = char(source.getPropertyValue(nameprop,[]));
    % Use the Title property if it's available.  Otherwise use the Name.
    if source.hasProperty(titleprop)
        title = char(source.getPropertyValue(titleprop,[]));
    else
        title = name;
    end
    if source.hasProperty(titleprop)
        shorttitle = char(source.getPropertyValue(shorttitleprop,[]));
    else
        shorttitle = title;
    end
    if source.hasProperty(dateprop)
        date = char(source.getPropertyValue(dateprop,[]).toString());
    else
        date = '';
    end
    if source.hasProperty(absnameprop)
        absname = char(source.getPropertyValue(absnameprop,[]));
    else
        absname = filename; % which might be empty, but that's OK
    end
    if strcmp(filename,name) && strcmp(filename,absname)
        % We've been given a fully qualified file name for both "name" and
        % "absname".  Remove the path from the name.  The full path will
        % still be shown for "absname".
        [~,n,e] = fileparts(filename);
        name = [n e];
    end
end


% This function creates the actual HTML output for the report.
function htmloutput = i_CreateHTML(source1,text1,filename1, ...
        source2,text2,filename2, ...
        showchars,ignore_whitespace,show_diffs_only,showLineNumbers)

softmarker = '<span class="diffsoft">  -</span>';
blankLine = char(32*ones(1,showchars));

% Calculate the "alignment" vectors.
[a1,a2] = diffcode(text1,text2);
% Get the lines of the files in the order they are to appear in the
% report.  Use blank lines where there are zeros in the alignment vectors
% (for insertions are deletions).
f1n = [{blankLine}; text1];
f2n = [{blankLine}; text2];
a1Final = f1n(a1+1);
a2Final = f2n(a2+1);

isfile1 = ~isempty(filename1);
isfile2 = ~isempty(filename2);

%ignore_whitespace = com.mathworks.services.Prefs.getBooleanPref(...
%    'ComparisonPreferenceIgnoreWhitespace',false);

htmloutput = cell(numel(text1)+numel(text2)+1000,1); % reasonable maximum
currentline = 1; % index of current output line

function writeLine(str,varargin)
   htmloutput{currentline} = sprintf(str,varargin{:});
   currentline = currentline+1;
end

function tag = getDiffLinePrefix(leftLineNum, rightLineNum)
    tag = sprintf('<span class="diffLine" data-left-line="%d" data-right-line="%d">', ...
        leftLineNum, rightLineNum);
end

function tag = getDiffLineSuffix()
    tag = '</span>';
end

function lineNumberText = getLeftLineNumberText(line)
    if showLineNumbers
        if isfile1
            lineNumberText = sprintf('<a href="javascript:openleft(%d);">%3d</a>',line,line);
        else
            lineNumberText = sprintf('%3d',line);
        end
    else
        lineNumberText = '';
    end
end

function lineNumberText = getRightLineNumberText(line)
    if showLineNumbers
        if isfile2
            lineNumberText = sprintf('<a href="javascript:openright(%d);">%3d</a>',line,line);
        else
            lineNumberText = sprintf('%3d',line);
        end
    else
        lineNumberText = '';
    end
end

function writeModifiedLine(line1,a1,line2,a2)
    leftlink = getLeftLineNumberText(a1);
    rightlink = getRightLineNumberText(a2);
    
    % Take line1 and line2 and colorize into newline1 and newline2.
    [newline1,newline2] = linediff(line1,line2,showchars,ignore_whitespace);
       
    % A peculiarity of ICE's rendering means that this only aligns
    % properly if the span is inside the hyperlink.
    writeLine('%s%s <span class="diffnomatch">%s x %s</span> <span>%s</span>%s\n', ...
              getDiffLinePrefix(a1,a2),leftlink,newline1,newline2,rightlink,getDiffLineSuffix);
end

function tag = getUnmodifiedLinePrefix(leftLineNum, rightLineNum)
    tag = sprintf('<span class="nodiff" data-left-line="%d" data-right-line="%d">', ...
        leftLineNum, rightLineNum);
end

function tag = getUnmodifiedLineSuffix()
    tag = '</span>';
end

function writeUnmodifiedLine(line1,a1,leftNonZeroLineNum,line2,a2,rightNonZeroLineNum)
    leftLine = a1;
    if a1==0
        leftLine = leftNonZeroLineNum;
        % This can happen when a whitespace-only line appears on one side
        % only.
        a1 = softmarker;
    else
        a1 = getLeftLineNumberText(a1);
    end
    line1 = formatLine(line1);
    rightLine = a2;
    if a2==0
        rightLine = rightNonZeroLineNum;
        % This can happen when a whitespace-only line appears on one side
        % only.
        a2 = softmarker;
    else
        a2 = getRightLineNumberText(a2);
    end
    line2 = formatLine(line2);
    writeLine('%s%s %s . %s <span>%s</span>%s\n',...
        getUnmodifiedLinePrefix(leftLine,rightLine),a1,line1,line2,a2, getUnmodifiedLineSuffix);
end

function writeDeletedLine(line1,a1,a2)
    % Text on left, blank on right.
    leftlink = getLeftLineNumberText(a1);
    writeLine('%s%s <span class="diffnew left">%s &lt; </span><span class="diffold">%s</span> %s%s\n', ...
                getDiffLinePrefix(a1,a2),leftlink,formatLine(line1),blankLine,softmarker,getDiffLineSuffix);
end

function writeInsertedLine(line2,a1,a2)
    % Blank on left, text on right.
    rightlink = getRightLineNumberText(a2);
    writeLine('%s%s <span class="diffold">%s</span><span class="diffnew right"> &gt; %s</span> <span>%s</span>%s\n', ...
        getDiffLinePrefix(a1,a2),softmarker,blankLine,formatLine(line2),rightlink,getDiffLineSuffix);
end

function writeJavaScriptLine(jsFile)
htmloutput{currentline} = comparisons.internal.html.createJSLine(jsFile);
currentline = currentline + 1;
end

function writeCssLine(cssFile)
cssUrl = comparisons.internal.html.createURLFromPath(cssFile);
writeLine('<link rel="stylesheet" type="text/css" href="%s"/>\n', cssUrl);
end

% Replaces tabs with spaces, and HTML-escapes characters as necessary.
function newline = formatLine(oldline)
    newline = blankLine;
    lineContent = replacetabs(oldline);
    lineLen = min(length(lineContent),length(blankLine));
    newline(1:lineLen) = lineContent(1:lineLen);
    newline = code2html(newline);
end

% Escapes a string so that it can be used as a literal string in Javascript
function str = javascriptEscape(str)
    str = strrep(str,'\','\\');
    str = strrep(str,'"','\"');
end

function lineNum = getLastNonZeroLineNum(diffcodes, num)
    lineNum = 0;
    if (num > 1)
        for jj = num-1:-1:1
            if (diffcodes(jj) ~= 0)
                lineNum = diffcodes(jj);
                break;
            end
        end
    end
end

[name1,title1,~,date1,absname1] = i_GetNames(source1,filename1);
[name2,title2,~,date2,absname2] = i_GetNames(source2,filename2);

% Generate the HTML
writeLine('%s\n',comparisons.internal.html.createHeader());
% Title is used in the Find Dialog.
if ~isequal(name1, name2)
    title = i_string('TextdiffTitle2',title1,title2);
else
    title = i_string('TextdiffTitle1',title1);
end
writeLine('<title>%s</title>\n', title);

writeLine('<script type="text/javascript">\n');
js_var_line = currentline;
writeLine(''); % We'll fill these lines in later.
writeLine('');
if isfile1
    writeLine('var LEFT_FILE = "%s";\n', javascriptEscape(filename1));
end
if isfile2
    writeLine('var RIGHT_FILE = "%s";\n', javascriptEscape(filename2));
end
writeLine('var WIDTH = %d; \n', showchars);
writeLine('var NUM_SPACES_PER_TAB = %d; \n', com.mathworks.widgets.text.EditorPrefsAccessor.getSpacesPerTab());
writeLine('</script>\n');

% Include jQuery.
jQueryJsFile = fullfile(matlabroot,'toolbox','shared','comparisons','+comparisons','+internal','+util','jquery-1.10.2.min.js');
writeJavaScriptLine(jQueryJsFile);

% Include a Javascript file in this directory.
textFolder = fullfile(matlabroot,'toolbox','shared','comparisons','+comparisons','+internal','+text');
diffJsFile = fullfile(textFolder,'report.js');
writeJavaScriptLine(diffJsFile);

% Add utils
utilsFile = fullfile(matlabroot, 'toolbox', 'shared', 'comparisons', '+comparisons', '+internal', '+util', 'utils.js');
writeJavaScriptLine(utilsFile);

% Include merge functionality JavaScript
mergeJsFile = fullfile(textFolder,'merge.js');
writeJavaScriptLine(mergeJsFile);

% Include merge CSS file
mergeCssFile = fullfile(textFolder,'merge.css');
writeCssLine(mergeCssFile);

% Include general CSS file
reportCSSFile = fullfile(textFolder,'report.css');
writeCssLine(reportCSSFile);

% Write some CSS statements to specify colors which we get from the
% ColorManager
writeLine('%s',comparisons.internal.html.createCSS());
writeLine('</head>');
writeLine('<body>');

writeLine('<div id="allContent">');

% Report header, formatted as a table.
writeLine('<table cellpadding="0" cellspacing="0" border="0">');
% Top line: name
writeLine('<tr>\n');
if isfile1
    writeLine(...
        ['<td></td><td>'...
            '<a id="leftFileLink" class="bold" href="javascript:openLeftFile();">%s</a>'...
        '</td>\n'], ...
        name1);
else
    writeLine('<td></td><td class="bold">%s</td>\n', name1);
end
if isfile2
    writeLine(...
        ['<td>'...
            '<a id="rightFileLink" class="bold" href="javascript:openRightFile();">%s</a>'...
        '</td>\n'], ...
        name2);
else
    writeLine('<td class="bold">%s</td>\n', name2);
end
writeLine('</tr>\n');
% Full paths, if different from names
if ~strcmp(absname1,name1) || ~strcmp(absname2,name2)
    writeLine('<tr>\n');
    writeLine('<td></td><td>%s</td><td>%s</td>\n',absname1,absname2);
    writeLine('</tr>\n');
end
% Dates
writeLine('<tr>\n');
writeLine('<td></td><td>%s</td><td>%s</td>\n',date1,date2);
writeLine('</tr>\n');
% Use whitespace in the cells of this row of the table to force the
% table to align with the pre-formatted text below it.
writeLine('<tr>\n');
writeLine('<td><pre>    </pre></td>\n');
writeLine('<td><pre>%s  </pre></td>\n',blankLine);
writeLine('<td><pre>%s</pre></td>\n',blankLine);
writeLine('</tr></table>\n');


% Some preliminary work before we start writing the main body of the
% report.

% Some names values to help below
NO_DIFFERENCE = 0;
INSERTION = 1;
DELETION = 2;
MODIFICATION = 3;
SKIP = 4;

match = zeros(size(a1));
for i=1:numel(match)
    match(i) = linesmatch(a1Final{i},a2Final{i},ignore_whitespace);
end

skip_padding = -1;
if show_diffs_only
    skip_padding = 3; % show three lines above and below any differences
end
if skip_padding>=0
    skip = pFindLinesToSkip(match,skip_padding);
else
    skip = false(size(match));
end

% Variables for use while generating the report
current_difference_type = NO_DIFFERENCE;
diffcount = 0;

header_line_index = currentline;
writeLine(''); % we'll come back and fill this line in later

function text = createText(array)
    text = [];
    if ~isempty(array)
        text = sprintf('%s\n', array{:});
        text = text(1:end-1);
        text = code2html(text);
    end
end

function writeTextSourceLine(id, text)
    % Text starting with or ending with new-line characters are handled
    % differently in different browsers.
    %
    % http://www.w3.org/TR/2011/WD-html5-author-20110809/the-pre-element.html
    % "...a leading newline character immediately following the pre 
    % element start tag is stripped."
    %
    % In IE, new-line characters at the end of the string within <pre> tags
    % are ignored when querying the node for the text contents.
    %
    % For both of the above reasons we will add an artifical string to the
    % start and end of the string inserted into the page.  These artifical 
    % additions should be stripped from the text when it is read within any
    % JavaScript function.
    
    text = ['BEGIN_PRE_TAG_IE_WORKAROUND' text 'END_PRE_TAG_IE_WORKAROUND'];
    writeLine(['<div id="' id '" style="display:none"><pre>%s</pre></div>'], text);
end
writeTextSourceLine('leftText', createText(text1));
writeTextSourceLine('rightText', createText(text2));
writeLine('<div id="diffstart">\n');

% If we don't put this line in, ICE fails to find "diff0", and navigation
% doesn't work.  Not at all clear why.
writeLine('<br/><pre><div id="ignorediv"><a id="ignorelink"></a></div>\n');

for n = 1:length(a1Final)
    line1 = a1Final{n};
    line2 = a2Final{n};
    if match(n)
        if skip(n)
            if current_difference_type~=SKIP
                % First "skip" line in this block
                msg = i_string('TextdiffNumSkipped',skip(n));
                lmsg = floor(numel(msg)/2);
                indent = char(32*ones(1,showchars-lmsg));
                writeLine('    %s<span class="diffskip">%s</span>\n',indent,msg);
                current_difference_type = SKIP;
            end
        else
            if current_difference_type ~= NO_DIFFERENCE && ...
               current_difference_type ~= SKIP
                % We're at the end of a block of differences.
                writeLine('</div>');
            end
            current_difference_type = NO_DIFFERENCE;
            writeUnmodifiedLine(line1,a1(n),getLastNonZeroLineNum(a1,n),line2,a2(n),getLastNonZeroLineNum(a2,n));
        end
    else
        assert(current_difference_type~=SKIP);
        if current_difference_type==NO_DIFFERENCE
            % Add an anchor so we can hyperlink to here from the previous
            % difference.  The div tag allows us to highlight this
            % section.
            writeLine('<div id="diff%d" onclick="select(''diff%d'')">',diffcount,diffcount);
            diffcount = diffcount + 1;
        end
        if a1(n)==0
            % Insertion; text on right-hand side only
            current_difference_type = INSERTION;
            writeInsertedLine(line2,getLastNonZeroLineNum(a1,n),a2(n));
        elseif a2(n)==0
            % Deletion; text on left-hand side only
            current_difference_type = DELETION;
            writeDeletedLine(line1,a1(n),getLastNonZeroLineNum(a2,n));
        else
            % Modification; text on both sides.  The formatting is handled
            % by writeModifiedLine.
            current_difference_type = MODIFICATION;
            writeModifiedLine(line1,a1(n),line2,a2(n));
        end
    end

end

if current_difference_type ~= NO_DIFFERENCE && ...
   current_difference_type ~= SKIP
    % We're at the end of a block of differences.
    writeLine('</div>');
end

writeLine('</pre>'); 
writeLine('</div>'); % End diffStart
writeLine('</div>'); % End allContent
writeLine('<div id="bottom">');

% Get the stats
leftMatches = find(a1(match>0));
rightMatches = find(a2(match>0));
numMatch = numel(intersect(leftMatches, rightMatches));
matchstr = i_string('TextdiffNumMatches',numMatch);
writeLine('<p>%s</p>', matchstr);

if diffcount==0
    % No differences to show, but check whether the files are
    % actually identical so that we can give accurate output.
    identical = com.mathworks.comparisons.compare.concr.BinaryComparison.compare(source1,source2);
    if identical
        % These files are identical byte-for-byte.
        htmloutput{header_line_index} = getHeaderLine('TextdiffNoDiffs');
    elseif ignore_whitespace
        % These files contain only whitespace differences.
        htmloutput{header_line_index} = getHeaderLine('TextdiffNoDiffs_Whitespace');
    else
        % These files are not identical byte-for-byte, but they contain
        % no textual differences.  The only likely explanation is the
        % end-of-line characters.
        htmloutput{header_line_index} = getHeaderLine('TextdiffNoDiffs_LineEndings');
    end
    htmloutput{js_var_line} = sprintf('var LAST_DIFF_ID="top"\n');
else
    if ignore_whitespace
        if diffcount == 1
            htmloutput{header_line_index} = getHeaderLine('TextdiffNumDiffs_WhitespaceOne');
        else
            htmloutput{header_line_index} = getHeaderLine('TextdiffNumDiffs_Whitespace', diffcount);
        end
    else
        if diffcount == 1
            htmloutput{header_line_index} = getHeaderLine('TextdiffNumDiffsOne');
        else
            htmloutput{header_line_index} = getHeaderLine('TextdiffNumDiffs', diffcount);
        end
    end
    htmloutput{js_var_line} = sprintf('var LAST_DIFF_ID="diff%d";\n',diffcount-1);
    writeLine('<p>%s</p>\n', i_string('TextdiffNumDiffsLeft',numel(text1) - numel(leftMatches)));
    writeLine('<p>%s</p>\n', i_string('TextdiffNumDiffsRight',numel(text2) - numel(rightMatches)));
end

htmloutput{js_var_line+1} = sprintf('var NUM_DIFFS="%d";\n',diffcount);
writeLine('</div>'); % End bottom
writeLine('</body></html>');

htmloutput = [htmloutput{1:currentline-1}];

end


function eq = linesmatch(line1,line2,ignore_whitespace)
     if ignore_whitespace
         % If the spaces are between words then we still want to consider 
         % them to be differences.
         
         % Normalize white spaces
         line1 = regexprep(line1, '\s', ' ');
         line2 = regexprep(line2, '\s', ' ');
         
         % Eliminate all whitespace where the preceding character is not a 
         % "word"
         expression = '(?<=[^\w])\s+';
         line1 = regexprep(line1, expression, '');
         line2 = regexprep(line2, expression, '');
                 
         % Eliminate all whitespace where the following character is not a 
         % "word"
         expression = '\s+(?=[^\w])';
         line1 = regexprep(line1, expression, '');
         line2 = regexprep(line2, expression, '');
         
         % Trim the leading and trailing white space
         line1 = strtrim(line1);
         line2 = strtrim(line2);
     end
     eq = strcmp(line1, line2);
end


function str = i_string(key,varargin)
    str = comparisons.internal.message('message', ['comparisons:comparisons:' key], varargin{:});
end

function str = getHeaderLine(message, varargin)
str = ['<div>' i_string(message, varargin{:}) '</div>'];
end
