#!/usr/bin/perl -W

# /*******************************************************************************/
# /*                                                                             */
# /*  Copyright 2004 Pascal Gloor                                                */
# /*                                                                             */
# /*  Licensed under the Apache License, Version 2.0 (the "License");            */
# /*  you may not use this file except in compliance with the License.           */
# /*  You may obtain a copy of the License at                                    */
# /*                                                                             */
# /*     http://www.apache.org/licenses/LICENSE-2.0                              */
# /*                                                                             */
# /*  Unless required by applicable law or agreed to in writing, software        */
# /*  distributed under the License is distributed on an "AS IS" BASIS,          */
# /*  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   */
# /*  See the License for the specific language governing permissions and        */
# /*  limitations under the License.                                             */
# /*                                                                             */
# /*******************************************************************************/

# Author  : Pascal Gloor <pascal.gloor@spale.com>
# Version : 1.0
# Date    : 01.04.2005

use strict;
require GraphViz;

# default include paths
my @incpaths = (
	'/usr/include',
	'/usr/local/include',
);

my %nodecolor = (
	'start'    => '#0000FF',
	'notfound' => '#FF0000',
	'default'  => '#000000',
);

my @depthcolor = (
	'#33CC00',
	'#CCCC00',
	'#CC3300',
);


printf("GHL -- Graphical C Header Recursive Lookup v1.0 by Pascal Gloor\n\n");

# global vars
my ($depth,$out,%node,%edge);

parse_args();
recursive_search();
gen_image();

# search function
sub recursive_search
{
	for(my $level = 0; $level < $depth; $level++)
	{
		my $file1;
		foreach $file1 ( keys %node )
		{
			# do not redo already searched files
			if ( exists $node{$file1}{done} )
			{ next; }

			my $file2;
			foreach $file2 ( parse_header($file1) )
			{
				# not found
				if ( $file2 eq '-1' )
				{ $node{$file1}{color} = $nodecolor{notfound}; next; }

				# default color
				$edge{$file1}{$file2} = "#999999";

				# set edge color
				if ( defined $depthcolor[$level] )
				{ $edge{$file1}{$file2} = $depthcolor[$level]; }

				# add node if not yet defined
				if ( !defined $node{$file2} )
				{ $node{$file2}{color} = $nodecolor{default}; }
			}
			# flag node as done
			$node{$file1}{done} = 1;
		}
	}
}

sub gen_image
{
	# create image
	my $img = GraphViz->new(
		rankdir => 0,
		bgcolor => '#EEEEEE',
		node =>
		{
			shape    => 'diamond',
			fontsize => 10,
			fontname => 'arial',
			height   => 0.2,
		},
	);

	# create all nodes
	my $file1;
	foreach $file1 (keys %node)
	{
		$node{$file1}{id} = $img->add_node($file1, fontcolor => "$node{$file1}{color}");
	}

	# create all links between nodes
	foreach $file1 (keys %node)
	{
		my $file2;
		foreach $file2 (keys %{$edge{$file1}})
		{
			my $dir = 'forward';
			if ( defined $edge{$file2}{$file1} )
			{
				$dir = 'both';
				$edge{$file1}{$file2} = "#FF0000";
				delete $edge{$file2}{$file1};
			}
			$img->add_edge(
				$node{$file1}{id} => $node{$file2}{id},
				color => $edge{$file1}{$file2},
				dir => $dir,
			);
		}
	}

	# write image
	open(IMG,">$out") || print STDERR sprintf("error: could not write to '%s'.\n",$out);
	print IMG $img->as_png;
	close(IMG);

	printf("\n");
	printf("File saved at '%s'.\n",$out);
}



# .c/.h files parser
sub parse_header
{
	my($file) = @_;
	my ($fh);
	my @inc;

	# try to find where that file could be

	open($fh,"$file") || undef $fh;

	if ( !defined $fh )
	{
		foreach(@incpaths)
		{
			open($fh,"$_/$file") || undef $fh;

			if ( defined $fh )
			{ last; }
		}
	}

	# not found
	if ( !defined $fh )
	{
		push(@inc,-1);
		return @inc;
	}

	# parsing
	while(<$fh>)
	{
		s/(\r|\n)$//;

		# yet we match #include <file.h> and #include "file.h"
		if ( /^#include (<|")((\w|\.|\/)+)(>|")/ )
		{ push(@inc,$2); }
	}

	close($fh);

	return @inc;
}

# cmd line arg checker
sub parse_args
{
	my $arg = 0;
	my $larg;

	while(defined $ARGV[$arg])
	{
		if ( $arg == 0 )
		{ $out = $ARGV[$arg]; }
		elsif ( $arg == 1 )
		{ $depth = $ARGV[$arg]; }
		elsif ( $ARGV[$arg] eq '-I' )
		{ $larg = '-I'; }
		elsif ( defined $larg && $larg eq '-I' )
		{ push(@incpaths,$ARGV[$arg]); undef $larg; }
		else
		{ $node{$ARGV[$arg]}{color} = $nodecolor{start}; }
		$arg++;
	}

	if ( $arg < 3 )
	{
		print STDERR sprintf("usage: %s <output> <depth> [-I <include>] <source> [source] ...\n",$0);
		print STDERR sprintf("\n");
		print STDERR sprintf("  output         : where to write the png file.\n");
		print STDERR sprintf("         example : /usr/local/apache/htdocs/ghl.png\n");
		print STDERR sprintf("  depth          : number of search recursion.\n");
		print STDERR sprintf("         example : 5\n");
		print STDERR sprintf("  -I<include>    : include paths to add. may be specified multiple \n");
		print STDERR sprintf("                   times. /usr/include and /usr/local/include are\n");
		print STDERR sprintf("                   set by default.\n");
		print STDERR sprintf("         example : -I /home/ghl/include\n");
		print STDERR sprintf("  source         : C code or header file(s).\n");
		print STDERR sprintf("         example : src/myprog.c inc/myprog.h\n");
		print STDERR sprintf("\n");
		exit(0);
	}

	printf("Include paths:\n");
	foreach(@incpaths) { printf("\t%s\n",$_); }

	printf("\n");
	printf("Header/Source file(s):\n");
	foreach(keys %node) { printf("\r%s\n",$_); }

}
