#!/usr/bin/perl
#
# Original version were part of Gerd Knorr's v4l scripts.
#
# Several improvements by (c) 2005-2007 Mauro Carvalho Chehab
#
# Largely re-written (C) 2007 Trent Piepho <xyzzy@speakeasy.org>
#
# Theory of Operation
#
# This acts as a sort of mini version of cpp, which will process
# #if/#elif/#ifdef/etc directives to strip out code used to support
# multiple kernel versions or otherwise not wanted to be sent upstream to
# git.
#
# Conditional compilation directives fall into two catagories,
# "processed" and "other".  The "other" directives are ignored and simply
# output as they come in without changes (see 'keep' exception).  The
# "processed" variaty are evaluated and only the lines in the 'true' part
# are kept, like cpp would do.
#
# If gentree knows the result of an expression, that directive will be
# "processed", otherwise it will be an "other".  gentree knows the value
# of LINUX_VERSION_CODE, BTTV_VERSION_CODE, the KERNEL_VERSION(x,y,z)
# macro, numeric constants like 0 and 1, and a few defines like
# I2C_CLASS_TV_DIGITAL
#
# An exception is if the comment "/*KEEP*/" appears after the expression,
# in which case that directive will be considered an "other" and not
# processed, other than to remove the keep comment.
#
# Known bugs:
# don't specify the root directory e.g. '/' or even '////'
# directives continued with a back-slash will always be ignored
# you can't modify a source tree in-place, i.e. source dir == dest dir

use strict;
use File::Find;
use Fcntl ':mode';
use Getopt::Long;

my $DEBUG = 0;
my $dead_code = 0;

GetOptions( "--debug" => \$DEBUG,
	    "--strip-dead-code" => \$dead_code );

my $VERSION = shift;
my $SRC = shift;
my $DESTDIR = shift;
my $BTTVCODE = KERNEL_VERSION(0,9,17);
my ($LINUXCODE, $extra) = kernel_version($VERSION);

if (!defined($DESTDIR)) {
	print "Usage:\ngentree.pl\t[--debug] <version> <source dir> <dest dir>\n\n";
	exit;
}

my %defs = (
	'LINUX_VERSION_CODE' => $LINUXCODE,
	'BTTV_VERSION_CODE' => $BTTVCODE,
	'_COMPAT_H' => 0,
	'I2C_CLASS_TV_ANALOG' => 1,
	'I2C_CLASS_TV_DIGITAL' => 1,
	'OLD_XMIT_LOCK' => 0,
	'COMPAT_SND_CTL_BOOLEAN_MONO' => 0,
	'NEED_SOUND_DRIVER_H' => 0,
	'TTUSB_KERNEL' => 1,
	'NO_PCM_LOCK' => 0,
	'NEED_ALGO_CONTROL' => 0,
);

#################################################################
# helpers

sub kernel_version($) {
	$_[0] =~ m/(\d+)\.(\d+)\.(\d+)(.*)/;
	return ($1*65536 + $2*256 + $3, $4);
}

# used in eval()
sub KERNEL_VERSION($$$) { return $_[0]*65536 + $_[1]*256 + $_[2]; }

