import random
import re
import traceback
from heapq import nlargest, nsmallest
from math import floor
from re import IGNORECASE

import numexpr

from . import errors
#from funcs import logThis

VALID_OPERATORS = 'k|rr|ro|mi|ma|ra|e|p'
VALID_OPERATORS_ARRAY = VALID_OPERATORS.split('|')
VALID_OPERATORS_2 = re.compile('|'.join(["({})".format(i) for i in VALID_OPERATORS_ARRAY]))
DICE_PATTERN = re.compile(
    r'^\s*(?:(?:(\d*d\d+)(?:(?:' + VALID_OPERATORS + r')(?:[lh<>]?\d+))*|(\d+)|([-+*/().=])?)\s*(\[.*\])?)(.*?)\s*$',
    IGNORECASE)


def roll_dice(author : str, rollStr : str = "1d20"):
    if rollStr == '0/0':  # easter eggs
        return("What do you expect me to do, destroy the universe?")

    adv = 0
    if re.search('(^|\s+)(adv|dis)(\s+|$)', rollStr) is not None:
        adv = 1 if re.search('(^|\s+)adv(\s+|$)', rollStr) is not None else -1
        rollStr = re.sub('(adv|dis)(\s+|$)', '', rollStr)
    res = roll(rollStr, adv=adv)
    out = res.result
    outStr = author + '  :game_die:\n' + out
    if len(outStr) > 1999:
        outputs = author + '  :game_die:\n[Output truncated due to length]\n**Result:** ' + str(res.plain)
    else:
        outputs = outStr
    return(outputs)

def list_get(index, default, l):
    try:
        a = l[index]
    except IndexError:
        a = default
    return a


def roll(rollStr, adv: int = 0, rollFor='', inline=False, double=False, show_blurbs=True, **kwargs):
    roller = Roll()
    result = roller.roll(rollStr, adv, rollFor, inline, double, show_blurbs, **kwargs)
    return result


def get_roll_comment(rollStr):
    """Returns: A two-tuple (dice without comment, comment)"""
    try:
        comment = ''
        no_comment = ''
        dice_set = re.split('([-+*/().=])', rollStr)
        dice_set = [d for d in dice_set if not d in (None, '')]
        #logThis("Found dice set: " + str(dice_set))
        for index, dice in enumerate(dice_set):
            match = DICE_PATTERN.match(dice)
            #logThis("Found dice group: " + str(match.groups()))
            no_comment += dice.replace(match.group(5), '')
            if match.group(5):
                comment = match.group(5) + ''.join(dice_set[index + 1:])
                break

        return no_comment, comment
    except:
        pass
    return rollStr, ''


