# coding: utf-8


# docs

from __future__ import annotations

from typing import Dict, List, Optional, Tuple, Union

import cv2
import numpy as np
from shapely.geometry import LineString
from shapely.geometry import Polygon as ShapelyPolygon
from shapely.geometry import mapping

from supervisely.sly_logger import logger
from supervisely.geometry import validation
from supervisely.geometry.constants import (
    CLASS_ID,
    CREATED_AT,
    EXTERIOR,
    ID,
    LABELER_LOGIN,
    POINTS,
    UPDATED_AT,
)
from supervisely.geometry.conversions import shapely_figure_to_coords_list
from supervisely.geometry.point import PointLocation
from supervisely.geometry.point_location import row_col_list_to_points
from supervisely.geometry.rectangle import Rectangle
from supervisely.geometry.vector_geometry import VectorGeometry


class Polyline(VectorGeometry):
    """Open 2D polyline (sequence of connected line segments). Immutable."""

    @staticmethod
    def geometry_name():
        """
        Returns the name of the geometry.

        :returns: name of the geometry
        :rtype: str
        """
        return "line"

    def __init__(
        self,
        exterior: Union[List[PointLocation], List[List[int, int]], List[Tuple[int, int]]],
        sly_id: Optional[int] = None,
        class_id: Optional[int] = None,
        labeler_login: Optional[int] = None,
        updated_at: Optional[str] = None,
        created_at: Optional[str] = None,
    ):
        """
        Polyline geometry for a single :class:`~supervisely.annotation.label.Label`. :class:`~supervisely.geometry.polyline.Polyline` object is immutable.

        :param exterior: List of exterior coordinates, the Polyline is defined with these points.
        :type exterior: List[:class:`~supervisely.geometry.point_location.PointLocation`], List[List[int, int]], List[Tuple[int, int]
        :param sly_id: Polyline ID in Supervisely server.
        :type sly_id: int, optional
        :param class_id: ID of ObjClass to which Polyline belongs.
        :type class_id: int, optional
        :param labeler_login: Login of the user who created Polyline.
        :type labeler_login: str, optional
        :param updated_at: Date and Time when Polyline was modified last. Date Format: Year:Month:Day:Hour:Minute:Seconds. Example: '2021-01-22T19:37:50.158Z'.
        :type updated_at: str, optional
        :param created_at: Date and Time when Polyline was created. Date Format is the same as in "updated_at" parameter.
        :type created_at: str, optional
        :raises ValueError: field exterior must contain at least two points to create Polyline.

        :Usage Example:

            .. code-block:: python

                import supervisely as sly

                exterior = [sly.PointLocation(730, 2104), sly.PointLocation(2479, 402), sly.PointLocation(1500, 780)]
                # or exterior = [[730, 2104], [2479, 402], [1500, 780]]
                # or exterior = [(730, 2104), (2479, 402), (1500, 780)]
                figure = sly.Polyline(exterior)
        """
        if len(exterior) < 2:
            raise ValueError(
                f'"{EXTERIOR}" field must contain at least two points to create "Polyline" object.'
            )

        super().__init__(
            exterior=exterior,
            interior=[],
            sly_id=sly_id,
            class_id=class_id,
            labeler_login=labeler_login,
            updated_at=updated_at,
            created_at=created_at,
        )

    @classmethod
    def from_json(cls, data: Dict) -> Polyline:
        """
        Convert a json dict to Polyline. Read more about `Supervisely format <https://docs.supervisely.com/data-organization/00_ann_format_navi>`_.

        :param data: Polyline in json format as a dict.
        :type data: dict
        :returns: Polyline from json.
        :rtype: :class:`~supervisely.geometry.polyline.Polyline`

        :Usage Example:

            .. code-block:: python

                import supervisely as sly

                figure_json = {
                    "points": {
                        "exterior": [
                            [2104, 730],
                            [402, 2479],
                            [780, 1500]
                        ],
                        "interior": []
                    }
                }
                figure = sly.Polyline.from_json(figure_json)
        """
        validation.validate_geometry_points_fields(data)
        labeler_login = data.get(LABELER_LOGIN, None)
        updated_at = data.get(UPDATED_AT, None)
        created_at = data.get(CREATED_AT, None)
        sly_id = data.get(ID, None)
        class_id = data.get(CLASS_ID, None)
        return cls(
            exterior=row_col_list_to_points(data[POINTS][EXTERIOR], flip_row_col_order=True),
            sly_id=sly_id,
            class_id=class_id,
            labeler_login=labeler_login,
            updated_at=updated_at,
            created_at=created_at,
        )

    def crop(self, rect: Rectangle) -> List[Polyline]:
        """
        Crops current Polyline.

        :param rect: Rectangle to crop Polyline from.
        :type rect: :class:`~supervisely.geometry.rectangle.Rectangle`
        :returns: List of Polygons from Rectangle.
        :rtype: List[:class:`~supervisely.geometry.polyline.Polyline`]

        :Usage Example:

            .. code-block:: python

                import supervisely as sly

                crop_figures = figure.crop(sly.Rectangle(0, 0, 100, 200))
        """
        try:
            clipping_window = [
                [rect.top, rect.left],
                [rect.top, rect.right],
                [rect.bottom, rect.right],
                [rect.bottom, rect.left],
            ]
            clipping_window_shpl = ShapelyPolygon(clipping_window)

            exterior = self.exterior_np
            intersections_polygon = LineString(exterior).intersection(clipping_window_shpl)
            mapping_shpl = mapping(intersections_polygon)
        except Exception:
            logger.warn("Line cropping exception, shapely.", exc_info=False)
            raise

        res_lines_pts = shapely_figure_to_coords_list(mapping_shpl)

        # tiny hack to combine consecutive segments
        lines_combined = []
        if res_lines_pts != [()]:
            for simple_l in res_lines_pts:
                if len(lines_combined) > 0:
                    prev = lines_combined[-1]
                    if prev[-1] == simple_l[0]:
                        lines_combined[-1] = list(prev) + list(simple_l[1:])
                        continue
                lines_combined.append(simple_l)

        return [Polyline(row_col_list_to_points(line)) for line in lines_combined]

    def _draw_impl(self, bitmap: np.ndarray, color, thickness=1, config=None):
        """ """
        self._draw_contour_impl(bitmap, color, thickness, config=config)

    def _draw_contour_impl(self, bitmap: np.ndarray, color, thickness=1, config=None):
        """ """
        exterior = self.exterior_np[:, ::-1]
        cv2.polylines(bitmap, pts=[exterior], isClosed=False, color=color, thickness=thickness)

    @property
    def area(self) -> float:
        """
        Polyline area, always 0.0.

        :returns: Area of current Polyline, always 0.0
        :rtype: float

        :Usage Example:

            .. code-block:: python

                print(figure.area)
                # Output: 0.0
        """
        return 0.0

    def approx_dp(self, epsilon: float) -> Polyline:
        """
        Approximates a Polyline with the specified precision.

        :param epsilon: Specifying the approximation accuracy.
        :type epsilon: float
        :returns: Approximated Polyline.
        :rtype: :class:`~supervisely.geometry.polyline.Polyline`

        :Usage Example:

            .. code-block:: python

                # Remember that Polyline class object is immutable, and we need to assign new instance of Polyline to a new variable
                approx_figure = figure.approx_dp(0.75)
        """
        exterior_np = self._approx_ring_dp(self.exterior_np, epsilon, closed=True).tolist()
        exterior = row_col_list_to_points(exterior_np, do_round=True)
        return Polyline(exterior)

    @classmethod
    def allowed_transforms(cls):
        """
        Returns the allowed transforms for the Polyline.
        """
        from supervisely.geometry.alpha_mask import AlphaMask
        from supervisely.geometry.any_geometry import AnyGeometry
        from supervisely.geometry.bitmap import Bitmap
        from supervisely.geometry.polygon import Polygon
        from supervisely.geometry.rectangle import Rectangle

        return [AnyGeometry, Rectangle, Bitmap, Polygon, AlphaMask]
