#!/usr/bin/perl -w
##
## Copyright (c) 2015 The University of Utah
## All rights reserved.
##
## This file is distributed under the University of Illinois Open Source
## License.  See the file COPYING for details.

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

use strict;
use File::Spec;
use File::Copy;
use File::Basename;
use File::Compare;
use Cwd;

# TODO might need to support __has_include_next, __has_include

sub usage() {
    print "usage: $0 [-debug] compiler 'compiler args' source-file\n";
    print "the file should not be in the cwd\n";
    print "the file will be assumed to be C++ unless it ends with .c\n";
    exit(-1);
}

my $debug = 0;
if ($ARGV[0] eq "-debug") {
    shift @ARGV;
    $debug = 1;
}

usage() unless scalar(@ARGV)==3;
my $COMP = $ARGV[0];
my $CFLAGS = $ARGV[1];
my $FILE = $ARGV[2];

# TODO bail if file is in cwd

my $EXT;
my $CPP;
if ($FILE =~ /\.c$/) {
    $EXT = ".c";
    $CPP = "";
    print "looks like '$FILE' is C code\n";
} else {
    $EXT = ".cpp";
    $CPP = "-x c++";
    print "looks like '$FILE' is C++ code (if not you'll need to edit this script)\n";
}

my @exts = (".c", ".cpp");

sub runit ($) {
    my $cmd = shift;
    print "[ $cmd ]\n";
    die if ((system "$cmd") == -1);
    my $exit_value  = $? >> 8;
    return $exit_value;
}

my @includes;
my %deps;
my $srcfile_copy;

# get the include path
open INF, "echo | $COMP $CFLAGS -v $CPP -E - 2>&1 |" or die;
my $go = 0;
print "compiler search path:\n";
while (my $line = <INF>) {
    chomp $line;
    if ($line eq "#include <...> search starts here:") {
	$go = 1;
	next;
    }
    if ($line eq "End of search list.") {
	$go = 0;
	next;
    }
    if ($go) {
	$line =~ s/^\s*//;
	$line =~ s/\s*$//;
	# WHY DO YOU DO THIS, OS X?
	$line =~ s/ \(framework directory\)$//;
	my $dir = Cwd::realpath($line);
	print "  $line\n";
	die "oops '$line'" unless -d $line;
	push @includes, $line;
	next;
    }
}
close INF;

# get the list of dependencies
open INF2, "$COMP $CFLAGS -c -w $CPP $FILE -M |" or die;
my $base = basename($FILE, @exts);
open OF, ">orig_deps.txt" or die;
while (my $f = <INF2>) {
    chomp $f;
    $f =~ s/$base\.o://g;
    $f =~ s/^\s*//;
    $f =~ s/\s*\\\s*//g;
    my @l = split /\s+/, $f;
    foreach my $g (@l) {
	print OF "$g ";
	# assume the actual src file is the first dependency
	if (!defined($srcfile_copy)) {
	    $srcfile_copy = basename($g);
	}
	die unless -f $g;
	my $r = Cwd::realpath($g);
	die unless -f $r;
	# this hash needs to contain canonical paths
	$deps{$r} = 1;
    }
}
close INF2;
close OF;

my %map;
my %rmap;

sub substitute($$$$) {
    (my $file, my $lref, my $next, my $line) = @_;
    my @list = @{$lref};

    print "  we're trying to localize '$file'\n" if $debug;
    print "    looking for next after $next\n" if $debug && $next ne "";
    
    foreach my $dir (@list) {
	my $path = File::Spec->catfile($dir, $file);
	print "    concatenating '$dir' + '$file'\n" if $debug;
	print "      to get '$path'\n" if $debug;
	if (-e $path) {
	    if ($next ne "" && $path eq $next) {
		$next = "";
	    } else {
		$path = Cwd::realpath($path);
		if (exists $map{$path}) {
		    my $f = $map{$path};
		    print "    found: -> '$dir' '$f'\n" if $debug;
		    print OUTF "#pragma \"$path\"\n";
		    print OUTF "#include \"$f\"";
		    print OUTF "\n";
		    return;
		}
	    }
	}
    }
    print "not found '$file'\n" if $debug;
    print OUTF "#error localize_headers should not have tried to include $file\n";
}

# make a local copy of a file with its include paths munged to refer
# to other copies -- when this is finished there should be no includes
# outside of the cwd
sub rewrite($$) {
    (my $absfile, my $target) = @_;
    open INF, "<$absfile" or die;
    open OUTF, ">$target" or die;
    my $n = 0;
    my($fnxxx, $dir, $suffixxx) = fileparse($absfile);
    my @dotincludes = @includes;
    unshift @dotincludes, $dir;
    while (my $line = <INF>) {
	chomp $line;
	$n++;
	if ($line =~ /^\s*#\s*include\s+\"(.*?)\"/) {
	    substitute($1, \@dotincludes, "", $line);
	    next;
	}
	if ($line =~ /^\s*#\s*include\s+\<(.*?)\>/) {
	    substitute($1, \@includes, "", $line);
	    next;
	}
	if ($line =~ /^\s*#\s*include_next\s+\<(.*?)\>/) {
	    substitute($1, \@includes, $absfile, $line);
	    next;
	}
	print OUTF $line."\n";
    }
    close INF;
    close OUTF;
}

foreach my $dep (sort keys %deps) {
    my($file, $dirs, $suffix) = fileparse($dep);

    my $s1 = scalar(keys %map);
    my $s2 = scalar(keys %rmap);

    my $count = 0;
    my $renamed = $file;
    while (exists($rmap{$renamed})) {
	$count++;
	$renamed = $file."_".$count;
    }

    print "adding dep '$dep' -> '$renamed'\n" if $debug;
    
    $map{$dep} = $renamed;
    $rmap{$renamed} = $dep;

    die unless ($s1 < scalar(keys %map));
    die unless ($s2 < scalar(keys %rmap));
}

# copy over all dependencies
my $all_deps = "";
my $n = 0;
foreach my $dep (sort keys %deps) {
    print "processing '$dep'\n" if $debug;
    rewrite($dep, $map{$dep});
    $all_deps .= "$map{$dep} ";
    $n++;
}

print "\n\'files_to_reduce.txt\' contains a list of all files copied to this dir\n\n";

open OUTF, ">files_to_reduce.txt" or die;
print OUTF "$all_deps\n";
close OUTF;

my $res;

$res = runit ("$COMP $CFLAGS -E $FILE > out1$EXT");
die unless $res == 0;
runit ("grep -v '^# ' out1$EXT | grep -v '^\\s*\$' > out1strip$EXT");

$res = runit ("$COMP $CFLAGS -E $srcfile_copy > out2$EXT");
die unless $res == 0;
runit ("grep -v '^# ' out2$EXT | grep -v '^\\s*\$' > out2strip$EXT");

print "\n";
print "original src file, preprocessed, is in out1$EXT, stripped of # lines as out1strip$EXT\n";
print "transformed src file, preprocessed, is in out2$EXT, stripped of # lines as out2strip$EXT\n";

print "\n";

if (compare("out1strip$EXT", "out2strip$EXT") == 0) {
    print "the stripped files are the same, which means this script might well have worked\n";
} else {
    print "the stripped files are different -- this script might have worked, but please\n";
    print "inspect the diffs manually\n";
}

print "\nreduce like this:\n";
print "     creduce interestingness_test `cat files_to_reduce.txt`\n\n";