class Roll(object):
    def __init__(self, parts=None):
        if parts ==None:
            parts = []
        self.parts = parts

    def get_crit(self):
        """Returns: 0 for no crit, 1 for 20, 2 for 1."""
        try:
            crit = next(p.get_crit() for p in self.parts if isinstance(p, SingleDiceGroup))
        except StopIteration:
            crit = 0
        return crit

    def get_total(self):
        """Returns: int"""
        return numexpr.evaluate(''.join(p.get_eval() for p in self.parts if not isinstance(p, Comment)))

    # # Dice Roller
    def roll(self, rollStr, adv: int = 0, rollFor='', inline=False, double=False, show_blurbs=True, **kwargs):
        try:
            if '**' in rollStr:
                raise errors.InvalidArgument("Exponents are currently disabled.")
            self.parts = []
            # split roll string into XdYoptsSel [comment] or Op
            # set remainder to comment
            # parse each, returning a SingleDiceResult
            dice_set = re.split('([-+*/().=])', rollStr)
            dice_set = [d for d in dice_set if not d in (None, '')]
            #logThis("Found dice set: " + str(dice_set))
            for index, dice in enumerate(dice_set):
                match = DICE_PATTERN.match(dice)
                #logThis("Found dice group: " + str(match.groups()))
                # check if it's dice
                if match.group(1):
                    roll = self.roll_one(dice.replace(match.group(5), ''), adv)
                    self.parts.append(roll)
                # or a constant
                elif match.group(2):
                    self.parts.append(Constant(value=int(match.group(2)), annotation=match.group(4)))
                # or an operator
                elif not match.group(5):
                    self.parts.append(Operator(op=match.group(3), annotation=match.group(4)))

                if match.group(5):
                    self.parts.append(Comment(match.group(5) + ''.join(dice_set[index + 1:])))
                    break

            # calculate total
            crit = self.get_crit()
            try:
                total = self.get_total()
            except SyntaxError:
                raise errors.InvalidArgument("No dice found to roll.")
            rolled = ' '.join(str(res) for res in self.parts if not isinstance(res, Comment))
            if rollFor =='':
                rollFor = ''.join(str(c) for c in self.parts if isinstance(c, Comment))
            # return final solution
            if not inline:
                # Builds end result while showing rolls
                reply = ' '.join(
                    str(res) for res in self.parts if not isinstance(res, Comment)) + '\n**Total:** ' + str(
                    floor(total))
                skeletonReply = reply
                rollFor = rollFor if rollFor != '' else 'Result'
                reply = '**{}:** '.format(rollFor) + reply
                if show_blurbs:
                    if adv == 1:
                        reply += '\n**Rolled with Advantage**'
                    elif adv == -1:
                        reply += '\n**Rolled with Disadvantage**'
                    if crit == 1:
                        critStr = "\n_**Critical Hit!**_  "
                        reply += critStr
                    elif crit == 2:
                        critStr = "\n_**Critical Fail!**_  "
                        reply += critStr
            else:
                # Builds end result while showing rolls
                reply = ' '.join(str(res) for res in self.parts if not isinstance(res, Comment)) + ' = `' + str(
                    floor(total)) + '`'
                skeletonReply = reply
                rollFor = rollFor if rollFor != '' else 'Result'
                reply = '**{}:** '.format(rollFor) + reply
                if show_blurbs:
                    if adv == 1:
                        reply += '\n**Rolled with Advantage**'
                    elif adv == -1:
                        reply += '\n**Rolled with Disadvantage**'
                    if crit == 1:
                        critStr = "\n_**Critical Hit!**_  "
                        reply += critStr
                    elif crit == 2:
                        critStr = "\n_**Critical Fail!**_  "
                        reply += critStr
            reply = re.sub(' +', ' ', reply)
            skeletonReply = re.sub(' +', ' ', str(skeletonReply))
            return DiceResult(result=int(floor(total)), verbose_result=reply, crit=crit, rolled=rolled,
                              skeleton=skeletonReply, raw_dice=self)
        except Exception as ex:
            if not isinstance(ex, (SyntaxError, KeyError, errors.AvraeException)):
                #logThis('Error in roll() caused by roll {}:'.format(rollStr))
                traceback.print_exc()
            return DiceResult(verbose_result="Invalid input: {}".format(ex))

    def roll_one(self, dice, adv: int = 0):
        result = SingleDiceGroup()
        result.rolled = []
        # splits dice and comments
        split = re.match(r'^([^\[\]]*?)\s*(\[.*\])?\s*$', dice)
        dice = split.group(1).strip()
        annotation = split.group(2)
        result.annotation = annotation if annotation != None else ''
        # Recognizes dice
        obj = re.findall('\d+', dice)
        obj = [int(x) for x in obj]
        numArgs = len(obj)

        ops = []
        if numArgs == 1:
            if not dice.startswith('d'):
                raise errors.InvalidArgument('Please pass in the value of the dice.')
            numDice = 1
            diceVal = obj[0]
            if adv != 0 and diceVal == 20:
                numDice = 2
                ops = ['k', 'h1'] if adv ==1 else ['k', 'l1']
        elif numArgs == 2:
            numDice = obj[0]
            diceVal = obj[-1]
            if adv != 0 and diceVal == 20:
                ops = ['k', 'h' + str(numDice)] if adv ==1 else ['k', 'l' + str(numDice)]
                numDice = numDice * 2
        else:  # split into xdy and operators
            numDice = obj[0]
            diceVal = obj[1]
            dice = re.split('(\d+d\d+)', dice)[-1]
            ops = VALID_OPERATORS_2.split(dice)
            ops = [a for a in ops if a != None]

        # dice repair/modification
        if numDice > 300 or diceVal < 1:
            raise errors.InvalidArgument('Too many dice rolled.')

        result.max_value = diceVal
        result.num_dice = numDice
        result.operators = ops

        for _ in range(numDice):
            try:
                tempdice = SingleDice()
                tempdice.value = random.randint(1, diceVal)
                tempdice.rolls = [tempdice.value]
                tempdice.max_value = diceVal
                tempdice.kept = True
                result.rolled.append(tempdice)
            except:
                result.rolled.append(SingleDice())

        if ops != None:

            rerollList = []
            reroll_once = []
            keep = None
            to_explode = []
            to_reroll_add = []

            valid_operators = VALID_OPERATORS_ARRAY
            last_operator = None
            for index, op in enumerate(ops):
                if last_operator != None and op in valid_operators and not op == last_operator:
                    result.reroll(reroll_once, 1)
                    reroll_once = []
                    result.reroll(rerollList, greedy=True)
                    rerollList = []
                    result.keep(keep)
                    keep = None
                    result.reroll(to_reroll_add, 1, keep_rerolled=True, unique=True)
                    to_reroll_add = []
                    result.reroll(to_explode, greedy=True, keep_rerolled=True)
                    to_explode = []
                if op == 'rr':
                    rerollList += parse_selectors([list_get(index + 1, 0, ops)], result, greedy=True)
                if op == 'k':
                    keep = [] if keep ==None else keep
                    keep += parse_selectors([list_get(index + 1, 0, ops)], result)
                if op == 'p':
                    keep = [] if keep ==None else keep
                    keep += parse_selectors([list_get(index + 1, 0, ops)], result, inverse=True)
                if op == 'ro':
                    reroll_once += parse_selectors([list_get(index + 1, 0, ops)], result)
                if op == 'mi':
                    _min = list_get(index + 1, 0, ops)
                    for r in result.rolled:
                        if r.value < int(_min):
                            r.update(int(_min))
                if op == 'ma':
                    _max = list_get(index + 1, 0, ops)
                    for r in result.rolled:
                        if r.value > int(_max):
                            r.update(int(_max))
                if op == 'ra':
                    to_reroll_add += parse_selectors([list_get(index + 1, 0, ops)], result)
                if op == 'e':
                    to_explode += parse_selectors([list_get(index + 1, 0, ops)], result, greedy=True)
                if op in valid_operators:
                    last_operator = op
            result.reroll(reroll_once, 1)
            result.reroll(rerollList, greedy=True)
            result.keep(keep)
            result.reroll(to_reroll_add, 1, keep_rerolled=True, unique=True)
            result.reroll(to_explode, greedy=True, keep_rerolled=True)

        return result


