"""
Dice - roll dice when asked, intended for RPGs
Copyright (C) 2010  Brian S. Stephan

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 3 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, see <http://www.gnu.org/licenses/>.
"""

import math
import re
import random

from extlib import irclib

from Module import Module

import ply.lex as lex
import ply.yacc as yacc

class Dice(Module):

    """Roll simple or complex dice strings."""

    tokens = ['NUMBER', 'TEXT']
    literals = ['#', '/', '+', '-', 'd', ';']

    def build(self):
        lex.lex(module=self)
        yacc.yacc(module=self)

    t_TEXT = r'\s+[^;]+'

    def t_NUMBER(self, t):
        r'\d+'
        t.value = int(t.value)
        return t

    def t_error(self, t):
        t.lexer.skip(1)

    precedence = (
        ('left', ';'),
        ('left', '+', '-'),
        ('right', 'd'),
        ('left', '#'),
        ('left', '/')
    )

    output = ""

    def roll_dice(self, keep, dice, size):
        """Takes the parsed dice string for a single roll (eg 3/4d20) and performs
        the actual roll. Returns a string representing the result.
        """

        a = range(dice)
        for i in range(dice):
            a[i] = random.randint(1, size)
        if keep != dice:
            b = sorted(a, reverse=True)
            b = b[0:keep]
        else:
            b = a
        total = sum(b)
        outstr = "[";
        if len(b) != len(a):
            # Bold is \002
            outstr += ",".join(str(i) for i in b) + ","
            temp = sorted(a, reverse=True)
            temp = temp[keep:]
            bstr = ",".join(str(i) for i in temp)
            outstr += bstr
        else:
            outstr += ",".join(str(i) for i in a)
        outstr += "]"	
        
        return (total, outstr)

    def process_roll(self, trials, mods, comment):
        """Processes rolls coming from the parser.

        This generates the inputs for the roll_dice() command, and returns
        the full string representing the whole current dice string (the part
        up to a semicolon or end of line).
        """

        output = ""
        repeat = 1
        if trials != None:
            repeat = trials
        for i in range(repeat):
            mode = 1
            total = 0
            curr_str = ""
            if i > 0:
                output += ", "
            for m in mods:
                keep = 0
                dice = 1
                res = 0
                # if m is a tuple, then it is a die roll
                # m[0] = (keep, num dice)
                # m[1] = num faces on the die
                if type(m) == tuple:
                    if m[0] != None:
                        if m[0][0] != None:
                            keep = m[0][0]
                        dice = m[0][1]
                    size = m[1]
                    if keep > dice or keep == 0:
                        keep = dice
                    res = self.roll_dice(keep, dice, size)
                    curr_str += "%d%s" % (res[0], res[1])
                    res = res[0]
                elif m == "+":
                    mode = 1
                    curr_str += "+"
                elif m == "-":
                    mode = -1
                    curr_str += "-"
                else:
                    res = m
                    curr_str += str(m)
                total += mode * res
            output += "%d (%s)" % (total, curr_str)
        if comment != None:
            output = "%s: %s" % (comment.strip(), output)
        return output

    def p_roll_r(self, p):
        # Chain rolls together.

        # General idea I had when creating this grammar: A roll string is a chain
        # of modifiers, which may be repeated for a certain number of trials. It can
        # have a comment that describes the roll
        # Multiple roll strings can be chained with semicolon

        'roll : roll ";" roll'
        global output
        p[0] = p[1] + "; " + p[3]
        output = p[0]

    def p_roll(self, p):
        # Parse a basic roll string.

        'roll : trial modifier comment'
        global output
        mods = []
        if type(p[2]) == list:
            mods = p[2]
        else:
            mods = [p[2]]
        p[0] = self.process_roll(p[1], mods, p[3])
        output = p[0]

    def p_roll_no_trials(self, p):
        # Parse a roll string without trials.

        'roll : modifier comment'
        global output
        mods = []
        if type(p[1]) == list:
            mods = p[1]
        else:
            mods = [p[1]]
        p[0] = self.process_roll(None, mods, p[2]) 
        output = p[0]

    def p_comment(self, p):
        # Parse a comment.

        '''comment : TEXT
                   |'''
        if len(p) == 2:
            p[0] = p[1]
        else:
            p[0] = None

    def p_modifier(self, p):
        # Parse a modifier on a roll string.

        '''modifier : modifier "+" modifier
                    | modifier "-" modifier'''
        # Use append to prevent nested lists (makes dealing with this easier)
        if type(p[1]) == list:
            p[1].append(p[2])
            p[1].append(p[3])
            p[0] = p[1]
        elif type(p[3]) == list:
            p[3].insert(0, p[2])
            p[3].insert(0, p[1])
            p[0] = p[3]
        else:
            p[0] = [p[1], p[2], p[3]]

    def p_die(self, p):
        # Return the left side before the "d", and the number of faces.

        'modifier : left NUMBER'
        p[0] = (p[1], p[2])

    def p_die_num(self, p):
        'modifier : NUMBER'
        p[0] = p[1]

    def p_left(self, p):
        # Parse the number of dice we are rolling, and how many we are keeping.
        'left : keep dice'
        if p[1] == None:
            p[0] = [None, p[2]]
        else:
            p[0] = [p[1], p[2]]

    def p_left_all(self, p):
        'left : dice'
        p[0] = [None, p[1]]

    def p_left_e(self, p):
        'left :'
        p[0] = None

    def p_total(self, p):
        'trial : NUMBER "#"'
        if len(p) > 1:
            p[0] = p[1]
        else:
            p[0] = None

    def p_keep(self, p):
        'keep : NUMBER "/"'
        if p[1] != None:
            p[0] = p[1]
        else:
            p[0] = None

    def p_dice(self, p): 
        'dice : NUMBER "d"'
        p[0] = p[1]

    def p_dice_one(self, p):
        'dice : "d"'
        p[0] = 1

    def p_error(self, p):
        # Provide the user with something (albeit not much) when the roll can't be parsed.
        global output
        output = "Unable to parse roll"

    def get_result(self):
        global output
        return output

    def do(self, connection, event, nick, userhost, what, admin_unlocked):
        match = re.search('^!roll\s+(.*)$', what)
        if match:
            dicestr = match.group(1)
            self.build()
            yacc.parse(dicestr)
            reply = self.get_result()
            if reply is not "":
                return nick + ': ' + reply

        match = re.search('^!ctech\s+(.*)$', what)
        if match:
            rollitrs = re.split(';\s*', match.group(1))
            reply = ""
            for count, roll in enumerate(rollitrs):
                pattern = '^(\d+)d(?:(\+|\-)(\d+))?(?:\s+(.*))?'
                regex = re.compile(pattern)
                matches = regex.search(roll)
                if matches is not None:
                    dice = int(matches.group(1))
                    comment = ''
                    modifier = 0

                    if matches.group(2) is not None and matches.group(3) is not None:
                        if str(matches.group(2)) == '-':
                            modifier = -1 * int(matches.group(3))
                        else:
                            modifier = int(matches.group(3))

                    if matches.group(4) is not None:
                        comment = matches.group(4)

                    result = roll + ': '

                    rolls = []
                    for d in range(dice):
                        rolls.append(random.randint(1, 10))
                    rolls.sort()
                    rolls.reverse()

                    # highest single die method
                    method1 = rolls[0]

                    # highest set method
                    method2 = 0
                    rolling_sum = 0
                    for i, r in enumerate(rolls):
                        # if next roll is same as current, sum and continue, else see if sum is best so far
                        if i+1 < len(rolls) and rolls[i+1] == r:
                            if rolling_sum == 0:
                                rolling_sum = r
                            rolling_sum += r
                        else:
                            if rolling_sum > method2:
                                method2 = rolling_sum
                            rolling_sum = 0
                    # check for set in progress (e.g. lots of 1s)
                    if rolling_sum > method2:
                        method2 = rolling_sum

                    # straight method
                    method3 = 0
                    rolling_sum = 0
                    count = 0
                    for i, r in enumerate(rolls):
                        # if next roll is one less as current, sum and continue, else check len and see if sum is best so far
                        if i+1 < len(rolls) and rolls[i+1] == r-1:
                            if rolling_sum == 0:
                                rolling_sum = r
                                count += 1
                            rolling_sum += r-1
                            count += 1
                        else:
                            if count >= 3 and rolling_sum > method3:
                                method3 = rolling_sum
                            rolling_sum = 0
                    # check for straight in progress (e.g. straight ending in 1)
                    if count >= 3 and rolling_sum > method3:
                        method3 = rolling_sum

                    # get best roll
                    best = max([method1, method2, method3])

                    # check for critical failure
                    botch = False
                    ones = 0
                    for r in rolls:
                        if r == 1:
                            ones += 1
                    if ones >= math.ceil(float(len(rolls))/2):
                        botch = True

                    if botch:
                        result += 'BOTCH'
                    else:
                        result += str(best + modifier)
                    rollres = ''
                    for i,r in enumerate(rolls):
                        rollres += str(r)
                        if i is not len(rolls)-1:
                            rollres += ','
                    result += ' [' + rollres
                    if modifier != 0:
                        if modifier > 0:
                            result += ' +' + str(modifier)
                        else:
                            result += ' -' + str(modifier * -1)
                    result += ']'

                    reply += result
                    if count is not len(rollitrs)-1:
                        reply += "; "
            if reply is not "":
                return nick + ': ' + reply

# vi:tabstop=4:expandtab:autoindent
# kate: indent-mode python;indent-width 4;replace-tabs on;