import bpy
import bmesh
import operator
from mathutils import Vector
from collections import defaultdict
from math import pi
import time
from math import radians, hypot

from . import utilities_uv


class op(bpy.types.Operator):
	bl_idname = "uv.textools_rectify"
	bl_label = "Rectify"
	bl_description = "Align selected faces or verts to rectangular distribution."
	bl_options = {'REGISTER', 'UNDO'}
	
	@classmethod
	def poll(cls, context):
		if not bpy.context.active_object:
			return False

		if bpy.context.active_object.type != 'MESH':
			return False

		if bpy.context.active_object.mode != 'EDIT':
			return False

		# No Sync mode
		if context.scene.tool_settings.use_uv_select_sync:
			return False

		return True
	

	def execute(self, context):
		rectify(self, context)
		return {'FINISHED'}



precision = 3


def rectify(self, context):
	obj = bpy.context.active_object


	bm = bmesh.from_edit_mesh(obj.data)
	uv_layers = bm.loops.layers.uv.verify()

	#Store selection
	utilities_uv.selection_store()

	main(False)

	#Restore selection
	utilities_uv.selection_restore()


def main(square = False, snapToClosest = False):

	startTime = time.clock()
	obj = bpy.context.active_object
	me = obj.data
	bm = bmesh.from_edit_mesh(me)
	uv_layers = bm.loops.layers.uv.verify()
	# bm.faces.layers.tex.verify()  # currently blender needs both layers.

	face_act = bm.faces.active    
	targetFace = face_act
		
	#if len(bm.faces) > allowedFaces:
	#    operator.report({'ERROR'}, "selected more than " +str(allowedFaces) +" allowed faces.")
	#   return 

	edgeVerts, filteredVerts, selFaces, nonQuadFaces, vertsDict, noEdge = ListsOfVerts(uv_layers, bm)   
	
	if len(filteredVerts) is 0: return 
	if len(filteredVerts) is 1: 
		SnapCursorToClosestSelected(filteredVerts)
		return 
	
	cursorClosestTo = CursorClosestTo(filteredVerts)
	#line is selected
	
	if len(selFaces) is 0:
		if snapToClosest is True:
			SnapCursorToClosestSelected(filteredVerts)
			return
		
		VertsDictForLine(uv_layers, bm, filteredVerts, vertsDict)
		
		if AreVectsLinedOnAxis(filteredVerts) is False:
			ScaleTo0OnAxisAndCursor(filteredVerts, vertsDict, cursorClosestTo)
			return SuccessFinished(me, startTime)
				
		MakeEqualDistanceBetweenVertsInLine(filteredVerts, vertsDict, cursorClosestTo)
		return SuccessFinished(me, startTime)
		
   
	#else:
	
	#active face checks
	if targetFace is None or targetFace.select is False or len(targetFace.verts) is not 4:
		targetFace = selFaces[0]
	else:
		for l in targetFace.loops:
			if l[uv_layers].select is False: 
				targetFace = selFaces[0]
				break 
			
	ShapeFace(uv_layers, operator, targetFace, vertsDict, square)
	
	for nf in nonQuadFaces:
		for l in nf.loops:
			luv = l[uv_layers]
			luv.select = False
	
	if square: FollowActiveUV(operator, me, targetFace, selFaces, 'EVEN')
	else: FollowActiveUV(operator, me, targetFace, selFaces)
	
	if noEdge is False:
		#edge has ripped so we connect it back 
		for ev in edgeVerts:
			key = (round(ev.uv.x, precision), round(ev.uv.y, precision))
			if key in vertsDict:
				ev.uv = vertsDict[key][0].uv
				ev.select = True
		
	return SuccessFinished(me, startTime)



def ListsOfVerts(uv_layers, bm):
	edgeVerts = []
	allEdgeVerts = []
	filteredVerts = []
	selFaces = []
	nonQuadFaces = []
	vertsDict = defaultdict(list)                #dict
	
	for f in bm.faces:
		isFaceSel = True
		facesEdgeVerts = []
		if (f.select == False):
			continue
		
		#collect edge verts if any
		for l in f.loops:
			luv = l[uv_layers]
			if luv.select is True:
				facesEdgeVerts.append(luv)
			else: isFaceSel = False
		
		allEdgeVerts.extend(facesEdgeVerts)
		if isFaceSel:            
			if len(f.verts) is not 4:
				nonQuadFaces.append(f)
				edgeVerts.extend(facesEdgeVerts)
			else: 
				selFaces.append(f)
				
				for l in f.loops:
					luv = l[uv_layers]
					x = round(luv.uv.x, precision)
					y = round(luv.uv.y, precision)
					vertsDict[(x, y)].append(luv)
		
		else: edgeVerts.extend(facesEdgeVerts)
	
	noEdge = False
	if len(edgeVerts) is 0:
		noEdge = True
		edgeVerts.extend(allEdgeVerts)
	
	if len(selFaces) is 0:
		for ev in edgeVerts:
			if ListQuasiContainsVect(filteredVerts, ev) is False:
				filteredVerts.append(ev)
	else: filteredVerts = edgeVerts
		
	return edgeVerts, filteredVerts, selFaces, nonQuadFaces, vertsDict, noEdge



