from __future__ import division
import numpy as np
import functools
import docrep

from .CountRoots import count_roots
from .ApproximateRoots import approximate_roots
from .RootFinder import find_roots, MultiplicityError
from .DemoRootFinder import demo_find_roots
from .Misc import doc_tab_to_space, docstrings, remove_para
from .Paths import ComplexPath

class Contour(object):
	"""
	A base class for contours in the complex plane.

	Attributes
	----------
	centralPoint : complex
		The point at the center of the contour.
	area : float
		The surface area of the contour.
	"""
	def __init__(self, segments):
		self.segments = np.array(segments, dtype=object)

	def __call__(self, t):
		"""
		The point on the contour corresponding the value of the 
		parameter t.

		Parameters
		----------
		t : float
			A real number :math:`0\leq t \leq 1` which parameterises
			the contour.

		Returns
		-------
		complex
			A point on the contour.

		Example
		-------

		>>> from cxroots.Paths import Circle
		>>> c = Circle(0,1)	# Circle |z|=1 parameterised by e^{it}
		>>> c(0.25)
		(6.123233995736766e-17+1j)
		>>> c(0) == c(1)
		True
		"""
		t = np.array(t)
		N = len(self.segments)
		segmentIndex = np.array(N*t, dtype=int)
		segmentIndex = np.mod(segmentIndex, N)
		segment = self.segments[segmentIndex]

		if hasattr(segmentIndex, '__iter__'):
			return np.array([self.segments[i](N*t[ti]%1) for ti, i in enumerate(segmentIndex)])
		else:
			return self.segments[segmentIndex](N*t%1)

	@property
	def centralPoint(self):
		raise NotImplemented('centralPoint needs to be implemented in the subclass.')

	@property
	def area(self):
		raise NotImplemented('area needs to be implemented in the subclass.')

	def contains(self, z):
		"""
		Tests whether the point z is within the contour.

		Parameters
		----------
		z : complex

		Returns
		-------
		bool
			True if z lies within the contour and false otherwise.
		"""
		raise NotImplemented('contains() needs to be implemented in the subclass.')

	@functools.wraps(ComplexPath.plot)
	def plot(self, *args, **kwargs):
		self._sizePlot()
		for segment in self.segments:
			segment.plot(*args, **kwargs)

	def _sizePlot(self):
		import matplotlib.pyplot as plt
		t = np.linspace(0,1,1000)
		z = self(t)
		xpad = (max(np.real(z))-min(np.real(z)))*0.1
		ypad = (max(np.imag(z))-min(np.imag(z)))*0.1

		xmin = min(np.real(z))-xpad
		xmax = max(np.real(z))+xpad
		ymin = min(np.imag(z))-ypad
		ymax = max(np.imag(z))+ypad
		plt.xlim([xmin, xmax])
		plt.ylim([ymin, ymax])

	def show(self, saveFile=None, **plotKwargs):
		""" 
		Shows the contour as a 2D plot in the complex plane.  Requires
		Matplotlib.

		Parameters
		----------
		saveFile : str (optional)
			If given then the plot will be saved to disk with name 
			'saveFile'.  If saveFile=None the plot is shown on-screen.
		**plotKwargs
			Key word arguments are as in :meth:`~cxroots.Contour.Contour.plot`.
		"""
		import matplotlib.pyplot as plt
		self.plot(**plotKwargs)

		if saveFile is not None:
			plt.savefig(saveFile, bbox_inches='tight')
			plt.close()
		else:
			plt.show()

	def subdivisions(self, axis='alternating'):
		""" 
		A generator for possible subdivisions of the contour. 
		
		Parameters
		----------
		axis : str, 'alternating' or any element of self.axisName.
			The axis along which the line subdividing the contour is a 
			constant (eg. subdividing a circle along the radial axis 
			will give an outer annulus and an inner circle).  If 
			alternating then the dividing axis will always be different
			to the dividing axis used to create the contour which is now 
			being divided.

		Yields
		------
		tuple
			A tuple with two contours which subdivide the original
			contour.
		"""
		if axis == 'alternating':
			if hasattr(self,'_createdBySubdivisionAxis'):
				axis = (self._createdBySubdivisionAxis + 1)%len(self.axisName)
			else:
				axis = 0

		for divisionFactor in divisionFactorGen():
			yield self.subdivide(axis, divisionFactor)

	def distance(self, z):
		"""
		Get the distance from the point z in the complex plane to the 
		nearest point on the contour.

		Parameters
		----------
		z : complex
			The point from which to measure the distance to the closest
			point on the contour to z.

		Returns
		-------
		float
			The distance from z to the point on the contour which is 
			closest to z.
		"""
		return min(segment.distance(z) for segment in self.segments)

	@functools.wraps(ComplexPath.integrate)
	def integrate(self, f, **integrationKwargs):
		return sum([segment.integrate(f, **integrationKwargs) for segment in self.segments])

	@remove_para('C')
	@functools.wraps(count_roots)
	def count_roots(self, f, df=None, **kwargs):
		return count_roots(self, f, df, **kwargs)

	@remove_para('C')
	@functools.wraps(approximate_roots)
	def approximate_roots(self, N, f, df=None, **kwargs):
		return approximate_roots(self, N, f, df, **kwargs)

	@remove_para('originalContour')
	@functools.wraps(find_roots)
	def roots(self, f, df=None, **kwargs):
		return find_roots(self, f, df, **kwargs)

	@remove_para('C')
	@functools.wraps(demo_find_roots)
	def demo_roots(self, *args, **kwargs):
		return demo_find_roots(self, *args, **kwargs)

def divisionFactorGen():
	"""A generator for divisionFactors."""
	yield 0.3	# being off-center is a better first choice for certain problems
	
	x = 0.5
	yield x
	for diff in np.linspace(0, 0.5, int(1+10/2.))[1:-1]:
		yield x + diff
		yield x - diff
