from typing import Dict, List, Sequence, TypeVar, Union

from turf.bbox import bbox
from turf.explode import explode
from turf.nearest_point import nearest_point

from turf.helpers import (
    all_geometry_types,
    FeatureCollection,
    MultiPolygon,
    Point,
    Polygon,
)
from turf.helpers import feature, feature_collection, geometry, point, polygon
from turf.helpers import Feature, FeatureCollection, Geometry
from turf.invariant import get_coords_from_features, get_geometry_type
from turf.utils.error_codes import error_code_messages
from turf.utils.exceptions import InvalidInput

GeoJson = TypeVar("GeoJson", Dict, Feature, FeatureCollection, Geometry)
PointFeature = TypeVar("PointFeature", Dict, Point, Sequence)
PolygonFeature = TypeVar("PolygonFeature", Dict, Polygon, MultiPolygon)


def polygon_tangents(
    start_point: PointFeature, polygon: PolygonFeature
) -> FeatureCollection:
    """Finds the tangents of a {Polygon or(MultiPolygon} from a {Point}.

    more:
    http://geomalgorithms.com/a15-_tangents.html

    :param point: point [lng, lat] or Point feature to calculate the tangent points from
    :param polygon: polygon to get tangents from

    :return:
        Feature Collection containing the two tangent points
    """
    point_features = []

    point_coord = get_coords_from_features(start_point, ("Point",))
    polygon_coords = get_coords_from_features(polygon, ("Polygon", "MultiPolygon"))

    geometry_type = get_geometry_type(polygon)

    if isinstance(geometry_type, str):
        geometry_type = [geometry_type]
        polygon_coords = [polygon_coords]

    box = bbox(polygon)

    near_point_index = 0
    near_point = False

    # If the point lies inside the polygon bbox then it's a bit more complicated
    # points lying inside a polygon can reflex angles on concave polygons
    if (
        (point_coord[0] > box[0])
        and (point_coord[0] < box[2])
        and (point_coord[1] > box[1])
        and (point_coord[1] < box[3])
    ):
        near_point = nearest_point(start_point, explode(polygon))
        near_point_index = near_point["properties"]["featureIndex"]

    for geo_type, poly_coords in zip(geometry_type, polygon_coords):
        if geo_type == "Polygon":
            tangents = process_polygon(
                poly_coords, point_coord, near_point, near_point_index
            )

        # bruteforce approach
        # calculate both tangents for each polygon
        # define all tangents as a new polygon and calculate tangetns out of those coordinates
        elif geo_type == "MultiPolygon":
            multi_tangents = []

            for poly_coord in poly_coords:
                tangents = process_polygon(
                    poly_coord, point_coord, near_point, near_point_index
                )
                multi_tangents.extend(tangents)

            tangents = process_polygon(
                [multi_tangents], point_coord, near_point, near_point_index
            )

        r_tangents = tangents[0]
        l_tangents = tangents[1]

        point_features.extend([point(r_tangents), point(l_tangents)])

    return feature_collection(point_features)


def process_polygon(
    polygon_coords: Sequence,
    point_coord: Sequence,
    near_point: Union[Point, None],
    near_point_index: int,
) -> Sequence:
    """Prepares a polygon to calculate the tangents

    :param polygon_coords: point [lng, lat] or Point feature to calculate the tangent points from
    :param point_coord: point [lng, lat]
    :param near_point: nearest point [lng, lat] on polygon towards view point
                         in case the view point lies inside the polygon
    :param near_point_index: index of neareast point on polygon
                        in case the view point lies inside the polygon

    :return:
        List of tangents coordinates [upper and lower]
    """
    r_tangents = polygon_coords[0][near_point_index]
    l_tangents = polygon_coords[0][0]

    if near_point:
        if near_point["geometry"]["coordinates"][1] < point_coord[1]:
            l_tangents = polygon_coords[0][near_point_index]

    tangents = calculate_tangents(
        polygon_coords[0],
        point_coord,
        r_tangents,
        l_tangents,
    )

    return tangents


def calculate_tangents(
    poly_coords: Sequence,
    point_coord: Sequence,
    r_tangents: Sequence,
    l_tangents: Sequence,
) -> Sequence:
    """Calculate the upper and lower tangents from the polygon by
    comparison with each neighbor coordinate in the polygon

    :param polygon_coords: point [lng, lat] or Point feature to calculate the tangent points from
    :param point_coord: point [lng, lat]
    :param r_tangents: current rightmost tangents point [lng, lat] of the polygon
    :param l_tangents: current leftmost tangents point [lng, lat] of the polygon

    :return:
        List of tangents coordinates [upper and lower]
    """
    edge_next = None
    edge_prev = is_left(
        poly_coords[0],
        poly_coords[len(poly_coords) - 1],
        point_coord,
    )

    for i in range(1, len(poly_coords) + 1):
        current_poly_coord = poly_coords[i - 1]

        if i == len(poly_coords):
            next_poly_coord = poly_coords[0]
        else:
            next_poly_coord = poly_coords[i]

        edge_next = is_left(current_poly_coord, next_poly_coord, point_coord)

        if (edge_prev <= 0) and (edge_next > 0):
            if not is_below(point_coord, current_poly_coord, r_tangents):
                r_tangents = current_poly_coord

        elif (edge_prev > 0) and (edge_next <= 0):
            if not is_above(point_coord, current_poly_coord, l_tangents):
                l_tangents = current_poly_coord

        edge_prev = edge_next

    return [r_tangents, l_tangents]


def is_above(point1: Sequence, point2: Sequence, point3: Sequence) -> bool:
    """Checks if point 1 is above point2 and point 3

    :param geojson: geojson or Feature
    :param geojson_types: string of the supposed feature tpye

    :return:
        Feature or FeatureCollection of the incoming geojson
    """
    return is_left(point1, point2, point3) > 0


def is_below(point1: Sequence, point2: Sequence, point3: Sequence) -> bool:
    """Checks if point 1 is below point2 and point 3

    :param geojson: geojson or Feature
    :param geojson_types: string of the supposed feature tpye

    :return:
        Feature or FeatureCollection of the incoming geojson
    """
    return is_left(point1, point2, point3) < 0


def is_left(point1: Sequence, point2: Sequence, point3: Sequence) -> float:
    """Calculates if point 1 is left of point2 and point 3

    :param geojson: geojson or Feature
    :param geojson_types: string of the supposed feature tpye

    :return:
        Feature or FeatureCollection of the incoming geojson
    """
    return (point2[0] - point1[0]) * (point3[1] - point1[1]) - (
        point3[0] - point1[0]
    ) * (point2[1] - point1[1])
