#!/usr/bin/perl -w
#< cmakeopts.pl - Given a directory, search for CMakeLists.txt files, recursively, 
# and show each option(NAME "test" ON) item
# 19/05/2013 - Add -o for output file, and put the ON/OFF first
# 20/07/2012 - If no file given, but there is a CMakeLists.txt, then use that
# 01/04/2012 - If given a file, also search for and follow 'add_subdirectory(...)'
# 2012-01-29 - Convert to also run in Windows
# 2012-01-12 - Change to lib_utils.pl, and add a 'purpose' statement.
use strict;
use warnings;
use File::Basename; # split path ($name,$dir) = fileparse($ff); or ($nm,$dir,$ext) = fileparse($fil, qr/\.[^.]*/);
use Cwd;
use File::stat;
my $os = $^O;
my $perl_dir = '/home/geoff/bin';
my $PATH_SEP = '/';
my $temp_dir = '/tmp';
my $is_os_win = ($os =~ /win/i) ? 1 : 0;
if ($is_os_win) {
    $perl_dir = 'C:\GTools\perl';
    $temp_dir = $perl_dir;
    $PATH_SEP = "\\";
}
unshift(@INC, $perl_dir);
require "lib_utils.pl" or die "ERROR: Unable to load 'lib_utils.pl";
# 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);

my $pgm_vers = "0.0.3 2012-04-01";
# my $pgm_vers = "0.0.2 2012-01-29";
# $pgm_vers = "0.0.1 2011-12-15";
my @in_files = ();

my @warnings = ();
my $load_log = 0;
my $verbosity = 0;
my $out_file = '';
my $final_msg = '';

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

my %options_found = ();

# debug
my $dbg01 = 0; # show each directory processed...
my $dbg02 = 0; # show sub-directory processed...
my $debug_on = 0;
my $def_file = 'C:\FG\30\flightgear';

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

sub process_dir($$$);

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

sub show_warnings() {
   if (@warnings) {
      prt( "Got ".scalar @warnings." WARNINGS..." );
      if (!VERB5()) {
          prt(" Use -v5 to list...");
      }
      prt("\n");
      if (VERB5()) {
          foreach my $itm (@warnings) {
             prt("$itm\n");
          }
          prt("\n");
      }
   } else {
      #prt( "\nNo warnings issued.\n\n" );
   }
}

sub pgm_exit($$) {
   my ($val,$msg) = @_;
   show_warnings();
   if (length($msg) > 1) {
        $msg .= "\n" if (!($msg =~ /\n$/));
        prt($msg);
   
   }
   prt("$final_msg\n") if (length($final_msg));
   close_log($outfile,$load_log);
   exit($val);
}


sub my_file_type($) {
    my ($file) = shift;
    return 1 if ($file eq 'CMakeLists.txt');
    return 0;
}

sub process_dir($$$) {
    my ($dir,$ra,$lev) = @_;
    my @dirs = ();
    my ($file,$ff,$cnt);
    my ($itm);
    if (opendir( DIR, $dir )) {
        $total_dirs++;
        prt("Reading [$dir]...\n") if (VERB9());
        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 ) {
                $total_files++;
                if (my_file_type($file)) {
                    push(@{$ra},$ff);
                }
            } else {
                # a link or ....
            }
        }
    } else {
        prtw("WARNING: Unable to open folder [$dir]... $!...\n");
    }
    if (@dirs) {
        $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 of type CMakeLists.txt, in scan of [$dir]\n");
        if (VERB9()) {
            $cnt = 0;
            foreach $itm (@{$ra}) {
                $cnt++;
                prt("$cnt $itm\n");
            }
        }
    }
    return $ra;
}

sub full_opts_line($) {
    my ($line) = shift;
    my $len = length($line);
    my $inquot = 0;
    my $inbrac = 0;
    my ($i,$ch,$qc);
    for ($i = 0; $i < $len; $i++) {
        $ch = substr($line,$i,1);
        if ($inbrac) {
            if ($inquot) {
                $inquot = 0 if ($ch eq $qc);
            } elsif ($ch eq '"') {
                $inquot = 1;
                $qc = $ch;
            } elsif ($ch eq ')') {
                $inbrac--;
                if ($inbrac == 0) {
                    return 1;
                }
            }
        } else {
            if ($inquot) {
                $inquot = 0 if ($ch eq $qc);
            } elsif ($ch eq '"') {
                $inquot = 1;
                $qc = $ch;
            } elsif ($ch eq '(') {
                $inbrac++;
            }
        }
    }
    return 0;
}

