#!/usr/bin/perl

use Term::ReadLine;
use Data::Dumper;
use Getopt::Long;
use Data::Dumper;

use strict;
use warnings;

my (@stack, @substack, @backup, @subbackup, @hist, @register);
my $term      = Term::ReadLine->new('rpn calc');
my $debug     = 0;
my $showstack = 1;
my $mgt       = 0;
my $tty       = 1;
my $VERSION   = '1.10';
my $sub       = 0;
my $maxstack  = 10;
my $maxreg    = 5;
my $silent    = 1;
my $op;

# management commands, always lower case letters or words
my %commands = (
                # stack commands
                s   => sub { $mgt = 1; dumpstack(); $mgt = 0; },
                sa  => sub { $mgt = 1; dumpstack(1); $mgt = 0;},
                sc  => sub { clearstack(); },
                scx => sub { clearstack(1); dumpstack(); },
                sr  => sub { reversestack(); },
                srt => sub { rotatestack(); },
                # collector
                '(' => sub { $sub = 1 },
                ')' => sub { stack2sub(); },
                # register stuff
                r   => sub { last_to_reg(); dumpstack(); },
                rcx => sub { clearreg(1); dumpstack(); },
                rc  => sub { clearreg(); },
                # main
                '?' => sub { help(); },
                u   => sub { undo(); dumpstack(); },
                h   => sub { showhist(); },
                q   => sub { exit; },
                # toggles
                td  => sub { $debug ^= 1; },
                ts  => sub { $showstack ^= 1; },
                # functions
                fs  => sub { showfuncs(); },
               );

# executed 1:1, or aliased
my %alias   = qw(^ ** x ^ < << > >> + + - - / / * * & & | |);

# holds user functions
my %custom;

# hand coded functions
my %func    = (
               '%' => sub {
                 # X % of Y
                 my ($a, $b) = getlast(2);
                 if (defined $b) {
                   return "($a / 100) * $b";
                 }
               },

               '%d' => sub {
                 # percentual difference
                 my ($a, $b) = getlast(2);
                 if (defined $b) {
                   return "(($a - $b) / $b) * 100"
                 }
               },

               '%+' => sub {
                 # Y + (X $ of Y)
                 my ($a, $b) = getlast(2);
                 if (defined $b) {
                   return "$a + (($a / 100) * $b)";
                 }
               },

               '%-' => sub {
                 # Y - (X $ of Y)
                 my ($a, $b) = getlast(2);
                 if (defined $b) {
                   return "$a - (($a / 100) * $b)";
                 }
               },

               'v' => sub {
                 # square root
                 my ($a) = getlast(1);
                 if (defined $a) {
                   return "$a ** (1 / 2)";
                 }
               },

               'm' => sub {
                 # median
                 my @values = getlast(2); # we need 2 or all in sub mode
                 if (scalar @values >= 2) {
                   my $c = $#values;
                   if (scalar @values % 2 == 0) {
                     # even
                     return "((sort qw(@values))[$c / 2] + (sort qw(@values))[($c / 2) + 1]) / 2";
                   }
                   else {
                     # uneven
                     return "(sort qw(@values))[$c / 2]";
                   }
                 }
                 else {
                   print "median only possible with 2 or more values\n";
                   undo();
                   return 0;
                 }
               },

               'a' => sub {
                 # average
                 my @values = getlast(2); # we need 2 or all in sub mode
                 if (scalar @values > 1) {
                   return "(" . join(' + ', @values) . ") / " . scalar @values;
                 }
                 else {
                   print "average only possible with 2 or more values\n";
                   undo();
                   return 0;
                 }
               },

               # converters:
               # gallons to liters
               'tl' => sub { return convert("* 3.785") },
               # yards to meters
               'tm' => sub { return convert("* 91.44") },
               # miles to kilometers
               'tk' => sub { return convert("* 1.609") },
               # inches to cm
               'tc' => sub { return convert("* 2.54") },
               # to 'bytes
               'tkb' => sub { return convert("/ 1000") },
               'tmb' => sub { return convert("/ 1000 / 1000") },
               'tgb' => sub { return convert("/ 1000 / 1000 / 1000") },
               'ttb' => sub { return convert("/ 1000 / 1000 / 1000 / 1000") },
              );

# math constants, always upper case letters, usable via eval{}
use constant PI => 3.141592653589793;
use constant V2 => 1.414213562373095;
use constant V3 => 1.732050807568877;

# handle command line
my ($o_h, $o_v, $o_s);
Getopt::Long::Configure( qw(no_ignore_case));
if (! GetOptions (
                  "version|v"          => \$o_v,
                  "help|h"             => \$o_h,
                  "debug|d"            => \$debug,
                  "nostack|n"          => \$o_s
                 )    ) {
  help();
  exit;
}

if ($o_v) {
  print "$0 version $VERSION\n";
  exit;
}

if ($o_h) {
  help();
  exit;
}

if ($o_s) {
  $showstack = 0;
}

