import sys
import importlib
import glob
import ursina
from pathlib import Path
from panda3d.core import NodePath
from ursina.vec2 import Vec2
from ursina.vec3 import Vec3
from ursina.vec4 import Vec4
from panda3d.core import Quat
from panda3d.core import TransparencyAttrib
from panda3d.core import Shader
from panda3d.core import TexGenAttrib

from ursina.texture import Texture
from panda3d.core import MovieTexture
from panda3d.core import TextureStage
from panda3d.core import CullFaceAttrib

from ursina import application
from ursina.collider import Collider, BoxCollider, SphereCollider, MeshCollider, CapsuleCollider
from ursina.mesh import Mesh
from ursina.sequence import Sequence, Func, Wait
from ursina.ursinamath import lerp
from ursina import curve
from ursina.curve import CubicBezier
from ursina import mesh_importer
from ursina.mesh_importer import load_model
from ursina.texture_importer import load_texture
from ursina.string_utilities import camel_to_snake
from textwrap import dedent
from panda3d.core import Shader as Panda3dShader
from ursina import shader
from ursina.shader import Shader
from ursina.string_utilities import print_info, print_warning
from ursina.ursinamath import Bounds

from ursina import color
from ursina.color import Color
try:
    from ursina.scene import instance as scene
except:
    pass

_Ursina_instance = None
_warn_if_ursina_not_instantiated = True # gets set to True after Ursina.__init__() to ensure the correct order.