sub full_fe_line($) {
    my $line = shift;
    return full_opts_line($line);
}

sub split_opts_line($) {
    my ($line) = shift;
    my $len = length($line);
    my $inquot = 0;
    my $inbrac = 0;
    my ($i,$ch,$qc,$command,$hadsp);
    my @arr = ();   # got nothing so far
    $command = '';  # start the collection
    $hadsp = 0;     # had no space yet
    for ($i = 0; $i < $len; $i++) {
        $ch = substr($line,$i,1);
        if ($ch eq '(') {
            last;   # found first '('
        } elsif ( !($ch =~ /\s/) ) {
            $command .= $ch;
        } else {    # have a SPACE
            if (length($command)) {
                # already had some non-space - trailing space
                # should check if the next sig char is the '('
            }
            # else have not yet started command,
            # so ignore this beginning space
        }
    }
    if (($ch ne '(')||(length($command)==0)) {
        prtw("WARNING: Option line did not conform! [$line]\n");
        return \@arr;
    }
    push(@arr,$command);    # push first item 'OPTION' or 'option'
    $command = '';
    # collect space spearated items, skipping spaces in quoted strings
    for (; $i < $len; $i++) {
        $ch = substr($line,$i,1);
        if ($inbrac) {
            if ($inquot) {
                $command .= $ch;
                $inquot = 0 if ($ch eq $qc);
            } else {
                if ($ch =~ /\s/) {
                    push(@arr,$command) if (length($command));
                    $command = '';
                } else {    # not a space
                    if ($ch eq ')') {
                        $inbrac--;
                        if ($inbrac == 0) {
                            last;
                        }
                    } else { # not end bracket
                        $command .= $ch;
                        if ($ch eq '"') {
                            $inquot = 1;
                            $qc = $ch;
                        }
                    }
                }
            }
        } else {
            if ($inquot) {
                $inquot = 0 if ($ch eq $qc);
            } elsif ($ch eq '"') {
                $inquot = 1;
                $qc = $ch;
            } elsif ($ch eq '(') {
                $inbrac++;
            }
        }
    }
    push(@arr,$command) if (length($command));
    return \@arr;
}

sub show_cmake_options($) {
    my $rha = shift;
    my ($in,$roptions);
    my ($i,$ra,$len,$option,$message,$default,$cnt,$msg);
    my $mino = 0;
    my $minm = 0;
    my $mind = 0;
    my $dnhead = 0;
    $msg = '';
    my $out = '';
    foreach $in (keys %{$rha}) {
        $roptions = ${$rha}{$in};
        $cnt = scalar @{$roptions};
        $total_options += $cnt;
        $mino = 0;
        $minm = 0;
        if ($cnt) {
            prt("$cnt OPTIONS found in [$in]\n");
            $out .= "$cnt OPTIONS found in [$in]\n";
            # get LENGTH
            for ($i = 0; $i < $cnt; $i++) {
                #                 0        1         2
                # push(@options, [$option, $message, $default]);
                $option = ${$roptions}[$i][0];
                $message = ${$roptions}[$i][1];
                $default = ${$roptions}[$i][2];
                $len = length($option);
                $mino = $len if ($len > $mino);
                $len = length($message);
                $minm = $len if ($len > $minm);
                $len = length($default);
                $mind = $len if ($len > $mind);
            }
            if (!$dnhead) {
                $option = "Option";
                $default = "DEF";
                $message = 'Description';
                $option .= ' ' while (length($option) < $mino);
                $default .= ' ' while (length($default) < $mind);
                prt("$option $default $message\n");
                $out .= "$option $default $message\n";
                $dnhead = 1;
            }
            for ($i = 0; $i < $cnt; $i++) {
                #                 0        1         2
                # push(@options, [$option, $message, $default]);
                $option = ${$roptions}[$i][0];
                $message = ${$roptions}[$i][1];
                $default = ${$roptions}[$i][2];
                $option .= ' ' while (length($option) < $mino);
                $default .= ' ' while (length($default) < $mind);
                ###$message .= ' ' while (length($message) < $minm);
                prt("$option $default $message\n");
                $out .= "$option $default $message\n";
            }
        } else {
            $msg .= "Found NO options in [$in]\n"; # if (VERB9());
        }
    }
    prt($msg) if (length($msg) && VERB2());
    if (length($out_file)) {
        rename_2_old_bak($out_file); # NEVER overwrite any existing file!!!
        write2file($out,$out_file);
        $final_msg = "Option list written to [$out_file]";
    }
}

