# BFG Forge
# Based on Level Buddy by Matt Lucas
# https://matt-lucas.itch.io/level-buddy

#  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 3 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, see <http://www.gnu.org/licenses/>.

import bpy, bpy.utils.previews, bmesh, glob, math, os, time
from . import import_md5mesh, lexer
from mathutils import Vector

# used when creating light and entities, and exporting
_scale_to_game = 64.0
_scale_to_blender = 1.0 / _scale_to_game

_editor_material_paths = ["textures/common", "textures/editor"]

preview_collections = {}

################################################################################
## FILE SYSTEM
################################################################################

class FileSystem:
	def __init__(self):
		# highest priority first
		self.search_dirs = []
		if bpy.context.scene.bfg.mod_dir:
			self.search_dirs.append(bpy.context.scene.bfg.mod_dir)
		self.search_dirs.append("basedev")
		self.search_dirs.append("base")

	def calculate_relative_path(self, filename):
		# e.g. if game_path is "D:\Games\DOOM 3",
		# "D:\Games\DOOM 3\basedev\models\mapobjects\arcade_machine\arcade_machine.lwo"
		# should return
		# "models\mapobjects\arcade_machine\arcade_machine.lwo"
		for search_dir in self.search_dirs:
			full_search_path = os.path.join(os.path.realpath(bpy.path.abspath(bpy.context.scene.bfg.game_path)), search_dir).lower()
			full_file_path = os.path.realpath(bpy.path.abspath(filename)).lower()
			if full_file_path.startswith(full_search_path):
				return os.path.relpath(full_file_path, full_search_path)
		return None

	def find_file_path(self, filename):
		for search_dir in self.search_dirs:
			full_path = os.path.join(os.path.realpath(bpy.path.abspath(bpy.context.scene.bfg.game_path)), search_dir, filename)
			if os.path.exists(full_path):
				return full_path
		return None

	def find_image_file_path(self, filename):
		if filename == "_black":
			filename = "textures/black"
		elif filename == "_white":
			filename = "guis/assets/white"
		path = self.find_file_path(filename)
		if not path:
			# try some other extensions
			name, extension = os.path.splitext(filename)
			if extension != ".tga":
				path = self.find_file_path(name + ".tga")
			if not path and extension != ".png":
				path = self.find_file_path(name + ".png")
		return path

	def find_files(self, pattern):
		# don't touch the same file more than once
		# e.g.
		# mymod/materials/base_wall.mtr
		# basedev/materials/base_wall.mtr
		# ignore the second one
		touched_files = []
		found_files = []
		for search_dir in self.search_dirs:
			full_path = os.path.join(os.path.realpath(bpy.path.abspath(bpy.context.scene.bfg.game_path)), search_dir)
			if os.path.exists(full_path):
				for f in glob.glob(os.path.join(full_path, pattern)):
					base = os.path.basename(f)
					if not base in touched_files:
						touched_files.append(base)
						found_files.append(f)
		return found_files

################################################################################
## UTILITY FUNCTIONS
################################################################################

def min_nullable(a, b):
	if a == None or b < a:
		return b
	return a

def max_nullable(a, b):
	if a == None or b > a:
		return b
	return a

def set_object_mode_and_clear_selection():
	if bpy.context.active_object:
		bpy.ops.object.mode_set(mode='OBJECT')
	bpy.ops.object.select_all(action='DESELECT')

def link_active_object_to_group(group):
	if not group in bpy.data.groups:
		bpy.ops.group.create(name=group)
	bpy.ops.object.group_link(group=group)

################################################################################
## MATERIALS
################################################################################

class MaterialDeclPathPropGroup(bpy.types.PropertyGroup):
	pass # name property inherited

class MaterialDeclPropGroup(bpy.types.PropertyGroup):
	# name property inherited
	diffuse_texture = bpy.props.StringProperty()
	editor_texture = bpy.props.StringProperty()
	heightmap_scale = bpy.props.FloatProperty() # 0 if normal_texture isn't a heightmap
	normal_texture = bpy.props.StringProperty()
	specular_texture = bpy.props.StringProperty()
	texture = bpy.props.StringProperty() # any stage texture map. will be the light texture for light materials.

def material_decl_preview_items(self, context):
	materials = []
	pcoll = preview_collections["material"]
	if pcoll.current_decl_path == context.scene.bfg.active_material_decl_path and not pcoll.force_refresh:
		return pcoll.materials
	fs = FileSystem()
	i = 0
	for decl in context.scene.bfg.material_decls:
		decl_path = os.path.dirname(decl.name)
		if decl_path == context.scene.bfg.active_material_decl_path:
			if context.scene.bfg.hide_bad_materials and decl_path not in _editor_material_paths and (decl.diffuse_texture == "" or not fs.find_image_file_path(decl.diffuse_texture)):
				# hide materials with missing diffuse texture, but not editor materials
				continue
			if decl.editor_texture in pcoll: # workaround blender bug, pcoll.load is supposed to return cached preview if name already exists
				preview = pcoll[decl.editor_texture]
			else:
				preview = None
				if decl.editor_texture != "":
					filename = fs.find_image_file_path(decl.editor_texture)
					if filename:
						preview = pcoll.load(decl.editor_texture, filename, 'IMAGE')
			materials.append((decl.name, os.path.basename(decl.name), decl.name, preview.icon_id if preview else 0, i))
			i += 1
	materials.sort()
	pcoll.materials = materials
	pcoll.current_decl_path = context.scene.bfg.active_material_decl_path
	pcoll.force_refresh = False
	return pcoll.materials

class ImportMaterials(bpy.types.Operator):
	bl_idname = "scene.import_materials"
	bl_label = "Import Materials"

	def __init__(self):
		self.num_materials_created = 0
		self.num_materials_updated = 0

	def parse_addnormals(self, decl, lex):
		lex.expect_token("(")
		return lex.parse_token()

	def parse_heightmap(self, decl, lex):
		lex.expect_token("(")
		texture = lex.parse_token()
		lex.expect_token(",")
		scale = float(lex.parse_token())
		lex.expect_token(")")
		return (texture, scale)

	def parse_material_file(self, filename):
		lex = lexer.Lexer(filename)
		num_materials_created = 0
		num_materials_updated = 0
		scene = bpy.context.scene
		print("Parsing", os.path.basename(filename), "...", end="", flush=True)
		while True:
			token = lex.parse_token()
			if token == None:
				break
			if token in [ "particle", "skin", "table"]:
				lex.parse_token() # name
				lex.skip_bracket_delimiter_section("{", "}")
			else:
				if token == "material":
					name = lex.parse_token()
				else:
					name = token
				if name in scene.bfg.material_decls:
					decl = scene.bfg.material_decls[name]
					num_materials_updated += 1
				else:
					num_materials_created += 1
					decl = scene.bfg.material_decls.add()
					decl.name = name
				lex.expect_token("{")
				num_required_closing = 1
				in_stage = False
				stage_blend = None
				stage_heightmap_scale = 0
				stage_texture = None
				while True:
					token = lex.parse_token()
					if token == None:
						break
					elif token == "{":
						num_required_closing += 1
						if num_required_closing == 2:
							# 2nd opening brace: now in a stage
							in_stage = True
							stage_blend = None
							stage_heightmap_scale = 0
							stage_texture = None
					elif token == "}":
						num_required_closing -= 1
						if num_required_closing == 0:
							break
						elif num_required_closing == 1:
							# one closing brace left: closing stage
							in_stage = False
							if stage_texture:
								decl.texture = stage_texture # any stage texture map. will be the light texture for light materials.
							if stage_blend and stage_texture:
								if stage_blend.lower() == "bumpmap":
									decl.normal_texture = stage_texture
									decl.heightmap_scale = stage_heightmap_scale
								elif stage_blend.lower() == "diffusemap":
									decl.diffuse_texture = stage_texture
								elif stage_blend.lower() == "specularmap":
									decl.specular_texture = stage_texture
					if in_stage:
						if token.lower() == "blend":
							stage_blend = lex.parse_token()
						elif token.lower() == "map":
							token = lex.parse_token()
							if token.lower() == "addnormals":
								stage_texture = self.parse_addnormals(decl, lex)
							elif token.lower() == "heightmap":
								(stage_texture, stage_heightmap_scale) = self.parse_heightmap(decl, lex)
							else:
								stage_texture = token
					else:
						if token.lower() == "bumpmap":
							token = lex.parse_token()
							if token.lower() == "addnormals":
								decl.normal_texture = self.parse_addnormals(decl, lex)
							elif token.lower() == "heightmap":
								(decl.normal_texture, decl.heightmap_scale) = self.parse_heightmap(decl, lex)
							else:
								decl.normal_texture = token
						elif token.lower() == "diffusemap":
							decl.diffuse_texture = lex.parse_token()
						elif token.lower() == "qer_editorimage":
							decl.editor_texture = lex.parse_token()
						elif token.lower() == "specularmap":
							decl.specular_texture = lex.parse_token()
		print(" %d materials" % (num_materials_created + num_materials_updated))
		return (num_materials_created, num_materials_updated)

	def update_material_decl_paths(self, scene):
		scene.bfg.material_decl_paths.clear()
		for decl in scene.bfg.material_decls:
			name = os.path.dirname(decl.name)
			if name.startswith("textures") and not name in scene.bfg.material_decl_paths:
				path = scene.bfg.material_decl_paths.add()
				path.name = name

	@classmethod
	def poll(cls, context):
		return context.scene.bfg.game_path != ""

	def execute(self, context):
		self.num_materials_created = 0
		self.num_materials_updated = 0
		start_time = time.time()
		fs = FileSystem()
		files = fs.find_files(os.path.join("materials", "*.mtr"))
		wm = context.window_manager
		wm.progress_begin(0, len(files))
		for i, f in enumerate(files):
			result = self.parse_material_file(f)
			wm.progress_update(i)
			self.num_materials_created += result[0]
			self.num_materials_updated += result[1]
		self.update_material_decl_paths(context.scene)
		preview_collections["light"].needs_refresh = True
		wm.progress_end()
		self.report({'INFO'}, "Imported %d materials, updated %d in %.2f seconds" % (self.num_materials_created, self.num_materials_updated, time.time() - start_time))
		return {'FINISHED'}

def create_material_texture(fs, mat, texture, slot_number):
	# textures may be shared between materials, so don't create one that already exists
	if texture in bpy.data.textures:
		tex = bpy.data.textures[texture]
	else:
		tex = bpy.data.textures.new(texture, type='IMAGE')

	# texture image may have changed
	img_filename = fs.find_image_file_path(texture)
	if img_filename:
		# try to use relative paths for image filenames
		try:
			img_filename = bpy.path.relpath(img_filename)
		except ValueError:
			pass
	if not tex.image or tex.image.filepath != img_filename:
		try:
			img = bpy.data.images.load(img_filename)
		except:
			pass
		else:
		   tex.image = img

	# update/create the texture slot
	if not mat.texture_slots[slot_number] or not mat.texture_slots[slot_number].name == texture:
		texSlot = mat.texture_slots.create(slot_number)
		texSlot.texture_coords = 'UV'
		texSlot.texture = tex

	return (tex, mat.texture_slots[slot_number])

def create_material(decl):
	if decl.name in bpy.data.materials:
		mat = bpy.data.materials[decl.name]
	else:
		mat = bpy.data.materials.new(decl.name)
	fs = FileSystem()
	decl_path = os.path.dirname(decl.name)
	mat.preview_render_type = 'CUBE'
	if decl_path in _editor_material_paths:
		# editor materials: use the editor texture if diffuse is missing
		create_material_texture(fs, mat, decl.diffuse_texture if decl.diffuse_texture != "" else decl.editor_texture, 0)
		mat.alpha = 0.5
		mat.transparency_method = 'Z_TRANSPARENCY'
		mat.use_shadeless = True
		mat.use_transparency = True
	else:
		mat.use_shadeless = bpy.context.scene.bfg.shadeless_materials
		if decl.diffuse_texture != "":
			create_material_texture(fs, mat, decl.diffuse_texture, 0)
		elif decl.texture != "": # fallback to generic texture if no diffuse
			create_material_texture(fs, mat, decl.texture, 0)
		elif decl.editor_texture != "": # fallback to editor texture if no diffuse or generic
			create_material_texture(fs, mat, decl.editor_texture, 0)
		if decl.normal_texture != "":
			(tex, slot) = create_material_texture(fs, mat, decl.normal_texture, 1)
			slot.use_map_color_diffuse = False
			if decl.heightmap_scale > 0:
				slot.use_map_displacement = True
				slot.displacement_factor = decl.heightmap_scale
			else:
				tex.use_normal_map = True
				slot.use_map_normal = True
		if decl.specular_texture != "":
			(_, slot) = create_material_texture(fs, mat, decl.specular_texture, 2)
			slot.use_map_color_diffuse = False
			slot.use_map_color_spec = True
			slot.use_map_specular = True
	return mat

