#!/usr/bin/perl

use strict;
use warnings;
use IPC::Open2;
use IPC::Cmd qw[can_run];

can_run('gperf') or die 'No gperf binary found, make sure to have gperf installed!';

my (%defines_0, %defines_1, %defines_2, %defines_4);

{
	my @argv;
	for my $arg (@ARGV) {
		if ($arg !~ /^-/) {
			push(@argv, $arg);
			next;
		}
		if ($arg =~ /^-D(.*?)=(.*)$/) {
			$defines_0{$1} = $2;
			next;
		}
		if ($arg =~ /^-D(.*?)$/) {
			$defines_0{$1} = '1';
			next;
		}
	}

	@ARGV = @argv; ## no critic (RequireLocalizedPunctuationVars)
}

print("/******** GENERATED FILE ********/\n");

my $rewritten_input = '';
my @sections;
my @slots;
my $ifdefs = 0;

# collect keywords and rewrite input file with in lookup keys

sub def_sub {
	my ($subs, @vals) = @_;
	my $i = 0;
	my $repl = $subs->[$i++];
	for my $r (@vals) {
		my $key = $subs->[$i++];
		$repl =~ s/(\W|^)\Q$key\E(\W|$)/$1$r$2/g;
	}
	return $repl;
}

while (my $line = <STDIN>) {
	my $num = scalar(@sections);
	my $new_section;
	if ($line =~ /CSH_SECTION|CSH_LOOKUP|CSH_NUM_LOOKUPS/
			&& $line =~ /^\s*#define\s+(\w+)(?:\s+(.*?)|\(\s*(\w+)\s*\)\s+(.*?)|\(\s*(\w+)\s*,\s*(\w+)\s*\)\s+(.*?)|\(\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*\)\s+(.*?))$/) {
		if ($2) {
			$defines_0{$1} = $2;
		}
		elsif ($3) {
			$defines_1{$1} = [$4, $3];
		}
		elsif ($5) {
			$defines_2{$1} = [$7, $5, $6];
		}
		elsif ($8) {
			$defines_4{$1} = [$12, $8, $9, $10, $11];
		}
		$rewritten_input .= "\n";
		next;
	}
	if ($line =~ /^\s*#endif/) {
		if ($ifdefs) {
			$ifdefs--;
		}
		$rewritten_input .= $line;
		next;
	}
	if ($line =~ /^\s*#if(n?)def\s+(\S+)/) {
		if ((!$1 && !$defines_0{$2}) || ($1 && $defines_0{$2}) || $ifdefs) {
			$ifdefs++;
		}
		$rewritten_input .= $line;
		next;
	}
	if ($line =~ /^\s*#if\s+/) {
		if ($ifdefs) {
			$ifdefs++;
		}
		$rewritten_input .= $line;
		next;
	}
	if ($ifdefs) {
		$rewritten_input .= $line;
		next;
	}
	for my $def (keys(%defines_0)) {
		my $sub = $defines_0{$def};
		$line =~ s/(\W|^)\Q$def\E(\W|$)/$1$sub$2/g;
	}
	for my $def (keys(%defines_1)) {
		my $subs = $defines_1{$def};
		$line =~ s/(\W|^)\Q$def\E\(\s*(\S+|"[^"]*")\s*\)(\W|$)/$1 . def_sub($subs, $2) . $3/eg;
	}
	for my $def (keys(%defines_2)) {
		my $subs = $defines_2{$def};
		$line =~ s/(\W|^)\Q$def\E\(\s*(\S+|"[^"]*")\s*,\s*(\S+|"[^"]*")\s*\)(\W|$)/$1 . def_sub($subs, $2, $3) . $4/eg;
	}
	for my $def (keys(%defines_4)) {
		my $subs = $defines_4{$def};
		$line =~ s/(\W|^)\Q$def\E\(\s*(\S+|"[^"]*")\s*,\s*(\S+|"[^"]*")\s*,\s*(\S+|"[^"]*")\s*,\s*(\S+|"[^"]*")\s*\)(\W|$)/$1 . def_sub($subs, $2, $3, $4, $5) . $6/eg;
	}
	if (($line =~ s/(__csh_lookup)(\s*\()/$1_$num$2/)) {
		$new_section = 0;
	}
	elsif (($line =~ s/(__csh_lookup)_n(\s*\()\s*(\d+)\s*,\s*/$1_$num$2/)) {
		$new_section = $3;
	}
	elsif (($line =~ s/CSH_SECTION/$num/)) {
		$new_section = 0;
	}
	if (defined($new_section)) {
		$rewritten_input .= $line;
		my $section = { keys => '', vals => {}, num => $num };
		push(@sections, $section);
		$slots[$new_section] = $num;
		next;
	}
	if ($line =~ s/CSH_NUM_LOOKUPS/{}/) {
		my $section = $sections[$slots[0]];
		die unless $section;
		my $n = values(%{$section->{vals}});
		$line =~ s/{}/$n/;
		$rewritten_input .= $line;
		next;
	}
	my ($rewrite, $key);
	if ($line =~ s/CSH_LOOKUP\(\s*"(.*?)"\s*\)/{}/) {
		$rewrite = 0;
		$key = $1;
	}
	elsif ($line =~ s/CSH_LOOKUP_N\(\s*(\d+)\s*,\s*"(.*?)"\s*\)/{}/) {
		$rewrite = $1;
		$key = $2;
	}
	if (!defined($rewrite)) {
		$rewritten_input .= $line;
		next;
	}
	my $section = $sections[$slots[$rewrite]];
	die unless $section;
	if (exists($section->{vals}{$key})) {
		$line =~ s/{}/$section->{vals}{$key}/;
	}
	else {
		my $iter = values(%{$section->{vals}});
		$line =~ s/{}/$iter/;
		$section->{keys} .= "$key,$iter\n";
		$section->{vals}{$key} = $iter;
	}
	$rewritten_input .= $line;
}

# pass collected outputs to gperf

print "struct __csh_hash_lookup { char *name; int num; };\n";

for my $section (@sections) {
	my $num = $section->{num};

	my ($rd, $wr);
	my $pid = open2($rd, $wr, qw(gperf -t -E -l -c -I -C -H), "__csh_hash_$num", '-N', "__csh_lookup_raw_$num");

	# gperf header and keys

	print { $wr } "struct __csh_hash_lookup;\n%%\n";
	print { $wr } $section->{keys};

	# read gperf output

	close($wr);
	my $hash_func_code;
	{
		local $/ = undef;
		$hash_func_code = <$rd>;
	}
	close($rd);
	waitpid($pid, 0);
	exit(1) if $?;

	# convert lookup function to static

	$hash_func_code =~ s/(^|\s)(const\s+)?struct\s+__csh_hash_lookup\s*\*/\nstatic$&/s;

	# print combined output

	print "#pragma GCC diagnostic push\n";
	print "#pragma GCC diagnostic ignored \"-Wmissing-field-initializers\"\n";
	print "#pragma GCC diagnostic ignored \"-Wshadow\"\n";
	print $hash_func_code;
	print "#pragma GCC diagnostic pop\n";
	# add convenience function
	print <<END;
#include "str.h"
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored \"-Wunused-function\"
static int __csh_lookup_$num(const str *s) {
	if (!s->s)
		return -1;
	const struct __csh_hash_lookup *h = __csh_lookup_raw_$num(s->s, s->len);
	if (!h)
		return -1;
	return h->num;
}
#pragma GCC diagnostic pop
END
}

print("static const struct __csh_hash_lookup *(*__csh_lookup_funcs[])(const char *, size_t) = {\n");
for my $section (@sections) {
	print("\t__csh_lookup_raw_$section->{num},\n");
}
print <<END;
};
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored \"-Wunused-function\"
static int __csh_lookup_section(unsigned int n, const str *s) {
	if (n >= G_N_ELEMENTS(__csh_lookup_funcs))
		return -1;
	if (!s->s)
		return -1;
	const struct __csh_hash_lookup *h = __csh_lookup_funcs[n](s->s, s->len);
	if (!h)
		return -1;
	return h->num;
}
#pragma GCC diagnostic pop
END

# adjust diagnostic line numbers and originating file
print $ARGV[0] ? "#line 1 \"$ARGV[0]\"\n" : "#line 1\n";
print $rewritten_input;
