#!/usr/bin/perl
use strict;
my($app_svnid) = '$HeadURL$ $LastChangedRevision$';
#  Allow bare words, so &ade_err_error() calls look nicer.
no strict 'subs';
use lib substr(`ade-config ade_lib_prefix`,0,-1) . '/include';
use ADE;
use lib substr(`fad-config fad_lib_prefix`,0,-1) . '/include';
use FAD;
use Getopt::Long qw(:config no_ignore_case);

my(%faddiff_defined_errors) = (
    faddiff_err_misc        => { fmt => "%s" },
    faddiff_err_access        => { fmt => "%s: can't %s" },
);

#  Options
my($opt_outfile, $opt_ignowner, $opt_igngroup, $opt_ignperms);
 
sub faddiff
{
    my($errstack_ref) = @_;
    my($fad1_file, $fad2_file, $out_handle, $fad1_handle, $fad2_handle);
    my($rc, $diff_rc, $optval);
    my(%data1_store, %data2_store, @data1_index, @data2_index, $count);

    #  Register application-specific errors
    &ade_err_registerdefderrs(\%faddiff_defined_errors);

    #  Defaults for options
    $opt_outfile  = "-";
    $opt_ignowner = 0;
    $opt_igngroup = 0;
    $opt_ignperms = 0;

    #  Register options
    if (($rc=&ade_opt_register($errstack_ref, "gumo:s", "ignore-gid,ignore-uid,ignore-mode,file:s", "main::faddiff_opt_handler_%s")) != $ade_err_ok) {
        return($rc);
    }

    #  Register handler functions
    if (($rc=&ade_msg_register($errstack_ref, \&faddiff_usage, \&faddiff_version, \&faddiff_listpaths)) != $ade_err_ok) {
        return($rc);
    } 

    #  Process options
    &ade_err_debug($errstack_ref, 10, "faddiff: processing options ...");
    if (($rc=&ade_opt_process($errstack_ref)) != $ade_err_ok) {
        return($rc);
    }

    #  Process arguments
    &ade_err_debug($errstack_ref, 10, "faddiff: processing arguments (\$ARGV[0]=$ARGV[0], \$ARGV[1]=$ARGV[1]) ... ");
    &ade_msg_usage($errstack_ref) if (!defined($ARGV[1]) || defined($ARGV[2]));
    $fad1_file = $ARGV[0];
    $fad2_file = $ARGV[1];
    if ($fad1_file eq "-" && $fad2_file eq "-") {
        &ade_err_error($errstack_ref, faddiff_err_misc, "only one file can be read from standard input");
        return($ade_err_fail);
    }

    #  Guts
    &ade_err_debug($errstack_ref, 5, "main: opening input and output files");
    if ($opt_outfile eq "-") {
        $out_handle = \*STDOUT;
    } else {
        &ade_tmp_registerfile($errstack_ref, $opt_outfile);
        if (($rc=&ade_fcm_openwritecompressed($errstack_ref, $opt_outfile, \*OUT_HANDLE)) != $ade_err_ok) {
            &ade_err_error($errstack_ref, faddiff_err_access, $opt_outfile, "open");
	    return($rc);
        }
        $out_handle = \*OUT_HANDLE;
    }
        
    if ($fad1_file eq "-") {
        $fad1_handle = \*STDIN;
    } else {
        &ade_err_debug($errstack_ref, 5, "main: opening input file $fad1_file");
        if (($rc=&ade_fcm_openreadcompressed($errstack_ref, $fad1_file, \*FAD1_HANDLE)) != $ade_err_ok) {
            &ade_err_error($errstack_ref, faddiff_err_access, $fad1_file, "open");
	    return($rc);
        }
        $fad1_handle = \*FAD1_HANDLE;
    }
        
    if ($fad2_file eq "-") {
        $fad2_handle = \*STDIN;
    } else {
        &ade_err_debug($errstack_ref, 5, "main: opening input file $fad2_file");
        if (($rc=&ade_fcm_openreadcompressed($errstack_ref, $fad2_file, \*FAD2_HANDLE)) != $ade_err_ok) {
            &ade_err_error($errstack_ref, faddiff_err_access, $fad2_file, "open");
	    return($rc);
        }
        $fad2_handle = \*FAD2_HANDLE;
    }
    
    &ade_err_debug($errstack_ref, 5, "faddiff: loading first FAD file ...");
    if (($rc=&fad_load($errstack_ref, $fad1_handle, \%data1_store, undef, undef)) != $ade_err_ok) {
        &ade_err_error($errstack_ref, faddiff_err_misc, "$fad1_file: corrupt FAD file");
        return($rc) 
    }
    &ade_err_debug($errstack_ref, 5, "faddiff: loading second FAD file ...");
    if (($rc=&fad_load($errstack_ref, $fad2_handle, \%data2_store, undef, undef)) != $ade_err_ok) {
        &ade_err_error($errstack_ref, faddiff_err_misc, "$fad2_file: corrupt FAD file");
        return($rc) 
    }
    &ade_err_debug($errstack_ref, 5, "faddiff: diffing indices ...");
    $count = 0;
    return ($rc) if (($rc=fad_diff($errstack_ref, \%data1_store, \%data2_store, \&faddiff_cb, { count_ref => \$count, out_handle => $out_handle })) != $ade_err_ok);

    &ade_err_debug($errstack_ref, 5, "main: closing input and output files");
    close FAD1_HANDLE if ($fad1_handle == \*FAD1_HANDLE);
    close FAD2_HANDLE if ($fad2_handle == \*FAD2_HANDLE);
    if ($out_handle  == \*OUT_HANDLE) {
        close OUT_HANDLE;  
        &ade_tmp_deregisterfile($errstack_ref, $opt_outfile);
    }

    #  The fad_diff function has been called and any errors it returned have
    #  already resulted in 'return' being called, so if we get here then 
    #  nothing has failed.
    #
    #  Now we need to return a return code based on whether there
    #  are any differences or not (just as /usr/bin/diff diff does).
    #  But if we return non-zero then the parent _ade_gep_main_withstack()
    #  function will think this function faddiff() has failed and display
    #  the error stack (which will be empty apart from the 'application
    #  entry function failed' frame that _ade_gep_main_withstack() puts on it).
    #
    #  Well, if we can suppress the displaying of the error message and
    #  leave it still to return an error return code then that is compliant
    #  with diff's behaviour. We can suppress the displaying of the error
    #  message by changing the error message display function.
    &ade_err_resetstack($errstack_ref, writers=>[\&_ade_msg_writer_null]);

    #  The exit code is derived from whether there were differences or not. 
    return($count ? $ade_err_fail : $ade_err_ok);
}

