#!/usr/bin/perl -w

# Copyright 2008 Stefan Fritsch <sf@debian.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# 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.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

use strict;
use File::Temp qw/tmpnam/;

if ( !scalar @ARGV or $ARGV[0] eq '-h' or $ARGV[0] eq '--help' ) {
    print << "EOF";
usage:
$0 <diff1> [ <diff2> [...] ]

Input will be taken from stdin and written to stdout.
The files diff1 ... need to be ed style diffs from 'diff --ed'.
*.gz, *.bz2, and *.lzo patches are decompressed automatically
EOF
    exit 1 if !scalar @ARGV;
    exit 0;
}

my $patches;
my $filename;

foreach my $filename (@ARGV) {
    if ( $filename =~ m{\.gz$} ) {
        $filename = "gunzip -c $filename |";
    }
    elsif ( $filename =~ m{\.bz2$} ) {
        $filename = "bunzip2 -c $filename |";
    }
    elsif ( $filename =~ m{\.lzo$} ) {
        $filename = "lzop -dc $filename |";
    }
    else {
        $filename = "<$filename";
    }
    $patches = merge_patches(read_patch($filename), $patches);
}


my $cur = 1;
my $p;
while (defined ($p = $patches->() )) {
    my ( $op, $lines, $lines_ref ) = @{$p};
    if ( $op eq 'D' ) {
        die "Invalid number in D: $lines" unless $lines > 0;
        while ($lines-- > 0) {
            my $line = <STDIN>;
            if ( !defined $line ) {
                die "end of input while at: @{$p}\n";
            }
        }
    }
    elsif ($op eq 'I') {
        print @{$lines_ref};
    }
    elsif ($op eq 'C') {
        die "Invalid number in C: $lines" unless $lines > 0;
        while ($lines-- > 0) {
            my $line = <STDIN>;
            if ( !defined $line ) {
                die "end of input while at: @{$p}\n";
            }
            print $line;
        }
    }
}
# copy rest of file
my $line;
while ( defined( $line = <STDIN> ) ) {
    print $line;
}


# Reads a diff --ed style patch file
# Returns iterator function over patch hunks
# - in reverse order
# - converted into simple operations: _C_opy, _I_nsert, _D_elete
# Each hunk is an array consisting of
# - operation
# - number of lines
# - ref to array of lines (only for insert)
sub read_patch {
    my $filename = shift;

    open( my $fh_patch, $filename ) or die "Could not open '$filename'\n";

    my $line;
    my @hunks;

    # current position in the file
    my $num;

    while ( defined( $line = <$fh_patch> ) ) {
        chomp $line;
        my ($op, $n, $m);
        if ( $line =~ /^(\d+)(a)$/ ) {
            # Na: add lines after line N
            $n = $1;
            $op = $2;
            $m = $n;
            $n++;
        }
        elsif ( $line =~ /^(\d+)([cd])$/ ) {
            # Nd: remove line N
            # Nc: remove line N, then insert after line N-1
            $n = $1;
            $op = $2;
            $m = $n;
        }
        elsif ( $line =~ /^(\d+),(\d+)([cd])$/ ) {
            # N,Md: remove lines N to M
            # N,Mc: remove lines N to M, then insert after line M-1
            $n = $1;
            $m = $2;
            $op = $3;
        }
        elsif ( $line eq 's/.//' ) {
            # replace inserted ".." line by "."
            my $lines_ref = $hunks[0]->[2];
            if ( ref($lines_ref) ne 'ARRAY') {
                die "Invalid format:$filename:$.:$line:Not preceeded by add or change\n";
            }
            if ( $lines_ref->[-1] ne "..\n" ) {
                die "Invalid format:$filename:$.:$line:Not preceeded by '..'\n";
            }
            else {
                $lines_ref->[-1] = ".\n";
            }
        }
        elsif ( $line eq 'a' ) {
            # insert some more lines after the 's/.//' replacement
            my $lines_ref = $hunks[0]->[2];
            if ( ref($lines_ref) ne 'ARRAY' ) {
                die "Invalid format:$filename:$.:$line:Not preceeded by add or change\n";
            }
            while ( defined( $line = <$fh_patch> ) and $line ne ".\n" ) {
                push @$lines_ref, $line;
                $hunks[0]->[1]++;
            }
        }
        else {
            die "Invalid format:$filename:$.:$line\n";
        }

        if (!defined $op) {
            # we are merging with the last op
            next;
        }

        if ( $num ) {
            if ( $num < $n || $num < $m ) {
                die "Not in reverse order:$filename:$.:$line\n$num\n";
            }
            my $copy = $num - $m - 1;
            unshift @hunks, [ 'C', $copy ] if $copy; 
        }
        $num = $n;

        if ($op eq 'd' || $op eq 'c') {
            unshift @hunks, [ 'D', $m - $n + 1];
        }
        if ($op eq 'a' || $op eq 'c') {
            my @lines;
            while ( defined( $line = <$fh_patch> ) and $line ne ".\n" ) {
                push @lines, $line;
            }
            unshift @hunks, [ 'I', scalar @lines, \@lines ];
        }
    }
    if (defined $num && $num > 1) {
        # copy the rest of the file, i.e. the beginning since we are operating
        # in reverse order
        unshift @hunks, [ 'C', $num - 1];
    }

    #print STDERR "@$_\n" foreach (@hunks);
    #print STDERR "\n";

    close($fh_patch) or die "Error reading patch";
    return sub { shift @hunks };
}

