import bpy
import math
from mathutils import Vector
import random
from bpy.props import StringProperty, IntProperty

# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

bl_info = {
	"name": "arewo",
	"description": "Replicates Objects with their animation offset in time",
	"author": "Frederik Steinmetz",
	"version": (1, 0),
	"blender": (2, 68, 0),
	"location": "Toolshelf",
	"warning": "This is an Alpha version", # used for warning icon and text in addons panel
	"wiki_url": "",
	"tracker_url": "http://www.blenderdiplom.com",
	"category": "Animation"}

# Arewo Simple
def run_linear_offset(loops, offset_frames, random_range, offset_position, offset_rotation, create_parent):
	
	obj = bpy.context.active_object
	if create_parent: # if a parent needs to be created
		if obj.parent == None:
			bpy.ops.object.empty_add(type = "PLAIN_AXES", location = (0, 0, 0), layers = obj.layers)
			par = bpy.context.active_object
			par.name = "Arewo_Parent"
			obj.parent = par
		else:
			print("Your object already has a parent, this may cause problems.")
	# store original object data
	base_mesh = obj.data
	original_location = obj.matrix_world.to_translation()
	original_rotation = obj.rotation_euler
	
	for n in range(3): # Gets the true values for pos and rot in case of delta
		original_rotation[n] += obj.delta_rotation_euler[n]
	
	for i in range(loops): 
		# Creates parameters for the new object
		name = obj.name + str(i)
		new_object = bpy.data.objects.new(name, base_mesh)
		bpy.context.scene.objects.link(new_object) 
		new_object.layers = obj.layers
		# Offset in Time, Space and Scale
		new_object.location = original_location
		new_object.delta_location = new_object.delta_location + Vector(offset_position) * (i + 1) # without for loop
		
		for n in range(3): 
			new_object.delta_rotation_euler[n] = obj.rotation_euler[n] + offset_rotation[n] * (i + 1)
		# creates a random number in +/- half the given range 
		random_offset = int(round((random.random() - 0.5) * random_range))
		if offset_frames == 0:
			offset_time = round(random.random() * random_range)
		else:
			offset_time = offset_frames * (i + 1) + random_offset
		
		# Offset keyframes if Exist 
		if obj.animation_data != None: 
			new_action = obj.animation_data.action.copy()
			fcurves = new_action.fcurves
			print(i, ": random: ", random_offset, ", offset Time: ", offset_time)
			for curve in fcurves: 
				keyframePoints = curve.keyframe_points
				for keyframe in keyframePoints:
					keyframe.co[0] += offset_time 
					keyframe.handle_left[0] += offset_time
					keyframe.handle_right[0] += offset_time

			new_object.animation_data_create() 
			new_object.animation_data.action = new_action
		
		if create_parent: # if a parent object was created, the new object will be a child of it
			new_object.parent = par
	bpy.context.scene.update() # probably unnecessary, just in case though

##################################################################################