def get_or_create_active_material(context):
	bfg = context.scene.bfg
	if bfg.active_material_decl in context.scene.bfg.material_decls:
		return create_material(context.scene.bfg.material_decls[bfg.active_material_decl])
	return None

def assign_material(obj, mat, where='ALL'):
	if obj.bfg.type == '2D_ROOM':
		if where == 'CEILING' or where == 'ALL':
			obj.bfg.ceiling_material = mat.name
		if where == 'WALL' or where == 'ALL':
			obj.bfg.wall_material = mat.name
		if where == 'FLOOR' or where == 'ALL':
			obj.bfg.floor_material = mat.name
		update_room_plane_materials(obj)
	else:
		if len(obj.data.materials) == 1:
			# one slot: easy, just reassign
			obj.data.materials[0] = mat
		else:
			obj.data.materials.clear()
			obj.data.materials.append(mat)

			# there was more than one material slot on this object
			# need to set material_index on all faces to 0
			bm = bmesh.new()
			bm.from_mesh(obj.data)
			for f in bm.faces:
				f.material_index = 0
			bm.to_mesh(obj.data)
			bm.free()

class AssignMaterial(bpy.types.Operator):
	"""Assign the material to the selected objects or object faces"""
	bl_idname = "scene.assign_material"
	bl_label = "Assign"
	where = bpy.props.StringProperty(name="where", default='ALL')

	def execute(self, context):
		obj = context.active_object
		if not obj:
			return {'CANCELLED'}
		mat = get_or_create_active_material(context)
		if not mat:
			return {'CANCELLED'}
		if obj.mode == 'EDIT' and hasattr(obj.data, "materials"):
			# edit mode: assign to selected mesh faces
			bm = bmesh.from_edit_mesh(obj.data)
			selected_faces = [f for f in bm.faces if f.select]
			if len(selected_faces) > 0:
				# create/find a slot
				material_index = -1
				for i, m in enumerate(obj.data.materials):
					if m == mat:
						material_index = i
						break
				if material_index == -1:
					obj.data.materials.append(mat)
					material_index = len(obj.data.materials) - 1

				# assign to faces
				for f in selected_faces:
					f.material_index = material_index

				# remove any material slots that are now unused
				# pop function update_data arg doesn't work, need to remap face material_index ourselves after removal
				old_material_names = []
				for m in obj.data.materials:
					old_material_names.append(m.name)
				remove_materials = []
				for i, m in enumerate(obj.data.materials):
					used = False
					for f in bm.faces:
						if f.material_index == i:
							used = True
							break
					if not used:
						remove_materials.append(m)
				if len(remove_materials) > 0:
					for m in remove_materials:
						obj.data.materials.pop(obj.data.materials.find(m.name), True)
				for f in bm.faces:
					f.material_index = obj.data.materials.find(old_material_names[f.material_index])

				bmesh.update_edit_mesh(obj.data)
			#bm.free() # bmesh.from_edit_mesh returns garbage after this is called
		else:
			for s in context.selected_objects:
				if hasattr(s.data, "materials"):
					assign_material(s, mat, self.where)
		return {'FINISHED'}

def refresh_selected_objects_materials(context):
	refreshed = [] # don't refresh the same material twice
	for obj in context.selected_objects:
		if hasattr(obj.data, "materials"):
			for mat in obj.data.materials:
				if mat not in refreshed and mat.name in context.scene.bfg.material_decls:
					decl = context.scene.bfg.material_decls[mat.name]
					create_material(decl)
					refreshed.append(mat)

class RefreshMaterials(bpy.types.Operator):
	"""Refresh the select objects' materials, recreating them from their corresponding material decls"""
	bl_idname = "scene.refresh_materials"
	bl_label = "Refresh Materials"

	@classmethod
	def poll(cls, context):
		return len(context.scene.bfg.material_decls) > 0

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

################################################################################
## MODELS
################################################################################

# creates a new object with the specified model either loaded into a new mesh, or linked to an existing mesh
# the object will be made active and selected
# return (object, error_message)
def create_model_object(context, filename, relative_path):
	# check that the required import addon is enabled
	extension = os.path.splitext(filename)[1]
	if extension.lower() == ".lwo":
		if not hasattr(bpy.types, "IMPORT_SCENE_OT_lwo"):
			return (None, "LightWave Object (.lwo) import addon not enabled")
	elif extension.lower() not in [".dae", ".md5mesh"]:
		return (None, "Model \"%s\" uses unsupported extension \"%s\"" % (filename, extension))

	set_object_mode_and_clear_selection()

	# if the model has already been loaded before, don't import - link to the existing mesh
	mesh = None
	for obj in context.scene.objects:
		if obj.bfg.entity_model == relative_path:
			mesh = obj.data
			break
	model_obj_name = os.path.splitext(os.path.basename(relative_path))[0]
	if mesh:
		obj = bpy.data.objects.new(model_obj_name, mesh)
		context.scene.objects.link(obj)
		context.scene.objects.active = obj
	else:
		# prep for diffing scene objects before and after import for consistency between importers
		# e.g. lwo importer doesn't select or make active the object(s) in creates
		obj_names = []
		for obj in context.scene.objects:
			obj_names.append(obj.name)
		# import
		if extension.lower() == ".dae":
			bpy.ops.wm.collada_import(filepath=filename)
		elif extension.lower() == ".lwo":
			bpy.ops.import_scene.lwo(filepath=filename, USE_EXISTING_MATERIALS=True)
		elif extension.lower() == ".md5mesh":
			import_md5mesh.read_md5mesh(filename)
		# diff scene objects
		# 0: error, 1: fine, >1: join objects
		imported_objects = []
		for obj in context.scene.objects:
			if not obj.name in obj_names:
				imported_objects.append(obj)
		if len(imported_objects) == 0:
			return (None, "Importing \"%s\" failed" % filename) # import must have failed
		elif len(imported_objects) == 1:
			# make sure the object is selected and active
			imported_objects[0].select = True
			context.scene.objects.active = imported_objects[0]
		else:
			# join objects
			obj = context.active_object
			bpy.ops.object.select_all(action='DESELECT')
			for obj in imported_objects:
				obj.select = True
			context.scene.objects.active = imported_objects[0]
			bpy.ops.object.join()
			# fix names
			context.scene.objects.active.name = model_obj_name
			context.scene.objects.active.data.name = model_obj_name
		obj = context.scene.objects.active
		# fixup material names
		for i, mat in enumerate(obj.data.materials):
			(name, ext) = os.path.splitext(mat.name)
			fix_material = False
			if extension.lower() == ".dae":
				# fix collada mangling
				name = name.replace("_", "/")
				if name.endswith("-material"):
					name = name[:-len("-material")]
				fix_material = True
			if ext != "":
				# remove filename extensions
				# e.g. "models/items/rocket_ammo/rocket_large.tga" should be "models/items/rocket_ammo/rocket_large"
				fix_material = True
			if fix_material:
				# if a material with the fixed name already exists, use it
				# otherwise rename this one
				new_mat = bpy.data.materials.get(name)
				if new_mat:
					obj.data.materials[i] = new_mat
				else:
					mat.name = name
	obj.select = True
	obj.bfg.entity_model = relative_path
	obj.scale = [_scale_to_blender, _scale_to_blender, _scale_to_blender]
	obj.lock_scale = [True, True, True]
	refresh_selected_objects_materials(context)
	bpy.ops.object.shade_smooth()
	return (obj, None)

################################################################################
## ENTITIES
################################################################################

class EntityDictPropGroup(bpy.types.PropertyGroup):
	# name property inherited
	value = bpy.props.StringProperty()

class EntityPropGroup(bpy.types.PropertyGroup):
	# name property inherited
	dict = bpy.props.CollectionProperty(type=EntityDictPropGroup)

	def get_dict_value(self, key, key_default=None):
		kvp = self.dict.get(key)
		if kvp:
			return kvp.value
		return key_default

class ModelDefPropGroup(bpy.types.PropertyGroup):
	# name property inherited
	inherit = bpy.props.StringProperty()
	mesh = bpy.props.StringProperty() # e.g. models/md5/monsters/zfat/zfat.md5mesh

class ImportEntities(bpy.types.Operator):
	bl_idname = "scene.import_entities"
	bl_label = "Import Entities"

	def parse_def_file(self, scene, filename):
		lex = lexer.Lexer(filename)
		num_entities_created = 0
		num_entities_updated = 0
		print("Parsing", os.path.basename(filename), "...", end="", flush=True)
		while True:
			token = lex.parse_token()
			if token == None:
				break
			if token == "entityDef":
				name = lex.parse_token()
				if name in scene.bfg.entities:
					entity = scene.bfg.entities[name]
					num_entities_updated += 1
				else:
					entity = scene.bfg.entities.add()
					entity.name = name
					num_entities_created += 1
				lex.expect_token("{")
				num_required_closing = 1
				while True:
					token = lex.parse_token()
					if token == None:
						break
					elif token == "{":
						num_required_closing += 1
					elif token == "}":
						num_required_closing -= 1
						if num_required_closing == 0:
							break
					elif token.startswith("editor_") or token in ["inherit", "model"]: # only store what we care about
						# parse as key-value pair
						key = token
						if key in entity.dict:
							kvp = entity.dict[key]
						else:
							kvp = entity.dict.add()
							kvp.name = key
						kvp.value = lex.parse_token()
			elif token == "model":
				name = lex.parse_token()
				model_def = scene.bfg.model_defs.get(name)
				if not model_def:
					model_def = scene.bfg.model_defs.add()
					model_def.name = name
				lex.expect_token("{")
				num_required_closing = 1
				while True:
					token = lex.parse_token()
					if token == None:
						break
					elif token == "{":
						num_required_closing += 1
					elif token == "}":
						num_required_closing -= 1
						if num_required_closing == 0:
							break
					elif token == "inherit":
						model_def.inherit = lex.parse_token()
					elif token == "mesh":
						model_def.mesh = lex.parse_token()
			else:
				name = lex.parse_token() # name, sometimes opening brace
				lex.skip_bracket_delimiter_section("{", "}", True if name == "{" else False)
		print(" %d entities" % (num_entities_created + num_entities_updated))
		return (num_entities_created, num_entities_updated)

	@classmethod
	def poll(cls, context):
		return context.scene.bfg.game_path != ""

	def execute(self, context):
		self.num_entities_created = 0
		self.num_entities_updated = 0
		start_time = time.time()
		fs = FileSystem()
		files = fs.find_files(os.path.join("def", "*.def"))
		wm = context.window_manager
		wm.progress_begin(0, len(files))
		for i, f in enumerate(files):
			result = self.parse_def_file(context.scene, f)
			wm.progress_update(i)
			self.num_entities_created += result[0]
			self.num_entities_updated += result[1]
		update_scene_entity_properties(context) # update entity objects with any new properties
		wm.progress_end()
		self.report({'INFO'}, "Imported %d entities, updated %d in %.2f seconds" % (self.num_entities_created, self.num_entities_updated, time.time() - start_time))
		return {'FINISHED'}

# chase the model def mesh
# e.g. entityDef monster_zombie_fat { "model" "monster_zombie_fat" }
# model monster_zombie_fat { "mesh" "models/md5/monsters/zfat/zfat.md5mesh" }
# and handle inherit/recursion
def find_model_def_mesh(model):
	model_def = bpy.context.scene.bfg.model_defs.get(model)
	if model_def:
		if model_def.mesh == "":
			if model_def.inherit != "":
				return find_model_def_mesh(model_def.inherit)
		else:
			return model_def.mesh
	return model

def create_object_color_material():
	name = "_object_color"
	# create the material if it doesn't exist
	if name in bpy.data.materials:
		mat = bpy.data.materials[name]
	else:
		mat = bpy.data.materials.new(name)
	mat.use_fake_user = True
	mat.use_object_color = True
	mat.use_shadeless = True

def create_object_entity_properties(context, entity, is_inherited=False):
	"""Create entity properties on the active object"""
	for kvp in entity.dict:
		if kvp.name.startswith("editor_var"):
			prop_name = kvp.name.split()[1]
			# prepend "inherited_" to inherited property names
			prop_name = "inherited_" + prop_name
			if not context.active_object.game.properties.get(prop_name):
				# don't create the prop if it already exists
				bpy.ops.object.game_property_new(type='STRING', name=prop_name)
	inherit = entity.dict.get("inherit")
	if inherit:
		parent_entity = context.scene.bfg.entities[inherit.value]
		create_object_entity_properties(context, parent_entity, True)

def update_scene_entity_properties(context):
	"""Add missing properties to existing entity objects"""
	for obj in context.scene.objects:
		if obj.bfg.type in ['BRUSH_ENTITY', 'ENTITY']:
			context.scene.objects.active = obj
			entity = context.scene.bfg.entities[obj.bfg.classname]
			create_object_entity_properties(context, entity)
			break

