#!/usr/bin/env perl
#
# Copyright (C) 2001-2022 Graeme Walker <graeme_walker@users.sourceforge.net>
# 
# 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 3 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, see <http://www.gnu.org/licenses/>.
# ===
#
# make2unity
#
# Generates "unity build" source files by parsing autoconf/automake
# artifacts throughout the source tree. Also optionally creates a
# compilation database ("compile_commands.json") for running
# clang-tidy.
#
# usage:
#   make2unity --out=<output> [options] <program>
#   make2unity [options] [<program> [<program> ...]]
#      --base=<dir>            -- base directory for makefile search
#      --config-status=<file>  -- path of config.status file
#      --out=<file>            -- output source file (if one <program>)
#      --cdb                   -- create a compilation database
#      --cdb-top=<dir>         -- top_srcdir (needed if --cdb)
#      --cdb-cxx=<exe>         -- compiler (needed if --cdb)
#
# The <program> arguments are allowed to have a ".cpp" suffix.
#
# Eg:
#     $ cd src
#     $ ../bin/make2unity emailrelay.cpp
#     $ g++ -pthread -I.... -c emailrelay.cpp
#     $ g++ -pthread -o emailrelay emailrelay.o -lpam -lssl -lcrypto
#

use strict ;
use warnings ;
use FileHandle ;
use File::Basename ;
use Getopt::Long ;
use Data::Dumper ;
use lib dirname($0) ;
use CompilationDatabase ;
use ConfigStatus ;
use AutoMakeParser ;
$AutoMakeParser::debug = 0 ;

my %opt = () ;
GetOptions( \%opt , "out=s" , "cdb" , "cdb-cxx=s" , "cdb-top=s" , "base=s" , "config-status=s" ) or die "make2unity: usage error" ;
die "make2unity: usage error" if scalar(@ARGV) == 0 ;
die "make2unity: usage error" if ( $opt{out} && scalar(@ARGV) != 1 ) ;

my $cfg_out = $opt{out} ;
my $cfg_cdb = exists $opt{cdb} ;
my $cfg_top_srcdir = $opt{'cdb-top'} ;
my $cfg_base_dir = exists $opt{base} ? $opt{base} : File::Basename::dirname($0)."/../src" ;
my $cfg_config_status = $opt{'config-status'} ;
my @cfg_programs = @ARGV ;
push @cfg_programs , "emailrelay" if !@cfg_programs ;
my $cfg_cxx = $opt{'cdb-cxx'} || "/usr/bin/c++" ;

my $cs = new ConfigStatus( $cfg_config_status ) ;
my %switches = $cs->switches() ;
my %vars = $cs->vars() ;

$vars{top_srcdir} = "." ;
$vars{top_builddir} = "." ;

my @makefiles = AutoMakeParser::readall( $cfg_base_dir , \%switches , \%vars ) ;

my $fh_cdb ;
if( $cfg_cdb )
{
	$fh_cdb = new FileHandle( "compile_commands.json" , "w" ) or die ;
	print $fh_cdb "[\n" ;
}

for my $cfg_program ( @cfg_programs )
{
	my $program = File::Basename::basename( $cfg_program , ".cpp" ) ;
	my $out = $cfg_out ? $cfg_out : "${program}.cpp" ;

	my $fh_out = new FileHandle( $out , "w" ) or die ;
	print $fh_out "/* autogenerated by make2unity */\n" ;
	my $stanza = undef ;
	my %libs = () ;
	my @out_lines = () ;
	for my $m ( @makefiles )
	{
		my $dir = File::Basename::dirname( $m->path() ) ;
		for my $p ( $m->programs() )
		{
			if( $p eq $program || ($p eq "$program.real") )
			{
				map { $libs{"lib".$_.".a"} = 1 } $m->our_libs( $p ) ;
				push @out_lines , "/* exe [$dir] */\n" ;
				print $fh_out "/* c++ -pthread".join(" -I ../",("",$m->includes("",0,0)))." -o $p $p.cpp ".join(" -l",("",$m->sys_libs($p)))." */\n" ;
				for my $src ( $m->sources($p) )
				{
					push @out_lines , "#include \"$src\"\n" ;
				}
				$stanza = stanza( $program , $m ) ;
			}
		}
	}
	for my $m ( @makefiles )
	{
		my $dir = File::Basename::dirname( $m->path() ) ;

		print $fh_out "/* lib [$dir] */\n" ;
		for my $library ( $m->libraries() )
		{
			if( exists($libs{$library}) ) # ignore this library if not linked in to $program
			{
				for my $src ( $m->sources($library) )
				{
					print $fh_out "#include \"$src\"\n" ;
				}
			}
			else
			{
				print $fh_out "/* (not linked) */\n" ;
			}
		}
	}
	print $fh_out @out_lines ; # (after all the library sources)
	$fh_out->close() or die ;
	print $fh_cdb $stanza , "\n" if ( $fh_cdb && defined($stanza) ) ;
}

if( $fh_cdb )
{
	print $fh_cdb "]\n" ;
	$fh_cdb->close() or die ;
}

sub stanza
{
	my ( $program , $m ) = @_ ;

	my $dir = cwd() ;
	my $src = "$program.cpp" ;
	my $autoconf_dir = "../src" ;
	my $program_dir = File::Basename::dirname( $m->path() ) ;
	my $moc_dir = "." ;
	my @includes = ( $autoconf_dir , $program_dir , $moc_dir , $m->includes($cfg_top_srcdir,0,1) ) ;
	my $includes = join( " -I" , ("",@includes) ) ;
	my $options = $m->compile_options() ;
	my $cmd = "$cfg_cxx $options $includes -c $program.cpp" ;

	my $s = '{
		 "directory" : "__DIR__" ,
		 "command" : "__CMD__" ,
		 "file" : "__SRC__" ,
		},' ;
	$s =~ s/\t//gm ;
	$s =~ s/__DIR__/$dir/m ;
	$s =~ s/__CMD__/$cmd/m ;
	$s =~ s/__SRC__/$src/m ;
	return $s ;
}

