#*****************************************************************************
#
# StdC.pm - C/C++ Compilation/Linking Rules for autodep
# (c) 1997-98 Kriang Lerdsuwanakij
# License: GNU General Public License
#
#	This program is free software; you can redistribute it and/or modify
#	it under the terms of the GNU General Public License as published by
#	the Free Software Foundation; either version 2 of the License, or
#	(at your option) any later version.
#
#	This program is distributed in the hope that it will be useful,
#	but WITHOUT ANY WARRANTY; without even the implied warranty of
#	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#	GNU General Public License for more details.
#
#	You should have received a copy of the GNU General Public License
#	along with this program; if not, write to the Free Software
#	Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
# History:
#	For later history, see ChangeLog
#
#	2.0.0	(Jul 23, 1998)
#		- Use interface version 2
#
#	1.4.0	(Jul 12, 1998)
#		- Update to use new $::shTarget[...]{source} structure.
#		- Remove references to $::shCurTarget.
#		- Add _CompatibleParam().
#		- Check Makefile in EXCLUDE list when making linker 
#		  dependency rule.
#		- Replace @sourceList check to @objList.
#
#	1.3.0	(Jul 9, 1998)
#		- Don't include sources start with `-' or `$' in the
#		  dependency list.
#		- Modify to use ::indentstring().
#		- Add sourceParam support.
#		- Use $::shProgName instead of $::progName.
#		- Make sure all subroutines uses pass by value.
#		- Use ::pushunique().
#		- Remove *.m processing.  Objective-C is not supported as it
#		  can use other method to include files.
#		- Don't search for include files for preprocessed files and
#		  assembler source.
#		- Quote `.' when finding/replacing file extensions.
#
#	1.2.0	(Jun 24, 1998)
#		- Don't handle sources start with `-' or `$'.
#
#	1.1.0	(Aug 17, 1997)
#		- Fix major bug - not all include was placed in Makefile
#		- Update to reflect change of @::shTarget structure
#
#	1.0.0	(Aug 7, 1997)
#		Changes from non-module code in autodep 0.9.0
#		- Add Cobj and C++obj target types
#		- Accept .c++ as a valid file type
#		- Accept .o, .a, .so and include in linking rules
#		- Improved trigraphs - match examples in December 1996 
#		  C++ draft.
#		- Digraphs supported
#
# Problems:
#	What should be processed first?  Backslash-newline or trigraph.
#
#*****************************************************************************

package StdC;

$pkgName = "StdC";
die "$pkgName: this module must only be called by autodep\n" 
		if ($::shProgName !~ /^autodep/);

# Data shared by all target of type C, Cobj, C++, C++obj

@source = ();			# List of source files and their parameters

@objList = ();

@includepath = (".");

@sourceSorted = ();		# Sorted version of @source with duplicate
				# entries removed
$numSource = 0;			# Number of element of @sourceSorted

@gi_checked = ();		# List of source files, headers checked
				# (Not shared)

$doneCompile = 0;		# Make sure _OutputCompile is called once

$debug = 0;
$debugDump = "";

sub interface {
	return (2, "1.1.0");
}

sub queryoption {
	return ("INCLUDEPATH");
}

sub processoption {
	my @parm = @_;
	my $class = shift @parm;
	my $option = uc(shift @parm);
	if ($option eq "INCLUDEPATH") {
		::pushunique @includepath, (@parm);
	}
}

