import bpy
import os
import bmesh
from mathutils import Vector
from collections import defaultdict
from math import pi
from random import random

from . import utilities_ui
from . import settings
from . import utilities_bake as ub #Use shorthand ub = utitlites_bake


# Notes: https://docs.blender.org/manual/en/dev/render/blender_render/bake.html
modes={
	'normal_tangent':	ub.BakeMode('',					type='NORMAL', 	color=(0.5, 0.5, 1, 1), use_project=True),
	'normal_object': 	ub.BakeMode('',					type='NORMAL', 	color=(0.5, 0.5, 1, 1), normal_space='OBJECT' ),
	'cavity': 			ub.BakeMode('bake_cavity',		type='EMIT', 	setVColor=ub.setup_vertex_color_dirty),
	'paint_base': 		ub.BakeMode('bake_paint_base',	type='EMIT'),
	'dust': 			ub.BakeMode('bake_dust',		type='EMIT', 	setVColor=ub.setup_vertex_color_dirty),
	'id_element':		ub.BakeMode('bake_vertex_color',type='EMIT', 	setVColor=ub.setup_vertex_color_id_element),
	'id_material':		ub.BakeMode('bake_vertex_color',type='EMIT', 	setVColor=ub.setup_vertex_color_id_material),
	'selection':		ub.BakeMode('bake_vertex_color',type='EMIT', 	color=(0, 0, 0, 1), setVColor=ub.setup_vertex_color_selection),
	'diffuse':			ub.BakeMode('',					type='DIFFUSE'),
	# 'displacment':		ub.BakeMode('',					type='DISPLACEMENT', use_project=True, color=(0, 0, 0, 1), engine='CYCLES'),
	'ao':				ub.BakeMode('',					type='AO', 		color=(1, 1, 1, 0), params=["bake_samples"], engine='CYCLES'),
	'ao_legacy':		ub.BakeMode('',					type='AO', 		params=["bake_samples"], engine='CYCLES'),
	'position':			ub.BakeMode('bake_position',	type='EMIT'),
	'curvature':		ub.BakeMode('',					type='NORMAL',	use_project=True, params=["bake_curvature_size"], composite="curvature"),
	'wireframe':		ub.BakeMode('bake_wireframe',	type='EMIT', 	color=(0, 0, 0, 1), params=["bake_wireframe_size"])
}

if hasattr(bpy.types,"ShaderNodeBevel"):
	# Has newer bevel shader (2.7 nightly build series)
	modes['bevel_mask'] = ub.BakeMode('bake_bevel_mask',				type='EMIT', 	color=(0, 0, 0, 1), params=["bake_bevel_samples","bake_bevel_size"])
	modes['normal_tangent_bevel'] = ub.BakeMode('bake_bevel_normal',	type='NORMAL', 	color=(0.5, 0.5, 1, 1), params=["bake_bevel_samples","bake_bevel_size"])
	modes['normal_object_bevel'] = ub.BakeMode('bake_bevel_normal',		type='NORMAL', 	color=(0.5, 0.5, 1, 1), normal_space='OBJECT', params=["bake_bevel_samples","bake_bevel_size"])



class op(bpy.types.Operator):
	bl_idname = "uv.textools_bake"
	bl_label = "Bake"
	bl_description = "Bake selected objects"

	@classmethod
	def poll(cls, context):
		if len(settings.sets) == 0:
			return False
		return True

	def execute(self, context):
		bake_mode = utilities_ui.get_bake_mode()

		if bake_mode not in modes:
			self.report({'ERROR_INVALID_INPUT'}, "Uknown mode '{}' only available: '{}'".format(bake_mode, ", ".join(modes.keys() )) )
			return

		# Store Selection
		selected_objects 	= [obj for obj in bpy.context.selected_objects]
		active_object 		= bpy.context.view_layer.objects.active
		ub.store_bake_settings()

		# Render sets
		bake(
			self = self, 
			mode = bake_mode,
			size = bpy.context.scene.texToolsSettings.size, 

			bake_single = bpy.context.scene.texToolsSettings.bake_force_single,
			sampling_scale = int(bpy.context.scene.texToolsSettings.bake_sampling),
			samples = bpy.context.scene.texToolsSettings.bake_samples,
			ray_distance = bpy.context.scene.texToolsSettings.bake_ray_distance
		)
		
		# Restore selection
		ub.restore_bake_settings()
		bpy.ops.object.select_all(action='DESELECT')
		for obj in selected_objects:
			obj.select_set( state = True, view_layer = None)
		if active_object:
			bpy.context.view_layer.objects.active = active_object

		return {'FINISHED'}



