=provides

=implementation

=cut

#!/usr/bin/perl -w
use strict;

my %opt = (
  quiet   => 0,
  diag    => 1,
  hints   => 1,
  changes => 1,
);

eval {
  require Getopt::Long;
  Getopt::Long::GetOptions(\%opt, qw(
    help quiet diag! hints! changes!
    patch=s copy=s diff=s
  )) or usage();
};

if ($@ and grep /^-/, @ARGV) {
  usage() if "@ARGV" =~ /^--?h(?:elp)?$/;
  die "Getopt::Long not found. Please don't use any options.\n";
}

usage() if $opt{help};

# Never use C comments in this file!!!!!
my $ccs  = '/'.'*';
my $cce  = '*'.'/';
my $rccs = quotemeta $ccs;
my $rcce = quotemeta $cce;

my @files;

if (@ARGV) {
  @files = map { glob $_ } @ARGV;
}
else {
  eval {
    require File::Find;
    File::Find::find(sub {
      $File::Find::name =~ /\.(xs|c|h|cc)$/i
          and push @files, $File::Find::name;
    }, '.');
  };
  if ($@) {
    @files = map { glob $_ } qw(*.xs *.c *.h *.cc);
  }
  my %filter = map { /(.*)\.xs$/ ? ("$1.c" => 1) : () } @files;
  @files = grep { !/\bppport\.h$/i && !exists $filter{$_} } @files;
}

unless (@files) {
  die "No input files given!\n";
}

my %API = map { /^(\w+)\|([^|]*)\|(\w*)$/
                ? ( $1 => { 
                      ($2                  ? ( todo     => $2 ) : ()),
                      (index($3, 'v') >= 0 ? ( varargs  => 1  ) : ()),
                      (index($3, 'p') >= 0 ? ( provided => 1  ) : ()),
                      (index($3, 'n') >= 0 ? ( nothxarg => 1  ) : ()),
                    } )
                : die "invalid spec: $_" } qw(
__PERL_API__
);

# TODO: remove
# use Data::Dumper; print Dumper(\%API);

# Scan for possible replacement candidates

my(%replace, %need, %hints, %depends);
my $replace = 0;
my $hint = '';

