#! /usr/bin/env perl
# PODNAME: cpppp
our $VERSION = '0.005'; # VERSION
# ABSTRACT: Command line tool to process cpppp templates
use v5.20;
use warnings;
use FindBin;
use experimental 'signatures';
use CodeGen::Cpppp;
use autouse 'Pod::Usage' => 'pod2usage';
use Getopt::Long;


our @original_argv= @ARGV;
our @script;
my %param;
our %option_spec= (
   'param|p=s'             => sub { set_param(split /=/, $_[1], 2) },
   'features=s'            => sub { set_feature($_) for split ',', $_[1] },
   'dump-pl'               => \my $opt_dump_pl,
   'list-sections'         => \my $opt_list_sections,
   'convert-linecomment-to-c89' => \my $convert_linecomment_to_c89,
   'call=s'                => sub { push @script, [ 'call_method', $_[1] ] },
   'eval=s'                => sub { push @script, [ 'do_eval', $_[1] ] },
   'out|section-out|o=s'   => sub { push @script, [ 'output', $_[1] ] },
   '<>'                    => sub { push @script, [ 'process_tpl', $_[0] ] },
   'help'                  => sub { pod2usage(1) },
   'version'               => sub { say CodeGen::Cpppp->VERSION; exit 0 },
);
GetOptions(%option_spec) or pod2usage(2);

# If no 'process_tpl' item exists, create one from STDIN.
unless (grep $_->[0] eq 'process_tpl', @script) {
   unshift @script, [ 'process_tpl', \*STDIN, 'stdin' ];
   # warn unsuspecting users
   STDERR->print("(reading template from stdin)\n")
      if -t STDIN && -t STDERR;
}
# 'process_tpl' needs to happen before any other action in the @script
# but the user may have specified the file name last.
if ($script[0][0] ne 'process_tpl') {
   my $i= 0;
   ++$i while $script[$i][0] ne 'process_tpl';
   unshift @script, splice(@script, $i, 1);
}
# --list-sections suppresses output
if ($opt_list_sections) {
   @script= grep $_->[0] ne 'output', @script;
}
elsif (!grep $_->[0] eq 'output') {
   # If there was no 'out' specified, add one to STDOUT
   push @script, [ 'output', '-' ];
}