# Run Object Offset
def run_object_offset(loops, offset_frames, random_range, starting_frame, spacing, inherit_scale, inherit_rotation, create_parent, hide_render):
	# starting frame of the evaluation of the placer object animation
	bpy.context.scene.frame_set(starting_frame - spacing)
	#Temporarily storing obj as vars
	par = obj = bpy.context.active_object
	
	if obj.parent != None:
		print("Your object has a parenting relation, results could be unexpected")
	if create_parent: # creates a parent object, if needed
		bpy.ops.object.empty_add(type = "PLAIN_AXES", location = (0, 0, 0), layers = obj.layers)
		par = bpy.context.active_object
		par.name = "Arewo_Parent"
	
	placer = bpy.data.objects[bpy.context.scene.placer_object] 
	evaluated_frame = starting_frame
	obj_list = []
	for i in range(loops):
		# defines the parameters for the new object
		name_object = obj.name + "_" + str(i)
		empty_mesh = bpy.data.meshes.new("Empty_Mesh")
		obj_list.append(bpy.data.objects.new(name_object, empty_mesh))
		obj_list[i].layers = obj.layers
		bpy.context.scene.objects.link(obj_list[i])
		# sychnronize the parameters with the placer animation
		obj_list[i].location = placer.matrix_world.to_translation()
		if inherit_scale: 
			obj_list[i].delta_scale = placer.matrix_world.to_scale()
		if inherit_rotation:
			obj_list[i].delta_rotation_euler = placer.matrix_world.to_euler()
		# creates a random number in +/- half the given range 
		
		random_offset = int(round((random.random() - 0.5) * random_range))
		if offset_frames == 0:
			offset_time = round(random.random() * random_range)
		else:
			random_offset = int(round((random.random() - 0.5) * random_range))
			offset_time = offset_frames * i + random_offset	  
		
		if obj.animation_data != None: # Offset keyframes - if Exist
			if obj.animation_data.action != None: # Offset keyframes - if Exist
				new_action = bpy.data.actions.new(obj.animation_data.action.name + str(i))
				# creates a copy of the original action
				new_action = obj.animation_data.action.copy()
				# offsets the keyframes by the calculated value
				fcurves = new_action.fcurves
				for curve in fcurves: 
					keyframePoints = curve.keyframe_points
					for keyframe in keyframePoints:
						keyframe.co[0] += offset_time 
						keyframe.handle_left[0] += offset_time
						keyframe.handle_right[0] += offset_time

				obj_list[i].animation_data_create()
				obj_list[i].animation_data.action = new_action

		if create_parent:
			obj_list[i].parent = par
		# evaluates the next frame of the placer animation
		evaluated_frame += spacing
		bpy.context.scene.frame_set(evaluated_frame)
	for me in obj_list:
		me.data = obj.data
	if hide_render:
		obj.hide_render = True
		obj.parent = None
	bpy.context.scene.update()
##################################################################################
# Arewo Experimental
def run_with_armature(loops, offset_frames, random_range, starting_frame, spacing, inherit_rotation, inherit_scale, mute_mods, hide_render):
	if mute_mods:
		run_mute_modifiers()
	else:
		run_enable_modifiers()

	bpy.context.scene.frame_set(starting_frame - spacing) # moves in time to evaluate placer location
	placer = bpy.data.objects[bpy.context.scene.placer_object]
	par = obj = original_obj = arm = placer # temp storing objects
	kinder = bpy.context.selected_objects # stores selected objects
	offset_time = offset_frames
	temp_location = obj.location # For placing the original at it's original location

	# determines which is the armature 
	armcount = 0
	for ob in kinder:
		ob.hide_render = False # in case they got hidden in the last run
		if ob.type == 'ARMATURE': 
			original_arm = ob 
			temp_location = original_arm.location # not working, gets changed everytime you change a parameter, should not be the case...
			armcount += 1
			if armcount > 1: # checks if there's already an armature in the selected objec
				print("Using multiple armatures can be problematic")
	evaluated_frame = starting_frame
	bpy.context.scene.frame_set(evaluated_frame)
	original_arm.location = placer.matrix_world.to_translation()
	if inherit_rotation:
		original_arm.delta_rotation_euler = placer.matrix_world.to_euler()
	if inherit_scale:
	#   original_arm.delta_scale = placer.matrix_world.to_scale() # give weird results for negative scales
		for n in range(3):
			original_arm.delta_scale[n] = placer.scale[n]

	# For loop starts
	for i in range(loops):
		evaluated_frame += spacing
		bpy.context.scene.frame_set(evaluated_frame) # advances in time to evaluate placer location 
		
		bpy.ops.object.select_all(action = 'DESELECT') # They will only be addressed directly
		for ob in kinder:
			ob.select = True		

		bpy.ops.object.duplicate_move(OBJECT_OT_duplicate={"linked":True})
		for ob in bpy.context.selected_objects: # determine the copy of the armature
			if ob.type == 'ARMATURE':
				arm = ob
		bpy.ops.object.make_single_user( # Makes the animation data a single user
			type = 'SELECTED_OBJECTS', 
			object = False, 
			obdata = False, 
			material = False, 
			texture = False, 
			animation = True
			)
		# Determining how many frammes the animation needs to be offset
		if offset_frames == 0:
			offset_time = round(random.random() * random_range)
		else: # creates a random number in +/- half the given range
			random_offset = int(round((random.random() - 0.5) * random_range))
			offset_time = offset_frames * i + random_offset
	
		original_arm.location = placer.matrix_world.to_translation()
		if inherit_rotation:
			original_arm.delta_rotation_euler = placer.matrix_world.to_euler()
		if inherit_scale:
			for n in range(3):
				original_arm.delta_scale[n] = placer.scale[n]
		# Offset the animation if there is one
		if arm.animation_data != None:
			if arm.animation_data.action != None: # Previously deleted actions still get stored in animation data, so double check
				animData = arm.animation_data
				action = animData.action
				fcurves = action.fcurves
				for curve in fcurves: 
					keyframePoints = curve.keyframe_points
					for keyframe in keyframePoints:
						keyframe.co[0] += offset_time
						keyframe.handle_left[0] += offset_time
						keyframe.handle_right[0] += offset_time
			 
	# END FOR LOOP
	# hides the original objects
	if hide_render:
		for ob in kinder:
			ob.hide_render = True
	bpy.context.scene.update()