def ListQuasiContainsVect(list, vect):
	for v in list:
		if AreVertsQuasiEqual(v, vect):
			return True
	return False



def SnapCursorToClosestSelected(filteredVerts):
	#TODO: snap to closest selected 
	if len(filteredVerts) is 1: 
		SetAll2dCursorsTo(filteredVerts[0].uv.x, filteredVerts[0].uv.y)
	
	return





def VertsDictForLine(uv_layers, bm, selVerts, vertsDict):
	for f in bm.faces:
		for l in f.loops:
				luv = l[uv_layers]
				if luv.select is True:
					x = round(luv.uv.x, precision)
					y = round(luv.uv.y, precision)
		 
					vertsDict[(x, y)].append(luv)



def AreVectsLinedOnAxis(verts):
	areLinedX = True
	areLinedY = True
	allowedError = 0.0009
	valX = verts[0].uv.x
	valY = verts[0].uv.y
	for v in verts:
		if abs(valX - v.uv.x) > allowedError:
			areLinedX = False
		if abs(valY - v.uv.y) > allowedError:
			areLinedY = False
	return areLinedX or areLinedY  



def ScaleTo0OnAxisAndCursor(filteredVerts, vertsDict, startv = None, horizontal = None):      
	
	verts = filteredVerts
	verts.sort(key=lambda x: x.uv[0])      #sort by .x
	
	first = verts[0]
	last = verts[len(verts)-1]
	
	if horizontal is None:
		horizontal = True
		if ((last.uv.x - first.uv.x) >0.0009):
			slope = (last.uv.y - first.uv.y)/(last.uv.x - first.uv.x)
			if (slope > 1) or (slope <-1):
				horizontal = False 
		else: 
			horizontal = False
	
	if horizontal is True:
		if startv is None:
			startv = first  
		
		SetAll2dCursorsTo(startv.uv.x, startv.uv.y)
		#scale to 0 on Y
		ScaleTo0('Y')
		return
	   
	else:
		verts.sort(key=lambda x: x.uv[1])  #sort by .y
		verts.reverse()     #reverse because y values drop from up to down
		first = verts[0]
		last = verts[len(verts)-1]
		if startv is None:
			startv = first  

		SetAll2dCursorsTo(startv.uv.x, startv.uv.y)
		#scale to 0 on X
		ScaleTo0('X')
		return



def SetAll2dCursorsTo(x,y):
	last_area = bpy.context.area.type
	bpy.context.area.type = 'IMAGE_EDITOR'
   
	bpy.ops.uv.cursor_set(location=(x, y))

	bpy.context.area.type = last_area
	return



def CursorClosestTo(verts, allowedError = 0.025):
	ratioX, ratioY = ImageRatio()
	
	#any length that is certantly not smaller than distance of the closest
	min = 1000
	minV = verts[0]
	for v in verts:
		if v is None: continue
		for area in bpy.context.screen.areas:
			if area.type == 'IMAGE_EDITOR':
				loc = area.spaces[0].cursor_location
				hyp = hypot(loc.x/ratioX -v.uv.x, loc.y/ratioY -v.uv.y)
				if (hyp < min):
					min = hyp
					minV = v
	
	if min is not 1000: 
		return minV
	return None




def SuccessFinished(me, startTime):
	#use for backtrack of steps 
	#bpy.ops.ed.undo_push()
	bmesh.update_edit_mesh(me)
	#elapsed = round(time.clock()-startTime, 2)
	#if (elapsed >= 0.05): operator.report({'INFO'}, "UvSquares finished, elapsed:", elapsed, "s.")
	return



def ShapeFace(uv_layers, operator, targetFace, vertsDict, square):
	corners = []
	for l in targetFace.loops:
		luv = l[uv_layers]
		corners.append(luv)
	
	if len(corners) is not 4: 
		#operator.report({'ERROR'}, "bla")
		return
	
	lucv, ldcv, rucv, rdcv = Corners(corners)
	
	cct = CursorClosestTo([lucv, ldcv, rdcv, rucv])
	if cct is None: 
		cct = lucv
	
	MakeUvFaceEqualRectangle(vertsDict, lucv, rucv, rdcv, ldcv, cct, square)
	return



