#!/usr/bin/perl -w
#
# subdirmk - &-filter (makefile generation program)
#  Copyright 2019 Ian Jackson
# SPDX-License-Identifier: LGPL-2.0-or-later
# There is NO WARRANTY.
#
# $(srcdir)/subdirmk/generate [--srcdir=SRCDIR] [--] SUBDIR...
#
# generates in each subdirectory
#     Dir.mk.tmp
#     Makefile
# and in toplevel
#     main.mk.tmp

use strict;
use POSIX;

print "$0 @ARGV\n" or die $!;

our $srcdir='.';

# error handling methods:
#
# Error in input file, while $err_file and $. set, eg in most of
# process_input_mk:
#         err "message";
#
# Other input or usage errors:
#         die "subdirmk: $file:$lno: problem\n";
#         die "subdirmk: some problem not locatable in that way\n";
#
# Usage error:
#         die "subdirmk $0: explanation of problem\n";
#
# System call error (not ENOENT) accessing input/output files:
#         die "description of problem eg maybe erbing noun: $!\n";
#
# Bug detedcted in `generate':
#         die "internal error (some information)?"; # or similar

while (@ARGV && $ARGV[0] =~ m/^-/) {
    $_ = shift @ARGV;
    last if $_ eq '--';
    if (s/^--srcdir=//) {
	$srcdir=$';
    } else {
	die "subdirmk $0: unknown option \`$_'\n";
    }
}
our @subdirs = @ARGV;

s{/+$}{} foreach @subdirs;

our $root = [ '.', [ ], 1 ];
# each node is [ 'relative subdir name', \@children, $mentioned ]

sub build_tree () {
    foreach my $subdir (@subdirs) {
	my @path = $subdir eq '.' ? () : split m{/+}, $subdir;
	my $node = $root;
	foreach my $d (@path) {
	    my ($c,) = grep { $_->[0] eq $d } @{ $node->[1] };
	    if (!$c) {
		$c = [ $d, [ ] ];
		push @{ $node->[1] }, $c;
	    }
	    $node = $c;
	}
	$node->[2] = 1;
    }
}

sub target_varname ($$) {
    my ($var_prefix, $target) = @_;
    return $var_prefix.'TARGETS'.($target eq 'all' ? '' : "_$target");
}

our $writing_output;
our $buffering_output;
our %output_files;
our %input_files;
our @output_makefiles;

sub close_any_output_file() {
    return unless defined $writing_output;
    O->error and die "error writing $writing_output.tmp: $! (?)\n";
    close O or die "error closing $writing_output.tmp: $!\n";
    $writing_output = undef;
}

sub oraw {
    die 'internal error' unless defined $writing_output;
    print O @_ or die "error writing $writing_output.tmp: $!\n";
}

sub oud { # undoubled
    if (defined $buffering_output) {
	$buffering_output .= $_ foreach @_;
	return;
    }
    oraw @_;
}

our $ddbl;

sub od { # maybe $-doubled
    if (!$ddbl) {
	oud @_;
	return;
    }
    foreach (@_) {
	my $e = $_;
	$e =~ s{\$}{\$\$}g;
	oud $e;
    }
}

sub start_output_file ($) {
    close_any_output_file();
    ($writing_output) = @_;
    die "internal error ($writing_output?)"
	if $output_files{$writing_output}++;
    my $tmp = "$writing_output.tmp";
    open O, ">", $tmp or die "create $tmp: $!\n";
    oraw "# autogenerated - do not edit\n";
}

sub install_output_files () {
    close_any_output_file();
    foreach my $f (sort keys %output_files) {
	rename "$f.tmp", $f or die "install new $f: $!\n";
    }
}