from ursina.scripts.property_generator import generate_properties_for_class
@generate_properties_for_class()
class Entity(NodePath):
    rotation_directions = (-1,-1,1)
    default_shader = None
    default_values = {
        # 'parent':scene,
        'name':'entity', 'enabled':True, 'eternal':False, 'position':Vec3(0,0,0), 'rotation':Vec3(0,0,0), 'scale':Vec3(1,1,1), 'model':None, 'origin':Vec3(0,0,0),
        'shader':None, 'texture':None, 'color':color.white, 'collider':None}

    def __init__(self, add_to_scene_entities=True, **kwargs):
        self._children = []
        super().__init__(self.__class__.__name__)

        self.name = camel_to_snake(self.__class__.__name__)
        self.ignore = False     # if True, will not try to run code.
        self.ignore_paused = False      # if True, will still run when application is paused. useful when making a pause menu for example.
        self.ignore_input = False

        self.parent = scene     # default parent is scene, which means it's in 3d space. to use UI space, set the parent to camera.ui instead.
        self.add_to_scene_entities = add_to_scene_entities # set to False to be ignored by the engine, but still get rendered.
        if add_to_scene_entities:
            scene.entities.append(self)

        self._shader_inputs = {}
        if Entity.default_shader:
            self.shader = Entity.default_shader

        self.setPythonTag('Entity', self)   # for the raycast to get the Entity and not just the NodePath
        self.scripts = []   # add with add_script(class_instance). will assign an 'entity' variable to the script.
        self.animations = []
        self.hovered = False    # will return True if mouse hovers entity.
        self.line_definition = None # returns a Traceback(filename, lineno, function, code_context, index).

        if application.trace_entity_definition and add_to_scene_entities or (not _Ursina_instance and _warn_if_ursina_not_instantiated and add_to_scene_entities):
            from inspect import getframeinfo, stack
            _stack = stack()
            caller = getframeinfo(_stack[1][0])
            if len(_stack) > 2 and _stack[1].code_context and 'super().__init__()' in _stack[1].code_context[0]:
                caller = getframeinfo(_stack[2][0])

            self.line_definition = caller
            if caller.code_context:
                self.code_context = caller.code_context[0]

                if (self.code_context.count('(') == self.code_context.count(')') and
                ' = ' in self.code_context and 'name=' not in self.code_context
                and 'Ursina()' not in self.code_context):

                    self.name = self.code_context.split(' = ')[0].strip().replace('self.', '')
                    # print('set name to:', self.code_context.split(' = ')[0].strip().replace('self.', ''))

                if application.print_entity_definition:
                    print(f'{Path(caller.filename).name} ->  {caller.lineno} -> {caller.code_context}')


        if not _Ursina_instance and _warn_if_ursina_not_instantiated and add_to_scene_entities:
            print_warning('Tried to instantiate Entity before Ursina. Please create an instance of Ursina first (app = Ursina())', self.line_definition)


        # make sure things get set in the correct order. both colliders and texture need the model to be set first.
        for key in ('model', 'origin', 'origin_x', 'origin_y', 'origin_z', 'collider', 'shader', 'texture', 'texture_scale', 'texture_offset'):
            if key in kwargs:
                setattr(self, key, kwargs[key])
                del kwargs[key]

        for key, value in kwargs.items():
            setattr(self, key, value)

        if self.enabled and hasattr(self, 'on_enable'):
            if isinstance(self.on_enable, Sequence):
                self.on_enable.start()
            elif callable(self.on_enable):
                self.on_enable()

        elif not self.enabled and hasattr(self, 'on_disable'):
            if isinstance(self.on_disable, Sequence):
                self.on_disable.start()
            elif callable(self.on_disable):
                self.on_disable()

        # look for @every decorator and start a looping Sequence for decorated method
        from ursina.ursinastuff import every, get_class_name
        for method in every.decorated_methods:
            if get_class_name(method._func) == self.types[0]:
                self.animations.append(Sequence(Func(method, self), Wait(method._every.interval), loop=True, started=True))


    def _list_to_vec(self, value):
        if isinstance(value, (int, float, complex)):
            return Vec3(value, value, value)

        if len(value) % 2 == 0:
            new_value = Vec2()
            for i in range(0, len(value), 2):
                new_value.add_x(value[i])
                new_value.add_y(value[i+1])

        if len(value) % 3 == 0:
            new_value = Vec3()
            for i in range(0, len(value), 3):
                new_value.add_x(value[i])
                new_value.add_y(value[i+1])
                new_value.add_z(value[i+2])

        return new_value


    def enable(self): # same as .enabled = True
        self.enabled = True

    def disable(self): # same as .enabled = False
        self.enabled = False


    def enabled_getter(self):
        return getattr(self, '_enabled', True)

    def enabled_setter(self, value):    # disabled entities will not be visible nor run code.
        if value and hasattr(self, 'on_enable') and not self.enabled:
            if isinstance(self.on_enable, Sequence):
                self.on_enable.start()
            elif callable(self.on_enable):
                self.on_enable()

        elif value is False and hasattr(self, 'on_disable') and self.enabled:
            if isinstance(self.on_disable, Sequence):
                self.on_disable.start()
            elif callable(self.on_disable):
                self.on_disable()


        if value is True:
            if hasattr(self, 'is_singleton') and not self.is_singleton():
                self.unstash()
        else:
            if hasattr(self, 'is_singleton') and not self.is_singleton():
                self.stash()

        self._enabled = value


    def model_getter(self):
        return getattr(self, '_model', None)

    def model_setter(self, value):  # set model with model='model_name' (without file type extension)
        if value is None:
            if self.model:
                self.model.removeNode()
                # print('removed model')
            self._model = value
            return

        if isinstance(value, NodePath): # pass procedural model
            if self.model and value != self.model:
                self.model.detachNode()
            self._model = value

        elif isinstance(value, str): # pass model asset name
            m = load_model(value, application.asset_folder)
            if not m:
                m = load_model(value, application.internal_models_compressed_folder)
            if m:
                if self.model is not None:
                    self.model.removeNode()

                m.name = value
                m.setPos(Vec3(0,0,0))
                self._model = m
                # if not value in mesh_importer.imported_meshes:
                #     print_info('loaded model successfully:', value)
            else:
                if application.raise_exception_on_missing_model:
                    raise ValueError(f"missing model: '{value}'")

                print_warning(f"missing model: '{value}'")
                return

        if self.model:
            self.model.reparentTo(self)
            self.model.setTransparency(TransparencyAttrib.M_dual)
            self.color = self.color # reapply color after changing model
            self.texture = self.texture # reapply texture after changing model
            self._vert_cache = None
            if isinstance(value, Mesh):
                if hasattr(value, 'on_assign'):
                    value.on_assign(assigned_to=self)


    def color_getter(self):
        return getattr(self, '_color', color.white)

    def color_setter(self, value):
        if isinstance(value, str):
            value = color.hex(value)

        if not isinstance(value, Vec4):
            value = Vec4(value[0], value[1], value[2], value[3])

        self._color = value

        if self.model:
            # print('SET COLOR TO', value, self.name)
            self.model.setColorScaleOff() # prevent inheriting color from parent
            self.model.setColorScale(value)


    def eternal_getter(self):
        return getattr(self, '_eternal', False)
    def eternal_setter(self, value):    # eternal entities does not get destroyed on scene.clear()
        self._eternal = value
        for c in self.children:
            c.eternal = value

    def double_sided_getter(self):
        return getattr(self, '_double_sided', False)
    def double_sided_setter(self, value):
        self._double_sided = value
        self.setTwoSided(value)

    def render_queue_getter(self):
        return getattr(self, '_render_queue', 0)
    def render_queue_setter(self, value):   # for custom sorting in case of conflict. To sort things in 2d, set .z instead of using this.
        self._render_queue = value
        if self.model:
            self.model.setBin('fixed', value)


    def parent_getter(self):
        return getattr(self, '_parent', None)

    def parent_setter(self, value):
        if hasattr(self, '_parent') and self._parent and hasattr(self._parent, '_children') and self in self._parent._children:
            self._parent._children.remove(self)

        if hasattr(value, '_children') and self not in value._children:
            value._children.append(self)

        self.reparent_to(value)
        self.enabled = self.enabled   # parenting will undo the .stash() done when setting .enabled to False, so reapply it here
        self._parent = value


    def world_parent_getter(self):
        return getattr(self, '_parent', None)

    def world_parent_setter(self, value):  # change the parent, but keep position, rotation and scale
        if hasattr(self, '_parent') and self._parent and hasattr(self._parent, '_children') and self in self._parent._children:
            self._parent._children.remove(self)

        if hasattr(value, '_children') and self not in value._children:
            value._children.append(self)

        self.wrtReparentTo(value)
        self.enabled = self._enabled   # parenting will undo the .stash() done when setting .enabled to False, so reapply it here
        self._parent = value


    @property
    def types(self): # get all class names including those this inhertits from.
        from inspect import getmro
        return [c.__name__ for c in getmro(self.__class__)]


    def visible_getter(self):
        return getattr(self, '_visible', True)

    def visible_setter(self, value):
        self._visible = value
        if value:
            self.show()
        else:
            self.hide()

    def visible_self_getter(self): # set visibility of self, without affecting children.
        return getattr(self, '_visible_self', True)

    def visible_self_setter(self, value):
        self._visible_self = value
        if not self.model:
            return
        if value:
            self.model.show()
        else:
            self.model.hide()


    def collider_getter(self):
        return getattr(self, '_collider', None)

    def collider_setter(self, value):   # set to 'box'/'sphere'/'capsule'/'mesh' for auto fitted collider.
        if value is None and self.collider:
            self._collider.remove()
            self._collider = None
            self.collision = False
            return

        # destroy existing collider
        if value and self.collider:
            self._collider.remove()

        if isinstance(value, Collider):
            self._collider = value

        elif value == 'box':
            if self.model:
                _bounds = self.model_bounds
                self._collider = BoxCollider(entity=self, center=_bounds.center, size=_bounds.size)
            else:
                self._collider = BoxCollider(entity=self)
            self._collider.name = value

        elif value == 'sphere':
            self._collider = SphereCollider(entity=self, center=-self.origin)
            self._collider.name = value

        elif value == 'capsule':
            self._collider = CapsuleCollider(entity=self, center=-self.origin)
            self._collider.name = value

        elif value == 'mesh' and self.model:
            self._collider = MeshCollider(entity=self, mesh=self.model, center=-self.origin)
            self._collider.name = value

        elif isinstance(value, Mesh):
            self._collider = MeshCollider(entity=self, mesh=value, center=-self.origin)

        elif isinstance(value, str):
            m = load_model(value)
            if not m:
                self._collider = None
                self._collision = False
                return
            self._collider = MeshCollider(entity=self, mesh=m, center=-self.origin)
            self._collider.name = value


        self.collision = bool(self._collider)
        return

    def collision_getter(self):
        return getattr(self, '_collision', False)

    def collision_setter(self, value):  # toggle collision without changing collider.
        self._collision = value
        if not hasattr(self, 'collider') or not self.collider:
            if self in scene.collidables:
                scene.collidables.remove(self)
            return

        if value:
            self.collider.node_path.unstash()
            scene.collidables.add(self)
        else:
            self.collider.node_path.stash()
            if self in scene.collidables:
                scene.collidables.remove(self)

    def origin_getter(self):
        return getattr(self, '_origin', Vec3.zero)

    def origin_setter(self, value):
        if not isinstance(value, (Vec2, Vec3)):
            value = self._list_to_vec(value)
        if isinstance(value, Vec2):
            value = Vec3(*value, self.origin_z)

        self._origin = value

        if self.model:
            self.model.setPos(-value[0], -value[1], -value[2])


    def origin_x_getter(self):
        return self.origin[0]
    def origin_x_setter(self, value):
        self.origin = Vec3(value, self.origin_y, self.origin_z)

    def origin_y_getter(self):
        return self.origin[1]
    def origin_y_setter(self, value):
        self.origin = Vec3(self.origin_x, value, self.origin_z)

    def origin_z_getter(self):
        return self.origin[2]
    def origin_z_setter(self, value):
        self.origin = Vec3(self.origin_x, self.origin_y, value)

    def world_position_getter(self):
        return Vec3(self.get_position(scene))

    def world_position_setter(self, value):
        if not isinstance(value, (Vec2, Vec3)):
            value = self._list_to_vec(value)
        if isinstance(value, Vec2):
            value = Vec3(*value, self.z)

        self.setPos(scene, Vec3(value[0], value[1], value[2]))

    def world_x_getter(self):
        return self.getX(scene)
    def world_y_getter(self):
        return self.getY(scene)
    def world_z_getter(self):
        return self.getZ(scene)

    def world_x_setter(self, value):
        self.setX(scene, value)
    def world_y_setter(self, value):
        self.setY(scene, value)
    def world_z_setter(self, value):
        self.setZ(scene, value)

    def position_getter(self):
        return Vec3(*self.getPos())

    def position_setter(self, value):   # right, up, forward. can also set self.x, self.y, self.z
        if not isinstance(value, (Vec2, Vec3)):
            value = self._list_to_vec(value)
        if isinstance(value, Vec2):
            value = Vec3(*value, self.z)

        self.setPos(value[0], value[1], value[2])

    def x_getter(self):
        return self.getX()
    def x_setter(self, value):
        self.setX(value)

    def y_getter(self):
        return self.getY()
    def y_setter(self, value):
        self.setY(value)

    def z_getter(self):
        return self.getZ()
    def z_setter(self, value):
        self.setZ(value)

    @property
    def X(self):    # shortcut for int(entity.x)
        return int(self.x)
    @property
    def Y(self):    # shortcut for int(entity.y)
        return int(self.y)
    @property
    def Z(self):    # shortcut for int(entity.z)
        return int(self.z)

    def world_rotation_getter(self):
        rotation = self.getHpr(scene)
        return Vec3(rotation[1], rotation[0], rotation[2]) * Entity.rotation_directions

    def world_rotation_setter(self, value):
        self.setHpr(scene, Vec3(value[1], value[0], value[2]) * Entity.rotation_directions)

    def world_rotation_x_getter(self):
        return self.world_rotation[0]

    def world_rotation_x_setter(self, value):
        self.world_rotation = Vec3(value, self.world_rotation[1], self.world_rotation[2])

    def world_rotation_y_getter(self):
        return self.world_rotation[1]

    def world_rotation_y_setter(self, value):
        self.world_rotation = Vec3(self.world_rotation[0], value, self.world_rotation[2])

    def world_rotation_z_getter(self):
        return self.world_rotation[2]

    def world_rotation_z_setter(self, value):
        self.world_rotation = Vec3(self.world_rotation[0], self.world_rotation[1], value)

    def rotation_getter(self):
        rotation = self.getHpr()
        return Vec3(rotation[1], rotation[0], rotation[2]) * Entity.rotation_directions

    def rotation_setter(self, value):   # can also set self.rotation_x, self.rotation_y, self.rotation_z
        if not isinstance(value, (Vec2, Vec3)):
            value = self._list_to_vec(value)
        if isinstance(value, Vec2):
            value = Vec3(*value, self.rotation_z)

        self.setHpr(Vec3(value[1], value[0], value[2]) * Entity.rotation_directions)

    def rotation_x_getter(self):
        return self.rotation.x
    def rotation_x_setter(self, value):
        self.rotation = Vec3(value, self.rotation[1], self.rotation[2])

    def rotation_y_getter(self):
        return self.rotation.y
    def rotation_y_setter(self, value):
        self.rotation = Vec3(self.rotation[0], value, self.rotation[2])

    def rotation_z_getter(self):
        return self.rotation.z
    def rotation_z_setter(self, value):
        self.rotation = Vec3(self.rotation[0], self.rotation[1], value)

    def quaternion_getter(self):
        return self.get_quat()
    def quaternion_setter(self, value):
        self.set_quat(value)

    def world_scale_getter(self):
        return Vec3(*self.getScale(scene))
    def world_scale_setter(self, value):
        if isinstance(value, (int, float, complex)):
            value = Vec3(value, value, value)

        self.setScale(scene, value)

    def world_scale_x_getter(self):
        return self.getScale(scene)[0]
    def world_scale_x_setter(self, value):
        self.setScale(scene, Vec3(value, self.world_scale_y, self.world_scale_z))

    def world_scale_y_getter(self):
        return self.getScale(scene)[1]
    def world_scale_y_setter(self, value):
        self.setScale(scene, Vec3(self.world_scale_x, value, self.world_scale_z))

    def world_scale_z_getter(self):
        return self.getScale(scene)[2]
    def world_scale_z_setter(self, value):
        self.setScale(scene, Vec3(self.world_scale_x, self.world_scale_y, value))

    def scale_getter(self):
        scale = self.getScale()
        return Vec3(scale[0], scale[1], scale[2])

    def scale_setter(self, value):  # can also set self.scale_x, self.scale_y, self.scale_z
        if not isinstance(value, (Vec2, Vec3)):
            value = self._list_to_vec(value)
        if isinstance(value, Vec2):
            value = Vec3(*value, self.scale_z)

        value = [e if e!=0 else .001 for e in value]
        self.setScale(value[0], value[1], value[2])

    def scale_x_getter(self):
        return self.scale[0]
    def scale_x_setter(self, value):
        self.setScale(value, self.scale_y, self.scale_z)

    def scale_y_getter(self):
        return self.scale[1]
    def scale_y_setter(self, value):
        self.setScale(self.scale_x, value, self.scale_z)

    def scale_z_getter(self):
        return self.scale[2]
    def scale_z_setter(self, value):
        self.setScale(self.scale_x, self.scale_y, value)

    def transform_getter(self): # get/set position, rotation and scale
        return (self.position, self.rotation, self.scale)
    def transform_setter(self, value):
        self.position, self.rotation, self.scale = value

    def world_transform_getter(self): # get/set world_position, world_rotation and world_scale
        return (self.world_position, self.world_rotation, self.world_scale)
    def world_transform_setter(self, value):
        self.world_position, self.world_rotation, self.world_scale = value


    @property
    def forward(self): # get forward direction.
        return Vec3(*scene.getRelativeVector(self, (0, 0, 1)))
    @property
    def back(self): # get backwards direction.
        return -self.forward
    @property
    def right(self): # get right direction.
        return Vec3(*scene.getRelativeVector(self, (1, 0, 0)))
    @property
    def left(self): # get left direction.
        return -self.right
    @property
    def up(self): # get up direction.
        return Vec3(*scene.getRelativeVector(self, (0, 1, 0)))
    @property
    def down(self): # get down direction.
        return -self.up

    @property
    def screen_position(self): # get screen position(ui space) from world space.
        from ursina import camera
        p3d = camera.getRelativePoint(self, Vec3.zero)
        full = camera.lens.getProjectionMat().xform(Vec4(*p3d, 1))
        recip_full3 = 1
        if full[3] > 0:
            recip_full3 = 1 / full[3]
        p2d = full * recip_full3
        return Vec2(p2d[0]*camera.aspect_ratio/2, p2d[1]/2)

    def shader_getter(self):
        if not hasattr(self, '_shader'):
            return None
        return self._shader

    def shader_setter(self, value):
        if value is None:
            self._shader = value
            self.setShaderAuto()
            return

        if isinstance(value, Panda3dShader): #panda3d shader
            self._shader = value
            self.setShader(value)
            return

        if isinstance(value, str):
            if not value in shader.imported_shaders:
                print_warning('missing shader:', value)
                return

            value = shader.imported_shaders[value]

        if isinstance(value, Shader):
            self._shader = value
            if not value.compiled:
                value.compile()

            self.setShader(value._shader)
            value.entity = self

            for key, value in value.default_input.items():
                if callable(value):
                    value = value()
                self.set_shader_input(key, value)

            return

        raise ValueError(f'{value} is not a Shader')


    def get_shader_input(self, name):
        if name in self._shader_inputs:
            return self._shader_inputs[name]
        return None


    def set_shader_input(self, name, value):
        self._shader_inputs[name] = value
        if isinstance(value, str):
            value = load_texture(value)

        if isinstance(value, Texture):
            value = value._texture    # make sure to send the panda3d texture to the shader

        super().set_shader_input(name, value)


    def texture_getter(self):
        return getattr(self, '_texture', None)

    def texture_setter(self, value):    # set model with texture='texture_name'. requires a model to be set beforehand.
        if value is None and self.texture:
            # print('remove texture')
            self.model.clearTexture()
            self._texture = None
            return

        if isinstance(value, str):
            texture_name = value
            value = load_texture(value)
            # print('loaded texture:', texture)
            if value is None:
                if application.raise_exception_on_missing_texture:
                    raise ValueError(f"missing texture: '{texture_name}'")

                print_warning(f"missing texture: '{texture_name}'")
                return

        self.model.setTextureOff(False)
        if value.__class__ is MovieTexture:
            self._texture = value
            self.model.setTexture(value, 1)
            return

        self._texture = value
        if self.model and value is not None:
            self.model.setTexture(value._texture, 1)


    def texture_scale_getter(self):
        return getattr(self, '_texture_scale', Vec2(1,1))

    def texture_scale_setter(self, value):  # how many times the texture should repeat, eg. texture_scale=(8,8).
        self._texture_scale = Vec2(*value)
        if self.model and self.texture:
            self.model.setTexScale(TextureStage.getDefault(), value[0], value[1])
            self.set_shader_input('texture_scale', value)

    def texture_offset_getter(self):
        return getattr(self, '_texture_offset', Vec2(0,0))

    def texture_offset_setter(self, value):
        value = Vec2(*value)
        if self.model and self.texture:
            self.model.setTexOffset(TextureStage.getDefault(), value[0], value[1])
            self.texture = self.texture
            self.set_shader_input('texture_offset', value)
        self._texture_offset = value

    def tileset_size_getter(self):         # if the texture is a tileset, say how many tiles there are so it only use one tile of the texture, e.g. tileset_size=[8,4]
        return self._tileset_size
    def tileset_size_setter(self, value):
        self._tileset_size = value
        self.texture_scale = Vec2(1/value[0], 1/value[1])

    def tile_coordinate_getter(self):      # set the tile coordinate, starts in the lower left.
        return self._tile_coordinate
    def tile_coordinate_setter(self, value):
        self._tile_coordinate = value
        self.texture_offset = Vec2(value[0] / self.tileset_size[0], value[1] / self.tileset_size[1])


    def alpha_getter(self):
        return self.color[3]

    def alpha_setter(self, value):
        if value > 1:
            value = value / 255
        self.color = color.color(self.color.h, self.color.s, self.color.v, value)


    def always_on_top_getter(self):
        return getattr(self, '_always_on_top', False)
    def always_on_top_setter(self, value):
        self._always_on_top = value
        self.set_bin("fixed", 0)
        self.set_depth_write(not value)
        self.set_depth_test(not value)

    def unlit_getter(self):
        return getattr(self, '_unlit', False)

    def unlit_setter(self, value):
        self._unlit = value
        self.setLightOff(value)

    def billboard_getter(self): # set to True to make this Entity always face the camera.
        return getattr(self, '_billboard', False)

    def billboard_setter(self, value):
        self._billboard = value
        if value:
            self.setBillboardPointEye(value)

    def wireframe_getter(self):
        return getattr(self, '_wireframe', False)

    def wireframe_setter(self, value):
        self._wireframe = value
        self.setRenderModeWireframe(value)


    def generate_sphere_map(self, size=512, name=f'sphere_map_{len(scene.entities)}'):
        from ursina import camera
        _name = 'textures/' + name + '.jpg'
        org_pos = camera.position
        camera.position = self.position
        base.saveSphereMap(_name, size=size)
        camera.position = org_pos

        # print('saved sphere map:', name)
        self.model.setTexGen(TextureStage.getDefault(), TexGenAttrib.MEyeSphereMap)
        self.reflection_map = name


    def generate_cube_map(self, size=512, name=f'cube_map_{len(scene.entities)}'):
        from ursina import camera
        _name = 'textures/' + name
        org_pos = camera.position
        camera.position = self.position
        base.saveCubeMap(_name+'.jpg', size=size)
        camera.position = org_pos

        # print('saved cube map:', name + '.jpg')
        self.model.setTexGen(TextureStage.getDefault(), TexGenAttrib.MWorldCubeMap)
        self.reflection_map = _name + '#.jpg'
        self.model.setTexture(loader.loadCubeMap(_name + '#.jpg'), 1)


    @property
    def model_bounds(self):
        if self.model:
            start, end = self.model.getTightBounds()
            start = Vec3(start)
            end = Vec3(end)
            center = (start + end) / 2
            size = end - start
            return Bounds(start=start, end=end, center=center, size=size)

        return Vec3(0,0,0)


    @property
    def bounds(self):
        _bounds = self.model_bounds
        return Bounds(start=_bounds.start*self.scale, end=_bounds.end*self.scale, center=_bounds.center, size=_bounds.size*self.scale)


    def get_position(self, relative_to=scene):  # get position relative to on other Entity. In most cases, use .position instead.
        return self.getPos(relative_to)


    def set_position(self, value, relative_to=scene): # set position relative to on other Entity. In most cases, use .position instead.
        self.setPos(relative_to, Vec3(value[0], value[1], value[2]))


    def rotate(self, value, relative_to=None):  # rotate around local axis.
        if not relative_to:
            relative_to = self

        self.setHpr(relative_to, Vec3(value[1], value[0], value[2]) * Entity.rotation_directions)


    def add_script(self, class_instance):
        if isinstance(class_instance, object) and not isinstance(class_instance, str):
            class_instance.entity = self
            class_instance.enabled = True
            self.scripts.append(class_instance)
            if hasattr(class_instance, 'on_script_added') and callable(class_instance.on_script_added):
                class_instance.on_script_added()
            # print('added script:', camel_to_snake(name.__class__.__name__))
            return class_instance


    def combine(self, analyze=False, auto_destroy=True, ignore=[]):
        from ursina.scripts.combine import combine

        self.model = combine(self, analyze, auto_destroy, ignore)
        return self.model


    def flipped_faces_getter(self):
        return getattr(self, '_flipped_faces', False)
    def flipped_faces_setter(self, value):
        self._flipped_faces = value
        if value:
            self.setAttrib(CullFaceAttrib.make(CullFaceAttrib.MCullClockwise))
        else:
            self.setAttrib(CullFaceAttrib.make(CullFaceAttrib.MCullCounterClockwise))


    def look_at(self, target, axis='forward', up=None): # up defaults to self.up
        from panda3d.core import Quat
        if isinstance(target, Entity):
            target = Vec3(*target.world_position)
        elif not isinstance(target, Vec3):
            target = Vec3(*target)

        up_axis = self.up
        if up:
            up_axis = up
        self.lookAt(target, up_axis)

        if axis == 'forward':
            return

        rotation_offset = {
            'back'    : Quat(0,0,1,0),
            'down'    : Quat(-.707,.707,0,0),
            'up'      : Quat(-.707,-.707,0,0),
            'right'   : Quat(-.707,0,.707,0),
            'left'    : Quat(-.707,0,-.707,0),
            }[axis]

        self.setQuat(rotation_offset * self.getQuat())


    def look_at_2d(self, target, axis='z'):
        from math import degrees, atan2
        if isinstance(target, Entity):
            target = Vec3(target.world_position)

        pos = target - self.world_position
        if axis == 'z':
            self.rotation_z = degrees(atan2(pos[0], pos[1]))
        elif axis == 'y':
            self.rotation_y = degrees(atan2(pos[0], pos[2]))
        elif axis == 'x':
            self.rotation_x = degrees(atan2(pos[1], pos[2]))

    def look_at_xy(self, target):
        self.look_at_2d(target)
    def look_at_xz(self, target):
        self.look_at_2d(target, 'y')


    def has_ancestor(self, possible_ancestor):
        if self.parent == possible_ancestor:
            return True

        p = self
        if isinstance(possible_ancestor, Entity):
            for i in range(100):
                if p.parent:
                    if p.parent == possible_ancestor:
                        return True

                    p = p.parent

        return False

    def has_disabled_ancestor(self):
        p = self
        for i in range(100):
            if not p.parent:
                return False
            if not hasattr(p, 'parent') or not hasattr(p.parent, 'enabled'):
                return False

            p = p.parent

            if p.enabled is False:
                return True

        return False

    def children_getter(self):
        return [e for e in getattr(self, '_children', []) if e]     # make sure list doesn't contain destroyed entities

    def children_setter(self, value):
        self._children = value


    @property
    def attributes(self): # attribute names. used by duplicate().
        return ('name', 'enabled', 'eternal', 'visible', 'parent',
            'origin', 'position', 'rotation', 'scale',
            'model', 'color', 'texture', 'texture_scale', 'texture_offset',
            'render_queue', 'always_on_top', 'collider', 'collision', 'scripts')

    def __str__(self):
        try:
            return self.name
        except:
            return '*destroyed entity*'

    def get_changes(self, target_class=None): # returns a dict of all the changes
        if not target_class:
            target_class = self.__class__

        changes = dict()
        for key, value in target_class.default_values.items():
            attr = getattr(self, key)
            if attr == target_class.default_values[key]:
                continue

            if hasattr(attr, '__name__'):
                attr = attr.__name__
                changes[key] = attr
                continue

            if attr and hasattr(attr, 'name') and attr.name and isinstance(attr.name, str):
                attr = attr.name
                if '.' in attr:
                    attr = attr.split('.')[0]

            # print('attr changed:', key, 'from:', target_class.default_values[key], 'to:', attr)
            if key == 'color':
                if isinstance(attr, str):
                    if not attr.startswith('#'):
                        attr = f'color.{attr}'
                elif isinstance(attr, Color):
                    attr = f"'{color.rgb_to_hex(*attr)}'"

            elif isinstance(attr, str):
                attr = f"'{attr}'"

            if attr == "'mesh'":
                continue

            changes[key] = attr
        return changes



    def __repr__(self):
        changes = self.get_changes(self.__class__)
        return f'{self.__class__.__name__}(' +  ''.join(f'{key}={value}, ' for key, value in changes.items()) + ')'


    def __deepcopy__(self, memo):
        return eval(repr(self))


#------------
# ANIMATIONS
#------------
    def animate(self, name, value, duration=.1, delay=0, curve=curve.in_expo, loop=False, resolution=None, interrupt='kill', time_step=None, unscaled=False, auto_play=True, auto_destroy=True):
        if duration == 0 and delay == 0:
            setattr(self, name, value)
            return None

        if delay:
            from ursina.ursinastuff import invoke
            return invoke(self.animate, name, value, duration=duration, curve=curve, loop=loop, resolution=resolution, time_step=time_step, auto_destroy=auto_destroy, delay=delay, unscaled=unscaled)

        animator_name = name + '_animator'
        # print('start animating value:', name, animator_name )
        if interrupt and hasattr(self, animator_name):
            getattr(getattr(self, animator_name), interrupt)() # call kill() or finish() depending on what the interrupt value is.
            # print('interrupt', interrupt, animator_name)
        if hasattr(self, animator_name) and getattr(self, animator_name) in self.animations:
            self.animations.remove(getattr(self, animator_name))

        sequence = Sequence(loop=loop, time_step=time_step, auto_destroy=auto_destroy, unscaled=unscaled)

        setattr(self, animator_name, sequence)
        self.animations.append(sequence)

        if not resolution:
            resolution = max(int(duration * 60), 1)

        for i in range(resolution+1):
            t = i / resolution
            t = curve(t)

            sequence.append(Wait(duration / resolution))
            sequence.append(Func(setattr, self, name, lerp(getattr(self, name), value, t)))

        if auto_play:
            sequence.start()
        return sequence

    def animate_position(self, value, duration=.1, **kwargs):
        x = self.animate('x', value[0], duration,  **kwargs)
        y = self.animate('y', value[1], duration,  **kwargs)
        z = None
        if len(value) > 2:
            z = self.animate('z', value[2], duration, **kwargs)
        return x, y, z

    def animate_rotation(self, value, duration=.1,  **kwargs):
        x = self.animate('rotation_x', value[0], duration,  **kwargs)
        y = self.animate('rotation_y', value[1], duration,  **kwargs)
        z = self.animate('rotation_z', value[2], duration,  **kwargs)
        return x, y, z

    def animate_scale(self, value, duration=.1, **kwargs):
        if isinstance(value, (int, float, complex)):
            value = Vec3(value, value, value)
        elif isinstance(value, tuple) and len(value) == 2:
            value = Vec3(*value, self.z)

        return self.animate('scale', value, duration=duration, **kwargs)

    # generate animation functions
    for e in ('x', 'y', 'z', 'rotation_x', 'rotation_y', 'rotation_z', 'scale_x', 'scale_y', 'scale_z'):
        exec(dedent(f'''
            def animate_{e}(self, value, duration=.1, delay=0, unscaled=False, **kwargs):
                return self.animate('{e}', value, duration=duration, delay=delay, unscaled=unscaled, **kwargs)
        '''))


    def shake(self, duration=.2, magnitude=1, speed=.05, direction=(1,1), delay=0, attr_name='position', interrupt='finish', unscaled=False):
        import random

        if hasattr(self, 'shake_sequence') and self.shake_sequence:
            getattr(getattr(self, 'shake_sequence'), interrupt)()

        self.shake_sequence = Sequence(Wait(delay))
        original_position = getattr(self, attr_name)

        for i in range(int(duration / speed)):
            self.shake_sequence.append(Func(setattr, self, attr_name,
                Vec3(
                    original_position[0] + (random.uniform(-.1, .1) * magnitude * direction[0]),
                    original_position[1] + (random.uniform(-.1, .1) * magnitude * direction[1]),
                    original_position[2],
                )))
            self.shake_sequence.append(Wait(speed))
            self.shake_sequence.append(Func(setattr, self, attr_name, original_position))

        self.animations.append(self.shake_sequence)
        self.shake_sequence.unscaled = unscaled
        self.shake_sequence.start()
        return self.shake_sequence

    def animate_color(self, value, duration=.1, interrupt='finish', unscaled=False, **kwargs):
        return self.animate('color', value, duration, interrupt=interrupt, unscaled=unscaled,**kwargs)

    def fade_out(self, value=0, duration=.5, unscaled=False, **kwargs):
        return self.animate('color', Vec4(self.color[0], self.color[1], self.color[2], value), duration=duration, unscaled=unscaled, **kwargs)

    def fade_in(self, value=1, duration=.5, unscaled=False, **kwargs):
        return self.animate('color', Vec4(self.color[0], self.color[1], self.color[2], value), duration=duration, unscaled=unscaled, **kwargs)

    def blink(self, value=ursina.color.clear, duration=.1, delay=0, curve=curve.in_expo_boomerang, interrupt='finish', unscaled=False, **kwargs):
        return self.animate_color(value, duration=duration, delay=delay, curve=curve, interrupt=interrupt, unscaled=unscaled, **kwargs)



    def intersects(self, traverse_target=scene, ignore:list=None, debug=False):
        if not ignore:
            ignore = []
        ignore = list(ignore)

        if isinstance(self.collider, MeshCollider):
            raise Exception('''error: mesh colliders can't intersect other shapes, only primitive shapes can. Mesh colliders can "receive" collisions though.''')

        from ursina.hit_info import HitInfo

        if not self.collision or not self.collider:
            self.hit = HitInfo(hit=False)
            return self.hit

        from ursina import distance
        if not hasattr(self, '_picker'):
            from panda3d.core import CollisionTraverser, CollisionNode, CollisionHandlerQueue

            self._picker = CollisionTraverser()  # Make a traverser
            self._pq = CollisionHandlerQueue()  # Make a handler
            self._pickerNode = CollisionNode('raycaster')
            self._pickerNode.set_into_collide_mask(0)
            self._pickerNP = self.attach_new_node(self._pickerNode)
            self._picker.addCollider(self._pickerNP, self._pq)
            self._pickerNP.show()
            self._pickerNode.addSolid(self._collider.shape)

        if debug:
            self._pickerNP.show()
        else:
            self._pickerNP.hide()

        self._picker.traverse(traverse_target)

        if self._pq.get_num_entries() == 0:
            self.hit = HitInfo(hit=False)
            return self.hit

        ignore.append(self)
        ignore.extend((e for e in scene.entities if not e.collision))

        self._pq.sort_entries()
        entries = self._pq.getEntries()
        entities = [e.get_into_node_path().parent for e in entries]

        entries = [        # filter out ignored entities
            e for i, e in enumerate(entries)
            if entities[i] in scene.collidables
            and entities[i] not in ignore
            ]

        if len(entries) == 0:
            return HitInfo(hit=False)

        hit_info = HitInfo(hit=True)
        hit_info.entities = [e.get_into_node_path().parent.getPythonTag('Entity') for e in entries]
        hit_info.entity = hit_info.entities[0]

        collision = entries[0]
        # nP = collision.get_into_node_path().parent
        hit_info.point = Vec3(*collision.get_surface_point(hit_info.entity))
        hit_info.world_point = Vec3(*collision.get_surface_point(scene))
        hit_info.distance = distance(self.world_position, hit_info.world_point)
        hit_info.normal = Vec3(*collision.get_surface_normal(collision.get_into_node_path().parent).normalized())
        hit_info.world_normal = Vec3(*collision.get_surface_normal(scene).normalized())

        return hit_info


    def __setattr__(self, name, value):
        try:
            super().__setattr__(name, value)
        except:
            pass
            # print('failed to set attribute:', name)



