
# Copyright (c) 2021-2022, PostgreSQL Global Development Group

package Project;

#
# Package that encapsulates a Visual C++ project file generation
#
# src/tools/msvc/Project.pm
#
use Carp;
use strict;
use warnings;
use File::Basename;

sub _new
{
	my ($classname, $name, $type, $solution) = @_;
	my $good_types = {
		lib => 1,
		exe => 1,
		dll => 1,
	};
	confess("Bad project type: $type\n") unless exists $good_types->{$type};
	my $self = {
		name                  => $name,
		type                  => $type,
		guid                  => $^O eq "MSWin32" ? Win32::GuidGen() : 'FAKE',
		files                 => {},
		references            => [],
		libraries             => [],
		suffixlib             => [],
		includes              => [],
		prefixincludes        => '',
		defines               => ';',
		solution              => $solution,
		disablewarnings       => '4018;4244;4273;4101;4102;4090;4267',
		disablelinkerwarnings => '',
		platform              => $solution->{platform},
	};

	bless($self, $classname);
	return $self;
}

sub AddFile
{
	my ($self, $filename) = @_;

	$self->FindAndAddAdditionalFiles($filename);
	$self->{files}->{$filename} = 1;
	return;
}

sub AddDependantFiles
{
	my ($self, $filename) = @_;

	$self->FindAndAddAdditionalFiles($filename);
	return;
}

sub AddFiles
{
	my $self = shift;
	my $dir  = shift;

	while (my $f = shift)
	{
		$self->AddFile($dir . "/" . $f, 1);
	}
	return;
}

# Handle Makefile rules by searching for other files which exist with the same
# name but a different file extension and add those files too.
sub FindAndAddAdditionalFiles
{
	my $self  = shift;
	my $fname = shift;
	$fname =~ /(.*)(\.[^.]+)$/;
	my $filenoext = $1;
	my $fileext   = $2;

	# For .c files, check if either a .l or .y file of the same name
	# exists and add that too.
	if ($fileext eq ".c")
	{
		my $file = $filenoext . ".l";
		if (-e $file)
		{
			$self->AddFile($file);
		}

		$file = $filenoext . ".y";
		if (-e $file)
		{
			$self->AddFile($file);
		}
	}
}

sub ReplaceFile
{
	my ($self, $filename, $newname) = @_;
	my $re = "\\/$filename\$";

	foreach my $file (keys %{ $self->{files} })
	{

		# Match complete filename
		if ($filename =~ m!/!)
		{
			if ($file eq $filename)
			{
				delete $self->{files}{$file};
				$self->AddFile($newname);
				return;
			}
		}
		elsif ($file =~ m/($re)/)
		{
			delete $self->{files}{$file};
			$self->AddFile("$newname/$filename");
			return;
		}
	}
	confess("Could not find file $filename to replace\n");
}

sub RemoveFile
{
	my ($self, $filename) = @_;
	my $orig = scalar keys %{ $self->{files} };
	delete $self->{files}->{$filename};
	if ($orig > scalar keys %{ $self->{files} })
	{
		return;
	}
	confess("Could not find file $filename to remove\n");
}

sub RelocateFiles
{
	my ($self, $targetdir, $proc) = @_;
	foreach my $f (keys %{ $self->{files} })
	{
		my $r = &$proc($f);
		if ($r)
		{
			$self->RemoveFile($f);
			$self->AddFile($targetdir . '/' . basename($f));
		}
	}
	return;
}

sub AddReference
{
	my $self = shift;

	while (my $ref = shift)
	{
		if (!grep { $_ eq $ref } @{ $self->{references} })
		{
			push @{ $self->{references} }, $ref;
		}
		$self->AddLibrary(
			"__CFGNAME__/" . $ref->{name} . "/" . $ref->{name} . ".lib");
	}
	return;
}

sub AddLibrary
{
	my ($self, $lib, $dbgsuffix) = @_;

	# quote lib name if it has spaces and isn't already quoted
	if ($lib =~ m/\s/ && $lib !~ m/^[&]quot;/)
	{
		$lib = '&quot;' . $lib . "&quot;";
	}

	if (!grep { $_ eq $lib } @{ $self->{libraries} })
	{
		push @{ $self->{libraries} }, $lib;
	}

	if ($dbgsuffix)
	{
		push @{ $self->{suffixlib} }, $lib;
	}
	return;
}

