import math
from itertools import islice

import numpy as np

import shapely
from shapely.affinity import affine_transform


def _oriented_envelope_min_area(geometry, **kwargs):
    """
    Computes the oriented envelope (minimum rotated rectangle) that encloses
    an input geometry.

    This is a fallback implementation for GEOS < 3.12 to have the correct
    minimum area behaviour.
    """
    if geometry is None:
        return None
    if geometry.is_empty:
        return shapely.from_wkt("POLYGON EMPTY")

    # first compute the convex hull
    hull = geometry.convex_hull
    try:
        coords = hull.exterior.coords
    except AttributeError:  # may be a Point or a LineString
        return hull
    # generate the edge vectors between the convex hull's coords
    edges = (
        (pt2[0] - pt1[0], pt2[1] - pt1[1])
        for pt1, pt2 in zip(coords, islice(coords, 1, None))
    )

    def _transformed_rects():
        for dx, dy in edges:
            # compute the normalized direction vector of the edge
            # vector.
            length = math.sqrt(dx**2 + dy**2)
            ux, uy = dx / length, dy / length
            # compute the normalized perpendicular vector
            vx, vy = -uy, ux
            # transform hull from the original coordinate system to
            # the coordinate system defined by the edge and compute
            # the axes-parallel bounding rectangle.
            transf_rect = affine_transform(hull, (ux, uy, vx, vy, 0, 0)).envelope
            # yield the transformed rectangle and a matrix to
            # transform it back to the original coordinate system.
            yield (transf_rect, (ux, vx, uy, vy, 0, 0))

    # check for the minimum area rectangle and return it
    transf_rect, inv_matrix = min(_transformed_rects(), key=lambda r: r[0].area)
    return affine_transform(transf_rect, inv_matrix)


_oriented_envelope_min_area_vectorized = np.frompyfunc(
    _oriented_envelope_min_area, 1, 1
)