class AddEntity(bpy.types.Operator):
	"""Add a new entity to the scene of the selected type"""
	bl_idname = "scene.add_entity"
	bl_label = "Add Entity"

	@classmethod
	def poll(cls, context):
		ae = context.scene.bfg.active_entity
		return ae and ae != ""

	def execute(self, context):
		ae = context.scene.bfg.active_entity
		if ae and ae != "":
			active_object = context.active_object
			selected_objects = context.selected_objects
			set_object_mode_and_clear_selection()
			entity = context.scene.bfg.entities[ae]
			entity_mins = entity.get_dict_value("editor_mins", "?")
			entity_maxs = entity.get_dict_value("editor_maxs", "?")
			model = entity.get_dict_value("model")
			if (entity_mins == "?" or entity_maxs == "?") and not model:
				# brush entity, create as empty
				if not (active_object and active_object.bfg.type in ['NONE','BRUSH'] and len(selected_objects) > 0):
					self.report({'ERROR'}, "Brush entities require a brush to be selected")
					return {'CANCELLED'}
				bpy.ops.object.empty_add(type='SPHERE')
				obj = context.active_object
				obj.empty_draw_size = 0.5
				obj.hide_render = True
				obj.location = active_object.location
				obj.lock_rotation = [True, True, True]
				obj.lock_scale = [True, True, True]
				obj.bfg.type = 'BRUSH_ENTITY'
			else:
				# normal entity
				obj = None
				if model: # create as mesh
					model = find_model_def_mesh(model) # handle "model" pointing to a model def, inheritance etc.
					if model:
						fs = FileSystem()
						filename = fs.find_file_path(model)
						if filename:
							(obj, error_message) = create_model_object(context, filename, model)
						else:
							error_message = "Model %s not found" % model
						if error_message:
							self.report({'ERROR'}, error_message)
				if not obj: # no model or create_model_object error: fallback to primitive
					if entity_mins == "?" or entity_maxs == "?":
						# need bounds
						self.report({'ERROR'}, "Entity def %s is missing editor_mins and editor_maxs" % entity.name)
						return {'CANCELLED'}
					bpy.ops.mesh.primitive_cube_add()
					obj = context.active_object
					entity_color = entity.get_dict_value("editor_color", "0 0 1") # default to blue
					obj.color = [float(i) for i in entity_color.split()] + [float(0.5)] # "r g b"
					obj.data.name = ae
					create_object_color_material()
					obj.data.materials.append(bpy.data.materials["_object_color"])
					obj.hide_render = True
					obj.show_wire = True
					obj.show_transparent = True

					# set dimensions
					mins = Vector([float(i) * _scale_to_blender for i in entity_mins.split()])
					maxs = Vector([float(i) * _scale_to_blender for i in entity_maxs.split()])
					size = maxs + -mins
					obj.dimensions = size

					# set origin
					origin = (mins + maxs) / 2.0
					bpy.ops.object.editmode_toggle()
					bpy.ops.mesh.select_all(action='SELECT')
					bpy.ops.transform.translate(value=origin)
					bpy.ops.object.editmode_toggle()
				obj.lock_rotation = [True, True, False]
				obj.lock_scale = [True, True, True]
				obj.show_axis = True # x will be forward
				obj.show_name = context.scene.bfg.show_entity_names
				obj.bfg.type = 'ENTITY'
			obj.bfg.classname = ae
			obj.name = ae
			link_active_object_to_group("entities")
			create_object_entity_properties(context, entity)

			# parent selected objects to this brush entity, and link them to the "entities" group
			# if there is a editor_material for this entity, assign that material to the selected objects
			if obj.bfg.type == 'BRUSH_ENTITY':
				group = bpy.data.groups["entities"]
				mat = None
				mat_name = entity.get_dict_value("editor_material")
				if mat_name:
					mat_decl = context.scene.bfg.material_decls.get(mat_name)
					if mat_decl:
						mat = create_material(mat_decl)
				for s in selected_objects:
					s.location -= obj.location
					s.parent = obj
					group.objects.link(s)
					if mat:
						assign_material(s, mat)
		return {'FINISHED'}

class ShowEntityDescription(bpy.types.Operator):
	"""Show entity description"""
	bl_idname = "object.show_entity_description"
	bl_label = "Show Entity Description"
	bl_options = {'REGISTER','UNDO','INTERNAL'}

	def draw(self, context):
		bfg = context.scene.bfg
		ent = bfg.entities[bfg.active_entity]
		ent_usage = ent.get_dict_value("editor_usage")
		col = self.layout.column()
		#col.label(ent_usage)
		# no support for text wrapping and multiline labels...
		n = 50
		for i in range(0, len(ent_usage), n):
			col.label(ent_usage[i:i+n])

	@classmethod
	def poll(cls, context):
		ae = context.scene.bfg.active_entity
		if ae and ae != "ae":
			ent = context.scene.bfg.entities[ae]
			return ent.dict.get("editor_usage") != None
		return False

	def invoke(self, context, event):
		return context.window_manager.invoke_popup(self)

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

class ShowEntityPropertyDescription(bpy.types.Operator):
	"""Show entity property description"""
	bl_idname = "object.show_entity_property_description"
	bl_label = "Show Entity Property Description"
	bl_options = {'REGISTER','UNDO','INTERNAL'}
	classname = bpy.props.StringProperty(default="")
	name = bpy.props.StringProperty(default="")

	def find_prop_info(self, context, entity):
		info = entity.get_dict_value("editor_var " + self.name)
		if info:
			return info
		inherit = entity.dict.get("inherit")
		if inherit:
			parent_entity = context.scene.bfg.entities[inherit.value]
			return self.find_prop_info(context, parent_entity)
		return None

	def draw(self, context):
		col = self.layout.column()
		if self.classname != "" and self.name != "":
			entity = context.scene.bfg.entities[self.classname]
			info = self.find_prop_info(context, entity)
			if not info:
				info = "No info"
			#col.label(info)
			# no support for text wrapping and multiline labels...
			n = 50
			for i in range(0, len(info), n):
				col.label(info[i:i+n])

	def invoke(self, context, event):
		return context.window_manager.invoke_popup(self)

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

class NewCustomEntityProperty(bpy.types.Operator):
	"""Create a new custom entity property"""
	bl_idname = "scene.new_custom_entity_property"
	bl_label = "New Entity Property"
	bl_options = {'REGISTER','UNDO','INTERNAL'}
	name = bpy.props.StringProperty(name="Name", default="")
	value = bpy.props.StringProperty(name="Value", default="")

	def invoke(self, context, event):
		return context.window_manager.invoke_props_dialog(self)

	def execute(self, context):
		if self.name == "":
			return {'CANCELLED'}
		obj = context.active_object
		# handle brush entities. the parent owns the properties
		old_active = None
		if obj.parent and obj.parent.bfg.type == 'BRUSH_ENTITY':
			old_active = context.scene.objects.active
			obj = obj.parent
			context.scene.objects.active = obj
		# check if a normal property already exists with this name
		prop = obj.game.properties.get(self.name)
		if not prop:
			# check if an inherited property already exists with this name
			prop = obj.game.properties.get("inherited_" + self.name)
			if prop:
				# show inherited properties
				context.scene.bfg.show_inherited_entity_props = True
			else:
				# check if a custom property already exists with this name
				prop_name = "custom_" + self.name
				prop = obj.game.properties.get(prop_name)
				if not prop:
					# finally, create the property
					bpy.ops.object.game_property_new(type='STRING', name=prop_name)
					prop = obj.game.properties[prop_name]
		# whether the property has been created, or already exists, set the value
		prop.value = self.value
		# restore active object
		if old_active:
			context.scene.objects.active = old_active
		else:
			context.scene.objects.active = obj # force ui refresh. game_property_new doesn't seem to trigger it.
		return {'FINISHED'}

class RemoveCustomEntityProperty(bpy.types.Operator):
	"""Remove a custom entity property"""
	bl_idname = "scene.remove_custom_entity_property"
	bl_label = "Remove Entity Property"
	bl_options = {'REGISTER','UNDO','INTERNAL'}
	name = bpy.props.StringProperty(default="")

	def execute(self, context):
		obj = context.active_object
		# handle brush entities. the parent owns the properties
		old_active = None
		if obj.parent and obj.parent.bfg.type == 'BRUSH_ENTITY':
			old_active = context.scene.objects.active
			obj = obj.parent
			context.scene.objects.active = obj
		prop_index = obj.game.properties.find(self.name)
		if prop_index != -1:
			bpy.ops.object.game_property_remove(index=prop_index)
		# restore active object
		if old_active:
			context.scene.objects.active = old_active
		return {'FINISHED'}

################################################################################
## LIGHTS
################################################################################

class AddLight(bpy.types.Operator):
	bl_idname = "scene.add_light"
	bl_label = "Add Light"

	def execute(self, context):
		set_object_mode_and_clear_selection()
		data = bpy.data.lamps.new(name="Light", type='POINT')
		obj = bpy.data.objects.new(name="Light", object_data=data)
		context.scene.objects.link(obj)
		obj.select = True
		context.scene.objects.active = obj
		obj.data.distance = 300.0 * _scale_to_blender
		obj.data.energy = obj.data.distance
		#obj.scale = obj.distance
		#obj.show_bounds = True
		#obj.draw_bounds_type = 'SPHERE'
		obj.data.use_sphere = True
		link_active_object_to_group("lights")
		return {'FINISHED'}

def get_light_radius(self):
	return self.data.distance

def set_light_radius(self, value):
	self.data.distance = value
	self.data.energy = value

def light_material_preview_items(self, context):
	lights = []
	pcoll = preview_collections["light"]
	if not pcoll.needs_refresh:
		return pcoll.lights
	fs = FileSystem()
	lights.append(("default", "default", "default", 0, 0))
	i = 1
	for decl in context.scene.bfg.material_decls:
		# material name must start with "lights" and have a texture
		if os.path.dirname(decl.name).startswith("lights") and decl.texture != "":
			preview = None
			if decl.texture in pcoll: # workaround blender bug, pcoll.load is supposed to return cached preview if name already exists
				preview = pcoll[decl.texture]
			else:
				filename = fs.find_image_file_path(decl.texture)
				if filename:
					preview = pcoll.load(decl.texture, filename, 'IMAGE')
				elif context.scene.bfg.hide_bad_materials:
					continue # hide if the texture file is missing
			lights.append((decl.name, os.path.basename(decl.name), decl.name, preview.icon_id if preview else 0, i))
			i += 1
	lights.sort()
	pcoll.lights = lights
	pcoll.needs_refresh = False
	return pcoll.lights

################################################################################
## STATIC MODELS
################################################################################

class AddStaticModel(bpy.types.Operator):
	"""Browse for a static model to add"""
	bl_idname = "scene.add_static_model"
	bl_label = "Add Static Model"
	filepath = bpy.props.StringProperty(default="", options={'HIDDEN', 'SKIP_SAVE'})
	filter_glob = bpy.props.StringProperty(default="*.dae;*.lwo;*.md5mesh", options={'HIDDEN'})

	@classmethod
	def poll(cls, context):
		return context.scene.bfg.game_path != ""

	def execute(self, context):
		# the func_static entity model value looks like this
		# "models/mapobjects/arcade_machine/arcade_machine.lwo"
		# so the file path must descend from one of the search paths
		fs = FileSystem()
		relative_path = fs.calculate_relative_path(self.properties.filepath)
		if not relative_path:
			self.report({'ERROR'}, "File \"%s\" not found. Path must descend from \"%s\"" % (self.properties.filepath, context.scene.bfg.game_path))
			return {'CANCELLED'}
		(obj, error_message) = create_model_object(context, self.properties.filepath, relative_path)
		if error_message:
			self.report({'ERROR'}, error_message)
			return {'CANCELLED'}
		else:
			obj.bfg.type = 'STATIC_MODEL'
			obj.bfg.classname = "func_static"
			link_active_object_to_group("static models")
		return {'FINISHED'}

	def invoke(self, context, event):
		context.window_manager.fileselect_add(self)
		return {'RUNNING_MODAL'}

################################################################################
## MAP
################################################################################

def update_room_plane_modifier(obj):
	if obj.modifiers:
		mod = obj.modifiers[0]
		if mod.type == 'SOLIDIFY':
			mod.thickness = obj.bfg.room_height
			mod.material_offset = 1
			mod.material_offset_rim = 2

def update_room_plane_materials(obj):
	if bpy.data.materials.find(obj.bfg.floor_material) != -1:
		obj.material_slots[0].material = bpy.data.materials[obj.bfg.floor_material]
	if bpy.data.materials.find(obj.bfg.ceiling_material) != -1:
		obj.material_slots[1].material = bpy.data.materials[obj.bfg.ceiling_material]
	if bpy.data.materials.find(obj.bfg.wall_material) != -1:
		obj.material_slots[2].material = bpy.data.materials[obj.bfg.wall_material]

def update_room(self, context):
	obj = context.active_object
	if obj.bfg.type == '2D_ROOM':
		update_room_plane_modifier(obj)
		update_room_plane_materials(obj)

