#!/usr/bin/perl -w
# NAME: exefixcmake.pl
# AIM: Given a single CMakeLists.txt, or a path to search for CMakeLists.txt, read in 
# and make sure for every 'add_executable( name ... )' thee also exists a
# set_target_properties( name PROPERTIES DEBUG_POSTFIX d ), put in if(WIN32)
# 21/09/2013 - attempt to exactly retain the file format
# 02/06/2013 - Process ALL CMakeLists.txt in the set
# 23/03/2013 - Enhance UI
# if added... and if -w, backup file and write new...
use strict;
use warnings;
use File::Basename;  # split path ($name,$dir,$ext) = fileparse($file [, qr/\.[^.]*/] )
use Cwd;
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.1 2012-07-18";
my $load_log = 0;
my $in_file = '';
my $verbosity = 0;
my $out_file = '';
my $write_files = 0;
my $add_win32_if = 1;

my @input_files = ();
my @input_dirs = ();

# ### DEBUG ###
my $debug_on = 0;
my $def_file = 'def_file';
##my $def_dir = 'C:\FG\16\simgear';
my $def_dir = 'C:\FG\18\flightgear';

### program variables
my @warnings = ();
my $cwd = cwd();
my $total_fixes = 0;
my $total_files = 0;
my $total_lines = 0;

# to distinguish between debug and release lib
# set( CMAKE_DEBUG_POSTFIX "d" )
my $seen_set_postfix = 0;
my @set_files = ();
my @fix_files = ();

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

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

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


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

sub not_complete_cmake_line($) {
    my $line = shift;
    my $len = length($line);
    my @brackets = ();
    my ($i,$ch,$inquot,$pc);
    $inquot = 0;
    $ch = '';
    for ($i = 0; $i < $len; $i++) {
        $pc = $ch;
        $ch = substr($line,$i,1);
        if ($inquot) {
            $inquot = 0 if (($ch eq '"') && ($pc ne "\\"));
        } else {
            if ($ch eq '"') {
                $inquot = 1;
            } elsif ($ch eq '(') {
                push(@brackets,$i);
            } elsif ($ch eq ')') {
                if (@brackets) {
                    pop @brackets;
                }
            } elsif ($ch eq '#') {
                if (!@brackets) {
                    last;
                }
            }
        }
    }
    $len = scalar @brackets;
    return $len;
}

# split line into 'tags' separated by (, ), or space(s)
# return ref array of tags
sub split_cmake_line($) {
    my $oline = shift;
    my $line = trim_all($oline);
    my $len = length($line);
    my @brackets = ();
    my @tags = ();
    my ($i,$ch,$inquot,$pc,$tag);
    $inquot = 0;
    $ch = '';
    $tag = '';
    for ($i = 0; $i < $len; $i++) {
        $pc = $ch;
        $ch = substr($line,$i,1);
        if ($inquot) {
            $tag .= $ch;
            if (($ch eq '"') && ($pc ne "\\")) {
                $inquot = 0;
                push(@tags,$tag);
                $tag = '';
            }
            next;
        } else {
            if ($ch eq '"') {
                $inquot = 1;
                push(@tags,$tag) if (length($tag));
                $tag = $ch;
                next;
            } elsif ($ch eq '(') {
                if (!@brackets) {
                    push(@tags,$tag) if (length($tag));
                    $tag = '';
                }
                push(@brackets,$i);
                next;
            } elsif ($ch eq ')') {
                if (@brackets) {
                    pop @brackets;
                }
                if (!@brackets) {
                    push(@tags,$tag) if (length($tag));
                    $tag = '';
                }
                next;
            } elsif ($ch eq '#') {
                if (!@brackets) {
                    last;
                }
            }
        }
        if ($ch =~ /\s/) {
            push(@tags,$tag) if (length($tag));
            $tag = '';
        } else {
            $tag .= $ch;    # build up a tag
        }
    }
    $len = scalar @brackets;
    return \@tags;
}

my %set_hash = ();
my $act_line = '';
my $bgn_line = 0;
my $end_line = 0;
sub do_subs($);

sub init_set_hash() {
    $set_hash{CMAKE_MODULE_PATH} = $cwd;
}

