#!/usr/bin/perl -w
#< find-incs.pl - given an input file, find the #include <"..."> files, and what they include
# 20/05/2012 - Further enhancements
# 2011-10-29 - Make adding system includes an option, and search local found includes, for includes
use strict;
use warnings;
use File::Basename;  # split path ($name,$dir,$ext) = fileparse($file [, qr/\.[^.]*/] )
use Cwd;
use File::stat;
my $os = $^O;
my $perl_dir = '/home/geoff/bin';
my $PATH_SEP = '/';
my $temp_dir = '/tmp';
if ($os =~ /win/i) {
    $perl_dir = 'C:\GTools\perl';
    $temp_dir = $perl_dir;
    $PATH_SEP = "\\";
}
unshift(@INC, $perl_dir);
require 'lib_utils.pl' or die "Unable to load 'lib_utils.pl' Check paths in \@INC...\n";
# log file stuff
our ($LF);
my $pgmname = $0;
if ($pgmname =~ /(\\|\/)/) {
    my @tmpsp = split(/(\\|\/)/,$pgmname);
    $pgmname = $tmpsp[-1];
}
my $outfile = $temp_dir.$PATH_SEP."temp.$pgmname.txt";
open_log($outfile);

# user variables
my $VERS = "0.0.2 2012-05-20";
# my $VERS = "0.0.1 2011-06-13";
my @in_files = ();

my @warnings = ();
my $load_log = 0;
my $verbosity = 0;

my $total_dirs = 0;
my $total_files = 0;
my $total_lines = 0;
my $total_bytes = 0;

# OPTIONS
my $search_system = 0;  # search for <system> include
my $search_local = 1;   # local includes are also searched


my @std_include_dirs = qw( /usr/include/ /usr/include/linux/ usr/local/include/ /usr/include/c++/4.4/ );
my @usr_include_dirs = ();
my %shtdirs = ();
# debug
my $dbg01 = 0; # show each directory processed...
my $dbg02 = 0; # show sub-directory processed...

my %done = ();
my %missed = ();
my @not_found = ();

sub process_dir($$$);
sub process_file($$);

if ($os =~ /WIN/i) {
    @std_include_dirs = ();
    push(@std_include_dirs,"C:\\Program Files\\Microsoft Visual Studio 8\\VC\\include\\");
    push(@std_include_dirs,"C:\\Program Files\\Microsoft Platform SDK for Windows Server 2003 R2\\Include\\");
    %shtdirs = (
        "C:\\Program Files\\Microsoft Visual Studio 8\\VC\\include\\" => "MSVC\\include\\",
        "C:\\Program Files\\Microsoft Platform SDK for Windows Server 2003 R2\\Include\\" => "PSDK\\Include\\" );
}

sub VERB1() { return $verbosity >= 1; }
sub VERB2() { return $verbosity >= 2; }
sub VERB5() { return $verbosity >= 5; }
sub VERB9() { return $verbosity >= 9; }

sub prtw($) {
   my ($tx) = shift;
   $tx =~ s/\n$//;
   prt("$tx\n");
   push(@warnings,$tx);
}

sub show_warnings() {
   if (@warnings) {
      prt( "\nGot ".scalar @warnings." WARNINGS...\n" );
      foreach my $itm (@warnings) {
         prt("$itm\n");
      }
      prt("\n");
   } else {
      #prt( "\nNo warnings issued.\n\n" );
   }
}

sub pgm_exit($$) {
   my ($val,$msg) = @_;
   show_warnings();
   prt($msg) if (length($msg));
   close_log($outfile,$load_log);
   exit($val);
}