# merges two patches
# in1 is applied first, then in2
# returns iterator function over merged patch hunks
sub merge_patches {
    my ($in1, $in2) = @_;

    return $in1 unless $in2;
    return $in2 unless $in1;

    my $cur1 = $in1->();
    my $cur2 = $in2->();

    return sub {
        my $ret;
        while (1) {

        #print STDERR "in:cur1: @$cur1\n" if $cur1;
        #print STDERR "in:cur2: @$cur2\n" if $cur2;
            if (!defined $cur1) {
                $ret = $cur2;
                $cur2 = $in2->();
                last;
            }
            if (!defined $cur2) {
                $ret = $cur1;
                $cur1 = $in1->();
                last;
            }

            if ($cur1->[0] eq 'I') {
                $ret = $cur1;
                $cur1 = $in1->();
                last;
            }
            elsif ($cur2->[0] eq 'D') {
                $ret = $cur2;
                $cur2 = $in2->();
                last;
            }
            elsif ($cur1->[1] >= $cur2->[1]) {
                if ($cur2->[0] eq 'C') {
                   $ret = [ $cur1->[0], $cur2->[1] ];
                }
                elsif ($cur1->[0] eq 'D') {
                    # cur2 was 'I'; discard it
                    $cur1->[1] -= $cur2->[1];
                    $cur1 = $in1->() unless $cur1->[1] > 0;
                    $cur2 = $in2->();
                    next;
                }
                else {
                    # cur2 is 'I', cur1 is 'C'
                    $ret = $cur2;
                }
                $cur1->[1] -= $cur2->[1];
                $cur2 = $in2->();
                $cur1 = $in1->() unless $cur1->[1] > 0;
                last;
            }
            else {
                # cur1 is smaller than cur2
                if ($cur1->[0] eq 'D') {
                    if ($cur2->[0] eq 'I') {
                        my $n = $cur1->[1];
                        my $list_ref = $cur2->[2];
                        $cur2->[2] = [@{$list_ref}[$n .. $#{$list_ref}]];
                        $cur2->[1] -= $n;
                        $cur1 = $in1->();
                        next;
                    }
                    else {
                        $ret = $cur1;
                        $cur2->[1] -= $cur1->[1];
                        $cur1 = $in1->();
                        last;
                    }
                }
                else {
                    # cur1 is 'C'
                    my $n = $cur1->[1];
                    $ret = [ $cur2->[0], $n];
                    if ($cur2->[0] eq 'I') {
                        my $list_ref = $cur2->[2];
                        push @$ret, [@{$list_ref}[0 .. $n -1]];
                        $cur2->[2] = [@{$list_ref}[$n .. $#{$list_ref}]]
                    }
                    $cur2->[1] -= $n;
                    $cur1 = $in1->();
                    last;
                }
            }
        }
        #print STDERR "out:ret: @$ret\n" if $ret;
        return $ret;
    };
}


# our style is roughly "perltidy -pbp"
# vim:sts=4:sw=4:expandtab
