"""Dice rollers used by the views, bots, etc."""
import logging
import random

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

logger = logging.getLogger(__name__)


class DiceRoller(object):

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

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

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

    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', 'ROLLSEP'),
        ('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 = list(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 = "[" + ",".join(str(i) for i in a) + "]"

        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).
        """
        rolls = mods[0][0][1] if mods[0][0][1] else 1
        if trials:
            assert trials <= 10, "Too many rolls (max: 10)."
        assert rolls <= 50, "Too many dice (max: 50) in roll."

        output = ""
        repeat = 1
        if trials is not 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] is not None:
                        if m[0][0] is not None:
                            keep = m[0][0]
                        dice = m[0][1]
                    size = m[1]
                    if keep > dice or keep == 0:
                        keep = dice
                    assert size >= 1, f"Die must have at least one side."
                    assert dice >= 1, f"At least one die must be rolled."
                    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
            if repeat == 1:
                if comment is not None:
                    output = "%d %s (%s)" % (total, comment.strip(), curr_str)
                else:
                    output = "%d (%s)" % (total, curr_str)
            else:
                output += "%d (%s)" % (total, curr_str)
                if i == repeat - 1:
                    if comment is not None:
                        output += " (%s)" % (comment.strip())
        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 ROLLSEP 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):
        """Raise ValueError on unparseable strings."""
        raise ValueError("Error occurred in parser")

    def get_result(self):
        global output
        return output

    def do_roll(self, dicestr):
        """
        Roll some dice and get the result (with broken out rolls).

        Keyword arguments:
        dicestr - format:
        N#X/YdS+M label
            N#: do the following roll N times (optional)
            X/: take the top X rolls of the Y times rolled (optional)
            Y : roll the die specified Y times (optional, defaults to 1)
            dS: roll a S-sided die
            +M: add M to the result (-M for subtraction) (optional)
        """
        self.build()
        yacc.parse(dicestr)
        return self.get_result()