import time
from panda3d.core import CollisionTraverser, CollisionNode
from panda3d.core import CollisionHandlerQueue, CollisionRay
from ursina import application
from ursina.window import instance as window
from ursina.scene import instance as scene
from ursina.camera import instance as camera
from ursina.hit_info import HitInfo
from ursina.vec3 import Vec3
from ursina.ursinamath import distance


class Mouse:
    def __init__(self):
        self.enabled = False
        self.visible = True
        self.locked = False
        self._locked_mouse_last_frame = False
        self.position = Vec3(0,0,0)
        self.delta = Vec3(0,0,0)    # movement since you pressed a mouse button.
        self.prev_x = 0
        self.prev_y = 0
        self.start_x = 0
        self.start_y = 0
        self.velocity = Vec3(0,0,0)
        self.moving = False
        self.prev_click_time = time.time()
        self.prev_click_pos = None
        self.double_click_distance = .5
        self.double_click_movement_limit = .01

        self.hovered_entity = None # returns the closest hovered entity with a collider.
        self.left = False
        self.right = False
        self.middle = False
        self.delta_drag = Vec3(0,0,0)   # movement between left mouse down and left mouse up.

        self.update_step = 1
        self.traverse_target = scene  # set this to None to disable collision with scene, which might be a good idea if you have lots of colliders.
        self._i = 0
        self._mouse_watcher = None
        self._picker = CollisionTraverser()  # Make a traverser
        self._pq = CollisionHandlerQueue()  # Make a handler
        self._pickerNode = CollisionNode('mouseRay')
        self._pickerNP = camera.attach_new_node(self._pickerNode)
        self._pickerRay = CollisionRay()  # Make our ray
        self._pickerNode.addSolid(self._pickerRay)
        self._picker.addCollider(self._pickerNP, self._pq)
        self._pickerNode.set_into_collide_mask(0)

        self.raycast = True
        self.collision = None
        self.collisions = []
        self.enabled = True

    @property
    def x(self):
        pointer = base.win.get_pointer(0)
        if not pointer.in_window:
            return 0
        return ((pointer.get_x() / window.size.x) -.5) * window.aspect_ratio  # same space as ui stuff

    @x.setter
    def x(self, value):
        self.position = (value, self.y)


    @property
    def y(self):
        pointer = base.win.get_pointer(0)
        if not pointer.in_window:
            return 0
        return -((pointer.get_y() / window.size.y) -.5) # same space as ui stuff


    @y.setter
    def y(self, value):
        self.position = (self.x, value)


    @property
    def position(self):
        return Vec3(self.x, self.y, 0)

    @position.setter
    def position(self, value):
        if not application.base:
            return
        application.base.win.move_pointer(
            0,
            round(value[0] + (window.size[0]/2) + (value[0]/2*window.size[0]) *1.124), # no idea why I have * with 1.124
            round(value[1] + (window.size[1]/2) - (value[1]*window.size[1])),
        )

    @property
    def locked(self):
        if not hasattr(self, '_locked'):
            return False
        return self._locked

    @locked.setter
    def locked(self, value):
        self._locked = value
        if value:
            window.set_mouse_mode(window.M_confined)
        else:
            window.set_mouse_mode(window.M_absolute)

        if not application.base:
            return

        # print('return', value)
        # self.position = Vec3(0,0,0)
        window.set_cursor_hidden(value)
        application.base.win.requestProperties(window)
        self._locked_mouse_last_frame = True


    @property
    def visible(self):
        if not hasattr(self, '_visible'):
            return True
        return self._visible

    @visible.setter
    def visible(self, value):
        self._visible = value
        window.set_cursor_hidden(not value)
        if application.base:
            application.base.win.requestProperties(window)


    def input(self, key):
        if not self.enabled:
            return

        if key.endswith('mouse down'):
            self.start_x = self.x
            self.start_y = self.y

        elif key.endswith('mouse up'):
            self.delta_drag = Vec3(self.x-self.start_x, self.y-self.start_y, 0)

        if key == 'left mouse down':
            self.left = True
            if self.hovered_entity:
                if self.hovered_entity.on_click:
                    self.hovered_entity.on_click()

                for s in self.hovered_entity.scripts:
                    if hasattr(s, 'on_click') and s.on_click:
                        s.on_click()

            # double click
            if time.time() - self.prev_click_time <= self.double_click_distance:
                if self.prev_click_pos and (abs(self.x-self.prev_click_pos[0]) > self.double_click_movement_limit or abs(self.y-self.prev_click_pos[1]) > self.double_click_movement_limit):
                    return # moused moved too much since previous click, so don't register double click.

                application.base.input('double click')
                if self.hovered_entity:
                    if hasattr(self.hovered_entity, 'on_double_click') and self.hovered_entity.on_double_click:
                            self.hovered_entity.on_double_click()

                    for s in self.hovered_entity.scripts:
                        if hasattr(s, 'on_double_click') and s.on_double_click:
                            s.on_double_click()

            self.prev_click_time = time.time()
            self.prev_click_pos = (self.x, self.y)


        if key == 'left mouse up':
            self.left = False
        if key == 'right mouse down':
            self.right = True
        if key == 'right mouse up':
            self.right = False
        if key == 'middle mouse down':
            self.middle = True
        if key == 'middle mouse up':
            self.middle = False



    def update(self):
        if application.window_type != 'onscreen':
            return

        if not self.enabled or not self._mouse_watcher.has_mouse() or self._locked_mouse_last_frame:
            self.velocity = Vec3(0,0,0)
            self._locked_mouse_last_frame = False
            return

        self.moving = self.x + self.y != self.prev_x + self.prev_y

        if self.moving:
            if self.locked:
                self.velocity = self.position
                self.position = (0,0)
            else:
                self.velocity = Vec3(self.x - self.prev_x, (self.y - self.prev_y) / window.aspect_ratio ,0)
        else:
            self.velocity = Vec3(0,0,0)

        if self.left or self.right or self.middle:
            self.delta = Vec3(self.x - self.start_x, self.y - self.start_y, 0)

        self.prev_x = self.x
        self.prev_y = self.y


        self._i += 1
        if self._i < self.update_step:
            return
        # collide with ui
        self._pickerNP.reparent_to(scene.ui_camera)
        self._pickerRay.set_from_lens(camera._ui_lens_node, self.x * 2 / window.aspect_ratio, self.y * 2)
        self._picker.traverse(camera.ui)
        if self._pq.get_num_entries() > 0:
            # print('collided with ui', self._pq.getNumEntries())
            self.find_collision()
            return

        # collide with world
        self._pickerNP.reparent_to(camera)
        self._pickerRay.set_from_lens(scene.camera.lens_node, self.x * 2 / window.aspect_ratio, self.y * 2)
        if self.traverse_target:
            self._picker.traverse(self.traverse_target)

        if self._pq.get_num_entries() > 0:
            self.find_collision()
        else:
            self.collision = None
            # print('mouse miss', base.render)
            # unhover all if it didn't hit anything
            for entity in scene.entities:
                if hasattr(entity, 'hovered') and entity.hovered:
                    entity.hovered = False
                    self.hovered_entity = None
                    if hasattr(entity, 'on_mouse_exit'):
                        entity.on_mouse_exit()
                    for s in entity.scripts:
                        if hasattr(s, 'on_mouse_exit'):
                            s.on_mouse_exit()

    @property
    def normal(self): # returns the normal of the polygon, in local space.
        if not self.collision is not None:
            return None
        return Vec3(*self.collision.normal)

    @property
    def world_normal(self): # returns the normal of the polygon, in world space.
        if not self.collision is not None:
            return None
        return Vec3(*self.collision.world_normal)

    @property
    def point(self): # returns the point hit, in local space
        if self.collision is not None:
            return Vec3(*self.collision.point)
        return None

    @property
    def world_point(self): # returns the point hit, in world space
        if self.collision is not None:
            return Vec3(*self.collision.world_point)
        return None

    @property
    def is_outside(self):
        return not self._mouse_watcher.has_mouse()


    def find_collision(self):
        self.collisions = []
        self.collision = None
        if not self.raycast or self._pq.get_num_entries() == 0:
            self.unhover_everything_not_hit()
            return False

        self._pq.sortEntries()

        for entry in self._pq.getEntries():
            for entity in scene.entities:
                if entry.getIntoNodePath().parent == entity and entity.collision:
                    if entity.collision:
                        hit = HitInfo(
                            hit = entry.collided(),
                            entity = entity,
                            distance = distance(entry.getSurfacePoint(scene), camera.getPos()),
                            point = entry.getSurfacePoint(entity),
                            world_point = entry.getSurfacePoint(scene),
                            normal = entry.getSurfaceNormal(entity),
                            world_normal = entry.getSurfaceNormal(scene),
                            )
                        self.collisions.append(hit)
                        break

        if self.collisions:
            self.collision = self.collisions[0]
            self.hovered_entity = self.collision.entity
            if not self.hovered_entity.hovered:
                self.hovered_entity.hovered = True
                if hasattr(self.hovered_entity, 'on_mouse_enter'):
                    self.hovered_entity.on_mouse_enter()
                for s in self.hovered_entity.scripts:
                    if hasattr(s, 'on_mouse_enter'):
                        s.on_mouse_enter()


        self.unhover_everything_not_hit()



    def unhover_everything_not_hit(self):
        for e in scene.entities:
            if e == self.hovered_entity:
                continue

            if hasattr(e, 'hovered') and e.hovered:
                e.hovered = False
                if hasattr(e, 'on_mouse_exit'):
                    e.on_mouse_exit()
                for s in e.scripts:
                    if hasattr(s, 'on_mouse_exit'):
                        s.on_mouse_exit()



instance = Mouse()


if __name__ == '__main__':
    from ursina import *
    from ursina import Ursina, Button, mouse
    app = Ursina()
    Button(parent=scene, text='a')

    def input(key):
        if key == 'space':
            mouse.locked = not mouse.locked
            print(mouse.velocity)

    Cursor()
    # mouse.visible = False


    app.run()
