#!/usr/bin/env perl
# SPDX-License-Identifier: BSD-3-Clause
# Copyright 2015-2018, Intel Corporation

#
# check_whitespace -- scrub source tree for whitespace errors
#

use strict;
use warnings;

use File::Basename;
use File::Find;
use Encode;
use v5.16;

my $Me = $0;
$Me =~ s,.*/,,;

$SIG{HUP} = $SIG{INT} = $SIG{TERM} = $SIG{__DIE__} = sub {
	die @_ if $^S;

	my $errstr = shift;

	die "$Me: ERROR: $errstr";
};

my $Errcount = 0;

#
# err -- emit error, keep total error count
#
sub err {
	warn @_, "\n";
	$Errcount++;
}

#
# decode_file_as_string -- slurp an entire file into memory and decode
#
sub decode_file_as_string {
	my ($full, $file) = @_;
	my $fh;
	open($fh, '<', $full) or die "$full $!\n";

	local $/;
	$_ = <$fh>;
	close $fh;

	# check known encodings or die
	my $decoded;
	my @encodings = ("UTF-8", "UTF-16", "UTF-16LE", "UTF-16BE");

	foreach my $enc (@encodings) {
		eval { $decoded = decode( $enc, $_, Encode::FB_CROAK ) };

		if (!$@) {
			$decoded =~ s/\R/\n/g;
			return $decoded;
		}
	}

	die "$Me: ERROR: Unknown file encoding";
}

#
# check_whitespace -- run the checks on the given file
#
sub check_whitespace {
	my ($full, $file) = @_;

	my $line = 0;
	my $eol;
	my $nf = 0;
	my $fstr = decode_file_as_string($full, $file);

	for (split /^/, $fstr) {
		$line++;
		$eol = /[\n]/s;
		if (/^\.nf$/) {
			err("$full:$line: ERROR: nested .nf") if $nf;
			$nf = 1;
		} elsif (/^\.fi$/) {
			$nf = 0;
		} elsif ($nf == 0) {
			chomp;
			err("$full:$line: ERROR: trailing whitespace") if /\s$/;
			err("$full:$line: ERROR: spaces before tabs") if / \t/;
		}
	}

	err("$full:$line: .nf without .fi") if $nf;
	err("$full:$line: noeol") unless $eol;
}

sub check_whitespace_with_exc {
	my ($full) = @_;

	$_ = $full;

	return 0 if /^[.\/]*src\/jemalloc.*/;
	return 0 if /^[.\/]*src\/common\/queue\.h/;
	return 0 if /^[.\/]*src\/common\/valgrind\/.*\.h/;

	$_ = basename($full);

	return 0 unless /^(README.*|LICENSE.*|Makefile.*|CMakeLists.txt|.gitignore|TEST.*|RUNTESTS|check_whitespace|.*\.([chp13s]|sh|map|cpp|hpp|inc|PS1|ps1|py|md|cmake))$/;
	return 0 if -z;

	check_whitespace($full, $_);
	return 1;
}

my $verbose = 0;
my $force = 0;
my $recursive = 0;

sub check {
	my ($file) = @_;
	my $r;

	if ($force) {
		$r = check_whitespace($file, basename($file));
	} else {
		$r = check_whitespace_with_exc($file);
	}

	if ($verbose) {
		if ($r == 0) {
			printf("skipped $file\n");
		} else {
			printf("checked $file\n");
		}
	}
}

my @files = ();

foreach my $arg (@ARGV) {
	if ($arg eq '-v') {
		$verbose = 1;
		next;
	}
	if ($arg eq '-f') {
		$force = 1;
		next;
	}
	if ($arg eq '-r') {
		$recursive = 1;
		next;
	}
	if ($arg eq '-g') {
		@files = `git ls-tree -r --name-only HEAD`;
		chomp(@files);
		next;
	}
	if ($arg eq '-h') {
		printf "Options:
     -g - check all files tracked by git
     -r dir - recursively check all files in specified directory
     -v verbose - print whether file was checked or not
     -f force - disable blacklist\n";
		exit 1;
	}

	if ($recursive == 1) {
		find(sub {
			my $full = $File::Find::name;

			if (!$force &&
			   ($full eq './.git' ||
			    $full eq './src/jemalloc' ||
			    $full eq './src/debug' ||
			    $full eq './src/nondebug' ||
			    $full eq './rpmbuild' ||
			    $full eq './dpkgbuild')) {
				$File::Find::prune = 1;
				return;
			}

			return unless -f;

			push @files, $full;
		}, $arg);

		$recursive = 0;
		next;
	}

	push @files, $arg;
}

if (!@files) {
	printf "Empty file list!\n";
}

foreach (@files) {
	check($_);
}

exit $Errcount;
