#!/usr/bin/perl -w
# tries to reduce the number of includes in KDE source files
# (c) 2001-2003 Dirk Mueller <mueller@kde.org>

use File::Basename;
use Cwd;

# declaration of useful subroutines
sub find_src_includes($);
sub find_fixable_sources ($);
sub find_fixable_headers($);
sub find_removable_includes ($);
sub warn_before_modifying ($);
sub remove_include ($$$);
sub replace_include ($$$);
sub replace_include_type ($$);
sub fix_duplicates($);
sub fix_compat_includes($);
sub fix_unnecessary($);
sub fix_include_type($);
sub copy_file($$);
sub process_source_file($);
sub extract_gcc_error($);

# some global variables
$verbose = 0; # turns on debugging
$modify = 0;  # if 1 it should try to fix the files as well
$experimental = 0; # try&error if an include is obsolete (slow!!)
@explicitfiles = (); # filled in if passing files on the command line

# statistic variables
$exp_success = 0;
$exp_failure = 0;

while (defined ($ARGV[0]))
{
  $_ = shift;
  if (/^--help$|^-h$/)  {
    print "Usage: fixkdeincludes [--verbose | -v] [--experimental | -e ] [--modify | -m ]\n";
    exit 0;
  }
  elsif (/^--verbose$|^-v$/) {
    $verbose = 1;       # Oh is there a problem...?
  }
  elsif (/^--modify$|^-m$/) {
    $modify = 1;
  }
  elsif (/^--experimental$|^-e$/) {
    $modify = 1;
    $experimental = 1;
  }
  elsif (!/^-/) {
    push @explicitfiles, $_;
  }
}

$cppExt     = "(cpp|cc|cxx|C|c\\+\\+)";
$hExt       = "(h|H|hh|hxx|hpp|h\\+\\+)";

# list of compat headers. scroll down ... much of boring stuff here..
%compatmap = (
 'qapp.h'        =>  "qapplication.h",
 'qarray.h'      =>  "qmemarray.h",
 'qbitarry.h'    =>  "qbitarray.h",
 'qbttngrp.h'    =>  "qbuttongroup.h",
 'qchkbox.h'     =>  "qcheckbox.h",
 'qclipbrd.h'    =>  "qclipboard.h",
 'qcollect.h'    =>  "qptrcollection.h",
 'qcollection.h' =>  "qptrcollection.h",
 'qcombo.h'      =>  "qcombobox.h",
 'qconnect.h'    =>  "qconnection.h",
 'qdatetm.h'     =>  "qdatetime.h",
 'qdrawutl.h'    =>  "qdrawutil.h",
 'qdstream.h'    =>  "qdatastream.h",
 'qfiledef.h'    =>  "private/qfiledefs_p.h",
 'qfiledlg.h'    =>  "qfiledialog.h",
 'qfileinf.h'    =>  "qfileinfo.h",
 'qfontdta.h'    =>  "qfontdata.h",
 'qfontinf.h'    =>  "qfontinfo.h",
 'qfontmet.h'    =>  "qfontmetrics.h",
 'qgrpbox.h'     =>  "qgroupbox.h",
 'qintcach.h'    =>  "qintcache.h",
 'qiodev.h'      =>  "qiodevice.h",
 'qlcdnum.h'     =>  "qlcdnumber.h",
 'qlined.h'      =>  "qlineedit.h",
 'qlist.h'       =>  "qptrlist.h",
 'qmenudta.h'    =>  "qmenudata.h",
 'qmetaobj.h'    =>  "qmetaobject.h",
 'qmlined.h'     =>  "qtmultilineedit.h",
 'qmsgbox.h'     =>  "qmessagebox.h",
 'qmultilinedit.h' =>  "qmultilineedit.h",
 'qobjcoll.h'    =>  "qobjectlist.h>\n\#include <qobjectdict.h",
 'qobjdefs.h'    =>  "qobjectdefs.h",
 'qpaintd.h'     =>  "qpaintdevice.h",
 'qpaintdc.h'    =>  "qpaintdevicedefs.h",
 'qpdevmet.h'    =>  "qpaintdevicemetrics.h",
 'qpmcache.h'    =>  "qpixmapcache.h",
 'qpntarry.h'    =>  "qpointarray.h",
 'qpopmenu.h'    =>  "qpopupmenu.h",
 'qprndlg.h'     =>  "qprintdialog.h",
 'qprogbar.h'    =>  "qprogressbar.h",
 'qprogdlg.h'    =>  "qprogressdialog.h",
 'qpsprn.h'      =>  "private/qpsprinter_p.h",
 'qpushbt.h'     =>  "qpushbutton.h",
 'qqueue.h'      =>  "qptrqueue.h",
 'qradiobt.h'    =>  "qradiobutton.h",
 'qrangect.h'    =>  "qrangecontrol.h",
 'qscrbar.h'     =>  "qscrollbar.h",
 'qsocknot.h'    =>  "qsocketnotifier.h",
 'qstack.h'      =>  "qptrstack.h",
 'qtabdlg.h'     =>  "qtabdialog.h",
 'qtstream.h'    =>  "qtextstream.h",
 'qvector.h'     =>  "qptrvector.h",
 'qwidcoll.h'    =>  "qwidgetlist.h>\n\#include <qwidgetintdict.h",
 'qwindefs.h'    =>  "qwindowdefs.h",

# and now the KDE specific compat includes
 'kapp.h'        =>  "kapplication.h",
 'kstddirs.h'    =>  "kstandarddirs.h",
 'kuniqueapp.h'  =>  "kuniqueapplication.h",
 'ktmainwindow.h'=>  "kmainwindow.h",
 'kcolorbtn.h'   =>  "kcolorbutton.h",
 'kcolordlg.h'   =>  "kcolordialog.h",
 'kxmlgui.h'     =>  "kxmlguifactory.h",
 'kdebugclasses.h' => "kdebug.h",
);


