#!/usr/bin/perl -w
# -*- perl -*-
use P4CGI ;
use strict ;
#use FileHandle; Can't do! Won't work for all perls... Y? Who knows?

#
#################################################################
#  CONFIGURATION INFORMATION 
#  All config info should be in P4CGI.pm
#
#################################################################
#
#  P4 file diff viewer
#  View diff between two files or two revisions
#
#################################################################

# Get file spec argument
my @files = split /,/,P4CGI::cgi()->param("FSPC") ;
&P4CGI::bail("No file specified") unless @files > 0 ;
map { $_ = &P4CGI::htmlEncode($_) } @files ;

my @revs = split / /,P4CGI::cgi()->param("REV") if defined P4CGI::cgi()->param("REV") ;
$files[0] =~ s/^([^\#]+)\#(\d+)/$1/ and do { $revs[0] = $2 ; } ;
&P4CGI::bail("No revision specified") unless @revs > 0 ;
map { s/^(\d+).*/$1/ ; } @revs ;


my @modes ;
@modes = split / /,P4CGI::cgi()->param("ACT") if defined P4CGI::cgi()->param("ACT") ;
&P4CGI::bail("No mode specified") unless @modes > 0 ;

my @files2 ;
@files2 = 
    split /,/,P4CGI::cgi()->param("FSPC2") if defined P4CGI::cgi()->param("FSPC2") ;
map { $_ = &P4CGI::htmlEncode($_) } @files2 ;

my @revs2 ;
@revs2 = 
    split / /,P4CGI::cgi()->param("REV2") if defined P4CGI::cgi()->param("REV2") ;
map { s/^(\d+).*/$1/ ; } @revs2 ;
if(defined $files2[0]) {
    $files2[0] =~ s/^([^\#]+)\#(\d+)/$1/ and do { $revs2[0] = $2 ; } ;
} ;

my $change = P4CGI::cgi()->param("CH") ;
$change =~ s/^(\d+).*/$1/ if defined $change ;

my $ignoreSpace = P4CGI::cgi()->param("IGNSP") ;

# Constants for the file diff display
# $NCONTEXT - number of lines context before and after a diff
my $NCONTEXT   = P4CGI::cgi()->param("CONTEXT") ;
$NCONTEXT   = 10 unless defined $NCONTEXT ;

# $MAXCONTEXT - max number of lines context between diffs
my $MAXCONTEXT = $NCONTEXT+20;


my $n ;
for($n = 0; $n < @files ; $n++) {
    $files2[$n] = $files[$n] unless defined $files2[$n] ;
    $revs2[$n] = $revs[$n]-1 unless defined $revs2[$n] ;
}

if((@files != @revs) ||
   (@files != @files2) ||
   (@files != @revs2)) {
    &P4CGI::bail("Argument counts not correct") ;    
} ;

my $title ;

if(@files == 1) {
    if($files[0] eq $files2[0]) {
	if($revs[0] < $revs2[0]) {
	    my $r = $revs2[0] ;
	    $revs2[0] = $revs[0] ;
	    $revs[0] = $r ;
	}
	$title = "Diff<br><code>$files[0]</code>\#$revs2[0] -&gt; \#$revs[0] " ;
    }
    else {
	$title = "Diff between<br><code>$files[0]\#$revs[0]</code><br>and<br><code>$files2[0]\#$revs2[0]" ;
    }
}
else {
    $title = "Diffs for change $change" ;
}

my $nextNCONTEXT= $NCONTEXT*2 ;
my @pstr ;
my $p ;
foreach $p (&P4CGI::cgi()->param) {
    next if $p eq "CONTEXT" ;
    push @pstr, $p . "=" . P4CGI::cgi()->param($p) ;
}

my @options ;
if($NCONTEXT < 9999999) {
    push @options, &P4CGI::buttonCell($ENV{SCRIPT_NAME},
				      "Show $nextNCONTEXT context lines",
				      @pstr,
				      "CONTEXT=$nextNCONTEXT",
				      "Show more context") ;
    push @options, &P4CGI::buttonCell($ENV{SCRIPT_NAME},
				      "Show all lines",
				      @pstr,
				      "CONTEXT=9999999",
				      "Show complete") ;
} ;

if(@files == 1 and $files[0] eq $files2[0]) {
    push @options, ("<td>&nbsp;</td>",
		    &P4CGI::buttonCell("fileLogView.cgi",
				       "Show file log for $files[0]",
				       "FSPC=$files[0]",
				       "File log")) ;
}


if(defined $ignoreSpace and $ignoreSpace eq "Y") {
    &P4CGI::cgi()->param("IGNSP","N") ;
    push @options, &P4CGI::buttonCell( &P4CGI::cgi()->url(-query=>1),
				      "Show changes in whitespaces",
				      "Show whitespace") ;
    $ignoreSpace = "-dw" ;
}
else {
    &P4CGI::cgi()->param("IGNSP","Y") ;
    push @options, &P4CGI::buttonCell( &P4CGI::cgi()->url(-query=>1),
				      "Ignore changes in whitespaces",
				      "Ignore whitespace") ;
    $ignoreSpace = "" ;
}

print
    &P4CGI::start_page("$title",@options) ;

my $currentFile ;
my $currentRev ;

local *P4 ;

my $P4lineNo ; # Line No of file
my $SameFile ; # Set to true of same files. 
my $f1Mark ;
my $f2Mark ;
my $f1Class ;
my $f2Class ;


sub getLine() 
{
    $P4lineNo++ if defined $P4lineNo ;
    return <P4> ;
}

my $NoFiles = scalar @files ;   

while(@files>0) {

    my $file1   = shift @files ;
    my $file2  = shift @files2 ;
    my $rev1    = shift @revs ;
    my $rev2   = shift @revs2 ;
    my $mode   = shift @modes ;

    next if ($mode eq "add") or ($mode eq "delete") or ($mode eq "branch") ; # Skip if diff not possible

    $SameFile = ($file1 eq $file2) ;

    if($SameFile) { # Make sure revisions is in correct order if it is the same file.
	if($rev1 < $rev2) {
	    my $r = $rev2 ;
	    $rev2 = $rev1 ;
	    $rev1 = $r ;
	}
    } ;

    my $f1Class = $SameFile ? "fileDiffAddedLines"   : "fileDiffFirst" ;
    my $f2Class = $SameFile ? "fileDiffDeletedLines" : "fileDiffSecond" ;
    
    $f1Mark = $SameFile ? "+" : "<" ;
    $f2Mark = $SameFile ? "-" : ">" ;    

    my @buttoncells ;
    if(!$SameFile or $NoFiles > 1) {
	push @buttoncells,&P4CGI::buttonLink("fileLogView.cgi",
					     "View log for file $file1",
					     "FSPC=$file1",
					     "File Log") ;
    } ;
    if(!$SameFile) {
	push @buttoncells,&P4CGI::buttonLink("fileLogView.cgi",
					     "View log for file $file2",
					     "FSPC=$file2",
					     "File Log") ;
	
    } ;
    my $title = "<span class=\"fileDiffMark\">${f1Mark}</span>&nbsp;<span class=\"$f1Class\">$file1#$rev1</span>&nbsp;" ;
    $title .= shift @buttoncells if @buttoncells  ;
    $title .= "<br>" ;
    $title .= "<span class=\"fileDiffMark\">${f2Mark}</span>&nbsp;<span class=\"$f2Class\">$file2#$rev2</span>&nbsp;" ;
    if(@buttoncells) {
	$title .= shift @buttoncells ;	
    }
    print "<br>\n" ;
    print &P4CGI::start_framedTable($title) ;
    print &P4CGI::start_table("class=\"fileDiff\"") ;


    my $f1 = "$file1#$rev1";
    my $f2 = "$file2#$rev2";

    $currentFile = $file1 ;
    $currentRev = $rev1 ;    

    ##
    ## Use "p4 diff2" to get a list of modifications (diff chunks)
    ##

    my $nchunk =0; # Counter for diff chunks 
    my @start ;    # Start line for chunk in latest file
    my @dels ;     # No. of deleted lines in chunk
    my @adds ;     # No. of added lines in chunk
    my @delLines ; # Memory for deleted lines
    
    if ($mode ne 'add' && $mode ne 'delete' && $mode ne 'branch') {
	
	&P4CGI::p4call(*P4, "diff2 $ignoreSpace \"$f2\" \"$f1\"");
	$_ = <P4>; 
	while (<P4>) {
	    
	    # Check if line matches start of a diff chunk
	    /(\d+),?(\d*)([acd])(\d+),?(\d*)/  or do { next ; } ;

	    # $la, $lb: start and end line in old (left) file
	    # $op: operation (one of a,d or c)
	    # $ra, $rb: start and end line in new (right) file
	    my ( $la, $lb, $op, $ra, $rb ) = ($1,$2,$3,$4,$5) ;
	    
	    # End lines may have to be calculated
	    if( !$lb ) { $lb = $op ne 'a' ? $la : $la - 1; }
	    if( !$rb ) { $rb = $op ne 'd' ? $ra : $ra - 1; }
	    	
	    my ( $dels, $adds ); # Temporary vars for No of adds/deletes
	    
	    # Calc. start position in new (right) file
	    $start[ $nchunk ] = $op ne 'd' ? $ra : $ra + 1;
	    # Calc. No. of deleted lines
	    $dels[ $nchunk ] = $dels = $lb - $la + 1;
	    # Calc. No. of added lines
	    $adds[ $nchunk ] = $adds = $rb - $ra + 1;
	    # Init deleted lines 
	    $delLines[ $nchunk ] = "";
	    
	    # Get the deleted lines from the old (left) file
	    while( $dels-- ) {
		$_ = <P4>;
		s/^. //;
		$_ = &P4CGI::htmlEncode($_) ;
		$delLines[ $nchunk ] .= 
		    "$_";
	    }
	    
	    # If it was a change, skip over separator
	    if ($op eq 'c') {	
		$_ = <P4>; 
	    }
	    
	    # Skip over added lines (we don't need to know them yet)
	    while( $adds-- ) {
		$_ = <P4>;
	    }

	    # Next chunk.
	    $nchunk++;
	}	
	close P4;
    }
    
    # Now walk through the diff chunks, reading the new (right) file and
    # displaying it as necessary.

    &P4CGI::p4call(*P4, "print -q \"$f1\"");
    
    $P4lineNo = 0; # Zero current line    
    my $n ;
    for( $n = 0; $n < $nchunk; $n++ )
    {
	# print up to this chunk.
	&catchup($start[ $n ] - $P4lineNo - 1) ;
	
	# display added lines -- these are in the file stream.
	if( $adds[ $n ] )
	{
	    &display($adds[ $n ],$f1Class,$f1Mark );
	}
	
	# display deleted lines -- we saved these from the diff
	if( $dels[ $n ] )
	{
	    my $line ;
	    foreach $line (split("\n",$delLines[ $n ])) {
		print "<tr><td class=\"fileDiffLineNo\"></td><td class=\"fileDiffMark\">$f2Mark</td>" ;
		$line =~ s/ /&nbsp;/g ;
		print "<td class=\"$f2Class\">$line</td></tr>\n" ;
	    }
	}
	
	# $curlin = $start[ $n ] + $adds[ $n ];
    }
    
    &catchup(999999999 );
    
    close P4;

    print 
	&P4CGI::end_table(),
	&P4CGI::end_framedTable();
}

print &P4CGI::end_page() ;


# Support for processing diff chunks.
#
# skip: skip lines in source file
# display: display lines in source file, handling funny chars 
# catchup: display & skip as necessary
#

##
## skip(no of lines)
##    Returns: 0 or No. of lines not skipped if file ends
sub skip {
    my $to = shift @_;
    while( $to > 0 && ( $_ = &getLine() ) ) {
	$to--;
    }
    return $to;
}

##
## display(no of lines,class,mark)
##   Displays a number of lines from handle
sub display {
    my $to = shift @_;
    my $class = shift @_;
    my $mark = shift @_;
    
    while( $to-- > 0 && ( $_ = &getLine() ) ) {
	my $line = &P4CGI::htmlEncode($_) ;
	$line =~ s/ /&nbsp;/g ;
	my $ls ;
	if($P4lineNo == 1 or ($P4lineNo % 5) == 0) {
	    $ls = "<td class=\"fileDiffLineNo\">" ;
	    $ls .= &P4CGI::ahref(-url=>"fileViewer.cgi",
				 -anchor => "L$P4lineNo",			  
				 "FSPC=$currentFile",
				 "REV=$currentRev",
				 "HELP=View file at line $P4lineNo",
				 $P4lineNo) ;
	    $ls .= "</td>" ;
	}
	else {
	    $ls = "<td></td>" ;
	}
	if($mark ne "") {
	    print "<tr>$ls<td class=\"fileDiffMark\">$mark</td><td class=\"$class\">$line</td></tr>\n" ;
	}
	else {
	    print "<tr>$ls<td></td><td class=\"$class\">$line</td></tr>\n" ;
	}
    }
}

##
## catchup(<handle>,no of lines)
##   Print/skip lines to next diff chunk
sub catchup {
    my $to = shift @_;

    if( $to > $MAXCONTEXT )
    {
	my $skipped = $to - $NCONTEXT ; 
	if($P4lineNo > 0) {		
	    &display($NCONTEXT,"fileDiffLine","");
	    $skipped -= $NCONTEXT ;
	}
	$skipped -= &skip($skipped );
	
	print 
	    "<tr><td></td><td></td><td class=\"fileDiffSkipped\">",
	    " $skipped lines skipped ",
	    "</td></tr>\n" if( $skipped );
	
	&display($NCONTEXT,"fileDiffLine","");
    }
    else
    {
	&display($to,"fileDiffLine","");
    }
}

#
# That's all folks
#