def bake(self, mode, size, bake_single, sampling_scale, samples, ray_distance):

	print("Bake '{}'".format(mode))

	bpy.context.scene.render.engine = modes[mode].engine #Switch render engine

	# Disable edit mode
	if bpy.context.view_layer.objects.active != None and bpy.context.object.mode != 'OBJECT':
		bpy.ops.object.mode_set(mode='OBJECT')

	ub.store_materials_clear()

	# Get the baking sets / pairs
	sets = settings.sets

	render_width = sampling_scale * size[0]
	render_height = sampling_scale * size[1]

	for s in range(0,len(sets)):
		set = sets[s]

		# Get image name
		name_texture = "{}_{}".format(set.name, mode)
		if bake_single:
			name_texture = "{}_{}".format(sets[0].name, mode)# In Single mode bake into same texture
		path = bpy.path.abspath("//{}.tga".format(name_texture))

		# Requires 1+ low poly objects
		if len(set.objects_low) == 0:
			self.report({'ERROR_INVALID_INPUT'}, "No low poly object as part of the '{}' set".format(set.name) )
			return

		# Check for UV maps
		for obj in set.objects_low:
			if not obj.data.uv_layers or len(obj.data.uv_layers) == 0:
				self.report({'ERROR_INVALID_INPUT'}, "No UV map available for '{}'".format(obj.name))
				return

		# Check for cage inconsistencies
		if len(set.objects_cage) > 0 and (len(set.objects_low) != len(set.objects_cage)):
			self.report({'ERROR_INVALID_INPUT'}, "{}x cage objects do not match {}x low poly objects for '{}'".format(len(set.objects_cage), len(set.objects_low), obj.name))
			return

		# Get Materials
		material_loaded = get_material(mode)
		material_empty = None
		if "TT_bake_node" in bpy.data.materials:
			material_empty = bpy.data.materials["TT_bake_node"]
		else:
			material_empty = bpy.data.materials.new(name="TT_bake_node")


		# Assign Materials to Objects
		if (len(set.objects_high) + len(set.objects_float)) == 0:
			# Low poly bake: Assign material to lowpoly
			for obj in set.objects_low:
				assign_vertex_color(mode, obj)
				assign_material(mode, obj, material_loaded, material_empty)
		else:
			# High to low poly: Low poly require empty material to bake into image
			for obj in set.objects_low:
				assign_material(mode, obj, None, material_empty)

			# Assign material to highpoly
			for obj in (set.objects_high+set.objects_float):
				assign_vertex_color(mode, obj)
				assign_material(mode, obj, material_loaded)


		# Setup Image
		is_clear = (not bake_single) or (bake_single and s==0)
		image = setup_image(mode, name_texture, render_width, render_height, path, is_clear)

		# Assign bake node to Material
		setup_image_bake_node(set.objects_low[0], image)
		

		print("Bake '{}' = {}".format(set.name, path))

		# Hide all cage objects i nrender
		for obj_cage in set.objects_cage:
			obj_cage.hide_render = True

		# Bake each low poly object in this set
		for i in range(len(set.objects_low)):
			obj_low = set.objects_low[i]
			obj_cage = None if i >= len(set.objects_cage) else set.objects_cage[i]

			# Disable hide render
			obj_low.hide_render = False

			bpy.ops.object.select_all(action='DESELECT')
			obj_low.select_set( state = True, view_layer = None)
			bpy.context.view_layer.objects.active = obj_low

			if modes[mode].engine == 'BLENDER_EEVEE':
				# Assign image to texture faces
				bpy.ops.object.mode_set(mode='EDIT')
				bpy.ops.mesh.select_all(action='SELECT')

				for area in bpy.context.screen.areas:
					if area.type == 'IMAGE_EDITOR':
						area.spaces[0].image = image
				# bpy.data.screens['UV Editing'].areas[1].spaces[0].image = image


				bpy.ops.object.mode_set(mode='OBJECT')

			for obj_high in (set.objects_high):
				obj_high.select_set( state = True, view_layer = None)
			cycles_bake(
				mode, 
				bpy.context.scene.texToolsSettings.padding,
				sampling_scale, 
				samples, 
				ray_distance,
				 len(set.objects_high) > 0, 
				 obj_cage
			)

			# Bake Floaters seperate bake
			if len(set.objects_float) > 0:
				bpy.ops.object.select_all(action='DESELECT')
				for obj_high in (set.objects_float):
					obj_high.select_set( state = True, view_layer = None)
				obj_low.select_set( state = True, view_layer = None)

				cycles_bake(
					mode, 
					0,
					sampling_scale, 
					samples, 
					ray_distance, 
					len(set.objects_float) > 0,
					obj_cage
				)

			# Set background image (CYCLES & BLENDER_EEVEE)
			for area in bpy.context.screen.areas:
				if area.type == 'IMAGE_EDITOR':
					area.spaces[0].image = image

		# Restore renderable for cage objects
		for obj_cage in set.objects_cage:
			obj_cage.hide_render = False


		# Downsample image?
		if not bake_single or (bake_single and s == len(sets)-1):
			# When baking single, only downsample on last bake
			if render_width != size[0] or render_height != size[1]:
				image.scale(size[0],size[1])
		
		# Apply composite nodes on final image result
		if modes[mode].composite:
			apply_composite(image, modes[mode].composite, bpy.context.scene.texToolsSettings.bake_curvature_size)

		# image.save()

	# Restore non node materials
	ub.restore_materials()