# load config, if any
if (-s "$ENV{HOME}/.rpnc") {
  if (open RC, "< $ENV{HOME}/.rpnc") {
    while (<RC>) {
      chomp();
      next if (/^\s*#/ || /^\s*$/);
      looptokenize($_);
    }
    close RC;
    $silent = 0;
  }
}


# run in commandline mode?
$op = shift;
if ($op) {
  $tty = 0;
  while (<STDIN>) {
    chomp;
    push @stack, split /\s\s*/;
  }
  print calc($op);
  exit;
}

# else: run interactively
# main
my $OUT = $term->OUT || \*STDOUT;
while ( defined ($_ = $term->readline(prompt())) ) {
  looptokenize($_);
}


1;


# converter helper
sub convert {
  my $code = shift;
  my ($a) = getlast(1);
  if (defined $a) {
    return "$a $code";
  }
}


sub looptokenize {
  # disassemble user input into tokens
  my $tokens = shift;

  if ($tokens =~ /^f\s/) {
    # function definition
    defun($tokens);
  }
  else {
    foreach my $tok (split /\s+/, $tokens) {
      if ($tok =~ /^-?[A-Z\.\d]+?$/) {
        # number or register fetch
        if ($tok =~ /^R(\d+?)/) {
          # fetch number from register $1 and put it to stack
          my $r = getreg($1);
          if ($r) {
            pushstack($r);
          }
          else {
            print "invalid register index!\n";
            next;
          }
        }
        else {
          # put number to stsack
          pushstack($tok);
        }
        dumpstack();
      }
      else {
        # operator or command, execute
        if (exists $commands{$tok}) {
          cmd($tok);
        }
        else {
          print calc($tok);
        }
      }
    }
  }
}

sub cmd {
  my $c = shift;

  if (exists $commands{$c}) {
    my $sub = $commands{$c};
    &$sub;
  }
  else {
    print "unknown command '$c'!\n";
  }
}

sub showhist {
  foreach my $entry (@hist) {
    printf "History: %10s = %s\n", $entry->[0], $entry->[1];
  }
}

sub clearstack {
  my $one = shift;

  backup();

  if ($sub) {
    if ($one) {
      pop @substack;
    }
    else {
      @substack = ();
    }
  }
  else {
    if ($one) {
      pop @stack;
    }
    else {
      @stack = ();
    }
  }
}

sub reversestack {
  backup();
  if ($sub) {
    @substack = reverse @substack;
  }
  else {
    @stack = reverse @stack;
  }
  dumpstack();
}

sub rotatestack {
  backup();
  if ($sub) {
    my $f = shift @substack;
    @substack = (@substack, $f);
  }
  else {
    my $f = shift @stack;
    @stack = (@stack, $f);
  }
  dumpstack();
}

sub pushstack {
  my $num = shift;
  if ($num) {
    if ($num =~ /^\./) {
      $num = '0' . $num;
    }
    if ($sub) {
      push @substack, $num;
    }
    else {
      push @stack, $num;
    }
  }
}

sub dumpstack {
  if (! $showstack && !$mgt) {
    return;
  }

  my $max    = shift;
  my $x      = ' ';
  my $prefix = 'stack';
  my @all;

  if ($sub) {
    @all = @substack;
    $prefix = 'collectorstack';
  }
  else {
    @all = @stack;
  }

  my $abs = scalar @all;
  if (! $max && $abs > $maxstack) {
    my $min = $max - ($max * 2);
    @all = @all[$min .. -1];
    printf "%s    [..]\n", $prefix;
  }

  if (@register) {
    my $p = 1;
    foreach my $n (@register) {
      printf "register  R%d: %s\n", $p++, $n;
    }
  }
  print "\n";

  my $p = scalar @all;
  foreach my $n (@all) {
    $x = 'X' if($p == 1);
    printf "%s %s %4d: %s\n", $prefix, $x, $p--, $n;
  }
  print "\n";
}

sub undo {
  if ($sub) {
    @substack = @subbackup;
  }
  else {
    @stack = @backup;
  }
}

sub backup {
  if ($sub) {
    @subbackup = @substack;
  }
  else {
    @backup = @stack;
  }
}


sub getlast {
  # return and remove last 1, 2 or all elements of current stack
  my $request = shift;

  my @all = ();

  backup();

  if ($sub) {
    # ignore request count
    @all = @substack;
    @substack = ();
  }
  else {
    if (@stack) {
      if (scalar @stack == 1) {
        if ($request > 1) {
          print "At least $request variables must be on the stack!\n";
        }
        else {
          @all = pop @stack;
        }
      }
      elsif (scalar @stack >= 2) {
        @all = splice(@stack, -1 * $request, $request);
      }
    }
    else {
      print "Please enter one or more numbers to operate on!\n";
    }
  }

  return @all;
}

sub getreg {
  # fetch $n'th element from register
  my $n = shift;
  if ($n <= scalar @register) {
    return $register[$n-1];
  }
  else {
    return 0;
  }
}

sub last_to_reg {
  # put last stack element to register
  my $n;
  if ($sub) {
    if (@substack) {
      $n = $substack[-1];
    }
  }
  else {
    if (@stack) {
      $n = $stack[-1];
    }
  }

  if ($n) {
    if (scalar @register == $maxreg) {
      shift @register;
    }
    push @register, $n;
  }
}

sub clearreg {
  my $one = shift;

  if ($one) {
    pop @register;
  }
  else {
    @register = ();
  }
}

sub stack2sub {
  if (! $sub && scalar @substack == 0 && scalar @stack > 1) {
    # not in collector mode, empty substack, move stack to substack, enter collect
    backup();
    @substack = @stack;
    @stack = ();
    $sub = 1;
  }
  else {
    # leave collector mode
    $sub = 0;
  }
}

sub prompt {
  my $count;
  my $prompt;

  if ($sub) {
    $count  = scalar @substack;
    $prompt = '%--(';
  }
  else {
    $count  = scalar @stack;
    $prompt = '%';
  }

  return sprintf "%3d %s ", $count, $prompt;
}

sub calc {
  my $op = shift;
  my $res;
  my $code;

  if (exists $alias{$op}) {
    my @last = getlast(2);
    $op = $alias{$op};
    $code = join(" $op ",  @last);
  }
  elsif (exists $func{$op}) {
    my $sub = $func{$op};
    $code = &$sub();
    return unless $code;
  }
  else {
    print "syntax error or unknown command ($op)!\n";
    undo();
    return;
  }

  # execute
  eval "\$res = $code";

  if ($@) {
    # error, reset stack
    print "Syntax error: $@, resetting stack\n";
    undo();
  }
  else {
    push @stack, $res;
    $sub = 0;

    if ($debug) {
      print "DEBUG: $code = $res\n";
    }

    if ($tty) {
      dumpstack();
      push @hist, [$res, $code];
      return "=> $res\n\n";
    }
    else {
      return "$res\n";
    }
  }
}


sub defun {
  # define a function, use N1 .. NN as function arguments
  my $code = shift;
  my ($op, $name, @tokens) = split /\s\s*/, $code;

  if ($name !~ /^[a-zA-Z0-9_]+$/) {
    print "invalid function name (a-z0-9_)!\n";
    return;
  }

  if (! exists $custom{$name}) {
    # no need to check twice and overwriting of custom function must be legal
    if (grep {$name eq $_} keys %commands) {
      print "reserved function name (command)!\n";
      return;
    }

    if (grep {$name eq $_} keys %func) {
      print "reserved function name (function)!\n";
      return;
    }
  }

  $custom{$name} = "@tokens";

  if ($custom{$name} =~ /^\{.*\}$/) {
    # perl code
    $func{$name} = sub { return eval "@tokens" };
  }
  else {
    # rpnc code
    $func{$name} = sub {
      my $max = scalar @_;
      my @args = reverse(@_);

      # replace N1..NN with actual stack items
      my @body;
      foreach my $item (@tokens) {
        if ($item =~ /^([A-Z])(\d+)$/) {
          my $letter = $1;
          my $i = $2;
          if ($i <= $max) {
            push @body, $args[$i-1];
          }
          else {
            print "undefined variable ${letter}${i}!\n";
            push @body, 0;
          }
        }
        else {
          push @body, $item;
        }
      }

      # execute @body
      looptokenize("@body");
    };
  }

  print "function $name() defined.\n" unless $silent;
}

sub showfuncs {
  foreach my $f (sort keys %custom) {
    print "Function $f():\n   $custom{$f}\n\n";
  }
}

sub help {
  print qq~
Reverse Polish Notation Calculator, version $VERSION.
Copyleft (L) 2019-2020 - Thomas von Dein.
Licensed under the terms of the GPL 3.0.

Commandline: rpn [-d] [<operator>]

If <operator> is provided, read numbers from STDIN,
otherwise runs interactively.

Configure:                                Available math operators:
 td   toggle debugging (-d)                (      enter collect mode
 ts   toggle display of stack (-n)         )      leave collect || stack => collect
                                           +      add
Stack Management:                          -      substract
 s    show the stack                       /      divide
 sa   show the whole stack                 *      multiply
 scx  clear X (last stack element)         ^      expotentiate
 sc   clear stack                          %      percent (%+  add %- substract)
 sr   reverse the stack                    %d     percentual difference
 srt  rotate the stack                     &      bitwise AND
                                           |      bitwise OR
Register Management:                       x      bitwise XOR
 r    put X to register                    < >    bitwise shift left or right
 R1-9 push value of register to stack      v      square root
 rcx  clear X (last register element)      m      median
 rc   clear register                       a      average

Converters:
 tl   gallons => liters                    tkb  bytes => kb
 tk   miles   => kilometers                tmb  bytes => mb
 tm   yards   => meters                    tgb  bytes => gb
 tc   inches => centimeters                ttb  bytes => tb

Various Commands:                         Functions:
 u    undo last operation                 f     <name> op op... (use N1..NN for stack)
 h    show history of past operations     fs    show list of defined functions
 q    finish (C-d works as well)          Using register: enter R + index, e.g. R1
 ?    print help                          Constants: PI V2 V3

~;
}
