#!/usr/bin/perl

use strict;
use warnings;
use 5.008001;

use Cwd qw(abs_path getcwd);
use File::Find;
use File::Spec qw(devnull);
use File::Temp;
use IO::Handle;
use Getopt::Long;

# Update for pg_bsd_indent version
my $INDENT_VERSION = "1.2";
my $devnull        = File::Spec->devnull;

# Common indent settings
my $indent_opts =
  "-bad -bap -bc -bl -d0 -cdb -nce -nfc1 -di12 -i4 -l79 -lp -nip -npro -bbb";

# indent-dependant settings
my $extra_opts = "";

my ($typedefs_file, $code_base, $excludes, $indent, $build);

my %options = (
	"typedefs=s"  => \$typedefs_file,
	"code-base=s" => \$code_base,
	"excludes=s"  => \$excludes,
	"indent=s"    => \$indent,
	"build"       => \$build,);
GetOptions(%options) || die "bad command line";

run_build($code_base) if ($build);

# command line option wins, then first non-option arg,
# then environment (which is how --build sets it) ,
# then locations. based on current dir, then default location
$typedefs_file ||= shift if @ARGV && $ARGV[0] !~ /\.[ch]$/;
$typedefs_file ||= $ENV{PGTYPEDEFS};

# build mode sets PGINDENT and PGENTAB
$indent ||= $ENV{PGINDENT} || $ENV{INDENT} || "pg_bsd_indent";
my $entab = $ENV{PGENTAB} || "entab";

# no non-option arguments given. so do everything in the current directory
$code_base ||= '.' unless @ARGV;

# if it's the base of a postgres tree, we will exclude the files
# postgres wants excluded
$excludes ||= "$code_base/src/tools/pgindent/exclude_file_patterns"
  if $code_base && -f "$code_base/src/tools/pgindent/exclude_file_patterns";

# globals
my @files;
my $filtered_typedefs_fh;


sub check_indent
{
	system("entab < $devnull");
	if ($?)
	{
		print STDERR
"Go to the src/tools/entab directory and do 'make' and 'make install'.\n",
		  "This will put the 'entab' command in your path.\n",
		  "Then run $0 again.\n";
		exit 1;
	}

	system("$indent -? < $devnull > $devnull 2>&1");
	if ($? >> 8 != 1)
	{
		print STDERR
		  "You do not appear to have 'indent' installed on your system.\n";
		exit 1;
	}

	if (`$indent -V` !~ m/ $INDENT_VERSION$/)
	{
		print STDERR
"You do not appear to have $indent version $INDENT_VERSION installed on your system.\n";
		exit 1;
	}

	system("$indent -gnu < $devnull > $devnull 2>&1");
	if ($? == 0)
	{
		print STDERR
		  "You appear to have GNU indent rather than BSD indent.\n",
		  "See the pgindent/README file for a description of its problems.\n";
		$extra_opts = "-cdb -bli0 -npcs -cli4 -sc";
	}
	else
	{
		$extra_opts = "-cli1";
	}
}


sub load_typedefs
{

	# try fairly hard to find the typedefs file if it's not set

	foreach my $try ('.', 'src/tools/pgindent', '/usr/local/etc')
	{
		$typedefs_file ||= "$try/typedefs.list"
		  if (-f "$try/typedefs.list");
	}

	# try to find typedefs by moving up directory levels
	my $tdtry = "..";
	foreach (1 .. 5)
	{
		$typedefs_file ||= "$tdtry/src/tools/pgindent/typedefs.list"
		  if (-f "$tdtry/src/tools/pgindent/typedefs.list");
		$tdtry = "$tdtry/..";
	}
	die "no typedefs file" unless $typedefs_file && -f $typedefs_file;

	open(my $typedefs_fh, '<', $typedefs_file)
	  || die "opening $typedefs_file: $!";
	my @typedefs = <$typedefs_fh>;
	close($typedefs_fh);

	# remove certain entries
	@typedefs =
	  grep { !m/^(FD_SET|date|interval|timestamp|ANY)\n?$/ } @typedefs;

	# write filtered typedefs
	my $filter_typedefs_fh = new File::Temp(TEMPLATE => "pgtypedefXXXXX");
	print $filter_typedefs_fh @typedefs;
	$filter_typedefs_fh->close();

	# temp file remains because we return a file handle reference
	return $filter_typedefs_fh;
}