sub new {
	my @parm = @_;
	my $class = shift @parm;
	my $self = {};
	my ($i, $numSource);

						# Check for DEBUG option 
						# here since new is only
						# called when ad.rule are
						# read.
	$debug = ::isinlist("StdCcpp", @{ $::shParameter{DEBUG} });

	$self->{name} = $parm[0]{name};
	$self->{type} = $parm[0]{type};
	$self->{source} = $parm[0]{source};	# $self shares the same
						# area of memory as $parm[0]
						# OK since we don't modify
						# anything in @shTarget

	$self->{lib} = [];
	$self->{object} = [];

	$numSource = scalar(@{ $self->{source} });

	$! = 1;
	die "$::shProgName: target $self->{name} contains no source files\n" 
		if ($numSource == 0);

					# Store executable name
					# Ignore when type is Cobj and C++obj
	if ($self->{type} eq "C" || $self->{type} eq "C++") {
		push @::shGeneratedFile,$self->{name};
		$::shVariable{adEXECS} .= " " . $self->{name};

		push @{ $parm[0]->{output} }, $self->{name};
		push @{ $parm[0]->{output2} }, { name => $self->{name},
					      param => [],
					      depend => []
					    };
	}

	for ($i = 0; $i < $numSource; $i++) {

		my $file = $self->{source}[$i]{name};
		my $line = $self->{source}[$i]{line};
		my $saveFile = $file;

					# No `.h' here
		if ($file !~ /^-/ &&
		    $file !~ /^\$/ &&
		    $file !~ /\.(c|C|cc|cxx|cpp|c\+\+|i|ii|s|S|o|a|so)$/) {
			$! = 1;
			die "$::shProgName: file with unknown extension at ",
				"line $parm[0]{line}: $file\n";
		}

		if ($file =~ /^-/ || $file =~ /^\$/) {
					# `-xxx' or `$xxx'
					# Library or link time option
					# No duplicate check here as someone
					# may need it.
			push @{ $self->{lib} },$file;
		}
					# No `.h', `.o', `.a', `.so' here
		elsif ($file =~ s/\.(c|C|cc|cxx|cpp|c\+\+|i|ii|s|S)$/\.o/) {

			my $explObj = ::isinlist("-o", 
					@{ $self->{source}[$i]{param} });

					# Explicitly specified object name
			if ($explObj) {
				$file = $self->{source}[$i]{param}[$explObj];
			}

					# Cooperate between C/C++ targets
					# Duplicate objects are removed from
					# @::shGeneratedFile and $(adOBJS)

					# Whether they cause conflict will 
					# be tested later during output

			if (! ::isinlist($file, @objList)) {
				push @objList,$file;

				push @::shGeneratedFile,$file;
				::appendunique $::shVariable{adOBJS},$file;
			}

					# Linkage style

			$ctype = "C" if ($self->{type} eq "C" || 
					 $self->{type} eq "Cobj");
			$ctype = "C++" if ($self->{type} eq "C++" || 
					 $self->{type} eq "C++obj");

					# Save object files
			::pushunique @{ $self->{object} }, $file;

					# All source files for every C/C++
					# target are kept together in
					# @source for error detection

					# output is shared so that
					# ::adddepend works
			push @source, { name => $saveFile,
					type => $ctype,
					realtype => $self->{type},
					param => $self->{source}[$i]{param},
					depend => $self->{source}[$i]{depend},
					output => $self->{source}[$i]{output}
				      };

		}
		else {			# `.o', `.a', or `.so'
			::pushunique @{ $self->{object} }, $file;
		}

		my $output = { name => $file,
			       param => [],
			       depend => []
			     };
		$self->{source}[$i]{output}{name} = $output->{name};
		$self->{source}[$i]{output}{param} = $output->{param};
		$self->{source}[$i]{output}{depend} = $output->{depend};
		if ($self->{type} eq "Cobj" || $self->{type} eq "C++obj") {
			push @{ $parm[0]->{output} }, $file;
			push @{ $parm[0]->{output2} }, $output;
		}
	}

	return bless $self, $class;
}

sub output {
	my @parm = @_;
	my $self = shift @parm;
	my $handle = shift @parm;

				# We will check for conflicts and
				# output all compilation rules
				# for all targets during `output' call for
				# the first target.
				# Source files that appear in several
				# target are emitted to the Makefile only
				# once.
	if (! $doneCompile) {
		$self->_FindConflictType();
		$self->_OutputCompile($handle);
		$doneCompile = 1;

		if ($debug) {
			print $handle "# DEBUG\n$debugDump";
		}
	}

					# Emit link commands for this target
	$self->_OutputLink($handle);
	return 1;
}

