#!/usr/bin/env python
#
# The Dice Machine
# Copyright (C) 2005, 2013 Nicholas J. Haggin
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

import sys
import os
try:
    import readline
except ImportError:
    print 'Warning: Could not load module readline.'
    print 'Tab completion and command history not available.\n'
    pass
import string
import cStringIO
import re
import cmd
import random

# A parser for D&D-style dice expressions.
class DiceExpressionParser(object):
    # States for our lexer.
    START = 0
    INT = 1

    # Token values for our lexer.
    DTOK = 256
    ARITHTOK = 257
    INTTOK = 258
    EOS = 259

    # Constructor.
    def __init__(self):
        self.cursign = "+"

    # Lexer for D&D-style dice roll expressions.
    def lexer(self, input):
        lexeme = ''
        token = -1
        state = self.START

        # Loop to construct tokens.
        while True:
            # Read a character.
            nextchar = input.read(1)
            # This lexer has become an unwashed n00b.
            if state == self.START:
                # We're out of stuff; return end-of-input.
                if not nextchar: return (self.EOS, '')
                # We have a rolls/sides separator.
                elif nextchar == 'd':
                    lexeme = nextchar
                    token = self.DTOK
                    break
                # We have an arithmetic operator.
                elif nextchar in '+-':
                    lexeme = nextchar
                    token = self.ARITHTOK
                    break
                # We eat whitespace.
                elif nextchar in string.whitespace:
                    continue
                # Assemble an integer.
                elif nextchar in string.digits:
                    lexeme = nextchar
                    state = self.INT
            # This lexer has slain a digit and gained skill.
            elif state == self.INT:
                # We're out of string. Token is complete.
                if not nextchar:
                    token = self.INTTOK
                    break
                # We're at the end of an integer.
                if nextchar in string.whitespace + 'd+-':
                    if nextchar in 'd+-':
                        input.seek(-1, 1)
                    token = self.INTTOK
                    break
                # Keep assembling an integer.
                elif nextchar in string.digits:
                    lexeme = lexeme + nextchar

        return (token, lexeme);

    # Top-level grammar production. Match a "roll sequence."
    def parse(self, input, pairs):
        self.cursign = "+"
        # Match a roll.
        baseroll = self.match_roll(input)
        if not baseroll: return None
        pairs.append((self.cursign, baseroll[0], baseroll[1]))

        next_tok, lexeme = self.lexer(input)
        if next_tok == self.EOS:
            return True
        # Match an arithmetic operator and then a roll.
        else:
            if next_tok == self.ARITHTOK:
                self.cursign = lexeme
                self.parse(input, pairs)
                return True
            else:
                print 'Parse error'
                return None

    # Other grammar production. Match a single roll.
    def match_roll(self, input):
        # Match int or d.
        next_tok, lexeme = self.lexer(input)
        if next_tok == self.INTTOK:
            rolls = lexeme
        elif next_tok == self.DTOK:
            rolls = '1'
        else:
            print 'Parse error'
            return None

        # Match d or int.
        next_tok, lexeme = self.lexer(input)
        if next_tok == self.DTOK:
            next_tok, lexeme = self.lexer(input)
            if next_tok != self.INTTOK:
                print 'Parse error'
                return None
        elif next_tok == self.INTTOK: pass
        else:
            print 'Parse error'
            return None

        sides = lexeme

        return (rolls, sides)

# The command interpreter. For each command foo, there is:
#
#   -A do_foo() function that does the operation
#   -If help is provided, a help_foo() function that shows the help
   
class dice_cmd(cmd.Cmd):
    pattern = re.compile('([0-9]*)d([0-9]+)')
    def __init__(self):
        cmd.Cmd.__init__(self)
        self.intro = """
The Dice Machine
Copyright (C) 2005, 2013 Nicholas J. Haggin

The Dice Machine comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it under certain
conditions. See http://www.gnu.org/licenses/gpl.html for more information.

Type 'help' to see a list of commands, or 'help command' for
help on an individual command.
"""
        self.prompt = '$ '
        self.ruler = '-'
        self.parser = DiceExpressionParser()
        random.seed(26401)

    # Don't do anything when the user gives an empty line.
    def emptyline(self):
        return None

    def help_help(self):
        print """
Syntax: help
Display a list of commands.

Syntax: help <command>
Get help on a particular command.
"""

    # Parse and evaluate the roll string.
    def make_rolls(self, args, outputlevel = 0):
        # Initialize.
        io = cStringIO.StringIO(args)
        rollpairs = []

        # Parse and evaluate.
        if self.parser.parse(io, rollpairs):
            accum = 0
            for pair in rollpairs:
                iter = int(pair[1])
                sides = int(pair[2])

                if outputlevel == 1:
                    print 'Rolling ' + pair[1] + 'd' + pair[2]
                for i in range(0, iter):
                    foo = random.randint(1, sides)
                    if outputlevel == 1:
                        print foo
                    exec('accum = accum ' + pair[0] + ' foo')

            print 'Final Result:', accum

    def do_roll(self, args):
        self.make_rolls(args)
        
    def help_roll(self):
        print """
Syntax: roll <pattern>
Generate a roll of the dice according to said pattern.
The pattern is a single D&D-style <rolls>d<sides>, i.e.,
4d8 = four rolls of an octahedral die, or a series of such
expressions added or subtracted.
"""

    def do_rollv(self, args):
        self.make_rolls(args, outputlevel = 1)

    def help_rollv(self):
        print """
Syntax: rollv <pattern>
Operates identically to roll, but shows each individual roll
as well as the combined result.
"""

    def do_shell(self, args):
        os.system(args)

    def help_shell(self):
        print """
Syntax: !<command>
Execute the command under the system shell.
"""

    def do_exit(self, args):
        sys.exit(0)

    def help_exit(self):
        print '\nSyntax: exit | quit\nQuit the program.\n'

    def do_quit(self, args):
        sys.exit(0)

    def help_quit(self):
        self.help_exit()

# Run the command-line interface.
def run_cli():
    cmd_interp = dice_cmd()
    cmd_interp.cmdloop()

if __name__ == '__main__':
    run_cli()