sub write_makefile ($$) {
    my ($dir_prefix,$depth) = @_;
    #print STDERR "write_makefile @_\n";
    start_output_file("${dir_prefix}Makefile");
    my $cd = $depth ? join('/', ('..',) x $depth) : '.';
    my $suppress_templates=
	'$(if $(filter-out clean real-clean, $(subdirmk_targets)),,'.
	' MAKEFILE_TEMPLATES=)';
    oraw <<END;
default: all
\$(filter-out all,\$(MAKECMDGOALS)) all: run-main.mk
	\@:
subdirmk_targets:=\$(or \$(MAKECMDGOALS),all)
Makefile run-main.mk:
	\$(MAKE) -C $cd -f main.mk \$(addprefix ${dir_prefix},\$(subdirmk_targets))$suppress_templates
.SUFFIXES:
.PHONY:	run-main.mk
END
}

our %varref;
our %varref_exp;

our ($dir_prefix, $dir_suffix, $dir_name,
     $var_prefix, $var_prefix_name);

sub dir_prefix ($) {
    my ($path) = @_;
    join '', map { "$_/" } @$path;
}

sub set_dir_vars ($) {
    my ($path) = @_;
    $dir_prefix = dir_prefix($path);
    $dir_suffix = join '', map { "/$_" } @$path;
    $dir_name = join '/', @$path ? @$path : '.';
    $var_prefix_name = join '_', @$path ? @$path : qw(TOP);
    $var_prefix = "${var_prefix_name}_";
}

our $err_file;

our @warn_ena_dfl = map { $_ => 1 } qw(
    local+global
    single-char-var
    unknown-warning
    broken-var-ref
);
our %warn_ena = @warn_ena_dfl;

our $warned;
our %warn_unk;

sub err ($) {
    my ($m) = @_;
    die defined $err_file
	? "subdirmk: ${err_file}:$.: $m\n"
	: "subdirmk: $m\n";
}

sub wrncore ($$) {
    my ($wk,$m) = @_;
    return 0 unless $warn_ena{$wk} // warn "internal error $wk ?";
    $warned++;
    print STDERR "subdirmk: warning ($wk): $m\n";
    return 1;
}

sub wrn ($$) {
    my ($wk,$m) = @_;
    our %warn_dedupe;
    return 0 if $warn_dedupe{$err_file,$.,$wk,$m}++;
    wrncore($wk, "${err_file}:$.: $m");
}

sub ddbl_only ($) {
    my ($e) = @_;
    return if $ddbl;
    err "escape &$e is valid only during \$-doubling";
}

