#!/usr/bin/perl

# Created on: 2008-01-16 08:05:58
# Create by:  ivanw
# $Id$
# $Revision$, $HeadURL$, $Date$
# $Revision$, $Source$, $Date$

use strict;
use warnings;
use version;
use List::Util qw/max min/;
use Getopt::Long;
use Pod::Usage;
use Data::Dumper qw/Dumper/;
use English qw/ -no_match_vars /;
use FindBin qw/$Bin/;
use VCS::Which;
use IO::Prompt qw/prompt/;
use Path::Tiny;
use File::Copy qw/copy/;

our $VERSION = version->new('0.6.0');
my ($name)   = $PROGRAM_NAME =~ m{^.*/(.*?)$}mxs;

my %option = (
    revision => '',
    diff     => 'vimdiff',
    verbose  => 0,
    man      => 0,
    help     => 0,
    VERSION  => 0,
);

if ( !@ARGV ) {
    pod2usage( -verbose => 1 );
}

main();
exit 0;

sub main {

    Getopt::Long::Configure('bundling');
    GetOptions(
        \%option,
        'revision|r=s',
        'replay|R',
        'merge|M',
        'change|c=i',
        'changed|changed-only|C',
        'max|m=i',
        'prev|previous|p',
        'three|3',
        'diff|d=s',
        'diff_option|diff-option|o=s@',
        'test|t!',
        'verbose|v+',
        'man',
        'help',
        'VERSION!',
    ) or pod2usage(2);

    if ( $option{'VERSION'} ) {
        print "$name Version = $VERSION\n";
        exit 1;
    }
    elsif ( $option{'man'} ) {
        pod2usage( -verbose => 2 );
    }
    elsif ( $option{'help'} || !@ARGV || !-e $ARGV[0] ) {
        warn `pwd`;
        warn "Can't find $ARGV[0]!\n" if !-e $ARGV[0];
        pod2usage( -verbose => 1 );
    }

    my $temp_dir = $ENV{TEMP} || '/tmp';
    for my $file (@ARGV) {
        # sort out the revision names
        my ( $rev_old, $rev_new ) = ('', '');
        if ( $option{'revision'} ) {
            ( $rev_old, $rev_new )
                = $option{'revision'} =~ /:/xms ? ( split /:/xms, $option{'revision'} )
                :                                 ( $option{'revision'} );
        }

        if ( $option{'change'} ) {
            ( $rev_old, $rev_new ) =
                $option{'change'} > 0 ? ( $option{'change'} - 1, $option{'change'} )
                :                       ( $option{'change'}, $option{'change'} - 1 );
            $option{'revision'} = "$rev_old\:$rev_new";
        }

        # create the temporary file object name
        my $tmp = "$temp_dir/$$.$name." . path($file)->basename;

        # create a new VCS::Which object for the file of interest
        my $vcs = VCS::Which->new(dir => $file);

        if ($option{replay}) {
            my $warned   = 0;
            my $versions = $vcs->log( $file );
            if ( $option{verbose} > 2 ) {
                my $i = 0;
                my $j = -2;
                print map {$i++ . "\t" . $j-- . "\t$versions->{$_}{rev}\n"} sort keys %$versions;
            }
            my $last;
            die "Could not find any versions for $file!\n" if !%$versions;

            while ( %$versions ) {
                if ( ( $option{prev} || $option{three} ) && $rev_old ) {
                    unlink $last if $last && -e $last;
                    $last = "$tmp.$rev_old";
                }
                else {
                    unlink "$tmp.$rev_old" if -e "$tmp.$rev_old";
                }

                my $rev_no = max keys %$versions;
                my $rev    = delete $versions->{$rev_no};
                $rev_old = $rev->{rev};

                my @files
                    = $option{three} && $last ? ( $file, $last, "$tmp.$rev_old" )
                    : $option{prev} && $last  ? ( $last, "$tmp.$rev_old" )
                    :                           ( $file, "$tmp.$rev_old" );

                my $diff = "$option{diff} " . join ' ', @files;

                # write the revisioned file to disk
                write_file( "$tmp.$rev_old", scalar $vcs->cat($file, $rev_old) );

                if ($option{test} || $option{verbose}) {
                    print "$diff\n" if $option{verbose} > 1;
                    my $other = { %$rev };

                    if ( @files == 2 ) {
                        my $diff = "diff -wuN " . join ' ', @files;
                        my @diff = `$diff`;
                        $other->{'Lines added'}   = grep {/^([+] [^+] )/gxms} @diff;
                        $other->{'Lines removed'} = grep {/^([-] [^-] )/gxms} @diff;
                    }

                    local $Data::Dumper::Indent = 1;
                    local $Data::Dumper::Sortkeys = 1;
                    print Data::Dumper->Dump([$other], ["*$rev_no"]);

                    next if @files == 2 && $other->{'Lines added'} == 0 && $other->{'Lines removed'} == 0;
                }

                print "y = yes continue (Default), n = not stop processing, s = skip this revision\n" if !$warned++;
                my $ans = prompt(
                    "Next revision $rev_old: Continue? [yns] ",
                    '-tty',
                    '-1',
                    -d => 'y',
                );
                print "\n\n";
                $ans = lc $ans;
                last if $ans eq 'n' || $ans eq 'q';
                next if $ans eq 's';

                # run the diff if not testing
                diff(@files) if !$option{test};
            }
            unlink "$tmp.$rev_old" if -e "$tmp.$rev_old";
            unlink $last           if $last && -e $last;
        }
        elsif ( $option{merge}) {
            $file = path($file);

            # copy the current file out of the way
            copy $file, "$temp_dir/" . $file->basename . ".merge";

            # get current branches "ours" version
            $vcs->checkout($file, qw/--ours/);
            # copy to "ours"
            copy $file, "$temp_dir/" . $file->basename . ".ours";

            # get current branches "theirs" version
            $vcs->checkout($file, qw/--theirs/);
            # copy to "theirs"
            copy $file, "$temp_dir/" . $file->basename . ".theirs";

            # return the file
            copy "$temp_dir/" . $file->basename . ".merge", $file;

            diff( "$temp_dir/" . $file->basename . ".ours", $file, "$temp_dir/" . $file->basename . ".theirs" );
        }
        elsif ($rev_new) {
            write_file( "$tmp.$rev_new", scalar $vcs->cat($file, $rev_new) );
            write_file( "$tmp.$rev_old", scalar $vcs->cat($file, $rev_old) );
            if ($option{test} || $option{verbose}) {
                print "$option{diff} $tmp.$rev_new $tmp.$rev_old\n";
            }
            if (!$option{test}) {
                diff( "$tmp.$rev_new", "$tmp.$rev_old" );
            }
        }
        elsif ($rev_old) {
            write_file( "$tmp.$rev_old", scalar $vcs->cat($file, $rev_old) );
            if ($option{test} || $option{verbose}) {
                print "$option{diff} $file $tmp.$rev_old\n";
            }
            if (!$option{test}) {
                diff( $file, "$tmp.$rev_old" );
            }
        }
        else {
            my $log = $vcs->log($file);
            my ($ver) = max keys %$log;
            my $rev = $log->{$ver}{rev} ? ".$log->{$ver}{rev}" : '';

            my $content;
            my $i = -1;
            while ( !$content && $i++ < 10 ) {
                $content = $vcs->cat($file, $i ? ":$i" : '');
            }
            write_file( "$tmp$rev", $content );

            if ($option{test} || $option{verbose}) {
                print "$option{diff} $file $tmp$rev\n";
            }
            if (!$option{test}) {
                diff( $file, "$tmp$rev" );
            }
        }
    }

    return;
}