sub do_subs($) {
    my $var = shift;
    return $var if (!($var =~ /\$/));
    return $var if (!($var =~ /\{/));
    return $var if (!($var =~ /\}/));
    my $len = length($var);
    return $var if ($len < 4);
    my $nvar = '';
    my ($i,$ch,$i2,$nc,$cnt,$j,$tag);
    for ($i = 0; $i < $len; $i++) {
        $i2 = $i + 1;
        $ch = substr($var,$i,1);
        $nc = ($i2 < $len) ? substr($var,$i2,1) : '';
        if (($ch eq '$')&&($nc eq '{')) {
            $cnt = 1;
            $tag = '';
            for ($j = $i2+1; $j < $len; $j++) {
                #$i2 = $j + 1;
                $ch = substr($var,$j,1);
                if ($ch eq $nc) {
                    $cnt++;
                } elsif ($ch eq '}') {
                    $cnt--;
                    if ($cnt == 0) {
                        last;
                    }
                } else {
                    $tag .= $ch;
                }
            }
            if (length($tag) && ($cnt == 0)) {
                if (($tag =~ /\$/)&&($tag =~ /\{/)&&($tag =~ /\}/)) {
                    $tag = do_subs($tag);
                }
                if (defined $set_hash{$tag}) {
                    $nvar .= $set_hash{$tag};
                    $i = $j;
                }
            }
        } else {
            $nvar .= $ch;
        }
    }
    return $nvar;
}

sub add_to_set_hash($) {
    my $rta = shift;
    my $len = scalar @{$rta};
    if ($len < 3) {
        prtw("WARNING: SET does NOT have 3+ components! line [$act_line]\n");
        return;
    }
    my ($i,$tmp,$i2);
    my $val = '';
    if (VERB9()) {
        prt("$bgn_line:$end_line: [$act_line]\n");
        $tmp = '';
        for ($i = 0; $i < $len; $i++) {
            $i2 = $i + 1;
            if ($i == 1) {
                $tmp .= '(';
            } else {
                $tmp .= ' ' if (length($tmp));
            }
            $tmp .= ${$rta}[$i];
            if ($i2 == $len) {
                $tmp .= ')';
            }
        }
        prt("$bgn_line:$end_line: [$tmp] recon\n");
    }
    my $var = ${$rta}[1];
    $var = do_subs($var);
    $set_hash{$var} = '';
    for ($i = 2; $i < $len; $i++) {
        $tmp = do_subs(${$rta}[$i]);
        $val .= ';' if (length($val));
        $val .= $tmp;
    }
    $set_hash{$var} = $val;
    prt("$bgn_line:$end_line: SET($var $val)\n") if (VERB9());
}