sub faddiff_opt_handler_g
{
    return(&faddiff_opt_handler_ignore_gid(@_));
}

sub faddiff_opt_handler_u
{
    return(&faddiff_opt_handler_ignore_uid(@_));
}

sub faddiff_opt_handler_m
{
    return(&faddiff_opt_handler_ignore_mode(@_));
}

sub faddiff_opt_handler_o
{
    return(&faddiff_opt_handler_file(@_));
}

sub faddiff_opt_handler_ignore_gid
{
    my($errstack_ref) = @_;

    $opt_igngroup = 1;
 
    return($ade_err_ok);
}

sub faddiff_opt_handler_ignore_uid
{
    my($errstack_ref) = @_;

    $opt_ignowner = 1;
 
    return($ade_err_ok);
}

sub faddiff_opt_handler_ignore_mode
{
    my($errstack_ref) = @_;

    $opt_ignperms = 1;
 
    return($ade_err_ok);
}

sub faddiff_opt_handler_file
{
    my($errstack_ref, $outfile) = @_;

    $opt_outfile = $outfile;
 
    return($ade_err_ok);
}

sub faddiff_version
{
    my($errstack_ref, $version_text_ref) = @_;

    return(&ade_smf_extractversionfromsvnstring($errstack_ref, $app_svnid, $version_text_ref));
}