sub AddIncludeDir
{
	my ($self, $incstr) = @_;

	foreach my $inc (split(/;/, $incstr))
	{
		if (!grep { $_ eq $inc } @{ $self->{includes} })
		{
			push @{ $self->{includes} }, $inc;
		}
	}
	return;
}

sub AddPrefixInclude
{
	my ($self, $inc) = @_;

	$self->{prefixincludes} = $inc . ';' . $self->{prefixincludes};
	return;
}

sub AddDefine
{
	my ($self, $def) = @_;

	$def =~ s/"/&quot;&quot;/g;
	$self->{defines} .= $def . ';';
	return;
}

sub FullExportDLL
{
	my ($self, $libname) = @_;

	$self->{builddef} = 1;
	$self->{def}      = "./__CFGNAME__/$self->{name}/$self->{name}.def";
	$self->{implib}   = "__CFGNAME__/$self->{name}/$libname";
	return;
}

sub UseDef
{
	my ($self, $def) = @_;

	$self->{def} = $def;
	return;
}

sub AddDir
{
	my ($self, $reldir) = @_;
	my $mf = read_makefile($reldir);

	$mf =~ s{\\\r?\n}{}g;
	if ($mf =~ m{^(?:SUB)?DIRS[^=]*=\s*(.*)$}mg)
	{
		foreach my $subdir (split /\s+/, $1)
		{
			next
			  if $subdir eq "\$(top_builddir)/src/timezone"
			  ;    #special case for non-standard include
			next
			  if $reldir . "/" . $subdir eq "src/backend/port/darwin";

			$self->AddDir($reldir . "/" . $subdir);
		}
	}
	while ($mf =~ m{^(?:EXTRA_)?OBJS[^=]*=\s*(.*)$}m)
	{
		my $s         = $1;
		my $filter_re = qr{\$\(filter ([^,]+),\s+\$\(([^\)]+)\)\)};
		while ($s =~ /$filter_re/)
		{

			# Process $(filter a b c, $(VAR)) expressions
			my $list   = $1;
			my $filter = $2;
			$list =~ s/\.o/\.c/g;
			my @pieces = split /\s+/, $list;
			my $matches = "";
			foreach my $p (@pieces)
			{

				if ($filter eq "LIBOBJS")
				{
					no warnings qw(once);
					if (grep(/$p/, @main::pgportfiles) == 1)
					{
						$p =~ s/\.c/\.o/;
						$matches .= $p . " ";
					}
				}
				else
				{
					confess "Unknown filter $filter\n";
				}
			}
			$s =~ s/$filter_re/$matches/;
		}
		foreach my $f (split /\s+/, $s)
		{
			next if $f =~ /^\s*$/;
			next if $f eq "\\";
			next if $f =~ /\/SUBSYS.o$/;
			$f =~ s/,$//
			  ;    # Remove trailing comma that can show up from filter stuff
			next unless $f =~ /.*\.o$/;
			$f =~ s/\.o$/\.c/;
			if ($f =~ /^\$\(top_builddir\)\/(.*)/)
			{
				$f = $1;
				$self->AddFile($f);
			}
			else
			{
				$self->AddFile("$reldir/$f");
			}
		}
		$mf =~ s{OBJS[^=]*=\s*(.*)$}{}m;
	}

	# Match rules that pull in source files from different directories, eg
	# pgstrcasecmp.c rint.c snprintf.c: % : $(top_srcdir)/src/port/%
	my $replace_re =
	  qr{^([^:\n\$]+\.c)\s*:\s*(?:%\s*: )?\$(\([^\)]+\))\/(.*)\/[^\/]+\n}m;
	while ($mf =~ m{$replace_re}m)
	{
		my $match  = $1;
		my $top    = $2;
		my $target = $3;
		my @pieces = split /\s+/, $match;
		foreach my $fn (@pieces)
		{
			if ($top eq "(top_srcdir)")
			{
				eval { $self->ReplaceFile($fn, $target) };
			}
			elsif ($top eq "(backend_src)")
			{
				eval { $self->ReplaceFile($fn, "src/backend/$target") };
			}
			else
			{
				confess "Bad replacement top: $top, on line $_\n";
			}
		}
		$mf =~ s{$replace_re}{}m;
	}

	$self->AddDirResourceFile($reldir);
	return;
}