sub process_in_file($) {
    my ($inf) = @_;
    if (! open INF, "<$inf") {
        pgm_exit(1,"ERROR: Unable to open file [$inf]\n"); 
    }
    my @lines = <INF>;
    close INF;
    my $lncnt = scalar @lines;
    my ($i,$line,$inc,$lnn,$tline,$len,$tmp,$ttmp,$rta,$test,$chg,$cline);
    my ($cnt,$head,$name,$msg);
    $total_files++;
    $total_lines += $lncnt;
    $head = "\n# $total_files: Processing $lncnt lines, from [$inf]...\n";
    if (VERB9()) {
        prt($head);
        $head = '';
    }
    $lnn = 0;
    my @nlines = ();
    my @olines = ();    # 20130921 - try to keep ORIGINAL format of lines
    my $total_chgs = 0;
    my $exe_cnt = 0;
    my $lib_cnt = 0;
    my $tp_cnt = 0;
    my $is_exe = 0;
    my $is_lib = 0;
    my $is_tp = 0;
    my @exe_lines = ();
    my @lib_lines = ();
    my @tp_lines = ();
    for ($i = 0; $i < $lncnt; $i++) {
        $lnn = $i + 1;
        $bgn_line = $lnn;
        $line = $lines[$i];
        chomp $line;
        $tline = trim_all($line);
        $len = length($tline);
        if ($len == 0) {
            push(@nlines,"");
            next;
        }
        if ($line =~ /^\s*\#/) {    # skip comments
            push(@nlines,$line);
            next;
        }
        push(@olines,$line);
        while (not_complete_cmake_line($line) && ($lnn < $lncnt)) {
            $i++;
            $lnn = $i + 1;
            $tmp = $lines[$i];
            chomp $tmp;
            push(@olines,$tmp);
            $ttmp = trim_all($tmp);
            $len = length($ttmp);
            next if ($len == 0);
            $line .= ' '.$tmp;
            $tline .= ' '.$ttmp;
        }
        $act_line = $line;
        $end_line = $lnn;
        $rta = split_cmake_line($line);
        $len = scalar @{$rta};
        $cnt = 0;
        $ttmp = '';
        $is_exe = 0;
        $is_lib = 0;
        $is_tp = 0;
        foreach $tmp (@{$rta}) {
            $cnt++;
            if ($cnt == 1) {
                $ttmp .= "$tmp(";
                if ($tmp =~ /^\s*add_executable/i) {
                    $exe_cnt++;
                    $is_exe = 1;
                    push(@exe_lines,$line);
                } elsif ($tmp =~ /^\s*add_library/i) {
                    $lib_cnt++;
                    $is_lib = 1;
                    push(@lib_lines,$line);
                } elsif ($tmp =~ /^\s*set_target_properties/i) {
                    $tp_cnt++;
                    $is_tp = 1;
                    push(@tp_lines,$line);
                } elsif ($tmp =~ /^set$/i) {
                    add_to_set_hash($rta);
                    if (($len > 2)&&(${$rta}[1] eq 'CMAKE_DEBUG_POSTFIX')) {
                        # set( CMAKE_DEBUG_POSTFIX "d" )
                        $seen_set_postfix++;
                        push(@set_files,$inf);
                    }
                }
            } else {
                $ttmp .= "$tmp ";
                if (($cnt == 2) && ($is_exe || $is_lib || $is_tp)) {
                    $name = $tmp;
                }
            }
        }
        $ttmp =~ s/\s+$//;
        $ttmp .= ")";
        ### if ($is_exe || $is_lib || $is_tp) {
        if ($is_exe || $is_tp) {
            prt($head) if (length($head) && VERB1());
            $head = '';
            prt("$ttmp # $name\n") if (VERB1());
        }
        #push(@nlines,$line);
        push(@nlines,@olines);
        @olines = ();
    }

    ### if ($exe_cnt || $lib_cnt) {
    if ($exe_cnt) {
        my ($rta2,$line2,$name2,$fnd,$len2,$prop,$fix_cnt);
        $fix_cnt = 0;
        my @fix_lines = ();
        foreach $line (@exe_lines) {
            $rta = split_cmake_line($line);
            $name = ${$rta}[1];
            $fnd = 0;
            $prop = '';
            foreach $line2 (@tp_lines) {
                $rta2 = split_cmake_line($line2);
                $len2 = scalar @{$rta2};
                if (($len2 > 4)&&(${$rta2}[2] eq 'PROPERTIES')&&(${$rta2}[3] eq 'DEBUG_POSTFIX')) {
                    $name2 = ${$rta2}[1];
                    $prop = ${$rta2}[4];
                    if ($name eq $name2) {
                        $fnd = 1;
                        last;
                    }
                }
            }
            if ($fnd) {
                prt("# Exe $name already has DEBUG_POSTFIX $prop property\n") if (VERB1());
            } else {
                push(@fix_lines,$line);
                $fix_cnt++;
            }
        }
        if ($fix_cnt) {
            # -------
            $total_fixes++;
            push(@fix_files,$inf);
            $msg = '';
            if ($add_win32_if) {
                $tmp = "if(WIN32)";
                push(@nlines,$tmp);
                $msg = "$tmp\n";
            }
            foreach $line (@fix_lines) {
                $rta = split_cmake_line($line);
                $name = ${$rta}[1];
                # set_target_properties( name PROPERTIES DEBUG_POSTFIX d )
                $tmp = "    set_target_properties( $name PROPERTIES DEBUG_POSTFIX d )";
                push(@nlines,$tmp);
                $msg .= "$tmp\n";
            }
            if ($add_win32_if) {
                $tmp = "endif()";
                push(@nlines,$tmp);
                $msg .= "$tmp\n";
            }
            prt($msg)  if (VERB1());
            if ($write_files) {
                rename_2_old_bak($inf);
                $line = join("\n",@nlines);
                $line .= "\n";
                write2file($line,$inf);
                prt("Written amended to [$inf]\n");
            } else {
                prt("Write is OFF. Use -w to write changes to file [$inf].\n");
            }
            # -------
        }
        prt("# Found $exe_cnt exe, $lib_cnt libs, and $tp_cnt props, $fix_cnt fixes in [$inf]\n");
    }
    ### }
    #if (length($out_file)) {
    #    write2file($line,$out_file);
    #    prt("Results written to [$out_file], with $total_chgs changes.\n");
    #}
}


sub add_cmakelists($);