sub process_input_mk ($$$$);
sub process_input_mk ($$$$) {
    my ($targets, $f, $esclitr, $enoent_ok) = @_;

    my $caps_re = qr{[A-Z]};
    my $lc_re = qr{[a-z]};

    my $esc;
    my $set_esc = sub {
	$esc = $$esclitr;
	$esc =~ s/\W/\\$&/g;
    };
    $set_esc->();

    my $input = new IO::File $f, '<';
    if (!$input) {
	err "open $f: $!" unless $!==ENOENT && $enoent_ok;
	return;
    }
    $input_files{$f}++;

    local $err_file=$f;

    my %srcdirmap = (
		  '^' => "\${top_srcdir}${dir_suffix}",
		  '~' => "\${top_srcdir}",
		    );
    my %pfxmap = (
		  ''  => $dir_prefix,
		 );
    $pfxmap{$_} = $srcdirmap{$_}.'/' foreach keys %srcdirmap;

    local $ddbl;
    my @nest = (['']);
    my $evalcall_brackets;

    my $push_nest = sub {
	my ($nk, $nndbl, $what) = @_;
	unshift @nest, [ $nk, $ddbl, $what, $. ];
	$ddbl = $nndbl;
    };
    my $pop_nest = sub {
	my ($nk) = @_;
	err "unexpectedly closed $nk in middle of $nest[0][0] ($nest[0][2])"
	    unless $nest[0][0] eq $nk;
	$ddbl = (shift @nest)[1];
    };

    # Our detection of variable settings does not have to be completely
    # accurate, since it is only going to be used for advice to the user.
    my $note_varref = sub {
	my ($vn,$amp) = @_;
	my $exp = !!$varref_exp{$vn}{$amp};
	$varref{$vn}{$exp}{$amp}{"$f:$."} = 1;
    };

    while (<$input>) {
	if (m#^\s*($esc)?(\w+)\s*(?:=|\+=|\?=|:=)# ||
	    m#^\s*(?:$esc\:macro|define)\s+($esc)?(\S+)\s#) {
	    $note_varref->($2,!!$1);
	}
	if (s#^\s*$esc\:changequote\s+(\S+)\s+$##) {
	    $$esclitr = $1;
	    $set_esc->();
	    next;
	} elsif (s#^\s*$esc\:endm\s+$##) {
	    $pop_nest->('macro');
	    od "endef\n";
	    next;
	} elsif (s#^\s*$esc\:warn\s+(\S.*)$##) {
	    foreach my $wk (split /\s+/, $1) {
		my $yes = $wk !~ s{^!}{};
		if (defined $warn_ena{$wk}) {
		    $warn_ena{$wk} = $yes;
		    next;
		} elsif ($yes) {
		    wrn 'unknown-warning',
			"unknown warning $wk requested";
		} else {
		    $warn_unk{$wk} //= "$f:$.";
		}
	    }
	    next;
	} elsif (s#^\s*$esc\:local\+global\s+(\S.*)$##) {
	    foreach my $vn (split /\s+/, $1) {
		my $pos = !($vn =~ s{^!}{});
		my $amp = $vn =~ s{^$esc}{};
		$varref_exp{$vn}{!!$amp} = $pos;
	    }
	    next;
	} elsif (s#^\s*$esc\:(?=(-?)include|macro)##) {
	    $buffering_output='';
	} elsif (m#^\s*$esc\:([a-z][-+0-9a-z_]*)#) {
	    err "unknown directive &:$1 or bad argumnt syntax";
	} elsif (s{^\s*${esc}TARGETS(?:_([0-9a-zA-Z_]+))?(?=\W)}{}) {
	    my $t = $1 // 'all';
	    my $vn = target_varname($var_prefix, $t);
	    $note_varref->($vn,1);
	    od $vn;
	    $targets->{$t} //= [ ];
	}
	for (;;) {
	    err 'cannot $-double &-processed RHS of directive'
		if $ddbl && defined $buffering_output;
	    unless ($nest[0][0] eq 'eval'
		    ? s{^(.*?)($esc|\$|[{}])}{}
		    : s{^(.*?)($esc|\$)}{}) { od $_; last; }
	    od $1;
	    if ($2 eq '{') {
		od $2;
		$evalcall_brackets++;
		next;
	    } elsif ($2 eq '}') {
		od $2;
		next if --$evalcall_brackets;
		$pop_nest->('eval');
		od '}';
		next;
	    } elsif ($2 eq '$') {
		od $2;
		if (s{^\$}{}) { od $&; }
		elsif (m{^[a-zA-Z]\w}) {
		    wrn 'single-char-var',
		    'possibly confusing unbracketed single-char $-expansion';
		}
		elsif (m{^$esc}) {
		    wrn 'broken-var-ref',
		    'broken $&... expansion; you probably meant &$';
		}
		elsif (m{^\(($esc)?([^()\$]+)\)} ||
		       m{^\{($esc)?([^{}\$]+)\}}) {
		    $note_varref->($2,!!$1);
	        }
		next;
	    }
	    if (s{^\\$esc}{}) { od "$$esclitr" }
	    elsif (s{^:}{}) { od "$$esclitr:" }
	    elsif (s{^\\\$}{}) { oud '$' }
	    elsif (s{^\\\s+$}{}) { }
	    elsif (s{^$esc}{}) { od "$$esclitr$$esclitr" }
	    elsif (m{^(?=$caps_re)}) { od $var_prefix }
	    elsif (s{^\$([A-Za-z]\w+)}{}) {
		$note_varref->($1,1);
		od "\${${var_prefix}$1}";
	    }
	    elsif (s{^([~^]?)(?=$lc_re)}{}) { od $pfxmap{$1} }
	    elsif (s{^_}{}) { od $var_prefix }
	    elsif (s{^=}{}) { od $var_prefix_name }
	    elsif (s{^([~^]?)/}{}) { od $pfxmap{$1} }
	    elsif (s{^\.}{}) { od $dir_name }
	    elsif (s{^([~^])\.}{}) { od $srcdirmap{$1} }
	    elsif (s{^\$\-}{}) { $ddbl=undef; }
	    elsif (s{^\$\+}{}) { $ddbl=1; }
	    elsif (s{^\$\(}{}) {
		ddbl_only($&); oud "\$(";
		$note_varref->($2,!!$1) if m{^($esc)?([^()\$]+\))};
	    }
	    elsif (s{^\$(\d+)}{}) { ddbl_only($&); oud "\${$1}"; }
	    elsif (s{^\(\s*$esc(?=$lc_re)}{}) { od "\$(call ${var_prefix}" }
	    elsif (s{^\(\s*(?=\S)}{}        ) { od "\$(call "              }
	    elsif (s{^\{}{}) {
		err 'macro invocation cannot be re-$-doubled' if $ddbl;
		od '${eval ${call ';
		$evalcall_brackets = 1;
		$push_nest->('eval',1, '&{...}');
		$note_varref->($2,!!$1) if m{^\s*($esc)?([^,{}\$]+)};
	    } elsif (s{^([~^]?)(?=[ \t])}{}) {
		my $prefix = $pfxmap{$1} // die "internal error ($1?)";
		my $after='';
		if (m{([ \t])$esc}) { ($_,$after) = ($`, $1.$'); }
		s{(?<=[ \t])(?=\S)(?!\\\s*$)}{$prefix}g;
		od $_;
		$_ = $after;
	    } elsif (s{^\#}{}) {
		$_ = '';
	    } elsif (s{^![ \t]+}{}) {
		od $_;
		$_ = '';
	    } else {
		m{^.{0,5}};
		err "bad &-escape \`$$esclitr$&'";
	    }
	}
	if (defined $buffering_output) {
	    $_=$buffering_output;
	    $buffering_output=undef;
	    if (m#^(-?)include\s+(\S+)\s+$#) {
		my $subf = "$srcdir/$2";
		process_input_mk($targets, $subf, $esclitr, $1);
		od "\n";
	    } elsif (m#^macro\s+(\S+)\s+$#) {
		od "define $1\n";
		$push_nest->('macro', 1, '&:macro');
	    } else {
		err "bad directive argument syntax";
	    }
	}
    }
    die "subdirmk: $f:$nest[0][3]: unclosed $nest[0][0] ($nest[0][2])\n"
	if $nest[0][0];
    $input->error and die "read $f: $!\n";
    close $input or die "close $f: $!\n";
}

sub filter_subdir_mk ($) {
    my ($targets) = @_;

    #use Data::Dumper;
    #print STDERR "filter @_\n";

    my $esclit = '&';

    my $pi = sub {
	my ($f, $enoentok) = @_;
	process_input_mk($targets, "${srcdir}/$f", \$esclit, $enoentok);
    };
    $pi->("Prefix.sd.mk",           1);
    $pi->("${dir_prefix}Dir.sd.mk", 0);
    $pi->("Suffix.sd.mk",           1);
}

sub process_subtree ($$);
sub process_subtree ($$) {
    # => list of targets (in form SUBDIR/)
    # recursive, children first
    my ($node, $path) = @_;

    #use Data::Dumper;
    #print STDERR Dumper(\@_);

    local %varref_exp;

    my $dir_prefix = dir_prefix($path);
    # ^ this is the only var which we need before we come back from
    #   the recursion.

    push @output_makefiles, "${dir_prefix}Dir.mk";
    write_makefile($dir_prefix, scalar @$path);

    my %targets = (all => []);
    foreach my $child (@{ $node->[1] }) {
	my @childpath = (@$path, $child->[0]);
	my $child_subdir = join '/', @childpath;
	mkdir $child_subdir or $!==EEXIST or die "mkdir $child_subdir: $!\n";
	local %warn_ena = @warn_ena_dfl;
	push @{ $targets{$_} }, $child_subdir foreach
	    process_subtree($child, \@childpath);
    }

    set_dir_vars($path);
    start_output_file("${dir_prefix}Dir.mk.tmp");

    if ($node->[2]) {
	filter_subdir_mk(\%targets);
    } else {
	my $sdmk = "${dir_prefix}Dir.sd.mk";
	if (stat $sdmk) {
	    die
 "subdirmk: $sdmk unexpectedly exists (${dir_prefix} not mentioned on subdirmk/generate command line, maybe directory is missing from SUBDIRMK_SUBDIRS)";
	} elsif ($!==ENOENT) {
	} else {
	    die "stat $sdmk: $!\n";
	}
    }

    oraw "\n";

    my @targets = sort keys %targets;
    foreach my $target (@targets) {
	my $target_varname = target_varname($var_prefix, $target);
	oraw "${dir_prefix}${target}:: \$($target_varname)";
	foreach my $child_subdir (@{ $targets{$target} }) {
	    oraw " $child_subdir/$target";
	}
	oraw "\n";
    }
    if (@targets) {
	oraw ".PHONY:";
	oraw " ${dir_prefix}${_}" foreach @targets;
	oraw "\n";
    }

    return @targets;
}

sub process_final ($) {
    my ($otargets) = @_;
    set_dir_vars([]);
    push @output_makefiles, "Final.mk";
    start_output_file("Final.mk.tmp");
    my %ntargets;
    my $esclit='&';
    process_input_mk(\%ntargets, "${srcdir}/Final.sd.mk", \$esclit, 1);
    delete $ntargets{$_} foreach @$otargets;
    my @ntargets = sort keys %ntargets;
    die "subdirmk: Final.sd.mk may not introduce new top-level targets".
	" (@ntargets)\n" if @ntargets;
}

sub process_tree() {
    my @targets = process_subtree($root, [ ]);
    process_final(\@targets);
    start_output_file("main.mk.tmp");
    foreach my $v (qw(top_srcdir abs_top_srcdir)) {
	oraw "$v=\@$v@\n";
    }
    oraw "SUBDIRMK_MAKEFILES :=\n";
    oraw "MAKEFILE_TEMPLATES :=\n";
    foreach my $mf (@output_makefiles) {
	oraw "SUBDIRMK_MAKEFILES += $mf\n";
    }
    foreach my $input (sort keys %input_files) {
	oraw "MAKEFILE_TEMPLATES += $input\n";
    }
    oraw "include \$(SUBDIRMK_MAKEFILES)\n";
}

sub flmap ($) { local ($_) = @_; s{:(\d+)$}{ sprintf ":%10d", $1 }e; $_; }

sub print_varref_warnings () {
    foreach my $vn (sort keys %varref) {
	my $vv = $varref{$vn};
	next unless $vv->{''}{''} && $vv->{''}{1};
	wrncore 'local+global', "saw both $vn and &$vn" or return;
	foreach my $exp ('', 1) {
	foreach my $amp ('', 1) {
	    printf STDERR
		($exp
		 ? " expectedly saw %s%s at %s\n"
		 : " saw %s%s at %s\n"),
		($amp ? '&' : ''), $vn, $_
		foreach
		sort { flmap($a) cmp flmap($b) }
		keys %{ $vv->{$exp}{$amp} };
	}
        }
    }
}

sub print_warning_warnings () {
    return unless $warned;
    foreach my $wk (sort keys %warn_unk) {
	wrncore 'unknown-warning',
	    "$warn_unk{$wk}: attempt to suppress unknown warning(s) \`$wk'";
    }
}

build_tree();
process_tree();
print_varref_warnings();
print_warning_warnings();
install_output_files();