sub _CompatibleParam(\@\@$) {
	my $list1 = $_[0];
	my $list2 = $_[1];
	my $file = $_[2];

	my $done = 1;
	my $out1;
	my $out2;
	
	$file =~ s/\.(c|C|cc|cxx|cpp|c\+\+|i|ii|s|S)$/\.o/;

	do {
		$out1 = ::isinlist("-o", @{ $list1 });
		$out2 = ::isinlist("-o", @{ $list2 });
		if ($out1 > 0 && $list1->[$out1] eq $file) {
			splice(@{ $list1 }, $out1-1, 2);
			$out1 = 0;
			$done = 0;
		}
		if ($out2 > 0 && $list2->[$out2] eq $file) {
			splice(@{ $list2 }, $out2-1, 2);
			$out2 = 0;
			$done = 0;
		}
	} while ($done == 0);

	my $str1 = join ' ', @{ $list1 };
	my $str2 = join ' ', @{ $list2 };

	if ($str1 eq $str2) {
		return 1;		# Exact matches
	}

					# We allow parameter difference
					# when a source file is used to
					# produce different object files
					# (via -o option)
	if ($out1 > 0 && $out2 > 0
	    && $list1->[$out1] ne $list2->[$out2]) {
		return 1;
	}
	elsif (($out1 > 0 && $out2 == 0) || ($out1 == 0 && $out2 > 0)) {
		return 1;
	}
	else {
		return 0;
	}
}

sub _FindConflictType {

	my @parm = @_;
	my $self = shift @parm;

	my ($first, $opt);
	my ($s,$j,$k,$l);
					# Let's build another sorted one
	@sourceSorted = sort { $a->{name} cmp $b->{name}
				|| join(' ',$a->{param})
					cmp join(' ',$b->{param}) } @source;

					# Remove duplicate source files
	$numSource = scalar(@sourceSorted);
	for ($i = 1; $i < $numSource; ) {
		if ($sourceSorted[$i]{name} eq $sourceSorted[$i-1]{name}) {

			if ($sourceSorted[$i]{type} eq 
					$sourceSorted[$i-1]{type}) {

				my $str1 = join ' ',
						@{$sourceSorted[$i]{param}};
				my $str2 = join ' ',
						@{$sourceSorted[$i-1]{param}};

				if (_CompatibleParam(
					@{$sourceSorted[$i]{param}},
					@{$sourceSorted[$i-1]{param}},
					$sourceSorted[$i]{name})) {

					if ($str1 eq $str2) {

							# Duplicate sources found

							# Combine dependencies
						if (scalar @{$sourceSorted[$i]{depend}}) {
							::pushunique(@{$sourceSorted[$i-1]{depend}},
								@{$sourceSorted[$i]{depend}});
						}

							# Keep source at $i-1
						splice(@sourceSorted,$i,1);
						$numSource--;
					}
					else {
						$i++;
					}
				}
				else {
					my $str1 = join ' ',
						@{$sourceSorted[$i]{param}};
					my $str2 = join ' ',
						@{$sourceSorted[$i-1]{param}};
					die "$::shProgName: conflict",
					" parameters for source:",
					" $sourceSorted[$i]{name} (`$str1\'",
					" and `$str2\')\n",
				}
			}
			else {
				die "$::shProgName: conflict TARGET type for",
				" source: $sourceSorted[$i]{name} (type",
				" $sourceSorted[$i]{realtype} and",
				" $sourceSorted[$i-1]{realtype})\n";
			}
		}
		else {
			$i++;
		}
	}

	return 1;
}