sub add_cmakelists($) {
    my $dir = shift;
    my @dirs = ();
    if (!opendir(DIR,$dir)) {
        prtw("WARNING: Unable to open directory [$dir]!\n");
        return;
    }
    my @files = readdir(DIR);
    closedir(DIR);
    my ($file,$ff);
    ut_fix_directory(\$dir);
    foreach $file (@files) {
        next if ($file eq '.');
        next if ($file eq '..');
        $ff = $dir.$file;
        if (-d $ff) {
            push(@dirs,$ff);
        } elsif (-f $ff) {
            if ($file =~ /^CMakeLists\.txt$/i) {
                push(@input_files,$ff);
            }
        } else {
            prtw("WARNING: What is THIS? [$ff]\n");
        }
    }
    foreach $dir (@dirs) {
        add_cmakelists($dir);
    }
}


sub process_inputs() {
    my ($item);
    foreach $item (@input_dirs) {
        $set_hash{PROJECT_SOURCE_DIR} = $item;
        add_cmakelists($item);
    }
    foreach $item (@input_files) {
        process_in_file($item);
    }
    prt("\nFound $total_fixes fixes in scan of $total_files files, $total_lines lines\n");
    foreach $item (@fix_files) {
        prt(" $item\n");
    }
    if ($seen_set_postfix) {
        $item = scalar @set_files;
        prt("For LIBRARIES, seen set CMAKE_DEBUG_POSTFIX ($seen_set_postfix) in $item file(s)\n");
        foreach $item (@set_files) {
            prt(" $item\n");
        }
    } else {
        prt("\nWARNING: set CMAKE_DEBUG_POSTFIX NOT SEEN\n");
    }
    prt("\n");
}

#########################################
### MAIN ###
parse_args(@ARGV);
init_set_hash();
process_inputs();
pgm_exit(0,"");
########################################

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

sub parse_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/i)||($sarg eq '?')) {
                give_help();
                pgm_exit(0,"Help exit(0)");
            } elsif ($sarg =~ /^v/) {
                if ($sarg =~ /^v.*(\d+)$/) {
                    $verbosity = $1;
                } else {
                    while ($sarg =~ /^v/) {
                        $verbosity++;
                        $sarg = substr($sarg,1);
                    }
                }
                prt("Verbosity = $verbosity\n") if (VERB1());
            } elsif ($sarg =~ /^l/) {
                if ($sarg =~ /^ll/) {
                    $load_log = 2;
                } else {
                    $load_log = 1;
                }
                prt("Set to load log at end. ($load_log)\n") if (VERB1());
#            } elsif ($sarg =~ /^o/) {
#                need_arg(@av);
#                shift @av;
#                $sarg = $av[0];
#                $out_file = $sarg;
#                prt("Set out file to [$out_file].\n") if (VERB1());
            } elsif ($sarg =~ /^w/) {
                $write_files = 1;
                prt("Set to write changed files after backup. ($write_files)\n") if (VERB1());
            } else {
                pgm_exit(1,"ERROR: Invalid argument [$arg]! Try -?\n");
            }
        } else {
            if (-f $arg) {
                $in_file = $arg;
                prt("Added input file [$in_file]\n") if (VERB1());
                push(@input_files,$in_file);
            } elsif (-d $arg) {
                prt("Added input directory [$arg]\n") if (VERB1());
                push(@input_dirs,$arg);
            } else {
                pgm_exit(1,"ERROR: [$arg] is NOT file or directory!\n");
            }
        }
        shift @av;
    }

    if ($debug_on) {
        prtw("WARNING: DEBUG is ON\n");
        if ((length($in_file) ==  0)) {
            if (-f $def_file) {
                $in_file = $def_file;
                prt("Set DEFAULT input to [$in_file]\n");
                push(@input_files,$in_file);
            }
        }
        if (!@input_dirs) {
            if (-d $def_dir) {
                prt("Set DEFAULT input to [$def_dir]\n");
                push(@input_dirs,$def_dir);
                $load_log = 3;
            }
        }
    }
    if (!@input_files && !@input_dirs) {
        pgm_exit(1,"ERROR: No input files or directories found in command!\n");
    }
}

sub give_help {
    prt("$pgmname: version $VERS\n");
    prt("Usage: $pgmname [options] (in-dir | in-file)\n");
    prt("Options:\n");
    prt(" --help  (-h or -?) = This help, and exit 0.\n");
    prt(" --verb[n]     (-v) = Bump [or set] verbosity. def=$verbosity\n");
    prt(" --load        (-l) = Load LOG at end. ($outfile)\n");
    ###prt(" --out <file>  (-o) = Write output to this file.\n");
    prt(" --write       (-w) = Write changes to files after backup.\n");
    prt("If given a directory, will search for ALL CMakeLists.txt in folder,\n");
    prt("else if given a CMakeLists.txt file, or set of files, only process it/them\n");
    prt("for change.\n");
}

# eof - exefixcmake.pl