# escape ^ $ . | { } [ ] ( ) * + ? \
sub escape_regex($) {
    my $txt = shift;
    my $ntxt = '';
    my $len = length($txt);
    my ($i,$ch);
    for ($i = 0; $i < $len; $i++) {
        $ch = substr($txt,$i,1);
        if (($ch eq '^')||($ch eq '$')||($ch eq '.')||($ch eq '|')||($ch eq '{')||($ch eq '}')) {
            $ntxt .= '\\';
        } elsif (($ch eq '\\')||($ch eq '/')||($ch eq '(')||($ch eq ')')||($ch eq '[')||($ch eq ']')) {
            $ntxt .= '\\';
        } elsif (($ch eq '*')||($ch eq '+')||($ch eq '?')) {
            $ntxt .= '\\';
        }
        $ntxt .= $ch;
    }
    return $ntxt;
}

# processing a CMakeLists.txt
sub process_cmake_lines($$$);

sub process_cmake_lines($$$) {
    my ($in,$rlines,$rha) = @_;
    my @options = ();
    my ($cnt,$lnn,$i,$i2,$line,$tline,$tmp);
    my ($ra,$len,$option,$message,$default,$subd,$ff);
    my ($feline,$fevar,$febgn,@arr);
    my $mino = 0;
    my $minm = 0;
    my ($name,$dir) = fileparse($in);
    if ($dir =~ /^\.(\\|\/){1}$/) {
        $dir = '';
    } else {
        $dir .= $PATH_SEP if ( !($dir =~ /(\\|\/)$/) );
    }
    $cnt = scalar @{$rlines};
    $in =~ s/^\.(\\|\/)//;
    $lnn = sprintf("%5d", $cnt);
    $in .= ' ' while (length($in) < 8+1+3);
    prt("Got $lnn lines, from [$in] to process...\n") if (VERB9());
    my @sub_dirs = ();
    for ($i = 0; $i < $cnt; $i++) {
        $total_lines++;
        $i2 = $i + 1;
        $line = ${$rlines}[$i];
        $total_bytes += length($line);
        chomp $line;
        $tline = trim_all($line);
        if ($tline =~ /^option\s*\(/i) { # seek OPTION(...) line
            while (($i2 < $cnt)&&(!full_opts_line($tline))) {
                $total_lines++;
                $i++;
                $i2 = $i + 1;
                $tmp = ${$rlines}[$i];
                $total_bytes += length($tmp);
                $tline .= ' ';
                $tline .= trim_all($tmp);
            }
            prt("$tline\n") if (VERB9());
            $ra = split_opts_line($tline);
            $len = scalar @{$ra};
            if ($len >= 2) {
                $tmp = ${$ra}[0];
                $option = ${$ra}[1];
                $message = 'NO MESSAGE';
                if ($len > 2) {
                    $message = ${$ra}[2];
                }
                $default = 'OFF';
                if ($len > 3) {
                    $default = ${$ra}[3];
                }
                push(@options, [$option, $message, $default]);
                if (VERB9()) {
                    prt("$option $message $default\n");
                    #foreach $tmp (@{$ra}) {
                    #    prt("$tmp ");
                    #}
                    #prt("\n");
                }
            } else {
                prtw("$pgmname:WARNING: Line $i2 did not SPLIT correctly! [$tline] file $in\n");
            }
        } elsif ($tline =~ /^foreach\s*\(/i) {
            $febgn = $tline;
            $tmp = $tline;
            $tmp =~ s/^foreach\s*\(\s*//i;
            $fevar = $tmp;
            prt("In [$in] got 'foreach' [$fevar]\n") if (VERB9());
            while (($i2 < $cnt)&&(!full_fe_line($tline))) {
                $total_lines++;
                $i++;
                $i2 = $i + 1;
                $tmp = ${$rlines}[$i];
                $total_bytes += length($tmp);
                $tmp = trim_all($tmp);
                $tline .= ' ' if (length($tline) && length($tmp));
                $tline .= $tmp;
            }
            $tmp = escape_regex($febgn);
            $tline =~ s/$tmp//;
            $tline =~ s/\)\s*$//;
            $feline = trim_all($tline);
            prt("item set [$feline]\n") if (VERB9());
            $tline = '';
            while ($i2 < $cnt) {
                $total_lines++;
                $i++;
                $i2 = $i + 1;
                $tmp = ${$rlines}[$i];
                $total_bytes += length($tmp);
                $tmp = trim_all($tmp);
                last if ($tmp =~ /^endforeach/i);
                $tline .= ' ' if (length($tline));
                $tline .= $tmp;
            }
            prt("action: [$tline]\n") if (VERB9());
            $tmp = escape_regex($fevar);
            if ($tline =~ /^\s*add_subdirectory\s*\(\s*\$\{\s*$tmp\s*\}\s*\)/) {
                @arr = split(/\s+/,$feline);
                $tmp = scalar @arr;
                prt("Need to process the set of $tmp items as subdirectories...!\n") if (VERB5());
                foreach $tmp (@arr) {
                    $ff = $dir.$tmp;
                    if (-d $ff) {
                        $ff .= $PATH_SEP."CMakeLists.txt";
                        if (-f $ff) {
                            prt("Found added item [$ff]\n") if (VERB5());
                            push(@sub_dirs,$ff);
                        } else {
                            prtw("WARNING: NOT found [$ff]\n");
                        }
                    } else {
                        prtw("WARNING: NOT found sub-directory [$ff]\n");
                    }
                }
            }
        } elsif ($tline =~ /^add_subdirectory\s*\((.+)\)\s*$/i) {
            $subd = trim_all($1);
            prt("$i2: [$line] sub [$subd]\n") if (VERB9());
            $ff = $dir.$subd;
            if (-d $ff) {
                $ff .= $PATH_SEP."CMakeLists.txt";
                if (-f $ff) {
                    prt("Found added item [$ff]\n") if (VERB5());
                    push(@sub_dirs,$ff);
                } else {
                    prtw("WARNING: NOT found [$ff]\n");
                }
            } else {
                prtw("WARNING: NOT found sub-directory [$ff]\n");
            }
        }
    }
    ${$rha}{$in} = \@options;
    foreach $in (@sub_dirs) {
        if (open FIL, "<$in") {
            my @lines = <FIL>;
            close FIL;
            process_cmake_lines($in,\@lines,$rha);
        }
    }
}

sub process_file($) {
    my ($in) = @_;
    my ($name,$dir) = fileparse($in);
    if (!my_file_type($name)) {
        return;
    }
    if (open FIL, "<$in") {
        my @lines = <FIL>;
        close FIL;
        process_cmake_lines($in,\@lines,\%options_found);
    } 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);
    }
}

