import bpy
import numpy as np
import mathutils
from .utils import handle_vec, SelectObjects, animatable_property
from .bsyn_object import BsynObject
from typing import Union
from .mesh import Mesh
from .curve import Curve
from ..utils import types


def _look_at_rotation(
    obj_camera: bpy.types.Object,
    at: mathutils.Vector = mathutils.Vector((0, 0, 0)),
    up: mathutils.Vector = mathutils.Vector((0, 0, 1)),
) -> mathutils.Euler:
    """Rotate camera to look at 'at', with 'up' maintained.

    :param obj_camera: Camera object
    :param at: Point to look at
    :param up: Up vector"""

    camera_position = obj_camera.location

    z_axis = -(
        at - camera_position
    ).normalized()  # Blender forwards is '-z', so use -ve here
    x_axis = up.cross(z_axis)
    y_axis = z_axis.cross(x_axis).normalized()
    is_close = all(abs(i) < 5e-3 for i in x_axis)

    if is_close:
        replacement = y_axis.cross(z_axis).normalized()
        x_axis = replacement

    R = mathutils.Matrix([x_axis, y_axis, z_axis]).transposed()
    euler = R.to_euler()
    return euler


class Camera(BsynObject):
    """Camera object, to handle movement, tracking, etc."""

    def __init__(self, camera: bpy.types.Object = None):
        if camera is None:
            camera = bpy.context.scene.camera

        self.camera = camera
        self._object = camera

    @classmethod
    def create(
        cls,
        name: str = "Camera",
        location: Union[list, tuple, np.ndarray] = None,
        rotation: Union[list, tuple, np.ndarray] = None,
    ) -> "Camera":
        """Create a new camera object.

        :param name: Name of camera
        :param location: Location of camera
        :param rotation_euler: Rotation of camera in euler angles"""

        with SelectObjects():  # revert selection after creating camera
            bpy.ops.object.camera_add(enter_editmode=False, align="VIEW")
            camera = bpy.context.object
            camera.name = name

        cam = cls(camera)

        if location is not None:
            cam.set_location(location)

        if rotation is not None:
            cam.set_rotation_euler(rotation)

        return cam

    @classmethod
    def from_scene(cls, name: str = "Camera") -> "Camera":
        """Create from a named camera in scene

        :param name: Name of camera"""
        assert name in bpy.data.objects, f"Camera {name} does not exist"
        return cls(bpy.data.objects[name])

    def update(self):
        """Update view layer and depsgraph"""
        bpy.context.view_layer.update()
        bpy.context.evaluated_depsgraph_get()

    @property
    def fov(self):
        """Field of view in degrees"""
        return self.camera.data.angle * 180 / np.pi

    @fov.setter
    def fov(self, fov):
        self.set_fov(fov)

    # FOV is not animatable, so keyframe focal length instead
    @animatable_property("lens", "data")
    def set_fov(self, fov):
        self.camera.data.angle = fov * np.pi / 180
        self.update()

    @property
    def focal_length(self):
        """Focal length in mm"""
        return self.camera.data.lens

    @focal_length.setter
    def focal_length(self, focal_length):
        self.set_focal_length(focal_length)

    @animatable_property("lens", "data")
    def set_focal_length(self, focal_length):
        self.camera.data.lens = focal_length
        self.update()

    @property
    def clip_start(self):
        return self.camera.data.clip_start

    @clip_start.setter
    def clip_start(self, clip_start):
        self.set_clip_start(clip_start)

    @animatable_property("clip_start", "data")
    def set_clip_start(self, clip_start):
        self.camera.data.clip_start = clip_start
        self.update()

    @property
    def clip_end(self):
        return self.camera.data.clip_end

    @clip_end.setter
    def clip_end(self, clip_end):
        self.set_clip_end(clip_end)

    @animatable_property("clip_end", "data")
    def set_clip_end(self, clip_end):
        self.camera.data.clip_end = clip_end
        self.update()

    def look_at(
        self,
        at: types.VectorLike = mathutils.Vector((0, 0, 0)),
        up: types.VectorLike = mathutils.Vector((0, 0, 1)),
    ):
        """Look at a point in space, with up vector.

        :param at: Point to look at
        :param up: Up vector"""
        at = handle_vec(at, 3)
        up = handle_vec(up, 3)

        self.rotation_euler = _look_at_rotation(self.camera, at, up)

    def look_at_object(
        self,
        obj: Union[Mesh, bpy.types.Object],
        up: types.VectorLike = mathutils.Vector((0, 0, 1)),
    ):
        """Look at an object, with up vector.

        :param obj: Object to look at
        :param up: Up vector"""

        if isinstance(obj, Mesh):
            obj = obj.obj

        self.look_at(obj.location, up)

    def place_and_rotate(self, pos, euler):
        self.location = pos
        self.euler = euler

    def place_and_look_at(self, pos, at, up=mathutils.Vector((0, 0, 1))):
        pos = handle_vec(pos)
        at = handle_vec(at)
        up = handle_vec(up)

        self.location = pos
        self.look_at(at, up)
