#!/usr/bin/env perl
######
# runtime.pl
# Tom Prince 2004/4/15
#
#  Generate the runtime functions used by the vm::stack machine.
#
#####

use strict;
use warnings;

use Getopt::Long;

my $opsymbolsFile;
my $runtimeBaseFile;
my $prefix;
my $srcTemplateDir;
my $headerOutDir;
my $srcOutDir;

GetOptions(
   "opsym-file=s"        => \$opsymbolsFile,
   "runtime-base-file=s" => \$runtimeBaseFile,
   "src-template-dir=s"  => \$srcTemplateDir,
   "prefix=s"            => \$prefix,
   "header-out-dir=s"    => \$headerOutDir,
   "src-out-dir=s"       => \$srcOutDir
) || die("Argument error");

my $outHeaderFile = "$headerOutDir/$prefix.h";
my $outSrcFile = "$srcOutDir/$prefix.cc";

my $stack = "Stack";

my $errors = 0;

sub report_error {
   my $filename = shift;
   my $line = shift;
   my $error = shift;
   print STDERR "$filename:$line: $error\n";
   $errors = 1;
}

sub assoc_error {
   my $filename = shift;
   my $line = shift;
   my $type = shift;
   report_error($filename, $line, "no asy type associated to '$type'");
}

sub clean_type {
   for (@_) {
       s/\s//g;
   }
}

sub clean_params {
   for (@_) {
       s/\n//g;
   }
}

my %type_map;
sub read_types {
   my @types = split /\n/, shift;
   my $filename = shift;
   my $line = shift;
   for (@types) {
       ++$line;
       # Remove // comments.
       s/\/\/.*//g;

       # Skip blank lines.
       next if /^\s*$/;

       my ($type,$code) =
           m|(\w*(?:\s*\*)?)
             \s*=>\s*
             (.*)
             |x;
       if (not $type) {
           report_error($filename, $line, "bad type declaration");
       }
       clean_type($type);
       $type_map{$type} = $code;
   }
}

# Scrape the symbol names of the operators from opsymbols.h.
my %opsymbols = ();
open(my $opsyms, $opsymbolsFile) ||
       die("Couldn't open $opsymbolsFile");