#########################################
###### MAIN ######
process_args(@ARGV);
process_input();
show_cmake_options(\%options_found);
prt("$total_dirs dirs, $total_files files, $total_lines lines, $total_bytes bytes, for $total_options options.\n");
pgm_exit(0,"");

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

sub need_arg {
    my ($arg,@av) = @_;
    pgm_exit(1,"ERROR: [$arg] must have a following argument!\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 =~ /^l/) {
                $load_log = 1;
                prt("Set to load log at end.\n");
            } elsif ($sarg =~ /^o/) {
                need_arg(@av);
                shift @av;
                $sarg = $av[0];
                $out_file = $sarg;
                prt("Output options to [$out_file].\n");
            } elsif ($sarg =~ /^v/) {
                if ($sarg =~ /^v.*(\d+)$/) {
                    $verbosity = $1;
                } else {
                    while ($sarg =~ /^v/) {
                        $verbosity++;
                        $sarg = substr($sarg,1);
                    }
                }
                prt("Set verbosity to $verbosity.\n") if (VERB1());
            } else {
                pgm_exit(1,"$pgmname:ERROR: Unknown option [$arg]! Try -?\n");
            }
        } else {
            push(@in_files,$arg);
            prt("Added input [$arg]\n");
        }
        shift @av;
    }
    if (!@in_files && $debug_on) {
        push(@in_files,$def_file);
        prt("Added DEFAULT input [$def_file]\n");
    }
    if (!@in_files && (-f "CMakeLists.txt")) {
        push(@in_files,"CMakeLists.txt");
        prt("Added local input [CMakeLists.txt]\n");
    }
    if ( ! @in_files ) {
        pgm_exit(1,"$pgmname:ERROR: No input found in command!\n");
    }
}

sub give_help {
    prt("$pgmname version $pgm_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(" --out file     (-o) = Write options list to this file.\n");
    prt(" --verb[Num]    (-v) = Bump [or set] verbosity. (def=$verbosity)\n");
    prt("Purpose: Given a directory, search for CMakeLists.txt files, recursively,\n");
    prt("and show each option(NAME \"test\" ON) item found.\n");
}

# eof - cmakeopts.pl