def MakeUvFaceEqualRectangle(vertsDict, lucv, rucv, rdcv, ldcv, startv, square = False):
	ratioX, ratioY = ImageRatio()
	ratio = ratioX/ratioY
	
	if startv is None: startv = lucv.uv
	elif AreVertsQuasiEqual(startv, rucv): startv = rucv.uv
	elif AreVertsQuasiEqual(startv, rdcv): startv = rdcv.uv
	elif AreVertsQuasiEqual(startv, ldcv): startv = ldcv.uv
	else: startv = lucv.uv
	
	lucv = lucv.uv
	rucv = rucv.uv
	rdcv = rdcv.uv
	ldcv = ldcv.uv    
   
	if (startv == lucv): 
		finalScaleX = hypotVert(lucv, rucv)
		finalScaleY = hypotVert(lucv, ldcv)
		currRowX = lucv.x
		currRowY = lucv.y
	
	elif (startv == rucv):
		finalScaleX = hypotVert(rucv, lucv)
		finalScaleY = hypotVert(rucv, rdcv)
		currRowX = rucv.x - finalScaleX
		currRowY = rucv.y
	   
	elif (startv == rdcv):
		finalScaleX = hypotVert(rdcv, ldcv)
		finalScaleY = hypotVert(rdcv, rucv)
		currRowX = rdcv.x - finalScaleX
		currRowY = rdcv.y + finalScaleY
		
	else:
		finalScaleX = hypotVert(ldcv, rdcv)
		finalScaleY = hypotVert(ldcv, lucv)
		currRowX = ldcv.x
		currRowY = ldcv.y +finalScaleY
	
	if square: finalScaleY = finalScaleX*ratio
	#lucv, rucv
	x = round(lucv.x, precision)
	y = round(lucv.y, precision)
	for v in vertsDict[(x,y)]:
		v.uv.x = currRowX
		v.uv.y = currRowY
  
	x = round(rucv.x, precision)
	y = round(rucv.y, precision)
	for v in vertsDict[(x,y)]:
		v.uv.x = currRowX + finalScaleX
		v.uv.y = currRowY
	
	#rdcv, ldcv
	x = round(rdcv.x, precision)
	y = round(rdcv.y, precision)    
	for v in vertsDict[(x,y)]:
		v.uv.x = currRowX + finalScaleX
		v.uv.y = currRowY - finalScaleY
		
	x = round(ldcv.x, precision)
	y = round(ldcv.y, precision)    
	for v in vertsDict[(x,y)]:
		v.uv.x = currRowX
		v.uv.y = currRowY - finalScaleY

		
	return



