# -*- coding: utf-8 -*-

"""
Basic geometry functions
------------------------

Overview
^^^^^^^^

The :py:mod:`.geometry` module provides basic geometry functions for
computing 2D transformations and distances.

The following functions are available:

* :py:func:`.translate`
* :py:func:`.scale`
* :py:func:`.rotate`
* :py:func:`.colvector`
* :py:func:`.vector_norm`
* :py:func:`.vector_projection`
* :py:func:`.vector_angle`
* :py:func:`.compute_center`
* :py:func:`.compute_rect_size`
* :py:func:`.compute_distance`
* :py:func:`.compute_angle`

Reference
^^^^^^^^^

.. autofunction:: translate
.. autofunction:: scale
.. autofunction:: rotate
.. autofunction:: colvector
.. autofunction:: vector_norm
.. autofunction:: vector_projection
.. autofunction:: vector_angle
.. autofunction:: compute_center
.. autofunction:: compute_rect_size
.. autofunction:: compute_distance
.. autofunction:: compute_angle
"""

# pylint: disable=C0103

from __future__ import annotations

import numpy as np

# ===============================================================================
# Transform matrix functions
# ===============================================================================


def translate(tx: float, ty: float) -> np.ndarray:
    """Return translation matrix

    Args:
        tx: Translation along X-axis
        ty: Translation along Y-axis

    Returns:
        Translation matrix
    """
    return np.array([[1, 0, tx], [0, 1, ty], [0, 0, 1]], float)


def scale(sx: float, sy: float) -> np.ndarray:
    """Return scale matrix

    Args:
        sx: Scale along X-axis
        sy: Scale along Y-axis

    Returns:
        Scale matrix
    """
    return np.array([[sx, 0, 0], [0, sy, 0], [0, 0, 1]], float)


def rotate(alpha: float) -> np.ndarray:
    """Return rotation matrix

    Args:
        alpha: Rotation angle (in radians)

    Returns:
        Rotation matrix
    """
    return np.array(
        [
            [np.cos(alpha), -np.sin(alpha), 0],
            [np.sin(alpha), np.cos(alpha), 0],
            [0, 0, 1],
        ],
        float,
    )


def colvector(x: float, y: float) -> np.ndarray:
    """Return vector from coordinates

    Args:
        x: x-coordinate
        y: y-coordinate

    Returns:
        Vector
    """
    return np.array([x, y, 1]).T


# ===============================================================================
# Operations on vectors (from coordinates)
# ===============================================================================


def vector_norm(xa: float, ya: float, xb: float, yb: float) -> float:
    """Return vector norm

    Args:
        xa: x-coordinate of first point
        ya: y-coordinate of first point
        xb: x-coordinate of second point
        yb: y-coordinate of second point

    Returns:
        Norm of vector (xa, xb)-->(ya, yb)
    """
    return np.linalg.norm(np.array((xb - xa, yb - ya)))


def vector_projection(
    dv: np.ndarray, xa: float, ya: float, xb: float, yb: float
) -> np.ndarray:
    """Return vector projection

    Args:
        dv: vector to project
        xa: x-coordinate of first point
        ya: y-coordinate of first point
        xb: x-coordinate of second point
        yb: y-coordinate of second point

    Returns:
        Projection of *dv* on vector (xa, xb)-->(ya, yb)
    """
    assert dv.shape == (2,)
    v_ab = np.array((xb - xa, yb - ya))
    u_ab = v_ab / np.linalg.norm(v_ab)
    return np.vdot(u_ab, dv) * u_ab + np.array((xb, yb))


def vector_rotation(theta: float, dx: float, dy: float) -> tuple[float, float]:
    """Compute theta-rotation on vector

    Args:
        theta: Rotation angle
        dx: x-coordinate of vector
        dy: y-coordinate of vector

    Returns:
        Tuple of (x, y) coordinates of rotated vector
    """
    return (rotate(theta) @ colvector(dx, dy)).ravel()[:2]


def vector_angle(dx: float, dy: float) -> float:
    """Return vector angle with X-axis

    Args:
        dx: x-coordinate of vector
        dy: y-coordinate of vector

    Returns:
        Angle between vector and X-axis (in radians)
    """
    # sign(dy) ==  1 --> return Arccos()
    # sign(dy) ==  0 --> return  0 if sign(dx) ==  1
    # sign(dy) ==  0 --> return pi if sign(dx) == -1
    # sign(dy) == -1 --> return 2pi-Arccos()
    if dx == 0 and dy == 0:
        return 0.0
    else:
        sx, sy = np.sign(dx), np.sign(dy)
        acos = np.arccos(dx / np.sqrt(dx**2 + dy**2))
        return sy * (np.pi * (sy - 1) + acos) + np.pi * (1 - sy**2) * (1 - sx) * 0.5


# ===============================================================================
# Misc.
# ===============================================================================


def compute_center(x1: float, y1: float, x2: float, y2: float) -> tuple[float, float]:
    """Compute center of rectangle

    Args:
        x1: x-coordinate of top-left corner
        y1: y-coordinate of top-left corner
        x2: x-coordinate of bottom-right corner
        y2: y-coordinate of bottom-right corner

    Returns:
        Tuple of (x, y) coordinates of center
    """
    return 0.5 * (x1 + x2), 0.5 * (y1 + y2)


def compute_rect_size(
    x1: float, y1: float, x2: float, y2: float
) -> tuple[float, float]:
    """Compute rectangle size

    Args:
        x1: x-coordinate of top-left corner
        y1: y-coordinate of top-left corner
        x2: x-coordinate of bottom-right corner
        y2: y-coordinate of bottom-right corner

    Returns:
        Tuple of (width, height)
    """
    return x2 - x1, np.fabs(y2 - y1)


def compute_distance(x1: float, y1: float, x2: float, y2: float) -> float:
    """Compute distance between two points

    Args:
        x1: x-coordinate of first point
        y1: y-coordinate of first point
        x2: x-coordinate of second point
        y2: y-coordinate of second point

    Returns:
        Distance between points
    """
    return np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)


def compute_angle(
    x1: float, y1: float, x2: float, y2: float, reverse: bool = False
) -> float:
    """Compute angle between two points

    Args:
        x1: x-coordinate of first point
        y1: y-coordinate of first point
        x2: x-coordinate of second point
        y2: y-coordinate of second point
        reverse: If True, return the angle in the opposite direction

    Returns:
        Angle between points (in degrees)
    """
    sign = -1 if reverse else 1
    if x2 == x1:
        return 0.0
    else:
        return np.arctan(-sign * (y2 - y1) / (x2 - x1)) * 180.0 / np.pi
