import time
import platform

from direct.showbase.ShowBase import ShowBase
from direct.task.Task import Task
from panda3d.core import ConfigVariableBool

from ursina.window import instance as window
from ursina import application
from ursina import input_handler
from ursina.scene import instance as scene
from ursina.camera import instance as camera
from ursina.mouse import instance as mouse
from ursina import entity


import __main__
time.dt = 0
time.dt_unscaled = 0
keyboard_keys = '1234567890qwertyuiopasdfghjklzxcvbnm'


from ursina.scripts.singleton_decorator import singleton
@singleton
class Ursina(ShowBase):
    def __init__(self, title='ursina', icon='textures/ursina.ico', borderless=True, fullscreen=False, size=None, forced_aspect_ratio=None, position=None, vsync=True, editor_ui_enabled=True, window_type='onscreen', development_mode=True, render_mode=None, show_ursina_splash=False, **kwargs):
        """The main class of Ursina. This class is a singleton, so you can only have one instance of it.

        Keyword Args (optional):
            title (str): The title of the window.\n
            fullscreen (bool): Whether the window should be fullscreen or not.\n
            size (tuple(int, int)): The size of the window.\n
            forced_aspect_ratio (bool): Whether the window should have a forced aspect ratio or not.\n
            position (tuple(int, int)): The position of the window.\n
            vsync (bool): Whether the window should have vsync enabled or not.\n
            borderless (bool): Whether the window should be borderless or not.\n
            show_ursina_splash (bool): Whether the Ursina splash should be shown or not.\n
            render_mode (str): The render mode of the window.\n
            development_mode (bool): Whether the development mode should be enabled or not.\n
            editor_ui_enabled (bool): Whether the editor UI should be enabled or not.\n
            window_type (str): The type of the window. Can be 'onscreen', 'offscreen' or 'none'.\n
        """
        entity._warn_if_ursina_not_instantiated = False
        application.window_type = window_type
        application.base = self
        application.development_mode = development_mode
        application.show_ursina_splash = show_ursina_splash

        try:
            import gltf
            gltf.patch_loader(self.loader)
        except:
            pass

        if 'gltf_no_srgb' in kwargs:
            application.gltf_no_srgb = kwargs['gltf_no_srgb']

        if not application.development_mode:
            fullscreen = True

        window.ready(title=title, icon=icon,
            borderless=borderless, fullscreen=fullscreen, size=size, forced_aspect_ratio=forced_aspect_ratio, position=position, vsync=vsync, window_type=window_type,
            editor_ui_enabled=editor_ui_enabled, render_mode=render_mode)

        super().__init__(windowType=application.window_type)
        window.apply_settings()
        # camera
        if application.window_type != 'none':
            camera._cam = self.camera
            camera._cam.reparent_to(camera)
            camera.render = self.render
            camera.position = (0, 0, -20)
            scene.camera = camera
            camera.set_up()

        # input
        if application.window_type == 'onscreen':
            self.buttonThrowers[0].node().setButtonDownEvent('buttonDown')
            self.buttonThrowers[0].node().setButtonUpEvent('buttonUp')
            self.buttonThrowers[0].node().setButtonRepeatEvent('buttonHold')
            self.buttonThrowers[0].node().setKeystrokeEvent('keystroke')
        self._input_name_changes = {
            'mouse1' : 'left mouse down', 'mouse1 up' : 'left mouse up', 'mouse2' : 'middle mouse down', 'mouse2 up' : 'middle mouse up', 'mouse3' : 'right mouse down', 'mouse3 up' : 'right mouse up',
            'wheel_up' : 'scroll up', 'wheel_down' : 'scroll down',
            'arrow_left' : 'left arrow', 'arrow_left up' : 'left arrow up', 'arrow_up' : 'up arrow', 'arrow_up up' : 'up arrow up', 'arrow_down' : 'down arrow', 'arrow_down up' : 'down arrow up', 'arrow_right' : 'right arrow', 'arrow_right up' : 'right arrow up',
            'lcontrol' : 'left control', 'rcontrol' : 'right control', 'lshift' : 'left shift', 'rshift' : 'right shift', 'lalt' : 'left alt', 'ralt' : 'right alt',
            'lcontrol up' : 'left control up', 'rcontrol up' : 'right control up', 'lshift up' : 'left shift up', 'rshift up' : 'right shift up', 'lalt up' : 'left alt up', 'ralt up' : 'right alt up',
            'control-mouse1' : 'left mouse down', 'control-mouse2' : 'middle mouse down', 'control-mouse3' : 'right mouse down',
            'shift-mouse1' : 'left mouse down', 'shift-mouse2' : 'middle mouse down', 'shift-mouse3' : 'right mouse down',
            'alt-mouse1' : 'left mouse down', 'alt-mouse2' : 'middle mouse down', 'alt-mouse3' : 'right mouse down',
            'page_down' : 'page down', 'page_down up' : 'page down up', 'page_up' : 'page up', 'page_up up' : 'page up up',
            }

        for e in keyboard_keys:
            self.accept(f'raw-{e}', self.input, [e, True])
            self.accept(f'raw-{e}-up', self.input_up, [e, True])
            self.accept(f'raw-{e}-repeat', self.input_hold, [e, True])

        self.accept('buttonDown', self.input)
        self.accept('buttonUp', self.input_up)
        self.accept('buttonHold', self.input_hold)
        self.accept('keystroke', self.text_input)
        ConfigVariableBool('paste-emit-keystrokes', False)

        self.disableMouse()
        mouse._mouse_watcher = self.mouseWatcherNode
        mouse.enabled = True
        self.mouse = mouse

        scene.set_up()
        self._update_task = self.taskMgr.add(self._update, "update")

        # try to load settings that need to be applied before entity creation
        application.load_settings()

        from ursina.prefabs.hot_reloader import HotReloader
        application.hot_reloader = HotReloader(__main__.__file__ if hasattr(__main__, '__file__') else 'None', enabled=application.development_mode)  # make sure it's running from a file and not an interactive session.

        try:
            from ursina import gamepad
        except Exception as e:
            print(e)

        if application.window_type != 'none':
            window.make_editor_gui()
            if 'use_ingame_console' in kwargs and kwargs['use_ingame_console']:
                import builtins
                from ursina import Entity, TextField, color
                window.console = Entity(parent=window.editor_ui, position=window.top_left, z=-999, eternal=True)
                window.console.text_field = TextField(parent=window.console, scale=.75, font='VeraMono.ttf', max_lines=20, position=(0,0), register_mouse_input=True, text_input=None, eternal=True)
                window.console.text_field.bg.color = color.black66
                window.console.text_field.bg.scale_x = 1.5
                def _custom_print(*args, **kwargs):  # makes print() poutput to the in-game console instead of the terminal if use_ingame_console is True
                    content = ' '.join([str(e) for e in args])
                    if 'error' in content:
                        content = f'{content}'
                    window.console.text_field.cursor.y = window.console.text_field.text.count('\n')
                    window.console.text_field.cursor.x = 999
                    window.console.text_field.add_text(f'\n{content}')
                    window.console.text_field.scroll_to_bottom(1)
                    window.console.text_field.render()
                builtins.print = _custom_print

                def _console_text_input(key):
                    if key == '|':
                        window.console.text_field.enabled = not window.console.text_field.enabled
                window.console.text_input = _console_text_input


        if 'editor_ui_enabled' in kwargs:
            window.editor_ui.enabled = kwargs['editor_ui_enabled']

        print('package_folder:', application.package_folder)
        print('asset_folder:', application.asset_folder)

        entity._Ursina_instance = self


    def _update(self, task):
        """Internal task that runs every frame. Updates time, mouse, sequences and entities."""
        if application.calculate_dt:
            time.dt_unscaled = globalClock.getDt()
            time.dt = time.dt_unscaled * application.time_scale          # time between frames
        mouse.update()

        if hasattr(__main__, 'update') and __main__.update and not application.paused:
            __main__.update()

        for seq in application.sequences:
            seq.update()

        for e in scene.entities:
            if not e.enabled or e.ignore:
                continue
            if application.paused and e.ignore_paused is False:
                continue
            if e.has_disabled_ancestor():
                continue

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

            if hasattr(e, 'scripts'):
                for script in e.scripts:
                    if script.enabled and hasattr(script, 'update') and callable(script.update):
                        script.update()

        return Task.cont


    def input_up(self, key, is_raw=False): # internal method for key release
        if not is_raw and key in keyboard_keys:
            return

        if key in ('wheel_up', 'wheel_down'):
            return

        key += ' up'
        self.input(key)


    def input_hold(self, key, is_raw=False):   # internal method for handling repeating input that occurs when you hold the key
        key = key.replace('control-', '')
        key = key.replace('shift-', '')
        key = key.replace('alt-', '')

        if key in self._input_name_changes:
            key = self._input_name_changes[key]

        key += ' hold'
        self.input(key)


    def input(self, key, is_raw=False): # internal method for handling input
        """Built-in input handler. Propagates the input to all entities and the input function of the main script. Main use case for this it to simulate input though code, like: app.input('a').

        Args:
            key (Any): The input key.
            is_raw (bool, optional): Whether or not the input should be treated as "raw". Defaults to False.
        """
        if not is_raw and key in keyboard_keys:
            return

        if 'mouse' not in key:
            for prefix in ('control-', 'shift-', 'alt-'):
                if key.startswith(prefix):
                    key = key.replace('control-', '')
                    key = key.replace('shift-', '')
                    key = key.replace('alt-', '')
                    if key in keyboard_keys:
                        return

        if key in self._input_name_changes:
            key = self._input_name_changes[key]

        # since we can rebind one key to multiple, get a list of keys
        bound_keys = input_handler.rebinds.get(key, (key, ))

        for key in bound_keys:
            input_handler.input(key)

        if not application.paused:
            if hasattr(__main__, 'input'):
                for key in bound_keys:
                    __main__.input(key)

        for e in scene.entities:
            if e.enabled is False or e.ignore or e.ignore_input:
                continue
            if application.paused and e.ignore_paused is False:
                continue
            if e.has_disabled_ancestor():
                continue


            if hasattr(e, 'input') and callable(e.input):
                break_outer = False
                for key in bound_keys:
                    if break_outer:
                        break
                    if e.input(key):    # if the input function returns True, eat the input
                        break_outer = True


            if hasattr(e, 'scripts'):
                break_outer = False
                if break_outer:
                    break

                for script in e.scripts:
                    if script.enabled and hasattr(script, 'input') and callable(script.input):
                        for key in bound_keys:
                            if script.input(key): # if the input function returns True, eat the input
                                break_outer = True
                                break

        mouse.input(key)


    def text_input(self, key):  # internal method for handling text input
        key_code = ord(key)
        if key_code < 32 or (key_code >= 127 and key_code <= 160):
            return

        if key == ' ' and input_handler.held_keys['control']:
            return

        if not application.paused:
            if hasattr(__main__, 'text_input'):
                __main__.text_input(key)

        for e in scene.entities:
            if e.enabled is False or e.ignore or e.ignore_input:
                continue
            if application.paused and e.ignore_paused is False:
                continue

            if hasattr(e, 'text_input') and callable(e.text_input):
                e.text_input(key)

            if hasattr(e, 'scripts'):
                for script in e.scripts:
                    if script.enabled and hasattr(script, 'text_input') and callable(script.text_input):
                        script.text_input(key)

    def step(self): # use this control the update loop yourself. call app.step() in a while loop for example, instead of app.run()
        self.taskMgr.step()


    def run(self, info=True):
        if application.show_ursina_splash:
            from ursina.prefabs.splash_screen import SplashScreen
            application.ursina_splash = SplashScreen()

        application.load_settings()
        if info:
            print('os:', platform.system())
            print('development mode:', application.development_mode)
            print('application successfully started')

        super().run()


if __name__ == '__main__':
    from ursina import *
    app = Ursina(use_ingame_console=True)
    def input(key):
        print(key)
    app.run()