# If the directory's Makefile bears a description string, add a resource file.
sub AddDirResourceFile
{
	my ($self, $reldir) = @_;
	my $mf = read_makefile($reldir);

	if ($mf =~ /^PGFILEDESC\s*=\s*\"([^\"]+)\"/m)
	{
		my $desc = $1;
		my $ico;
		if ($mf =~ /^PGAPPICON\s*=\s*(.*)$/m) { $ico = $1; }
		$self->AddResourceFile($reldir, $desc, $ico);
	}
	return;
}

sub AddResourceFile
{
	my ($self, $dir, $desc, $ico) = @_;

	my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =
	  localtime(time);
	my $d = sprintf("%02d%03d", ($year - 100), $yday);

	if (Solution::IsNewer("$dir/win32ver.rc", 'src/port/win32ver.rc'))
	{
		print "Generating win32ver.rc for $dir\n";
		open(my $i, '<', 'src/port/win32ver.rc')
		  || confess "Could not open win32ver.rc";
		open(my $o, '>', "$dir/win32ver.rc")
		  || confess "Could not write win32ver.rc";
		my $icostr = $ico ? "IDI_ICON ICON \"src/port/$ico.ico\"" : "";
		while (<$i>)
		{
			s/FILEDESC/"$desc"/gm;
			s/_ICO_/$icostr/gm;
			s/(VERSION.*),0/$1,$d/;
			if ($self->{type} eq "dll")
			{
				s/VFT_APP/VFT_DLL/gm;
				my $name = $self->{name};
				s/_INTERNAL_NAME_/"$name"/;
				s/_ORIGINAL_NAME_/"$name.dll"/;
			}
			else
			{
				/_INTERNAL_NAME_/ && next;
				/_ORIGINAL_NAME_/ && next;
			}
			print $o $_;
		}
		close($o);
		close($i);
	}
	$self->AddFile("$dir/win32ver.rc");
	return;
}

sub DisableLinkerWarnings
{
	my ($self, $warnings) = @_;

	$self->{disablelinkerwarnings} .= ','
	  unless ($self->{disablelinkerwarnings} eq '');
	$self->{disablelinkerwarnings} .= $warnings;
	return;
}

sub Save
{
	my ($self) = @_;

	# If doing DLL and haven't specified a DEF file, do a full export of all symbols
	# in the project.
	if ($self->{type} eq "dll" && !$self->{def})
	{
		$self->FullExportDLL($self->{name} . ".lib");
	}

	# Warning 4197 is about double exporting, disable this per
	# http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=99193
	$self->DisableLinkerWarnings('4197') if ($self->{platform} eq 'x64');

	# Dump the project
	open(my $f, '>', "$self->{name}$self->{filenameExtension}")
	  || croak(
		"Could not write to $self->{name}$self->{filenameExtension}\n");
	$self->WriteHeader($f);
	$self->WriteFiles($f);
	$self->Footer($f);
	close($f);
	return;
}

sub GetAdditionalLinkerDependencies
{
	my ($self, $cfgname, $separator) = @_;
	my $libcfg = (uc $cfgname eq "RELEASE") ? "MD" : "MDd";
	my $libs = '';
	foreach my $lib (@{ $self->{libraries} })
	{
		my $xlib = $lib;
		foreach my $slib (@{ $self->{suffixlib} })
		{
			if ($slib eq $lib)
			{
				$xlib =~ s/\.lib$/$libcfg.lib/;
				last;
			}
		}
		$libs .= $xlib . $separator;
	}
	$libs =~ s/.$//;
	$libs =~ s/__CFGNAME__/$cfgname/g;
	return $libs;
}

# Utility function that loads a complete file
sub read_file
{
	my $filename = shift;
	my $F;
	local $/ = undef;
	open($F, '<', $filename) || croak "Could not open file $filename\n";
	my $txt = <$F>;
	close($F);

	return $txt;
}

sub read_makefile
{
	my $reldir = shift;
	my $F;
	local $/ = undef;
	open($F, '<', "$reldir/GNUmakefile")
	  || open($F, '<', "$reldir/Makefile")
	  || confess "Could not open $reldir/Makefile\n";
	my $txt = <$F>;
	close($F);

	return $txt;
}

1;