def flip_mesh_normals(mesh):
	bm = bmesh.new()
	bm.from_mesh(mesh)
	for f in bm.faces:
		f.normal_flip()
	bm.to_mesh(mesh)
	bm.free()

def apply_boolean(dest, src, bool_op, flip_normals=False):
	# auto unwrap this 3D room or brush if that's what the user wants
	if src.bfg.type in ['3D_ROOM', 'BRUSH'] and src.bfg.auto_unwrap:
		auto_unwrap(src.data, src.location, src.scale)

	# generate mesh for the source object
	# transform to worldspace
	bpy.ops.object.select_all(action='DESELECT')
	dest.select = True
	me = src.to_mesh(bpy.context.scene, True, 'PREVIEW')
	me.transform(src.matrix_world)

	# 2D rooms are always unwrapped (the to_mesh result, not the object - it's just a plane)
	if src.bfg.type == '2D_ROOM':
		auto_unwrap(me)

	if flip_normals:
		flip_mesh_normals(me)

	# bool object - need a temp object to hold the result of to_mesh
	ob_bool = bpy.data.objects.new("_bool", me)

	# copy materials
	for mat in src.data.materials:
		if not mat.name in dest.data.materials:
			dest.data.materials.append(mat)

	# apply the boolean modifier
	mod = dest.modifiers.new(name=src.name, type='BOOLEAN')
	mod.object = ob_bool
	mod.operation = bool_op
	mod.solver = 'CARVE'
	bpy.ops.object.modifier_apply(apply_as='DATA', modifier=src.name)

def flip_object_normals(obj):
	bpy.ops.object.select_all(action='DESELECT')
	obj.select = True
	bpy.ops.object.editmode_toggle()
	bpy.ops.mesh.select_all(action='SELECT')
	bpy.ops.mesh.flip_normals()
	bpy.ops.object.editmode_toggle()

def move_object_to_layer(obj, layer_number):
	layers = 20 * [False]
	layers[layer_number] = True
	obj.layers = layers

def add_all_materials(obj):
	i = 0
	for m in bpy.data.materials:
		if len(obj.data.materials) > i:
			has_material = False
			for mat in obj.data.materials:
				if mat.name == m.name:
					has_material = True
			if not has_material:
				obj.data.materials[i] = m
		else:
			obj.data.materials.append(m)
		i += 1

def build_map(context, rooms, brushes, map_name):
	scene = context.scene

	# get all the temp bool objects from the last time this map was built
	bool_objects = [obj for obj in bpy.data.objects if obj.name.startswith(map_name + "_bool")]

	# create map object
	# if a map object already exists, its old mesh is removed
	set_object_mode_and_clear_selection()
	old_map_mesh = None
	map_mesh_name = map_name + "_mesh"
	if map_mesh_name in bpy.data.meshes:
		old_map_mesh = bpy.data.meshes[map_mesh_name]
		old_map_mesh.name = "_worldspawn_old"
	if len(rooms) > 0:
		# first room: generate the mesh and transform to worldspace
		if rooms[0].bfg.type == '3D_ROOM' and rooms[0].bfg.auto_unwrap:
			auto_unwrap(rooms[0].data)
		map_mesh = rooms[0].to_mesh(scene, True, 'PREVIEW')
		map_mesh.name = map_mesh_name
		map_mesh.transform(rooms[0].matrix_world)

		# 2D rooms are always unwrapped (the to_mesh result, not the object - it's just a plane)
		if rooms[0].bfg.type == '2D_ROOM':
			auto_unwrap(map_mesh)
	else:
		map_mesh = bpy.data.meshes.new(map_mesh_name)
	if map_name in bpy.data.objects:
		map = bpy.data.objects[map_name]
		map.data = map_mesh
	else:
		map = bpy.data.objects.new(map_name, map_mesh)
		scene.objects.link(map)
	if old_map_mesh:
		bpy.data.meshes.remove(old_map_mesh)
	map.layers[scene.active_layer] = True
	scene.objects.active = map
	map.select = True
	map.hide = False

	# combine rooms
	if len(rooms) > 0:
		flip_object_normals(map)
	for i, room in enumerate(rooms):
		if i > 0:
			# not the first room: bool union with existing mesh
			apply_boolean(map, room, 'UNION', flip_normals=True)
	map.select = True
	if len(rooms) > 0:
		flip_object_normals(map)

	# combine brushes
	for brush in brushes:
		apply_boolean(map, brush, 'UNION')

	link_active_object_to_group("map")
	move_object_to_layer(map, scene.bfg.map_layer)
	map.hide_select = True
	bpy.ops.object.select_all(action='DESELECT')

	# cleanup temp bool objects
	for obj in bool_objects:
		mesh = obj.data
		bpy.data.objects.remove(obj)
		bpy.data.meshes.remove(mesh)

class AddRoom(bpy.types.Operator):
	bl_idname = "scene.add_room"
	bl_label = "Add Room"

	def execute(self, context):
		scene = context.scene
		set_object_mode_and_clear_selection()
		bpy.ops.mesh.primitive_plane_add(radius=1)
		bpy.ops.object.modifier_add(type='SOLIDIFY')
		obj = context.active_object
		obj.lock_scale = [False, False, True]
		obj.modifiers[0].offset = 1
		obj.modifiers[0].use_even_offset = True
		obj.modifiers[0].use_flip_normals = True
		obj.modifiers[0].use_quality_normals = True
		obj.name = "room2D"
		obj.data.name = "room2D"
		obj.bfg.room_height = 4
		obj.bfg.type = '2D_ROOM'
		if context.scene.bfg.wireframe_rooms:
			obj.draw_type = 'WIRE'
		obj.game.physics_type = 'NO_COLLISION'
		obj.hide_render = True
		if len(bpy.data.materials) > 0:
			mat = get_or_create_active_material(context)
			if mat:
				obj.data.materials.append(mat)
				obj.data.materials.append(mat)
				obj.data.materials.append(mat)
				obj.bfg.ceiling_material = mat.name
				obj.bfg.wall_material = mat.name
				obj.bfg.floor_material = mat.name
			else:
				obj.data.materials.append(bpy.data.materials[0])
				obj.data.materials.append(bpy.data.materials[0])
				obj.data.materials.append(bpy.data.materials[0])
				obj.bfg.ceiling_material = bpy.data.materials[0].name
				obj.bfg.wall_material = bpy.data.materials[0].name
				obj.bfg.floor_material = bpy.data.materials[0].name
		else:
			bpy.ops.object.material_slot_add()
			bpy.ops.object.material_slot_add()
			bpy.ops.object.material_slot_add()
			obj.bfg.ceiling_material = ""
			obj.bfg.wall_material = ""
			obj.bfg.floor_material = ""
		scene.objects.active = obj
		update_room_plane_modifier(obj)
		update_room_plane_materials(obj)
		link_active_object_to_group("rooms")
		return {'FINISHED'}

class AddBrush(bpy.types.Operator):
	bl_idname = "scene.add_brush"
	bl_label = "Add Brush"
	s_type = bpy.props.StringProperty(name="s_type", default='BRUSH')

	def execute(self, context):
		scene = context.scene
		set_object_mode_and_clear_selection()
		bpy.ops.mesh.primitive_cube_add(radius=1)
		obj = context.active_object
		if context.scene.bfg.wireframe_rooms:
			obj.draw_type = 'WIRE'
		if self.s_type == '3D_ROOM':
			obj.name = "room3D"
			obj.data.name = "room3D"
		else:
			obj.name = "brush"
			obj.data.name = "brush"
		obj.bfg.type = self.s_type
		mat = get_or_create_active_material(context)
		if mat:
			obj.data.materials.append(mat)
		scene.objects.active = obj
		bpy.ops.object.editmode_toggle()
		bpy.ops.mesh.select_all(action='SELECT')
		bpy.ops.object.auto_uv_unwrap()
		bpy.ops.object.editmode_toggle()
		obj.game.physics_type = 'NO_COLLISION'
		obj.hide_render = True
		if self.s_type == '3D_ROOM':
			flip_object_normals(obj)
			link_active_object_to_group("rooms")
		else:
			link_active_object_to_group("brushes")
		return {'FINISHED'}

class CopyRoom(bpy.types.Operator):
	bl_idname = "scene.copy_room"
	bl_label = "Copy Room"
	copy_op = bpy.props.StringProperty(name="copy_op", default='ALL')

	def execute(self, context):
		obj = context.active_object
		selected_objects = context.selected_objects
		for s in selected_objects:
			if s.bfg.type == '2D_ROOM':
				if self.copy_op == 'HEIGHT' or self.copy_op == 'ALL':
					s.bfg.room_height = obj.bfg.room_height
				if self.copy_op == 'MATERIAL_CEILING' or self.copy_op == 'MATERIAL_ALL' or self.copy_op == 'ALL':
					s.bfg.ceiling_material = obj.bfg.ceiling_material
				if self.copy_op == 'MATERIAL_WALL' or self.copy_op == 'MATERIAL_ALL' or self.copy_op == 'ALL':
					s.bfg.wall_material = obj.bfg.wall_material
				if self.copy_op == 'MATERIAL_FLOOR' or self.copy_op == 'MATERIAL_ALL' or self.copy_op == 'ALL':
					s.bfg.floor_material = obj.bfg.floor_material
				update_room_plane_modifier(s)
				update_room_plane_materials(s)
		return {'FINISHED'}

class ConvertRoom(bpy.types.Operator):
	"""Convert the selected 2D room(s) to 3D room(s)"""
	bl_idname = "scene.convert_room"
	bl_label = "Convert Room"

	def execute(self, context):
		selected_objects = list(context.selected_objects) # copy the list, selected objects will change
		for obj in selected_objects:
			if obj.bfg.type == '2D_ROOM':
				obj.bfg.type = '3D_ROOM'

				# create a new mesh, applying the solidify modifer
				# swap the old mesh with the new one, preserving the name
				# then delete the old mesh
				old_mesh = obj.data
				new_mesh_name = old_mesh.name
				old_mesh.name = "_temp" + old_mesh.name
				new_mesh = obj.to_mesh(context.scene, True, 'PREVIEW')
				new_mesh.name = new_mesh_name
				obj.data = new_mesh
				bpy.data.meshes.remove(old_mesh)

				# remove the solidify modifier
				context.scene.objects.active = obj
				bpy.ops.object.modifier_remove(modifier=obj.modifiers[0].name)

				# 2D room UVs are never valid, so unwrap
				bpy.ops.object.auto_uv_unwrap()
		return {'FINISHED'}

class BuildMap(bpy.types.Operator):
	bl_idname = "scene.build_map"
	bl_label = "Build Map"
	bool_op = bpy.props.StringProperty(name="bool_op", default='INTERSECT')

	def execute(self, context):
		# worldspawn
		rooms = []
		brushes = []
		for obj in context.scene.objects:
			if obj.parent and obj.parent.bfg.type == 'BRUSH_ENTITY':
				continue # ignore children of brush entities
			if obj.bfg.type in ['2D_ROOM', '3D_ROOM']:
				rooms.append(obj)
			elif obj.bfg.type == 'BRUSH':
				brushes.append(obj)

		build_map(context, rooms, brushes, "_worldspawn")

		# brush entities
		for obj in context.scene.objects:
			if obj.bfg.type == 'BRUSH_ENTITY':
				brushes = []
				for child in obj.children:
					if child.bfg.type == 'BRUSH':
						brushes.append(child)
				if len(brushes) > 0:
					build_map(context, [], brushes, "_" + obj.name)
		return {'FINISHED'}

################################################################################
## UV UNWRAPPING
################################################################################

