from collections.abc import Callable, Iterable, Sequence
from functools import partial, singledispatch
from operator import is_
from typing import Any, TypeVar

from hypothesis import strategies as _st

from rene import exact
from rene._utils import deduplicate, locate_point_in_point_point_point_circle
from rene.constants import MIN_CONTOUR_VERTICES_COUNT
from rene.enums import Location, Orientation

_BoxT = TypeVar('_BoxT', bound=exact.Box)
_ContourT = TypeVar('_ContourT', bound=exact.Contour)
_EmptyT = TypeVar('_EmptyT', bound=exact.Empty)
_MultipolygonT = TypeVar('_MultipolygonT', bound=exact.Multipolygon)
_MultisegmentT = TypeVar('_MultisegmentT', bound=exact.Multisegment)
_PointT = TypeVar('_PointT', bound=exact.Point)
_PolygonT = TypeVar('_PolygonT', bound=exact.Polygon)
_SegmentT = TypeVar('_SegmentT', bound=exact.Segment)
_T1 = TypeVar('_T1')
_T2 = TypeVar('_T2')


def apply(function: Callable[..., _T2], args: Iterable[_T1], /) -> _T2:
    return function(*args)


def are_sequences_equivalent(
    first: Sequence[Any], second: Sequence[Any], /
) -> bool:
    return len(first) == len(second) and all(
        first[index] == second[index] for index in range(len(first))
    )


equivalence = is_


def implication(antecedent: bool, consequent: bool, /) -> bool:
    return not antecedent or consequent


def is_contour_triangular(contour: exact.Contour, /) -> bool:
    return len(contour.vertices) == MIN_CONTOUR_VERTICES_COUNT


def is_multisegment_inside_box(
    multisegment: _MultisegmentT, box: _BoxT, /
) -> bool:
    return all(
        is_segment_inside_box(segment, box)
        for segment in multisegment.segments
    )


def is_point_inside_box(point: _PointT, box: _BoxT, /) -> bool:
    return (
        box.min_x <= point.x <= box.max_x and box.min_y <= point.y <= box.max_y
    )


def is_point_inside_circumcircle(
    point: _PointT,
    first_vertex: _PointT,
    second_vertex: _PointT,
    third_vertex: _PointT,
    /,
) -> bool:
    return (
        locate_point_in_point_point_point_circle(
            point, first_vertex, second_vertex, third_vertex
        )
        is Location.INTERIOR
    )


def is_segment_inside_box(segment: _SegmentT, box: _BoxT, /) -> bool:
    return is_point_inside_box(segment.start, box) and is_point_inside_box(
        segment.end, box
    )


def pack(function: Callable[..., _T2], /) -> Callable[[Iterable[_T1]], _T2]:
    return partial(apply, function)


def reverse_box_coordinates(box: _BoxT, /) -> _BoxT:
    return type(box)(box.min_y, box.max_y, box.min_x, box.max_x)


@singledispatch
def reverse_compound_coordinates(compound: Any, /) -> Any:
    raise TypeError(f'Unsupported type: {type(compound)!r}.')


@reverse_compound_coordinates.register(exact.Empty)
def _(compound: exact.Empty, /) -> exact.Empty:
    return compound


@reverse_compound_coordinates.register(exact.Contour)
def _(compound: exact.Contour, /) -> exact.Contour:
    return reverse_contour_coordinates(compound)


@reverse_compound_coordinates.register(exact.Multipolygon)
def _(compound: exact.Multipolygon, /) -> exact.Multipolygon:
    return reverse_multipolygon_coordinates(compound)


@reverse_compound_coordinates.register(exact.Multisegment)
def _(compound: exact.Multisegment, /) -> exact.Multisegment:
    return reverse_multisegment_coordinates(compound)


@reverse_compound_coordinates.register(exact.Polygon)
def _(compound: exact.Polygon, /) -> exact.Polygon:
    return reverse_polygon_coordinates(compound)


@reverse_compound_coordinates.register(exact.Segment)
def _(compound: exact.Segment, /) -> exact.Segment:
    return reverse_segment_coordinates(compound)


def reverse_contour_vertices(contour: _ContourT, /) -> _ContourT:
    vertices = contour.vertices
    return type(contour)(reverse_sequence(vertices))


_T = TypeVar('_T')


def reverse_contour_coordinates(contour: _ContourT, /) -> _ContourT:
    reversed_vertices = [
        reverse_point_coordinates(vertex) for vertex in contour.vertices
    ]
    result = type(contour)(reversed_vertices)
    if result.orientation is not contour.orientation:
        result = type(contour)(
            [*reversed_vertices[:1], *reversed_vertices[:0:-1]]
        )
    return result


def reverse_multipolygon_polygons(
    multipolygon: _MultipolygonT, /
) -> _MultipolygonT:
    return type(multipolygon)(reverse_sequence(multipolygon.polygons))


