#!/usr/bin/perl -w

# This perl script is intended to go through the fluxbox source
# code searching for the special NLS strings. It then dumps
# the requested details.
# 
# I started trying to write it fairly generic, but it was difficult :-)
# Should be fairly adaptable though
#
# It doesn't currently handle more than one NLS define per line
#    => If you get an "undefined" error, its probably 2 on one line

$VERSION = "0.1";

use strict;
use Getopt::Std;

$Getopt::Std::STANDARD_HELP_VERSION = 1;

# the boolitem and focusitem is pretty dodgy, but it'll do for now
my $match_re = "(?:_FB(?:TK)?TEXT|_BOOLITEM|_FOCUSITEM)";
# regular expression for not a unquoted quote
my $noquote = q'(?:[^\"]|\\")';

my $fielddelim = "\0";
my $recorddelim = "\0";

#############################
# Parse and validate arguments
my %opts;

my $command = $0;
$command =~ s,^.*/,,;

my $fullcommand = "$command " . join(" ", @ARGV);

if (!getopts("d:fhn:pr:vFHN:R", \%opts)) {
    HELP_MESSAGE("error");
    exit(1);
}

sub HELP_MESSAGE {
    my $arg = shift;
    my $FD = *STDOUT;
    if (defined($arg) && $arg eq "error") {
        $FD = *STDERR;
    }

    print $FD "Usage: $command [options] directory\n";
    print $FD " Where options can be:\n";
    print $FD "    -R\tDon't recurse into subdirectories.\n";
    print $FD "    -f\tThe argument is a file, not a directory\n";
    print $FD "    -F\tPrint full NLS names, not shorthand ones\n";
    print $FD "    -d delim\tUse delim as the default delimiter\n";
    print $FD "    -r delim\tUse delim as the record delimiter\n";
    print $FD "    -n\tHeader name, default FLUXBOX_NLS_HH\n";
    print $FD "    -N\tNamespace for header\n";
    print $FD "    -v\tverbose output\n";
    print $FD "    -h\tPrint this help message\n";
    print $FD "\nPlus one of the following options that direct how to operate:\n";
    print $FD "    -H\tGenerate a header file for the strings encountered (-n implied).\n";
    print $FD "    -p\tPrint out a null-separated tuple of Set,String,Default,Description\n";
    print $FD "    \t\n";
    print $FD "\n";

}

if (defined($opts{"h"})) {
    HELP_MESSAGE();
    exit(0);
}

my $num_modes = 0;
my $mode;

sub mode_opt {
    my $opt = shift;
    my $modename = shift;
    return if (!defined($opts{$opt}));
    $num_modes++;
    $mode = $modename;
}

mode_opt("H", "header");
mode_opt("p", "print");

if ($num_modes == 0) {
    print STDERR "Must give one mode of operation!\n";
    HELP_MESSAGE("error");
    exit(1);
} elsif ($num_modes > 1) {
    print STDERR "Too many modes of operation - must give exactly one!\n";
    HELP_MESSAGE("error");
    exit(1);
}

my $recurse = 1;
$recurse = 0 if (defined($opts{"R"}));

my $fullnames = 0;
$fullnames = 1 if (defined($opts{"f"}) || $mode eq "header");

my $headername = "FLUXBOX_NLS_HH";
$headername = $opts{"n"} if (defined($opts{"n"}));

my $namespace;
$namespace = $opts{"N"} if (defined($opts{"N"}));

my $verbose = 0;
$verbose = 1 if (defined($opts{"v"}));

if (defined($opts{"d"})) {
    $fielddelim = $opts{"d"};
    $recorddelim = $opts{"d"};
}

if (defined($opts{"r"})) {
    $recorddelim = $opts{"r"};
}


if (scalar(@ARGV) == 0) {
    print STDERR "Must give at least one more argument - the directory to scan\n";
    exit(1);
}


my @args = @ARGV;
if (!defined($opts{"f"})) {
    foreach my $dir (@args) {
        if (! -d $dir) {
            print STDERR "$dir is not a directory, aborting\n";
            exit(2);
        }
    }
} elsif (defined($opts{"f"})) {
    $recurse = 0;

    foreach my $file (@args) {
        if (! -r $file) {
            print STDERR "$file is not a readable file, aborting\n";
            exit(2);
        }
    }
}


#############################
# Actually do stuff! (finally...)

my %sets;