def apply_composite(image, scene_name, size):
	previous_scene = bpy.context.window.scene

	# Get Scene with compositing nodes
	scene = None
	if scene_name in bpy.data.scenes:
		scene = bpy.data.scenes[scene_name]
	else:
		path = os.path.join(os.path.dirname(__file__), "resources/compositing.blend")+"\\Scene\\"
		bpy.ops.wm.append(filename=scene_name, directory=path, link=False, autoselect=False)
		scene = bpy.data.scenes[scene_name]

	if scene:
		# Switch scene
		bpy.context.window.scene = scene

		#Setup composite nodes for Curvature
		if "Image" in scene.node_tree.nodes:
			scene.node_tree.nodes["Image"].image = image

		if "Offset" in scene.node_tree.nodes:
			scene.node_tree.nodes["Offset"].outputs[0].default_value = size
			print("Assign offset: {}".format(scene.node_tree.nodes["Offset"].outputs[0].default_value))

		# Render image
		bpy.ops.render.render(use_viewport=False)
		

		# Get last images of viewer node and render result
		image_viewer_node = get_last_item("Viewer Node", bpy.data.images)
		image_render_result = get_last_item("Render Result", bpy.data.images)

		#Copy pixels
		image.pixels = image_viewer_node.pixels[:]
		image.update()

		if image_viewer_node:
			bpy.data.images.remove(image_viewer_node)
		if image_render_result:
			bpy.data.images.remove(image_render_result)

		#Restore scene & remove other scene
		bpy.context.window.scene = previous_scene
		
		# Delete compositing scene
		bpy.data.scenes.remove(scene)



def get_last_item(key_name, collection):
	# bpy.data.images
	# Get last image of a series, e.g. .001, .002, 003
	keys = []
	for item in collection:
		if key_name in item.name:
			keys.append(item.name)

	print("Search for {}x : '{}'".format(len(keys), ",".join(keys) ) )

	if len(keys) > 0:
		return collection[keys[-1]]

	return None




