
# Copyright 2010 Jaap Karssenberg <jaap.karssenberg@gmail.com>
#
# Inspired by and partially based on code from clac.py,
# Which is copyright 2009 Mark Borgerding and licensed under the GPL version 3


 # We are doing math in this module ...


import logging
import re
import math
import cmath

from zim.plugins import PluginClass
from zim.actions import action
from zim.errors import Error

from zim.gui.pageview import PageViewExtension


logger = logging.getLogger('zim.plugins.inlinecalculator')


# helper functions
def dip(x):
	'demote, if possible, a complex to scalar'
	if isinstance(x, complex) and x.imag == 0:
		return x.real
	else:
		return x

def which_call(x, mathfunc, cmathfunc, allowNegative=True):
	x = dip(x)
	if isinstance(x, complex) or (allowNegative == False and x < 0):
		return cmathfunc(x)
	else:
		return mathfunc(x)

# math functions defined here

def degrees(x):
	return x * 180 / math.pi

def radians(x):
	return x * math.pi / 180

def log(x, b=math.e):
	'log(x[, base]) -> the logarithm of x to the given base.\nIf the base not specified, returns the natural logarithm (base e) of x.'
	if isinstance(x, complex) or x < 0:
		return dip(cmath.log(x) / cmath.log(b))
	else:
		return math.log(x) / math.log(b)

def real(x):
	'return just the real portion'
	if isinstance(x, complex):
		return x.real
	else:
		return x

def imag(x):
	'return just the imaginary portion'
	if isinstance(x, complex):
		return x.imag
	else:
		return 0

def sign(x):
	'returns -1,0,1 for negative,zero,positive numbers'
	if x == 0:
		return 0
	elif x > 0:
		return 1
	else:
		return -1

def log2(x):
	'logarithm base 2'
	return log(x, 2)

def gcd(x, y):
	'greatest common denominator'
	while x > 0:
		(x, y) = (y % x, x) # Guido showed me this one on the geek cruise
	return y

def lcm(x, y):
	'least common multiple'
	return x * y / gcd(x, y)

def phase(z):
	'phase of a complex in radians'
	z = cpx(z)
	return math.atan2(z.imag, z.real)

def cpx(x):
	'convert a number or tuple to a complex'
	if isinstance(x, tuple):
		return complex(x[0], x[1])
	else:
		return complex(x)

def conj(x):
	'complex conjugate'
	x = cpx(x)
	return complex(x.real, -x.imag)

def complexify(x, func):
	'call func on the real and imaginary portions, creating a complex from the respective results'
	if isinstance(x, complex) and x.imag != 0:
		return dip(complex(func(x.real), func(x.imag)))
	else:
		return func(x)

# overwrite the built-in math functions that don't handle complex
def round(x):
	'nearest integer'
	if isinstance(x, complex):
		return complexify(x, round)
	else:
		return math.floor(x + .5)

def floor(x):
	'round towards negative infinity'
	return complexify(x, math.floor)

def ceil(x):
	'round towards positive infinity'
	return complexify(x, math.ceil)


# functions and constants available  within the safe eval construct
GLOBALS = {
	'__builtins__': None, # Don't allow open() etc.
	# builtins we want to keep
	'abs': abs,
	'ord': ord,
	'chr': chr,
	'hex': hex,
	'oct': oct,
	'int': int,
	# direct imports
	'e': math.e,
	'pi': math.pi,
	'atan2': math.atan2,
	'fmod': math.fmod,
	'frexp': math.frexp,
	'hypot': math.hypot,
	'ldexp': math.ldexp,
	'modf': math.modf,
	# other nice-to-have constants
	'j': cmath.sqrt(-1),
	# marshall between the math and cmath functions automatically
	'acos': lambda x: which_call(x, math.acos, cmath.acos),
	'asin': lambda x: which_call(x, math.asin, cmath.asin),
	'atan': lambda x: which_call(x, math.atan, cmath.atan),
	'cos': lambda x: which_call(x, math.cos, cmath.cos),
	'cosh': lambda x: which_call(x, math.cosh, cmath.cosh),
	'sin': lambda x: which_call(x, math.sin, cmath.sin),
	'sinh': lambda x: which_call(x, math.sinh, cmath.sinh),
	'tan': lambda x: which_call(x, math.tan, cmath.tan),
	'tanh': lambda x: which_call(x, math.tanh, cmath.tanh),
	'exp': lambda x: which_call(x, math.exp, cmath.exp),
	'log10': lambda x: which_call(x, math.log10, cmath.log10, False),
	'sqrt': lambda x: which_call(x, math.sqrt, cmath.sqrt, False),
	# functions defined here
	'degrees': degrees,
	'radians': radians,
	'log': log,
	'real': real,
	'imag': imag,
	'sign': sign,
	'log2': log2,
	'gcd': gcd,
	'lcm': lcm,
	'phase': phase,
	'conj': conj,
	'round': round,
	'floor': floor,
	'ceil': ceil,
	# synonyms
	'mag': abs,
	'angle': phase,
}