if (defined($opts{"f"})) {
    foreach my $file (@args) {
        process_file($file);
    }
} else {
    foreach my $dir (@args) {
        process_dir($dir);
    }
}

# Now we have the data, we need to print it out
eval "mode_$mode()";
exit(0);

# this function is given the fbtext arguments
# But the first argument is the macro name...
sub store {
    my ($type, $set, $str, $default, $desc) = @_;

    if ($type eq "_FBTKTEXT") {
        $set = "FbTk$set";
    }

    if ($fullnames == 1) {
        $str = $set . $str;
        $set = $set . "Set";
    }

    $sets{$set}->{$str}{"default"} = $default;
    $sets{$set}->{$str}{"desc"} = $desc;

}

# C strings can just be a bunch of quoted strings adjacent to 
# each other. This just puts them all together, removes the quotes
# and unquotes anything we want to.
# there may be newlines embedded... compare everything /s
sub squish {
    my $str = shift;

    # remove first and last quote
    $str =~ s/^\s*\"//s;
    $str =~ s/\"\s*$//s;
    
    # now remove any inner quotes and intervening spaces
    $str =~ s/([^\\])\"\s*\"/$1/sg;

    # finally, unescape any remaining quotes
    $str =~ s/\\\"/\"/g;

    return $str;
}

sub process_dir {
    my $dir = shift;
    print STDERR "Processing directory '$dir'\n" if ($verbose == 1);
    opendir(DIR, $dir) || die "can't opendir $dir: $!";
    my @files = grep { ( /\.(cc|hh)$/ && -f "$dir/$_" ) || 
                           ( -d "$dir/$_" && $_ !~ /^\.\.?$/ )
                         } readdir(DIR);
    closedir DIR;

    foreach my $file (@files) {
        if (-d "$dir/$file") {
            process_dir("$dir/$file") if ($recurse == 1);
        } else {
            process_file("$dir/$file");
        }
    }
}

# assumptions for now:
# - no more than one NLS thing on any single line
# - internal parenthesis are balanced
# - one nls thing can span several lines
sub process_file {
    my $file = shift;

    print STDERR "Processing file '$file'\n" if ($verbose == 1);
    open(FILE, "<$file") || die "Can't open file $file: $!";

    while (<FILE>) {
        chomp;
        if (/$match_re/ && $_ !~ /^\#(define|undef)/) {
            my $tail = $_;
            # strip away leading stuff
            # note that this doesn't work with more than one match on a line
            $tail =~ s/^.*($match_re)/$1/;
            # now we just need to find the end, looking out for any
            # quotes
            my $end = 0;
            my $full  = $tail;
            while ($end == 0) {
                # match the defined macro, plus the first 4 arguments 
                # (ignore any more), then handle them
                if ($full =~ /^($match_re)\(([^,]+),\s*([^,]+),((?:\s*\"$noquote*\")+),((?:\s*"$noquote*")+)\s*(?:,.*)?\)/s ) {
                    store($1, $2, $3, squish($4), squish($5));
                    $end++;
                } else {
                    my $extra = <FILE>;
                    last if (!defined($extra));
                    $full .= $extra;
                }
            }
        }
    }
    close(FILE);
}


sub mode_print {
    foreach my $set (sort keys %sets) {
        foreach my $str (sort keys %{$sets{$set}}) {
            print $set . $fielddelim . $str . $fielddelim . $sets{$set}->{$str}{"default"} . $fielddelim . $sets{$set}->{$str}{"desc"} . $recorddelim;
        }
    }
}

sub mode_header {
    print "// This file generated by $fullcommand, on " . localtime() . "\n\n";
    print "#ifndef $headername\n";
    print "#define $headername\n\n";
    print "namespace $namespace {\n\n" if (defined($namespace));
    print "enum {\n";

    my $setnum = 0;
    foreach my $set (sort keys %sets) {
        $setnum++;
        printf "\t%s = %d,\n", $set, $setnum; 

        my $strnum = 0;
        foreach my $str (sort keys %{$sets{$set}}) {
            $strnum++;
            printf "\t%s = %d,\n", $str, $strnum; 
        }
        print "\n";
    }
    print "\tdummy_not_used = 0 // just for the end\n\n";
    print "}; // end enum\n\n";
    print "}; // end namespace $namespace\n\n" if (defined($namespace));
    print "#endif // $headername\n";
}