class Part:
    """Class to hold one part of the roll string."""
    pass


class SingleDiceGroup(Part):
    def __init__(self, num_dice: int = 0, max_value: int = 0, rolled=None, annotation: str = "", result: str = "",
                 operators=None):
        if operators ==None:
            operators = []
        if rolled ==None:
            rolled = []
        self.num_dice = num_dice
        self.max_value = max_value
        self.rolled = rolled  # list of SingleDice
        self.annotation = annotation
        self.result = result
        self.operators = operators

    def keep(self, rolls_to_keep):
        if rolls_to_keep ==None: return
        for _roll in self.rolled:
            if not _roll.value in rolls_to_keep:
                _roll.kept = False
            elif _roll.kept:
                rolls_to_keep.remove(_roll.value)

    def reroll(self, rerollList, max_iterations=1000, greedy=False, keep_rerolled=False, unique=False):
        if not rerollList: return  # don't reroll nothing - minor optimization
        if unique:
            rerollList = list(set(rerollList))  # remove duplicates
        if len(rerollList) > 100:
            raise OverflowError("Too many dice to reroll (max 100)")
        last_index = 0
        count = 0
        should_continue = True
        while should_continue:  # let's only iterate 250 times for sanity
            should_continue = False
            if any(d.value in set(rerollList) for d in self.rolled[last_index:] if d.kept and not d.exploded):
                should_continue = True
            to_extend = []
            for r in self.rolled[last_index:]:  # no need to recheck everything
                count += 1
                if count > max_iterations:
                    should_continue = False
                if r.value in rerollList and r.kept and not r.exploded:
                    try:
                        tempdice = SingleDice()
                        tempdice.value = random.randint(1, self.max_value)
                        tempdice.rolls = [tempdice.value]
                        tempdice.max_value = self.max_value
                        tempdice.kept = True
                        to_extend.append(tempdice)
                        if not keep_rerolled:
                            r.drop()
                        else:
                            r.explode()
                    except:
                        to_extend.append(SingleDice())
                        if not keep_rerolled:
                            r.drop()
                        else:
                            r.explode()
                    if not greedy:
                        rerollList.remove(r.value)
            last_index = len(self.rolled)
            self.rolled.extend(to_extend)

    def get_total(self):
        """Returns:
        int - The total value of the dice."""
        return sum(r.value for r in self.rolled if r.kept)

    def get_eval(self):
        return str(self.get_total())

    def get_num_kept(self):
        return sum(1 for r in self.rolled if r.kept)

    def get_crit(self):
        """Returns:
        int - 0 for no crit, 1 for crit, 2 for crit fail."""
        if self.get_num_kept() == 1 and self.max_value == 20:
            if self.get_total() == 20:
                return 1
            elif self.get_total() == 1:
                return 2
        return 0

    def __str__(self):
        return "{0.num_dice}d{0.max_value}{1} ({2}) {0.annotation}".format(
            self, ''.join(self.operators), ', '.join(str(r) for r in self.rolled))

    def to_dict(self):
        return {'type': 'dice', 'dice': [d.to_dict() for d in self.rolled], 'annotation': self.annotation,
                'value': self.get_total(), 'is_crit': self.get_crit(), 'num_kept': self.get_num_kept(),
                'text': str(self), 'num_dice': self.num_dice, 'dice_size': self.max_value, 'operators': self.operators}