class ExpressionError(Error):

	description = _(
		'The inline calculator plugin was not able\n'
		'to evaluate the expression at the cursor.')
		# T: error description


_multiline_re = re.compile(r'--+\s+[+-]')
	# for multiline summation with "--- +" and similar


class InlineCalculatorPlugin(PluginClass):

	plugin_info = {
		'name': _('Inline Calculator'), # T: plugin name
		'description': _('''\
This plugin allows you to quickly evaluate simple
mathematical expressions in zim.

This is a core plugin shipping with zim.
'''), # T: plugin description
		'author': 'Jaap Karssenberg',
		'help': 'Plugins:Inline Calculator',
	}

	#~ plugin_preferences = (
		# key, type, label, default
	#~ )


	def process_text(self, text):
		'''Takes a piece of text and parses it for expressions
		to evaluate. Returns the text with result inserted or replaced.
		Will raise an exception on errors.
		'''
		# This method is separated from eval_math() for easy testing

		if '\n' in text:
			return self._process_multiline(text)
		else:
			return self._process_line(text)

	def _process_line(self, line):
		# Check for non-math prefix
		prefix = ''
		if ':' in line:
			i = line.rindex(':') + 1
			prefix = line[:i]
			line = line[i:]

		# Allow for chaining like "1 + 2 = 3 + 0.5 = 3.5" etc.
		if line.count('=') > 1:
			parts = line.split('=')
			prefix += '='.join(parts[:-2]) + '='
			line = '='.join(parts[-2:])

		# Check for whitespace postfix after previous answer
		postfix = ''
		stripped = line.rstrip()
		if '=' in line \
		and stripped != line and not stripped.endswith('='):
			i = len(line) - len(stripped)
			postfix = line[-i:]
			line = stripped

		# Strip previous answer and '='
		if '=' in line:
			i = line.index('=')
			line = line[:i]

		result = self.safe_eval(line)

		return prefix + line + '= ' + str(result) + postfix

	def _process_multiline(self, text):
		lines = text.splitlines()
		for i, line in enumerate(lines):
			if _multiline_re.match(line):
				operator = line.strip()[-1]
				break
		else:
			raise ExpressionError(_('Could not parse expression'))
				# T: error message

		sep = ' %s ' % operator
		expression = sep.join('(%s)' % l for l in lines[:i])
		result = self.safe_eval(expression)

		lines = lines[:i + 1] + [str(result)]
		return '\n'.join(lines) + '\n'


	def safe_eval(self, expression):
		'''Safe evaluation of a python expression'''
		try:
			return eval(expression, GLOBALS, {})
		except Exception as error:
			msg = '%s: %s' % (error.__class__.__name__, error)
			raise ExpressionError(msg)


class InlineCalculatorPageViewExtension(PageViewExtension):

	@action(_('Evaluate _Math')) # T: menu item
	def eval_math(self):
		'''Action called by the menu item or key binding,
		will look at the cursor for an expression to evaluate.
		'''
		buffer = self.pageview.textview.get_buffer()
			# XXX- way to long chain of objects here

		# FIXME: what do we do for selections ?

		cursor = buffer.get_iter_at_mark(buffer.get_insert())
		start, end = buffer.get_line_bounds(cursor.get_line())
		line = start.get_text(end)

		if not line or line.isspace():
			# Empty line, look at previous line
			if cursor.get_line() > 0:
				start, end = buffer.get_line_bounds(cursor.get_line() - 1)
				cursor = end.copy()
				cursor.backward_char()
				line = start.get_text(end)
			else:
				return # silent fail

		if _multiline_re.match(line):
			# Search for start of block - iterate back to empty line
			lineno = cursor.get_line()
			while lineno >= 0:
				mystart, myend = buffer.get_line_bounds(lineno)
				myline = mystart.get_text(myend)
				if not myline or myline.isspace():
					break
				else:
					start = mystart
					lineno -= 1
		else:
			# One line expression, just pass it on
			# FIXME skip forward past next word if any if last char is '='
			end = cursor

		orig = start.get_text(end)
		logger.debug('Inline calculator eval: >%s<', orig)
		new = self.plugin.process_text(orig)
		with buffer.user_action:
			buffer.delete(start, end)
			buffer.insert(start, new)
