# Copyright (C) 2020-2021 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.

"""
A class to hold ADAS attributes
"""

import copy
from typing import Optional

from here.content.hmc.baseattributes import BaseLayer, get_end_offset
from here.platform.adapter import Adapter

offset_epsilon = 1e-5


class AdasAttributes(BaseLayer):
    """
    A class to hold ADAS attributes
    """

    def get_attribute(self, pid, attribute_name, adapter: Optional[Adapter] = None):
        """
        Get the attribute for given partitionId and attribute name
        Intended attributes: curvature_heading, slope, elevation,
        built_up_area_road, link_accuracy

        :param pid: a partition ID
        :param attribute_name: attribute collection name in the partition
        :param adapter: the ``Adapter`` to transform data between different representations,
            ``None`` to use the adapter defined in the constructor or, if unset, in ``content``.
        :return: attribute dict or an adapter-specific type
        """

        content_adapter = self._content_adapter(adapter)

        # first fill the attribute
        self.fill_attribute(pid, attribute_name)

        if content_adapter is None:
            if attribute_name in ("curvature_heading", "elevation", "slope"):
                segments, nodes = None, None
                if (
                    attribute_name in self.attributes
                    and pid in self.attributes[attribute_name]["segments"]
                ):
                    segments = self.attributes[attribute_name]["segments"][pid]
                if (
                    attribute_name in self.attributes
                    and pid in self.attributes[attribute_name]["nodes"]
                ):
                    nodes = self.attributes[attribute_name]["nodes"][pid]
                return segments, nodes
            else:
                if attribute_name in self.attributes and pid in self.attributes[attribute_name]:
                    return self.attributes[attribute_name][pid]
                else:
                    return None
        else:
            if attribute_name in ("link_accuracy", "slope"):
                columns, data = self.get_tabular_data(pid, attribute_name)
                return content_adapter.from_tabular(columns, data)
            else:  # curvature_heading, elevation
                segment_columns, segment_data, node_columns, node_data = self.get_tabular_data(
                    pid, attribute_name
                )
                segment_df = content_adapter.from_tabular(segment_columns, segment_data)
                node_df = content_adapter.from_tabular(node_columns, node_data)
                return segment_df, node_df

    def fill_attribute(self, pid, attribute_name):
        """
        Fill the attribute for given attribute name
        Intended attributes: curvature, heading, slope, elevation,
        built_up_area_road, link_accuracy

        :param pid: a partition ID
        :param attribute_name: attribute collection name in the partition
        """

        # initialize self.attributes[attribute_name] container
        self.attributes.setdefault(attribute_name, {})

        # do nothing if pid is already filled
        if pid in self.attributes[attribute_name]:
            return

        # get partition object
        partition = self.get_partition(pid)

        if "elevation" in attribute_name:
            # associate segments/nodes with attributes via segment_anchors
            segment_attributes, node_attributes = self.resolve_adas_segment_node_anchors(
                partition, attribute_name
            )

            # update attributes container
            self.attributes[attribute_name].update(
                {"segments": segment_attributes, "nodes": node_attributes}
            )
        elif attribute_name in ("curvature_heading", "slope"):
            # associate segments/nodes with attributes via segment_anchors
            segment_attributes, node_attributes = self.resolve_adas_segment_anchors(
                partition, attribute_name
            )

            # update attributes container
            if "curvature_heading" in attribute_name:
                self.attributes[attribute_name].update(
                    {"segments": segment_attributes, "nodes": node_attributes}
                )
            else:
                # slope attribute does not have node association
                self.attributes[attribute_name].update(segment_attributes)
        else:
            # for example, link_accuracy
            attributes = self.resolve_single_segment_anchors(partition, attribute_name)
            self.attributes[attribute_name].update(attributes)

    @staticmethod
    def resolve_adas_segment_anchors(partition, attribute_name):
        """
        Associate segment/node with curvature/heading
        :param partition: a partition object
        :param attribute_name: attribute collection name in the partition
        :return: a dict of segment -> list of (offset, attribute)
                 a dict of (pid,sid,node_string) -> list of (pid,sid,node_string,attribute)
        :raises ValueError: For segment curvatures,
            raises errors if start/end offsets are not equal.
            For node curvatures,
            raises errors if number of associated segments are not equal to 2.
        """

        segment_attributes = {}
        node_attributes = {}

        for attr in getattr(partition, attribute_name):

            # clone this attribute and remove unnecessary fields for output
            value = copy.copy(attr)
            del value.segment_anchor_index[:]

            for idx in attr.segment_anchor_index:

                # get a segment anchor for this attribute
                anchor = partition.segment_anchor[idx]

                # get start offset for this anchor
                start_offset = anchor.first_segment_start_offset.value

                # get end offset for this anchor
                end_offset = get_end_offset(anchor)

                nsegments = len(anchor.oriented_segment_ref)

                if nsegments == 1:
                    # this attribute is associated along the segment

                    # TODO: Need to check there are cases falling into this category
                    # if abs(start_offset - end_offset) > offset_epsilon:
                    #     message = "Two offset values from {} in {} are different.".format(
                    #         attribute_name, partition.partition_name
                    #     )
                    #     message += "offset ({}, {})".format(start_offset, end_offset)
                    #     raise ValueError(message)

                    osr = anchor.oriented_segment_ref[0]
                    pid = osr.segment_ref.partition_name
                    sid = osr.segment_ref.identifier

                    segment_attributes.setdefault(pid, {})
                    segment_attributes[pid].setdefault(sid, [])
                    segment_attributes[pid][sid].append((start_offset, value))
                    segment_attributes[pid][sid].sort(key=lambda x: x[0])

                elif nsegments == 2:
                    # this attribute is associated a node where two segments intersect

                    # from the first segment
                    osr0 = anchor.oriented_segment_ref[0]
                    pid0 = osr0.segment_ref.partition_name
                    sid0 = osr0.segment_ref.identifier
                    # maybe additional check for comparing with 1.0 for end nodes?
                    node0 = "start" if abs(start_offset - 0.0) < offset_epsilon else "end"

                    # from the second segment
                    osr1 = anchor.oriented_segment_ref[1]
                    pid1 = osr1.segment_ref.partition_name
                    sid1 = osr1.segment_ref.identifier
                    # maybe additional check for comparing with 1.0 for end nodes?
                    node1 = "start" if abs(end_offset - 0.0) < offset_epsilon else "end"

                    # fill sid0
                    node_attributes.setdefault(pid0, {})
                    node_attributes[pid0].setdefault(sid0, {})
                    node_attributes[pid0][sid0].setdefault(node0, [])
                    node_attributes[pid0][sid0][node0].append(
                        (pid0, sid0, node0, pid1, sid1, node1, value)
                    )

                    # fill sid1
                    node_attributes.setdefault(pid1, {})
                    node_attributes[pid1].setdefault(sid1, {})
                    node_attributes[pid1][sid1].setdefault(node1, [])
                    node_attributes[pid1][sid1][node1].append(
                        (pid0, sid0, node0, pid1, sid1, node1, value)
                    )

                else:
                    message = "Too many segments (={}) associated with {} in {}".format(
                        nsegments, attribute_name, partition.partition_name
                    )
                    raise ValueError(message)

        return segment_attributes, node_attributes

    @staticmethod
    def resolve_adas_segment_node_anchors(partition, attribute_name):
        """
        Associate segment/node with curvature/heading
        :param partition: a partition object
        :param attribute_name: attribute collection name in the partition
        :return: a dict of segment -> list of (offset, attribute)
                 a dict of (pid,sid,node_string) -> list of (pid,sid,node_string,attribute)
        :raises ValueError: if start/end offsets are not equal
        """

        segment_attributes = {}
        node_attributes = {}

        for attr in getattr(partition, attribute_name):

            # clone this attribute and remove unnecessary fields for output
            value = copy.copy(attr)
            del value.segment_anchor_index[:]

            # attribute associated with segments
            for idx in attr.segment_anchor_index:

                # get a segment anchor for this attribute
                anchor = partition.segment_anchor[idx]

                # get start offset for this anchor
                start_offset = anchor.first_segment_start_offset.value

                # get end offset for this anchor
                end_offset = get_end_offset(anchor)

                # associate with segments if any
                for osr in anchor.oriented_segment_ref:

                    if abs(start_offset - end_offset) > offset_epsilon:
                        message = "Two offset values from {} in {} are different.".format(
                            attribute_name, partition.partition_name
                        )
                        message += "offset ({}, {})".format(start_offset, end_offset)
                        raise ValueError(message)

                    pid = osr.segment_ref.partition_name
                    sid = osr.segment_ref.identifier

                    segment_attributes.setdefault(pid, {})
                    segment_attributes[pid].setdefault(sid, [])
                    segment_attributes[pid][sid].append((start_offset, value))
                    segment_attributes[pid][sid].sort(key=lambda x: x[0])

            # attribute associated with nodes
            for idx in attr.node_anchor_index:

                # get a segment anchor for this attribute
                anchor = partition.node_anchor[idx]

                npid = anchor.node_ref.partition_name
                nid = anchor.node_ref.identifier

                # fill node
                node_attributes.setdefault(npid, {})
                node_attributes[npid][nid] = value

        return segment_attributes, node_attributes

    def get_tabular_data(self, pid, attribute_name):
        """
        Convert the given attribute to tabular data
        :param pid: partition ID
        :param attribute_name: attribute name to look for
        :return:
            for curvature_heading and elevation:
                segment_columns, segment_data, node_columns, node_data
            otherwise: columns, data
        """

        if "link_accuracy" in attribute_name:
            return self.get_tabular_data_from_single_segment(pid, attribute_name)
        elif "slope" in attribute_name:
            segment_columns = ("partitionId", "segmentId", "offset", "attribute")
            segment_data = []
            if attribute_name in self.attributes and pid in self.attributes[attribute_name]:
                for sid, offsets in self.attributes[attribute_name][pid].items():
                    for offset, value in offsets:
                        segment_data.append((pid, sid, offset, value))
            return segment_columns, segment_data
        elif "elevation" in attribute_name:
            segment_columns = ("partitionId", "segmentId", "offset", "attribute")
            node_columns = ("partitionId", "nodeId", "attribute")
            segment_data = []
            node_data = []
            if (
                attribute_name in self.attributes
                and pid in self.attributes[attribute_name]["segments"]
            ):
                for sid, offsets in self.attributes[attribute_name]["segments"][pid].items():
                    for offset, value in offsets:
                        segment_data.append((pid, sid, offset, value))
            if (
                attribute_name in self.attributes
                and pid in self.attributes[attribute_name]["nodes"]
            ):
                for nid, value in self.attributes[attribute_name]["nodes"][pid].items():
                    node_data.append((pid, nid, value))
            return segment_columns, segment_data, node_columns, node_data
        else:  # curvature_heading
            segment_columns = ("partitionId", "segmentId", "offset", "attribute")
            node_columns = ("partitionId", "segmentId", "node", "connectivity")
            segment_data = []
            node_data = []
            if (
                attribute_name in self.attributes
                and pid in self.attributes[attribute_name]["segments"]
            ):
                for sid, offsets in self.attributes[attribute_name]["segments"][pid].items():
                    for offset, value in offsets:
                        segment_data.append((pid, sid, offset, value))
            if (
                attribute_name in self.attributes
                and pid in self.attributes[attribute_name]["nodes"]
            ):
                for sid, nodes in self.attributes[attribute_name]["nodes"][pid].items():
                    for node, connectivities in nodes.items():
                        for connectivity in connectivities:
                            node_data.append((pid, sid, node, connectivity))
            return segment_columns, segment_data, node_columns, node_data
