#!/usr/bin/perl -w

use strict;
use warnings;
use Getopt::Long;
use File::Basename;
use Pod::Usage;
use Gear::Rules;
use RPM::Source::Editor;

my $specfile;
my $help=0;
my $opt_patchshift=0;
my ($opt_donorspec, $opt_patchsource, $opt_patchprefix, $opt_dry_run, $opt_debian_mode);

GetOptions (
    'h|help'  => \$help,
    'n|dry-run' => \$opt_dry_run,
    'spec=s' => \$specfile,
    'from=s' => \$opt_patchsource,
    'shift=i' => \$opt_patchshift,
    'prefix=i' => \$opt_patchprefix,
    'inject-spec=s' => \$opt_donorspec,
    'no-inject-spec|debian' => \$opt_debian_mode,
) or pod2usage("$0: Error in command line arguments.\n");
pod2usage("
$0 can and usually should be run
without command line arguments in a gear repository.
Its arguments should be placed in the spec file.
However, there are also command line arguments for testing
and debug purposes.\n") if $help;

$opt_patchsource//=$ARGV[0];

unless ($specfile) {
    # TODO: use Gear::Rules::is_gear_repository_dir()
    if (-d '.git' and (-f '.gear-rules' or -f '.gear/rules')) {
	my $rules=Gear::Rules->new();
	$specfile=$rules->get_spec();
    }
}
die "file $specfile not found" unless -f $specfile;

my $spec=RPM::Source::Editor->new(SPECFILE=>$specfile);
my @call;
$spec->main_section->map_body(sub {
    if (m/^#\s*BeginPatches\((\S+)\)(?:\[([^\]]*)\])?:/) {
	push @call,  => &__set_args($1,$2);
    }
			      });
push @call, &__mk_args() unless scalar @call;

foreach my $opt (@call) {
    &inject_patches($spec,$opt);
}

if ($opt_dry_run) {
    print $spec->get_spec();
} else {
    $spec->write_spec($specfile);
}
    
sub __mk_args {
    my %out=(
	-patchsource=> $opt_patchsource,
	-spec  => $opt_donorspec,
	-shift => $opt_patchshift,
	-prefix => $opt_patchprefix,
	-debian => $opt_debian_mode,
	);
    return \%out;
}

sub __set_args {
    my ($patchsource, $in)=@_;
    my $out=&__mk_args;
    $out->{-patchsource} = $patchsource;
    if ($in) {
	foreach my $set (split(/,\s*/,$in)) {
	    if ($set=~/\s*(\S+)=(\S+)/) {
		$out->{'-'.$1}=$2;
	    } else {
		die "Bad arguments: $patchsource $in ($set)";
	    }
	}
    }
    return $out;
}


sub inject_patches {
    my ($spec,$opt)=@_;
    my ($patchsource,$patchshift)=($opt->{-patchsource},$opt->{-shift});
    my $donorspec=$opt->{-spec} || $opt->{-inject_spec};
    my $debian_mode=$opt->{-debian} || $opt->{-no_inject_spec};
    my $patchprefix=$opt->{-prefix};
    $debian_mode=1 if $patchprefix;
    die "bad dir $patchsource: $patchsource/applied/ and $patchsource/not-applied/ not found.\n" unless  -d $patchsource.'/applied' and -d $patchsource.'/not-applied';
    unless ($donorspec or $debian_mode) {
	my @candidates=glob($patchsource.'/applied/*.spec');
	# TODO
	$donorspec=$candidates[0] if @candidates;
    }
    die "file $donorspec not found" if $donorspec and ! -f $donorspec;
    unless ($donorspec or $debian_mode) {
	$debian_mode=1;
	warn "spec file not found in $patchsource/applied/" unless $patchsource eq 'debian' or $patchsource eq 'ubuntu';
    }

    my @add_main;
    my @add_prep;
    my %patchadded;
    my %patchknown;
    my @candidate;

    if ($debian_mode) {
	my @patches;
	push @patches, glob("$patchsource/applied/*.patch");
	push @patches, glob("$patchsource/applied/*.diff");
	my $i=0;
	foreach my $patch (sort {$a cmp $b} @patches) {
	    my $patchnum=$i+$patchshift;
	    push @add_main, 'Patch'.$patchnum.': '.$patch."\n";
	    push @add_prep, '%patch'.$patchnum.($patchprefix ? ' -p'.$patchprefix : '')."\n";
	    $i++;
	}
    } else {
	my $donor=RPM::Source::Editor->new(SPECFILE=>$donorspec);
	$donor->main_section->map_body(
sub {
    if (/^\s*(#|$)/) {
	push @candidate, $_;
    } elsif (/^(\s*Patch)(\d+)(\s*:\s*(\S+)\s*)$/) {
	my ($patchprefix,$patchnumber,$patchrest,$patchfile)=($1,$2,$3,$4);
	$patchknown{$patchnumber}=1;
	push @candidate, $patchprefix.($patchshift? $patchnumber+$patchshift : $patchnumber).$patchrest;
	if (-f $patchsource.'/applied/'.$patchfile) {
	    push @add_main, @candidate;
	    $patchadded{$patchnumber}=1;
	} elsif (! -f $patchsource.'/not-applied/'.$patchfile) {
	    warn "patch not found: $_";
	}
	@candidate=();
    } else {
	@candidate=();
    }
	    });

	@candidate=();
	$donor->get_section('prep')->map_body(
	    sub {
		if (/^\s*#\s*\%?patch/) {
		    @candidate=();
		} elsif (/^\s*(?:#|$)/) {
		    push @candidate, $_;
		} elsif (m/^(\s*\%patch)(\d+)(\s*.*)$/) {
		    my ($patchprefix,$patchnumber,$patchrest)=($1,$2,$3);
		    my $line=$patchprefix.($patchshift? $patchnumber+$patchshift : $patchnumber).$patchrest;
		    chomp $line;
		    push @candidate, $line."\n";
		    if ($patchadded{$patchnumber}) {
			push @add_prep, @candidate;
		    } elsif (!$patchknown{$patchnumber}) {
			warn "prep: patch not found: $_";
		    }
		    @candidate=();
		} else {
		    @candidate=();
		}
	    });
    }
    
    &replace_patches($spec->main_section,$patchsource,\@add_main);
    &replace_patches($spec->get_section('prep'),$patchsource,\@add_prep);
}

sub replace_patches {
    my ($section,$patchsource,$add_body)=@_;
    my $patchsourcename=basename($patchsource);
    my $placepattern=$patchsource;
    $placepattern="(?:$patchsource|$patchsourcename)" if $patchsourcename ne $patchsource;
    my @new_body;
    my @body=@{$section->get_bodyref};
    while (scalar @body>0 and $body[0] !~ m/^#\s*BeginPatches\($placepattern\)[\[:]/) {
	push @new_body, shift @body;
    }
    die "Oops! # BeginPatches($patchsourcename): not found!" unless @body;
    die "Oops! expected BeginPatches($patchsourcename), got $body[0]" unless $body[0] =~ m/^#\s*BeginPatches\($placepattern\)[\[:]/;
    push @new_body, shift @body;
    while (@body and $body[0]!~/^#\s*EndPatches\($placepattern\):/) {
	shift @body;
    }
    die "Oops! # EndPatches($patchsourcename): not found!" unless @body;
    die "Oops! expected EndPatches($patchsourcename), got $body[0]" unless $body[0] =~ m/^#\s*EndPatches\($placepattern\):/;
    push @new_body, @$add_body;
    push @new_body, @body;
    $section->set_body(\@new_body);
}


__END__

=head1	NAME

srpm-spec-inject-patches - inject patches into a spec from set of dirs.

=head1	SYNOPSIS

B<srpm-spec-inject-patches>
[B<-h|--help>]
[B<-n|--dry-run>]
[B<--spec> I<spec file to insert>]
[B<--from> I<directory>]
[B<--inject-spec> I<path to spec file>]
[B<--no-inject-spec,--debian>]
[B<--shift> I<number>]
[B<--prefix> I<number>]

=head1	DESCRIPTION

B<srpm-spec-inject-patches> can and usually should be run
without command line arguments in a gear repository.
Its arguments should be placed in the spec file.
However, there are also command line arguments for testing
and debug purposes.

To manage external patches with the help of this utility
create a directory for each source of patches. For example:
 mkdir fedora debian suse
Create subdirs `applied' and `not-applied' in each directory.
Unpack the contents of the corresponding source package to
its directory (for example, src.rpm from Fedora to fedora dir)
removing upstream sources. Move the spec file and the patches
you want to apply to the 'applied' subdir. Move the rest
to the 'not-applied' subdir. Add the dir to the git.
Add the gear rule to copy patches form applied subdir.
Add the placeholders to the main and prep sections of the spec
file with the format below:

# BeginPatches(<dirname>)<[optional variables]>: <optional comments>
# EndPatches(<dirname>): <optional comments>

for example,

 # ------ inserted with srpm-spec-inject-patches(1) -------
 # BeginPatches(fedora)[shift=300]: ----------------------------------
 # EndPatches(fedora): ------------------------------------

note that the variables (in the `[' `]' brackets) are read and set
in the main section only. You can place variables in prep section
placeholder, but they will be ignored.

Now run the script and the placeholders will be filled with the
patches found in `applied' subdir.

For the list and meaning of suppored variables, see OPTIONS

=head1	OPTIONS

=over

=item	B<-h, --help>

Display this help and exit.

=item	B<-n, --dry-run>

Do not overwrite the spec; just print the result to stdout.

=item	B<--spec> I<spec file to insert>

Spec file to insert patches to. By default, if a gear repository detected,
the default gear repository spec file is used.

=item	[B<--from> I<directory>]

Directory for the source of patches. It should contain `applied' and
`not-applied' subdirs. The argument corresponds to <dirname> in the
placeholders
 # BeginPatches(<dirname>)<[optional variables]>: <optional comments>
 # EndPatches(<dirname>): <optional comments>

=item	[B<--shift> I<number>]

Shift the patch numbers of inserted patches by a given number.
The corresponding variable is called I<shift>.

=item	[B<--inject-spec> I<path to spec file>]

The spec file that has patches to be inserted. The utility will pick
the PatchXX: line for each patch to be inserted (from the `applied'
subdir) together with the preceding comments and will pick the patch
prefixes (%patchXX -p<prefix>) and other options from the %prep section.
Also the utility will check that all patches mentioned in the spec
are found either in `applied' or `not-applied' subdir.
The corresponding variable is called I<spec> (also I<inject_spec>).

By default, a spec file found in `applied' subdir is used.
If a spec file is not found or not provided a warning is issued
unless `debian' mode is enabled. 

=item	[B<--no-inject-spec,--debian>]

Enables `debian' mode. In `debian' mode no spec file is expected
for the given set of patches. Instead, patches are added in the
lexicographical order. If the directory for the source of patches
has name `debian' or `ubuntu', then `debian' mode is enabled by
default. 

The corresponding variable assignment looks like
I<no_inject_spec=1> or I<debian=1>.

=item	[B<--prefix> I<number>]

Set prefix in %patch -p<prefix>. Assume `debian' mode.
The corresponding variable is called I<prefix>.

=back

=head1	AUTHOR

Written by Igor Vlasenko <viy@altlinux.org>.

=head1	COPYING

Copyright (c) 2017 Igor Vlasenko, ALT Linux Team.

This 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.

=cut