def FollowActiveUV(operator, me, f_act, faces, EXTEND_MODE = 'LENGTH_AVERAGE'):
	bm = bmesh.from_edit_mesh(me)
	uv_act = bm.loops.layers.uv.active
	
	# our own local walker
	def walk_face_init(faces, f_act):
		# first tag all faces True (so we dont uvmap them)
		for f in bm.faces:
			f.tag = True
		# then tag faces arg False
		for f in faces:
			f.tag = False
		# tag the active face True since we begin there
		f_act.tag = True

	def walk_face(f):
		# all faces in this list must be tagged
		f.tag = True
		faces_a = [f]
		faces_b = []

		while faces_a:
			for f in faces_a:
				for l in f.loops:
					l_edge = l.edge
					if (l_edge.is_manifold is True) and (l_edge.seam is False):
						l_other = l.link_loop_radial_next
						f_other = l_other.face
						if not f_other.tag:
							yield (f, l, f_other)
							f_other.tag = True
							faces_b.append(f_other)
			# swap
			faces_a, faces_b = faces_b, faces_a
			faces_b.clear()

	def walk_edgeloop(l):
		"""
		Could make this a generic function
		"""
		e_first = l.edge
		e = None
		while True:
			e = l.edge
			yield e

			# don't step past non-manifold edges
			if e.is_manifold:
				# welk around the quad and then onto the next face
				l = l.link_loop_radial_next
				if len(l.face.verts) == 4:
					l = l.link_loop_next.link_loop_next
					if l.edge is e_first:
						break
				else:
					break
			else:
				break

	def extrapolate_uv(fac,
					   l_a_outer, l_a_inner,
					   l_b_outer, l_b_inner):
		l_b_inner[:] = l_a_inner
		l_b_outer[:] = l_a_inner + ((l_a_inner - l_a_outer) * fac)

	def apply_uv(f_prev, l_prev, f_next):
		l_a = [None, None, None, None]
		l_b = [None, None, None, None]

		l_a[0] = l_prev
		l_a[1] = l_a[0].link_loop_next
		l_a[2] = l_a[1].link_loop_next
		l_a[3] = l_a[2].link_loop_next

		#  l_b
		#  +-----------+
		#  |(3)        |(2)
		#  |           |
		#  |l_next(0)  |(1)
		#  +-----------+
		#        ^
		#  l_a   |
		#  +-----------+
		#  |l_prev(0)  |(1)
		#  |    (f)    |
		#  |(3)        |(2)
		#  +-----------+
		#  copy from this face to the one above.

		# get the other loops
		l_next = l_prev.link_loop_radial_next
		if l_next.vert != l_prev.vert:
			l_b[1] = l_next
			l_b[0] = l_b[1].link_loop_next
			l_b[3] = l_b[0].link_loop_next
			l_b[2] = l_b[3].link_loop_next
		else:
			l_b[0] = l_next
			l_b[1] = l_b[0].link_loop_next
			l_b[2] = l_b[1].link_loop_next
			l_b[3] = l_b[2].link_loop_next

		l_a_uv = [l[uv_act].uv for l in l_a]
		l_b_uv = [l[uv_act].uv for l in l_b]

		if EXTEND_MODE == 'LENGTH_AVERAGE':
			fac = edge_lengths[l_b[2].edge.index][0] / edge_lengths[l_a[1].edge.index][0]
		elif EXTEND_MODE == 'LENGTH':
			a0, b0, c0 = l_a[3].vert.co, l_a[0].vert.co, l_b[3].vert.co
			a1, b1, c1 = l_a[2].vert.co, l_a[1].vert.co, l_b[2].vert.co

			d1 = (a0 - b0).length + (a1 - b1).length
			d2 = (b0 - c0).length + (b1 - c1).length
			try:
				fac = d2 / d1
			except ZeroDivisionError:
				fac = 1.0
		else:
			fac = 1.0

		extrapolate_uv(fac,
					   l_a_uv[3], l_a_uv[0],
					   l_b_uv[3], l_b_uv[0])

		extrapolate_uv(fac,
					   l_a_uv[2], l_a_uv[1],
					   l_b_uv[2], l_b_uv[1])

	# -------------------------------------------
	# Calculate average length per loop if needed

	if EXTEND_MODE == 'LENGTH_AVERAGE':
		bm.edges.index_update()
		edge_lengths = [None] * len(bm.edges)   #NoneType times the length of edges list
		
		for f in faces:
			# we know its a quad
			l_quad = f.loops[:] 
			l_pair_a = (l_quad[0], l_quad[2])
			l_pair_b = (l_quad[1], l_quad[3])

			for l_pair in (l_pair_a, l_pair_b):
				if edge_lengths[l_pair[0].edge.index] is None:

					edge_length_store = [-1.0]
					edge_length_accum = 0.0
					edge_length_total = 0

					for l in l_pair:
						if edge_lengths[l.edge.index] is None:
							for e in walk_edgeloop(l):
								if edge_lengths[e.index] is None:
									edge_lengths[e.index] = edge_length_store
									edge_length_accum += e.calc_length()
									edge_length_total += 1

					edge_length_store[0] = edge_length_accum / edge_length_total

	# done with average length
	# ------------------------

	walk_face_init(faces, f_act)
	for f_triple in walk_face(f_act):
		apply_uv(*f_triple)

	bmesh.update_edit_mesh(me, False)


def ImageRatio():
	ratioX, ratioY = 256,256
	for a in bpy.context.screen.areas:
		if a.type == 'IMAGE_EDITOR':
			img = a.spaces[0].image
			if img is not None and img.size[0] is not 0:
				ratioX, ratioY = img.size[0], img.size[1]
			break
	return ratioX, ratioY



def Corners(corners):
	firstHighest = corners[0]
	for c in corners:
		if c.uv.y > firstHighest.uv.y:
			firstHighest = c    
	corners.remove(firstHighest)
	
	secondHighest = corners[0]
	for c in corners:
		if (c.uv.y > secondHighest.uv.y):
			secondHighest = c
	
	if firstHighest.uv.x < secondHighest.uv.x:
		leftUp = firstHighest
		rightUp = secondHighest
	else:
		leftUp = secondHighest
		rightUp = firstHighest
	corners.remove(secondHighest)
	
	firstLowest = corners[0]
	secondLowest = corners[1]
	
	if firstLowest.uv.x < secondLowest.uv.x:
		leftDown = firstLowest
		rightDown = secondLowest
	else:
		leftDown = secondLowest
		rightDown = firstLowest
	
	return leftUp, leftDown, rightUp, rightDown




def AreVertsQuasiEqual(v1, v2, allowedError = 0.0009):
	if abs(v1.uv.x -v2.uv.x) < allowedError and abs(v1.uv.y -v2.uv.y) < allowedError:
		return True
	return False



def hypotVert(v1, v2):
    hyp = hypot(v1.x - v2.x, v1.y - v2.y)
    return hyp

bpy.utils.register_class(op)