import copy
import numpy as np
from .scene_graph import TemporalSceneGraph, SceneGraph
from .node import MultiNode


class Scene(object):
    def __init__(self, timesteps, map=None, dt=1, name="", frequency_multiplier=1, aug_func=None,  non_aug_scene=None):
        self.map = map
        self.timesteps = timesteps
        self.dt = dt
        self.name = name

        self.nodes = []

        self.robot = None

        self.temporal_scene_graph = None

        self.frequency_multiplier = frequency_multiplier

        self.description = ""

        self.aug_func = aug_func
        self.non_aug_scene = non_aug_scene

    def add_robot_from_nodes(self, robot_type):
        nodes_list = [node for node in self.nodes if node.type == robot_type]
        non_overlapping_nodes = MultiNode.find_non_overlapping_nodes(nodes_list, min_timesteps=3)
        self.robot = MultiNode(robot_type, 'ROBOT', non_overlapping_nodes, is_robot=True)

        for node in non_overlapping_nodes:
            self.nodes.remove(node)
        self.nodes.append(self.robot)

    def get_clipped_pos_dict(self, timestep, state):
        pos_dict = dict()
        existing_nodes = self.get_nodes_clipped_at_time(timesteps=np.array([timestep]),
                                                        state=state)
        tr_scene = np.array([timestep, timestep])
        for node in existing_nodes:
            pos_dict[node] = node.get(tr_scene, {'position': ['x', 'y']})

        return pos_dict

    def get_scene_graph(self,
                        timestep,
                        attention_radius=None,
                        edge_addition_filter=None,
                        edge_removal_filter=None) -> SceneGraph:
        """
        Returns the Scene Graph for a given timestep. If the Temporal Scene Graph was pre calculated,
        the temporal scene graph is sliced. Otherwise the scene graph is calculated on the spot.

        :param timestep: Timestep for which the scene graph is returned.
        :param attention_radius: Attention radius for each node type permutation. (Only online)
        :param edge_addition_filter: Filter for adding edges (Only online)
        :param edge_removal_filter:  Filter for removing edges (Only online)
        :return: Scene Graph for given timestep.
        """
        if self.temporal_scene_graph is None:
            timestep_range = np.array([timestep - len(edge_removal_filter), timestep])
            node_pos_dict = dict()
            present_nodes = self.present_nodes(np.array([timestep]))

            for node in present_nodes[timestep]:
                node_pos_dict[node] = np.squeeze(node.get(timestep_range, {'position': ['x', 'y']}))
            tsg = TemporalSceneGraph.create_from_temp_scene_dict(node_pos_dict,
                                                                 attention_radius,
                                                                 duration=(len(edge_removal_filter) + 1),
                                                                 edge_addition_filter=edge_addition_filter,
                                                                 edge_removal_filter=edge_removal_filter
                                                                 )

            return tsg.to_scene_graph(t=len(edge_removal_filter),
                                      t_hist=len(edge_removal_filter),
                                      t_fut=len(edge_addition_filter))
        else:
            return self.temporal_scene_graph.to_scene_graph(timestep,
                                                            len(edge_removal_filter),
                                                            len(edge_addition_filter))

    def calculate_scene_graph(self,
                              attention_radius,
                              edge_addition_filter=None,
                              edge_removal_filter=None) -> None:
        """
        Calculate the Temporal Scene Graph for the entire Scene.

        :param attention_radius: Attention radius for each node type permutation.
        :param edge_addition_filter: Filter for adding edges.
        :param edge_removal_filter: Filter for removing edges.
        :return: None
        """
        timestep_range = np.array([0, self.timesteps-1])
        node_pos_dict = dict()

        for node in self.nodes:
            if type(node) is MultiNode:
                node_pos_dict[node] = np.squeeze(node.get_all(timestep_range, {'position': ['x', 'y']}))
            else:
                node_pos_dict[node] = np.squeeze(node.get(timestep_range, {'position': ['x', 'y']}))

        self.temporal_scene_graph = TemporalSceneGraph.create_from_temp_scene_dict(node_pos_dict,
                                                                                   attention_radius,
                                                                                   duration=self.timesteps,
                                                                                   edge_addition_filter=edge_addition_filter,
                                                                                   edge_removal_filter=edge_removal_filter)

    def duration(self):
        """
        Calculates the duration of the scene.

        :return: Duration of the scene in s.
        """
        return self.timesteps * self.dt

    def present_nodes(self,
                      timesteps,
                      type=None,
                      min_history_timesteps=0,
                      min_future_timesteps=0,
                      return_robot=True) -> dict:
        """
        Finds all present nodes in the scene at a given timestemp

        :param timesteps: Timestep(s) for which all present nodes should be returned
        :param type: Node type which should be returned. If None all node types are returned.
        :param min_history_timesteps: Minimum history timesteps of a node to be returned.
        :param min_future_timesteps: Minimum future timesteps of a node to be returned.
        :param return_robot: Return a node if it is the robot.
        :return: Dictionary with timesteps as keys and list of nodes as value.
        """

        present_nodes = {}

        for node in self.nodes:
            if node.is_robot and not return_robot:
                continue
            if type is None or node.type == type:
                lower_bound = timesteps - min_history_timesteps
                upper_bound = timesteps + min_future_timesteps
                mask = (node.first_timestep <= lower_bound) & (upper_bound <= node.last_timestep)
                if mask.any():
                    timestep_indices_present = np.nonzero(mask)[0]
                    for timestep_index_present in timestep_indices_present:
                        if timesteps[timestep_index_present] in present_nodes.keys():
                            present_nodes[timesteps[timestep_index_present]].append(node)
                        else:
                            present_nodes[timesteps[timestep_index_present]] = [node]

        return present_nodes

    def get_nodes_clipped_at_time(self, timesteps, state):
        clipped_nodes = list()

        existing_nodes = self.present_nodes(timesteps)
        all_nodes = set().union(*existing_nodes.values())
        if not all_nodes:
            return clipped_nodes

        tr_scene = np.array([timesteps.min(), timesteps.max()])
        for node in all_nodes:
            if isinstance(node, MultiNode):
                copied_node = copy.deepcopy(node.get_node_at_timesteps(tr_scene))
                copied_node.id = self.robot.id
            else:
                copied_node = copy.deepcopy(node)

            clipped_value = node.get(tr_scene, state[node.type])
            copied_node.overwrite_data(clipped_value)
            copied_node.first_timestep = tr_scene[0]

            clipped_nodes.append(copied_node)

        return clipped_nodes

    def sample_timesteps(self, batch_size, min_future_timesteps=0) -> np.ndarray:
        """
        Sample a batch size of possible timesteps for the scene.

        :param batch_size: Number of timesteps to sample.
        :param min_future_timesteps: Minimum future timesteps in the scene for a timestep to be returned.
        :return: Numpy Array of sampled timesteps.
        """
        if batch_size > self.timesteps:
            batch_size = self.timesteps
        return np.random.choice(np.arange(0, self.timesteps-min_future_timesteps), size=batch_size, replace=False)

    def augment(self):
        if self.aug_func is not None:
            return self.aug_func(self)
        else:
            return self

    def get_node_by_id(self, id):
        for node in self.nodes:
            if node.id == id:
                return node

    def __repr__(self):
        return f"Scene: Duration: {self.duration()}s," \
               f" Nodes: {len(self.nodes)}," \
               f" Map: {'Yes' if self.map is not None else 'No'}."