while (<$opsyms>) {
   if (m/^OPSYMBOL\(\"(.*)\", ([A-Za-z_]+)\);/) {
       $opsymbols{ $1 } = $2;
   }
}

close($opsyms);

# Turn a name into a symbol.
sub symbolize {
   my $name = shift;
   if ($name =~ /^[A-Za-z0-9_]+$/) {
       return "SYM($name)";
   }
   if ($opsymbols{ $name }) {
       return $opsymbols{ $name };
   }
   if ($name =~ /operator (\w+)/ && $opsymbols{ $1 }) {
     return $opsymbols{ $1 }
   }
   return "symbol::trans(\"" . $name . "\")"
}

sub asy_params {
   my $params = shift;
   my @params = split m/,\s*/, $params;
   my $filename = shift;
   my $line = shift;
   for (@params) {
       my ($explicit, $type, $name, $default) =
           m|^\s*
             (explicit)*\s*(\w*(?:\s*\*)?)
             \s*
             (\w*)(=*)|xs;
       clean_type($type);
       if (not $type_map{$type}) {
           assoc_error($filename, $line, $type);
       }
       $_ = "formal(" . $type_map{$type} . ", " .
       symbolize(lc($name)) . ", " .
           ($default ? "true" : "false") . ", " .
           ($explicit ? "true" : "false") . ")";
   }
   return @params;
}

sub c_params {
  my @params = @_;
  for (@params) {
      my ($explicit, $type, $name, $default, $value) =
           m|^\s*
             (explicit)*\s*(\w*(?:\s*\*)?)
             \s*
             (\w*)(=*)([\w.+\-]*)|xs;
      $_ = "  $type $name=vm::pop" . ($type =~ /^item$/ ? "" : "<$type>") .
          "($stack" . ($default ? "," . $value : "") . ");\n";
  }
  reverse @params;
}

$/ = "\f\n";

open STDIN, "<$srcTemplateDir/$prefix.in" or die "can't open input file $srcTemplateDir/$prefix.in";
open BASE, "<$runtimeBaseFile" or die "can't open $runtimeBaseFile";
open STDOUT, ">$outSrcFile" or die "can't open output file $outSrcFile";

binmode STDIN, ":unix:crlf";
binmode BASE, ":unix:crlf";

my $autogenerated=
"/***** Autogenerated from $prefix.in; changes will be overwritten *****/\n\n";

my $basesource_line = 1;
my $source_line = 1;

print $autogenerated;

print "#line $basesource_line \"$srcTemplateDir/runtimebase.in\"\n";
my $baseheader = <BASE>;
print $baseheader;
$basesource_line += ($baseheader =~ tr/\n//);;
my $basesource_type_line = $basesource_line;

print "#line $source_line \"$srcTemplateDir/$prefix.in\"\n";
my $header = <>;
print $header;
$source_line += ($header =~ tr/\n//);;
my $source_type_line = $source_line;

my $basetypes = <BASE>;
$basesource_line += ($basetypes =~ tr/\n//);;

my $types = <>;
$source_line += ($types =~ tr/\n//);;

print "#line $basesource_line \"$srcTemplateDir/runtimebase.in\"\n";
$baseheader = <BASE>;
print $baseheader;
$basesource_line += ($baseheader =~ tr/\n//);;

print "#line $source_line \"$prefix.in\"\n";
$header = <>;
print $header;
$source_line += ($header =~ tr/\n//);;

print "\n#ifndef NOSYM";
print "\n#include \"$prefix.symbols.h\"\n";
print "\n#endif";
print "\nnamespace run {\n";

read_types($basetypes, "runtimebase.in", $basesource_type_line);

read_types($types, "$prefix.in", $source_type_line);

### Begining of `$prefix.h'
my @header;
push @header, $autogenerated;
# TODO: Capitalize prefix
push @header, "#pragma once\n";
push @header, "namespace run {\n";

my $count = 0;
my @builtin;
while (<>) {
 my ($comments,$type,$name,$cname,$params,$code) =
   m|^((?:\s*//[^\n]*\n)*) # comment lines
     \s*
     (\w*(?:\s*\*)?)   # return type
     \s*
     ([^(:]*)\:*([^(]*) # function name
     \s*
     \(([\w\s*,=.+\-]*)\)  # parameters
     \s*
     \{(.*)}           # body
     |xs;

 if (not $type) {
     report_error("$prefix.in", $source_line, "bad function definition");
 }

 if($cname) {push @header, "void $cname(vm::stack *);\n";}
 else {$cname="gen_$prefix${count}";}  # Unique C++ function name

 clean_type($type);

 my @params = split m/,\s*/, $params;

 # Build addFunc call for asymptote
 if($name) {
 $name =~ s/Operator\s*//;
 if (not $type_map{$type}) {
     assoc_error("$prefix.in", $source_line, $type);
 }
 my @asy_params = asy_params($params, "$prefix.in", $source_line);
 push @builtin, "#line $source_line \"$srcTemplateDir/$prefix.in\"\n"
     . "  addFunc(ve, run::" . $cname
     . ", " . $type_map{$type}
     . ", " . symbolize($name)
     . ( @params ? ", " . join(", ",@asy_params)
                  : "" )
     . ");\n";
 }

 # Build REGISTER_BLTIN command for builtin functions which are not added to
 # the environment.
 if (not $name and $cname) {
   push @builtin, "#line $source_line \"$srcTemplateDir/$prefix.in\"\n"
     . "  REGISTER_BLTIN(run::" . $cname
     . ',"' . $cname . '"' . ");\n";
 }

 # Handle marshalling of values to/from stack
 my $qualifier = ($type eq "item" ? "" : "<$type>");
 $code =~ s/\breturn ([^;]*);/{$stack->push$qualifier($1); return;}/g;
 my $args = join("",c_params(@params));

 print $comments;
 my $ncomments = ($comments =~ tr/\n//);
 $source_line += $ncomments;
 print "#line $source_line \"$srcTemplateDir/$prefix.in\"\n";
 my $prototype=$type . " " . $name . "(" . $params . ");";
 my $nprototype = ($prototype =~ tr/\n//)+1;
 $source_line += $nprototype;
 if($name) {
   clean_params($prototype);
   print "// $prototype\n";
 }
 print "void $cname(stack *";
 if($type ne "void" or $params ne "") {print $stack;}
 print ")\n{\n$args";
 print "#line $source_line \"$srcTemplateDir/$prefix.in\"";
 print "$code}\n\n";

 $source_line -= $ncomments+$nprototype;
 $source_line += ($_ =~ tr/\n//);
 ++$count;
}

print "} // namespace run\n";

print "\nnamespace trans {\n\n";
print "void gen_${prefix}_venv(venv &ve)\n{\n";
print @builtin;
print "}\n\n";
print "} // namespace trans\n";

### End of `header.h'
push @header, "}\n\n";

undef $/;
my $orig_header = "";
my $HEADER;
if (-e $outHeaderFile) {
   open $HEADER, "<", $outHeaderFile;
   $orig_header = <$HEADER>;
   close $HEADER;
}

my $new_header = join "", @header;
if ($new_header ne $orig_header) {
       open $HEADER, ">", $outHeaderFile;
       print $HEADER $new_header;
   close $HEADER;
}

if ($errors) {
 unlink($outHeaderFile);
 unlink($outSrcFile);
}
exit($errors);