sub evalexp($) {
	local $_ = shift;
	s|/\*.*?\*/||go;	# delete /* */ comments
	s|//.*$||o;		# delete // comments
	s/\bdefined\s*\(/(/go;	# defined(foo) to (foo)
	while (/\b([_A-Za-z]\w*)\b/go) {
		if (exists $defs{$1}) {
			my $id = $1; my $pos = $-[0];
			s/$id/$defs{$id}/;
			pos = $-[0];
		} elsif ($1 ne 'KERNEL_VERSION') {
			return(undef);
		}
	}
	return(eval($_) ? 1 : 0);
}

#################################################################
# filter out version-specific code

sub filter_source ($$) {
	my ($in,$out) = @_;
	my $line;
	my $level=0;
	my %if = ();
	my %state = ();

	my @dbgargs = \($level, %state, %if, $line);
	sub dbgline($\@) {
		my $level = ${$_[1][0]};
		printf STDERR ("/* BP %4d $_[0] state=$_[1][1]->{$level} if=$_[1][2]->{$level} level=$level (${$_[1][3]}) */\n", $.) if $DEBUG;
	}

	open IN, '<', $in or die "Error opening $in: $!\n";
	open OUT, '>', $out or die "Error opening $out: $!\n";

	print STDERR "File: $in, for kernel $VERSION($LINUXCODE)/\n" if $DEBUG;

	while ($line = <IN>) {
		chomp $line;
		next if ($line =~ m/^#include \"compat.h\"/o);
#		next if ($line =~ m/[\$]Id:/);

		# For "#if 0 /*KEEP*/;" the ; should be dropped too
		if ($line =~ m@^\s*#\s*if(n?def)?\s.*?(\s*/\*\s*(?i)keep\s*\*/;?)@) {
			$state{$level} = "ifother";
			$if{$level} = 1;
			dbgline "#if$1 (keep)", @dbgargs;
			$line =~ s/\Q$2\E//;
			$level++;
		}
		# preserve #if 0/#if 1, if $dead_code = 0
		elsif (!$dead_code && $line =~ m@^\s*#\s*if\s*([01])[^\d]@) {
			$state{$level} = "ifother";
			$if{$level} = 1;
			dbgline "#if $1", @dbgargs;
			$level++;
		}
		# handle all ifdef/ifndef lines
		elsif ($line =~ /^\s*#\s*if(n?)def\s*(\w+)/o) {
			if (exists $defs{$2}) {
				$state{$level} = 'if';
				$if{$level} = ($1 eq 'n') ? !$defs{$2} : $defs{$2};
				dbgline "#if$1def $2", @dbgargs;
				$level++;
				next;
			}
			$state{$level} = "ifother";
			$if{$level} = 1;
			dbgline "#if$1def (other)", @dbgargs;
			$level++;
		}
		# handle all ifs
		elsif ($line =~ /^\s*#\s*if\s+(.*)$/o) {
			my $res = evalexp($1);
			if (defined $res) {
				$state{$level} = 'if';
				$if{$level} = $res;
				dbgline '#if '.($res?'(yes)':'(no)'), @dbgargs;
				$level++;
				next;
			} else {
				$state{$level} = 'ifother';
				$if{$level} = 1;
				dbgline '#if (other)', @dbgargs;
				$level++;
			}
		}
		# handle all elifs
		elsif ($line =~ /^\s*#\s*elif\s+(.*)$/o || $line =~ /^\s*#\s*elseif\s+(.*)$/o) {
			my $exp = $1;
			$level--;
			$level < 0 and die "more elifs than ifs";
			$state{$level} =~ /if/ or die "unmatched elif";

			if ($state{$level} eq 'if' && !$if{$level}) {
				my $res = evalexp($exp);
				defined $res or die 'moving from if to ifother';
				$state{$level} = 'if';
				$if{$level} = $res;
				dbgline '#elif1 '.($res?'(yes)':'(no)'), @dbgargs;
				$level++;
				next;
			} elsif ($state{$level} ne 'ifother') {
				$if{$level} = 0;
				$state{$level} = 'elif';
				dbgline '#elif0', @dbgargs;
				$level++;
				next;
			}
			$level++;
		}
		elsif ($line =~ /^\s*#\s*else/o) {
			$level--;
			$level < 0 and die "more elses than ifs";
			$state{$level} =~ /if/ or die "unmatched else";
			$if{$level} = !$if{$level} if ($state{$level} eq 'if');
			$state{$level} =~ s/^if/else/o; # if -> else, ifother -> elseother, elif -> elif
			dbgline '#else', @dbgargs;
			$level++;
			next if $state{$level-1} !~ /other$/o;
		}
		elsif ($line =~ /^\s*#\s*endif/o) {
			$level--;
			$level < 0 and die "more endifs than ifs";
			dbgline '#endif', @dbgargs;
			next if $state{$level} !~ /other$/o;
		}

		my $print = 1;
		for (my $i=0;$i<$level;$i++) {
			next if $state{$i} =~ /other$/o;	# keep code in ifother/elseother blocks
			if (!$if{$i}) {
				$print = 0;
				dbgline 'DEL', @{[\$i, \%state, \%if, \$line]};
				last;
			}
		}
		print OUT "$line\n" if $print;
	}
	close IN;
	close OUT;
}

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

sub parse_dir {
	my $file = $File::Find::name;

	return if ($file =~ /CVS/);
	return if ($file =~ /~$/);

	my $f2 = $file;
	$f2 =~ s/^\Q$SRC\E/$DESTDIR/;

	my $mode = (stat($file))[2];
	if ($mode & S_IFDIR) {
		print("mkdir -p '$f2'\n");
		system("mkdir -p '$f2'");  # should check for error
		return;
	}
	print "from $file to $f2\n";

	if ($file =~ m/.*\.[ch]$/) {
		filter_source($file, $f2);
	} else {
		system("cp $file $f2");
	}
}


# main

printf "kernel is %s (0x%x)\n",$VERSION,$LINUXCODE;

# remove any trailing slashes from dir names.  don't pass in just '/'
$SRC =~ s|/*$||; $DESTDIR =~ s|/*$||;

print "finding files at $SRC\n";

find({wanted => \&parse_dir, no_chdir => 1}, $SRC);