def setup_image(mode, name, width, height, path, is_clear):
	image = None

	print("Path "+path)
	if name in bpy.data.images:
		image = bpy.data.images[name]
		if image.source == 'FILE':
			# Clear image if it was deleted outside
			if not os.path.isfile(image.filepath):
				image.user_clear()
				bpy.data.images.remove(image)
		# bpy.data.images[name].update()

		# if bpy.data.images[name].has_data == False:
			

		# Previous image does not have data, remove first
	# 	print("Image pointer exists but no data "+name)
	# 	image = bpy.data.images[name]
	# 	image.update()
	# image.generated_height = height
	# bpy.data.images.remove(bpy.data.images[name])

	if name not in bpy.data.images:
		# Create new image with 32 bit float
		is_float_32 = bpy.context.preferences.addons["textools"].preferences.bake_32bit_float == '32'
		image = bpy.data.images.new(name, width=width, height=height, float_buffer=is_float_32)
		if "_normal_" in image.name:
    			image.colorspace_settings.name = 'Non-Color'
		else:
			image.colorspace_settings.name = 'sRGB'


	else:
		# Reuse existing Image
		image = bpy.data.images[name]
		# Reisze?
		if image.size[0] != width or image.size[1] != height or image.generated_width != width or image.generated_height != height:
			image.generated_width = width
			image.generated_height = height
			image.scale(width, height)

	# Fill with plain color
	if is_clear:
		image.generated_color = modes[mode].color
		image.generated_type = 'BLANK'


	image.file_format = 'TARGA'

	# TODO: Verify that the path exists
	# image.filepath_raw = path

	return image



def setup_image_bake_node(obj, image):

	if len(obj.data.materials) <= 0:
			print("ERROR, need spare material to setup active image texture to bake!!!")
	else:
		for slot in obj.material_slots:
			if slot.material:
				if(slot.material.use_nodes == False):
					slot.material.use_nodes = True

				# Assign bake node
				tree = slot.material.node_tree
				node = None
				if "bake" in tree.nodes:
					node = tree.nodes["bake"]
				else:
					node = tree.nodes.new("ShaderNodeTexImage")
				node.name = "bake"
				node.select = True
				node.image = image
				tree.nodes.active = node



def assign_vertex_color(mode, obj):
	if modes[mode].setVColor:
		modes[mode].setVColor(obj)



def assign_material(mode, obj, material_bake=None, material_empty=None):
	ub.store_materials(obj)

	bpy.context.view_layer.objects.active = obj
	obj.select_set( state = True, view_layer = None)

	# Select All faces
	bpy.ops.object.mode_set(mode='EDIT')
	bm = bmesh.from_edit_mesh(bpy.context.active_object.data);
	faces = [face for face in bm.faces if face.select]
	bpy.ops.mesh.select_all(action='SELECT')


	if material_bake:
		# Setup properties of bake materials
		if mode == 'wireframe':
			if "Value" in material_bake.node_tree.nodes:
				material_bake.node_tree.nodes["Value"].outputs[0].default_value = bpy.context.scene.texToolsSettings.bake_wireframe_size
		if mode == 'bevel_mask':
			if "Bevel" in material_bake.node_tree.nodes:
				material_bake.node_tree.nodes["Bevel"].inputs[0].default_value = bpy.context.scene.texToolsSettings.bake_bevel_size
				material_bake.node_tree.nodes["Bevel"].samples = bpy.context.scene.texToolsSettings.bake_bevel_samples
		if mode == 'normal_tangent_bevel':
			if "Bevel" in material_bake.node_tree.nodes:
				material_bake.node_tree.nodes["Bevel"].inputs[0].default_value = bpy.context.scene.texToolsSettings.bake_bevel_size
				material_bake.node_tree.nodes["Bevel"].samples = bpy.context.scene.texToolsSettings.bake_bevel_samples
		if mode == 'normal_object_bevel':
			if "Bevel" in material_bake.node_tree.nodes:
				material_bake.node_tree.nodes["Bevel"].inputs[0].default_value = bpy.context.scene.texToolsSettings.bake_bevel_size
				material_bake.node_tree.nodes["Bevel"].samples = bpy.context.scene.texToolsSettings.bake_bevel_samples



	# Don't apply in diffuse mode
	if mode != 'diffuse':
		if material_bake:
			# Override with material_bake
			if len(obj.material_slots) == 0:
				obj.data.materials.append(material_bake)

			else:
				obj.material_slots[0].material = material_bake
				obj.active_material_index = 0
				bpy.ops.object.material_slot_assign()

		elif material_empty:
			#Assign material_empty if no material available
			if len(obj.material_slots) == 0:
				obj.data.materials.append(material_empty)

			else: # not obj.material_slots[0].material:
				obj.material_slots[0].material = material_empty
				obj.active_material_index = 0
				bpy.ops.object.material_slot_assign()

	# Restore Face selection
	bpy.ops.mesh.select_all(action='DESELECT')
	for face in faces:
		face.select = True

	bpy.ops.object.mode_set(mode='OBJECT')

			

			



