# coding: utf-8

# docs
from __future__ import annotations

from copy import deepcopy
from typing import Dict, Iterable, List, Optional, Tuple, Union

import cv2
import numpy as np

from supervisely.geometry.constants import (
    EXTERIOR,
    GEOMETRY_SHAPE,
    GEOMETRY_TYPE,
    INTERIOR,
    POINTS,
)
from supervisely.geometry.geometry import Geometry
from supervisely.geometry.image_rotator import ImageRotator
from supervisely.geometry.point_location import PointLocation, points_to_row_col_list
from supervisely.geometry.rectangle import Rectangle


class VectorGeometry(Geometry):
    """Base class for vector geometries (polygon, polyline) defined by exterior/interior contours. Immutable."""

    def __init__(
        self,
        exterior: Union[List[PointLocation], List[List[int, int]], List[Tuple[int, int]]],
        interior: Union[
            List[List[PointLocation]],
            List[List[List[int, int]]],
            List[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,
    ):
        """
        VectorGeometry is a base class of geometry for a single :class:`~supervisely.annotation.label.Label`. :class:`~supervisely.geometry.vector_geometry.VectorGeometry` object is immutable.

        :param exterior: Exterior coordinates, object contour is defined with these points (used for :class:`~supervisely.geometry.polygon.Polygon`).
        :type exterior: List[:class:`~supervisely.geometry.point_location.PointLocation`], List[List[int, int]], List[Tuple[int, int]
        :param interior: Interior coordinates, object holes is defined with these points (used for :class:`~supervisely.geometry.polygon.Polygon`).
        :type interior: List[List[:class:`~supervisely.geometry.point_location.PointLocation`]], List[List[List[int, int]]], List[List[Tuple[int, int]]]
        :param sly_id: VectorGeometry ID in Supervisely server.
        :type sly_id: int, optional
        :param class_id: ID of :class:`~supervisely.project.obj_class.ObjClass` to which VectorGeometry belongs.
        :type class_id: int, optional
        :param labeler_login: Login of the user who created VectorGeometry.
        :type labeler_login: str, optional
        :param updated_at: Date and Time when VectorGeometry 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 VectorGeometry was created. Date Format is the same as in "updated_at" parameter.
        :type created_at: str, optional
        :raises TypeError: if exterior or interior parameters are not a list of :class:`~supervisely.geometry.point_location.PointLocation` objects

        :Usage Example:

            .. code-block:: python

                import supervisely as sly

                exterior = [sly.PointLocation(730, 2104), sly.PointLocation(2479, 402), sly.PointLocation(3746, 1646)]
                # or exterior = [[730, 2104], [2479, 402], [3746, 1646]]
                # or exterior = [(730, 2104), (2479, 402), (3746, 1646)]
                interior = [[sly.PointLocation(1907, 1255), sly.PointLocation(2468, 875), sly.PointLocation(2679, 1577)]]
                # or interior = [[[730, 2104], [2479, 402], [3746, 1646]]]
                # or interior = [[(730, 2104), (2479, 402), (3746, 1646)]]

                figure = sly.Polygon(exterior, interior)
        """
        result_exterior = []
        if not isinstance(exterior, list):
            raise TypeError('Argument "exterior" must be a list of coordinates')
        for p in exterior:
            if isinstance(p, PointLocation):
                result_exterior.append(p)
            elif isinstance(p, tuple) and len(p) == 2:
                result_exterior.append(PointLocation(p[0], p[1]))
            elif isinstance(p, list) and len(p) == 2:
                result_exterior.append(PointLocation(p[0], p[1]))
            else:
                raise TypeError(
                    'Type of items (coordinates) in list "exterior" have to be tuple(int, int) or list[int, int] or PointLocation(row, col)'
                )

        result_interior = []
        if not isinstance(interior, list):
            raise TypeError('Argument "interior" must be a list of lists with coordinates')
        for coords in interior:
            if not isinstance(interior, list):
                raise TypeError('"interior" coords must be a list of coordinates')
            p_coords = []
            for p in coords:
                if isinstance(p, PointLocation):
                    p_coords.append(p)
                elif isinstance(p, tuple) and len(p) == 2:
                    p_coords.append(PointLocation(p[0], p[1]))
                elif isinstance(p, list) and len(p) == 2:
                    p_coords.append(PointLocation(p[0], p[1]))
                else:
                    raise TypeError(
                        'Type of items (coordinates) in list "interior" have to be tuple(int, int) or list[int, int] or PointLocation(row, col)'
                    )
            result_interior.append(p_coords)

        self._exterior = deepcopy(result_exterior)
        self._interior = deepcopy(result_interior)
        super().__init__(
            sly_id=sly_id,
            class_id=class_id,
            labeler_login=labeler_login,
            updated_at=updated_at,
            created_at=created_at,
        )

    def to_json(self) -> Dict:
        """
        Convert the VectorGeometry to a json dict. Read more about `Supervisely format <https://docs.supervisely.com/data-organization/00_ann_format_navi>`_.

        :returns: Json format as a dict
        :rtype: dict

        :Usage Example:

            .. code-block:: python

                figure_json = figure.to_json()
                print(figure_json)
                # Output: {
                #    "points": {
                #        "exterior": [
                #            [2104, 730],
                #            [402, 2479],
                #            [1646, 3746]
                #        ],
                #        "interior": [
                #            [
                #                [1255, 1907],
                #                [875, 2468],
                #                [1577, 2679]
                #            ]
                #        ]
                #    }
                # }
        """
        packed_obj = {
            POINTS: {
                EXTERIOR: points_to_row_col_list(self._exterior, flip_row_col_order=True),
                INTERIOR: [
                    points_to_row_col_list(i, flip_row_col_order=True) for i in self._interior
                ],
            },
            GEOMETRY_SHAPE: self.geometry_name(),
            GEOMETRY_TYPE: self.geometry_name(),
        }
        self._add_creation_info(packed_obj)
        return packed_obj

    @property
    def exterior(self) -> List[PointLocation]:
        """
        VectorGeometry exterior points.

        :returns: VectorGeometry exterior points
        :rtype: List[:class:`~supervisely.geometry.point_location.PointLocation`]

        :Usage Example:

            .. code-block:: python

                exterior = figure.exterior
        """
        return deepcopy(self._exterior)

    @property
    def exterior_np(self) -> np.ndarray:
        """
        Converts exterior attribute of VectorGeometry to numpy array.

        :returns: Numpy array
        :rtype: np.ndarray

        :Usage Example:

            .. code-block:: python

                print(figure.exterior_np)
                # Output:
                # [[ 730 2104]
                #  [2479  402]
                #  [3746 1646]]
        """
        return np.array(points_to_row_col_list(self._exterior), dtype=np.int64)

    @property
    def interior(self) -> List[List[PointLocation]]:
        """
        VectorGeometry interior points.

        :returns: VectorGeometry interior points
        :rtype: List[List[:class:`~supervisely.geometry.point_location.PointLocation`]]

        :Usage Example:

            .. code-block:: python

                interior = figure.interior
        """
        return deepcopy(self._interior)

    @property
    def interior_np(self):
        """
        Converts interior attribute of VectorGeometry to numpy array.

        :returns: Numpy array
        :rtype: List[np.ndarray]

        :Usage Example:

            .. code-block:: python

                print(figure.interior_np)
                # Output:
                # [array([[1907, 1255],
                #        [2468,  875],
                #        [2679, 1577]])]
        """
        return [np.array(points_to_row_col_list(i), dtype=np.int64) for i in self._interior]

    def _transform(self, transform_fn):
        """ """
        result = deepcopy(self)
        result._exterior = [transform_fn(p) for p in self._exterior]
        result._interior = [[transform_fn(p) for p in i] for i in self._interior]
        return result

    def resize(self, in_size: Tuple[int, int], out_size: Tuple[int, int]) -> VectorGeometry:
        """
        Resizes current VectorGeometry.

        :param in_size: Input image size (height, width) to which belongs VectorGeometry.
        :type in_size: Tuple[int, int]
        :param out_size: Desired output image size (height, width) to which belongs VectorGeometry.
        :type out_size: Tuple[int, int]
        :returns: Resized VectorGeometry.
        :rtype: :class:`~supervisely.geometry.vector_geometry.VectorGeometry`

        :Usage Example:

            .. code-block:: python

                # Remember that VectorGeometry class object is immutable, and we need to assign new instance of VectorGeometry to a new variable
                in_height, in_width = 300, 400
                out_height, out_width = 600, 800
                resize_figure = figure.resize((in_height, in_width), (out_height, out_width))
        """
        return self._transform(lambda p: p.resize(in_size, out_size))

    def scale(self, factor: float) -> VectorGeometry:
        """
        Scales current VectorGeometry.

        :param factor: Scale parameter.
        :type factor: float
        :returns: Scaled VectorGeometry.
        :rtype: :class:`~supervisely.geometry.vector_geometry.VectorGeometry`

        :Usage Example:

            .. code-block:: python

                # Remember that VectorGeometry class object is immutable, and we need to assign new instance of VectorGeometry to a new variable
                scale_figure = figure.scale(0.75)
        """
        return self._transform(lambda p: p.scale(factor))

    def translate(self, drow: int, dcol: int) -> VectorGeometry:
        """
        Translates current VectorGeometry.

        :param drow: Horizontal shift.
        :type drow: int
        :param dcol: Vertical shift.
        :type dcol: int
        :returns: Translated VectorGeometry.
        :rtype: :class:`~supervisely.geometry.vector_geometry.VectorGeometry`

        :Usage Example:

            .. code-block:: python

                # Remember that VectorGeometry class object is immutable, and we need to assign new instance of VectorGeometry to a new variable
                translate_figure = figure.translate(150, 250)
        """
        return self._transform(lambda p: p.translate(drow, dcol))

    def rotate(self, rotator: ImageRotator) -> VectorGeometry:
        """
        Rotates current VectorGeometry.

        :param rotator: ImageRotator for rotate.
        :type rotator: :class:`~supervisely.geometry.image_rotator.ImageRotator`
        :returns: Rotated VectorGeometry.
        :rtype: :class:`~supervisely.geometry.vector_geometry.VectorGeometry`

        :Usage Example:

            .. code-block:: python

                from supervisely.geometry.image_rotator import ImageRotator

                # Remember that VectorGeometry class object is immutable, and we need to assign new instance of VectorGeometry to a new variable
                height, width = 300, 400
                rotator = ImageRotator((height, width), 25)
                rotate_figure = figure.rotate(rotator)

        """
        return self._transform(lambda p: p.rotate(rotator))

    def fliplr(self, img_size: Tuple[int, int]) -> VectorGeometry:
        """
        Flips current VectorGeometry in horizontal.

        :param img_size: Input image size (height, width) to which belongs VectorGeometry.
        :type img_size: Tuple[int, int]
        :returns: Flipped VectorGeometry in horizontal.
        :rtype: :class:`~supervisely.geometry.vector_geometry.VectorGeometry`

        :Usage Example:

            .. code-block:: python

                # Remember that VectorGeometry class object is immutable, and we need to assign new instance of VectorGeometry to a new variable
                height, width = 300, 400
                fliplr_figure = figure.fliplr((height, width))
        """
        return self._transform(lambda p: p.fliplr(img_size))

    def flipud(self, img_size: Tuple[int, int]) -> VectorGeometry:
        """
        Flips current VectorGeometry in vertical.

        :param img_size: Input image size (height, width) to which belongs VectorGeometry.
        :type img_size: Tuple[int, int]
        :returns: Flipped VectorGeometry in vertical.
        :rtype: :class:`~supervisely.geometry.vector_geometry.VectorGeometry`

        :Usage Example:

            .. code-block:: python

                # Remember that VectorGeometry class object is immutable, and we need to assign new instance of VectorGeometry to a new variable
                height, width = 300, 400
                flipud_figure = figure.flipud((height, width))
        """
        return self._transform(lambda p: p.flipud(img_size))

    def to_bbox(self) -> Rectangle:
        """
        Creates Rectangle object from current VectorGeometry.

        :returns: Rectangle from VectorGeometry.
        :rtype: :class:`~supervisely.geometry.rectangle.Rectangle`

        :Usage Example:

            .. code-block:: python

                rectangle = figure.to_bbox()
        """
        exterior_np = self.exterior_np
        rows, cols = exterior_np[:, 0], exterior_np[:, 1]
        return Rectangle(
            top=round(min(rows).item()),
            left=round(min(cols).item()),
            bottom=round(max(rows).item()),
            right=round(max(cols).item()),
        )

    def _draw_impl(self, bitmap, color, thickness=1, config=None):
        """
        :param bitmap: np.ndarray
        :param color: [R, G, B]
        :param thickness: used only in Polyline and Point.
        """
        self._draw_contour_impl(bitmap, color, thickness, config=config)

    def _draw_contour_impl(self, bitmap, color, thickness=1, config=None):
        """Draws the figure contour on a given bitmap canvas
        :param bitmap: np.ndarray
        :param color: [R, G, B]
        :param thickness: (int)
        """
        raise NotImplementedError()

    @staticmethod
    def _approx_ring_dp(ring, epsilon, closed):
        """ """
        new_ring = cv2.approxPolyDP(ring.astype(np.int32), epsilon, closed)
        new_ring = np.squeeze(new_ring, 1)
        if len(new_ring) < 3 and closed:
            new_ring = ring.astype(np.int32)
        return new_ring

    def approx_dp(self, epsilon):
        """ """
        raise NotImplementedError()