sub process_dir($$$) {
    my ($dir,$ra,$lev) = @_;
    my @dirs = ();
    my ($file,$ff,$cnt);
    if (opendir( DIR, $dir )) {
        $total_dirs++;
        prt("Reading [$dir]...\n");
        my @files = readdir(DIR);
        closedir(DIR);
        $dir .= '/' if !($dir =~ /\/$/);
        foreach $file (@files) {
            next if ($file eq '.');
            next if ($file eq '..');
            $ff = $dir.$file;
            if ( -d $ff ) {
                push(@dirs,$ff);
            } elsif ( -f $ff ) {
                push(@{$ra},$ff);
                $total_files++;
            } else {
                # a link or ....
            }
        }
    } else {
        prtw("WARNING: Unable to open folder [$dir]... $!...\n");
    }
    if (@dirs) {
        my ($cnt,$itm);
        $cnt = scalar @dirs;
        prt( "[$dbg02] $lev: Found $cnt subs in [$dir]...\n" ) if ($dbg02);
        foreach $itm (@dirs) {
            process_dir($itm,$ra,($lev + 1));
        }
    }
    if ($lev == 0) {
        $cnt = scalar @{$ra};
        prt("Found $cnt file items, in scan of [$dir]\n");
    }
    return $ra;
}

sub process_file($$) {
    my ($in,$par) = @_;
    my ($i,$line,$tline,$lnn,$inc,$ff,$ok,$sdir,$shd,$sff);
    my @the_incs = ();
    if (open FIL, "<$in") {
        my @lines = <FIL>;
        close FIL;
        my $cnt = scalar @lines;
        $in =~ s/^\.(\\|\/)//;
        $lnn = sprintf("%5d", $cnt);
        my ($name,$dir) = fileparse($in);
        $sff = $in;
        if (defined $shtdirs{$dir}) {
            $sff = $shtdirs{$dir}.$name;
        }
        $sff .= ' ' while (length($sff) < 8+1+3);
        prt("Got $lnn lines, from [$sff] to process...\n");
        $dir .= $PATH_SEP if !($dir =~ /(\/|\\)$/);
        $total_files++;
        $par .= ">" if (length($par));
        $par .= $name;
        for ($i = 0; $i < $cnt; $i++) {
            $total_lines++;
            $lnn = sprintf("%5d",($i+1));
            $line = $lines[$i]; # extract file line
            $total_bytes += length($line);
            chomp $line;
            $tline = trim_all($line);
            if ($tline =~ /^\#\s*include\s+(.+)$/) {
                $inc = $1;
                $inc =~ s/\/\/.*$//;
                $inc =~ s/\/\*.*$//;
                $inc = trim_all($inc);
                $inc =~ s/^<//;
                $inc =~ s/>$//;
                $inc =~ s/^"//;
                $inc =~ s/"$//;
                $ff = $dir.$inc;
                $ok = 'NF';
                if (-f $ff) {
                    $ok = 'ok - done';
                    if (!defined $done{$ff}) {
                        push(@the_incs,[$ff,$par]);
                        $done{$ff} = 1;
                        $ok = "ok [$ff] $par";
                    }
                } elsif ($search_system) {
                    foreach $sdir (@std_include_dirs) {
                        $shd = $sdir;
                        if (defined $shtdirs{$sdir}) {
                            $shd = $shtdirs{$sdir};
                        }
                        $ff = $sdir.$inc;
                        $sff = $shd.$inc;
                        if (-f $ff) {
                            $ok = 'ok- done';
                            if (!defined $done{$ff}) {
                                push(@the_incs,[$ff,$par]);
                                $done{$ff} = 1;
                                $ok = "ok [$sff] $par";
                            }
                            last;
                        }
                    }
                    if ($ok eq 'NF') {
                        foreach $sdir (@usr_include_dirs) {
                            $ff = $sdir.$inc;
                            if (-f $ff) {
                                $ok = 'ok- done';
                                if (!defined $done{$ff}) {
                                    push(@the_incs,[$ff,$par]);
                                    $done{$ff} = 1;
                                    $ok = "ok [$ff] $par";
                                }
                                last;
                            }
                        }
                    }
                }
                if ($ok eq 'NF') {
                    if (!defined $missed{$inc}) {
                        push(@not_found, [$inc, $dir, $in]);
                        $missed{$inc} = 1;
                        prt("$lnn: NOT FOUND [$inc] $ok\n");
                    }
                } else {
                    prt("$lnn: INCLUDE [$inc] $ok\n");
                }
            }
        }
        $cnt = scalar @the_incs;
        for ($i = 0; $i < $cnt; $i++) {
            $inc = $the_incs[$i][0];
            $par = $the_incs[$i][1];
            process_file($inc,$par);
        }
    } else {
        prtw("WARNING: Unable to open file [$in]!\n");
    }
}