def get_material(mode):

	

	if modes[mode].material == "":
		return None # No material setup requires

	# Find or load material
	name = modes[mode].material
	path = os.path.join(os.path.dirname(__file__), "resources/materials.blend")+"\\Material\\"
	if "bevel" in mode:
		path = os.path.join(os.path.dirname(__file__), "resources/materials_2.80.blend")+"\\Material\\"
	
	print("Get mat {}\n{}".format(mode, path))

	if bpy.data.materials.get(name) is None:
		print("Material not yet loaded: "+mode)
		bpy.ops.wm.append(filename=name, directory=path, link=False, autoselect=False)

	return bpy.data.materials.get(name)




def cycles_bake(mode, padding, sampling_scale, samples, ray_distance, is_multi, obj_cage):
	

	# if modes[mode].engine == 'BLENDER_EEVEE': 
	# 	# Snippet: https://gist.github.com/AndrewRayCode/760c4634a77551827de41ed67585064b
	# 	bpy.context.scene.render.bake_margin = padding

	# 	# AO Settings
	# 	bpy.context.scene.render.bake_type = modes[mode].type
	# 	bpy.context.scene.render.use_bake_normalize = True

	# 	if modes[mode].type == 'AO':
	# 		bpy.context.scene.world.light_settings.use_ambient_occlusion = True
	# 		bpy.context.scene.world.light_settings.gather_method = 'RAYTRACE'
	# 		bpy.context.scene.world.light_settings.samples = samples

	# 	bpy.context.scene.render.use_bake_selected_to_active = is_multi
	# 	bpy.context.scene.render.bake_distance = ray_distance
	# 	bpy.context.scene.render.use_bake_clear = False

	# 	bpy.ops.object.bake_image()


	if modes[mode].engine == 'CYCLES' or modes[mode].engine == 'BLENDER_EEVEE' :

		if modes[mode].normal_space == 'OBJECT':
			#See: https://twitter.com/Linko_3D/status/963066705584054272
			bpy.context.scene.render.bake.normal_r = 'POS_X'
			bpy.context.scene.render.bake.normal_g = 'POS_Z'
			bpy.context.scene.render.bake.normal_b = 'NEG_Y'

		elif modes[mode].normal_space == 'TANGENT':
			bpy.context.scene.render.bake.normal_r = 'POS_X'
			bpy.context.scene.render.bake.normal_b = 'POS_Z'
			# Adjust Y swizzle from Addon preferences
			swizzle_y = bpy.context.preferences.addons["textools"].preferences.swizzle_y_coordinate
			if swizzle_y == 'Y-':
				bpy.context.scene.render.bake.normal_g = 'NEG_Y'
			elif swizzle_y == 'Y+':
				bpy.context.scene.render.bake.normal_g = 'POS_Y'

		# Set samples
		bpy.context.scene.cycles.samples = samples

		# Speed up samples for simple render modes
		if modes[mode].type == 'EMIT' or modes[mode].type == 'DIFFUSE':
			bpy.context.scene.cycles.samples = 1

		# Pixel Padding
		bpy.context.scene.render.bake.margin = padding * sampling_scale

		# Disable Direct and Indirect for all 'DIFFUSE' bake types
		if modes[mode].type == 'DIFFUSE':
			bpy.context.scene.render.bake.use_pass_direct = False
			bpy.context.scene.render.bake.use_pass_indirect = False
			bpy.context.scene.render.bake.use_pass_color = True

		if obj_cage is None:
			# Bake with Cage
			bpy.ops.object.bake(
				type=modes[mode].type, 
				use_clear=False, 
				cage_extrusion=ray_distance, 

				use_selected_to_active=is_multi, 
				normal_space=modes[mode].normal_space
			)
		else:
			# Bake without Cage
			bpy.ops.object.bake(
				type=modes[mode].type, 
				use_clear=False, 
				cage_extrusion=ray_distance, 

				use_selected_to_active=is_multi, 
				normal_space=modes[mode].normal_space,

				#Use Cage and assign object
				use_cage=True, 	
				cage_object=obj_cage.name
			)

bpy.utils.register_class(op)