def auto_unwrap(mesh, obj_location=Vector(), obj_scale=Vector((1, 1, 1))):
	if bpy.context.mode == 'EDIT_MESH':
		bm = bmesh.from_edit_mesh(mesh)
	else:
		bm = bmesh.new()
		bm.from_mesh(mesh)
	uv_layer = bm.loops.layers.uv.verify()
	bm.faces.layers.tex.verify()  # currently blender needs both layers.
	for f in bm.faces:
		if bpy.context.mode == 'EDIT_MESH' and not f.select:
			continue # ignore faces that aren't selected in edit mode
		texture_size = (128, 128)
		mat = mesh.materials[f.material_index]
		if len(mat.texture_slots) > 0:
			tex = bpy.data.textures[mat.texture_slots[0].name]
			if hasattr(tex, "image") and tex.image: # if the texture type isn't set to "Image or Movie", the image attribute won't exist
				texture_size = tex.image.size
		nX = f.normal.x
		nY = f.normal.y
		nZ = f.normal.z
		if nX < 0:
			nX = nX * -1
		if nY < 0:
			nY = nY * -1
		if nZ < 0:
			nZ = nZ * -1
		face_normal_largest = nX
		face_direction = 'x'
		if face_normal_largest < nY:
			face_normal_largest = nY
			face_direction = 'y'
		if face_normal_largest < nZ:
			face_normal_largest = nZ
			face_direction = 'z'
		if face_direction == 'x':
			if f.normal.x < 0:
				face_direction = '-x'
		if face_direction == 'y':
			if f.normal.y < 0:
				face_direction = '-y'
		if face_direction == 'z':
			if f.normal.z < 0:
				face_direction = '-z'
		scale_x = _scale_to_game / texture_size[0] * (1.0 / bpy.context.scene.bfg.global_uv_scale)
		scale_y = _scale_to_game / texture_size[1] * (1.0 / bpy.context.scene.bfg.global_uv_scale)
		for l in f.loops:
			luv = l[uv_layer]
			if luv.pin_uv is not True:
				if face_direction == 'x':
					luv.uv.x = ((l.vert.co.y * obj_scale[1]) + obj_location[1]) * scale_x
					luv.uv.y = ((l.vert.co.z * obj_scale[2]) + obj_location[2]) * scale_y
				if face_direction == '-x':
					luv.uv.x = (((l.vert.co.y * obj_scale[1]) + obj_location[1]) * scale_x) * -1
					luv.uv.y = ((l.vert.co.z * obj_scale[2]) + obj_location[2]) * scale_y
				if face_direction == 'y':
					luv.uv.x = (((l.vert.co.x * obj_scale[0]) + obj_location[0]) * scale_x) * -1
					luv.uv.y = ((l.vert.co.z * obj_scale[2]) + obj_location[2]) * scale_y
				if face_direction == '-y':
					luv.uv.x = ((l.vert.co.x * obj_scale[0]) + obj_location[0]) * scale_x
					luv.uv.y = ((l.vert.co.z * obj_scale[2]) + obj_location[2]) * scale_y
				if face_direction == 'z':
					luv.uv.x = ((l.vert.co.x * obj_scale[0]) + obj_location[0]) * scale_x
					luv.uv.y = ((l.vert.co.y * obj_scale[1]) + obj_location[1]) * scale_y
				if face_direction == '-z':
					luv.uv.x = (((l.vert.co.x * obj_scale[0]) + obj_location[0]) * scale_x) * 1
					luv.uv.y = (((l.vert.co.y * obj_scale[1]) + obj_location[1]) * scale_y) * -1
	if bpy.context.mode == 'EDIT_MESH':
		bmesh.update_edit_mesh(mesh)
	else:
		bm.to_mesh(mesh)
		bm.free()
		mesh.update()

class AutoUnwrap(bpy.types.Operator):
	bl_idname = "object.auto_uv_unwrap"
	bl_label = "Auto Unwrap"
	bl_options = {'REGISTER','UNDO'}

	@classmethod
	def poll(cls, context):
		return bpy.context.mode in ['EDIT_MESH', 'OBJECT']

	def execute(self, context):
		obj = context.active_object
		auto_unwrap(obj.data, obj.location, obj.scale)
		return {'FINISHED'}

class FitUV(bpy.types.Operator):
	"""Fit the selected face UVs to the texture dimensions along the specified axis"""
	bl_idname = "object.uv_fit"
	bl_label = "Fit UV"
	bl_options = {'REGISTER','UNDO'}
	axis = bpy.props.StringProperty(name="Axis", default='BOTH')

	@classmethod
	def poll(cls, context):
		return context.mode == 'EDIT_MESH'

	def execute(self, context):
		obj = context.active_object
		bm = bmesh.from_edit_mesh(obj.data)
		uv_layer = bm.loops.layers.uv.active
		if not uv_layer:
			return {'CANCELLED'}
		for f in bm.faces:
			if not f.select:
				continue
			# calculate min/max
			min = [None, None]
			max = [None, None]
			for l in f.loops:
				uv = l[uv_layer].uv
				if self.axis in ['HORIZONTAL', 'BOTH']:
					min[0] = min_nullable(min[0], uv.x)
					max[0] = max_nullable(max[0], uv.x)
				if self.axis in ['VERTICAL', 'BOTH']:
					min[1] = min_nullable(min[1], uv.y)
					max[1] = max_nullable(max[1], uv.y)
			# apply fitting
			for l in f.loops:
				uv = l[uv_layer].uv
				if self.axis in ['HORIZONTAL', 'BOTH']:
					range = max[0] - min[0]
					if range != 0: # will be 0 if UVs are uninitialized
						uv.x = uv.x / range * context.scene.bfg.uv_fit_repeat
				if self.axis in ['VERTICAL', 'BOTH']:
					range = max[1] - min[1]
					if range != 0: # will be 0 if UVs are uninitialized
						uv.y = uv.y / range * context.scene.bfg.uv_fit_repeat
		bmesh.update_edit_mesh(obj.data)
		return {'FINISHED'}

class FlipUV(bpy.types.Operator):
	"""Flip the selected face UVs along the specified axis"""
	bl_idname = "object.uv_flip"
	bl_label = "Flip UV"
	bl_options = {'REGISTER','UNDO'}
	axis = bpy.props.StringProperty(name="Axis", default='HORIZONTAL')

	@classmethod
	def poll(cls, context):
		return context.mode == 'EDIT_MESH'

	def execute(self, context):
		prev_area = context.area.type
		context.area.type = 'IMAGE_EDITOR'
		bpy.ops.uv.select_all(action='SELECT')
		if self.axis == 'HORIZONTAL':
			bpy.ops.transform.resize(value=(-1, 1, 1))
		elif self.axis == 'VERTICAL':
			bpy.ops.transform.resize(value=(1, -1, 1))
		context.area.type = prev_area
		return {'FINISHED'}

class NudgeUV(bpy.types.Operator):
	"""Nudge the selected face UVs in the specified direction"""
	bl_idname = "object.uv_nudge"
	bl_label = "Nudge UV"
	bl_options = {'REGISTER','UNDO'}
	dir = bpy.props.StringProperty(name="Direction", default='LEFT')

	@classmethod
	def poll(cls, context):
		return context.mode == 'EDIT_MESH'

	def execute(self, context):
		prev_area = context.area.type
		context.area.type = 'IMAGE_EDITOR'
		bpy.ops.uv.select_all(action='SELECT')
		if self.dir == 'LEFT':
			bpy.ops.transform.translate(value=(context.scene.bfg.uv_nudge_increment, 0, 0))
		elif self.dir == 'RIGHT':
			bpy.ops.transform.translate(value=(-context.scene.bfg.uv_nudge_increment, 0, 0))
		elif self.dir == 'UP':
			bpy.ops.transform.translate(value=(0, -context.scene.bfg.uv_nudge_increment, 0))
		elif self.dir == 'DOWN':
			bpy.ops.transform.translate(value=(0, context.scene.bfg.uv_nudge_increment, 0))
		context.area.type = prev_area
		return {'FINISHED'}

def is_uv_flipped(context):
	# just the first face
	obj = context.active_object
	bm = bmesh.from_edit_mesh(obj.data)
	uv_layer = bm.loops.layers.uv.active
	if uv_layer:
		for f in bm.faces:
			if not f.select:
				continue
			v1 = f.loops[1][uv_layer].uv - f.loops[0][uv_layer].uv
			v2 = f.loops[2][uv_layer].uv - f.loops[1][uv_layer].uv
			if v1.cross(v2) >= 0:
				return False
			else:
				return True
	return False

class RotateUV(bpy.types.Operator):
	"""Rotate the selected face UVs"""
	bl_idname = "object.uv_rotate"
	bl_label = "Rotate UV"
	bl_options = {'REGISTER','UNDO'}
	dir = bpy.props.StringProperty(name="Direction", default='HORIZONTAL')

	@classmethod
	def poll(cls, context):
		return context.mode == 'EDIT_MESH'

	def execute(self, context):
		prev_area = context.area.type
		context.area.type = 'IMAGE_EDITOR'
		bpy.ops.uv.select_all(action='SELECT')
		degrees = context.scene.bfg.uv_rotate_degrees
		if self.dir == 'RIGHT':
			degrees *= -1
		if is_uv_flipped(context):
			degrees *= -1 # swap left and right if the face normal is flipped
		bpy.ops.transform.rotate(value=math.radians(degrees))
		context.area.type = prev_area
		return {'FINISHED'}

################################################################################
## GUI PANELS
################################################################################

class SettingsPanel(bpy.types.Panel):
	bl_label = "Settings"
	bl_space_type = 'VIEW_3D'
	bl_region_type = 'TOOLS'
	bl_category = "BFGForge"

	def draw(self, context):
		scene = context.scene
		col = self.layout.column(align=True)
		col.prop(scene.bfg, "game_path", "Path")
		col.prop(scene.bfg, "mod_dir")
		col.operator(ImportMaterials.bl_idname, ImportMaterials.bl_label, icon='MATERIAL')
		col.operator(ImportEntities.bl_idname, ImportEntities.bl_label, icon='POSE_HLT')
		flow = col.column_flow(2)
		flow.prop(scene.bfg, "wireframe_rooms")
		flow.prop(scene.bfg, "backface_culling")
		flow.prop(scene.bfg, "show_entity_names")
		flow.prop(scene.bfg, "hide_bad_materials")
		flow.prop(scene.bfg, "shadeless_materials")
		col.prop(context.scene.bfg, "global_uv_scale")

class CreatePanel(bpy.types.Panel):
	bl_label = "Create"
	bl_space_type = 'VIEW_3D'
	bl_region_type = 'TOOLS'
	bl_category = "BFGForge"

	def draw(self, context):
		scene = context.scene
		col = self.layout.column(align=True)
		row = col.row(align=True)
		row.operator(BuildMap.bl_idname, "Build Map", icon='MOD_BUILD').bool_op = 'UNION'
		row.prop(context.scene.bfg, "map_layer")
		col.operator(AddRoom.bl_idname, "Add 2D Room", icon='SURFACE_NCURVE')
		col.operator(AddBrush.bl_idname, "Add 3D Room", icon='SNAP_FACE').s_type = '3D_ROOM'
		col.operator(AddBrush.bl_idname, "Add Brush", icon='SNAP_VOLUME').s_type = 'BRUSH'
		col = self.layout.column()
		if len(scene.bfg.entities) > 0:
			row = col.row(align=True)
			row.prop_search(scene.bfg, "active_entity", scene.bfg, "entities", "", icon='POSE_HLT')
			row.operator(ShowEntityDescription.bl_idname, "", icon='INFO')
			row.operator(AddEntity.bl_idname, "", icon='ZOOMIN')
		col.operator(AddLight.bl_idname, AddLight.bl_label, icon='LAMP_POINT')
		col.operator(AddStaticModel.bl_idname, AddStaticModel.bl_label, icon='MESH_MONKEY')

class MaterialPanel(bpy.types.Panel):
	bl_label = "Material"
	bl_space_type = 'VIEW_3D'
	bl_region_type = 'TOOLS'
	bl_category = "BFGForge"

	def draw(self, context):
		scene = context.scene
		if len(scene.bfg.material_decls) > 0:
			col = self.layout.column()
			col.prop_search(scene.bfg, "active_material_decl_path", scene.bfg, "material_decl_paths", "", icon='MATERIAL')
			col.template_icon_view(scene.bfg, "active_material_decl")
			col.prop(scene.bfg, "active_material_decl", "")
			obj = context.active_object
			if obj and len(context.selected_objects) > 0:
				if obj.bfg.type == '2D_ROOM':
					col.label("Assign:", icon='MATERIAL')
					row = col.row(align=True)
					row.operator(AssignMaterial.bl_idname, "Ceiling").where = 'CEILING'
					row.operator(AssignMaterial.bl_idname, "Wall").where = 'WALL'
					row.operator(AssignMaterial.bl_idname, "Floor").where = 'FLOOR'
					row.operator(AssignMaterial.bl_idname, "All").where = 'ALL'
				elif hasattr(obj.data, "materials") or len(context.selected_objects) > 1: # don't hide if multiple selections
					col.operator(AssignMaterial.bl_idname, AssignMaterial.bl_label, icon='MATERIAL')