sub _OutputCompile {

	my @parm = @_;
	my $self = shift @parm;
	my $handle = shift @parm;
	my ($file,$type,$objFile);
	my (@include,$include);

	print $handle "# Compiling C/C++ sources\n\n";
	$numSource = scalar(@sourceSorted);
	for ($i = 0; $i < $numSource; $i++) {

		$file = $sourceSorted[$i]->{name};
		$type = $sourceSorted[$i]->{type};

		print "Processing source: $file\n";

		if ($type eq "C" || $type eq "Cobj") {
			$outCC = "\$(CC)";
			$outCCFLAGS = "\$(CCFLAGS)";
		}
		elsif ($type eq "C++" || $type eq "C++obj") {
			$outCC = "\$(CXX)";
			$outCCFLAGS = "\$(CXXCFLAGS)";
		}
					# Extend target type here
					# ...

					# Ignore the first item which is
					# ".".
		if (scalar @includepath > 1) {
			my $i;
			foreach $i (1 .. scalar(@includepath)-1) {
				$outCCFLAGS .= " -I$includepath[$i]";
			}
		}

		$objFile = $file;
		$objFile =~ s/\.(c|C|cc|cxx|cpp|c\+\+|i|ii|s|S)$/\.o/;

		my $explObj = ::isinlist("-o", 
				@{ $sourceSorted[$i]{param} });

					# Explicitly specified object name
		if ($explObj) {
			$objFile = $sourceSorted[$i]{param}[$explObj];
		}


					# Do not check these files
		@gi_checked = @{ $::shParameter{EXCLUDE} };

		if ($file =~ /\.(c|C|cc|cxx|cpp|c\+\+)/) {
					# Files that needs preprocessing
			@include = getinclude($file);
		}

					# Build dependency rule

		my $str = "";
		if (! ::isinlist("Makefile",@{ $::shParameter{EXCLUDE} })) {
			$str = "Makefile ";
		}

		$str .= join ' ', @{ $sourceSorted[$i]{depend} };
		$str .= " ";

		foreach $include (@include) {

				# Is the include file specified in CURDIR ?
		
			if (::isinlist($include,@{ $::shParameter{CURDIR} })) {
						# Yes
						# Use current dir
				$str .= "$include ";
			}
			else {			# No
						# Use $(srcdir)
				$str .= "\$(srcdir)/$include ";
			}
		}

		my $strOut = ::indentstring("$objFile : ", $str, 0);
		print $handle $strOut;

					# Build compilation command

		$str = "$outCCFLAGS -c ";
				# Is the source file specified in CURDIR ?
		if (! ::isinlist($file,@{ $::shParameter{CURDIR} })) {
							# No
			$str .= "\$(srcdir)/";		# Add $(srcdir)/
		}
		$str .= "$file ";
		$str .= join ' ', @{ $sourceSorted[$i]{param} };
		$strOut = ::indentstring("$outCC ", $str, 1);
		print $handle $strOut,"\n";
	}
}

sub _OutputLink {

	my @parm = @_;
	my $self = shift @parm;
	my $handle = shift @parm;

	if ($self->{type} eq "Cobj") {
		return;			# No linking needed
	}
	elsif ($self->{type} eq "C++obj") {
		return;			# No linking needed
	}

	print "Processing target: $self->{name}\n";

	print $handle "# Linking $self->{name}\n\n";

	if ($self->{type} eq "C") {
		$outCC = "\$(CC)";
		$outCCFLAGS = "\$(CFLAGS)";
	}
	elsif ($self->{type} eq "C++") {
		$outCC = "\$(CXX)";
		$outCCFLAGS = "\$(CXXFLAGS)";
	}
					# Extend target type here
					# ...

					# Linker dependency list
	my $str = "";
	if (! ::isinlist("Makefile",@{ $::shParameter{EXCLUDE} })) {
		$str = "Makefile ";
	}

	$str .= join ' ', @{ $self->{object} };
	my $strOut = ::indentstring("$self->{name} : ", $str, 0);
	print $handle $strOut;

					# Linker command line
	$str = "-o $self->{name} $outCCFLAGS \$(LDFLAGS) ";
	$str .= join ' ', @{ $self->{object} }, @{ $self->{lib} }, "\$(LIBS)";
	$strOut = ::indentstring("$outCC ", $str, 1);
	print $handle $strOut,"\n";
}

#-----------------------------------------------------------------------------
#
# Source for files in @includepath
#
# Syntax: searchfile(FILE)
#         where FILE is the file to be searched
# Return: path found or ""

sub searchfile {
	my @parm = @_;
	my $file = shift @parm;
	my $dir;
	foreach $dir (@includepath) {
		my $file2 = $dir."/".$file;
		if (-f $file2) {
			return $dir;
		}
	}
	return "";
}

#-----------------------------------------------------------------------------
#
# Get a list of include files
#
# Syntax: getinclude(LIST)
#         where LIST is the list of files to be checked
# Return: list of existing files together with their include files
#
# Overwrite: $_, @gi_checked
#

sub getinclude {
	local $nestCount = 0;
	local @include = ();
	getinclude_(@_);
	return sort @include;
}

