# SPDX-FileCopyrightText: 2011-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later

__all__ = (
    "region_2d_to_vector_3d",
    "region_2d_to_origin_3d",
    "region_2d_to_location_3d",
    "location_3d_to_region_2d",
)


def region_2d_to_vector_3d(region, rv3d, coord):
    """
    Return a direction vector from the viewport at the specific 2d region
    coordinate.

    :arg region: region of the 3D viewport, typically bpy.context.region.
    :type region: :class:`bpy.types.Region`
    :arg rv3d: 3D region data, typically bpy.context.space_data.region_3d.
    :type rv3d: :class:`bpy.types.RegionView3D`
    :arg coord: 2d coordinates relative to the region:
       (event.mouse_region_x, event.mouse_region_y) for example.
    :type coord: 2d vector
    :return: normalized 3d vector.
    :rtype: :class:`mathutils.Vector`
    """
    from mathutils import Vector

    viewinv = rv3d.view_matrix.inverted()
    if rv3d.is_perspective:
        persinv = rv3d.perspective_matrix.inverted()

        out = Vector((
            (2.0 * coord[0] / region.width) - 1.0,
            (2.0 * coord[1] / region.height) - 1.0,
            -0.5
        ))

        w = out.dot(persinv[3].xyz) + persinv[3][3]

        view_vector = ((persinv @ out) / w) - viewinv.translation
    else:
        view_vector = -viewinv.col[2].xyz

    view_vector.normalize()

    return view_vector


def region_2d_to_origin_3d(region, rv3d, coord, *, clamp=None):
    """
    Return the 3d view origin from the region relative 2d coords.

    .. note::

       Orthographic views have a less obvious origin,
       the far clip is used to define the viewport near/far extents.
       Since far clip can be a very large value,
       the result may give with numeric precision issues.

       To avoid this problem, you can optionally clamp the far clip to a
       smaller value based on the data you're operating on.

    :arg region: region of the 3D viewport, typically bpy.context.region.
    :type region: :class:`bpy.types.Region`
    :arg rv3d: 3D region data, typically bpy.context.space_data.region_3d.
    :type rv3d: :class:`bpy.types.RegionView3D`
    :arg coord: 2d coordinates relative to the region;
       (event.mouse_region_x, event.mouse_region_y) for example.
    :type coord: 2d vector
    :arg clamp: Clamp the maximum far-clip value used.
       (negative value will move the offset away from the view_location)
    :type clamp: float or None
    :return: The origin of the viewpoint in 3d space.
    :rtype: :class:`mathutils.Vector`
    """
    viewinv = rv3d.view_matrix.inverted()

    if rv3d.is_perspective:
        origin_start = viewinv.translation.copy()
    else:
        persmat = rv3d.perspective_matrix.copy()
        dx = (2.0 * coord[0] / region.width) - 1.0
        dy = (2.0 * coord[1] / region.height) - 1.0
        persinv = persmat.inverted()
        origin_start = (
            (persinv.col[0].xyz * dx) +
            (persinv.col[1].xyz * dy) +
            persinv.translation
        )

        if clamp != 0.0:
            if rv3d.view_perspective != 'CAMERA':
                # this value is scaled to the far clip already
                origin_offset = persinv.col[2].xyz
                if clamp is not None:
                    if clamp < 0.0:
                        origin_offset.negate()
                        clamp = -clamp
                    if origin_offset.length > clamp:
                        origin_offset.length = clamp

                origin_start -= origin_offset

    return origin_start


def region_2d_to_location_3d(region, rv3d, coord, depth_location):
    """
    Return a 3d location from the region relative 2d coords, aligned with
    *depth_location*.

    :arg region: region of the 3D viewport, typically bpy.context.region.
    :type region: :class:`bpy.types.Region`
    :arg rv3d: 3D region data, typically bpy.context.space_data.region_3d.
    :type rv3d: :class:`bpy.types.RegionView3D`
    :arg coord: 2d coordinates relative to the region;
       (event.mouse_region_x, event.mouse_region_y) for example.
    :type coord: 2d vector
    :arg depth_location: the returned vectors depth is aligned with this since
       there is no defined depth with a 2d region input.
    :type depth_location: 3d vector
    :return: normalized 3d vector.
    :rtype: :class:`mathutils.Vector`
    """
    from mathutils import Vector

    coord_vec = region_2d_to_vector_3d(region, rv3d, coord)
    depth_location = Vector(depth_location)

    origin_start = region_2d_to_origin_3d(region, rv3d, coord)
    origin_end = origin_start + coord_vec

    if rv3d.is_perspective:
        from mathutils.geometry import intersect_line_plane
        viewinv = rv3d.view_matrix.inverted()
        view_vec = viewinv.col[2].copy()
        return intersect_line_plane(
            origin_start,
            origin_end,
            depth_location,
            view_vec, 1,
        )
    else:
        from mathutils.geometry import intersect_point_line
        return intersect_point_line(
            depth_location,
            origin_start,
            origin_end,
        )[0]


def location_3d_to_region_2d(region, rv3d, coord, *, default=None):
    """
    Return the *region* relative 2d location of a 3d position.

    :arg region: region of the 3D viewport, typically bpy.context.region.
    :type region: :class:`bpy.types.Region`
    :arg rv3d: 3D region data, typically bpy.context.space_data.region_3d.
    :type rv3d: :class:`bpy.types.RegionView3D`
    :arg coord: 3d world-space location.
    :type coord: 3d vector
    :arg default: Return this value if ``coord``
       is behind the origin of a perspective view.
    :return: 2d location
    :rtype: :class:`mathutils.Vector` or ``default`` argument.
    """
    from mathutils import Vector

    prj = rv3d.perspective_matrix @ Vector((coord[0], coord[1], coord[2], 1.0))
    if prj.w > 0.0:
        width_half = region.width / 2.0
        height_half = region.height / 2.0

        return Vector((
            width_half + width_half * (prj.x / prj.w),
            height_half + height_half * (prj.y / prj.w),
        ))
    else:
        return default
