# coding: utf-8

from __future__ import annotations

import uuid
from typing import Dict, List, Optional

from bidict import bidict

from supervisely._utils import take_with_default
from supervisely.annotation.label import LabelJsonFields
from supervisely.annotation.obj_class import ObjClass
from supervisely.collection.key_indexed_collection import KeyObject
from supervisely.geometry.constants import (
    CLASS_ID,
    CREATED_AT,
    LABELER_LOGIN,
    UPDATED_AT,
)
from supervisely.project.project_meta import ProjectMeta
from supervisely.project.project_settings import LabelingInterface
from supervisely.video_annotation.constants import ID, KEY, OBJECTS_MAP
from supervisely.video_annotation.key_id_map import KeyIdMap
from supervisely.video_annotation.video_tag import VideoTag
from supervisely.video_annotation.video_tag_collection import VideoTagCollection


class VideoObject(KeyObject):
    """Tracked object across video frames; obj_class plus optional tags. Immutable."""

    def __init__(
        self,
        obj_class: ObjClass,
        tags: Optional[VideoTagCollection] = None,
        key: Optional[uuid.UUID] = None,
        class_id: Optional[int] = None,
        labeler_login: Optional[str] = None,
        updated_at: Optional[str] = None,
        created_at: Optional[str] = None,
    ):
        """
        :param obj_class: Object class (e.g. 'car' with Rectangle geometry).
        :type obj_class: :class:`~supervisely.annotation.obj_class.ObjClass`
        :param tags: Tags for this object.
        :type tags: :class:`~supervisely.video_annotation.video_tag_collection.VideoTagCollection`, optional
        :param key: UUID key. Auto-generated if not provided.
        :type key: uuid.UUID, optional
        :param class_id: Server-side class ID.
        :type class_id: int, optional
        :param labeler_login: Login of user who created the object.
        :type labeler_login: str, optional
        :param updated_at: Last modification timestamp.
        :type updated_at: str, optional
        :param created_at: Creation timestamp.
        :type created_at: str, optional

        :Usage Example:

            .. code-block:: python

                import supervisely as sly

                obj_class_car = sly.ObjClass('car', sly.Rectangle)
                video_obj_car = sly.VideoObject(obj_class_car)
                video_obj_car_json = video_obj_car.to_json()
                print(video_obj_car_json)
        """
        self.labeler_login = labeler_login
        self.updated_at = updated_at
        self.created_at = created_at

        self._obj_class = obj_class
        self._key = take_with_default(key, uuid.uuid4())
        self._tags = take_with_default(tags, VideoTagCollection())
        self._class_id = take_with_default(class_id, None)

    def _add_creation_info(self, d):
        if self.labeler_login is not None:
            d[LABELER_LOGIN] = self.labeler_login
        if self.updated_at is not None:
            d[UPDATED_AT] = self.updated_at
        if self.created_at is not None:
            d[CREATED_AT] = self.created_at

    @property
    def obj_class(self) -> ObjClass:
        """
        ObjClass of the current VideoObject.

        :returns: ObjClass object
        :rtype: :class:`~supervisely.annotation.obj_class.ObjClass`

        :Usage Example:

            .. code-block:: python

                import supervisely as sly

                obj_class_car = sly.ObjClass('car', sly.Rectangle)
                video_obj_car = sly.VideoObject(obj_class_car)
                obj_car_json = video_obj_car.obj_class.to_json()
                print(obj_car_json)
                # Output: {
                #     "title": "car",
                #     "shape": "rectangle",
                #     "color": "#8A0F7B",
                #     "geometry_config": {},
                #     "hotkey": ""
                # }
        """
        return self._obj_class

    def key(self) -> uuid.UUID:
        """
        Object key.

        :returns: Object key
        :rtype: :class:`~supervisely.video_annotation.key_id_map.KeyIdMap`
        :rtype: uuid.UUID

        :Usage Example:

            .. code-block:: python

                import supervisely as sly

                obj_class_car = sly.ObjClass('car', sly.Rectangle)
                video_obj_car = sly.VideoObject(obj_class_car)
                key = video_obj_car.key()
                print(key)
                # Output: 158e6cf4f4ac4c639fc6994aad127c16
        """

        return self._key

    @property
    def tags(self) -> VideoTagCollection:
        """
        VideoTagCollection of the current VideoObject.

        :returns: VideoTagCollection object
        :rtype: :class:`~supervisely.video_annotation.video_tag_collection.VideoTagCollection`

        :Usage Example:

            .. code-block:: python

                import supervisely as sly

                # Create VideoTagCollection
                meta_car = sly.TagMeta('car_tag', sly.TagValueType.ANY_STRING)
                from supervisely.video_annotation.video_tag import VideoTag
                car_tag = VideoTag(meta_car, value='acura')
                from supervisely.video_annotation.video_tag_collection import VideoTagCollection
                video_tags = VideoTagCollection([car_tag])

                # Create VideoObject
                obj_class_car = sly.ObjClass('car', sly.Rectangle)
                video_obj_car = sly.VideoObject(obj_class_car, video_tags)

                # Get VideoTagCollection
                tags = video_obj_car.tags
                tags_json = tags.to_json()
                print(tags_json)
                # Output: [
                #     {
                #         "name": "car_tag",
                #         "value": "acura",
                #         "key": "4f82fbcab74c44259d7a0e29d604602e"
                #     }
                # ]
        """
        return self._tags.clone()

    @property
    def class_id(self) -> int:
        """
        Object class ID.

        :returns: Object class ID.
        :rtype: int

        :Usage Example:

            .. code-block:: python

                import supervisely as sly

                obj_class_car = sly.ObjClass('car', sly.Rectangle)
                video_obj_car = sly.VideoObject(obj_class_car)
                class_id = video_obj_car.class_id
        """

        return self._class_id

    def add_tag(self, tag: VideoTag) -> VideoObject:
        """
        Adds VideoTag to the current VideoObject.

        :param tag: VideoTag to be added.
        :type tag: :class:`~supervisely.video_annotation.video_tag.VideoTag`
        :returns: VideoObject object
        :rtype: :class:`~supervisely.video_annotation.video_object.VideoObject`

        :Usage Example:

            .. code-block:: python

                import supervisely as sly

                # Create VideoObject
                obj_class_car = sly.ObjClass('car', sly.Rectangle)
                video_obj_car = sly.VideoObject(obj_class_car)

                # Create VideoTag
                meta_car = sly.TagMeta('car_tag', sly.TagValueType.ANY_STRING)
                from supervisely.video_annotation.video_tag import VideoTag
                car_tag = VideoTag(meta_car, value='acura')

                # Add VideoTag
                new_obj_car = video_obj_car.add_tag(car_tag)
                new_obj_car_json = new_obj_car.to_json()
                print(new_obj_car_json)
                # Output: {
                #     "key": "1ab52285ee634c93b724fa655b785eae",
                #     "classTitle": "car",
                #     "tags": [
                #         {
                #             "name": "car_tag",
                #             "value": "acura",
                #             "key": "d9e52b275e074c538f162a6d679aed9e"
                #         }
                #     ]
                # }
        """
        return self.clone(tags=self._tags.add(tag))

    def add_tags(self, tags: List[VideoTag]) -> VideoObject:
        """
        Adds VideoTags to the current VideoObject.

        :param tag: List of VideoTags to be added.
        :type tag: List[:class:`~supervisely.video_annotation.video_tag.VideoTag`]
        :returns: VideoObject object
        :rtype: :class:`~supervisely.video_annotation.video_object.VideoObject`

        :Usage Example:

            .. code-block:: python

                import supervisely as sly

                # Create VideoObject
                obj_class_vehicle = sly.ObjClass('vehicle', sly.Rectangle)
                video_obj_vehicle = sly.VideoObject(obj_class_vehicle)

                # Create VideoTags
                from supervisely.video_annotation.video_tag import VideoTag
                meta_car = sly.TagMeta('car_tag', sly.TagValueType.ANY_STRING)
                car_tag = VideoTag(meta_car, value='acura')
                meta_bus = sly.TagMeta('bus_tag', sly.TagValueType.ANY_STRING)
                bus_tag = VideoTag(meta_bus, value='volvo')

                # Add VideoTags
                new_obj_vehicle = video_obj_vehicle.add_tags([car_tag, bus_tag])
                new_obj_vehicle_json = new_obj_vehicle.to_json()
                print(new_obj_vehicle_json)
                # Output: {
                #     "key": "94055c5e8cb146368f627fc608fb6b44",
                #     "classTitle": "vehicle",
                #     "tags": [
                #         {
                #             "name": "car_tag",
                #             "value": "acura",
                #             "key": "6679f47c96734565919fbffc278532a1"
                #         },
                #         {
                #             "name": "bus_tag",
                #             "value": "volvo",
                #             "key": "d0a60dc929de491a85a09fea59adb818"
                #         }
                #     ]
                # }
        """
        return self.clone(tags=self._tags.add_items(tags))

    def to_json(self, key_id_map: Optional[KeyIdMap] = None) -> Dict:
        """
        Convert the VideoObject to a json dict. Read more about `Supervisely format <https://docs.supervisely.com/data-organization/00_ann_format_navi>`_.

        :param key_id_map: KeyIdMap object.
        :type key_id_map: :class:`~supervisely.video_annotation.key_id_map.KeyIdMap`, optional
        :returns: Json format as a dict
        :rtype: dict

        :Usage Example:

            .. code-block:: python

                import supervisely as sly

                obj_class_car = sly.ObjClass('vehicle', sly.Rectangle)
                video_obj_car = sly.VideoObject(obj_class_car)

                obj_car_json = video_obj_car.to_json()
                print(obj_car_json)
                # Output: {
                #     "key": "ce26e77a45bc45e88e3e17da1672d01f",
                #     "classTitle": "vehicle",
                #     "tags": []
                # }
        """
        data_json = {
            KEY: self.key().hex,
            LabelJsonFields.OBJ_CLASS_NAME: self.obj_class.name,
            LabelJsonFields.TAGS: self.tags.to_json(key_id_map),
        }

        if key_id_map is not None:
            item_id = key_id_map.get_object_id(self.key())
            if item_id is not None:
                data_json[ID] = item_id

        self._add_creation_info(data_json)
        return data_json

    @classmethod
    def from_json(
        cls, data: Dict, project_meta: ProjectMeta, key_id_map: Optional[KeyIdMap] = None
    ) -> VideoObject:
        """
        Convert a json dict to VideoObject. Read more about `Supervisely format <https://docs.supervisely.com/data-organization/00_ann_format_navi>`_.

        :param data: Dict in json format.
        :type data: dict
        :param project_meta: Input ProjectMeta object.
        :type project_meta: :class:`~supervisely.project.project_meta.ProjectMeta`
        :param key_id_map: KeyIdMap object.
        :type key_id_map: :class:`~supervisely.video_annotation.key_id_map.KeyIdMap`, optional
        :raises RuntimeError: if object class name is not found in the given project meta
        :returns: VideoObject object
        :rtype: :class:`~supervisely.video_annotation.video_object.VideoObject`

        :Usage Example:

            .. code-block:: python

                import supervisely as sly

                obj_car_json = {"classTitle": "vehicle", "tags": []}
                obj_class_car = sly.ObjClass('vehicle', sly.Rectangle)
                obj_collection = sly.ObjClassCollection([obj_class_car])
                meta = sly.ProjectMeta(obj_classes=obj_collection)

                video_obj_from_json = sly.VideoObject.from_json(obj_car_json, meta)
        """
        obj_class_name = data[LabelJsonFields.OBJ_CLASS_NAME]
        obj_class = project_meta.get_obj_class(obj_class_name)
        if obj_class is None:
            raise RuntimeError(
                f"Failed to deserialize a object from JSON: class name {obj_class_name!r} "
                f"was not found in the given project meta."
            )

        is_multiview = False
        try:
            if project_meta.labeling_interface == LabelingInterface.MULTIVIEW:
                is_multiview = True
        except AttributeError:
            is_multiview = False

        raw_id = data.get(ID, None)

        if not is_multiview:
            key = uuid.UUID(data[KEY]) if KEY in data else uuid.uuid4()
            if key_id_map is not None:
                key_id_map.add_object(key, raw_id)
        else:
            if KEY in data:
                key = uuid.UUID(data[KEY])
            elif key_id_map is not None and isinstance(raw_id, int):
                existing_key = key_id_map.get_object_key(raw_id)
                if isinstance(existing_key, uuid.UUID):
                    key = existing_key
                else:
                    key = uuid.uuid4()
            else:
                key = uuid.uuid4()

            if key_id_map is not None and isinstance(raw_id, int):
                if key_id_map.get_object_id(key) is None:
                    key_id_map.add_object(key, raw_id)

        class_id = data.get(CLASS_ID, None)
        labeler_login = data.get(LABELER_LOGIN, None)
        updated_at = data.get(UPDATED_AT, None)
        created_at = data.get(CREATED_AT, None)

        return cls(
            obj_class=obj_class,
            key=key,
            tags=VideoTagCollection.from_json(data[LabelJsonFields.TAGS], project_meta.tag_metas),
            class_id=class_id,
            labeler_login=labeler_login,
            updated_at=updated_at,
            created_at=created_at,
        )

    def clone(
        self,
        obj_class: Optional[ObjClass] = None,
        tags: Optional[VideoTagCollection] = None,
        key: Optional[uuid.UUID] = None,
        class_id: Optional[int] = None,
        labeler_login: Optional[str] = None,
        updated_at: Optional[str] = None,
        created_at: Optional[str] = None,
    ) -> VideoObject:
        """
        Makes a copy of VideoObject with new fields, if fields are given, otherwise it will use fields of the original VideoObject.

        :param obj_class: ObjClass object.
        :type obj_class: :class:`~supervisely.annotation.obj_class.ObjClass`, optional
        :param tags: VideoTagCollection object.
        :type tags: :class:`~supervisely.video_annotation.video_tag_collection.VideoTagCollection`, optional
        :param key: UUID key associated with the object.
        :type key: uuid.UUID, optional
        :param class_id: ID of ObjClass to which VideoObject belongs.
        :type class_id: int, optional
        :param labeler_login: Login of the user who created VideoObject.
        :type labeler_login: str, optional
        :param updated_at: Date and Time when VideoObject 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 VideoObject was created. Date Format is the same as in "updated_at" parameter.
        :type created_at: str, optional
        :returns: VideoObject object
        :rtype: :class:`~supervisely.video_annotation.video_object.VideoObject`

        :Usage Example:

            .. code-block:: python

                import supervisely as sly
                from supervisely.video_annotation.video_tag import VideoTag
                from supervisely.video_annotation.video_tag_collection import VideoTagCollection

                obj_class_car = sly.ObjClass('vehicle', sly.Rectangle)
                video_obj_car = sly.VideoObject(obj_class_car)

                # New ObjClass to clone
                obj_class_bus = sly.ObjClass('bus', sly.Rectangle)

                # New VideoTagCollection to clone
                meta_car = sly.TagMeta('car_tag', sly.TagValueType.ANY_STRING)
                car_tag = VideoTag(meta_car, value='acura')
                tags = VideoTagCollection([car_tag])
                # Clone
                new_obj_vehicle = video_obj_car.clone(obj_class=obj_class_bus, tags=tags)
                new_obj_vehicle_json = new_obj_vehicle.to_json()
                print(new_obj_vehicle_json)
                # Output: {
                #     "key": "39ae5b9ce1ca405c9f53544374b3f5be",
                #     "classTitle": "bus",
                #     "tags": [
                #         {
                #             "name": "car_tag",
                #             "value": "acura",
                #             "key": "3119b6e38fe24fe7a220e881154fd9ba"
                #         }
                #     ]
                # }
        """
        return self.__class__(
            obj_class=take_with_default(obj_class, self.obj_class),
            key=take_with_default(key, self._key),
            tags=take_with_default(tags, self.tags),
            class_id=take_with_default(class_id, self.class_id),
            labeler_login=take_with_default(labeler_login, self.labeler_login),
            updated_at=take_with_default(updated_at, self.updated_at),
            created_at=take_with_default(created_at, self.created_at),
        )