class SingleDice:
    def __init__(self, value: int = 0, max_value: int = 0, kept: bool = True, exploded: bool = False):
        self.value = value
        self.max_value = max_value
        self.kept = kept
        self.rolls = [value]  # list of ints (for X -> Y -> Z)
        self.exploded = exploded

    def drop(self):
        self.kept = False

    def explode(self):
        self.exploded = True

    def update(self, new_value):
        self.value = new_value
        self.rolls.append(new_value)

    def __str__(self):
        formatted_rolls = [str(r) for r in self.rolls]
        if int(formatted_rolls[-1]) == self.max_value or int(formatted_rolls[-1]) == 1:
            formatted_rolls[-1] = '**' + formatted_rolls[-1] + '**'
        if self.exploded:
            formatted_rolls[-1] = '__' + formatted_rolls[-1] + '__'
        if self.kept:
            return ' -> '.join(formatted_rolls)
        else:
            return '~~' + ' -> '.join(formatted_rolls) + '~~'

    def __repr__(self):
        return "<SingleDice object: value={0.value}, max_value={0.max_value}, kept={0.kept}, rolls={0.rolls}>".format(
            self)

    def to_dict(self):
        return {'type': 'single_dice', 'value': self.value, 'size': self.max_value, 'is_kept': self.kept,
                'rolls': self.rolls, 'exploded': self.exploded}