# now it starts to get interesting again

# Look for source files in the given directory ($dir, first parameter)
sub find_fixable_sources ($)
{
  # for now I grep the directory (requires srcdir==builddir)
  # actually it should read the Makefile and
  # find the _SOURCES / _OBJECTS tags that are put there by
  # automake and am_edit, but thats an excercise to the reader ;-)

  my ( $dir ) = @_;

  opendir (DIR, "$dir") || die "Couldn't read '$dir'\n";
  my @sources = grep { /^.*\.$cppExt$/o } readdir(DIR);
  closedir(DIR);
  
  print "found sources: [ " . join(' ', @sources) . " ] in $dir\n" if ($verbose);

  # prefix them with $dir
  my @retsources = ();
  foreach $source(@sources) {
    push @retsources, "$dir/$source";
  }
  
  return @retsources;
}

# Look for header files in the given directory ($dir, first parameter)
sub find_fixable_headers ($)
{
  # for now I grep the directory (requires srcdir==builddir)
  # actually it should read the Makefile and
  # find the _HEADERS tags that are put there by
  # automake and am_edit, but thats an excercise to the reader ;-)

  my ( $dir ) = @_;

  opendir (DIR, "$dir") || die "Couldn't read '$dir'\n";
  my @headers = grep { /^.*\.$hExt$/o } readdir(DIR);
  closedir(DIR);
  
  print "found headers: [ " . join(' ', @headers) . " ] in $dir\n" if ($verbose);

  # prefix them with $dir
  my @retheaders = ();
  foreach $source(@headers) {
    push @retheaders, "$dir/$source";
  }
  
  return @retheaders;
}

