import sys
from time import time
from weakref import WeakKeyDictionary
from math import sqrt

from SelfCallMixin import *
from OneStrokeGesture import *

class Stroke:
	"""
		Holds a list of points (of any dimension really), with timestamps of their creation time.
	"""
	def __init__(self):
		self.done = False
		self.start = time()
		self.points = []
		# xmin, ymin, xmax, ymax, ...
		self.extents = []
		self.circles = []
	
	def __len__(self):
		return len(self.points)
	
	def AddPoint(self, p, done=False):
		if self.extents:
			for i in range(len(p)):
				self.extents[i] = min(p[i], self.extents[i])
				self.extents[i + len(p)] = max(p[i], self.extents[i + len(p)])
		else:
			self.extents = list(p) * 2
		self.points.append([time()] + list(p))
		self.done = done
	
	def GetPoints(self, timestamps=False):
		return [p[int(not timestamps):] for p in self.points]
	
	def IsDone(self):
		return self.done
	
	def LinesIntersect(self, l1, l2):
		def same_sign(a, b):
			return ((a * b) >= 0)
		
		x1 = l1[0][0]
		y1 = l1[0][1]
		x2 = l1[1][0]
		y2 = l1[1][1]
		x3 = l2[0][0]
		y3 = l2[0][1]
		x4 = l2[1][0]
		y4 = l2[1][1]
		# Compute a1, b1, c1, where line joining points 1 and 2
		# is "a1 x + b1 y + c1 = 0".
		a1 = y2 - y1
		b1 = x1 - x2
		c1 = (x2 * y1) - (x1 * y2)
		
		if a1 == 0 and b1 == 0:
			# line has no length
			return None
		
		# Compute r3 and r4.
		r3 = ((a1 * x3) + (b1 * y3) + c1)
		r4 = ((a1 * x4) + (b1 * y4) + c1)
		
		# Check signs of r3 and r4. If both point 3 and point 4 lie on
		# same side of line 1, the line segments do not intersect.
		if (r3 != 0) and (r4 != 0) and same_sign(r3, r4):
			return None
		
		# Compute a2, b2, c2
		a2 = y4 - y3
		b2 = x3 - x4
		c2 = (x4 * y3) - (x3 * y4)
		
		if a2 == 0 and b2 == 0:
			# line has no length
			return None
		
		# Compute r1 and r2
		r1 = (a2 * x1) + (b2 * y1) + c2
		r2 = (a2 * x2) + (b2 * y2) + c2
		
		# Check signs of r1 and r2. If both point 1 and point 2 lie
		# on same side of second line segment, the line segments do
		# not intersect.
		if (r1 != 0) and (r2 != 0) and same_sign(r1, r2):
			return None
		
		# Line segments intersect: compute intersection point.
		denom = (a1 * b2) - (a2 * b1)
		
		if (denom == 0):
			# TODO: these lines are co-linear. They should probably return an intersection but that seems buggy!
			return None
		
		if (denom < 0):
			offset = -denom / 2
		else:
			offset = denom / 2
		
		# The denom/2 is to get rounding instead of truncating. It
		# is added or subtracted to the numerator, depending upon the
		# sign of the numerator.
		num = (b1 * c2) - (b2 * c1)
		if (num < 0):
			x = (num - offset) / denom
		else:
			x = (num + offset) / denom
		
		num = (a2 * c1) - (a1 * c2)
		if (num < 0):
			y = ( num - offset) / denom
		else:
			y = (num + offset) / denom
		
		# lines_intersect
		return (x, y)
	
	def TestLines(self, them):
		mypoints = self.GetPoints(False)
		thpoints = them.GetPoints(False)
		
		for a in range(len(mypoints) - 1):
			for b in range(len(thpoints) - 1):
				l = self.LinesIntersect([mypoints[a], mypoints[a + 1]], [thpoints[b], thpoints[b + 1]])
				if l:
					self.circles.append(l)
					return True
	
	def Colliders2d(self, others):
		strokesCollided = [o for o in others if not (self.extents[2] < o.extents[0]) and not (self.extents[0] > o.extents[2]) and not (self.extents[3] < o.extents[1]) and not (self.extents[1] > o.extents[3])]
		return [r for r in strokesCollided if self.TestLines(r)]
	
	def Surrounded(self, others):
		"""
			Find strokes who's bounding boxes are entirely within mine, and who's lines I don't cross.
		"""
		strokesSurrounded = [o for o in others if (self.extents[2] > o.extents[0]) and (self.extents[0] < o.extents[2]) and (self.extents[3] > o.extents[1]) and (self.extents[1] < o.extents[3])]
		return [r for r in strokesSurrounded if not self.TestLines(r)]