sub process_exclude
{
	if ($excludes && @files)
	{
		open(my $eh, '<', $excludes) || die "opening $excludes";
		while (my $line = <$eh>)
		{
			chomp $line;
			my $rgx;
			eval " \$rgx = qr!$line!;";
			@files = grep { $_ !~ /$rgx/ } @files if $rgx;
		}
		close($eh);
	}
}


sub read_source
{
	my $source_filename = shift;
	my $source;

	open(my $src_fd, '<', $source_filename)
	  || die "opening $source_filename: $!";
	local ($/) = undef;
	$source = <$src_fd>;
	close($src_fd);

	return $source;
}


sub write_source
{
	my $source          = shift;
	my $source_filename = shift;

	open(my $src_fh, '>', $source_filename)
	  || die "opening $source_filename: $!";
	print $src_fh $source;
	close($src_fh);
}


sub pre_indent
{
	my $source = shift;

	# remove trailing whitespace
	$source =~ s/[ \t]+$//gm;

	## Comments

	# Convert // comments to /* */
	$source =~ s!^([ \t]*)//(.*)$!$1/* $2 */!gm;

	# 'else' followed by a single-line comment, followed by
	# a brace on the next line confuses BSD indent, so we push
	# the comment down to the next line, then later pull it
	# back up again.  Add space before _PGMV or indent will add
	# it for us.
	# AMD: A symptom of not getting this right is that you see errors like:
	# FILE: ../../../src/backend/rewrite/rewriteHandler.c
	# Error@2259:
	# Stuff missing from end of file
	$source =~ s!(\}|[ \t])else[ \t]*(/\*)(.*\*/)[ \t]*$!$1else\n    $2 _PGMV$3!gm;

	# Indent multi-line after-'else' comment so BSD indent will move it
	# properly. We already moved down single-line comments above.
	# Check for '*' to make sure we are not in a single-line comment that
	# has other text on the line.
	$source =~ s!(\}|[ \t])else[ \t]*(/\*[^*]*)[ \t]*$!$1else\n    $2!gm;

	# Mark some comments for special treatment later
	$source =~ s!/\* +---!/*---X_X!g;

	## Other

	# Work around bug where function that defines no local variables
	# misindents switch() case lines and line after #else.  Do not do
	# for struct/enum.
	my @srclines = split(/\n/, $source);
	foreach my $lno (1 .. $#srclines)
	{
		my $l2 = $srclines[$lno];

		# Line is only a single open brace in column 0
		next unless $l2 =~ /^\{[ \t]*$/;

		# previous line has a closing paren
		next unless $srclines[ $lno - 1 ] =~ /\)/;

		# previous line was struct, etc.
		next
		  if $srclines[ $lno - 1 ] =~
			  m!=|^(struct|enum|[ \t]*typedef|extern[ \t]+"C")!;

		$srclines[$lno] = "$l2\nint pgindent_func_no_var_fix;";
	}
	$source = join("\n", @srclines) . "\n";    # make sure there's a final \n

	# Prevent indenting of code in 'extern "C"' blocks.
	# we replace the braces with comments which we'll reverse later
	my $extern_c_start = '/* Open extern "C" */';
	my $extern_c_stop  = '/* Close extern "C" */';
	$source =~