sub getinclude_ {
	my @parms = @_;
	my ($file, $dir);
	my $include;
	my @recurseFile = ();
	my ($line, $line2);

	$! = 1;	
	die "$::shProgName: illegal call to getinclude_\n" 
		unless defined($nestCount);
	$nestCount++;

ITEM:	foreach $file (@parms) {

					# Check if we have already add this
					# file to include list
		if (::isinlist($file,@gi_checked)) {
			next ITEM;			# Already checked
							# Skip this
		}

		if (::isinlist($file,@{ $::shParameter{CURDIR} })) {
					# File to be generated in current dir
					# May not present now

					# Add this file to checked list
			push(@gi_checked, $file);

					# Add filename to the list
			::pushunique @include, $file;

			next ITEM;
		}

		$dir = ".";
					# Does not warn if file not found!
		if ((-f $file || $nestCount > 1 
				&& ($dir = searchfile($file)) ne "")
		    && open(CHECKFILE, "$dir/$file")) {

			if ($dir ne ".") {
				$file = "$dir/$file";
			}
			$file =~ s[//][/]g;

					# Add this file to checked list
			push(@gi_checked, $file);

					# Add filename to the list
			::pushunique @include, $file;

					# Check file line by line
			while (<CHECKFILE>) {
				chomp;

					# Join lines ended with `\'
				if (s/\\$//) {
					$_ .= <CHECKFILE>;
					redo;		# Goto chomp command
				}

					# Replace trigraphs
				$line = "";
				for ( ; ; ) {
					if (/\?\?/) {
						if (s/^(.*?)\?\?\?//) {
							$line .= $1.'?';
						}
						elsif (s/^(.*?)\?\?\=//) {
							$line .= $1.'#';
						}
						elsif (s/^(.*?)\?\?\///) {
							$line .= $1."\\";
						}
						elsif (s/^(.*?)\?\?\'//) {
							$line .= $1.'^';
						}
						elsif (s/^(.*?)\?\?\(//) {
							$line .= $1.'[';
						}
						elsif (s/^(.*?)\?\?\)//) {
							$line .= $1.']';
						}
						elsif (s/^(.*?)\?\?\!//) {
							$line .= $1.'|';
						}
						elsif (s/^(.*?)\?\?\<//) {
							$line .= $1.'{';
						}
						elsif (s/^(.*?)\?\?\>//) {
							$line .= $1.'}';
						}
						elsif (s/^(.*?)\?\?\-//) {
							$line .= $1.'~';
						}
						else {
							s/^(.*?)\?\?//;
							$line .= $1.'??';
						}
					}
					else {
						$line .= $_;
						last;
					}
				}

				$line2 = $line;
					# Replace digraphs
				$line2 =~ s/\<\%/\{/g;
				$line2 =~ s/\%\>/\}/g;
				$line2 =~ s/\<\:/\[/g;
				$line2 =~ s/\:\>/\]/g;
				$line2 =~ s/\%\:\%\:/\#\#/g;
				$line2 =~ s/\%\:/\#/g;

					# For debugging
				if ($debug) {
					$debugDump .= $line2 . "\n";
				}

					# Look for ` # include "..." '
				if ($line2 =~ /^\s*#\s*include\s*\".+\"/) {
					$line2 =~ s/^\s*#\s*include\s*\"//;
					$include = $line2;
					$include =~ s/\".*//;
					push(@recurseFile, $include);
				}

					# Look for ` # include <...> '
				elsif ($line2 =~ /^\s*#\s*include\s*\<.+\>/) {
					$line2 =~ s/^\s*#\s*include\s*\<//;
					$include = $line2;
					$include =~ s/\>.*//;
					push(@recurseFile, $include);
				}

					# Silently skip computed includes

			}
			close(CHECKFILE);
		}
		else {			# Cannot open file

			if ($nestCount == 1 && $file eq $parms[0]) {

					# For the first item of top-level
					# call to this function, i.e. source
					# files specified in TARGET commands

				die "$::shProgName: cannot open $file: $!\n";
			}

					# Otherwise, it may be system
					# header files.  Ignore it
		}
					# Look recursively for files
					# included in include files
		getinclude_(@recurseFile);
	}

	$nestCount--;

	return;
}

1;