class Sketch(SelfCallMixin):
	"""
		A Newton "Notes" like drawing application, sufficiently abstracted that it can be ported to different front-ends.
		
		Sketch holds a list of timestamped strokes.
		
		Override:
			DrawLine(points)
			DrawSelectLine(points)

			These methods should be able to draw a point too (if only one coordinate is passed in)
	"""
	selectTime = 1.25
	gestures = [
		["Erase", Path([(0, 0), (2, 10), (4, 0), (6, 10), (8, 0), (10, 10)]), locals.StartAny | locals.EndAny],
		["Erase", Path([(0, 10), (2, 0), (4, 10), (6, 0), (8, 10), (10, 0)]), locals.StartAny | locals.EndAny],
		["Erase", Path([(10, 0), (8, 10), (6, 0), (4, 10), (2, 0), (0, 10)]), locals.StartAny | locals.EndAny],
		["Erase", Path([(10, 10), (8, 0), (6, 10), (4, 0), (2, 10), (0, 0)]), locals.StartAny | locals.EndAny],
	]
	
	def __init__(self):
		self.gesture = OneStrokeGesture(self.gestures)
		self.strokes = []
		self.selected = WeakKeyDictionary()
		self.select = None
		self.down = False
	
	def Down(self):
		return self.down
	
	def DrawLine(self, points):
		pass
	
	def DrawSelectLine(self, points):
		pass
	
	def DrawSelectedBox(self, points):
		pass
	
	def StartSelect(self, pos):
		pass
	
	##
	##	Core routines
	##
	
	def Draw(self):
		[self.DrawLine(st.GetPoints(False)) for st in self.GetStrokes()]
		if self.GetSelected():
			self.DrawSelectLine(self.GetSelected().GetPoints(False))
	
	def AddPoint(self, pos, done=False):
		self.strokes[-1].AddPoint(pos, done)
	
	def PenDown(self, pos):
		self.strokes.append(Stroke())
		self.AddPoint(pos)
		self.down = True
	
	def PenTo(self, pos):
		if len(self.strokes) and not self.strokes[-1].IsDone():
			self.AddPoint(pos)
	
	def PenUp(self, pos):
		# if there is only one point or all points are too close, then delete the last stroke
		if len(self.strokes[-1].GetPoints()) == 1 and tuple(self.strokes[-1].GetPoints()[0]) == pos:
			self.strokes = self.strokes[:-1]
		else:
			self.AddPoint(pos, True)
			# has the user drawn a known gesture?
			strokes = self.gesture.FindGesture(self.strokes[-1].GetPoints())
			if strokes[0][1] < 2.0:
				self.CallMethod(strokes[0][0] + "Stroke")
		
		if self.select:
			# figured out what has been selected and show it as selected
			print "surrounded", self.strokes[-1].Surrounded(self.strokes[:-1])
			self.select = None
			self.strokes = self.strokes[:-1]
		self.down = False
	
	def EraseStroke(self):
		[self.strokes.remove(s) for s in self.strokes[-1].Collders2d(self.strokes[:-1])]
		self.strokes = self.strokes[:-1]
	
	def Update(self):
		# if we have some strokes
		# and the last stroke only has one point
		# and the time since starting is greater than two seconds
		if not self.select and len(self.strokes) and len(self.strokes[-1].GetPoints()) == 1 and time() > self.strokes[-1].start + self.selectTime:
			# select mode activated
			self.select = self.strokes[-1]
			self.StartSelect(self.strokes[-1].GetPoints()[-1])
	
	def GetStrokes(self):
		return self.strokes
	
	def GetSelected(self):
		return self.select