s!(^#ifdef[ \t]+__cplusplus.*\nextern[ \t]+"C"[ \t]*\n)\{[ \t]*$!$1$extern_c_start!gm;
	$source =~ s!(^#ifdef[ \t]+__cplusplus.*\n)\}[ \t]*$!$1$extern_c_stop!gm;

	return $source;
}


sub post_indent
{
	my $source          = shift;
	my $source_filename = shift;

	# put back braces for extern "C"
	$source =~ s!^/\* Open extern "C" \*/$!{!gm;
	$source =~ s!^/\* Close extern "C" \*/$!}!gm;

	## Comments

	# remove special comment marker
	$source =~ s!/\*---X_X!/* ---!g;

	# Pull up single-line comment after 'else' that was pulled down above
	$source =~ s!else\n[ \t]+/\* _PGMV!else\t/*!g;

	# Indent single-line after-'else' comment by only one tab.
	$source =~ s!(\}|[ \t])else[ \t]+(/\*.*\*/)[ \t]*$!$1else\t$2!gm;

	# Add tab before comments with no whitespace before them (on a tab stop)
	$source =~ s!(\S)(/\*.*\*/)$!$1\t$2!gm;

	# Remove blank line between opening brace and block comment.
	$source =~ s!(\t*\{\n)\n([ \t]+/\*)$!$1$2!gm;

	# cpp conditionals

	# Reduce whitespace between #endif and comments to one tab
	$source =~ s!^\#endif[ \t]+/\*!#endif   /*!gm;

	# Remove blank line(s) before #else, #elif, and #endif
	$source =~ s!\n\n+(\#else|\#elif|\#endif)!\n$1!g;

	# Add blank line before #endif if it is the last line in the file
	$source =~ s!\n(#endif.*)\n\z!\n\n$1\n!;

	## Functions

	# Work around misindenting of function with no variables defined.
	$source =~ s!^[ \t]*int[ \t]+pgindent_func_no_var_fix;[ \t]*\n{1,2}!!gm;

	# Use a single space before '*' in function return types
	$source =~ s!^([A-Za-z_]\S*)[ \t]+\*$!$1 *!gm;

	#  Move prototype names to the same line as return type.  Useful
	# for ctags.  Indent should do this, but it does not.  It formats
	# prototypes just like real functions.

	my $ident   = qr/[a-zA-Z_][a-zA-Z_0-9]*/;
	my $comment = qr!/\*.*\*/!;

	$source =~ s!
			(\n$ident[^(\n]*)\n                  # e.g. static void
			(
				$ident\(\n?                      # func_name( 
				(.*,([ \t]*$comment)?\n)*        # args b4 final ln
				.*\);([ \t]*$comment)?$          # final line
			)
		!$1 . (substr($1,-1,1) eq '*' ? '' : ' ') . $2!gmxe;

	## Other

	# Remove too much indenting after closing brace.
	$source =~ s!^\}\t[ \t]+!}\t!gm;

	# Workaround indent bug that places excessive space before 'static'.
	$source =~ s!^static[ \t]+!static !gm;

	# Remove leading whitespace from typedefs
	$source =~ s!^[ \t]+typedef enum!typedef enum!gm
	  if $source_filename =~ 'libpq-(fe|events).h$';

	# Remove trailing blank lines
	$source =~ s!\n+\z!\n!;

	return $source;
}


sub run_indent
{
	my $source        = shift;
	my $error_message = shift;

	my $cmd =
	  "$indent $indent_opts $extra_opts -U" . $filtered_typedefs_fh->filename;

	my $tmp_fh = new File::Temp(TEMPLATE => "pgsrcXXXXX");
	my $filename = $tmp_fh->filename;
	print $tmp_fh $source;
	$tmp_fh->close();

	$$error_message = `$cmd $filename 2>&1`;

	return "" if ($? || length($$error_message) > 0);

	unlink "$filename.BAK";

	open(my $src_out, '<', $filename);
	local ($/) = undef;
	$source = <$src_out>;
	close($src_out);

	return $source;

}

# XXX Ideally we'd implement entab/detab in pure perl.

sub detab
{
	my $source = shift;

	my $tmp_fh = new File::Temp(TEMPLATE => "pgdetXXXXX");
	print $tmp_fh $source;
	$tmp_fh->close();

	open(my $entab, '-|', "$entab -d -t4 -qc " . $tmp_fh->filename);
	local ($/) = undef;
	$source = <$entab>;
	close($entab);

	return $source;
}


sub entab
{
	my $source = shift;

	my $tmp_fh = new File::Temp(TEMPLATE => "pgentXXXXX");
	print $tmp_fh $source;
	$tmp_fh->close();

	open(my $entab, '-|',
		"$entab -d -t8 -qc " . $tmp_fh->filename . " | $entab -t4 -qc");
	local ($/) = undef;
	$source = <$entab>;
	close($entab);

	return $source;
}


# for development diagnostics
sub diff
{
	my $pre   = shift;
	my $post  = shift;
	my $flags = shift || "";

	print STDERR "running diff\n";

	my $pre_fh  = new File::Temp(TEMPLATE => "pgdiffbXXXXX");
	my $post_fh = new File::Temp(TEMPLATE => "pgdiffaXXXXX");

	print $pre_fh $pre;
	print $post_fh $post;

	$pre_fh->close();
	$post_fh->close();

	system( "diff $flags "
		  . $pre_fh->filename . " "
		  . $post_fh->filename
		  . " >&2");
}


sub run_build
{
	eval "use LWP::Simple;";

	my $code_base = shift || '.';
	my $save_dir = getcwd();

	# look for the code root
	foreach (1 .. 5)
	{
		last if -d "$code_base/src/tools/pgindent";
		$code_base = "$code_base/..";
	}

	die "no src/tools/pgindent directory in $code_base"
	  unless -d "$code_base/src/tools/pgindent";

	chdir "$code_base/src/tools/pgindent";

	my $rv = getstore("http://buildfarm.postgresql.org/cgi-bin/typedefs.pl",
		"tmp_typedefs.list");

	die "fetching typedefs.list" unless is_success($rv);

	$ENV{PGTYPEDEFS} = abs_path('tmp_typedefs.list');

	$rv =
	  getstore("ftp://ftp.postgresql.org/pub/dev/indent.netbsd.patched.tgz",
		"indent.netbsd.patched.tgz");

	die "fetching indent.netbsd.patched.tgz" unless is_success($rv);

	# XXX add error checking here

	mkdir "bsdindent";
	chdir "bsdindent";
	system("tar -z -xf ../indent.netbsd.patched.tgz");
	system("make > $devnull 2>&1");

	$ENV{PGINDENT} = abs_path('indent');

	chdir "../../entab";

	system("make > $devnull 2>&1");

	$ENV{PGENTAB} = abs_path('entab');

	chdir $save_dir;

}


sub build_clean
{
	my $code_base = shift || '.';

	# look for the code root
	foreach (1 .. 5)
	{
		last if -d "$code_base/src/tools/pgindent";
		$code_base = "$code_base/..";
	}

	die "no src/tools/pgindent directory in $code_base"
	  unless -d "$code_base/src/tools/pgindent";

	chdir "$code_base";

	system("rm -rf src/tools/pgindent/bsdindent");
	system("git clean -q -f src/tools/entab src/tools/pgindent");
}


# main

# get the list of files under code base, if it's set
File::Find::find(
	{   wanted => sub {
			my ($dev, $ino, $mode, $nlink, $uid, $gid);
			(($dev, $ino, $mode, $nlink, $uid, $gid) = lstat($_))
			  && -f _
			  && /^.*\.[ch]\z/s
			  && push(@files, $File::Find::name);
		  }
	},
	$code_base) if $code_base;

process_exclude();

$filtered_typedefs_fh = load_typedefs();

check_indent();

# make sure we process any non-option arguments.
push(@files, @ARGV);

foreach my $source_filename (@files)
{
	my $source        = read_source($source_filename);
	my $error_message = '';

	$source = pre_indent($source);

	# Protect backslashes in DATA() and wrapping in CATALOG()

	$source = detab($source);
	$source =~ s!^((DATA|CATALOG)\(.*)$!/*$1*/!gm;

	$source = run_indent($source, \$error_message);
	if ($source eq "")
	{
		print STDERR "Failure in $source_filename: " . $error_message . "\n";
		next;
	}

 # Restore DATA/CATALOG lines; must be done here so tab alignment is preserved
	$source =~ s!^/\*((DATA|CATALOG)\(.*)\*/$!$1!gm;
	$source = entab($source);

	$source = post_indent($source, $source_filename);

	write_source($source, $source_filename);
}

build_clean($code_base) if $build;
