# coding: utf-8
"""Collection with :class:`TagMeta<supervisely.annotation.tag_meta.TagMeta>` instances"""

# docs
from __future__ import annotations

from collections import Counter
from typing import Dict, Iterator, List, Optional

from supervisely.annotation.renamer import Renamer
from supervisely.annotation.tag_meta import TagMeta, TagValueType
from supervisely.collection.key_indexed_collection import KeyIndexedCollection
from supervisely.io.json import JsonSerializable


class TagMetaCollection(KeyIndexedCollection, JsonSerializable):
    """
    Collection with :class:`TagMeta<supervisely.annotation.tag_meta.TagMeta>` instances. :class:`TagMetaCollection<TagMetaCollection>` object is immutable.

    :raises: :class:`DuplicateKeyError`, if instance with given name already exists
    :Usage example:

     .. code-block:: python

        import supervisely as sly

        # Create TagMetas
        meta_weather = sly.TagMeta('Weather', sly.TagValueType.ANY_STRING)

        season_values = ["Winter", "Spring", "Summer", "Autumn"]
        meta_season = sly.TagMeta('Season', sly.TagValueType.ONEOF_STRING, possible_values=season_values)

        # Create TagMetaCollection from TagMetas
        tag_metas = sly.TagMetaCollection([meta_weather, meta_season])

        # Add items to TagMetaCollection
        meta_potato = sly.TagMeta('potato', sly.TagValueType.NONE)

        # Remember that TagMetaCollection is immutable, and we need to assign new instance of TagMetaCollection to a new variable
        tag_metas = tag_metas.add(meta_potato)

        # You can also add multiple items to collection
        meta_cabbage = sly.TagMeta('cabbage', sly.TagValueType.NONE)
        meta_carrot = sly.TagMeta('carrot', sly.TagValueType.NONE)
        meta_turnip = sly.TagMeta('turnip', sly.TagValueType.NONE)

        tag_metas = tag_metas.add_items([meta_cabbage, meta_carrot, meta_turnip])

        # Has key, checks if given key exist in collection
        tag_metas.has_key("cabbage")
        # Output: True

        # Intersection, finds intersection of given list of instances with collection items
        meta_dog = sly.TagMeta('dog', sly.TagValueType.NONE)
        meta_cat = sly.TagMeta('cat', sly.TagValueType.NONE)
        meta_turtle = sly.TagMeta('turtle', sly.TagValueType.NONE)

        tag_metas_animals = sly.TagMetaCollection([meta_dog, meta_cat, meta_turtle])

        metas_intersections = tag_metas.intersection(tag_metas_animals)
        print(metas_intersections.to_json())
        # Output: []

        # Let's add the potato TagMeta from another collection and compare them again
        tag_metas_animals = tag_metas_animals.add(meta_potato)

        metas_intersections = tag_metas.intersection(tag_metas_animals)
        print(metas_intersections.to_json())
        # Output: [
        #     {
        #         "name":"potato",
        #         "value_type":"none",
        #         "color":"#8A710F",
        #         "hotkey":"",
        #         "applicable_type":"all",
        #         "classes":[]
        #     }
        # ]

        # Difference, finds difference between collection and given list of TagMetas or TagMetaCollection
        meta_car = sly.TagMeta('car', sly.TagValueType.NONE)
        meta_bicycle = sly.TagMeta('bicycle', sly.TagValueType.NONE)

        tag_metas_vehicles = sly.TagMetaCollection([meta_car, meta_bicycle])

        meta_pedestrian = sly.TagMeta('pedestrian', sly.TagValueType.NONE)
        meta_road = sly.TagMeta('road', sly.TagValueType.NONE)

        difference = tag_metas_vehicles.difference([meta_pedestrian, meta_road])
        print(difference.to_json())
        # Output: [
        #     {
        #         "name":"car",
        #         "value_type":"none",
        #         "color":"#0F138A",
        #         "hotkey":"",
        #         "applicable_type":"all",
        #         "classes":[]
        #     },
        #     {
        #         "name":"bicycle",
        #         "value_type":"none",
        #         "color":"#0F8A25",
        #         "hotkey":"",
        #         "applicable_type":"all",
        #         "classes":[]
        #     }
        # ]

        # Merge, merges collection and given list of TagMetas
        tag_metas_vehicles = sly.TagMetaCollection([meta_car, meta_bicycle])
        tag_metas_merge = sly.TagMetaCollection([meta_pedestrian, meta_road])

        merged_collections = tag_metas_vehicles.merge(tag_metas_merge)
        print(merged_collections.to_json())
        # Output: [
        #     {
        #         "name":"pedestrian",
        #         "value_type":"none",
        #         "color":"#698A0F",
        #         "hotkey":"",
        #         "applicable_type":"all",
        #         "classes":[]
        #     },
        #     {
        #         "name":"road",
        #         "value_type":"none",
        #         "color":"#0F8A59",
        #         "hotkey":"",
        #         "applicable_type":"all",
        #         "classes":[]
        #     },
        #     {
        #         "name":"car",
        #         "value_type":"none",
        #         "color":"#0F138A",
        #         "hotkey":"",
        #         "applicable_type":"all",
        #         "classes":[]
        #     },
        #     {
        #         "name":"bicycle",
        #         "value_type":"none",
        #         "color":"#0F8A25",
        #         "hotkey":"",
        #         "applicable_type":"all",
        #         "classes":[]
        #     }
        # ]

        # Merge will raise ValueError if item name from given list is in collection but items in both are different
        meta_bicycle_1 = sly.TagMeta('bicycle', sly.TagValueType.NONE)
        meta_bicycle_2 = sly.TagMeta('bicycle', sly.TagValueType.ANY_STRING)

        tag_metas_1 = sly.TagMetaCollection([meta_bicycle_1])
        tag_metas_2 = sly.TagMetaCollection([meta_bicycle_2])

        test_merge = tag_metas_1.merge(tag_metas_2)
        # Output: ValueError: Error during merge for key 'bicycle': values are different

        # Let's try to create now a collection where TagMetas have identical names
        meta_cow = sly.TagMeta('cow', sly.TagValueType.NONE)
        meta_chicken = sly.TagMeta('cow', sly.TagValueType.NONE)

        tag_metas = sly.TagMetaCollection([meta_cow, meta_chicken])
        # Output: DuplicateKeyError: "Key 'cow' already exists"
    """

    item_type = TagMeta

    def to_json(self) -> List[Dict]:
        """
        Convert the TagMetaCollection to a list of json dicts. Read more about `Supervisely format <https://docs.supervisely.com/data-organization/00_ann_format_navi>`_.

        :return: List of dicts in json format
        :rtype: :class:`List[dict]`
        :Usage example:

         .. code-block:: python

            import supervisely as sly

            tag_metas = tag_metas.to_json()
            # Output:
            # [
            #   {
            #    "name": "Weather",
            #    "value_type": "any_string",
            #    "color": "#8A620F",
            #    "hotkey": "",
            #    "applicable_type": "all",
            #    "classes": []
            #   },
            #   {
            #    "name": "Season",
            #    "value_type": "oneof_string",
            #    "color": "#700F8A",
            #    "values": ["Winter", 'Spring", "Summer", "Autumn"],
            #    "hotkey": "",
            #    "applicable_type": "all",
            #    "classes": []
            #   }
            # ]
        """
        return [tag_meta.to_json() for tag_meta in self]

    @classmethod
    def from_json(cls, data: List[Dict]) -> TagMetaCollection:
        """
        Convert a list with dicts in json format to TagMetaCollection. Read more about `Supervisely format <https://docs.supervisely.com/data-organization/00_ann_format_navi>`_.

        :param data: List with dicts in json format.
        :type data: List[dict]
        :return: TagMetaCollection object
        :rtype: :class:`TagMetaCollection<TagMetaCollection>`
        :Usage example:

         .. code-block:: python

            import supervisely as sly

            data = [
                {
                    "name":"Weather",
                    "value_type":"any_string",
                    "color":"#8A620F",
                    "hotkey":"",
                    "applicable_type":"all",
                    "classes":[]
                },
                {
                    "name":"Season",
                    "value_type": "oneof_string",
                                  "color": "#700F8A",
                                  "values": ["Winter", "Spring", "Summer", "Autumn"],
                    "hotkey":"",
                    "applicable_type":"all",
                    "classes":[]
            }
            ]

            tag_metas = sly.TagMetaCollection.from_json(data)
        """
        tags = [TagMeta.from_json(tag_meta_json) for tag_meta_json in data]
        return cls(tags)

    def get_id_mapping(self, raise_if_no_id: Optional[bool] = False) -> Dict[int, TagMeta]:
        """
        Create dict matching TagMetas id to TagMeta.

        :param raise_if_no_id: Raise ValueError if where is TagMetas without id.
        :type raise_if_no_id: bool, optional
        :return: Json format as a dict
        :rtype: :class:`dict`
        :raises: :class:`KeyError`, if where is duplication of TagMetas id
        """
        res = {}
        without_id = []
        for tag_meta in self:
            if tag_meta.sly_id is not None:
                if tag_meta.sly_id in res:
                    raise KeyError(
                        f"TagMeta with id={tag_meta.sly_id} already exists (duplication). "
                        f"Please contact tech support"
                    )
                else:
                    res[tag_meta.sly_id] = tag_meta
            else:
                without_id.append(tag_meta)
        if len(without_id) > 0 and raise_if_no_id is True:
            raise ValueError("There are TagMetas without id")
        return res

    def __iter__(self) -> Iterator[TagMeta]:
        return next(self)

    def refresh_ids_from(self, tags: TagMetaCollection) -> None:
        """Update ids of TagMetas in collection from given collection.

        :param tags: TagMetaCollection to update ids from.
        :type tags: TagMetaCollection
        """
        for new_tag in tags:
            my_tag = self.get(new_tag.name)
            if my_tag is None:
                continue
            my_tag._set_id(new_tag.sly_id)

    def get_by_id(self, tag_meta_id: int) -> TagMeta:
        """Return TagMeta with given id.

        :param tag_meta_id: TagMeta id to search for.
        :type tag_meta_id: int
        :return: TagMeta with given id.
        :rtype: TagMeta
        """
        for tag_meta in self:
            if tag_meta.sly_id == tag_meta_id:
                return tag_meta
        return None

    def get_tag_name_by_id(self, tag_meta_id: int) -> Optional[str]:
        for tag_meta in self:
            if tag_meta.sly_id == tag_meta_id:
                return tag_meta.name
        return None

    def merge(self, other: TagMetaCollection) -> TagMetaCollection:
        """
        Merge two TagMetaCollection objects.

        :param other: TagMetaCollection object.
        :type other:  TagMetaCollection
        :raises: :class:`ValueError` if item name from given collection is in TagMetaCollection but TagMetas in both are different
        :return: TagMetaCollection object
        :rtype: :class:`TagMetaCollection<TagMetaCollection>`

        :Usage Example:

         .. code-block:: python

            import supervisely as sly

            item_cat = sly.TagMeta('cat', sly.TagValueType.NONE)
            item_turtle = sly.TagMeta('turtle', sly.TagValueType.ANY_STRING)
            collection = sly.TagMetaCollection([item_cat, item_turtle])

            item_dog = sly.TagMeta('dog', sly.TagValueType.NONE)
            item_turtle = sly.TagMeta('turtle', sly.TagValueType.ANY_STRING)
            other_collection = sly.TagMetaCollection([item_dog, item_turtle])

            merge = collection.merge(other_collection)
            print(merge.to_json())
            # Output: [
            #     {
            #         "name": "dog",
            #         "value_type": "none",
            #         "color": "#8A6C0F",
            #         "hotkey": "",
            #         "applicable_type": "all",
            #         "classes": []
            #     },
            #     {
            #         "name": "cat",
            #         "value_type": "none",
            #         "color": "#0F4A8A",
            #         "hotkey": "",
            #         "applicable_type": "all",
            #         "classes": []
            #     },
            #     {
            #         "name": "turtle",
            #         "value_type": "any_string",
            #         "color": "#4F0F8A",
            #         "hotkey": "",
            #         "applicable_type": "all",
            #         "classes": []
            #     }
            # ]
        """
        new_tags = []
        for other_tag in other.items():
            our_tag = self.get(other_tag.key())
            if our_tag is None:
                new_tags.append(other_tag)
            elif our_tag != other_tag:
                if (
                    our_tag.value_type == TagValueType.ONEOF_STRING
                    and other_tag.value_type == TagValueType.ONEOF_STRING
                ):
                    if Counter(our_tag.possible_values) == Counter(other_tag.possible_values):
                        continue
                raise ValueError(
                    "Error during merge for tag {!r}: values are different".format(other_tag.key())
                )
        return self.clone(new_tags + self.items())


def make_renamed_tag_metas(
    src_tag_metas: TagMetaCollection, renamer: Renamer, skip_missing: bool = False
) -> TagMetaCollection:
    """make_renamed_tag_metas"""
    result_tags = []
    for src_tag in src_tag_metas:
        renamed_name = renamer.rename(src_tag.name)
        if renamed_name is not None:
            result_tags.append(src_tag.clone(name=renamed_name))
        elif not skip_missing:
            raise KeyError(
                "Tag meta named {} could not be mapped to a destination name.".format(src_tag.name)
            )
    return TagMetaCollection(items=result_tags)