def reverse_multipolygon_coordinates(
    multipolygon: _MultipolygonT, /
) -> _MultipolygonT:
    return type(multipolygon)(
        [
            reverse_polygon_coordinates(polygon)
            for polygon in multipolygon.polygons
        ]
    )


def reverse_multisegment(multisegment: _MultisegmentT, /) -> _MultisegmentT:
    return type(multisegment)(reverse_sequence(multisegment.segments))


def reverse_multisegment_coordinates(
    multisegment: _MultisegmentT, /
) -> _MultisegmentT:
    return type(multisegment)(
        [
            reverse_segment_coordinates(segment)
            for segment in multisegment.segments
        ]
    )


def reverse_point_coordinates(point: _PointT, /) -> _PointT:
    return type(point)(point.y, point.x)


def reverse_polygon_coordinates(polygon: _PolygonT, /) -> _PolygonT:
    return type(polygon)(
        reverse_contour_coordinates(polygon.border),
        [reverse_contour_coordinates(hole) for hole in polygon.holes],
    )


def reverse_polygon_holes(polygon: _PolygonT, /) -> _PolygonT:
    return type(polygon)(polygon.border, reverse_sequence(polygon.holes))


def reverse_segment_coordinates(segment: _SegmentT, /) -> _SegmentT:
    return type(segment)(
        reverse_point_coordinates(segment.start),
        reverse_point_coordinates(segment.end),
    )


def reverse_segment_endpoints(segment: _SegmentT, /) -> _SegmentT:
    return type(segment)(segment.end, segment.start)


def reverse_sequence(value: Sequence[_T], /) -> Sequence[_T]:
    return list(reversed(value))


def rotate_contour(contour: _ContourT, offset: int, /) -> _ContourT:
    return type(contour)(rotate_sequence(contour.vertices, offset))


def rotate_each_polygon_hole(polygon: _PolygonT, offset: int, /) -> _PolygonT:
    return type(polygon)(
        polygon.border,
        [rotate_contour(hole, offset) for hole in polygon.holes],
    )


def rotate_multipolygon(
    multipolygon: _MultipolygonT, offset: int, /
) -> _MultipolygonT:
    return type(multipolygon)(rotate_sequence(multipolygon.polygons, offset))


def rotate_multisegment(
    multisegment: _MultisegmentT, offset: int, /
) -> _MultisegmentT:
    return type(multisegment)(rotate_sequence(multisegment.segments, offset))


def rotate_polygon_border(polygon: _PolygonT, offset: int, /) -> _PolygonT:
    return type(polygon)(rotate_contour(polygon.border, offset), polygon.holes)


def rotate_polygon_holes(polygon: _PolygonT, offset: int, /) -> _PolygonT:
    return type(polygon)(
        polygon.border, rotate_sequence(polygon.holes, offset)
    )


def rotate_sequence(sequence: Sequence[_T1], offset: int, /) -> list[_T1]:
    if not sequence:
        return []
    offset %= len(sequence)
    return [
        *[sequence[index] for index in range(offset, len(sequence))],
        *[sequence[index] for index in range(offset)],
    ]


_Orienteer = Callable[[_PointT, _PointT, _PointT], Orientation]


def to_convex_hull(
    points: Sequence[_PointT], orienteer: _Orienteer[_PointT], /
) -> list[_PointT]:
    points = deduplicate(sorted(points))
    lower, upper = (
        _to_sub_hull(points, orienteer),
        _to_sub_hull(reversed(points), orienteer),
    )
    return lower[:-1] + upper[:-1] or points


to_distinct = dict.fromkeys


def to_max_convex_hull(
    points: Sequence[_PointT], orienteer: _Orienteer[_PointT], /
) -> list[_PointT]:
    points = deduplicate(sorted(points))
    lower, upper = (
        _to_max_sub_hull(points, orienteer),
        _to_max_sub_hull(reversed(points), orienteer),
    )
    return lower[:-1] + upper[:-1] or points


def to_pairs(
    values: _st.SearchStrategy[_T], /
) -> _st.SearchStrategy[tuple[_T, _T]]:
    return _st.tuples(values, values)


def to_triplets(
    values: _st.SearchStrategy[_T], /
) -> _st.SearchStrategy[tuple[_T, _T, _T]]:
    return _st.tuples(values, values, values)


def _to_max_sub_hull(
    points: Iterable[_PointT], orienteer: _Orienteer[_PointT], /
) -> list[_PointT]:
    result: list[_PointT] = []
    for point in points:
        while len(result) >= 2:
            if (
                orienteer(result[-2], result[-1], point)
                is Orientation.CLOCKWISE
            ):
                del result[-1]
            else:
                break
        result.append(point)
    return result


def _to_sub_hull(
    points: Iterable[_PointT], orienteer: _Orienteer[_PointT], /
) -> list[_PointT]:
    result: list[_PointT] = []
    for point in points:
        while len(result) >= 2:
            if (
                orienteer(result[-2], result[-1], point)
                is not Orientation.COUNTERCLOCKWISE
            ):
                del result[-1]
            else:
                break
        result.append(point)
    return result
