import bpy
import bmesh

from math import hypot
from collections import defaultdict
from . import utilities_uv


precision = 3



class op(bpy.types.Operator):
	bl_idname = "uv.textools_rectify"
	bl_label = "Rectify"
	bl_description = "Align selected UV faces or vertices to rectangular distribution"
	bl_options = {'REGISTER', 'UNDO'}

	@classmethod
	def poll(cls, context):
		if bpy.context.area.ui_type != 'UV':
			return False
		if not bpy.context.active_object:
			return False
		if bpy.context.active_object.mode != 'EDIT':
			return False
		if bpy.context.active_object.type != 'MESH':
			return False
		if not bpy.context.active_object.data.uv_layers:
			return False
		if context.scene.tool_settings.use_uv_select_sync:
			return False
		return True


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



def rectify(self, context, me=None, bm=None, uv_layers=None):
	if me is None:
		me = bpy.context.active_object.data
		bm = bmesh.from_edit_mesh(me)
		uv_layers = bm.loops.layers.uv.verify()

	# Store selection
	faces_loops = utilities_uv.selection_store(bm, uv_layers, return_selected_faces_loops=True)

	# Find selection islands
	islands = utilities_uv.getSelectionIslands(bm, uv_layers, selected_faces=set(faces_loops.keys()))

	for island in islands:
		bpy.ops.uv.select_all(action='DESELECT')
		utilities_uv.set_selected_faces(island, bm, uv_layers)
		main(me, bm, uv_layers, island, faces_loops)
	
	# Restore selection
	utilities_uv.selection_restore(bm, uv_layers)



def main(me, bm, uv_layers, selFacesMix, faces_loops, return_discarded_faces=False):

	filteredVerts, selFaces, vertsDict, discarded_faces = ListsOfVerts(bm, uv_layers, selFacesMix, faces_loops)   

	if len(filteredVerts) < 2:
		if return_discarded_faces:
			return set()
		return

	if not selFaces:
		if discarded_faces:
			if return_discarded_faces:
				return discarded_faces
		else:
			# Line is selected -> align on axis
			for luv in filteredVerts:
				x = round(luv.uv.x, precision)
				y = round(luv.uv.y, precision)
				if luv not in vertsDict[(x, y)]:
					vertsDict[(x, y)].append(luv)

			areLinedX = True
			areLinedY = True
			allowedError = 0.00001
			valX = filteredVerts[0].uv.x
			valY = filteredVerts[0].uv.y
			for v in filteredVerts:
				if abs(valX - v.uv.x) > allowedError:
					areLinedX = False
				if abs(valY - v.uv.y) > allowedError:
					areLinedY = False
			
			if not (areLinedX or areLinedY):
				verts = filteredVerts
				verts.sort(key=lambda x: x.uv[0])	#sort by .x
				first = verts[0]
				last = verts[len(verts)-1]

				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 == True:
					#scale to 0 on Y
					for v in verts:
						x = round(v.uv.x, precision)
						y = round(v.uv.y, precision)
						for luv in vertsDict[(x, y)]:
							luv.uv.y = first.uv.y
				else:
					#scale to 0 on X
					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]

					for v in verts:
						x = round(v.uv.x, precision)
						y = round(v.uv.y, precision)
						for luv in vertsDict[(x, y)]:
							luv.uv.x = first.uv.x

	else:
		# At least one face is selected -> rectify
		targetFace = bm.faces.active
		# Active face checks
		if targetFace is None or len({loop for loop in targetFace.loops}.intersection(filteredVerts)) != len(targetFace.verts) or targetFace.select == False or len(targetFace.verts) != 4:
			targetFace = selFaces[0]
		
		ShapeFace(uv_layers, targetFace, vertsDict)
		
		FollowActiveUV(me, targetFace, selFaces)

		bmesh.update_edit_mesh(me, loop_triangles=False)

		if return_discarded_faces:
			return discarded_faces



