# Copyright (C) 2021-2022 HERE Global B.V. and its affiliate(s).
# All rights reserved.
#
# This software and other materials contain proprietary information
# controlled by HERE and are protected by applicable copyright legislation.
# Any use and utilization of this software and other materials and
# disclosure to any third parties is conditional upon having a separate
# agreement with HERE for the access, use, utilization or disclosure of this
# software. In the absence of such agreement, the use of the software is not
# allowed.

"""
Bindings for the Associated Segments from Address Attributes and Street Names Layers.
"""
from typing import Dict, Iterable, List, Mapping, Optional, Set

from google.protobuf.message import Message
from here.content.base import ContentPartition, SingleLayerBinding
from here.content.content import Content as ParentContent
from here.content.content import hmc_schema_version
from here.content.hmc2.address_attributes import AddressAttributes
from here.content.hmc2.base_attributes import Attribute, BaseAttributesMultiLayerBinding
from here.content.hmc2.street_names import StreetNames
from here.platform.adapter import Adapter


class SegmentToStreetName(AddressAttributes, BaseAttributesMultiLayerBinding):
    """Class to associate segment with address attributes and street names"""

    _content: ParentContent

    def __init__(self, content: ParentContent, layer_id: str, adapter: Optional[Adapter] = None):
        """
        Instantiate a class Multilayer Street Names binding.
        Assign content to Class attribute _content.

        :param content: Content object that references a catalog at a fixed version
        :param layer_id: ID of the only layer that contains the content
        :param adapter: the ``Adapter`` to transform data between different representations,
            ``None`` to use the adapter defined in ``content``.
        """
        super().__init__(content, layer_id, adapter)
        SegmentToStreetName._content = content

    @classmethod
    def schema_hrn(cls) -> List[str]:
        """
        :return: the HRN of schema this binding can parse and index
        """

        return [
            f"hrn:here:schema:::com.here.schema.rib:address-attributes_v2:{hmc_schema_version}",
            f"hrn:here:schema:::com.here.schema.rib:street-names_v2:{hmc_schema_version}",  # noqa: E231,E501
        ]

    @classmethod
    def associated_layers(cls) -> Dict[str, SingleLayerBinding]:
        """
        Provide associated dependent/independent layers to the base layer.

        :return: dict of key as single layer and value as SingleLayerBinding Object.
        """
        return {"street_names": StreetNames(content=cls._content, layer_id="street-names")}

    @classmethod
    def object_types(cls) -> Mapping[str, type]:
        """
        :return: the types of attributes supported by this binding
        """
        return {
            "address_range": Attribute,
            "street_section": Attribute,
        }

    @classmethod
    def _internal_object_types(cls) -> Mapping[str, type]:
        """
        :return: the types of attributes supported by this binding
        """
        return {}

    @classmethod
    def parse_and_index(cls, partition_id: str, msg: Message) -> ContentPartition:
        """
        Parse and index decoded message.

        :param partition_id: identifier of the partition
        :param msg: decoded content from the layer, in the form of a Protobuf message
        :return: the content of a partition parsed and indexed in its own data structure
        """

        assert hasattr(msg, "address_range")
        assert hasattr(msg, "street_section")

        address_attributes = super().parse_and_index(partition_id, msg)
        street_names_layer = cls.associated_layers()["street_names"]

        raw_address_range = address_attributes.objects("address_range")
        raw_street_section = address_attributes.objects("street_section")

        partition_ids = cls._extract_partition_ids(raw_address_range, raw_street_section)

        street_section_objects: Dict[str, Dict] = {}
        for pid in partition_ids:
            s_names = street_names_layer.get(pid, "street_section")
            street_section_objects.update(**s_names)

        address_range = cls._parse_address_range(raw_address_range, street_section_objects)
        street_section = cls._parse_street_section(raw_street_section, street_section_objects)

        return ContentPartition(cls, address_range=address_range, street_section=street_section)

    @classmethod
    def _extract_partition_ids(cls, address_range: Iterable, street_section: Iterable) -> Set[str]:
        """
        Extracts the partition IDs from the `address_range` and `street_section` attributes.

        :param address_range: address range attributes
        :param street_section: street section attributes
        :return: Set of partition IDs
        """
        partition_ids = {
            ssr["partition_name"]
            for ar in address_range
            for ssr in ar.attribute["street_section_ref"]
        }
        partition_ids.update(
            {ss.attribute["street_section_ref"]["partition_name"] for ss in street_section}
        )
        return partition_ids

    @classmethod
    def _parse_address_range(
        cls, address_range: Iterable, street_section_objects: Dict[str, Dict]
    ):
        """
        Parses `address_range` attributes and replaces `street_section_ref`
            with actual `street_section` objects.

        :param address_range: raw address range attributes with references
            to street_section via partition_id and identifier.
        :param street_section_objects: concrete street section objects.
        :return: parsed address attributes having street section objects
            instead of street section references.
        """
        for ar in address_range:
            if "street_section_ref" in ar.attribute:
                ar.attribute["street_section"] = [
                    street_section_objects[ssr["partition_name"]][ssr["identifier"]]
                    for ssr in ar.attribute["street_section_ref"]
                ]
                del ar.attribute["street_section_ref"]
        return address_range

    @classmethod
    def _parse_street_section(
        cls, street_section: Iterable, street_section_objects: Dict[str, Dict]
    ):
        """
        Parses `street_section` attributes and replaces `street_section_ref`
            with actual `street_section` objects.

        :param street_section: raw street section attributes with references
            to street_section via partition_id and identifier.
        :param street_section_objects: concrete street section objects.
        :return: parsed address attributes having street section objects
            instead of street section references.
        """
        for ss in street_section:
            if "street_section_ref" in ss.attribute:
                ssr = ss.attribute["street_section_ref"]
                ss.attribute["street_section"] = street_section_objects[ssr["partition_name"]][
                    ssr["identifier"]
                ]
                del ss.attribute["street_section_ref"]
        return street_section
