###
# Copyright (c) 2002-2004, Jeremiah Fincher
# All rights reserved.
###

from __future__ import division

import re
import math
import cmath
import types
import string
from gozerbot.commands import cmnds
from gozerbot.examples import examples 
from gozerbot.plughelp import plughelp
from gozerbot.plugins import plugins
from gozerbot.persistconfig import PersistConfig
from gozerbot.generic import  handle_exception, rlog

plughelp.add('calc', 'calculator expressions')

cfg = PersistConfig()
cfg.define('fac-max', 100)
baseArg = ('int', 'base', lambda i: i <= 36)

def fac(x):
    if x > cfg.get('fac-max'):
        raise OverflowError("will not computate factorials > %d" % cfg.get('fac-max'))
    return [j for j in [1] for i in range(2,x+1) for j in [j*i]] [-1]

class Calc:
    def __init__(self):
        self.got_karma = 'karma' in plugins.available()
        if self.got_karma:
            self._karma = plugins.plugs['karma']

    def _convertDecimalToBase(self, number, base):
        """Convert a decimal number to another base; returns a string."""
        if number == 0:
            return '0'
        elif number < 0:
            negative = True
            number = -number
        else:
            negative = False
        digits = []
        while number != 0:
            digit = number % base
            if digit >= 10:
                digit = string.uppercase[digit - 10]
            else:
                digit = str(digit)
            digits.append(digit)
            number = number // base
        digits.reverse()
        return '-'*negative + ''.join(digits)

    def _convertBaseToBase(self, number, toBase, fromBase):
        """Convert a number from any base, 2 through 36, to any other
        base, 2 through 36. Returns a string."""
        number = long(str(number), fromBase)
        if toBase == 10:
            return str(number)
        return self._convertDecimalToBase(number, toBase)

    _mathEnv = {'__builtins__': types.ModuleType('__builtins__'), 'i': 1j}
    _mathEnv.update(math.__dict__)
    _mathEnv.update(cmath.__dict__)
    def _sqrt(x):
        if isinstance(x, complex) or x < 0:
            return cmath.sqrt(x)
        else:
            return math.sqrt(x)

    _mathEnv['sqrt'] = _sqrt
    _mathEnv['abs'] = abs
    _mathEnv['max'] = max
    _mathEnv['min'] = min
    _mathEnv['fac'] = fac
    _mathRe = re.compile(r'((?:(?<![A-Fa-f\d)])-)?'
                         r'(?:0x[A-Fa-f\d]+|'
                         r'0[0-7]+|'
                         r'\d+\.\d+|'
                         r'\.\d+|'
                         r'\d+\.|'
                         r'\d+))')
    _karmaRe = re.compile(r'\[([^\]]+)\]')
    def _floatToString(self, x):
        if -1e-10 < x < 1e-10:
            return '0'
        elif -1e-10 < int(x) - x < 1e-10:
            return str(int(x))
        else:
            return str(x)

    def _complexToString(self, x):
        realS = self._floatToString(x.real)
        imagS = self._floatToString(x.imag)
        if imagS == '0':
            return realS
        elif imagS == '1':
            imagS = '+i'
        elif imagS == '-1':
            imagS = '-i'
        elif x.imag < 0:
            imagS = '%si' % imagS
        else:
            imagS = '+%si' % imagS
        if realS == '0' and imagS == '0':
            return '0'
        elif realS == '0':
            return imagS.lstrip('+')
        elif imagS == '0':
            return realS
        else:
            return '%s%s' % (realS, imagS)

    def _addKarma(self, text):
        for item in self._karmaRe.findall(text):
            karma = str(self._karma.karma.get(item))
            text = text.replace('[%s]' % item, karma)
        return text

    ###
    # So this is how the 'calc' command works:
    # First, we make a nice little safe environment for evaluation; basically,
    # the names in the 'math' and 'cmath' modules.  Then, we remove the ability
    # of a random user to get ints evaluated: this means we have to turn all
    # int literals (even octal numbers and hexadecimal numbers) into floats.
    # Then we delete all square brackets, underscores, and whitespace, so no
    # one can do list comprehensions or call __...__ functions.
    ###
    def calc(self, bot, ievent):
        """<math expression>

        Returns the value of the evaluated <math expression>.  The syntax is
        Python syntax; the type of arithmetic is floating point.  Floating
        point arithmetic is used in order to prevent a user from being able to
        crash to the bot with something like '10**10**10**10'.  One consequence
        is that large values such as '10**24' might not be exact.
        """
        text = ievent.rest.lower().replace('^', '**')
        # for tashiro ;)
        text = text.replace('the ', '')
        text = text.replace('meaning of life', '42')
        text = text.replace('answer to life', '42')
        text = text.replace('answer to life, the universe and everything', '42')
        text = text.replace('answer to life, universe and everything', '42')
        text = text.replace('number of horns on a unicorn', '1')
        text = text.replace('number of days in a week', '7')
        text = text.replace('number of tits on a female', '2')
        text = text.replace('number of boobs on a female', '2')
        # handle karma
        if self.got_karma:
            text = self._addKarma(text)
        else:
            for c in '_[]':
                if c in text:
                    ievent.reply('There\'s really no reason why you should have underscores or brackets in your mathematical expression, please remove them.')
                    return
        if 'lambda' in text:
            ievent.reply('You can\'t use lambda in this command.')
            return
        def handleMatch(m):
            rlog(2, 'calc', 'handleMatch(%s)' % m.group(1))
            s = m.group(1)
            if s.startswith('0x'):
                i = int(s, 16)
            elif s.startswith('0') and '.' not in s:
                try:
                    i = int(s, 8)
                except ValueError:
                    i = int(s)
            else:
                i = float(s)
            x = complex(i)
            if x == abs(x):
                x = abs(x)
            return str(x)
        text = self._mathRe.sub(handleMatch, text)
        try:
            rlog(2, 'calc', 'evaluating %s from %s' % (text, ievent.nick))
            x = complex(eval(text, self._mathEnv, self._mathEnv))
            ievent.reply(self._complexToString(x))
        except ZeroDivisionError:
            ievent.reply('Division by zero')
        except OverflowError:
            maxFloat = math.ldexp(0.9999999999999999, 1024)
            ievent.reply('The answer exceeded %s or so.' % maxFloat)
        except TypeError:
            ievent.reply('Something in there wasn\'t a valid number.')
        except NameError, e:
            ievent.reply('%s is not a defined function.' % str(e).split()[1])
        except SyntaxError, e:
            ievent.reply('Syntax error')
        except ValueError, e:
            ievent.reply('Invalid value')
        except Exception, e:
            handle_exception(ievent=ievent)

    def icalc(self, bot, ievent):
        """<math expression>

        This is the same as the calc command except that it allows integer
        math, and can thus cause the bot to suck up CPU.  Hence it requires
        the 'trusted' capability to use.
        """
        text = ievent.rest.lower().replace('^', '**')
        for c in '_[]':
            if c in text:
                ievent.reply('There\'s really no reason why you should have underscores or brackets in your mathematical expression, please remove them.')
                return
        if 'lambda' in text:
            ievent.reply('You can\'t use lambda in this command.')
            return
        try:
            rlog(2, 'icalc', 'evaluating %s from %s' % (text, ievent.nick))
            ievent.reply(str(eval(text, self._mathEnv, self._mathEnv)))
        except ZeroDivisionError:
            ievent.reply('Division by zero')
        except OverflowError:
            maxFloat = math.ldexp(0.9999999999999999, 1024)
            ievent.reply('The answer exceeded %s or so.' % maxFloat)
        except TypeError:
            ievent.reply('Something in there wasn\'t a valid number.')
        except NameError, e:
            ievent.reply('%s is not a defined function.' % str(e).split()[1])
        except SyntaxError, e:
            ievent.reply('Syntax error')
        except Exception, e:
            handle_exception(ievent=ievent)

    _rpnEnv = {
        'dup': lambda s: s.extend([s.pop()]*2),
        'swap': lambda s: s.extend([s.pop(), s.pop()])
        }
    def rpn(self, bot, ievent):
        """<rpn math expression>

        Returns the value of an RPN expression.
        """
        stack = []
        for arg in ievent.args:
            try:
                x = complex(arg)
                if x == abs(x):
                    x = abs(x)
                stack.append(x)
            except ValueError: # Not a float.
                if arg in self._mathEnv:
                    f = self._mathEnv[arg]
                    if callable(f):
                        called = False
                        arguments = []
                        while not called and stack:
                            arguments.append(stack.pop())
                            try:
                                stack.append(f(*arguments))
                                called = True
                            except TypeError:
                                pass
                        if not called:
                            ievent.reply('Not enough arguments for %s' % arg)
                            return
                    else:
                        stack.append(f)
                elif arg in self._rpnEnv:
                    self._rpnEnv[arg](stack)
                else:
                    arg2 = stack.pop()
                    arg1 = stack.pop()
                    s = '%s%s%s' % (arg1, arg, arg2)
                    try:
                        stack.append(eval(s, self._mathEnv, self._mathEnv))
                    except SyntaxError:
                        ievent.reply('%s is not a defined function' % arg)
                        return
        if len(stack) == 1:
            ievent.reply(str(self._complexToString(complex(stack[0]))))
        else:
            s = ', '.join(map(self._complexToString, map(complex, stack)))
            ievent.reply('Stack: [%s]' % s)

calculator = Calc()

def handle_calc(bot, ievent):
    print 'HANDLE_CALC', bot, ievent
    calculator.calc(bot, ievent)

def handle_icalc(bot, ievent):
    calculator.icalc(bot, ievent)

def handle_rpn(bot, ievent):
    calculator.rpn(bot, ievent)

cmnds.add('calc', handle_calc, ['USER'])
examples.add('calc', 'calculates a sum', 'calc sin(pi) + 2^8')
cmnds.add('icalc', handle_icalc, ['ICALC', 'OPER'])
examples.add('icalc', 'calculates a sum with integer math', 'icalc 1.0 + 4.5')
cmnds.add('rpn', handle_rpn, ['USER'])
examples.add('rpn', 'returns the value of an RPN expression', 'rpn 5 1 2 + 4 * + 3')

# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