# helper function
def layer(n):
	return (n - 1) * [False] + [True] + (20 - n) * [False]

# function to temporarily disable all but the armature modifiers - for performance increas, duh
def run_mute_modifiers():
	objects = bpy.context.selected_objects
	for ob in objects:
		for mod in ob.modifiers:
			if mod.type != 'ARMATURE':
				mod.show_viewport = False

# function to reenable all modifiers
def run_enable_modifiers():
	objects = bpy.context.selected_objects
	for ob in objects:
		for mod in ob.modifiers:
			mod.show_viewport = True
# Able to apply a modifier and transfers the changes to all objects sharing this datablock
def apply_for_multi(remove_existing, remove_same):
	users = []
	success = False
	mod_type = 'ARMATURE' # temp storing modifier name
	original = bpy.context.active_object
	for ob in bpy.data.objects:
		ob.select = False
		if ob.data == original.data:
			users.append(ob)
	
	original.select = True
	bpy.ops.object.make_single_user(object = True, obdata = True)
	try:
		mod_type = original.modifiers[0].type
		bpy.ops.object.modifier_apply(apply_as='DATA', modifier = original.modifiers[0].name)
		success = True
	except: 
		print("No modifiers found on this object, doing nothing")
	if success:
		for obj in users:
			obj.data = original.data
			if remove_existing:
				for mod in obj.modifiers:
					obj.modifiers.remove(mod)
			elif remove_same:
				for mod in obj.modifiers:
					if mod.type == mod_type:
						obj.modifiers.remove(mod)
						
def randomize_datablocks(seed):
	objs = bpy.context.selected_objects
	
	datablocks = []
	materials = [mat.material for mat in bpy.context.active_object.material_slots]
	if (len(materials) > 1):
		for i in range(len(materials)):
			datablocks.append(bpy.context.active_object.data.copy())
			datablocks[i].materials.clear()
			datablocks[i].materials.append(materials[i])	
			
	
	bpy.ops.object.select_all(action = 'DESELECT')

	for ob in objs:
		print(ob.type)
		if ob.type == ('MESH' or 'CURVE' or 'SURFACE' or 'FONT'):
			ran = random.randrange(len(materials))
			ob.data = datablocks[ran]
		else:
			print('Object cannot have a material assigned, doing nothing...')
			
			
def randomize_materials(seed):
	objs = bpy.context.selected_objects
	bpy.ops.object.make_single_user(object = True, obdata = True, material = False, texture = False, animation = False)
	materials = [mat.material for mat in bpy.context.active_object.material_slots]
	for ob in objs:
		if ob.type == ('MESH' or 'CURVE' or 'SURFACE' or 'FONT'):
			ran = random.randrange(len(materials))
			ob.data.materials.clear()
			ob.data.materials.append(materials[ran])
		else:
			print(ob.name, ' cannot have one or more materials assigned to it, doing nothing...')
		