sub find_removable_includes ($)
{
  my $srcfile = shift @_;
  open(SRC, "< $srcfile") || die "find_removable_includes: couldn't open '$srcfile'\n";
  
  my @includes = ();

  # we skip all includes that are somehow ifdefed

  my $cpplevel = 0;
  $cpplevel = -1 if ($srcfile=~m/^.*\.$hExt$/); # plan for header-protection #ifndef/#define/#endif
  while (<SRC>) {
    if ($_ =~ m/^\#if/) {
      $cpplevel = $cpplevel + 1;
      next;
    }
    if ($_ =~ m/^\#endif/) {
      $cpplevel = $cpplevel - 1;
      next;
    }
    #if ($cpplevel == 0 && $_ =~ m/^\#include\s*[\"<]([qk]\S*\.h)[\">]\S*/) {
    if ($cpplevel == 0 && (($_ =~ m/^\#include\s*\"(\S+\.h)\"\S*/) || ($_ =~ m/^\#include\s*\<([qk]\S+.h)\>\S*/)))  {
      push @includes, $1;
      next;
    }
  }
  close SRC;

  print "No fixable includes found in $srcfile\n" if ($verbose and not @includes);
  print "found includes: [ " . join(' ', @includes) . " ]\n" if ($verbose and @includes);

  return @includes;
}

sub find_installed_headers($)
{
  my $sdir = shift @_;
  my @includes = ();

  open(I, "<$sdir/Makefile.am") || die "couldn't open $sdir/Makefile.am $!";

  my $data = join('', <I>);
  $data =~ s/\\\s*\n/ /g;

  # now search for not installed headers
  foreach my $line (split /^/, $data) {
    if($line =~ /^(\w+)_HEADERS\s*=(.*)$/) {
      next if $1 eq "noinst";
      push @includes, split (' ', $2);
    }
  }
  close(I);
  return @includes;
}

# first parameter: srcfile
# second parameter: include to remove
# third parameter is the duplicate level: this include is removed $level times
sub remove_include ($$$)
{
  my $srcfile = shift @_;
  my $include = quotemeta(shift @_);
  my $level = shift @_;

  die "$srcfile is not read/writeable!\n" if( ! -r $srcfile || ! -w $srcfile);
  open(I, "< $srcfile") or die "remove_include: couldn't open '$srcfile'\n";
  my @contents = <I>;
  close(I);

  # ok, CPU time doesn't count so we do it the lazy way
  # we should remove the last occurence of the include in the
  # file because in case its a duplicate removing the first
  # one could make a difference. 
  my @revcontents = reverse @contents;
  @contents = ();

  # we skip all inludes that are somehow ifdefed
  # note the logic is reversed because it operates
  # on reversed lines :)
  my $cpplevel = 0;
  $cpplevel = -1 if ($srcfile=~m/^.*\.$hExt$/); # plan for header-protection #ifndef/#define/#endif
  foreach $line (@revcontents) {
    if ($line =~ m/^\#if/) {
      $cpplevel = $cpplevel - 1;
      push @contents, $line;
      next;
    }

    if ($line =~ m/^\#endif/) {
      $cpplevel = $cpplevel + 1;
      push @contents, $line;
      next;
    }

    if ($level && $cpplevel == 0 && 
       (($line =~ m/^\#include\s*\"$include\"\S*/) || ($line =~ m/^\#include\s*\<$include\>\S*/)))  {
      $level = $level - 1;
      # skipping the line..
      next;
    }

    push @contents, $line;
  }

  # now we have the fixed contents in @contents, although in wrong order
  open(O, "> $srcfile") || die "remove_include: couldn't open '$srcfile' for writing\n";
  print O reverse @contents;
  close (O);
}

# first parameter: srcfile
# second parameter: include to replace
# third parameter the include file to replace it with
sub replace_include ($$$)
{
  my $srcfile = shift @_;
  my $include = quotemeta(shift @_);
  my $destinclude = shift @_;

  die "$srcfile is not read/writeable!\n" if( ! -r $srcfile || ! -w $srcfile);
  open(I, "< $srcfile") or die "replace_include: couldn't open '$srcfile'\n";
  my @contents = <I>;
  close(I);

  # ok, CPU time doesn't count so we do it the lazy way
  my @revcontents = reverse @contents;
  @contents = ();

  # we skip all inludes that are somehow ifdefed
  # note the logic is reversed because it operates
  # on reversed lines :)
  my $cpplevel = 0;
  $cpplevel = -1 if ($srcfile=~m/^.*\.$hExt$/); # plan for header-protection #ifndef/#define/#endif
  foreach $line (@revcontents) {
    if ($line =~ m/^\#if/) {
      $cpplevel = $cpplevel - 1;
      push @contents, $line;
      next;
    }

    if ($line =~ m/^\#endif/) {
      $cpplevel = $cpplevel + 1;
      push @contents, $line;
      next;
    }

    if ($cpplevel == 0 &&
      (($line =~ m/^\#include\s*\"$include\"\S*/) || ($line =~ m/^\#include\s*\<$include\>\S*/)))
    {
      print "HAH! found $include to replace in $srcfile!\n" if($verbose);
      $line =~ s/(\#include\s*[\"<])$include([\">]\S*)/$1$destinclude$2/;
    }

    push @contents, $line;
  }

  # now we have the fixed contents in @contents
  open(O, "> $srcfile") || die "replace_include: couldn't open '$srcfile' for writing\n";
  print O reverse @contents;
  close (O);
}

# fixes #include <foo.h> -> #include "foo.h"
sub replace_include_type ($$)
{
  my ($srcfile, $include) = @_;

  die "$srcfile is not read/writeable!\n" if( ! -r $srcfile || ! -w $srcfile);
  open(I, "< $srcfile") or die "replace_include: couldn't open '$srcfile'\n";
  my @contents = <I>;
  close(I);

  grep(s/^(\#include)\s*<$include>(.*)$/$1 \"$include\"$2/, @contents);

  # now we have the fixed contents in @contents
  open(O, "> $srcfile") || die "replace_include: couldn't open '$srcfile' for writing\n";
  print O @contents;
  close (O);
}

sub fix_duplicates($)
{
  my $srcfile = shift @_;

  my @includes = &find_removable_includes($srcfile);

  my %inclMap = ();

  # initialize
  foreach $include (@includes) {
    $inclMap{$include} = 0;
  }
  
  # count number of occurences
  foreach $include (@includes) {
    $inclMap{$include} = $inclMap{$include} + 1;
  }
  
  # check for duplicates
  foreach $include (keys %inclMap) {
    next if $inclMap{$include} <= 1;
    
    print "$srcfile: duplicate level ". $inclMap{$include} .": ". $include ."\n";

    &remove_include($srcfile, $include, $inclMap{$include} - 1) if($modify);
  }
}

sub extract_gcc_error($)
{
  my $out = shift;
 
 # print "out: $out\n";

  while ($out =~ m/^(.*?):([0-9]+):(.*)$/mg) # filename:lineno:message
  {
      my $field1 = $1 || "";
      my $field2 = $2 || "";
      my $field3 = $3 || "";

 #     print "f1: $field1, f2: $field2, f3: $field3\n";

      next if ($field3 =~ m/\s+warning:.*/);
      next if ($field3 =~ m/^\s*$/);
      return basename($field1);
  }
  return "BUG!";
}

sub fix_compat_includes($)
{
  my $srcfile = shift @_;

  my @includes = &find_removable_includes($srcfile);

  my %inclMap = ();

  # initialize
  foreach $include (@includes) {
    $inclMap{$include} = 0;
  }
  
  # count number of occurences
  foreach $include (@includes) {
    $inclMap{$include} = $inclMap{$include} + 1;
  }
  
  # check for compat headers
  foreach $include (keys %inclMap) {
    if( defined $compatmap{$include}) {
      print "$srcfile: compat header: $include, to be replaced by ". $compatmap{$include} ."\n";
      &replace_include($srcfile, $include, $compatmap{$include}) if($modify);
    }
  }
}

sub fix_include_type($)
{
  my $srcfile = shift @_;
  my $srcdir = dirname($srcfile);

  open(I, "<$srcfile") || die "couldn't open $srcfile in _fix_include_type";
  my @bracketincs = grep s/^\s*\#include\s*<([^>]+)>\s*$/$1/, <I>;
  close(I);

  foreach my $include (@bracketincs) {
    next if (!(-r "$srcdir/$include"));
    next if (grep (/^$include$/, @instheaders));
    next if ($include eq "config.h"); # oh don't get me started on that

    print "$srcfile: #include <$include> should use #include \"...\"\n";
    &replace_include_type($srcfile, $include) if($modify);
  }
}

# copies a file from src to dest, overwrites destination if exists
sub copy_file($$)
{
  my $src = shift(@_);
  my $dst = shift(@_);

  open(I, "< $src") or die "copy_file: can't open $src for input\n";
  my @fcontents = <I>;
  close(I);
  open(O, "> $dst") or die "copy_file: can't open $dst for output\n";
  print O @fcontents;
  close(O);
}

# interrupt handler for fix_unnecessary
sub sighandler_fix_unnecessary()
{
  my($sig) = @_;
  print "Caught a SIG$sig--shutting down after restoring $srcfile\n";
  chdir($srcdir);
  unlink $srcfile || warn "couldn't unlink $srcfile";
  rename $localbackup, $srcfile || warn "couldn't rename $localbackup to $srcfile";
  exit(1);
}

sub fix_unnecessary($)
{
  local $srcfile = shift @_;
  local $srcdir = dirname($srcfile);

  # find canonical path for srcdir
  my $origdir = cwd;
  chdir($srcdir);
  $srcdir = cwd;
  print "srcdir=$srcdir\n" if($verbose);

  my $builddir = $srcdir;
  my $makecmd = "make";
  if (defined $ENV{"OBJ_REPLACEMENT"})
  {
    # we have to use sed here, because perl can't do s#a#b#
    $builddir = `echo $srcdir | sed -e "\$OBJ_REPLACEMENT"`;
    chomp $builddir;
    $makecmd = "makeobj";
  }
  print "builddir=$builddir\n" if($verbose);

  my $tot = $exp_success + $exp_failure;
  print "=============== $srcfile (successes: $exp_success; total: $tot)\n";

  $srcfile = basename($srcfile);

  # first figure out some details
  my @includes = &find_removable_includes($srcfile);

  my $blanksrc = $srcfile;
  $blanksrc =~ s/(.*)\.[^\.]+/$1/;

  print "Checking for initial compilation: ";
  chdir($builddir);
  my $objextension = "BUG";
  if($srcfile =~ /\.$hExt$/o) {
    $output = `$makecmd all 2>&1`;
    $objextension = "all" if ( 0 == ($? >> 8));
  }
  else {
    unlink "$blanksrc.lo";
    my $output = `$makecmd $blanksrc.lo 2>&1`;
    $objextension = ".lo" if ( 0 == ($? >> 8));
    if($objextension eq "BUG") {
      print "failed with .lo... ";
      unlink "$blanksrc.o";
      $output = `$makecmd $blanksrc.o 2>&1`;
      $objextension = ".o" if ( 0 == ($? >> 8));
    }
    if($srcfile =~ /$hExt/) {
      $output = `$makecmd $blanksrc.o 2>&1`;
      $objextension = ".o" if ( 0 == ($? >> 8));
    }
  }
  if($objextension eq "BUG") {
    warn "can't figure out right compile command for $srcfile :-(\n" .
         "??? unused, or didn't compile in the first place?\n" .
         "$output";
    chdir($origdir);
    exit 1;
  }

  print "worked with $objextension\n";

  # now try to drop some includes 
  foreach $include (@includes) {
    # kdatastream is special because
    # it will break the application if removed even
    # if it continues to compile
    next if( $include eq "kdatastream.h");
    # I also like to have kdebug.h still in
    # so that it's easy to add kdDebug calls
    next if( $include eq "kdebug.h");
    # avoid this one as it might cause
    # certain code parts to be disabled from compilation
    next if( $include eq "qmodules.h");
    # don't remove this one either. causes conditional
    # code to be compiled incorrectly
    next if( $include eq "tdeversion.h");
    # don't remove the config.h include
    # conditional code may depend on this file
    next if( $include eq "config.h");
    # check if it is its own header file
    my $blankhdr = $include;
    $blankhdr =~ s/(.*)\.[^\.]+/$1/;
    next if ($blankhdr eq $blanksrc);	

    chdir($srcdir);

    local $localbackup = $srcfile . "#fixkdeincludes";

    # preserve timestamp if possible for CVS
    unlink $localbackup;
    rename $srcfile, $localbackup;
    copy_file($localbackup, $srcfile);

    # revert to backup in case of interrupt (Ctrl+C)
    $SIG{'INT'} = \&sighandler_fix_unnecessary;

    # check if it still compiles
    if($verbose) {
      chdir($builddir);
      # testing headers? need to compile everything
      if($objextension eq "all") {
        # wait a second for makefile timestamp comparisons
        sleep 1;
        `$makecmd all 2>&1`;
      }
      else {
        unlink "$blanksrc$objextension";
        `$makecmd $blanksrc$objextension 2>&1`;
      }
      die "unexpected error $output\nexitcode=" . ($? >> 8) if($? >> 8);
      chdir($srcdir);
    }

    # duplicates have to be nuked here , so it will be dropped maximum once
    print "trying without $include: ";
    &remove_include($srcfile, $include, 1);

    chdir($builddir);

    # try if it compiles
    if($objextension eq "all") {
      sleep 1;
      $output=`$makecmd $objextension 2>&1`;
    }
    else {
      unlink "$builddir/$blanksrc$objextension";
      $output=`$makecmd $blanksrc$objextension 2>&1`;
    }
    my $retcode = ($? >> 8);
    #print "retcode=$retcode\n$output" if ($verbose);

    chdir($srcdir);
    if($retcode == 0) {
      # wow, it worked, lets continue!
      print "SUCCESS!\n";
      $SIG{'INT'} = 'DEFAULT';
      unlink $localbackup;
      $exp_success = $exp_success + 1;
    }
    else {
      # is this a fixable error?
      if($objextension eq "all" and 
         &extract_gcc_error($output) ne $srcfile) {
        print "failed (error in " . &extract_gcc_error($output) . ")\n";
        # FIXME: implement fixup of the compilation error
        # so that we can be much more agressive in removing
        # unneeded includes from headers
      }
      else
      {
        # better luck next time
        print "FATALLY failed\n";
      }
      unlink $srcfile;
      rename $localbackup, $srcfile;
      $SIG{'INT'} = 'DEFAULT';

      $exp_failure = $exp_failure + 1;
    }
  }

  print "\n";

  chdir($origdir);
}

sub process_source_file($)
{
  local $file = shift @_;
  my $pure = basename($file);
  print "Checking: $file\n" if($verbose);
  &fix_include_type($file);
  &fix_compat_includes($file);
  &fix_duplicates($file);
  &fix_unnecessary($file) if ($experimental && !grep (/^$pure$/, @instheaders));
  print "\n" if ($verbose);
}

sub check_for_automake_srcdir($)
{
  my $dir = shift;

  return 0 if !( -r "$dir/Makefile.am");
  return 1 if !( -r "$dir/Makefile");

  # ok, now its either srcdir with srcdir==builddir, or its builddir,
  # which we don't want. 

  open(I, "<$dir/Makefile") || die "couldn't read $dir/Makefile";
  while(<I>) {
    if(/^srcdir\s*=\s*(\S+)/) {
      close(I);

      if($1 ne ".") {
        print "Skipping build dir: $dir\n" if($verbose);
        return 0;
      }
      return 1;
    }
  }
  
  close(I);
  # ok, this makefile isn't generated by automake, we don't want that
  return 0;
}

#############################################################################
# here is the main logic
#

# warn about modified files
if($modify) {
  `cvscheck | grep '^[MmC]'`;
  print "WARNING: you have pending local changes. You might commit them by accident!\n\n" if($? >> 8 == 0);
}

if($experimental) {
  print "WARNING: The experimental mode is indeed experimental.\n";
  print "It tries to reduce includes by testing if it would compile\n";
  print "without a particular include. It might introduce subtle bugs\n";
  print "or break compilation for make check or make final.\n\n";
  print "This operation mode is known to be unsafe. You've been warned.\n";
}

# process files from the command line, if any
if ( $#explicitfiles >= 0 ) {
  foreach $file( @explicitfiles ) {
    &process_source_file( $file );
  }
  exit 0;
}

# first generate a list of subdirectories
@dirlist = ();
push @dirlist, "." if (&check_for_automake_srcdir("."));
die "current directory isn't srcdir!" if (!scalar @dirlist);
foreach $dir ( @dirlist ) {
 opendir (DIR, "$dir") || warn "Couldn't read '$dir'";
 my $subdir = "";
 while( $subdir = readdir(DIR)) {
   next if ($subdir =~ /^\./);
   next if !( -d "$dir/$subdir");
   next if (! &check_for_automake_srcdir("$dir/$subdir"));

   push @dirlist, "$dir/$subdir";
 }
 closedir(DIR);
}

# now iterate over all subdirs
foreach $dir(@dirlist) {

  # check if this directory wants not to be fixed
  if(open(M, "$dir/Makefile.am")) {
    my @mcontents = grep /(\-UQT_NO_COMPAT|\-UKDE_NO_COMPAT)/, <M>;
    close(M);
    if ( @mcontents ) {
      print "Skipping directory: $dir\n";
      next;
    }
  }

  @headers = &find_fixable_headers($dir);
  @instheaders = &find_installed_headers($dir);
  foreach $file(@headers) {
    &process_source_file($file);
  }
  @sources = &find_fixable_sources($dir);
  foreach $file(@sources) {
    &process_source_file($file);
  }
}