class ObjectPanel(bpy.types.Panel):
	bl_label = "Object"
	bl_space_type = 'VIEW_3D'
	bl_region_type = 'TOOLS'
	bl_category = "BFGForge"

	def draw_object_label(self, col, obj):
		obj_icon = 'OBJECT_DATAMODE'
		if obj.type == 'LAMP':
			obj_icon = 'LAMP_POINT'
		elif obj.bfg.type in ['BRUSH_ENTITY','ENTITY']:
			obj_icon = 'POSE_HLT'
		obj_label = ""
		if obj.bfg.type != 'NONE':
			obj_label += obj.bfg.bl_rna.properties['type'].enum_items[obj.bfg.type].name + ": "
		obj_label += obj.name
		col.label(obj_label, icon=obj_icon)

	def draw_entity_properties(self, context, col, obj):
		col.prop(context.scene.bfg, "show_inherited_entity_props")
		for prop in obj.game.properties:
			is_inherited = prop.name.startswith("inherited_")
			if not context.scene.bfg.show_inherited_entity_props and is_inherited:
				continue # user doesn't want to see inherited props
			is_custom = prop.name.startswith("custom_")
			row = col.row(align=True)
			name = prop.name
			if is_inherited:
				name = name[len("inherited_"):] # remove the prefix
			elif is_custom:
				name = name[len("custom_"):] # remove the prefix
			row.label(name + ":")
			row.prop(prop, "value", text="")
			props = row.operator(ShowEntityPropertyDescription.bl_idname, "", icon='INFO')
			props.classname = obj.bfg.classname
			props.name = name
			if is_custom:
				# custom properties can be removed
				row.operator(RemoveCustomEntityProperty.bl_idname, "", icon='X').name = prop.name
		col.operator(NewCustomEntityProperty.bl_idname, NewCustomEntityProperty.bl_label, icon='ZOOMIN')

	def draw(self, context):
		obj = context.active_object
		if obj and len(context.selected_objects) > 0:
			col = self.layout.column()
			self.draw_object_label(col, obj)
			if obj.bfg.type == '2D_ROOM':
				sub = col.column(align=True)
				sub.prop(obj.bfg, "room_height")
				sub.operator(CopyRoom.bl_idname, "Copy Room Height", icon='PASTEFLIPUP').copy_op = 'HEIGHT'
				sub = col.column()
				sub.enabled = False
				sub.prop(obj.bfg, "ceiling_material", "Ceiling")
				sub.prop(obj.bfg, "wall_material", "Wall")
				sub.prop(obj.bfg, "floor_material", "Floor")
				col.label("Copy Materials:", icon='PASTEFLIPUP')
				row = col.row(align=True)
				row.operator(CopyRoom.bl_idname, "Ceiling").copy_op = 'MATERIAL_CEILING'
				row.operator(CopyRoom.bl_idname, "Wall").copy_op = 'MATERIAL_WALL'
				row.operator(CopyRoom.bl_idname, "Floor").copy_op = 'MATERIAL_FLOOR'
				row.operator(CopyRoom.bl_idname, "All").copy_op = 'MATERIAL_ALL'
				col.operator(ConvertRoom.bl_idname, ConvertRoom.bl_label, icon='SNAP_FACE')
			elif obj.bfg.type in ['3D_ROOM', 'BRUSH']:
				col.prop(obj.bfg, "auto_unwrap")
			elif obj.bfg.type in ['BRUSH_ENTITY','ENTITY']:
				self.draw_entity_properties(context, col, obj)
			elif obj.type == 'LAMP':
				row = col.row()
				row.prop(obj, "bfg_light_radius")
				row.prop(obj.data, "color", "")
				col.prop(obj.data, "use_specular")
				col.prop(obj.data, "use_diffuse")
				col.template_icon_view(obj.bfg, "light_material")
				col.prop(obj.bfg, "light_material", "")
			if hasattr(obj.data, "materials") or len(context.selected_objects) > 1: # don't hide if multiple selections
				col.operator(RefreshMaterials.bl_idname, RefreshMaterials.bl_label, icon='MATERIAL')
			# if this object is part of a brush entity (i.e. a child of one), show the brush entity properties
			if obj.parent and obj.parent.bfg.type == 'BRUSH_ENTITY':
				self.draw_object_label(col, obj.parent)
				self.draw_entity_properties(context, col, obj.parent)

class UvPanel(bpy.types.Panel):
	bl_label = "UV"
	bl_space_type = 'VIEW_3D'
	bl_region_type = 'TOOLS'
	bl_category = "BFGForge"

	def draw(self, context):
		obj = context.active_object
		if not obj or len(context.selected_objects) == 0 or not hasattr(obj.data, "materials"):
			return
		col = self.layout.column(align=True)
		col.operator(AutoUnwrap.bl_idname, AutoUnwrap.bl_label, icon='UV_FACESEL')
		if context.mode != 'EDIT_MESH':
			return
		col.separator()
		col.label("Nudge", icon='FORWARD')
		row = col.row(align=True)
		row.operator(NudgeUV.bl_idname, "Left").dir = 'LEFT'
		row.operator(NudgeUV.bl_idname, "Right").dir = 'RIGHT'
		row = col.row(align=True)
		row.operator(NudgeUV.bl_idname, "Up").dir = 'UP'
		row.operator(NudgeUV.bl_idname, "Down").dir = 'DOWN'
		col.prop(context.scene.bfg, "uv_nudge_increment", "Increment")
		col.separator()
		col.label("Rotate", icon='FILE_REFRESH')
		row = col.row(align=True)
		row.operator(RotateUV.bl_idname, "Left").dir = 'LEFT'
		row.operator(RotateUV.bl_idname, "Right").dir = 'RIGHT'
		col.prop(context.scene.bfg, "uv_rotate_degrees", "Degrees")
		col.separator()
		col.label("Flip", icon='LOOP_BACK')
		row = col.row(align=True)
		row.operator(FlipUV.bl_idname, "Horizontal").axis = 'HORIZONTAL'
		row.operator(FlipUV.bl_idname, "Vertical").axis = 'VERTICAL'
		col.separator()
		col.label("Fit", icon='FULLSCREEN_ENTER')
		row = col.row(align=True)
		row.operator(FitUV.bl_idname, "Horizontal").axis = 'HORIZONTAL'
		row.operator(FitUV.bl_idname, "Vertical").axis = 'VERTICAL'
		row.operator(FitUV.bl_idname, "Both").axis = 'BOTH'
		col.prop(context.scene.bfg, "uv_fit_repeat", "Repeat")

################################################################################
## PROPERTIES
################################################################################

def update_wireframe_rooms(self, context):
	for obj in context.scene.objects:
		if obj.bfg.type in ['2D_ROOM', '3D_ROOM', 'BRUSH']:
			obj.draw_type = 'WIRE' if context.scene.bfg.wireframe_rooms else 'TEXTURED'

def get_backface_culling(self):
	return bpy.context.space_data.show_backface_culling

def set_backface_culling(self, value):
	bpy.context.space_data.show_backface_culling = value

def update_show_entity_names(self, context):
	for obj in context.scene.objects:
		if obj.bfg.type == 'ENTITY':
			obj.show_name = context.scene.bfg.show_entity_names

def update_hide_bad_materials(self, context):
	preview_collections["material"].force_refresh = True
	preview_collections["light"].needs_refresh = True

def update_shadeless_materials(self, context):
	for mat in bpy.data.materials:
		mat_path = os.path.dirname(mat.name)
		if mat.name != "_object_color" and mat_path not in _editor_material_paths:
			mat.use_shadeless = context.scene.bfg.shadeless_materials

class BfgScenePropertyGroup(bpy.types.PropertyGroup):
	game_path = bpy.props.StringProperty(name="RBDOOM-3-BFG Path", description="RBDOOM-3-BFG Path", subtype='DIR_PATH')
	mod_dir = bpy.props.StringProperty(name="Mod Directory")
	wireframe_rooms = bpy.props.BoolProperty(name="Wireframe rooms", default=True, update=update_wireframe_rooms)
	backface_culling = bpy.props.BoolProperty(name="Backface culling", get=get_backface_culling, set=set_backface_culling)
	show_entity_names = bpy.props.BoolProperty(name="Show entity names", default=False, update=update_show_entity_names)
	hide_bad_materials = bpy.props.BoolProperty(name="Hide bad materials", description="Hide materials with missing diffuse textures", default=True, update=update_hide_bad_materials)
	shadeless_materials = bpy.props.BoolProperty(name="Fullbright materials", description="Disable lighting on materials", default=True, update=update_shadeless_materials)
	show_inherited_entity_props = bpy.props.BoolProperty(name="Show inherited properties", description="Show inherited entity properties", default=False)
	map_layer = bpy.props.IntProperty(name="Layer", default=0, min=0, max=19)
	material_decl_paths = bpy.props.CollectionProperty(type=MaterialDeclPathPropGroup)
	active_material_decl_path = bpy.props.StringProperty(name="", default="")
	material_decls = bpy.props.CollectionProperty(type=MaterialDeclPropGroup)
	active_material_decl = bpy.props.EnumProperty(name="", items=material_decl_preview_items)
	entities = bpy.props.CollectionProperty(type=EntityPropGroup)
	active_entity = bpy.props.StringProperty(name="Active Entity", default="")
	model_defs = bpy.props.CollectionProperty(type=ModelDefPropGroup)
	global_uv_scale = bpy.props.FloatProperty(name="Global UV Scale", description="Scale Automatically unwrapped UVs by this amount", default=0.5, step=0.1, min=0.1, max=10)
	uv_fit_repeat = bpy.props.FloatProperty(name="UV Fit Repeat", default=1.0, step=0.1, min=0.1, max=10)
	uv_nudge_increment = bpy.props.FloatProperty(name="Nudge Increment", default=_scale_to_blender)
	uv_rotate_degrees = bpy.props.FloatProperty(name="UV Rotate Degrees", default=90.0, step=10.0, min=1.0, max=90.0)

class BfgObjectPropertyGroup(bpy.types.PropertyGroup):
	auto_unwrap = bpy.props.BoolProperty(name="Auto unwrap on Build Map", description="Auto Unwrap this object when the map is built", default=True)
	classname = bpy.props.StringProperty(name="Classname", default="")
	entity_model = bpy.props.StringProperty(name="Entity model", default="")
	room_height = bpy.props.FloatProperty(name="Room Height", default=4, step=20, precision=1, update=update_room)
	floor_material = bpy.props.StringProperty(name="Floor Material", update=update_room)
	wall_material = bpy.props.StringProperty(name="Wall Material", update=update_room)
	ceiling_material = bpy.props.StringProperty(name="Ceiling Material", update=update_room)
	light_material = bpy.props.EnumProperty(name="", items=light_material_preview_items)
	type = bpy.props.EnumProperty(items=[
		('NONE', "None", ""),
		('2D_ROOM', "2D Room", ""),
		('3D_ROOM', "3D Room", ""),
		('BRUSH', "Brush", ""),
		('ENTITY', "Entity", ""),
		('BRUSH_ENTITY', "Brush Entity", ""),
		('STATIC_MODEL', "Static Model", "")
	], name="BFG Forge Object Type", default='NONE')

################################################################################
## MAIN
################################################################################

def register():
	bpy.types.Scene.bfg = bpy.props.PointerProperty(type=BfgScenePropertyGroup)
	bpy.types.Object.bfg = bpy.props.PointerProperty(type=BfgObjectPropertyGroup)
	# not in BfgObjectPropertyGroup because get/set self object would be BfgObjectPropertyGroup, not bpy.types.Object
	bpy.types.Object.bfg_light_radius = bpy.props.FloatProperty(name="Radius", get=get_light_radius, set=set_light_radius)
	pcoll = bpy.utils.previews.new()
	pcoll.materials = ()
	pcoll.current_decl_path = ""
	pcoll.force_refresh = False
	preview_collections["material"] = pcoll
	pcoll = bpy.utils.previews.new()
	pcoll.lights = ()
	pcoll.needs_refresh = True
	preview_collections["light"] = pcoll

def unregister():
	del bpy.types.Scene.bfg
	del bpy.types.Object.bfg
	del bpy.types.Object.bfg_light_radius
	for pcoll in preview_collections.values():
		bpy.utils.previews.remove(pcoll)
	preview_collections.clear()

if __name__ == "__main__":
	register()


#!/usr/bin/env python
# -*- coding: utf-8 -*-

__author__ = 'Michael Liao (askxuefeng@gmail.com)'

DEPRECATED = True

from google.appengine.ext import db

DEFAULT_GROUP = 'default'

def get_instance_settings_as_dict(widget_instance):
    '''
    get widget instance settings as dict which contains key-value pairs (both str/unicode).

    Args:
        widget_instance: WidgetInstance object.
    Returns:
        Settings as dict which both key and value are str/unicode.
    '''
    list = WidgetInstanceSetting.all().filter('widget_instance ==', widget_instance).fetch(100)
    d = {}
    for setting in list:
        d[setting.setting_key] = setting.setting_value
    import logging
    logging.info('get_instance_settings_as_dict: ' + str(d))
    return d

def update_instance_settings(widget_instance, setting_as_dict):
    '''
    Update instance settings.

    Args:
      widget_instance: WidgetInstance object.
      setting_as_dict: new settings as dict contains key as str and value as str or unicde.
    Returns:
      None
    '''
    group = widget_instance.widget_group
    db.delete(get_instance_settings(widget_instance))
    for name, value in setting_as_dict.items():
        WidgetInstanceSetting(
                widget_group=group,
                widget_instance=widget_instance,
                setting_name=name,
                setting_value=value
        ).put()


import os
import numpy as np
import pickle
import open3d as o3d
from absl import app
from absl import flags

flags.DEFINE_string('task', 'stack-block-pyramid', '')
flags.DEFINE_string('data_dir', './training_datasets/voxel_grids', '')
flags.DEFINE_string('data_source', './training_datasets/rgbd', '')
flags.DEFINE_integer('num_per_class', '100', '')

FLAGS = flags.FLAGS