sub faddiff_listpaths
{
    my($errstack_ref, $pathlist_text_ref) = @_;
    my($rc);

    ${$pathlist_text_ref} = undef;

    return($ade_err_ok);
}

sub faddiff_usage
{
    my($errstack_ref, $usage_text_ref, $passno) = @_;

    if ($passno == 1) {
        ${$usage_text_ref} = "<fad-file-1> <fad-file-2>";
    } elsif ($passno == 2) {
        ${$usage_text_ref} = "         -u        | --ignore-uid            ignore different UIDs\n" .
                             "         -g        | --ignore-gid            ignore different GIDs\n" .
                             "         -m        | --ignore-mode           ignore different file permissions\n" .
                             "         -o        | --file=<outfile>        send output to <outfile>";
    }

    return($ade_err_ok);
} 

sub faddiff_cb
{
    my($errstack_ref, $param, $change_ref) = @_;
    my($oldstate, $newstate, $file, $changetype);
    my($have_displayed_a_difference_for_current_file_flag, $i);
    my($count_ref, $out_handle);

    #  Unpack the two parameters from the one the callback interface allowed to pass.
    $count_ref = ${$param}{count_ref};
    $out_handle = ${$param}{out_handle};

    #  For each sort of difference detected for the one file for which this callback was called ...
    for ($i=0, $have_displayed_a_difference_for_current_file_flag=0; $i<$#{@{$change_ref}{changes}}+1; $i++) {
  
        #  Create some aliases
        $changetype = ${$change_ref}{changes}[$i]{type};
        $oldstate   = ${$change_ref}{changes}[$i]{old};
        $newstate   = ${$change_ref}{changes}[$i]{new};
        $file       = ${$change_ref}{file};
        &ade_err_debug($errstack_ref, 4, "faddiff_cb: changetype=$changetype, oldstate=$oldstate, newstate=$newstate, file=$file");

        #  Skip to next change if flags say ignore this type of change.
        ($changetype eq $CT_OWNER && $opt_ignowner) && next;
        ($changetype eq $CT_GROUP && $opt_igngroup) && next;
        ($changetype eq $CT_MODE  && $opt_ignperms) && next;

        #  Print name of file or comma separator (for second change type onwards)
        print $out_handle $have_displayed_a_difference_for_current_file_flag ? ", " : "$file: ";
        
        #  Display in on the change
        if ($changetype eq $CT_TYPE) {
            print $out_handle "type($oldstate -> $newstate)"; 
        } elsif ($changetype eq $CT_OWNER) {
            print $out_handle "owner($oldstate -> $newstate)"; 
        } elsif ($changetype eq $CT_GROUP) {
            print $out_handle "group($oldstate -> $newstate)"; 
        } elsif ($changetype eq $CT_MODE) {
            print $out_handle sprintf("mode(%o -> %o)", $oldstate, $newstate); 
        } elsif ($changetype eq $CT_CONT_MAJMIN) {
            print $out_handle "major/minor($oldstate -> $newstate)"; 
        } elsif ($changetype eq $CT_LINKS) {
            print $out_handle "links($oldstate -> $newstate)"; 
        } elsif ($changetype eq $CT_CONT_SYMLINK) {
            print $out_handle "linked_to($oldstate -> $newstate)"; 
        } elsif ($changetype eq $CT_CONT_CRC) {
            print $out_handle "checksum($oldstate -> $newstate)"; 
        } elsif ($changetype eq $CT_ADDED) {
            print $out_handle "added()"; 
        } elsif ($changetype eq $CT_DELETED) {
            print $out_handle "deleted()"; 
        } else {
            #  Uncatered for change type
            &ade_err_internal($errstack_ref, "faddiff_cb: unexpected CT: $changetype");
        }

        $have_displayed_a_difference_for_current_file_flag++;
    }
    print $out_handle $have_displayed_a_difference_for_current_file_flag ? "\n" : "";

    #  Increment count of files changed (not changes)
    ${$count_ref}++;

    return($ade_err_ok);
}

&ade_gep_main(\&faddiff);