# ----------------------------- Operators ------------------------------
# Operator Arewo simple
class arewo_simple(bpy.types.Operator):
	bl_idname = "anim.arewo_simple"
	bl_label = "Linear Offset"
	bl_description = "Linear version, only allows for limited options, only one object, no modifiers supported"
	bl_options = {'REGISTER', 'UNDO'}
	# Defining the adjustable parameters
	loops = bpy.props.IntProperty(
			name = "Iterations",
			default = 2,
			min = 1,
			max = 10000,
			description = "Number of copies to be generated."
			)
	offset_frames = bpy.props.IntProperty(
			name = "Offset Frames",
			default = 10,
			min = 0,
			max = 10000,
			description = "Offset for the animation in frames"
			)
	random_offset = bpy.props.IntProperty(
			name = "Random Offset",
			default = 0,
			min = 0,
			max = 10000,
			description = "Random offset for the animation in frames"
			)
	offset_position = bpy.props.FloatVectorProperty(
			name = "Location Offset",
			default = (1, 0, 0),
			subtype = 'TRANSLATION',
			description = "Linear location offset"
			)
	offset_rotation = bpy.props.FloatVectorProperty(
			name = "Rotation Offset",
			default = (0, 0, 0),
			subtype = 'EULER',
			description = "Rotation offset"
			)
	create_parent = bpy.props.BoolProperty(
			name = "Create Parent", 
			default = False,
			description = "Create a parent object for all added objects"
			)
	def execute(self, context): 
		run_linear_offset(
				self.loops, 
				self.offset_frames, 
				self.random_offset, 
				self.offset_position, 
				self.offset_rotation, 
				self.create_parent
				)
		return {'FINISHED'}
##################################################################################
# Operator arewo Offset Object 
class arewo_object_offset(bpy.types.Operator):
	bl_idname = "anim.arewo_object_offset"
	bl_label = "Object Offset"
	bl_description = "Allows great control via a placer object. Only one Mesh, no modifiers supported"
	bl_options = {'REGISTER', 'UNDO'}
	loops = bpy.props.IntProperty(
			name = "Iterations",
			default = 2,
			min = 1,
			max = 10000,
			description = "Number of copies to be generated."
			)
	offset_frames = bpy.props.IntProperty(
			name = "Offset Frames",
			default = 10,
			min = 0,
			max = 10000,
			description = "Offset for the animation in frames"
			)
	random_offset = bpy.props.IntProperty(
			name = "Random Offset",
			default = 0,
			min = 0,
			max = 10000,
			description = "Random offset for the animation in frames"
			)
	starting_frame = bpy.props.IntProperty(
			name = "Start Frame",
			default = 1,
			min = 0,
			max = 10000,
			description = "Starting time of the path / animation for the placer object"
			)
	spacing = bpy.props.IntProperty(
			name = "Spacing",
			default = 1,
			min = 0,
			max = 1000,
			description = "No. of Frames in the Placer animation to be skipped before placing the next copy"
			)
	inherit_scale = bpy.props.BoolProperty(
			name = "Inherit Scale", 
			default = False,
			description = "Also copies the scale of the placer object"
			)
	inherit_rotation = bpy.props.BoolProperty(
			name = "Inherit Rotation", 
			default = False,
			description = "Also copies the rotation of the placer object"
			)
	create_parent = bpy.props.BoolProperty(
			name = "Create Parent", 
			default = False,
			description = "Create a parent object for all added objects"
			)
	hide_render = bpy.props.BoolProperty(
			name = "Hide Render", 
			default = True,
			description = "Keeps the original object from being rendered"
	)
	def execute(self, context): 
		run_object_offset(
			self.loops, 
			self.offset_frames, 
			self.random_offset,
			self.starting_frame, 
			self.spacing,
			self.inherit_scale,
			self.inherit_rotation,
			self.create_parent,
			self.hide_render
			)
		return {'FINISHED'} 
##################################################################################

