#!/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_include_prefix`,0,-1;
use ADE;
use lib substr `fad-config fad_include_prefix`,0,-1;
use FAD;
use Getopt::Long qw(:config no_ignore_case);

&ade_err_registerdefderrs({
    faddiff_err_misc        => { fmt => "%s" },
    faddiff_err_access        => { fmt => "%s: can't %s" },
    faddiff_err_internal        => { fmt => "%s" },
});

my($opt_outfile, $opt_ignowner, $opt_igngroup, $opt_ignperms);
my(@faddiff_config_hasharray) = (
    { dsc => "opt_outfile",  var => \$opt_outfile,  dfl => "-" },
    { dsc => "opt_ignowner", var => \$opt_ignowner, dfl => 0 },
    { dsc => "opt_igngroup", var => \$opt_igngroup, dfl => 0 },
    { dsc => "opt_ignperms", var => \$opt_ignperms, dfl => 0 },
);
 
sub faddiff
{
    my($errstack_ref) = @_;
    my($fad1_file, $fad2_file, $out_handle, $fad1_handle, $fad2_handle);
    my($rc, $diff_rc, $optval, $procopts_hashref);
    my(%data1_store, %data2_store, @data1_index, @data2_index, $count);

    ##########################################################################
    #
    #  Process options
    #
    ##########################################################################

    $procopts_hashref = {
        "g|ignore-gid"  => sub { $opt_igngroup = 1; }, 
        "u|ignore-uid"  => sub { $opt_ignowner = 1; }, 
        "m|ignore-mode" => sub { $opt_ignperms = 1; }, 
        "o|file=s"      => \$opt_outfile 
    };
    if (($rc=&ade_spc_procopts($errstack_ref, \&faddiff_listpaths, \&faddiff_usage, \&faddiff_version, \@faddiff_config_hasharray, $procopts_hashref)) != $ade_err_ok) {
        return($rc); 
    } 

    ##########################################################################
    #
    #  Process arguments
    #
    ##########################################################################

    &ade_err_debug($errstack_ref, 10, "faddiff: processing arguments (\$ARGV[0]=$ARGV[0], \$ARGV[1]=$ARGV[1]) ... ");
    (!defined($ARGV[1]) || defined($ARGV[2])) && &ade_msg_usage($errstack_ref, \&faddiff_usage, 1);
    $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 ...");
    return($rc) if (($rc=&fad_load($errstack_ref, $fad1_handle, \%data1_store, undef, undef)) != $ade_err_ok);
    &ade_err_debug($errstack_ref, 5, "faddiff: loading second FAD file ...");
    return($rc) if (($rc=&fad_load($errstack_ref, $fad2_handle, \%data2_store, undef, undef)) != $ade_err_ok);

    &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 exit code is derived from whether there were differences or not. 
    #  Err ... no, we can't do that (yet) in ADE-compatible programs; non-zero
    #  *always* represents an error.
    return($ade_err_ok);
}

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

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

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

    %{$pathlist_ref} = ();
    return($ade_err_ok);
}

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

    if ($passno == 1) {
        print "<fad-file-1> <fad-file-2>\n";
    } elsif ($passno == 2) {
        print "         -u        | --ignore-uid            ignore different UIDs\n";
        print "         -g        | --ignore-gid            ignore different GIDs\n";
        print "         -m        | --ignore-mode           ignore different file permissions\n";
        print "         -o        | --file=<outfile>        send output to <outfile>\n";
    } else {
        &ade_err_error($errstack_ref, faddiff_err_internal, "faddiff_usage: $passno: bad pass number");
        return($ade_err_fail);
    }

    return($ade_err_ok);
}

sub faddiff_cb
{
    my($errstack_ref, $param, $change_ref) = @_;
    my($oldstate, $newstate, $file, $changetype);
    my($hadone, $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, $hadone=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 ($hadone++) ? ", " : "$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_error($errstack_ref, faddiff_err_internal, "faddiff_cb: unexpected CT: $changetype");
            return($ade_err_fail);
        }
    }
    print $out_handle "\n";

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

    return($ade_err_ok);
}

&ade_gep_main(\&faddiff);