while (<DATA>) {
  if ($hint) {
    if (m{^\s*\*\s(.*?)\s*$}) {
      $hints{$hint} ||= '';  # suppress warning with older perls
      $hints{$hint} .= "$1\n";
    }
    else {
      $hint = '';
    }
  }
  $hint = $1 if m{^\s*$rccs\sHint:\s+(\w+)\s*$};

  $replace     = $1 if m{^\s*$rccs\s+Replace:\s+(\d+)\s+$rcce\s*$};
  $replace{$2} = $1 if $replace and m{^\s*#\s*define\s+(\w+)(?:\([^)]*\))?\s+(\w+)};
  $replace{$2} = $1 if m{^\s*#\s*define\s+(\w+)(?:\([^)]*\))?\s+(\w+).*$rccs\s+Replace\s+$rcce};
  $replace{$1} = $2 if m{^\s*$rccs\s+Replace (\w+) with (\w+)\s+$rcce\s*$};

  if (m{^\s*$rccs\s+(\w+)\s+depends\s+on\s+(\w+(\s*,\s*\w+)*)\s+$rcce\s*$}) {
    push @{$depends{$1}}, map { s/\s+//g; $_ } split /,/, $2;
  }

  $need{$1} = 1 if m{^#if\s+defined\(NEED_(\w+)(?:_GLOBAL)?\)};
}

# TODO: remove
# use Data::Dumper; print Dumper(\%replace, \%need, \%hints, \%depends);

my(%files, %global);
my $filename;

# TODO: perform global checks

for $filename (@files) {
  unless (open IN, "<$filename") {
    warn "Unable to read from $filename: $!\n";
    next;
  }

  info("Scanning $filename ...");

  my $c = do { local $/; <IN> };
  close IN;

  my %file = (orig => $c, changes => 0);

  # temporarily remove C comments from the code
  my @ccom;
  $c =~ s{
    (
        [^"'/]+
      |
        (?:"[^"\\]*(?:\\.[^"\\]*)*" [^"'/]*)+
      |
        (?:'[^'\\]*(?:\\.[^'\\]*)*' [^"'/]*)+
    )
  |
    (/ (?:
        \*[^*]*\*+(?:[^$ccs][^*]*\*+)* /
        |
        /[^\n]*
      ))
  }{
    defined $2 and push @ccom, $2;
    defined $1 ? $1 : "$ccs$#ccom$cce";
  }egsx;

  $file{ccom} = \@ccom;
  $file{code} = $c;
  $file{has_inc_ppport} = ($c =~ /#.*include.*ppport\.h/);

  my $func;

  for $func (keys %API) {
    if ($c =~ /\b(?:[Pp]erl_)?$func\b/) {
      $file{uses_Perl}{$func}++ if $c =~ /\bPerl_$func\b/;
      if (exists $API{$func}{provided}) {
        $file{uses}{$func}++;
        push @{$global{uses}{$func}}, $filename;
        my @deps = rec_depend($func);
        if (@deps) {
          $file{uses_deps}{$func} = \@deps;
          for (@deps) {
            $file{uses}{$_} = 0 unless exists $file{uses}{$_};
            push @{$global{uses}{$_}}, $filename;
          }
        }
        for ($func, @deps) {
          if (exists $need{$_}) {
            $file{needs}{$_}++;
            push @{$global{needs}{$_}}, $filename;
          }
        }
      }
      if (exists $API{$func}{todo}) {
        if ($c =~ /\b$func\b/) {
          $file{uses_todo}{$func}++;
          push @{$global{uses_todo}{$func}}, $filename;
        }
      }
    }
  }

  while ($c =~ /^\s*#\s*define\s+(NEED_(\w+?)(_GLOBAL)?)\b/mg) {
    if (exists $need{$2}) {
      $file{defined $3 ? 'needed_global' : 'needed_static'}{$2}++;
      push @{$global{defined $3 ? 'needed_global' : 'needed_static'}{$2}}, $filename;
    }
    else {
      warning("Possibly wrong #define $1 in $filename");
    }
  }

  $files{$filename} = \%file;
}

# TODO: remove
# use Data::Dumper; print Dumper(\%files, \%global);

for $filename (@files) {
  exists $files{$filename} or next;

  info("=== Analyzing $filename ===");

  my %file = %{$files{$filename}};
  my $func;
  my $c = $file{code};

  for $func (keys %{$file{uses_Perl}}) {
    if ($API{$func}{varargs}) {
      # TODO: check if aTHX needs to be passed
    }
    else {
      warning("Uses Perl_$func instead of $func");
      $file{changes} += ($c =~ s{\bPerl_$func(\s*)\((\s*aTHX_?)?\s*}
                                {$func$1(}g);
    }
  }

  for $func (keys %{$file{uses}}) {
    next unless $file{uses}{$func};   # if it's only a dependency
    if (exists $file{uses_deps}{$func}) {
      diag("Uses $func, which depends on ", join(', ', @{$file{uses_deps}{$func}}));
    }
    elsif (exists $replace{$func}) {
      warning("Uses $func instead of $replace{$func}");
      $file{changes} += ($c =~ s/\b$func\b/$replace{$func}/g);
    }
    elsif (exists $hints{$func}) {
      diag("Uses $func");
    }
    hint($func);
  }

  for $func (keys %{$file{uses_todo}}) {
    warning("Uses $func, which may not be portable below perl ",
            format_version($API{$func}{todo}));
  }

  for $func (keys %{$file{needed_static}}) {
    if (not exists $file{uses}{$func}) {
      diag("No need to define NEED_$func if $func is never used");
      $file{changes} += ($c =~ s/^\s*#\s*define\s+NEED_$func\b.*\r?\n?//mg);
    }
  }

  for $func (keys %{$file{needed_global}}) {
    if (not exists $global{uses}{$func}) {
      diag("No need to define NEED_${func}_GLOBAL if $func is never used");
      $file{changes} += ($c =~ s/^\s*#\s*define\s+NEED_${func}_GLOBAL\b.*\r?\n?//mg);
    }
  }

  $file{needs_inc_ppport} = keys %{$file{uses}};

  if ($file{needs_inc_ppport}) {
    my $pp = '';

    for $func (keys %{$file{needs}}) {
      $pp .= "#define NEED_$func\n"
        unless exists $file{needed_global}{$func} || exists $file{needed_static}{$func};
    }

    if ($pp && ($c =~ s/^(?=[^\S\r\n]*#\s*define\s+NEED_\w+)/$pp/m)) {
      $pp = '';
      $file{changes}++;
    }

    $pp .= qq(#include "ppport.h"\n) unless $file{has_inc_ppport};

    if ($pp) {
      $file{changes} += ($c =~ s/(.*^\s*#\s*define\s+NEED_\w+.*?)^/$1$pp/ms)
                     || ($c =~ s/^(?=[^\S\r\n]*#\s*include.*ppport\.h)/$pp/m)
                     || ($c =~ s/^(\s*#\s*include.*XSUB.*\s*?)^/$1$pp/m)
                     || ($c =~ s/^/$pp/);
    }
  }
  else {
    if ($file{has_inc_ppport}) {
      diag("No need to include 'ppport.h'");
      $file{changes} += ($c =~ s/^\s*#\s*include.*ppport\.h.*\s*?^//m);
    }
  }

  # put back in our C comments
  my $ix;
  my @ccom = @{$file{ccom}};
  for $ix (0 .. $#ccom) {
    $c =~ s/$rccs$ix$rcce/$ccom[$ix]/;
  }

  # TODO: remove
  # use Data::Dumper; print Dumper(\%file);

  if ($file{changes}) {
    if ($opt{changes}) {
      # TODO: make more flexible (use Text::Diff, File::Temp, if available)
      info("Suggested changes:");

      open OUT, ">/tmp/ppport.h.$$";
      print OUT $c;
      close OUT;

      open DIFF, "diff -u $filename /tmp/ppport.h.$$|" ;
      while (<DIFF>) {
        s!/tmp/ppport\.h\.$$!$filename.patched!;
        print STDOUT;
      }
      close DIFF;

      unlink("/tmp/ppport.h.$$");
    }
  }
  else {
    info("Looks good");
  }
}

exit 0;

#######################################################################

sub rec_depend
{
  my $func = shift;
  return () unless exists $depends{$func};
  map { ($_, rec_depend($_)) } @{$depends{$func}};
}

sub format_version
{
  my $ver = shift;

  $ver =~ s/$/000000/;
  my($r,$v,$s) = $ver =~ /(\d+)\.(\d{3})(\d{3})/;

  $v = int $v;
  $s = int $s;

  if ($r < 5 || ($r == 5 && $v < 6)) {
    if ($s % 10) {
      die "invalid version '$ver'\n";
    }
    $s /= 10;

    $ver = sprintf "%d.%03d", $r, $v;
    $s > 0 and $ver .= sprintf "_%02d", $s;

    return $ver;
  }

  return sprintf "%d.%d.%d", $r, $v, $s;
}

sub info
{
  $opt{quiet} and return;
  print @_, "\n";
}

sub diag
{
  $opt{quiet} and return;
  $opt{diag} and print @_, "\n";
}

sub warning
{
  $opt{quiet} and return;
  print "*** ", @_, "\n";
}

my %given_hints;
sub hint
{
  $opt{quiet} and return;
  $opt{hints} or return;
  my $func = shift;
  exists $hints{$func} or return;
  $given_hints{$func}++ and return;
  my $hint = $hints{$func};
  $hint =~ s/^/   /mg;
  print "   --- hint for $func ---\n", $hint;
}

sub usage
{
  my($usage) = do { local(@ARGV,$/)=($0); <> } =~ /^=head\d\s+SYNOPSIS\s*^(.*?)\s*^=/ms;
  my %M = ( 'I' => '*' );
  $usage =~ s/^\s*perl/$^X/;
  $usage =~ s/([A-Z])<([^>]+)>/$M{$1}$2$M{$1}/g;

  print <<ENDUSAGE;

Usage: $usage

See perldoc $0 for details.

ENDUSAGE

  exit 2;
}