# Operator Armature Offset
class arewo_advanced(bpy.types.Operator):
	bl_idname = "anim.arewo_armature_offset"
	bl_label = "Armature Offset"
	bl_description = "Allows great control via a placer object. Multiple objects and armature supported"
	bl_options = {'REGISTER', 'UNDO'}
	loops = bpy.props.IntProperty(
			name = "Iterations",
			default = 2,
			min = 1,
			max = 10000,
			description = "Number of copies to be generated."
			)
	offset_frames = bpy.props.IntProperty(
			name = "Offset Frames",
			default = 10,
			min = 0,
			max = 10000,
			description = "Offset for the animation in frames"
			)
	random_offset = bpy.props.IntProperty(
			name = "Random Offset",
			default = 0,
			min = 0,
			max = 1000,
			description = "Random offset for the animation in frames"
			)
	starting_frame = bpy.props.IntProperty(
			name = "Start Frame",
			default = 1,
			min = 0,
			max = 10000,
			description = "Starting time of the path / animation for the placer object"
			)
	spacing = bpy.props.IntProperty(
			name = "Spacing",
			default = 1,
			min = 1,
			max = 10000,
			description = "No. of Frames in the Placer animation to be skipped before placing the next copy"
			)
	inherit_scale = bpy.props.BoolProperty(
			name = "Inherit Scale", 
			default = False,
			description = "Makes the copies the same scale as the placer object at the evaluated frame"
			)
	inherit_rotation = bpy.props.BoolProperty(
			name = "Inherit Rotation", 
			default = False,
			description = "Gives the copies the same rotation as the placer object at the evaluated frame"
			)
	mute_mods = bpy.props.BoolProperty(
			name = "Mute Modifiers",
			default = False,
			description = "Temporarily hide modifiers, you can re-enable them by using the Enable Modifiers button from the Speed Up Tools",
			) 
	hide_render = bpy.props.BoolProperty(
			name = "Hide Render", 
			default = True,
			description = "Keeps the original object from being rendered"
	)
	def execute(self, context): 
		run_with_armature(
			self.loops, 
			self.offset_frames, 
			self.random_offset, 
			self.starting_frame, 
			self.spacing,
			self.inherit_rotation, 
			self.inherit_scale,
			self.mute_mods,
			self.hide_render
			)
		return {'FINISHED'} 
##################################################################################

class mute_modifiers(bpy.types.Operator):
	bl_idname = "object.mute_modifiers"
	bl_label = "Mute Modifiers"
	bl_description = "Shuts off Viewport visibility of every modifier of the selected objects that is not an Armature"
	bl_options = {'REGISTER', 'UNDO'} 
	def execute(self, context):
		run_mute_modifiers()
		return {'FINISHED'} 
 
class enable_modifiers(bpy.types.Operator):
	bl_idname = "object.enable_modifiers"
	bl_label = "Enable Modifiers"
	bl_description = "Enables Viewport visibility of all modifiers of the selected objects"
	bl_options = {'REGISTER', 'UNDO'} 
	def execute(self, context):
		run_enable_modifiers()
		return {'FINISHED'}
	
class apply_modifier_for_multi(bpy.types.Operator):
	bl_idname = "object.apply_modifier_for_multi"
	bl_label = "Apply Modifier"
	bl_description = "Applies the 1. Modifier of the selected objects and passes the changes to all objects sharing the same datablock"
	bl_options = {'REGISTER', 'UNDO'}
	remove_existing = bpy.props.BoolProperty(
			name = "Remove Existing", 
			default = False,
			description = "Removes all modifiers from objects with the same datablock"
	)   
	remove_same = bpy.props.BoolProperty(
			name = "Remove Same Type", 
			default = True,
			description = "Removes modifiers with the same type as the applied one from objects"
	)   
	def execute(self, context):
		apply_for_multi(
			self.remove_existing, 
			self.remove_same
			)
		return {'FINISHED'} 
class random_data(bpy.types.Operator):
	bl_idname = "object.random_data"
	bl_label = "Randomize Datablocks"
	bl_description = "Assigns a random material to each selected object, picking from the material slots of the active_object"
	bl_options = {'REGISTER', 'UNDO'}  
	seed = bpy.props.IntProperty(
		name = "Seed",
		default = 0,
		min = 1,
		max = 100,
		description = "Seed for the random distribution"
		)
	def execute(self, context):
		
		randomize_datablocks(self.seed)
		return {'FINISHED'}	