def normalize(arr):
    """
    normalize an array to [0, 1]
    """
    arr_min = arr.min()
    arr_max = arr.max()
    return (arr - arr_min) / (arr_max - arr_min)

def load_rgb_segm_depth(path, num):

  rgb_image_batches = []
  # segm_image_batches = []
  depth_image_batches = []
  class_paths = []
  file_name_batches = []

  sub_paths = os.listdir(path)

  data_types = ['positive', 'negative']

  img_types = ['color', 'depth']

  for sub_path in sub_paths:
    for data_type in data_types:

      rgb_images = []
      # segm_images = []
      depth_images = []
      file_names = []
      class_path = os.path.join(path, sub_path, data_type)
      print(class_path)

      for img_type in img_types:

        img_path = os.path.join(class_path, img_type)
        images = os.listdir(img_path)
        for i ,img_name in enumerate(images):
          if i == num:
            break

          with open(os.path.join(img_path, img_name), 'rb') as f:
            file_names.append(img_name[:img_name.find('.')])
            img = pickle.load(f)

          if img_type == 'color':
            rgb_images.append(img)
          # elif img_type == 'segm':
          #   segm_images.append(img)
          elif img_type == 'depth':
            depth_images.append(img)

      rgb_image_batches.append(rgb_images)
      # segm_image_batches.append(segm_images)
      depth_image_batches.append(depth_images)
      file_name_batches.append(file_names)
      class_paths.append(class_path)

  return rgb_image_batches, depth_image_batches, file_name_batches, class_paths

def rgbd_to_voxel_grids(rgb_img, depth_img):

  rgbd_img = o3d.geometry.RGBDImage.create_from_color_and_depth(
    o3d.geometry.Image(rgb_img),
    o3d.geometry.Image(depth_img),
    convert_rgb_to_intensity=False)

  # x_dim = 50
  # y_dim = 45
  # z_dim = 100

  x_dim = 35
  y_dim = 30
  z_dim = 70

  # # xyz
  # camera_pose = np.array([[-6.12323400e-17, -1.00000000e+00,  1.22464680e-16, 1],
  #       [-7.07106781e-01,  1.29893408e-16,  7.07106781e-01, 0],
  #       [-7.07106781e-01, -4.32978028e-17, -7.07106781e-01, 0.75],
  #       [0,0,0,1]])

  #xzy
  camera_pose = np.array([[-6.12323400e-17, -1.22464680e-16,  1.00000000e+00,1 ],
  [-7.07106781e-01, -7.07106781e-01, -1.29893408e-16,0],
  [ 7.07106781e-01, -7.07106781e-01, -4.32978028e-17,0.75],
  [0,0,0,1]])

  intrinsic_matrix = np.array([[450., 0, 320.],
                                    [0, 450., 240.],
                                    [0, 0, 1]])
  camera_intrinsic = o3d.camera.PinholeCameraIntrinsic(640, 480, intrinsic_matrix)

  pcd = o3d.geometry.PointCloud.create_from_rgbd_image(
      rgbd_img, camera_intrinsic, camera_pose)

  # o3d.visualization.draw_geometries([pcd])
  min_bound = pcd.get_min_bound()
  max_bound = pcd.get_max_bound()

  min_bound[0] += 0.000
  max_bound[0] -= 0.0018

  min_bound[1] += 0.00000105
  max_bound[1] -= 0.000275

  min_bound[2] += 0.0011
  max_bound[2] -= 0.0011

  cropping_bound = o3d.geometry.AxisAlignedBoundingBox(min_bound, max_bound)

  # Crop the point cloud using the bounding box
  cropped_point_cloud = pcd.crop(cropping_bound)

  # o3d.visualization.draw_geometries([cropped_point_cloud])

  # Get point cloud coordinates
  points = np.asarray(cropped_point_cloud.points)

  # Get point cloud colors
  colors = np.asarray(cropped_point_cloud.colors)

  # Define voxel size
  # voxel_size = 0.0000105
  voxel_size = 0.000015

  # Calculate voxel grid dimensions
  max_bound = np.max(points, axis=0)
  min_bound = np.min(points, axis=0)
  dimensions = np.ceil((max_bound - min_bound) / voxel_size)

  print(f'dimentsions: {dimensions}')
  # Calculate voxel grid indices for each point
  indices = np.floor((points - min_bound) / voxel_size)

  # Initialize voxel grid with zeros
  voxel_grid = np.zeros((int(dimensions[0]), int(dimensions[1]), int(dimensions[2]), 3))

  # Fill voxel grid with colors
  for i in range(points.shape[0]):
      voxel_grid[int(indices[i][0]), int(indices[i][1]), int(indices[i][2])] = colors[i]

  # Convert voxel grid to 3D numpy array
  voxel_grid = np.asarray(voxel_grid)
  voxel_grid = np.pad(voxel_grid, ((0, x_dim-voxel_grid.shape[0]), (0, y_dim-voxel_grid.shape[1]), (0, z_dim-voxel_grid.shape[2]), (0, 0)), mode='constant')

  return voxel_grid

def main(unused_argv):
  rgb_image_batches, depth_image_batches, file_name_batches, class_paths = load_rgb_segm_depth(os.path.join(FLAGS.data_source, FLAGS.task), FLAGS.num_per_class)

  for idx, class_path in enumerate(class_paths):
    for n in range(FLAGS.num_per_class):
      print(f'Processing {class_path}...')

      voxel_grid = rgbd_to_voxel_grids(rgb_image_batches[idx][n], depth_image_batches[idx][n])

      voxel_grid = np.mean(voxel_grid, axis=-1, keepdims=True)
      # voxel_grid = voxel_grid.reshape(25, 20, 50)

      save_path = os.path.join(FLAGS.data_dir, class_path)
      if not os.path.exists(save_path):
          os.makedirs(save_path)

      # save_path = os.path.join(file_path, file_name[:file_name.find('.')] + '.npy')
      # voxel_grid = np.mean(voxel_grid, axis=-1, keepdims=True)
      np.save(os.path.join(save_path, file_name_batches[idx][n] + '.npy'), voxel_grid)

if __name__ == '__main__':
  app.run(main)


from zlib import adler32
from functools import wraps
from flask_restful import Resource as DefaultResurce, ResponseBase, OrderedDict, request, unpack, marshal

VALIDATION_ERROR_MESSAGE = 'Fields validation error'