sub mycmp_decend {
   return -1 if ( ${$a}[0] > ${$b}[0] );
   return  1 if ( ${$a}[0] < ${$b}[0] );
   return 0;
}

sub process_input() {
    my ($in);
    my @files = ();
    foreach $in (@in_files) {
        if (-f $in) {
            process_file($in,"");
        } elsif (-d $in) {
            process_dir($in,\@files,0);
        } else {
            pgm_exit(1,"ERROR: Input [$in] is NOT file or directory!\n");
        }
    }
    foreach $in (@files) {
        process_file($in,"");
    }
}

sub show_done() {
    my ($key);
    foreach $key (sort keys %done) {
        prt("$key\n");
    }
}

sub show_not_found() {
    my $cnt = scalar @not_found;
    my ($i,$inc,$dir,$in,$len,$min);
    prt("NOT FOUND: $cnt include files...\n");
    $min = 0;
    for ($i = 0; $i < $cnt; $i++) {
        $inc = $not_found[$i][0];
        $dir = $not_found[$i][1];
        $in  = $not_found[$i][2];
        $len = length($inc);
        $min = $len if ($len > $min);
    }
    for ($i = 0; $i < $cnt; $i++) {
        $inc = $not_found[$i][0];
        $dir = $not_found[$i][1];
        $in  = $not_found[$i][2];
        
        $inc .= ' ' while (length($inc) < $min);
        prt("$inc, in $in\n");
    }

}

#########################################
###### MAIN ######
process_args(@ARGV);
process_input();
prt("$total_dirs dirs, $total_files files, $total_lines lines, $total_bytes bytes.\n");
show_done();
prt("$total_dirs dirs, $total_files files, $total_lines lines, $total_bytes bytes.\n");
show_not_found();
pgm_exit(0,"Normal exit\n");

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

sub need_arg {
    my ($arg,@av) = @_;
    pgm_exit(1,"ERROR: Need argument following [$arg]!\n") if (!@av);
}


sub process_args {
    my (@av) = @_;
    my ($arg,$sarg);
    while (@av) {
        $arg = $av[0];
        if ($arg =~ /^-/) {
            $sarg = substr($arg,1);
            $sarg = substr($sarg,1) while ($sarg =~ /^-/);
            if (($sarg =~ /^h/) || ($sarg eq '?')) {
                give_help();
                pgm_exit(0,"Help exit 0");
            } elsif ($sarg =~ /^i/) {
                need_arg(@av);
                shift @av;
                $sarg = $av[0];
                if (-d $sarg) {
                    $sarg .= '/' if ( !($sarg =~ /\/$/) );
                    prt("adding [$sarg] folder to check includes.\n");
                    push(@usr_include_dirs,$sarg);
                } else {
                    pgm_exit(1,"ERROR: Can NOT locate folder [$sarg]!\n");
                }
            } elsif ($sarg =~ /^l/) {
                $load_log = 1;
                prt("Set to load log at end.\n");
            } elsif ($sarg =~ /^v/) {
                if ($sarg =~ /^v.*(\d+)$/) {
                    $verbosity = $1;
                } else {
                    while ($sarg =~ /^v/) {
                        $verbosity++;
                        $sarg = substr($sarg,1);
                    }
                }
                prt("Verbosity = $verbosity\n") if (VERB1());
            } else {
                pgm_exit(1,"ERROR: Unknonw option [$arg]! Try -?\n");
            }
        } else {
            push(@in_files,$arg);
            prt("Added input [$arg]\n");
        }
        shift @av;
    }
    
    if ( ! @in_files ) {
        pgm_exit(1,"ERROR: No input found in command!\n");
    }
}

sub give_help {
    prt("$pgmname version $VERS\n");
    prt("Usage: [options] input\n");
    prt("Options:\n");
    prt(" --help      (-h,-?) = This help and exit.\n");
    prt(" --load         (-l) = Load log at end.\n");
    prt(" --inc <dir>    (-i) = Add this as an include directory.\n");
    prt(" --verb[n]     (-v) = Bump [or set] verbosity. def=$verbosity\n");
}

# eof - find-incs.pl