sub set_param($var, $value) {
   $var =~ /^( [\$\@\%]? ) [\w_]+ $/x
      or die "Parameter name '$var' is not valid\n";
   if ($1) {
      my $expr= $1 eq '$'? '$param{$var}='.$value
         : $1 eq '@'? '$param{$var}=['.$value.']'
         : '$param{$var}={'.$value.'}';
      # Automatically require modules mentioned in the expression
      while (/\b([A-Za-z][\w_]+(::[A-Za-z0-9][\w_]+)+)\b/) {
         my $fname= $1 . '.pm';
         $fname =~ s,::,/,g;
         eval { require $fname };
      }
      eval "use strict; use warnings; $expr; 1"
         or die "Error evaluating parameter '$var': $@\n";
   } else {
      $param{'$'.$var} //= $value;
      $param{'@'.$var} //= [ split ',', $value ]
         if $value =~ /,/;
      my ($k, $v);
      $param{'%'.$var} //= { map +(($k,$v)=split('=',$_,2)), split ',', $value }
         if $value =~ /=/;
   }
}
sub set_feature($expr) {
   my ($k, $v)= split '=', $expr, 2;
   set_param("feature_$k", $v // 1);
}

my $cpppp= CodeGen::Cpppp->new(
   convert_linecomment_to_c89 => $convert_linecomment_to_c89,
);

my $tpl;
sub process_tpl(@input_args) {
   if ($opt_dump_pl) {
      my $parse= $cpppp->parse_cpppp(@input_args);
      my $code= $cpppp->_gen_perl_template_package($parse, with_data => 1);
      my $sec= $input_args[1] // $input_args[0];
      $cpppp->output->declare_sections($sec);
      $cpppp->output->append($sec, $code)
   } else {
      my $tpl_class= $cpppp->compile_cpppp(@input_args);
      my $tpl_params= $tpl_class->coerce_parameters(\%param);
      $tpl= $cpppp->new_template($tpl_class, $tpl_params);
   }
}

sub do_eval($code) {
   eval $code or die "Eval '$code' failed: $@\n";
}

sub call_method($code) {
   defined $tpl or die "No template is defined, for --call";
   do_eval("\$tpl->$code");
}

sub output($spec) {
   my ($filespec, $sections)= reverse split /=/, $spec, 2;
   if ($filespec eq '-' || !length $filespec) {
      print $cpppp->get_filtered_output($sections);
   } else {
      $cpppp->write_sections_to_file($sections, split('@', $filespec, 2));
   }
   $cpppp->output->consume(defined $sections? ($sections) : ());
}

# All the global options are taken care of.  Now execute the "script options"
# in the order they were given.
for (@script) {
   my ($method, @args)= @$_;
   $method= main->can($method) or die 'bug';
   $method->(@args);
}

if ($opt_list_sections) {
   say "name\tline_count";
   for my $s ($cpppp->output->section_list) {
      my $line_count= ()= $cpppp->output->get($s) =~ /\n/g;
      say "$s\t$line_count";
   }
   exit 0;
}

# Lets a template main::re_exec(@different_args)
sub re_exec(@new_argv) {
   exec($^X, "$FindBin::RealBin/$FindBin::RealScript", @new_argv)
      or die "exec: $!";
}

__END__

=pod

=encoding UTF-8

=head1 NAME

cpppp - Command line tool to process cpppp templates

=head1 USAGE

  cpppp [OPTIONS] [TEMPLATE_FILE ACTIONS...]... > file.c
  cpppp [OPTIONS] [TEMPLATE_FILE ACTIONS...]... --list-sections
  cpppp --version
  
  Common Options:  (see --help for more)
    -p --param NAME=VALUE
       --features ENABLE1,ENABLE2,DISABLE=0
  Common Actions:
    --call 'TEMPLATE_METHOD(...)'
    --eval 'PERL CODE'
    -o --out [SECTION_LIST=]FILENAME[@MARK]

Transform perl+C source into C.  See --help for a list of options.

Warning: this evaluates arbitrary perl from the template files.

=head1 OPTIONS

=over

=item -p

=item --param NAME=VALUE

Specify a parameter to the template.  If NAME includes the Perl sigil, the
value will be evaluated as a perl expression.  If NAME lacks a sigil, the VALUE
will be parsed with a more convenient syntax:

  cpppp -p '$type="int"'
  cpppp -p type=int
  cpppp -p '@types=qw( int float char )'
  cpppp -p types=int,float,char
  cpppp -p '%typemap={int => "int32",float => "double"}'
  cpppp -p typemap=int=int32,float=double

=item --features LIST

This is a shorthand to specify lots of --param options for parameters whose
names start with "feature_".  The bare name is equivalent to "=1", which would
generally enable some feature.

  # (there are no standard features; these would need declared in the template)
  cpppp -p feature_debug=1 -p feature_assert=1 -p feature_comments=0
  cpppp --features 'debug,assert,comments=0'

=item --convert-linecomment-to-c89

Convert all '//' comments to '/*' comments.

=item --dump-pl

Output the generated perl sourcecode for the top-level perl script and don't
execute it.

=back

The following "action" options are processed sequentially, like a script,
acting on the first template filename to the left of the action on the command
line, or the first filename overall if an action option appears first.

=over

=item --eval 'PERL CODE'

After loading the template, evaluate a string of perl code.  The template
object is named C<$tpl> and the Cpppp object is C<$cpppp>.

=item --call 'METHOD(...)'

Shortcut for C<--eval> to call a method of C<$tpl>.

=item -o

=item --out [SECTION=]FILENAME[@MARKER]

Write output to FILENAME instead of C<stdout> (unless FILENAME is "-" then do
still write C<stdout> )  If FILENAME already exists, a backup will be created,
unless you specified a @MARKER.

If a @MARKER is specified, the content will be written within the existing
content of the file (without a backup) between lines that match
C<< BEGIN marker >> and C<< END marker >>.  If those lines are not found, the
operations aborts.

An equal sign indicates that only specific sections should be sent to this file.
The SECTION is either the name of one section, or a comma-delimited list of
section names, or a range C<A..B> of section names.  All content diverted to
this FILENAME will be omitted from further output.

=item --list-sections

List the sections of output (in order they would be written) and exit without
writing any files.  The output is TSV.  More columns may be added in the future.

=back

eventually it will support many that C<cpp> has, for defining things and parsing
headers to discover existing types.

=head1 AUTHOR

Michael Conrad <mike@nrdvana.net>

=head1 VERSION

version 0.005

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2024 by Michael Conrad.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut