from ursina import *
from ursina.shaders import unlit_shader, lit_with_shadows_shader, matcap_shader, triplanar_shader, normals_shader
from time import perf_counter
import csv
import builtins
import pyperclip
import inspect
import sys

class LevelEditor(Entity):
    def __init__(self, **kwargs):
        super().__init__()
        builtins.LEVEL_EDITOR = self
        self.scene_folder = application.asset_folder / 'scenes'
        self.scenes = [[LevelEditorScene(x, y, f'untitled_scene[{x},{y}]') for y in range(8)] for x in range(8)]
        self.current_scene = None

        self.grid = Entity(parent=self, model=Grid(16,16), rotation_x=90, scale=64, collider='box', color=color.white33, enabled=False)
        self.origin_mode = 'center'
        self.editor_camera = EditorCamera(parent=self, rotation_x=20, eternal=False, rotation_smoothing=0)
        original_editor_camera_rotation_speed = self.editor_camera.rotation_speed
        def _update():
            self.editor_camera.rotation_speed = original_editor_camera_rotation_speed * int(not mouse.left)   # don't rotate when holding left mouse button
        Entity(parent=self.editor_camera, update=_update)


        self.ui = Entity(parent=camera.ui, name='LEVEL_EDITOR.ui')
        self.point_renderer = Entity(parent=self, model=Mesh([], mode='point', thickness=.1, render_points_in_3d=True), texture='circle_outlined', always_on_top=True, unlit=True, render_queue=1)
        self.cubes = [Entity(wireframe=True, color=color.azure, parent=self, enabled=True) for i in range(128)] # max selection

        self.origin_mode_menu = ButtonGroup(['last', 'center', 'individual'], min_selection=1, position=window.top_left+Vec2(.45,0), parent=self.ui)
        self.origin_mode_menu.scale *= .5
        self.origin_mode_menu.on_value_changed = self.render_selection
        self.local_global_menu = ButtonGroup(['local', 'global'], default='global', min_selection=1, position=window.top_left+Vec2(.25,0), parent=self.ui)
        self.local_global_menu.scale *= .5
        self.local_global_menu.on_value_changed = self.render_selection
        # self.play_button = Button(parent=camera.ui, position=window.top, scale=.02, text='p', origin_y=.5)
        # def play_button_on_click():
        #     self.edit_mode = not self.edit_mode
        # self.play_button.on_click = play_button_on_click

        self.target_fov = 90

        self.sun_handler = SunHandler()
        self.sky = Sky(parent=scene)
        self.gizmo = Gizmo()
        self.rotation_gizmo = RotationGizmo()
        self.scale_gizmo = ScaleGizmo()
        self.box_gizmo = BoxGizmo()
        self.gizmo_toggler = GizmoToggler()

        self.quick_grabber = QuickGrabber()   # requires gizmo, selector
        self.quick_scaler = QuickScaler()    # requires scale_gizmo, gizmo_toggler, selector
        self.quick_rotator = QuickRotator()
        self.rotate_to_view = RotateRelativeToView(target_entity=None)
        self.selector = Selector()
        self.selection_box = SelectionBox(model=Quad(0, mode='line'), origin=(-.5,-.5,0), scale=(0,0,1), color=color.white33, mode='new')

        self.prefab_folder = application.asset_folder / 'prefabs'
        from ursina.editor.prefabs.poke_shape import PokeShape
        # from ursina.editor.prefabs.sliced_cube import SlicedCube
        # print('-----------------', PokeShape)
        self.built_in_prefabs = [ClassSpawner, WhiteCube, TriplanarCube, Pyramid, PokeShape]
        self.prefabs = []
        self.spawner = Spawner()
        self.deleter = Deleter()
        self.grouper = Grouper()
        self.level_menu = LevelMenu()
        self.goto_scene = self.level_menu.goto_scene
        self.duplicator = Duplicator()
        self.copier = Copier()

        self.model_menu = ModelMenu()
        self.texture_menu = TextureMenu()
        self.color_menu = ColorMenu()
        self.shader_menu = ShaderMenu()
        self.collider_menu = ColliderMenu()
        self.class_menu = ClassMenu()
        self.menu_handler = MenuHandler()
        self.right_click_menu = RightClickMenu()
        self.hierarchy_list = HierarchyList()
        self.inspector = Inspector()
        self.point_of_view_selector = PointOfViewSelector()
        self.help = Help()

        self._edit_mode = True

    def add_entity(self, entity):
        for key, value in dict(original_parent=LEVEL_EDITOR, selectable=True, collision=False, collider_type='None').items():
            # print('set', key, value)
            if not hasattr(entity, key):
                setattr(entity, key, value)

        entity.parent = LEVEL_EDITOR.current_scene.scene_parent
        LEVEL_EDITOR.current_scene.entities.append(entity)


    @property
    def entities(self):
        if not self.current_scene:
            return []
        return self.current_scene.entities

    @entities.setter
    def entities(self, value):
        if not self.current_scene:
            return
        self.current_scene.entities = value

    @property
    def selection(self):
        if not self.current_scene:
            return []
        return self.current_scene.selection

    @selection.setter
    def selection(self, value):
        if not self.current_scene:
            return
        self.current_scene.selection = value

    def on_enable(self):
        self._camera_original_fov = camera.fov
        camera.fov = self.target_fov

    def on_disable(self):
        camera.fov = self._camera_original_fov

    def update(self):
        for key in 'gsxyz':
            if held_keys[key]:
                self.render_selection()
                return

        if mouse.left:
            self.render_selection()


    def input(self, key):
        combined_key = input_handler.get_combined_key(key)
        if combined_key == 'control+s':
            if not self.current_scene:
                print("no current_scene, can't save")
                return

            self.current_scene.save()

        if self.current_scene:
            if combined_key == 'control+z':
                self.current_scene.undo.undo()
            elif combined_key == 'control+y':
                self.current_scene.undo.redo()

        if self.selection and combined_key == 'f':
            self.editor_camera.animate_position(self.gizmo.world_position, duration=.1, curve=curve.linear)

        elif combined_key == 'control+e':
            self.edit_mode = not self.edit_mode

        elif combined_key == 'control++':
            for e in self.ui.children:
                e.scale *= 1.1
        elif combined_key == 'control+-':
            for e in self.ui.children:
                e.scale /= 1.1


    @property
    def edit_mode(self):
        return self._edit_mode

    @edit_mode.setter
    def edit_mode(self, value):
        if not self.current_scene:
            return

        if not value and self._edit_mode:   # enter play mode
            for e in self.children:
                e.ignore = True
                e.visible = False

            self.editor_camera.original_target_z = self.editor_camera.target_z
            self.editor_camera.enabled = False
            self.ui.enabled = False
            for e in self.current_scene.entities:
                # switch editor colliders out for what they should have during play
                if hasattr(e, 'edit_mode') and e.edit_mode:
                    e.edit_mode = False

                e.editor_collider = e.collider
                if e.collider:
                    e.editor_collider = e.collider.name

                if hasattr(e, 'collider_type') and e.collider_type != 'None':
                    e.collider = e.collider_type
                else:
                    e.collider = None


                if hasattr(e, 'start') and callable(e.start):
                    e.start()

        elif value and not self._edit_mode: # back to editor mode
            self.ui.enabled = True
            for e in self.current_scene.entities:
                if hasattr(e, 'stop') and callable(e.stop):
                    e.stop()
                e.collider = e.editor_collider

            for e in self.children:
                e.ignore = False
                e.visible = True

            self.editor_camera.enabled = True
            self.editor_camera.target_z = self.editor_camera.original_target_z
            camera.z = self.editor_camera.target_z

        self._edit_mode = value
        # print('set edit mode to', value)


    def render_selection(self, update_gizmo_position=True):
        for i, e in enumerate(self.entities):
            if e == None:
                print(f'error in entities {i}, is {e}')
                self.entities.remove(e)

        # self.point_renderer.model.vertices = [e.world_position for e in self.entities if e.selectable and not e.model ]
        self.point_renderer.model.vertices = []
        self.point_renderer.model.colors = []

        for e in self.entities:
            if not e or e.model and e.model.name == 'cube':
                continue

            self.point_renderer.model.vertices.append(e.world_position)

            if not e in self.selection:
                gizmo_color = color.orange
                if hasattr(e.__class__, 'gizmo_color'):
                    gizmo_color = e.__class__.gizmo_color

                self.point_renderer.model.colors.append(gizmo_color)
            else:
                gizmo_color = color.azure
                if hasattr(e.__class__, 'gizmo_color_selected'):
                    gizmo_color = e.__class__.gizmo_color_selected
                self.point_renderer.model.colors.append(gizmo_color)


        # self.point_renderer.model.colors = [color.azure if e in self.selection else lerp(color.orange, color.hsv(0,0,1,0), distance(e.world_position, camera.world_position)/100) for e in self.entities if e.selectable and not e.collider]
        self.point_renderer.model.triangles = []
        # print('--------------', len(self.point_renderer.model.vertices), len(self.point_renderer.model.colors), self.point_renderer.model.serialize())
        self.point_renderer.model.generate()

        # self.gizmo.enabled = bool(self.selection and self.selection[-1])
        self.selection = [e for e in self.selection if e]

        if update_gizmo_position and self.selection:
            if self.origin_mode_menu.value in ('last', 'individual'):
                self.gizmo.world_position = self.selection[-1].world_position
            elif self.origin_mode_menu.value == 'center':
                self.gizmo.world_position = sum([e.world_position for e in self.selection]) / len(self.selection)

            if self.local_global_menu.value == 'local' and self.origin_mode_menu.value == 'last':
                self.gizmo.world_rotation = self.selection[-1].world_rotation
            else:
                self.gizmo.world_rotation = Vec3(0,0,0)

        [e.disable() for e in self.cubes]
        # [setattr(e, 'parent', self) for e in self.cubes]
        for i, e in enumerate([e for e in self.selection if e.collider]):
            if i < len(self.cubes):
                self.cubes[i].world_transform = e.world_transform
                self.cubes[i].origin = e.origin
                self.cubes[i].model = copy(e.model)
                self.cubes[i].enabled = True

        # print('---------- rendered selection')

        LEVEL_EDITOR.hierarchy_list.render_selection()


    def on_enable(self):
        if hasattr(self, 'ui'):
            self.ui.enabled = True

    def on_disable(self):
        self.ui.enabled = False

class ErrorEntity(Entity):
    def __init__(self, model='wireframe_cube', color=color.red, **kwargs):
        super().__init__(model=model, color=color, **kwargs)

class LevelEditorScene:
    def __init__(self, x, y, name, **kwargs):
        super().__init__()
        self.coordinates = [x,y]
        self.name = name
        self.path = None    # must be assigned to be able to load
        self.entities = []
        self.selection = []
        self.scene_parent = None
        self.undo = Undo()
        # self.undo_handler     # gets assigned later

    def save(self):
        if not self.path and not self.entities:
            print('cant save scene with not path and no entities')
            return

        LEVEL_EDITOR.scene_folder.mkdir(parents=True, exist_ok=True)
        list_of_dicts = []

        fields = ['class', ]
        for e in LEVEL_EDITOR.current_scene.entities:
            if hasattr(e, 'is_gizmo'):
                continue

            changes = e.get_changes(e.__class__)
            for key, value in changes.items():
                if value == None:
                    changes[key] = False
            # if 'subdivisions' in changes:
            changes['class'] = e.__class__.__name__
            if hasattr(e, 'collider_type'):
                changes['collider_type'] = f"'{e.collider_type}'"
            print('changes:', changes)
            list_of_dicts.append(changes)
            for key in changes.keys():
                if key not in fields:
                    fields.append(key)


        name = LEVEL_EDITOR.current_scene.name
        self.path =  LEVEL_EDITOR.scene_folder / f'{name}.csv'

        with self.path.open('w', encoding='UTF8') as file:
            writer = csv.DictWriter(file, fieldnames=fields, delimiter=';')
            writer.writeheader()
            writer.writerows(list_of_dicts)

        print('saved:', self.path)


    def load(self):
        if not self.path:
            print('cant load scene, no path')
            return
        if self.scene_parent:
            print('error, scene already loaded')
            return

        # get all imported classes
        imported_classes = dict()
        for _module_name, module in list(sys.modules.items()):
            if hasattr(module, '__file__') and module.__file__ and not module.__file__.startswith(sys.prefix):  # filter out built-in modules and modules from the standard library
                for _, obj in inspect.getmembers(module):
                    if inspect.isclass(obj):
                        imported_classes[obj.__name__] = obj

        t = perf_counter()
        with self.path.open('r') as f:
            self.scene_parent = Entity()
            reader = csv.DictReader(f, delimiter=';')
            _fields = reader.fieldnames[1:]

            for line in reader:
                kwargs = {key : value for key, value in line.items() if value and not key == 'class'}
                if not 'parent' in kwargs:
                    kwargs['parent'] = self.scene_parent

                for key, value in kwargs.items():
                    if key == 'parent':
                        continue

                    try:
                        value = eval(value)
                        kwargs[key] = value
                    except:
                        pass

                if not line["class"] in imported_classes:
                    target_class = ErrorEntity
                else:
                    target_class = imported_classes[line["class"]]


                try:
                    instance = target_class(**kwargs)
                except:
                    instance = ErrorEntity()

                self.entities.append(instance)


                for e in self.entities:
                    if not e.shader:
                        e.shader = lit_with_shadows_shader
                    e.selectable = True

                    if not hasattr(e, 'collider_type'):
                        e.collider_type = None

                    e.original_parent = e.parent
                    if e.model and e.model.name == 'cube':
                        e.collider = 'box'
                        e.collision = False


        if self.scene_parent:
            print(f'loaded scene: "{self.name}" in {perf_counter()-t}')
            return self.scene_parent


    def unload(self):
        [setattr(e, 'parent', LEVEL_EDITOR) for e in LEVEL_EDITOR.cubes]
        [destroy(e) for e in self.entities]
        # if not self.scene_parent:
        #     # print('cant unload scene, its already empty')
        #     return

        self.selection = []
        self.entities = []
        if self.scene_parent:
            destroy(self.scene_parent)


class Undo(Entity):
    def __init__(self, **kwargs):
        super().__init__(parent=LEVEL_EDITOR, undo_data=[], undo_index=-1)

    def record_undo(self, data):
        print('record undo:', data)
        self.undo_data = self.undo_data[:self.undo_index+1]
        self.undo_data.append(data)
        self.undo_index += 1

    def undo(self):
        if self.undo_index < 0:
            return

        current_undo_data = self.undo_data[self.undo_index]

        if current_undo_data[0] == 'restore entities':     # restore deleted entity
            for id, recipe in zip(current_undo_data[1], current_undo_data[2]):
                # print('------------', recipe)
                clone = eval(recipe)
                clone.selectable = True
                clone.original_parent = clone.parent
                clone.shader = lit_with_shadows_shader
                # print('------------', recipe, id, 'clone:', clone)
                LEVEL_EDITOR.entities.insert(id, clone)

        elif current_undo_data[0] == 'delete entities': # delete newly created entity
            target_entities = [LEVEL_EDITOR.entities[id] for id in current_undo_data[1]]
            [LEVEL_EDITOR.selection.remove(e) for e in target_entities if e in LEVEL_EDITOR.selection]
            [setattr(e, 'parent', LEVEL_EDITOR) for e in LEVEL_EDITOR.cubes]
            [LEVEL_EDITOR.entities.remove(e) for e in target_entities]
            [destroy(e) for e in target_entities]

        else:
            for data in current_undo_data:
                id, attr, original, new = data
                setattr(LEVEL_EDITOR.entities[id], attr, original)

        LEVEL_EDITOR.render_selection()     # make sure the gizmo position updates
        self.undo_index -= 1

    def redo(self):
        if self.undo_index+2 > len(self.undo_data):
            return

        current_undo_data = self.undo_data[self.undo_index+1]

        # do the same as for undo, but opposite
        if current_undo_data[0] == 'delete entities':     # delete entity
            # pass
            for id, recipe in zip(current_undo_data[1], current_undo_data[2]):
                clone = eval(recipe)
                clone.selectable = True
                clone.original_parent = clone.parent
                clone.shader = lit_with_shadows_shader
                LEVEL_EDITOR.entities.insert(id, clone)

        elif current_undo_data[0] == 'restore entities': # restore entity
            pass
            target_entities = [LEVEL_EDITOR.entities[id] for id in current_undo_data[1]]
            [LEVEL_EDITOR.selection.remove(e) for e in target_entities if e in LEVEL_EDITOR.selection]
            [setattr(e, 'parent', LEVEL_EDITOR) for e in LEVEL_EDITOR.cubes]
            [LEVEL_EDITOR.entities.remove(e) for e in target_entities if e in LEVEL_EDITOR.entities]
            [destroy(e) for e in target_entities]


        else:
            for data in current_undo_data:
                id, attr, original, new = data
                setattr(LEVEL_EDITOR.entities[id], attr, new)

        LEVEL_EDITOR.render_selection()     # make sure the gizmo position updates
        self.undo_index += 1


axis_colors = {
    'x' : color.magenta,
    'y' : color.yellow,
    'z' : color.cyan
}

if not load_model('arrow', application.internal_models_compressed_folder):
    p = Entity(enabled=False)
    Entity(parent=p, model='cube', scale=(1,.05,.05))
    Entity(parent=p, model=Cone(4), x=.5, scale=.2, rotation=(0,90,0))
    arrow_model = p.combine()
    arrow_model.save('arrow.ursinamesh', folder=application.internal_models_compressed_folder, max_decimals=4)

if not load_model('scale_gizmo', application.internal_models_compressed_folder):
    p = Entity(enabled=False)
    Entity(parent=p, model='cube', scale=(.05,.05,1))
    Entity(parent=p, model='cube', z=.5, scale=.2)
    arrow_model = p.combine()
    arrow_model.save('scale_gizmo.ursinamesh', folder=application.internal_models_compressed_folder, max_decimals=4)


class GizmoArrow(Draggable):
    def __init__(self, model='arrow', collider='box', **kwargs):
        super().__init__(model=model, origin_x=-.55, always_on_top=True, render_queue=1, is_gizmo=True, shader=unlit_shader, **kwargs)
        for key, value in kwargs.items():
            setattr(self, key, value)

        self.record_undo = True     # this can be set to False when moving this though code for example, and you don't want it to record undo.
        self.original_rotation = self.rotation


    def drag(self):
        self.world_parent = LEVEL_EDITOR
        LEVEL_EDITOR.gizmo.world_parent = self
        for e in LEVEL_EDITOR.selection:
            if not hasattr(e.parent, 'is_gizmo') or e.parent.is_gizmo == False:
                e.original_parent = e.parent
            else:
                e.original_parent = scene

            if LEVEL_EDITOR.local_global_menu.value == 'global':
                e.world_parent = self
            else:
                e.world_parent = LEVEL_EDITOR.gizmo.fake_gizmo

            e.always_on_top = False
            e._original_world_transform = e.world_transform

    def drop(self):
        LEVEL_EDITOR.gizmo.world_parent = LEVEL_EDITOR

        for e in LEVEL_EDITOR.selection:
            e.world_parent = e.original_parent
            print('---------------', e.original_parent, isinstance(e.original_parent, GizmoArrow))

        if not LEVEL_EDITOR.selection:
            return

        changed = ( # don't record undo if transform didn't change
            distance(LEVEL_EDITOR.selection[0].world_transform[0], LEVEL_EDITOR.selection[0]._original_world_transform[0]) > .0001 or
            distance(LEVEL_EDITOR.selection[0].world_transform[1], LEVEL_EDITOR.selection[0]._original_world_transform[1]) > .0001 or
            distance(LEVEL_EDITOR.selection[0].world_transform[2], LEVEL_EDITOR.selection[0]._original_world_transform[2]) > .0001
            )

        if self.record_undo and changed:
            changes = []
            for e in LEVEL_EDITOR.selection:
                changes.append([LEVEL_EDITOR.entities.index(e), 'world_transform', e._original_world_transform, e.world_transform])

            LEVEL_EDITOR.current_scene.undo.record_undo(changes)

        self.parent = LEVEL_EDITOR.gizmo.arrow_parent
        self.position = (0,0,0)
        self.rotation = self.original_rotation
        LEVEL_EDITOR.render_selection()

    def input(self, key):
        super().input(key)
        if key == 'control':
            self.step = (1,1,1)
        elif key == 'control up':
            self.step = (0,0,0)



class Gizmo(Entity):
    def __init__(self, **kwargs):
        super().__init__(parent=LEVEL_EDITOR, enabled=False)
        self.arrow_parent = Entity(parent=self)
        self.lock_axis_helper_parent = Entity(parent=LEVEL_EDITOR,
            # model='wireframe_cube',
        )
        self.lock_axis_helper = Entity(parent=self.lock_axis_helper_parent,
            # model=Circle(6, radius=.2), color=color.red, double_sided=True, always_on_top=True, render_queue=1
        ) # this will help us lock the movement to an axis on local space


        self.subgizmos = {
            'xz' : GizmoArrow(parent=self.arrow_parent, gizmo=self, model='cube', collider='plane', scale=.6, scale_y=.05, origin=(-.75,0,-.75), color=lerp(color.magenta, color.cyan, .5), plane_direction=(0,1,0)),
            'x'  : GizmoArrow(parent=self.arrow_parent, gizmo=self, color=axis_colors['x'], lock=(0,1,1)),
            'y'  : GizmoArrow(parent=self.arrow_parent, gizmo=self, rotation=(0,0,-90), color=axis_colors['y'], lock=(1,0,1)),
            'z'  : GizmoArrow(parent=self.arrow_parent, gizmo=self, rotation=(0,-90,0), color=axis_colors['z'], plane_direction=(0,1,0), lock=(1,1,0)),
        }

        for e in self.arrow_parent.children:
            e.highlight_color = color.white
            e.original_scale = e.scale

        self.fake_gizmo = Entity(parent=LEVEL_EDITOR, enabled=False)
        self.fake_gizmo.subgizmos = dict()
        for key, value in self.subgizmos.items():
            self.fake_gizmo.subgizmos[key] = duplicate(self.subgizmos[key], parent=self.fake_gizmo, collider=None, ignore=True)


    def input(self, key):   # this will execute before GizmoArrow drag()
        if key == 'left mouse down' and mouse.hovered_entity in self.subgizmos.values():
            self.drag()

        if key == 'left mouse up' and LEVEL_EDITOR.local_global_menu.value == 'local':
            self.drop()


    def drag(self, show_gizmo_while_dragging=True):
        for i, axis in enumerate('xyz'):
            self.subgizmos[axis].plane_direction = self.up

            self.subgizmos[axis].lock = [0,0,0]
            if LEVEL_EDITOR.local_global_menu.value == 'global':
                self.subgizmos[axis].lock = [1,1,1]
                self.subgizmos[axis].lock[i] = 0

            if axis == 'y':
                self.subgizmos[axis].plane_direction = camera.back


        self.subgizmos['xz'].plane_direction = self.up
        [setattr(e, 'visible_self', show_gizmo_while_dragging) for e in self.subgizmos.values()]


        # use fake gizmo technique to lock movement to local axis. if in global mode, skip this and use the old simpler way.
        if LEVEL_EDITOR.local_global_menu.value == 'local':
            self.lock_axis_helper_parent.world_transform = self.world_transform
            self.lock_axis_helper.position = (0,0,0)
            self.fake_gizmo.world_transform = self.world_transform

            self.fake_gizmo.enabled = True
            self.visible = False
            [setattr(e, 'visible_self', show_gizmo_while_dragging) for e in self.fake_gizmo.subgizmos.values()]
            [setattr(e, 'visible_self', False) for e in self.subgizmos.values()]


    def drop(self):
        self.fake_gizmo.enabled = False
        self.visible = True
        [setattr(e, 'visible_self', False) for e in self.fake_gizmo.subgizmos.values()]
        [setattr(e, 'visible_self', True) for e in self.subgizmos.values()]
        [setattr(e, 'scale', e.original_scale) for e in self.subgizmos.values()]


    def update(self):
        if held_keys['r'] or held_keys['s']:
            return
        self.world_scale = distance(self.world_position, camera.world_position) * camera.fov * .0005

        for i, axis in enumerate('xyz'):
            if self.subgizmos[axis].dragging:
                setattr(self.lock_axis_helper, axis, self.subgizmos[axis].get_position(relative_to=self.lock_axis_helper_parent)[i])
                self.fake_gizmo.world_position = self.lock_axis_helper.world_position

        if self.subgizmos['xz'].dragging:
            self.fake_gizmo.world_position = self.subgizmos['xz'].world_position



class RotationGizmo(Entity):
    model = None

    def __init__(self, **kwargs):
        super().__init__(parent=LEVEL_EDITOR.gizmo)
        if not RotationGizmo.model:
            RotationGizmo.model = load_model('rotation_gizmo_model', application.internal_models_compressed_folder)
            if not RotationGizmo.model:
                path = Circle(24).vertices
                path.append(path[0])
                RotationGizmo.model = Pipe(base_shape=Quad(radius=0), path=[Vec3(e)*32 for e in path])
                RotationGizmo.model.save('rotation_gizmo_model.ursinamesh', application.internal_models_compressed_folder)

        self.rotator = Entity(parent=LEVEL_EDITOR.gizmo)
        self.axis = Vec3(0,1,0)
        self.subgizmos = {}
        self.sensitivity = 36000
        self.dragging = False



        for i, dir in enumerate((Vec3(-1,0,0), Vec3(0,1,0), Vec3(0,0,-1))):
            b = Button(parent=self, model=copy(RotationGizmo.model), collider='mesh',
                color=axis_colors[('x','y','z')[i]], is_gizmo=True, always_on_top=True, render_queue=1, unlit=True, double_sided=True,
                on_click=Sequence(Func(setattr, self, 'axis', dir), Func(self.drag)),
                drop=self.drop,
                name=f'rotation_gizmo_{"xyz"[i]}',
                scale=1/32
                )
            b.look_at(dir)
            b.original_color = b.color
            b.start_dragging = b.on_click   # for the quick rotate
            b.on_mouse_enter = Func(setattr, b, 'color', color.white)
            b.on_mouse_exit = Func(setattr, b, 'color', b.original_color)

            self.subgizmos['xyz'[i]] = b


    def drag(self):
        self.rotator.world_parent = scene
        # print('drag')
        for e in LEVEL_EDITOR.selection:
            e.world_parent = self.rotator
            e._original_world_transform = e.world_transform
        self.dragging = True

    def drop(self):
        # print('drop')
        self.rotator.world_parent = LEVEL_EDITOR.gizmo
        changes = []
        for e in LEVEL_EDITOR.selection:
            e.world_parent = e.original_parent
            changes.append([LEVEL_EDITOR.entities.index(e), 'world_transform', e._original_world_transform, e.world_transform])

        LEVEL_EDITOR.current_scene.undo.record_undo(changes)
        self.dragging = False
        self.rotator.rotation = (0,0,0)
        LEVEL_EDITOR.render_selection()

    def input(self, key):
        if key == 'left mouse up' and self.dragging:
            self.dragging = False
            self.drop()


    def update(self):
        if self.dragging:
            rotation_amount = Vec3(sum(mouse.velocity), sum(mouse.velocity), sum(mouse.velocity)) * self.sensitivity * time.dt * self.axis * Vec3(1,1,-1)
            if not LEVEL_EDITOR.origin_mode_menu.value == 'individual':
                self.rotator.rotation -= rotation_amount
            else:
                for e in LEVEL_EDITOR.selection:
                    e.rotation -= rotation_amount




class ScaleGizmo(Draggable):
    def __init__(self, **kwargs):
        super().__init__(parent=LEVEL_EDITOR.gizmo, model='cube', scale=.25, color=color.orange, visible=True, always_on_top=True, render_queue=1, is_gizmo=True, dragging=False, shader=unlit_shader)
        self.scaler = Entity(parent=LEVEL_EDITOR.gizmo)
        self.axis = Vec3(1,1,1)
        self.on_click = Func(setattr, self, 'axis', Vec3(1,1,1))
        self.subgizmos = {}
        self.sensitivity = 300

        for i, dir in enumerate((Vec3(1,0,0), Vec3(0,1,0), Vec3(0,0,1))):
            b = Button(parent=self, model='scale_gizmo', origin_z=-.5, scale=4, collider='box',
                color=axis_colors[('x','y','z')[i]], is_gizmo=True, always_on_top=True, render_queue=1, shader=unlit_shader,
                on_click=Sequence(Func(setattr, self, 'axis', dir), Func(self.drag)), name=f'scale_gizmo_{"xyz"[i]}')
            b.look_at(dir)
            self.subgizmos['xyz'[i]] = b


    def drag(self):
        for e in LEVEL_EDITOR.selection:
            e.world_parent = self.scaler
            e._original_world_transform = e.world_transform
        self.dragging = True

    def drop(self):
        changes = []
        for e in LEVEL_EDITOR.selection:
            e.world_parent = e.original_parent
            changes.append([LEVEL_EDITOR.entities.index(e), 'world_transform', e._original_world_transform, e.world_transform])

        LEVEL_EDITOR.current_scene.undo.record_undo(changes)
        self.dragging = False
        self.scaler.scale = 1
        LEVEL_EDITOR.render_selection()



    def update(self):
        if self.dragging:
            if not LEVEL_EDITOR.origin_mode_menu.value == 'individual':
                self.scaler.scale += Vec3(sum(mouse.velocity), sum(mouse.velocity), sum(mouse.velocity)) * self.sensitivity * time.dt * self.axis
            else:
                for e in LEVEL_EDITOR.selection:
                    e.scale += Vec3(sum(mouse.velocity), sum(mouse.velocity), sum(mouse.velocity)) * self.sensitivity * time.dt * self.axis

class BoxGizmo(Entity):
    def __init__(self):
        super().__init__(parent=LEVEL_EDITOR)
        self.target = None
        self.scaler = Entity(parent=self)
        self.helper = Entity(parent=self, model='cube', unlit=True, color=color.azure, enabled=False)
        self.sensitivity = 600
        self.scale_from_center = False    # scale from center if holding alt
        self.normal = None
        self.axis_name = None

    def input(self, key):
        if key == 'a':
            [setattr(e, 'collision', True) for e in LEVEL_EDITOR.entities]
            mouse.update()

            if mouse.hovered_entity in LEVEL_EDITOR.entities and mouse.normal and mouse.normal != Vec3(0):
                self.target = mouse.hovered_entity
                self.target._original_world_transform = self.target.world_transform
                # self.target.original_parent = self.target.parent

                self.normal = Vec3(mouse.normal)
                self.axis_name = 'xyz'[[abs(int(e)) for e in self.normal].index(1)]

                self.scale_from_center = held_keys['alt']
                if not self.scale_from_center:
                    self.scaler.parent = self.target
                    self.scaler.position = -self.normal * .5
                    self.scaler.rotation = Vec3(0)
                    self.scaler.world_parent = self
                else:
                    self.scaler.position = self.target.world_position
                    self.scaler.rotation = self.target.world_rotation

                self.target.world_parent = self.scaler

                self.helper.parent = self
                self.helper.parent = self.target
                self.helper.position = self.normal / 2
                self.helper.rotation = Vec3(0)

                self.helper.world_scale = .05

                LEVEL_EDITOR.local_global_menu.original_value = LEVEL_EDITOR.local_global_menu.value
                if not LEVEL_EDITOR.local_global_menu.value == 'local':
                    LEVEL_EDITOR.local_global_menu.value = 'local'

                LEVEL_EDITOR.selection = [self.helper, ]
                LEVEL_EDITOR.render_selection()
                LEVEL_EDITOR.gizmo.enabled = True
                LEVEL_EDITOR.gizmo.drag(show_gizmo_while_dragging=False)
                LEVEL_EDITOR.gizmo.subgizmos[self.axis_name].start_dragging()


        elif key == 'a up' and self.target:
            [setattr(e, 'collision', False) for e in LEVEL_EDITOR.entities]
            self.target.world_parent = self.target.original_parent
            self.normal = None
            self.helper.parent = self
            self.scaler.scale = 1

            LEVEL_EDITOR.gizmo.drop()
            LEVEL_EDITOR.gizmo.subgizmos[self.axis_name].record_undo = False
            LEVEL_EDITOR.gizmo.subgizmos[self.axis_name].stop_dragging()
            LEVEL_EDITOR.gizmo.subgizmos[self.axis_name].record_undo = True
            LEVEL_EDITOR.selection = []
            LEVEL_EDITOR.local_global_menu.value = LEVEL_EDITOR.local_global_menu.original_value
            LEVEL_EDITOR.gizmo.enabled = False
            self.helper.enabled = False

            LEVEL_EDITOR.current_scene.undo.record_undo([(LEVEL_EDITOR.entities.index(self.target), 'world_transform', self.target._original_world_transform, self.target.world_transform), ])
            self.target = None


    def update(self):
        if self.target and held_keys['a'] and self.helper and self.scaler:
            relative_position = self.helper.get_position(relative_to=self.scaler)
            value = abs(relative_position[[abs(int(e)) for e in self.normal].index(1)])
            if self.scale_from_center:
                value *= 2

            setattr(self.target, f'scale_{self.axis_name}', value)

            if not self.scale_from_center:
                self.target.world_position = lerp(self.scaler.world_position, self.helper.world_position, .5)




class GizmoToggler(Entity):
    def __init__(self, **kwargs):
        super().__init__(parent=LEVEL_EDITOR)
        self.animator = Animator({
            'w' : LEVEL_EDITOR.gizmo.arrow_parent,
            'e' : LEVEL_EDITOR.scale_gizmo,
            'u' : LEVEL_EDITOR.rotation_gizmo,
            # 't' : box_gizmo,

            'q' : None,
        })

    def input(self, key):
        key = input_handler.get_combined_key(key)

        if key in self.animator.animations and not mouse.left:
            self.animator.state = key


class QuickGrabber(Entity):
    def __init__(self, **kwargs):
        super().__init__(parent=LEVEL_EDITOR)
        self.target_entity = None
        self.target_axis = None
        self.plane = Entity(model='quad', collider='box', scale=Vec3(999,999,1), visible_self=False, enabled=False)
        self.offset_helper = Entity()
        self.start_position = Vec3(0,0,0)
        self.axis_lock = [0,1,0]
        self.is_dragging = False
        self.shortcuts = {
            'left mouse down': Func(self.start_moving_on_axis, 'xz'),
            'd': Func(self.start_moving_on_axis, 'xz'),
            'w': Func(self.start_moving_on_axis, 'xz', auto_select_hovered_entity=False),
            'x': Func(self.start_moving_on_axis, 'x'),
            'y': Func(self.start_moving_on_axis, 'y'),
            'z': Func(self.start_moving_on_axis, 'z'),
            }

    def start_moving_on_axis(self, axis, auto_select_hovered_entity=True):
        if not auto_select_hovered_entity and len(LEVEL_EDITOR.selection) > 1:
            return

        self.target_entity = LEVEL_EDITOR.selector.get_hovered_entity()
        LEVEL_EDITOR.gizmo.enabled = False
        # print('MOVE ON AXIS', axis)
        if self.target_entity:
            LEVEL_EDITOR.selection = [self.target_entity, ]
            self.plane.enabled = True
            self.plane.position = self.target_entity.world_position

            if axis == 'y' or axis == 'xy':
                self.plane.look_at(self.plane.position + Vec3(0,0,-1))
            else:
                self.plane.look_at(self.plane.position + Vec3(0,1,0))

            if len(axis) > 1:
                self.axis_lock = [0,0,0]
            else:
                self.axis_lock = [axis!='x', axis!='y', axis!='z']

            mouse.traverse_target = self.plane
            mouse.update()
            self.offset_helper.position = mouse.world_point
            self.start_position = self.offset_helper.world_position

            if not hasattr(self.target_entity.parent, 'is_gizmo') or self.target_entity.parent.is_gizmo == False:
                self.target_entity.original_parent = self.target_entity.parent
            else:
                self.target_entity.original_parent = scene

            self.target_entity._original_world_position = self.target_entity.world_position
            self.target_entity.world_parent = self.offset_helper

            self.is_dragging = True


    def input(self, key):
        combined_key = input_handler.get_combined_key(key)

        if not key.endswith(' up') and (held_keys['shift'] or held_keys['alt'] or held_keys['s'] or mouse.right or mouse.middle or held_keys['r']):
            return

        if combined_key == 'left mouse up' and self.target_entity and distance(self.target_entity._original_world_position, self.target_entity.world_position) < .1 and (time.time()-mouse.prev_click_time) < .5:  # prevent accidentally moving the entity when you meant to select it
            self.is_dragging = False
            mouse.traverse_target = scene
            self.target_entity.world_parent = self.target_entity.original_parent
            self.target_entity.world_position = self.target_entity._original_world_position
            LEVEL_EDITOR.selection = [self.target_entity, ]
            LEVEL_EDITOR.render_selection()
            self.target_entity = None
            self.plane.enabled = False
            return

        if combined_key in self.shortcuts.keys():
            if self.target_entity:
                return
            self.shortcuts[key]()

        elif (key in [f'{e} up' for e in self.shortcuts.keys()] or 'left mouse down' in self.shortcuts and key == 'left mouse up') and self.target_entity:
            self.drop()


    def drop(self):
        self.is_dragging = False
        mouse.traverse_target = scene
        self.target_entity.world_parent = self.target_entity.original_parent

        if self.target_entity.world_position != self.target_entity._original_world_position:
            changes = []
            for e in LEVEL_EDITOR.selection:
                changes.append([LEVEL_EDITOR.entities.index(e), 'world_position', e._original_world_position, e.world_position])

            LEVEL_EDITOR.current_scene.undo.record_undo(changes)
            LEVEL_EDITOR.selection = []
            LEVEL_EDITOR.render_selection()
        else:
            LEVEL_EDITOR.selection = [self.target_entity, ]
            LEVEL_EDITOR.render_selection()

        self.target_entity = None
        self.plane.enabled = False


    def on_disable(self):
        self.drop()


    def update(self):
        if not self.is_dragging or not mouse.world_point:
            return

        if mouse.right:
            return

        pos = mouse.world_point
        if held_keys['control']:
            pos = round(pos)

        for i, e in enumerate(pos):
            if self.axis_lock[i]:
                pos[i] = self.start_position[i]

        self.offset_helper.world_position = pos
        if held_keys['control']:
            snap_step = 1
            self.offset_helper.world_position = Vec3(*[round(e * snap_step) /snap_step for e in self.offset_helper.world_position])
            self.target_entity.world_position = Vec3(*[round(e * snap_step) /snap_step for e in self.target_entity.world_position])


class QuickScaler(Entity):
    def __init__(self, **kwargs):
        super().__init__(
            parent=LEVEL_EDITOR,
            gizmos_to_toggle={
                's' :  LEVEL_EDITOR.scale_gizmo,
                'sx' : LEVEL_EDITOR.scale_gizmo,
                'sy' : LEVEL_EDITOR.scale_gizmo,
                'sz' : LEVEL_EDITOR.scale_gizmo,
            },
            clear_selection=False,
            dragging=False,
            original_gizmo_state='q'
            )


    def input(self, key):
        if held_keys['control'] or held_keys['shift'] or held_keys['alt'] or mouse.left or mouse.middle or held_keys['r'] or held_keys['d'] or held_keys['t']:
            return

        if (held_keys['x'] or held_keys['y'] or held_keys['z']) and key == 's':
            self.original_gizmo_state = LEVEL_EDITOR.gizmo_toggler.animator.state
            return

        if held_keys['s'] and key in 'xyz':
            key = 's' + key

        if key in ('s', 'sx', 'sy', 'sz'):
            self.original_gizmo_state = LEVEL_EDITOR.gizmo_toggler.animator.state
            LEVEL_EDITOR.gizmo_toggler.animator.state = 'e'

            if not key == 's':
                LEVEL_EDITOR.scale_gizmo.axis = (Vec3(1,0,0), Vec3(0,1,0), Vec3(0,0,1))[('sx', 'sy', 'sz').index(key)]


        if key in self.gizmos_to_toggle.keys():
            LEVEL_EDITOR.selector.enabled = False
            LEVEL_EDITOR.selection_box.enabled = False

            LEVEL_EDITOR.gizmo.arrow_parent.visible = False
            LEVEL_EDITOR.scale_gizmo.visible = False
            self.gizmos_to_toggle[key].visible_self = False
            if not key in ('sx', 'sy', 'sz'):
                self.clear_selection = not LEVEL_EDITOR.selection

            if not LEVEL_EDITOR.selection:
                LEVEL_EDITOR.selector.input('left mouse down')

            self.gizmos_to_toggle[key].input('left mouse down')
            self.gizmos_to_toggle[key].start_dragging()


        # print('------------', key)
        if key in ('s up', 'x up', 'y up', 'z up'):
            for e in self.gizmos_to_toggle.values():
                e.input('left mouse up')
            # self.gizmos_to_toggle[key].drop()
            if self.clear_selection:
                LEVEL_EDITOR.selection.clear()
                LEVEL_EDITOR.render_selection()

            LEVEL_EDITOR.gizmo.arrow_parent.visible = True
            LEVEL_EDITOR.scale_gizmo.visible = True
            LEVEL_EDITOR.scale_gizmo.axis = Vec3(1,1,1)
            # self.gizmos_to_toggle[key].visible_self = True
            LEVEL_EDITOR.gizmo_toggler.animator.state = self.original_gizmo_state

            LEVEL_EDITOR.selector.enabled = True
            LEVEL_EDITOR.selection_box.enabled = True
            mouse.traverse_target = scene


    def update(self):
        for key in self.gizmos_to_toggle.keys():
            if held_keys[key] and not held_keys['control'] and not held_keys['shift'] and mouse.velocity != Vec3(0,0,0):
                LEVEL_EDITOR.render_selection(update_gizmo_position=False)
                return


class QuickRotator(Entity):
    def __init__(self):
        super().__init__(parent=LEVEL_EDITOR)

    def input(self, key):
        if held_keys['control'] or held_keys['shift'] or held_keys['alt'] or held_keys['s']:
            return

        if key == 'r' and len(LEVEL_EDITOR.selection) <= 1:
            if not LEVEL_EDITOR.selection:
                LEVEL_EDITOR.selection = [LEVEL_EDITOR.selector.get_hovered_entity(), ]
                LEVEL_EDITOR.render_selection()

            if not LEVEL_EDITOR.selection:
                return

            self.target_entity = LEVEL_EDITOR.selection[0]
            LEVEL_EDITOR.rotation_gizmo.subgizmos['y'].input('left mouse down')
            LEVEL_EDITOR.rotation_gizmo.subgizmos['y'].start_dragging()

        elif key == 'r up' and hasattr(self, 'target_entity') and self.target_entity:
            key = key[:-3]
            LEVEL_EDITOR.rotation_gizmo.subgizmos['y'].input('left mouse up')
            LEVEL_EDITOR.rotation_gizmo.subgizmos['y'].drop()
            LEVEL_EDITOR.selection.clear()
            LEVEL_EDITOR.render_selection()
            self.target_entity = None

    def update(self):
        if held_keys['r'] and not held_keys['control'] and not held_keys['shift'] and mouse.velocity != Vec3(0,0,0):
            LEVEL_EDITOR.render_selection(update_gizmo_position=False)
            return


class RotateRelativeToView(Entity):
    _rotation_helper = Entity(name='RotateRelativeToView_rotation_helper', add_to_scene_entities=False)
    sensitivity = Vec2(200,200)

    def __init__(self, **kwargs):
        super().__init__(parent=LEVEL_EDITOR, **kwargs)

    def input(self, key):
        if held_keys['control'] or held_keys['shift'] or held_keys['alt'] or held_keys['s'] or held_keys['r']:
            return

        if key == 't':
            if len(LEVEL_EDITOR.selection) > 1:
                return

            if not LEVEL_EDITOR.selection:
                LEVEL_EDITOR.selection = [LEVEL_EDITOR.selector.get_hovered_entity(), ]
                LEVEL_EDITOR.render_selection()

            if not LEVEL_EDITOR.selection:
                return

            self.target_entity = LEVEL_EDITOR.selection[0]
            __class__._rotation_helper.world_parent = scene
            __class__._rotation_helper.position = self.target_entity.world_position
            __class__._rotation_helper.rotation = Vec3(0,0,0)

            self._entity_original_parent = self.target_entity.parent
            self._entity_original_rotation = self.target_entity.world_rotation
            self.target_entity.world_parent = __class__._rotation_helper
            self._mouse_start_x = mouse.x
            self._mouse_start_y = mouse.y

        elif key == 't up' and self.target_entity:
            self.target_entity.world_parent = self._entity_original_parent
            LEVEL_EDITOR.selection.clear()
            LEVEL_EDITOR.render_selection()
            self.target_entity = None
            self.x_mov = 0
            self.y_mov = 0

    def update(self):
        if self.target_entity and held_keys['t']:
            __class__._rotation_helper.rotation_y -= mouse.velocity[0] * __class__.sensitivity.x / camera.aspect_ratio
            __class__._rotation_helper.rotation_x += mouse.velocity[1] * __class__.sensitivity.y




class Selector(Entity):
    def __init__(self):
        super().__init__(parent=LEVEL_EDITOR)

    def input(self, key):
        if key == 'left mouse down':
            if mouse.hovered_entity:
                # print('sroifjseofisjeoij')
                return

            clicked_entity = self.get_hovered_entity()

            if clicked_entity in LEVEL_EDITOR.entities and not held_keys['alt']:
                if held_keys['shift']:
                    if not clicked_entity in LEVEL_EDITOR.selection:
                        LEVEL_EDITOR.selection.append(clicked_entity) # append
                else:
                    LEVEL_EDITOR.selection = [clicked_entity, ]   # overwrite

            if held_keys['alt'] and clicked_entity in LEVEL_EDITOR.selection:
                LEVEL_EDITOR.selection.remove(clicked_entity) # remove

            if not clicked_entity and not held_keys['shift'] and not held_keys['alt']: # clear
                LEVEL_EDITOR.selection.clear()

            LEVEL_EDITOR.render_selection()

        if held_keys['control'] and key == 'a':
            LEVEL_EDITOR.selection = [e for e in LEVEL_EDITOR.entities]
            LEVEL_EDITOR.render_selection()

        elif key == 'h':
            LEVEL_EDITOR.point_renderer.enabled = not LEVEL_EDITOR.point_renderer.enabled

        if key == 'left mouse up':
            LEVEL_EDITOR.gizmo.enabled = bool(LEVEL_EDITOR.selection)



    def get_hovered_entity(self):
        LEVEL_EDITOR.entities = [e for e in LEVEL_EDITOR.entities if e]
        # [print(str(e)) for e in LEVEL_EDITOR.entities]
        entities_in_range = [(distance_2d(e.screen_position, mouse.position), e) for e in LEVEL_EDITOR.entities if e and e.selectable and not e.collider]
        entities_in_range = [e for e in entities_in_range if e[0] < .03]
        entities_in_range.sort()

        clicked_entity = None
        if entities_in_range:
            return entities_in_range[0][1]

        # try getting entities with box collider
        [setattr(e, 'collision', True) for e in LEVEL_EDITOR.entities if not hasattr(e, 'is_gizmo')]
        # print('-------------', len([e for e in LEVEL_EDITOR.entities  if not hasattr(e, 'is_gizmo') and e.collider and e.collision]))
        mouse.update()

        if mouse.hovered_entity in LEVEL_EDITOR.entities:

            [setattr(e, 'collision', False) for e in LEVEL_EDITOR.entities if not hasattr(e, 'is_gizmo')]
            return mouse.hovered_entity

        [setattr(e, 'collision', False) for e in LEVEL_EDITOR.entities if not hasattr(e, 'is_gizmo')]



class SelectionBox(Entity):
    def __init__(self, **kwargs):
        super().__init__(parent=LEVEL_EDITOR.ui, visible=False, **kwargs)

    def input(self, key):
        if key == 'left mouse down':
            if mouse.hovered_entity and mouse.hovered_entity not in LEVEL_EDITOR.selection:
                # print('-------', 'clicked on gizmo, dont box select')
                return
            self.position = mouse.position
            self.scale = .001
            self.visible = True
            self.mode = 'new'
            if held_keys['shift']:
                self.mode = 'add'
            if held_keys['alt']:
                self.mode = 'subtract'

        if key == 'left mouse up' and self.visible:
            self.visible = False

            if self.scale_x < 0:
                self.x += self.scale_x
                self.scale_x = abs(self.scale_x)
            if self.scale_y < 0:
                self.y += self.scale_y
                self.scale_y = abs(self.scale_y)

            if self.scale_x < .01 or self.scale_y < .01 or held_keys['w']:
                return

            if self.mode == 'new':
                LEVEL_EDITOR.selection.clear()

            for e in LEVEL_EDITOR.entities:
                if not e.selectable:
                    continue

                pos = e.screen_position
                if pos.x > self.x and pos.x < self.x + abs(self.scale_x) and pos.y > self.y and pos.y < self.y + abs(self.scale_y):
                    if self.mode in ('add', 'new') and not e in LEVEL_EDITOR.selection:
                        LEVEL_EDITOR.selection.append(e)
                    elif self.mode == 'subtract' and e in LEVEL_EDITOR.selection:
                        LEVEL_EDITOR.selection.remove(e)

            LEVEL_EDITOR.render_selection()
            self.mode = 'new'

    def update(self):
        if mouse.left:
            if mouse.x == mouse.start_x and mouse.y == mouse.start_y:
                return

            self.scale_x = mouse.x - self.x
            self.scale_y = mouse.y - self.y




class WhiteCube(Entity):
    default_values = Entity.default_values | dict(model='cube', shader='lit_with_shadows_shader', texture='white_cube', collider='box', name='cube') # combine dicts
    def __init__(self, **kwargs):
        super().__init__(**__class__.default_values | kwargs)

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


class ClassSpawner(Entity):
    default_values = Entity.default_values | dict(class_to_spawn='', model='wireframe_cube', color=color.blue, name='ClassSpawner') # combine dicts
    '''Prefab for spawning target class on play'''
    def __init__(self, **kwargs):
        super().__init__(**__class__.default_values | kwargs)
        self.class_instance = None

    def draw_inspector(self):
        return {'class_to_spawn': type}

    def start(self):
        self.enabled = False
        if self.class_to_spawn not in LEVEL_EDITOR.class_menu.available_classes:
            print_warning('class to spawn not found in LEVEL_EDITOR.class_menu.available_classes:', self.class_to_spawn)
            return

        if self.class_to_spawn and self.class_to_spawn != 'None':
            print('spawn class', self.class_to_spawn)
            self.class_instance = LEVEL_EDITOR.class_menu.available_classes[self.class_to_spawn](world_transform=self.world_transform, add_to_scene_entities=False)

    def stop(self):
        if self.class_instance:
            destroy(self.class_instance)
            self.class_instance = None
        self.enabled = True

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


class TriplanarCube(Entity):
    default_values = Entity.default_values | dict(model='cube', shader='triplanar_shader', texture='white_cube', collider='box', name='cube') # combine dicts

    def __init__(self, **kwargs):
        super().__init__(**__class__.default_values | kwargs)
        self.set_shader_input('side_texture', load_texture('brick'))

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


class Pyramid(Entity):
    default_values = Entity.default_values | dict(name='pyramid', model=Cone(4), texture='brick') # combine dicts

    def __init__(self, **kwargs):
        super().__init__(**__class__.default_values | kwargs)

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


class Rock(Entity):
    default_values = Entity.default_values | dict(name='rock', model='procedural_rock_0', collider='box', color=hsv(20,.2,.45)) # combine dicts
    gizmo_color = color.brown
    def __init__(self, **kwargs):
        super().__init__(**__class__.default_values | kwargs)

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


class Spawner(Entity):
    def __init__(self):
        super().__init__(parent=LEVEL_EDITOR)
        self.target = None
        self.ui = Entity(parent=LEVEL_EDITOR.ui, position=window.bottom)
        self.update_menu()

    def update_menu(self):
        [destroy(e) for e in self.ui.children]
        # for file in LEVEL_EDITOR.prefab_folder.glob('**/*.py')
        import_all_classes(LEVEL_EDITOR.prefab_folder, debug=True)
        # LEVEL_EDITOR.prefabs =

        for i, prefab in enumerate(LEVEL_EDITOR.built_in_prefabs + LEVEL_EDITOR.prefabs):
            button = Button(parent=self.ui, scale=.075/2, text=' ', text_size=.5, on_click=Func(self.spawn_entity, prefab))
            if hasattr(prefab, 'icon'):
                button.icon = prefab.icon
            else:
                button.text = '\n'.join(chunk_list(prefab.__name__, 5))

        grid_layout(self.ui.children, origin=(0,-.5), spacing=(.005,0), max_x=32)



    def input(self, key):
        if key == 'i':
            mouse.traverse_target = LEVEL_EDITOR.grid
            self.spawn_entity()

        elif key == 'i up' and self.target:
            self.drop_entity()
            mouse.traverse_target = scene

        elif self.target and key == 'left mouse up':
            self.drop_entity()

    def spawn_entity(self, _class=Entity):
        if not LEVEL_EDITOR.current_scene:
            print_on_screen('<red>select a scene first', position=(0,0), origin=(0,0))
            return

        LEVEL_EDITOR.grid.enabled = True
        self.target = _class(position=mouse.world_point, original_parent=LEVEL_EDITOR, selectable=True, collision=False)
        if not hasattr(self.target, 'collider_type'):
            self.target.collider_type = 'None'
        # print(self.target.model.name)
        # if not self.target.collider:
        #     self.target.collider = 'box'
        #     self.target.collision = False
        if not self.target.shader:
            self.target.shader = lit_with_shadows_shader

        LEVEL_EDITOR.current_scene.entities.append(self.target)
        LEVEL_EDITOR.render_selection()


    def drop_entity(self):
        LEVEL_EDITOR.current_scene.undo.record_undo(('delete entities', [LEVEL_EDITOR.current_scene.entities.index(self.target), ], [repr(self.target), ]))
        LEVEL_EDITOR.selection = [self.target, ]
        self.target = None
        LEVEL_EDITOR.grid.enabled = False


    def update(self):
        if mouse.world_point and self.target:
            if held_keys['n'] or mouse.left:
                self.target.position = mouse.world_point


class Deleter(Entity):
    def __init__(self):
        super().__init__(parent=LEVEL_EDITOR)
        self.shortcuts = ['delete', 'control+x']

    def input(self, key):
        combined_key = input_handler.get_combined_key(key)
        if LEVEL_EDITOR.selection and combined_key in self.shortcuts:
            self.delete_selected()

    def delete_selected(self):
        LEVEL_EDITOR.current_scene.undo.record_undo((
            'restore entities',
            [LEVEL_EDITOR.entities.index(e) for e in LEVEL_EDITOR.selection],
            [repr(e) for e in LEVEL_EDITOR.selection],
            ))

        # print(LEVEL_EDITOR.selection)
        before = len(LEVEL_EDITOR.entities)
        # LEVEL_EDITOR.entities = [e for e in LEVEL_EDITOR.entities if e not in LEVEL_EDITOR.selection]
        for e in LEVEL_EDITOR.selection:
            if e in LEVEL_EDITOR.entities:
                LEVEL_EDITOR.entities.remove(e)

        # print('---------------', before, '-->', len(LEVEL_EDITOR.entities))
        [setattr(e, 'parent', LEVEL_EDITOR) for e in LEVEL_EDITOR.cubes]
        [destroy(e) for e in LEVEL_EDITOR.selection]
        LEVEL_EDITOR.selection.clear()
        LEVEL_EDITOR.render_selection()


class Grouper(Entity):
    def __init__(self):
        super().__init__(parent=LEVEL_EDITOR)

    def input(self, key):
        if held_keys['control'] and key == 'g' and LEVEL_EDITOR.selection:
            group_entity = Entity(parent=LEVEL_EDITOR.current_scene.scene_parent, name='[group]', selectable=True)
            LEVEL_EDITOR.entities.append(group_entity)

            parents = tuple(set([e.parent for e in LEVEL_EDITOR.selection]))
            if len(parents) == 1:
                group_entity.world_parent = parents[0]

            group_entity.world_position = sum([e.world_position for e in LEVEL_EDITOR.selection]) / len(LEVEL_EDITOR.selection)
            for e in LEVEL_EDITOR.selection:
                e.world_parent = group_entity

            LEVEL_EDITOR.selection = [group_entity, ]
            LEVEL_EDITOR.render_selection()



class PointOfViewSelector(Entity):
    def __init__(self, **kwargs):
        super().__init__(parent=LEVEL_EDITOR.ui, model='cube', collider='box', texture='white_cube', scale=.05, position=window.top_right-Vec2(.1,.05))
        self.front_text = Text(parent=self, text='front', z=-.5, scale=10, origin=(0,0), color=color.azure)

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

    def on_click(self):
        if mouse.normal == Vec3(0,0,-1):   LEVEL_EDITOR.editor_camera.animate_rotation((0,0,0)) # front
        elif mouse.normal == Vec3(0,0,1):  LEVEL_EDITOR.editor_camera.animate_rotation((0,180,0)) # back
        elif mouse.normal == Vec3(1,0,0):  LEVEL_EDITOR.editor_camera.animate_rotation((0,90,0)) # right
        elif mouse.normal == Vec3(-1,0,0): LEVEL_EDITOR.editor_camera.animate_rotation((0,-90,0)) # right
        elif mouse.normal == Vec3(0,1,0):  LEVEL_EDITOR.editor_camera.animate_rotation((90,0,0)) # top
        elif mouse.normal == Vec3(0,-1,0): LEVEL_EDITOR.editor_camera.animate_rotation((-90,0,0)) # top


    def update(self):
        self.rotation = -LEVEL_EDITOR.editor_camera.rotation

    def input(self, key):
        if held_keys['shift']:
            if key == '1':   LEVEL_EDITOR.editor_camera.animate_rotation((0,0,0)) # front
            elif key == '3': LEVEL_EDITOR.editor_camera.animate_rotation((0,90,0)) # right
            elif key == '7': LEVEL_EDITOR.editor_camera.animate_rotation((90,0,0)) # top
            elif key == '5': camera.orthographic = not camera.orthographic


# class PaintBucket(Entity):
#     def input(self, key):
#         if held_keys['alt'] and key == 'c' and mouse.hovered_entity:
#             self.color = mouse.hovered_entity.color

class Copier(Entity):
    prefix = 'ursina_editor_copy_data:```py\n'

    def input(self, key):
        if held_keys['control'] and key == 'c':
            if LEVEL_EDITOR.selection:
                code = __class__.prefix
                for e in LEVEL_EDITOR.selection:
                    entity_repr = repr(e)
                    if not 'collider_type=' in entity_repr and hasattr(e, 'collider_type'):
                        entity_repr = f'{entity_repr[:-1]}collider_type=\'{e.collider_type}\')'
                    code += entity_repr + '\n'

                pyperclip.copy(f'{code}\n```')

        if held_keys['control'] and key == 'v':
            value = pyperclip.paste()
            if value.startswith('ursina_editor_copy_data:```py\n') and value.endswith('\n```'):
                cleaned_code = value[len(__class__.prefix):-4].strip().split('\n')
                clones = []
                for line in cleaned_code:
                    instance = eval(line)
                    instance.selectable = True
                    LEVEL_EDITOR.current_scene.entities.append(instance)
                    clones.append(instance)

                LEVEL_EDITOR.entities.extend(clones)
                LEVEL_EDITOR.selection = clones
                LEVEL_EDITOR.current_scene.undo.record_undo(('delete entities', [LEVEL_EDITOR.entities.index(en) for en in clones], [repr(e) for e in clones]))
                print('------------------------')
                LEVEL_EDITOR.render_selection()




class LevelMenu(Entity):
    def __init__(self, **kwargs):
        super().__init__(parent=LEVEL_EDITOR)
        self.menu = Entity(parent=LEVEL_EDITOR.ui, model=Quad(radius=.05), color=color.black, scale=.2, origin=(.5,0), x=camera.aspect_ratio*.495, y=-.3, collider='box')
        self.menu.grid = Entity(parent=self.menu, model=Grid(8,8), z=-1, origin=self.menu.origin, color=color.dark_gray)
        self.content_renderer = Entity(parent=self.menu, scale=1/8, position=(-1,-.5,-1), model=Mesh(), color='#222222') # scales the content so I can set the position as (x,y) instead of (-1+(x/8),-.5+(y/8))
        self.cursor = Entity(parent=self.content_renderer, model='quad', color=color.lime, origin=(-.5,-.5), z=-2, alpha=.5)
        self.current_scene_indicator = Entity(parent=self.content_renderer, model='circle', color=color.azure, origin=(-.5,-.5), z=-1, enabled=False)
        # self.tabs = [Button(parent=self.menu, scale=(1/4,1/8), position=(-1+(i/4),.5), origin=(-.5,-.5), color=color.hsv(90*i,.5,.3)) for i in range(4)]


        self.current_scene_label = Text(parent=self.menu, x=-1, y=-.5, text='current scene:', z=-10, scale=2.5)

        self.load_scenes()
        # self.goto_scene(0, 0)
        self.draw()


    def load_scenes(self):
        for scene_file in LEVEL_EDITOR.scene_folder.glob('*.csv'):
            if '__' in scene_file.name:
                continue

            # print('found scene:', scene_file)
            name = scene_file.stem
            if '[' in name and ']' in name:
                x, y = (int(e) for e in name.split('[')[1].split(']')[0].split(','))
                # print('scene is at coordinate:', x, y)
                LEVEL_EDITOR.scenes[x][y].path = scene_file


    def draw(self):
        if not hasattr(self, 'quad_vertices'):
            self.quad_vertices = load_model('quad', application.internal_models_compressed_folder, use_deepcopy=True).vertices
            self.quad_vertices = [Vec3(*e)*.75 for e in self.quad_vertices]

        self.content_renderer.model.clear()
        for x in range(8):
            for y in range(8):
                if LEVEL_EDITOR.scenes[x][y].path:
                    self.content_renderer.model.vertices += [Vec3(*v)+Vec3(x+.5,y+.5,0) for v in self.quad_vertices]

        self.content_renderer.model.generate()


    def update(self):
        self.cursor.enabled = self.menu.hovered
        if self.menu.hovered:
            grid_pos = [floor((mouse.point.x+1) * 8), floor((mouse.point.y+.5) * 8)]
            self.cursor.position = grid_pos


    def input(self, key):
        combined_key = input_handler.get_combined_key(key)
        if combined_key == 'shift+m':
            self.menu.enabled = not self.menu.enabled

        # if key == 'left mouse down' and self.menu.hovered:
        #     self.click_start_pos = [int((mouse.point.x+1) * 8), int((mouse.point.y+.5) * 8)]

        if key == 'left mouse down' and self.menu.hovered:
            x, y = [int((mouse.point.x+1) * 8), int((mouse.point.y+.5) * 8)]
            # start_x, start_y = self.click_start_pos
            #
            # if x != start_x or y != start_y: # move scene
            #     print(f'move scene at {start_x},{start_y} to {x},{y}')
            #     scene_a = LEVEL_EDITOR.scenes[start_x][start_y]
            #     scene_a.coordinates = (x,y)
            #     scene_a.name = scene_a.name.split('[')[0] + f'[{x},{y}]'
            #     if scene_a.path:
            #         scene_a.path = scene_a.path.parent / (scene_a.name + '.py')
            #
            #     scene_b = LEVEL_EDITOR.scenes[x][y]
            #     scene_b.coordinates = (start_x, start_y)
            #     scene_b.name = scene_a.name.split('[')[0] + f'[{start_x},{start_y}]'
            #     if scene_b.path:
            #         scene_b.path = scene_b.path.parent / (scene_b.name + '.py')
            #
            #     # swap scenes
            #     LEVEL_EDITOR.scenes[self.click_start_pos[0]][self.click_start_pos[1]], LEVEL_EDITOR.scenes[x][y] = LEVEL_EDITOR.scenes[x][y], LEVEL_EDITOR.scenes[self.click_start_pos[0]][self.click_start_pos[1]]
            #
            #     self.draw()
            #     return
            # print(x, y)
            if not held_keys['shift'] and not held_keys['alt']:
                self.goto_scene(x, y)

            elif held_keys['shift'] and not held_keys['alt']: # append
                LEVEL_EDITOR.scenes[x][y].load()

            elif held_keys['alt'] and not held_keys['shift']: # remove
                LEVEL_EDITOR.scenes[x][y].unload()


        # hotkeys for loading neighbour levels
        if held_keys['shift'] and held_keys['alt'] and key in 'wasd':
            if not LEVEL_EDITOR.current_scene:
                return

            coords = copy(LEVEL_EDITOR.current_scene.coordinates)

            if key == 'd': coords[0] += 1
            if key == 'a': coords[0] -= 1
            if key == 'w': coords[1] += 1
            if key == 's': coords[1] -= 1

            # print(LEVEL_EDITOR.current_scene.coordinates, '-->', coords)
            coords[0] = clamp(coords[0], 0, 8)
            coords[1] = clamp(coords[1], 0, 8)
            self.goto_scene(coords[0], coords[1])


        # elif key == 'right mouse down' and self.hovered:
        #     x, y = [int((mouse.point.x+1) * 8), int((mouse.point.y+.5) * 8)]
        #     self.right_click_menu.enabled = True
        #     self.right_click_menu.position = (x,y)



    def goto_scene(self, x, y):
        self.current_scene_indicator.enabled = True
        self.current_scene_indicator.position = (x,y)
        [[LEVEL_EDITOR.scenes[_x][_y].unload() for _x in range(8)] for _y in range(8)]
        LEVEL_EDITOR.current_scene = LEVEL_EDITOR.scenes[x][y]
        loaded_scene = LEVEL_EDITOR.current_scene.load()
        if loaded_scene is None:
            LEVEL_EDITOR.current_scene.scene_parent = Entity()
        self.current_scene_label.text = LEVEL_EDITOR.current_scene.name
        self.draw()
        LEVEL_EDITOR.render_selection()

        LEVEL_EDITOR.inspector.update_inspector()
        LEVEL_EDITOR.sun_handler.update_bounds(LEVEL_EDITOR.current_scene.scene_parent)


class HierarchyList(Entity):
    def __init__(self):
        super().__init__(parent=LEVEL_EDITOR.ui, position=window.top_left+Vec2(0,-.05))
        self.quad_model = load_model('quad', application.internal_models_folder, use_deepcopy=True)
        self.bg = Entity(parent=self, model='quad', collider='box', origin=(-.5,.5), color=color.black90, scale=(.15,10))
        self.entity_list_text = Text(font=Text.default_monospace_font, parent=self, scale=.6, line_height=1, position=Vec2(.005,0), z=-2)
        self.selected_renderer = Entity(parent=self.entity_list_text, scale=(.25,Text.size), model=Mesh(vertices=[]), color=hsv(210,.9,.6), origin=(-.5,.5), x=-.01, z=-1)
        self.selected_renderer.world_parent = self
        self.selected_renderer.z= -.1
        self.prev_y = None
        self.i = 0

    def input(self, key):
        if key == 'left mouse down' and self.bg.hovered:
            y = int(-mouse.point.y * self.bg.scale_y / Text.size / self.entity_list_text.scale_y)
            if y < len(LEVEL_EDITOR.entities):
                if not held_keys['control'] and not held_keys['shift']:     # select one
                    LEVEL_EDITOR.selection = [LEVEL_EDITOR.entities[self.entity_indices[y]], ]
                elif held_keys['control'] and not held_keys['shift']:       # add one
                    LEVEL_EDITOR.selection.append(LEVEL_EDITOR.entities[self.entity_indices[y]])
                elif held_keys['shift'] and self.prev_y:                    # add multiple
                    from_y = min(self.prev_y, y)
                    to_y = max(self.prev_y, y)
                    for _ in range(from_y, to_y+1):
                        LEVEL_EDITOR.selection.append(LEVEL_EDITOR.entities[self.entity_indices[_]])

            elif not held_keys['control'] and not held_keys['shift']:
                LEVEL_EDITOR.selection.clear()

            self.prev_y = y
            LEVEL_EDITOR.render_selection()
            # LEVEL_EDITOR.render_selection()


        if key == 'left mouse up':
            LEVEL_EDITOR.render_selection()


    def draw(self, entity, indent=0):
        if entity not in LEVEL_EDITOR.entities:
            return
        self.entity_indices[self.i] = LEVEL_EDITOR.entities.index(entity)
        if not entity in LEVEL_EDITOR.selection:
            self._text += f'<gray>{" "*indent}{entity.name if entity.name else "lol"}\n'
        else:
            self.selected_renderer.model.vertices.extend([Vec3(v)-Vec3(0,self.i,0) for v in self.quad_model.vertices])
            self._text += f'<white>{" "*indent}{entity.name}\n'

        self.i += 1


    def render_selection(self):
        self._text = ''
        self.selected_renderer.model.vertices = []
        self.entity_indices = [-1 for e in LEVEL_EDITOR.entities]

        self.i = 0
        current_node = None
        if LEVEL_EDITOR.current_scene is None or LEVEL_EDITOR.current_scene.scene_parent is None:
            return

        for entity in LEVEL_EDITOR.current_scene.scene_parent.get_descendants():
            if hasattr(entity, 'is_gizmo') and entity.is_gizmo:
                continue

            if entity.parent == LEVEL_EDITOR.current_scene.scene_parent:
                self.draw(entity, indent=0)

        self.entity_list_text.text = self._text
        self.selected_renderer.model.generate()


Text.default_font = 'VeraMono.ttf'
class InspectorInputField(InputField):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.text_field.x = .05
        self.text_field.y = -.25
        self.text_field.world_scale = 25 * .75
        self.text_field.text_entity.color = color.light_gray
        self.highlight_color = color._32


class InspectorButton(Button):
    defaults = dict(model='quad', origin=(-.5,.5), text='?', text_origin=(-.5,0), text_color=color.light_gray, color=color.black90, highlight_color=color._32)

    def __init__(self, **kwargs):
        kwargs = __class__.defaults | kwargs
        super().__init__(**kwargs)
        self.text_entity.x = .025
        self.text_entity.scale *= .75


class ColorField(InspectorButton):
    def __init__(self, attr_name='color', is_shader_input=False, value=color.white, **kwargs):
        super().__init__(**kwargs)
        self.attr_name = attr_name
        self.is_shader_input = is_shader_input

        self.preview = Entity(parent=self, model=Quad(aspect=2/1), scale=(.5,.8), origin=(.5,.5), x=1, z=-.1, y=-.05, collider='box', on_click=self.on_click)
        # self.text_entity.scale *= .75
        self.value = value

    @property
    def value(self):
        return self.preview.color

    @value.setter
    def value(self, value):
        self.preview.color = value

    def on_click(self):
        LEVEL_EDITOR.color_menu.color_field = self
        LEVEL_EDITOR.color_menu.position = self.preview.get_position(relative_to=camera.ui).xy + Vec2(.025,-.01)
        LEVEL_EDITOR.menu_handler.state = 'color_menu'


class Inspector(Entity):
    def __init__(self):
        super().__init__(parent=LEVEL_EDITOR.hierarchy_list, x=.15)
        self.selected_entity = None
        self.ui = Entity(parent=self)
        self.name_field = InspectorInputField(parent=self.ui, default_value='name', origin=(-.5,.5), scale_x=.15*3, scale_y=.05*.75, color=hsv(210,.9,.6))
        self.input_fields = [self.name_field, ]
        self.transform_fields = []

        for y, names in enumerate((('x','y','z'), ('rotation_x','rotation_y','rotation_z'), ('scale_x','scale_y','scale_z'))):
            for x in range(3):
                default = '0'
                if y == 2:
                    default = '1'

                field = InspectorInputField(max_width=8, model='quad', parent=self.name_field, scale=(1/3,1), origin=(-.5,.5), default_value=default, limit_content_to=ContentTypes.math, x=x/3, y=-y-1, color=color._8)
                def on_submit(names=names, x=x, field=field):
                    try:
                        value = float(eval(field.text[:8]))
                        if isinstance(value, float):
                            field.text_field.text_entity.text = str(value)[:8]
                            for e in LEVEL_EDITOR.selection:
                                setattr(e, names[x], float(field.text_field.text_entity.text))
                    except: # invalid/incomplete math
                        # print('invalid')
                        return

                # field.submit_on = 'enter'
                field.on_submit = on_submit
                field.on_value_changed = on_submit

                self.transform_fields.append(field)
                # self.input_fields.append(field)

        for i in range(len(self.transform_fields)-1):
            self.transform_fields[i].next_field = self.transform_fields[i+1]

        self.fields = dict(
            model =   InspectorButton(parent=self.name_field, text='model: ', y=-4, on_click=Func(setattr, LEVEL_EDITOR.menu_handler, 'state', 'model_menu')),
            texture = InspectorButton(parent=self.name_field, text='texture: ', y=-4-1, on_click=Sequence(Func(setattr, LEVEL_EDITOR.menu_handler, 'state', 'texture_menu'), Func(setattr, LEVEL_EDITOR.texture_menu, 'target_attr', 'texture'))),
            color =   ColorField(parent=self.name_field, text='c:color: ', y=-4-2, attr_name='color', is_shader_input=False),
            collider_type = InspectorButton(parent=self.name_field, text='collider_type: ', y=-4-3, on_click=Func(setattr, LEVEL_EDITOR.menu_handler, 'state', 'collider_menu')),
            shader =  InspectorButton(parent=self.name_field, text='shader: ', y=-4-4, on_click=Func(setattr, LEVEL_EDITOR.menu_handler, 'state', 'shader_menu')),
        )


        Entity(model=Grid(3,3), parent=self.transform_fields[0], scale=3, origin=(-.5,.5), z=-.1, color=color._64)

        self.shader_inputs_parent = Entity(parent=self.name_field, y=-9)
        self.scale = .6
        self.update_inspector()


    def input(self, key):
        if key != 'left mouse up':
            return

        if LEVEL_EDITOR.selection and (mouse.left or held_keys['d']):
            if not self.selected_entity:
                self.update_inspector()
            elif self.selected_entity != LEVEL_EDITOR.selection[0]:
                self.update_inspector()


    def update_inspector(self):
        # print('update inspector')
        self.ui.enabled = bool(LEVEL_EDITOR.selection)
        if not LEVEL_EDITOR.selection:
            return

        self.selected_entity = LEVEL_EDITOR.selection[0]
        self.fields['color'].preview.color = LEVEL_EDITOR.selection[0].color
        self.name_field.text_field.text_entity.text = self.selected_entity.name

        for i, attr_name in enumerate(('x', 'y', 'z', 'rotation_x', 'rotation_y', 'rotation_z', 'scale_x', 'scale_y', 'scale_z')):
            self.transform_fields[i].text_field.text_entity.text = str(round(getattr(self.selected_entity, attr_name),4))

        for name in ('model', 'texture', 'collider_type', 'shader'):
            unique_field_values = tuple(set([getattr(e, name) for e in LEVEL_EDITOR.selection if hasattr(e, name)]))
            if unique_field_values == ():
                text = '*error*'

            elif len(unique_field_values) == 1: # all selected entities has the same value, so draw that
                text = unique_field_values[0]
                if hasattr(text, 'name'):
                    text = text.name
            else:
                text = '--- mixed ---'

            self.fields[name].text_entity.text = (f'{name[0]}:{text}')

        [destroy(e) for e in self.shader_inputs_parent.children]
        from ursina.prefabs.vec_field import VecField

        i = 0
        if self.selected_entity.shader:
            shader_inputs = {key: value for key, value in self.selected_entity.shader.default_input.items() if key != 'shadow_color'}
            for name, value in shader_inputs.items():
                instance_value = self.selected_entity.get_shader_input(name)
                if instance_value:
                    # print('use instance value,', instance_value)
                    value = instance_value

                if isinstance(value, str):  # texture
                    b = InspectorButton(parent=self.shader_inputs_parent, text=f' {name}: {value}', highlight_color=color.black90, y=-i)
                    b.text_entity.scale *= .6
                    b.on_click = Sequence(
                        Func(setattr, LEVEL_EDITOR.menu_handler, 'state', 'texture_menu'),
                        Func(setattr, LEVEL_EDITOR.texture_menu, 'target_attr', name)
                        )


                if isinstance(value, Vec2) or (hasattr(value, '__len__') and len(value) == 2):
                    field = VecField(default_value=instance_value, parent=self.shader_inputs_parent, model='quad', scale=(1,1), x=.5, y=-i-.5, text=f'  {name}')
                    for e in field.fields:
                        e.text_field.scale *= .6
                        e.text_field.text_entity.color = color.light_gray
                    field.text_entity.scale *= .6*.75
                    field.text_entity.color = color.light_gray

                    def on_submit(name=name, field=field):
                        for e in LEVEL_EDITOR.selection:
                            # setattr(e, name, float(field.text_field.text_entity.text))
                            e.set_shader_input(name, field.value)
                    field.on_value_changed = on_submit

                # # float
                # # int
                # # Vec3

                elif isinstance(value, Color):
                    color_field = ColorField(parent=self.shader_inputs_parent, text=f' {name}', y=-i, is_shader_input=True, attr_name=name, value=value)
                    color_field.text_entity.scale *= .6


                i += 1

        i += 0
        if hasattr(self.selected_entity, 'draw_inspector'):
            divider = Entity(parent=self.shader_inputs_parent, model='quad', collider='box', origin=(-.5,.5), scale=(1,.5), color=color.black90, y=-i)
            i += 1
            # print('-------------', self.selected_entity.draw_inspector())
            for name, _type in self.selected_entity.draw_inspector().items():
                if not hasattr(self.selected_entity, name):
                    continue
                attr = getattr(self.selected_entity, name)
                if attr is False or attr is True:
                # if isinstance(attr, bool):
                    b = InspectorButton(parent=self.shader_inputs_parent, text=f' {name}:', highlight_color=color.red, y=-i, origin=(-.5,0))
                    b.text_entity.scale *= .6
                    def toggle_value(name=name):
                        new_value = not getattr(self.selected_entity, name)
                        for e in LEVEL_EDITOR.selection:
                            setattr(e, name, new_value)
                            if hasattr(e, 'generate'):
                                e.generate()

                    b.on_click = toggle_value

                elif _type in (float, int):
                    field = VecField(default_value=attr, parent=self.shader_inputs_parent, model='quad', scale=(1,1), x=.5, y=-i, text=f'  {name}')
                    for e in field.fields:
                        e.text_field.scale *= .6
                        e.text_field.text_entity.color = color.light_gray
                    field.text_entity.scale *= .6*.75
                    field.text_entity.color = color.light_gray

                    def on_submit(name=name, field=field):
                        for e in LEVEL_EDITOR.selection:
                            setattr(e, name, field.value)

                    field.on_value_changed = on_submit

                elif isinstance(_type, type):
                    text = attr
                    if hasattr(attr, 'name'):
                        text = attr.__name__

                    b = InspectorButton(parent=self.shader_inputs_parent, text=f' {name}: {text}', y=-i, origin=(-.5,0))
                    b.text_entity.scale *= .6
                    b.on_click = Func(setattr, LEVEL_EDITOR.menu_handler, 'state', 'class_menu')

                i += 1



class MenuHandler(Entity):
    def __init__(self):
        super().__init__(parent=LEVEL_EDITOR)
        self._state = None
        self.states = {
            'None' : Entity(),
            'model_menu' : LEVEL_EDITOR.model_menu,
            'texture_menu' : LEVEL_EDITOR.texture_menu,
            'shader_menu' : LEVEL_EDITOR.shader_menu,
            'color_menu' : LEVEL_EDITOR.color_menu,
            'collider_menu': LEVEL_EDITOR.collider_menu,
            'class_menu': LEVEL_EDITOR.class_menu
        }
        self.keybinds = {'m' : 'model_menu', 'v' : 'texture_menu', 'n' : 'shader_menu', 'b' : 'color_menu', 'escape' : 'None'}

    @property
    def state(self):
        return self._state

    @state.setter
    def state(self, value):
        target_state = self.states[value]
        print('toggle:', value, 'from:', self._state)
        if self._state == value:
            target_state.enabled = not target_state.enabled
            return

        for key, e in self.states.items():        # only show set state and disable the rest
            if e:
                e.enabled = value == key

        self._state = value


    def input(self, key):
        if key == 'escape' and self.state != 'None':
            self.state = 'None'
            return
        # print(key, self.keybinds)
        if self.state != 'None':
            return

        if not held_keys['control'] and not held_keys['shift'] and not held_keys['alt'] and key in self.keybinds and LEVEL_EDITOR.selection:
            self.state = self.keybinds[key]
            # print('sets state:', self.keybinds[key], self.state)

class AssetMenu(ButtonList):
    def __init__(self):
        super().__init__(button_dict=dict(), parent=LEVEL_EDITOR.ui, enabled=False, popup=True, z=-2, name=__class__.__name__, scale=.5)

    def on_enable(self):
        if not self.asset_names:
            print('no texture assets found')
        self.button_dict = {name : Func(self.on_select_asset, name) for name in self.asset_names}
        self.x = mouse.x
        self.y = mouse.y


class ModelMenu(AssetMenu):
    def on_enable(self):
        # self.model_names = [e.stem for e in application.internal_models_compressed_folder.glob('**/*.ursinamesh')]
        self.asset_names = ['None', 'cube', 'sphere', 'plane']
        for file_type in ('.bam', '.obj', '.ursinamesh'):
            self.asset_names += [e.stem for e in application.asset_folder.glob(f'**/*{file_type}') if not 'animation' in e.stem]

        super().on_enable()

    def on_select_asset(self, name):
        if name == 'None':
            name = None

        changes = []
        for e in LEVEL_EDITOR.selection:
            index = LEVEL_EDITOR.entities.index(e)
            if not e.model:
                changes.append((index, 'model', None, name))
            else:
                changes.append((index, 'model', e.model.name, name))

        # for e in LEVEL_EDITOR.selection:
        #     e.model = name
        #     if name == 'cube':
        #         e.collider = 'box'
        #     else:
        #         e.collider = None

        LEVEL_EDITOR.menu_handler.state = 'None'


class TextureMenu(AssetMenu):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.target_attr = 'texture'

    def on_enable(self):
        search_for = ''

        self.asset_names = ['None', 'white_cube', 'brick', 'grass_tintable', 'radial_gradient', 'cog']
        for file_type in ('.png', '.jpg', '.jpeg'):
            self.asset_names += [e.stem for e in application.asset_folder.glob(f'**/{search_for}*{file_type}')]

        super().on_enable()

    def on_select_asset(self, name):
        if name == 'None':
            name = None

        if self.target_attr == 'texture':
            LEVEL_EDITOR.current_scene.undo.record_undo([(LEVEL_EDITOR.entities.index(e), 'texture', e.texture, name) for e in LEVEL_EDITOR.selection])
            for e in LEVEL_EDITOR.selection:
                e.texture = name
        else:
            LEVEL_EDITOR.current_scene.undo.record_undo([(LEVEL_EDITOR.entities.index(e), self.target_attr, e.get_shader_input(self.target_attr), name) for e in LEVEL_EDITOR.selection])
            for e in LEVEL_EDITOR.selection:
                e.set_shader_input(self.target_attr, name)

        LEVEL_EDITOR.inspector.update_inspector()
        LEVEL_EDITOR.menu_handler.state = 'None'


class ShaderMenu(AssetMenu):
    def on_enable(self):
        self.asset_names = [
            'unlit_shader',
            'lit_with_shadows_shader',
            'triplanar_shader',
            'matcap_shader',
            'normals_shader',
        ]
        super().on_enable()

    def on_select_asset(self, name):
        LEVEL_EDITOR.menu_handler.state = 'None'
        LEVEL_EDITOR.current_scene.undo.record_undo([(LEVEL_EDITOR.entities.index(e), 'shader', e.shader, name) for e in LEVEL_EDITOR.selection])
        for e in LEVEL_EDITOR.selection:
            exec(f'from ursina.shaders import {name}')
            exec(f'e.shader = {name}')
        LEVEL_EDITOR.inspector.update_inspector()


class ColorMenu(Entity):
    def __init__(self):
        super().__init__(parent=LEVEL_EDITOR.ui, enabled=False)
        self.bg = Entity(parent=self, collider='box', z=.1, color=color.black, alpha=.8, origin=(-.5,.5), scale=(.6,.15), position=(-.05,.03), model=Quad(aspect=.6/.15))

        self.h_slider = Slider(name='h', min=0, max=360, step=1, text='h', dynamic=True, world_parent=self, on_value_changed=self.on_slider_changed)
        self.h_slider.bg.color = color.white
        self.h_slider.bg.texture = 'rainbow'
        self.h_slider.bg.texture.filtering = True

        self.s_slider = Slider(name='s', min=0, max=100, step=1, default=50, text='s', dynamic=True, world_parent=self, on_value_changed=self.on_slider_changed)
        self.s_slider.bg.color = color.white
        self.s_slider.bg.model.colors = [color.white for i in self.s_slider.bg.model.vertices]

        self.v_slider = Slider(name='v', min=0, max=100, default=50, step=1, text='v', dynamic=True, world_parent=self, on_value_changed=self.on_slider_changed)
        self.v_slider.bg.model.colors = [color.black for i in self.v_slider.bg.model.vertices]
        self.v_slider.bg.color = color.white

        self.a_slider = Slider(name='a', min=0, max=100, default=100, step=1, text='a', dynamic=True, world_parent=self, on_value_changed=self.on_slider_changed)
        self.a_slider.bg.model.colors = [color.white for i in self.a_slider.bg.model.vertices]
        self.a_slider.bg.color = color.white
        for i, v in enumerate(self.a_slider.bg.model.vertices):
            if v[0] < 0:
                self.a_slider.bg.model.colors[i] = color.clear
        self.a_slider.bg.model.generate()

        for i, e in enumerate((self.h_slider, self.s_slider, self.v_slider, self.a_slider)):
            e.y = -i * .03
            e.knob.color = color.white

        self.scale *= .5

        self.bg = Entity(parent=self, model='quad', collider='box', visible_self=False, scale=10, z=1, on_click=self.close)
        self.apply_color = True     # set to False when you want to move the sliders but not update the color of the entities.


    def on_slider_changed(self):
        value = color.hsv(self.h_slider.value, self.s_slider.value/100, self.v_slider.value/100, self.a_slider.value/100)

        if self.apply_color:
            LEVEL_EDITOR.inspector.fields['color'].preview.color = value
            if not LEVEL_EDITOR.inspector.fields['color'].is_shader_input:
                for e in LEVEL_EDITOR.selection:
                    e.color = value
            else:
                # print('is shader input, set', inspector.fields['color'].attr_name)
                for e in LEVEL_EDITOR.selection:
                    e.set_shader_input(LEVEL_EDITOR.inspector.fields['color'].attr_name, value)


        for i, v in enumerate(self.s_slider.bg.model.vertices):
            if v[0] < 0:
                self.s_slider.bg.model.colors[i] = color.gray
            else:
                self.s_slider.bg.model.colors[i] = color.hsv(value.h, 1, value.v)

        self.s_slider.bg.model.generate()

        for i, v in enumerate(self.v_slider.bg.model.vertices):
            if v[0] > 0:
                self.v_slider.bg.model.colors[i] = color.hsv(value.h, value.s, 1)

        self.v_slider.bg.model.generate()

        self.a_slider.bg.color = value


    def on_enable(self):
        for e in LEVEL_EDITOR.selection:
            e.original_color = e.color

        self.apply_color = False
        self.h_slider.value = LEVEL_EDITOR.inspector.fields['color'].preview.color.h
        self.s_slider.value = LEVEL_EDITOR.inspector.fields['color'].preview.color.s * 100
        self.v_slider.value = LEVEL_EDITOR.inspector.fields['color'].preview.color.v * 100
        self.a_slider.value = LEVEL_EDITOR.inspector.fields['color'].preview.color.a * 100
        self.apply_color = True


    def close(self):
        LEVEL_EDITOR.menu_handler.state = 'None'
        LEVEL_EDITOR.current_scene.undo.record_undo([(LEVEL_EDITOR.entities.index(e), 'color', e.original_color, e.color) for e in LEVEL_EDITOR.selection])


class ColliderMenu(AssetMenu):
    def on_enable(self):
        self.asset_names = ['None', 'box', 'sphere', 'mesh', ]
        super().on_enable()

    def on_select_asset(self, name):
        if name == 'None':
            name = None
        # LEVEL_EDITOR.current_scene.undo.record_undo([(LEVEL_EDITOR.entities.index(e), 'collider', e.texture, name) for e in LEVEL_EDITOR.selection])
        for e in LEVEL_EDITOR.selection:
            e.collider_type = name

        LEVEL_EDITOR.inspector.update_inspector()
        LEVEL_EDITOR.menu_handler.state = 'None'


class ClassMenu(AssetMenu):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.available_classes = {'None': None}

    def on_enable(self):
        self.asset_names = self.available_classes.keys()
        super().on_enable()

    def on_select_asset(self, name):
        # LEVEL_EDITOR.current_scene.undo.record_undo([(LEVEL_EDITOR.entities.index(e), 'collider', e.texture, name) for e in LEVEL_EDITOR.selection])
        for e in LEVEL_EDITOR.selection:
            if hasattr(e, 'class_to_spawn'):
                e.class_to_spawn = name

        LEVEL_EDITOR.inspector.update_inspector()
        LEVEL_EDITOR.menu_handler.state = 'None'


class Help(Button):
    def __init__(self, **kwargs):
        super().__init__(parent=LEVEL_EDITOR.ui, text='?', scale=.025, model='circle', origin=(-.5,.5), text_origin=(0,0), position=window.top_left)
        self.tooltip = Text(
            position=self.position + Vec3(.05,-.05,-10),
            # wordwrap=0,
            font=Text.default_monospace_font,
            enabled=False,
            text=dedent('''
                Hotkeys:
                n:          add new cube

                d:          quick drag
                w:          move tool
                x/y/z:      hold to quick move on axis

                c:          quick rotate on y axis
                t:          tilt

                e:          scale tool
                s:          quick scale
                s + x/y/z:  quick scale on axis

                f:          move editor camera to point
                shift+f:    reset editor camera position
                shift+p:    toggle perspective/orthographic
                shift+d:    duplicate
            ''').strip(),
            background=True,
            scale=.5
        )
        self.tooltip.background.color = color.black
        self.tooltip.original_scale = .75


class Duplicator(Entity):
    def __init__(self, **kwargs):
        super().__init__(parent=LEVEL_EDITOR, clones=None)
        self.plane = Entity(model='plane', collider='box', scale=Vec3(100,.1,100), enabled=False, visible=False)
        self.dragger = Draggable(parent=scene, model=None, collider=None, enabled=False)
        self.dragging = False
        self.start_position = None
        self.clone_from_position = None
        self.axis_lock = None
        self.axis_lock_gizmos = [
            Entity(model='cube', scale=Vec3(100,.01,.01), color=color.magenta, parent=self.dragger, unlit=True, enabled=False),
            Entity(model='cube', scale=Vec3(.01,100,.01), color=color.yellow, parent=self.dragger, unlit=True, enabled=False),
            Entity(model='cube', scale=Vec3(.01,.01,100), color=color.cyan, parent=self.dragger, unlit=True, enabled=False),
        ]

    def update(self):
        if self.plane.enabled and mouse.world_point:
            self.dragger.position = mouse.world_point
            if self.axis_lock is not None:

                self.axis_lock_gizmos[self.axis_lock].enabled = True
                if self.axis_lock == 0:
                    self.dragger.z = self.start_position.z
                if self.axis_lock == 2:
                    self.dragger.x = self.start_position.x


    def input(self, key):
        combined_key = input_handler.get_combined_key(key)
        if combined_key == 'shift+d' and LEVEL_EDITOR.selection:
            # print('duplicate')

            LEVEL_EDITOR.menu_handler.state = 'None'
            self.clones = []
            for e in LEVEL_EDITOR.selection:
                print(repr(e))
                clone = deepcopy(e)
                clone.original_parent = e.parent
                clone.color = e.color
                clone.shader = e.shader
                clone.origin = e.origin
                clone.selectable = True
                for key, value in e._shader_inputs.items():
                    clone.set_shader_input(key, value)

                clone.collision = False
                clone.collider_type = e.collider_type
                self.clones.append(clone)

            LEVEL_EDITOR.entities.extend(self.clones)
            LEVEL_EDITOR.selection = self.clones
            LEVEL_EDITOR.current_scene.undo.record_undo(('delete entities', [LEVEL_EDITOR.entities.index(en) for en in self.clones], [repr(e) for e in self.clones],))

            self.clone_from_position = self.clones[-1].position
            self.plane.y = LEVEL_EDITOR.selection[-1].world_y
            self.plane.enabled = True

            mouse.traverse_target = self.plane
            mouse.update()
            self.start_position = mouse.world_point
            self.dragger.world_position = self.start_position
            self.dragger.enabled = True
            self.axis_lock = None

            for e in LEVEL_EDITOR.selection:
                e.world_parent = self.dragger


        elif self.plane.enabled and key == 'left mouse up':
            for e in self.clones:
                e.world_parent = e.original_parent

            self.plane.enabled = False
            self.dragger.enabled = False
            self.clones = []
            mouse.traverse_target = scene
            [e.disable() for e in self.axis_lock_gizmos]
            LEVEL_EDITOR.render_selection()

        elif self.plane.enabled and key == 'middle mouse down':
            if self.axis_lock == None:
                delta_position = (abs(self.dragger.x-self.start_position.x), abs(self.dragger.y-self.start_position.y), abs(self.dragger.z-self.start_position.z))
                max_val = max(delta_position)
                self.axis_lock = delta_position.index(max_val)
                for e in self.axis_lock_gizmos:
                    e.world_position = self.clones[-1].world_position
                # print('lock on axis:', delta_position, max_val, self.axis_lock)
            else:
                self.axis_lock = None



class SunHandler(Entity):
    def __init__(self, **kwargs):
        super().__init__(parent=LEVEL_EDITOR, **kwargs)
        self.sun = DirectionalLight(shadow_map_resolution=(2048,2048))
        self.sun.look_at(Vec3(-2,-1,-1))
        # self.update_bounds()

    def update_bounds(self, entity=None):
        if entity is None:
            if not LEVEL_EDITOR.current_scene:
                return
            entity = LEVEL_EDITOR.current_scene.scene_parent
        self.sun.update_bounds(entity)

    def input(self, key):
        if key == 'l':
            print('toggle sun')
            self.update_bounds()


from ursina.prefabs.radial_menu import RadialMenu
class RightClickMenu(Entity):
    def __init__(self):
        super().__init__(parent=LEVEL_EDITOR.ui)
        self.radial_menu = RadialMenu(
            parent=LEVEL_EDITOR.ui,
            buttons = (
                Button(highlight_color=color.azure, model='circle', text='Model', scale=1.5, on_click=Func(setattr, LEVEL_EDITOR.menu_handler, 'state', 'model_menu')),
                Button(highlight_color=color.azure, model='circle', text='Tex', scale=1.5, on_click=Sequence(Func(setattr, LEVEL_EDITOR.menu_handler, 'state', 'texture_menu'), Func(setattr, LEVEL_EDITOR.texture_menu, 'target_attr', 'texture'))),
                Button(highlight_color=color.azure, model='circle', text='Col', scale=1.5, on_click=Sequence(Func(setattr, LEVEL_EDITOR.menu_handler, 'state', 'color_menu'), Func(setattr, LEVEL_EDITOR.color_menu, 'position', mouse.position))),
                Button(highlight_color=color.azure, model='circle', text='Sh',  scale=1.5, on_click=Func(setattr, LEVEL_EDITOR.menu_handler, 'state', 'shader_menu')),
                Button(highlight_color=color.black, model='circle', text='del', scale=.75, color=color.red, on_click=LEVEL_EDITOR.deleter.delete_selected),
                Button(highlight_color=color.azure, model='circle', text='Coll', scale=1.5),
            ),
            enabled=False,
            scale=.05
        )

    def input(self, key):
        if key == 'right mouse down':
            self.start_click_pos = mouse.position

        if key == 'right mouse up':
            if LEVEL_EDITOR.selection and sum(abs(e) for e in mouse.position-self.start_click_pos) < .005 and LEVEL_EDITOR.selector.get_hovered_entity() in LEVEL_EDITOR.selection:
                self.radial_menu.enabled = True



class Search(Entity):
    def __init__(self, **kwargs):
        super().__init__(parent=LEVEL_EDITOR.ui, **kwargs)
        self.input_field = InputField(parent=LEVEL_EDITOR.ui, enabled=False)

    def input(self, key):
        if key == 'space' and LEVEL_EDITOR.selection:
            self.input_field.enabled = True
            self.input_field.text = ''

        # elif len(key) == 1:
        #     print('---', self.input_field.text)

def get_major_axis_relative_to_view(entity): # if we're looking at the entity from the right/left, return 0, top/bot:1, front/back: 2
    r = round(camera.back.dot(entity.right), 1)
    u = round(camera.back.dot(entity.up), 1)
    f = round(camera.back.dot(entity.forward), 1)
    dir = (r, u, f)
    axis_index = dir.index(max(dir, key=abs))
    is_positive_direction = dir[axis_index] > 0

    return axis_index, is_positive_direction


if __name__ == '__main__':
    from ursina import *
    # from ursina.editor.level_editor import LevelEditor

    app = Ursina(vsync=False)

    class Tree(Entity):
        def __init__(self, **kwargs):
            super().__init__(**kwargs)
            self.model = 'cube'
            self.color = color.brown

            self.top = Entity(name='tree_top', parent=self, y=1.5, model='cube', color=color.green, selectable=True)
            LEVEL_EDITOR.entities.append(self.top)


    level_editor = LevelEditor()
    # level_editor.goto_scene(0,0)


    from ursina.prefabs.first_person_controller import FirstPersonController
    level_editor.class_menu.available_classes |= {'WhiteCube': WhiteCube, 'EditorCamera':EditorCamera, 'FirstPersonController':FirstPersonController}
    from ursina.shaders import ssao_shader
    # camera.clip_plane_far = 100
    # camera.clip_plane_near = 1
    # camera.shader = ssao_shader
    app.run()


    # level_editor.prefabs.append(Tree)
    # level_editor.spawner.update_menu()

    # level_editor.selection = [level_editor.entities[0], ]
    # window.center_on_screen()
    # Sky()