sub write_file {
    my ($file, @contents) = @_;
    $file = path($file);
    $file->parent->mkpath;
    my $fh = $file->openw;
    print {$fh} @contents;
    close $fh;
}

sub diff {
    my (@files) = @_;

    if ( $option{changed} && @files == 2 ) {
        return unless `diff '$files[0]' '$files[1]'`;
    }

    system $option{diff}, @{ $option{diff_option} || [] }, @files;
}

__DATA__

=head1 NAME

vcsvimdiff - Uses vimdiff to compare a file with it unmodified version or historic versions from git, subversion, bazaar or cvs.

=head1 VERSION

This documentation refers to vcsvimdiff version 0.6.0.

=head1 SYNOPSIS

   vcsvimdiff [option] file

 OPTIONS:
  -r --revision=rev1[:rev2]
                Specify the revision(s) to compare with. HEAD is used if only
                one revision specified.
  -c --change=int
                Specify changes from a specific revision (see vcs help diff)
  -R --replay   Replay the revision history of "file", --verbose shows the
                details of each change before going into vimdiff
  -m --max=int  Maximum number of revisions to replay
  -3 --three    Three way diff for --replay, showing the file on disk, the
                revision one newer than the current revision and the current
                revision.
  -M --merge    Assume in a git merge and show "ours" and "theirs" versions
                along git's conflicted version
  -p --previous
                If set diff are between the revision currently being used and
                the either the last looked revision or the file on disk
  -C --changed-only
                Don't run vimdiff unless the files actually differ
  -t --test     Turn on testing (vimdiff wont actually be run)
     --no-test  Trurn off testing
  -d --diff=str Allows you to specify a diffing program (Default vimdiff)
  -o --diff-option[=]str
                Pass extra options of diff command (eg --diff-option=-R)

  -v --verbose  Show more detailed option
     --version  Prints the version information
     --help     Prints this help information
     --man      Prints the full documentation for vcsvimdiff

=head1 DESCRIPTION

=head1 SUBROUTINES/METHODS

=head1 DIAGNOSTICS

=head1 CONFIGURATION AND ENVIRONMENT

=head1 DEPENDENCIES

=head1 INCOMPATIBILITIES

=head1 BUGS AND LIMITATIONS

There are no known bugs in this module.

Please report problems to Ivan Wills (ivan.wills@gmail.com).

Patches are welcome.

=head1 AUTHOR

Ivan Wills - (ivan.wills@gmail.com)

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2009 Ivan Wills (14 Mullion Close, Hornsby Heights, NSW, 2077)
All rights reserved.

This module is free software; you can redistribute it and/or modify it under
the same terms as Perl itself. See L<perlartistic>.  This program is
distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.

=cut