class Constant(Part):
    def __init__(self, value: int = 0, annotation: str = ""):
        self.value = value
        self.annotation = annotation if annotation != None else ''

    def __str__(self):
        return "{0.value} {0.annotation}".format(self)

    def get_eval(self):
        return str(self.value)

    def to_dict(self):
        return {'type': 'constant', 'value': self.value, 'annotation': self.annotation}


class Operator(Part):
    def __init__(self, op: str = "+", annotation: str = ""):
        self.op = op if op != None else ''
        self.annotation = annotation if annotation != None else ''

    def __str__(self):
        return "{0.op} {0.annotation}".format(self)

    def get_eval(self):
        return self.op

    def to_dict(self):
        return {'type': 'operator', 'value': self.op, 'annotation': self.annotation}


class Comment(Part):
    def __init__(self, comment: str = ""):
        self.comment = comment

    def __str__(self):
        return self.comment.strip()

    def to_dict(self):
        return {'type': 'comment', 'value': self.comment}


def parse_selectors(opts, res, greedy=False, inverse=False):
    """Returns a list of ints."""
    for o in range(len(opts)):
        if opts[o][0] =='h':
            opts[o] = nlargest(int(opts[o].split('h')[1]), (d.value for d in res.rolled if d.kept))
        elif opts[o][0] =='l':
            opts[o] = nsmallest(int(opts[o].split('l')[1]), (d.value for d in res.rolled if d.kept))
        elif opts[o][0] =='>':
            if greedy:
                opts[o] = list(range(int(opts[o].split('>')[1]) + 1, res.max_value + 1))
            else:
                opts[o] = [d.value for d in res.rolled if d.value > int(opts[o].split('>')[1])]
        elif opts[o][0] =='<':
            if greedy:
                opts[o] = list(range(1, int(opts[o].split('<')[1])))
            else:
                opts[o] = [d.value for d in res.rolled if d.value < int(opts[o].split('<')[1])]
    out = []
    for o in opts:
        if isinstance(o, list):
            out.extend(int(l) for l in o)
        elif not greedy:
            out.extend(int(o) for a in res.rolled if a.value ==int(o) and a.kept)
        else:
            out.append(int(o))

    if not inverse:
        return out

    inverse_out = []
    for rolled in res.rolled:
        if rolled.kept and rolled.value in out:
            out.remove(rolled.value)
        elif rolled.kept:
            inverse_out.append(rolled.value)
    return inverse_out


class DiceResult:
    """Class to hold the output of a dice roll."""

    def __init__(self, result: int = 0, verbose_result: str = '', crit: int = 0, rolled: str = '', skeleton: str = '',
                 raw_dice: Roll = None):
        self.plain = result
        self.total = result
        self.result = verbose_result
        self.crit = crit
        self.rolled = rolled
        self.skeleton = skeleton if skeleton != '' else verbose_result
        self.raw_dice = raw_dice  # Roll

    def __str__(self):
        return self.result

    def __repr__(self):
        return '<DiceResult object: total={}>'.format(self.total)

    def consolidated(self):
        """Gets the most simplified version of the roll string."""
        if self.raw_dice ==None:
            return "0"
        parts = []  # list of (part, annotation)
        last_part = ""
        for p in self.raw_dice.parts:
            if isinstance(p, SingleDiceGroup):
                last_part += str(p.get_total())
            else:
                last_part += str(p)
            if not isinstance(p, Comment) and p.annotation:
                parts.append((last_part, p.annotation))
                last_part = ""
        if last_part:
            parts.append((last_part, ""))

        to_roll = ""
        last_annotation = ""
        out = ""
        for numbers, annotation in parts:
            if annotation and annotation != last_annotation and to_roll:
                out += f"{roll(to_roll).total:+} {last_annotation}"
                to_roll = ""
            if annotation:
                last_annotation = annotation
            to_roll += numbers
        if to_roll:
            out += f"{roll(to_roll).total:+} {last_annotation}"
        out = out.strip('+ ')
        return out


if __name__ == '__main__':
    while True:
        print(roll(input().strip()))