class marshal_with(object):
    """A decorator that apply marshalling to the return values of your methods.

    >>> from flask_restful import fields, marshal_with
    >>> mfields = { 'a': fields.Raw }
    >>> @marshal_with(mfields)
    ... def get():
    ...     return { 'a': 100, 'b': 'foo' }
    ...
    ...
    >>> get()
    OrderedDict([('a', 100)])

    >>> @marshal_with(mfields, envelope='data')
    ... def get():
    ...     return { 'a': 100, 'b': 'foo' }
    ...
    ...
    >>> get()
    OrderedDict([('data', OrderedDict([('a', 100)]))])

    see :meth:`flask_restful.marshal`
    """
    def __init__(self, fields, envelope=None):
        """
        :param fields: a dict of whose keys will make up the final
                       serialized response output
        :param envelope: optional key that will be used to envelop the serialized
                         response
        """
        self.fields = fields
        self.envelope = envelope

    def __call__(self, f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            resp = f(*args, **kwargs)
            if isinstance(resp, tuple):
                data, code, headers = unpack(resp)
                if code != 200 or data.get('success') is False:
                    return resp
                return marshal(data, self.fields, self.envelope), code, headers
            else:
                return marshal(resp, self.fields, self.envelope)
        return wrapper

class Resource(DefaultResurce):

    def __serialize_errors(self, resp):
        """reformat the error structure and add the error codes"""
        if not resp.get('errors'):
            return resp
        elif resp['errors'].get('global_error'):
            ge_message = resp['errors'].pop('global_error')
            resp['errors']['global_error'] = {
                'code': adler32(ge_message.encode('UTF-16')),
                'message': ge_message
            }
            return resp
        else:
            # compose field errors
            field_errors = {}
            for field in resp['errors']:
                field_errors[field] = {
                    'code': adler32(resp['errors'][field].encode('UTF-16')),
                    'message': resp['errors'][field]
                }
            resp['errors'] = {
                'fields': field_errors
            }
            # compose global_error
            if resp['errors'].get('global_error'):
                ge_message = resp['errors']['global_error']
                resp['errors']['global_error'] = {
                    'code': adler32(ge_message.encode('UTF-16')),
                    'message': ge_message
                }
            else:
                resp['errors']['global_error'] = {
                    'code': adler32(VALIDATION_ERROR_MESSAGE.encode('UTF-16')),
                    'message': VALIDATION_ERROR_MESSAGE
                }
            return resp

    def dispatch_request(self, *args, **kwargs):

        # Taken from flask
        #noinspection PyUnresolvedReferences
        meth = getattr(self, request.method.lower(), None)
        if meth is None and request.method == 'HEAD':
            meth = getattr(self, 'get', None)
        assert meth is not None, 'Unimplemented method %r' % request.method

        for decorator in self.method_decorators:
            meth = decorator(meth)

        resp = meth(*args, **kwargs)

        # adds default success key
        if isinstance(resp, dict):
            if resp.get('success', None) is None:
                resp['success'] = True
            elif resp.get('success', True) is False:
                resp = self.__serialize_errors(resp)
        elif isinstance(resp, tuple):
            if resp[0].get('success', True) is False:
                list(resp)[0] = self.__serialize_errors(resp[0])
                resp = tuple(resp)

        if isinstance(resp, ResponseBase):  # There may be a better way to test
            return resp

        representations = self.representations or OrderedDict()

        #noinspection PyUnresolvedReferences
        mediatype = request.accept_mimetypes.best_match(representations, default=None)
        if mediatype in representations:
            data, code, headers = unpack(resp)
            resp = representations[mediatype](data, code, headers)
            resp.headers['Content-Type'] = mediatype
            return resp

        return resp


import pickle as pkl
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.ticker import FixedLocator, FixedFormatter

def log_histogram(data, name):
    """
    Create ridgeline plots, where each individual plot is a
    log-y scaled histogram.

    data: 2D Numpy array
    Each row contains all the values to include in a single histogram.
    0th row will be in the lowest ridgeline plot.
    Every Nth row will be a separate histogram. N is selected according
    to n_target_rows, below to avoid overwhelming the viewer.

    name: str
    A name stem for the results image.
    """
    dpi = 300
    border = .125
    eps = 1e-6
    # n_bins = 50
    n_bins = 100
    n_target_rows = 16

    fig = plt.figure()
    ax = fig.gca()

    n_rows, n_cols = data.shape
    d_keep = np.maximum(1, np.floor(n_rows / n_target_rows))
    i_keep = np.arange(0, n_rows, d_keep, dtype=int)
    data = data[i_keep, :]
    n_rows, n_cols = data.shape

    x_min = np.min(data)
    x_max = np.max(data)
    x_range = x_max - x_min + eps
    x_border = x_range * border
    bin_edges = np.linspace(x_min, x_max, n_bins + 1)
    bin_centers = ((bin_edges[:-1] + bin_edges[1:]) / 2)

    y_range = n_rows
    y_border = y_range * border
    ax.set_ylabel("Epoch")
    scale = .1

    for i in np.arange(n_rows)[::-1]:
        y_0 = i

        ax.plot(
            [x_min, x_max],
            [y_0, y_0],
            color="gray",
            alpha=.4,
            linewidth=.1,
        )

        counts, bins = np.histogram(data[i, :], bins=bin_edges)
        x = np.array([x_min] + list(bin_centers) + [x_max])
        y = y_0 + np.log2(np.array([0] + list(counts) + [0]) + 1) * scale
        ax.add_patch(patches.Polygon(
            np.concatenate((x[:, np.newaxis], y[:, np.newaxis]), axis=1),
            alpha=1,
            edgecolor="black",
            facecolor="lightsteelblue",
            linewidth=.2,
        ))

    y_formatter = FixedFormatter([str(i) for i in i_keep])
    y_locator = FixedLocator(list(np.arange(i_keep.size)))
    ax.yaxis.set_major_formatter(y_formatter)
    ax.yaxis.set_major_locator(y_locator)

    plt.savefig("hist_" + name + ".png", dpi=dpi)
    plt.close()

# This code is specific to my use case.
# It parses the values I pulled out and stashed in a dict with a
# key naming scheme that lets me re-create a Numpy array from it.

# filename = "data/params/scs_weight_01.pkl"
filename = "data/params/scs_p_01.pkl"
# filename = "data/params/scs_out_01.pkl"

def main():
    with open(filename, "rb") as f:
        scs_out = pkl.load(f)
        n_vals_per_iter = 0
        n_iters = 0
        n_epochs = 0
        out_vals = {}
        for name, vals in scs_out.items():
            n_vals_per_iter = vals.size
            layer, i_epoch, i_iter = name.split("_")
            n_iters = np.maximum(n_iters, int(i_iter) + 1)
            n_epochs = np.maximum(n_epochs, int(i_epoch) + 1)
            out_vals[layer] = None

        for layer in out_vals.keys():
            out_vals[layer] = np.zeros((n_epochs, n_iters * n_vals_per_iter))

        for name, vals in scs_out.items():
            layer, i_epoch, i_iter = name.split("_")
            i_iter = int(i_iter)
            i_epoch = int(i_epoch)
            out_vals[layer][
                    i_epoch,
                    n_vals_per_iter * i_iter: n_vals_per_iter * (i_iter + 1)
                    ] = vals

    for layer, vals in out_vals.items():
        log_histogram(vals, layer)

if __name__ == "__main__":
    main()


import server
import os
import config
import time
import threading

class Chord:
    def __init__(self):
        self.qnt_key = None
        self.qnt_nodes = None
        self.cfg = config.Config()
        self.host = self.cfg.getHost().strip("\n")
        self.portaInicial = int(self.cfg.getMinPort().strip("\n"))
        self.srv = [] #lista de servidores
        self.id = []
        self.portas = []
        self.qnt_key = None
        self.qnt_nodes = None

    def main(self):
        self.qnt_key = input("Quantidade de Bits: ")
        self.qnt_nodes = input("Quantidade de Nós: ")
        self.snap_time = input("Tempo Snapshot (seg): ")

        self.id.insert(0, 2**int(self.qnt_key) - 1)
        self.portas.insert(0, self.portaInicial)

        for i in range(1, int(self.qnt_nodes)):
            self.id.insert(i, int(self.id[i-1] - ((2**int(self.qnt_key))/int(self.qnt_nodes))))
            self.portas.insert(i, self.portaInicial+ 4*i)

        for i in range(0, int(self.qnt_nodes)):
            if i == 0:
                self.srv.insert(i, (self.id[int(self.qnt_nodes)-1], self.id[i], self.id[i+1],self.portas[int(self.qnt_nodes)-1], self.portas[i], self.portas[i+1]))
            elif i == (int(self.qnt_nodes )-1):
                self.srv.insert(i, (self.id[i-1], self.id[i], self.id[0],self.portas[i-1], self.portas[i], self.portas[0]))
            else:
                self.srv.insert(i, (self.id[i-1], self.id[i], self.id[i+1],self.portas[i-1], self.portas[i], self.portas[i+1]))

        chord = open("thisChord.txt","w")
        for i in range(0, int(self.qnt_nodes) ): #inicia os servidores passando os parametros criados pelo chord
            cAnt, cAtu, cSuc, pAnt, pAtu, pSuc = self.srv[i]
            chord.write(self.qnt_key + " " + self.qnt_nodes + " " + str(cAnt) + " " + str(cAtu) + " " + str(cSuc) + " " + str(pAnt) + " " + str(pAtu) + " " + str(pSuc) +  " " + self.snap_time + "\n")
            os.system("start /B start cmd.exe @cmd /k python server.py " + self.qnt_key + " " + self.qnt_nodes + " " + str(cAnt) + " " + str(cAtu) + " " + str(cSuc) + " " + str(pAnt) + " " + str(pAtu) + " " + str(pSuc) +  " " + self.snap_time)

        #inicia cliente
        os.system("start /B start cmd.exe @cmd /k python client.py ")

if __name__ == '__main__':
    chord = Chord()
    chord.main()


from flask import Flask, render_template, request
#import requests
import pickle
#import numpy as np
import sklearn
from sklearn.preprocessing import StandardScaler

app = Flask(__name__)
model = pickle.load(open('model.pkl', 'rb'))
@app.route('/',methods=['GET'])
def index():
    return render_template('index.html')

standard_to = StandardScaler()
@app.route("/predict", methods=['POST'])
def predict():

    if request.method == 'POST':

        #1Age
        Age=float(request.form['Age'])

        #2BusinessTravel
        BusinessTravel=int(request.form['BusinessTravel'])

        #3Department
        Department=int(request.form['Department'])

        #4distanceFromHome
        DistanceFromHome=float(request.form['DistanceFromHome'])
        #5Education
        Education=float(request.form['Education'])

        #6EducationField
        EducationField=int(request.form['EducationField'])

        #7EnvironmentSatisfaction
        EnvironmentSatisfaction=float(request.form['EnvironmentSatisfaction'])

        #8JobInvolvement
        JobInvolvement=float(request.form['JobInvolvement'])

        #9JobLevel
        JobLevel=float(request.form['JobLevel'])

        #10JobRole
        JobRole=int(request.form['JobRole'])

        #11JobSatisfaction
        JobSatisfaction=float(request.form['JobSatisfaction'])

        #12MaritalStatus
        MaritalStatus=int(request.form['MaritalStatus'])

        #14MonthlyIncome
        MonthlyIncome=float(request.form['MonthlyIncome'])

        #15NumCompaniesWorked
        NumCompaniesWorked=float(request.form['NumCompaniesWorked'])

        #16OverTime
        OverTime=int(request.form['OverTime'])

        #17PercentSalaryHike
        PercentSalaryHike=float(request.form['PercentSalaryHike'])

        #18PerformanceRating
        PerformanceRating=float(request.form['PerformanceRating'])

        #19RelationshipSatisfaction
        RelationshipSatisfaction=float(request.form['RelationshipSatisfaction'])

       #20StockOptionLevel
        StockOptionLevel=float(request.form['StockOptionLevel'])

        #21TotalWorkingYears
        TotalWorkingYears=float(request.form['TotalWorkingYears'])

        #22TrainingTimesLastYear
        TrainingTimesLastYear=float(request.form['TrainingTimesLastYear'])

        #23YearsSinceLastPromotion
        WorkLifeBalance=float(request.form['WorkLifeBalance'])

        #24YearsAtCompany
        YearsAtCompany=float(request.form['YearsAtCompany'])

        #25YearsInCurrentRole
        YearsInCurrentRole=float(request.form['YearsInCurrentRole'])

        #26YearsSinceLastPromotion
        YearsSinceLastPromotion=float(request.form['YearsSinceLastPromotion'])

        #27YearsWithCurrManager
        YearsWithCurrManager=float(request.form['YearsWithCurrManager'])

        #28Stability
        Stability=float(request.form['Stability'])

        #29Fidelity
        Fidelity=float(request.form['Fidelity'])

        #30Income_YearsComp
        Income_YearsComp=float(request.form['Income_YearsComp'])

        #31Total_Satisfaction
        TotalSatisfaction_mean=float(request.form['Total_Satisfaction'])

        prediction=model.predict([[Age,
                                    Department,
                                    DistanceFromHome,
                                    Education,
                                    EducationField,
                                    EnvironmentSatisfaction,
                                    JobInvolvement,
                                    JobLevel,
                                    JobRole,
                                    BusinessTravel,
                                    JobSatisfaction,
                                    MaritalStatus,
                                    MonthlyIncome,
                                    NumCompaniesWorked,
                                    OverTime,
                                    PercentSalaryHike,
                                    PerformanceRating,
                                    RelationshipSatisfaction,
                                    StockOptionLevel,
                                    TotalWorkingYears,
                                    TrainingTimesLastYear,
                                    WorkLifeBalance,
                                    YearsAtCompany,
                                    YearsInCurrentRole,
                                    YearsSinceLastPromotion,
                                    YearsWithCurrManager,
                                     Stability,
                                     Fidelity,
                                    Income_YearsComp,
                                    TotalSatisfaction_mean
                                   ]])

        #output=prediction
        if(prediction==0):
            return render_template('result.html',prediction_text="This employee likely to leave Company")
        elif(prediction==1):
            return render_template('result.html',prediction_text="This employee will not leave the Company")
        else:
            return render_template('result.html',prediction_text=" data not present")

if __name__=="__main__":
  app.run()


import matplotlib.pyplot as plt
import seaborn as sns; sns.set()  # for plot styling
import numpy as np
from sklearn.datasets.samples_generator import make_blobs
from numpy import genfromtxt

#humm, encontre este codigo en un servidor remoto
#estaba junto con el "traffic.pcap"
# que podria ser?, like some sample code

my_data2 = np.genfromtxt('test_2.txt', delimiter=',')
db = DBSCAN(eps=10000, min_samples=100000).fit(my_data2)
labels = db.labels_
n_clusters_ = len(set(labels)) - (1 if -1 in labels else 0)
core_samples_mask = np.zeros_like(db.labels_, dtype=bool)
core_samples_mask[db.core_sample_indices_] = True
unique_labels = set(labels)
colors = [plt.cm.Spectral(each)
          for each in np.linsspace(0, 1, len(unique_labels))]
for k, col in zip(unique_labels, colors):
    class_member_mask = (labels == k)
    xy = X[class_member_mask & core_samples_mask]
    plt.plot(xy[:, 0], xy[:, 1], 'o', markerfacecolor=tuple(col),
             markeredgecolor='k', markersize=14)

#NOTE: what you see in the sky put it format TMCTF{replace_here}
#where "replace_here" is what you see
plt.title('aaaaaaaa: %d' % n_clusters_)
plt.show()


import solution

class Solution(solution.Solution):
    def solve(self, test_input=None):
        logs, k = test_input
        return self.findingUsersActiveMinutes([x[:] for x in logs], k)

    def findingUsersActiveMinutes(self, logs, k):
        """
        :type logs: List[List[int]]
        :type k: int
        :rtype: List[int]
        """
        from collections import defaultdict
        d = defaultdict(set)
        for i,t in logs:
            d[i].add(t)
        ans = [0] * k
        for key in d:
            ans[len(d[key]) - 1] += 1
        return ans


import tkinter as tk
from PIL import ImageTk, Image

class Caption:
    """ """
    text = ''

    lastx = None
    lasty = None
    speech_bubble = 'imgs/spb-300x165.png'
    thought_bubble = 'imgs/thought.png'

    long_sample = ("I am the hope of the universe. I am the answer to all living things "
                   "that cry out for peace. I am protector of the innocent. I am the "
                   "light in the darkness. I am truth. Ally to good! Nightmare to you!")

    def __init__(self, canvas, caption_text=None, bubble_type='speech', position={'x': 0, 'y': 0}):
        self.canvas = canvas
        if not caption_text:
            self.text = caption_text
        self.type = bubble_type  # 'speech' or 'thought'
        self.speech_img = ImageTk.PhotoImage(Image.open(self.speech_bubble))
        self.thought_img = ImageTk.PhotoImage(Image.open(self.thought_bubble))
        self.lastx, self.lasty = position

        if bubble_type == 'thought':
            bg_image = self.thought_img
            text_posx = position['x'] + 50
            text_posy = position['y'] + 50
        else:
            bg_image = self.speech_img
            text_posx = position['x'] + 75
            text_posy = position['y'] + 25

        self.cid = self.canvas.create_image(position['x'], position['y'], anchor=tk.NW, image=bg_image)
        caption_tag = 'caption-{}'.format(int(self.cid / 2))  # local caption id
        layer_tag = 'layer-{}'.format(1)
        self.canvas.itemconfig(self.cid, tag=(caption_tag, layer_tag))

        # caption text
        self.tid = self.canvas.create_text(text_posx, text_posy,
                                           anchor=tk.NW,
                                           width=200,
                                           text=caption_text,
                                           tags='caption-text')
        # caption_text_tag = 'caption-{}-text'.format(self.cid)
        # canvas.addtag_withtag(caption_text_tag, tid)
        self.canvas.itemconfig(self.tid, tag=(caption_tag, layer_tag))

        print(caption_tag)


"""
热力图
"""

from pyecharts import options as opts
from pyecharts.charts import Geo
from pyecharts.globals import GeoType, ThemeType
from pyecharts.faker import Faker
import random, os

map = (
    Geo(init_opts=opts.InitOpts(width="1000px", height="800px", renderer="canvas",
                                theme=ThemeType.LIGHT, animation_opts=opts.AnimationOpts(animation=True)))
        .add_schema(maptype="china")
        .add(series_name="中国热力图",
             data_pair=[list(z) for z in
                        zip(Faker.provinces,
                            [random.randint(0, (i + 1) * 200 + 100) for i in range(len(Faker.provinces))])],
             type_=GeoType.EFFECT_SCATTER, symbol_size=20,
             color=Faker.visual_color[random.randint(0, len(Faker.visual_color)-1)])
        .add(series_name="广东热力图",
             data_pair=[list(z) for z in
                        zip(Faker.guangdong_city,
                            [random.randint(0, (i + 1) * 100) for i in range(len(Faker.provinces))])],
             type_=GeoType.EFFECT_SCATTER, symbol_size=10,
             color=Faker.visual_color[1])
        .set_series_opts(label_opts=opts.LabelOpts(is_show=False))
        .set_global_opts(visualmap_opts=opts.VisualMapOpts(max_=400, pos_top=20),
                         title_opts=opts.TitleOpts(title=""),
                         )
)
map.render(path="E:/tmp/geo_heatmap.html")

os.system('E:/tmp/geo_heatmap.html')