def ListsOfVerts(bm, uv_layers, selFacesMix, faces_loops):
	allEdgeVerts = []
	filteredVerts = []
	selFaces = []
	discarded_faces = set()
	vertsDict = defaultdict(list)
	
	for f in selFacesMix:
		isFaceSel = True
		facesEdgeVerts = [l[uv_layers] for l in faces_loops[f]]
		if len(faces_loops[f]) < len(f.loops):
			isFaceSel = False
		
		allEdgeVerts.extend(facesEdgeVerts)
		if isFaceSel:
			if len(f.verts) != 4:
				filteredVerts.extend(facesEdgeVerts)
				discarded_faces.add(f)
			else: 
				selFaces.append(f)
				for luv in facesEdgeVerts:
					x = round(luv.uv.x, precision)
					y = round(luv.uv.y, precision)
					vertsDict[(x, y)].append(luv)
		else:
			filteredVerts.extend(facesEdgeVerts)

	if len(filteredVerts) == 0:
		filteredVerts.extend(allEdgeVerts)

	return filteredVerts, selFaces, vertsDict, discarded_faces



def ShapeFace(uv_layers, targetFace, vertsDict):
	corners = []
	for l in targetFace.loops:
		luv = l[uv_layers]
		corners.append(luv)
	
	if len(corners) != 4:
		return
	
	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
	

	verts = [leftUp, leftDown, rightDown, rightUp]

	ratioX, ratioY = ImageRatio()
	min = float('inf')
	minV = verts[0]
	for v in verts:
		if v is None:
			continue
		for area in bpy.context.screen.areas:
			if area.ui_type == 'UV':
				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

	MakeUvFaceEqualRectangle(vertsDict, leftUp, rightUp, rightDown, leftDown, minV)



def MakeUvFaceEqualRectangle(vertsDict, leftUp, rightUp, rightDown, leftDown, startv):
	ratioX, ratioY = ImageRatio()
	ratio = ratioX/ratioY
	
	if startv is None: startv = leftUp.uv
	elif AreVertsQuasiEqual(startv, rightUp): startv = rightUp.uv
	elif AreVertsQuasiEqual(startv, rightDown): startv = rightDown.uv
	elif AreVertsQuasiEqual(startv, leftDown): startv = leftDown.uv
	else: startv = leftUp.uv
	
	leftUp = leftUp.uv
	rightUp = rightUp.uv
	rightDown = rightDown.uv
	leftDown = leftDown.uv    
   
	if (startv == leftUp): 
		finalScaleX = hypotVert(leftUp, rightUp)
		finalScaleY = hypotVert(leftUp, leftDown)
		currRowX = leftUp.x
		currRowY = leftUp.y
	
	elif (startv == rightUp):
		finalScaleX = hypotVert(rightUp, leftUp)
		finalScaleY = hypotVert(rightUp, rightDown)
		currRowX = rightUp.x - finalScaleX
		currRowY = rightUp.y
	   
	elif (startv == rightDown):
		finalScaleX = hypotVert(rightDown, leftDown)
		finalScaleY = hypotVert(rightDown, rightUp)
		currRowX = rightDown.x - finalScaleX
		currRowY = rightDown.y + finalScaleY
		
	else:
		finalScaleX = hypotVert(leftDown, rightDown)
		finalScaleY = hypotVert(leftDown, leftUp)
		currRowX = leftDown.x
		currRowY = leftDown.y +finalScaleY
	
	#leftUp, rightUp
	x = round(leftUp.x, precision)
	y = round(leftUp.y, precision)
	for v in vertsDict[(x,y)]:
		v.uv.x = currRowX
		v.uv.y = currRowY
  
	x = round(rightUp.x, precision)
	y = round(rightUp.y, precision)
	for v in vertsDict[(x,y)]:
		v.uv.x = currRowX + finalScaleX
		v.uv.y = currRowY
	
	#rightDown, leftDown
	x = round(rightDown.x, precision)
	y = round(rightDown.y, precision)    
	for v in vertsDict[(x,y)]:
		v.uv.x = currRowX + finalScaleX
		v.uv.y = currRowY - finalScaleY
		
	x = round(leftDown.x, precision)
	y = round(leftDown.y, precision)    
	for v in vertsDict[(x,y)]:
		v.uv.x = currRowX
		v.uv.y = currRowY - finalScaleY



def FollowActiveUV(me, f_act, faces):
	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 == True and l_edge.seam == 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:
				# walk 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]

		try:
			fac = edge_lengths[l_b[2].edge.index][0] / edge_lengths[l_a[1].edge.index][0]
		except ZeroDivisionError:
			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
	bm.edges.index_update()
	edge_lengths = [None]*len(bm.edges)
	
	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


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



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 and img.size[0] != 0:
				ratioX, ratioY = img.size[0], img.size[1]
			break
	return ratioX, ratioY



def AreVertsQuasiEqual(v1, v2, allowedError = 0.00001):
	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)