class random_materials(bpy.types.Operator):
	bl_idname = "object.random_materials"
	bl_label = "Randomize Materials"
	bl_description = "Assigns a random material to each selected object, picking from the material slots of the active_object"
	bl_options = {'REGISTER', 'UNDO'}  
	seed = bpy.props.IntProperty(
		name = "Seed",
		default = 0,
		min = 1,
		max = 100,
		description = "Seed for the random distribution"
		)
	def execute(self, context):
		randomize_materials(self.seed)
		return {'FINISHED'}	
	
# ------------- The Panel ----------------
class arewo_panel(bpy.types.Panel):
	
	"""Animation Offset""" 
	bl_idname = "arewo.replicate" # unique identifier for buttons and menu items to reference.
	bl_label = "Arewo" # display name in the interface.
	bl_space_type = "VIEW_3D"
	bl_region_type = "TOOLS"
	bl_category = "ARewO"
	bl_context = "objectmode"
	bpy.types.Scene.placer_object = StringProperty(name="Placer Object")

	def draw(self, context):
		layout = self.layout
		split = layout.split()
		row = layout.row()
		col = split.column(align = True)
		col.operator("anim.arewo_simple", icon = "MOD_ARRAY")
		if len(bpy.context.selected_objects) > 0 and bpy.context.active_object != None:
			col.enabled = True
		else:
			col.enabled = False
		split = row.split()
		
		col = split.column(align = True)
		col.operator("anim.arewo_object_offset", icon = "PARTICLE_POINT")
		if (bpy.context.active_object == None or len(bpy.context.selected_objects) == 0 or bpy.context.scene.placer_object == "" or bpy.context.active_object.type != 'MESH' ):
			col.active = False
		else:
			col.active = True
		
		row2 = layout.row()
		split = row2.split()		
		col = split.column(align = True)
		col.operator("anim.arewo_armature_offset", icon = "ARMATURE_DATA")
		if (bpy.context.active_object == None or len(bpy.context.selected_objects) < 2 or bpy.context.scene.placer_object == "" or not 'ARMATURE' in (ob.type for ob in bpy.context.selected_objects)):
			col.active = False
		else:
			col.active = True
		# Helper tools
		layout.prop_search(
			context.scene,  # not sure
			"placer_object",	# Scene property
			bpy.context.scene, # where to search
			"objects",	  # category to search in
			"Placer:"		 # lable name
			)

		layout.label("Speed Up Tools")
		row = layout.row()
		split = row.split()
		col = split.column(align = True)
		col.operator("object.mute_modifiers", icon = 'VISIBLE_IPO_OFF')
		col.operator("object.enable_modifiers", icon = 'VISIBLE_IPO_ON')
		col.operator("object.apply_modifier_for_multi", icon = 'MOD_REMESH')
		row = layout.row()
		split = row.split()		
		col = split.column(align = True)
		col.operator("object.random_data", icon = 'MATERIAL')
		col.operator("object.random_materials", icon = 'MATERIAL')
		if len(bpy.context.active_object.material_slots) < 2:
			col.active = False
		else:
			col.active = True
		
# Registering
 
def register():
	bpy.utils.register_class(arewo_panel)
	bpy.utils.register_class(arewo_simple)
	bpy.utils.register_class(arewo_object_offset)
	bpy.utils.register_class(arewo_advanced)
	bpy.utils.register_class(mute_modifiers)
	bpy.utils.register_class(enable_modifiers)
	bpy.utils.register_class(apply_modifier_for_multi)
	bpy.utils.register_class(random_data)
	bpy.utils.register_class(random_materials)
	
	
def unregister():
	bpy.utils.unregister_class(arewo_panel)
	bpy.utils.unregister_class(arewo_simple)
	bpy.utils.unregister_class(arewo_object_offset)
	bpy.utils.unregister_class(arewo_advanced)
	bpy.utils.unregister_class(mute_modifiers)
	bpy.utils.unregister_class(enable_modifiers)
	bpy.utils.unregister_class(apply_modifier_for_multi)
	bpy.utils.unregister_class(random_data)
	bpy.utils.unregister_class(random_materials)
	
if __name__ == "__main__":
	register()
	print("ARewO successfully registered")