if __name__ == '__main__':
    from ursina import *
    app = Ursina()

    e = Entity(model='quad', color=color.orange, position=(0,0,1), scale=1.5, rotation=(0,0,45), texture='brick')

    '''example of inheriting Entity'''
    class Player(Entity):
        def __init__(self, **kwargs):
            super().__init__()
            self.model='cube'
            self.color = color.red
            self.scale_y = 2

            for key, value in kwargs.items():
                setattr(self, key, value)

        # input and update functions gets automatically called by the engine
        def input(self, key):
            if key == 'space':
                # self.color = self.color.inverse()
                self.animate_x(2, duration=1)

        def update(self):
            self.x += held_keys['d'] * time.dt * 10
            self.x -= held_keys['a'] * time.dt * 10

    player = Player(x=-1)


    # test
    e = Entity(model='cube', collider='box', texture='shore', color=hsv(.3,1,.5))
    print(repr(e))
    # a = Entity()
    # b = Entity(parent=a)


    # e.animate_x(3, duration=2, delay=.5, loop=True)
    # e.animate_position(Vec3(1,1,1), duration=1, loop=True)
    # e.animate_rotation(Vec3(45,45,45))
    # e.animate_scale(2, duration=1, curve=curve.out_expo_boomerang, loop=True)
    # e.animate_color(color.green, loop=True)
    # e.shake()
    # e.fade_out(delay=.5)
    # e.fade_in(delay=2.5)
    # e.blink(color.red, duration=1, curve=curve.linear_boomerang, loop=True)

    app.run()
