from pacman import exceptions
from collections import namedtuple
import logging
logger = logging.getLogger(__name__)


class ValidRouteChecker(object):

    def __init__(self, partitioned_graph, placements, routing_infos,
                 routing_tables, machine):
        """constructor for the valid route checker

        :param partitioned_graph: the subgraph of the problem spec
        :param placements: the placements container
        :param routing_infos:  the routing info container
        :param routing_tables: the routing tables generated by the
        routing algorithum
        :param machine: the spinnmachine object
        :type machine: spinnmachine.machine.Machine object
        :return: None
        :raise None: this method does not raise any known excpetion
        """
        self._partitioned_graph = partitioned_graph
        self._placements = placements
        self._routing_infos = routing_infos
        self._routing_tables = routing_tables
        self._machine = machine

    def validate_routes(self):
        """ this method goes though the placements given during init and
        checks that the routing entries within the routing tables support
        reaching the correction destinations as well as not producing any
        cycles.

        :return: None
        :raises PacmanRoutingException: when either no routing table entry is
        found by the search on a given router, a cycle is detected (this is
         where a collection of routing entries have resulted in the search
         visiting the same router duirng the same trace) or when it has
         compelted the trace and there are still destinations which have
         not been visited

        """
        placement_tuple = namedtuple('Placement', 'x y p')
        for placement in self._placements.placements:
            outgoing_edges_for_partitioned_vertex = \
                self._partitioned_graph.outgoing_subedges_from_subvertex(
                    placement.subvertex)
            #locate all placements to which this placement/subvertex will
            # communicate with
            destination_placements = set()
            for outgoing_edge in outgoing_edges_for_partitioned_vertex:
                dest_placement = self._placements.get_placement_of_subvertex(
                    outgoing_edge.post_subvertex)
                destination_placements.add(
                    placement_tuple(dest_placement.x, dest_placement.y,
                                    dest_placement.p))
            #only check placements that have outgoing edges
            if len(outgoing_edges_for_partitioned_vertex) > 0:
                #check that the routing elements for this placement work
                # as expected
                self._search_route(placement, destination_placements,
                                   outgoing_edges_for_partitioned_vertex,
                                   placement_tuple)

    def _search_route(self, source_placement, dest_placements, outgoing_edges,
                      placement_tuple):
        """entrance method to locate if the routing tables work for the
        source to desks as defined

        :param source_placement: the placement from which the search started
        :param dest_placements: the placements to which this trace should visit
        only once
        :param outgoing_edges: the outgoing edges from the partitioned_vertex
        which resides on the processor.
        :return: None
        :raise PacmanRoutingException: when the trace completes and there are
        still destinations not visited
        """
        key = self._check_keys(outgoing_edges)
        for dest in dest_placements:
            logger.debug("[{}:{}:{}]".format(dest.x, dest.y, dest.p))

        located_dests = set()

        self._start_trace_via_routing_tables(
            source_placement, key, located_dests, placement_tuple)

        #start removing from located_dests and check if dests not reached
        failed_to_reach_dests = list()
        for dest in dest_placements:
            if dest in located_dests:
                located_dests.remove(dest)
            else:
                failed_to_reach_dests.append(dest)

        #check for error if trace didnt reach a destination it was meant to
        error_message = ""
        if len(failed_to_reach_dests) > 0:
            output_string = ""
            for dest in failed_to_reach_dests:
                output_string += "[{}:{}:{}]".format(dest.x, dest.y, dest.p)
            source_processor = "[{}:{}:{}]".format(
                source_placement.x, source_placement.y, source_placement.p)
            error_message += "failed to locate all dstinations with subvertex" \
                             " {} on processor {} with key {} as it didnt " \
                             "reach dests {}".format(
                             source_placement.subvertex.label, source_processor,
                             key, output_string)
        # check for error if the trace went to a destination it shouldnt have
        if len(located_dests) > 0:
            output_string = ""
            for dest in located_dests:
                output_string += "[{}:{}:{}]".format(dest.x, dest.y, dest.p)
            source_processor = "[{}:{}:{}]".format(
                source_placement.x, source_placement.y, source_placement.p)
            error_message += "trace went to more failed to locate all " \
                             "dstinations with subvertex {} on processor {} " \
                             "with key {} as it didnt reach dests {}".format(
                             source_placement.subvertex.label, source_processor,
                             key, output_string)
        #raise error if required
        if error_message != "":
            raise exceptions.PacmanRoutingException(error_message)
        else:
            logger.debug("successful test between {} and {}"
                         .format(source_placement.subvertex.label,
                                 dest_placements))

    def _start_trace_via_routing_tables(
            self, source_placement, key, reached_placements, placement_tuple):
        """this method starts the trace, by using the source placemnts
        router and tracing from the route.

        :param source_placement: the soruce placement used by the trace
        :param placement_tuple: the reprenstation of a placement
        :param key: the key being used by the partitioned_vertex which resides
        on the soruce placement
        :param reached_placements: the placements reached during the trace
        :return: None
        :raises None: this method does not raise any known exception
        """
        current_router_table = \
            self._routing_tables.get_routing_table_for_chip(source_placement.x,
                                                            source_placement.y)
        visited_routers = set()
        visited_routers.add(current_router_table)
        #get src router
        entry = self._locate_routing_entry(current_router_table, key)
        self._recursive_trace_to_dests(
            entry, current_router_table, source_placement.x, source_placement.y,
            key, visited_routers, reached_placements, placement_tuple)

    # locates the next dest pos to check
    def _recursive_trace_to_dests(self, entry, current_router, chip_x, chip_y,
                                  key, visited_routers, reached_placements,
                                  placement_tuple):
        """ this method recurively searches though routing tables till
        no more entries are registered with this key

        :param entry: the orginal entry used by the first router which
        resides on the soruce placement chip.
        :param placement_tuple: represnetation of a placement
        :param current_router: the router currently being visited during the
         trace
        :param key: the key being used by the partitioned_vertex which resides
        on the soruce placement
        :param visited_routers: the list of routers which have been visited
        during this tracve so far
        :param reached_placements: the placements reached during the trace
        :return: None
        :raise None: this method does not raise any known exceptions
        """
        #determine where the route takes us
        chip_links = entry.link_ids
        processor_values = entry.processor_ids
        if len(chip_links) > 0:  # if goes downa chip link
            if len(processor_values) > 0:  # also goes to a processor
                self._check_processor(processor_values, current_router,
                                      reached_placements, placement_tuple)
            # only goes to new chip
            for link_id in chip_links:
                #locate next chips router
                machine_router = \
                    self._machine.get_chip_at(chip_x, chip_y).router
                link = machine_router.get_link(link_id)
                next_router = \
                    self._routing_tables.get_routing_table_for_chip(
                        link.destination_x, link.destination_y)
                #check that we've not visited this router before
                self._check_visited_routers(chip_x, chip_y, visited_routers)
                #locate next entry
                entry = self._locate_routing_entry(next_router, key)
                # get next route value from the new router
                self._recursive_trace_to_dests(
                    entry, next_router, link.destination_x, link.destination_y,
                    key, visited_routers, reached_placements, placement_tuple)
        elif len(processor_values) > 0:  # only goes to a processor
            self._check_processor(processor_values, current_router,
                                  reached_placements, placement_tuple)

    @staticmethod
    def _check_visited_routers(chip_x, chip_y, visited_routers):
        """checks if the trace has visited this router already

        :param next_router: the next router to add to visited routers
        :param visited_routers: routers already visted
        :return: None
        :raise PacmanRoutingException: when a router has been visited twice.
        """
        visited_routers_router = (chip_x, chip_y)
        if visited_routers_router in visited_routers:
            raise exceptions.PacmanRoutingException(
                "visited this router before, there is a cycle here")
        else:
            visited_routers.add(visited_routers_router)

    def _check_keys(self, outgoing_subedges_from_a_placement):
        """checks that all the subedges handed to the algorithum have the
        same key

        :param outgoing_subedges_from_a_placement:  the subegdes to check
        :return :None
        :raises Exception: when the keymask_combo is different between the
        subedges
        """
        key = None
        for sub_edge in outgoing_subedges_from_a_placement:
            current_key = self._routing_infos.get_key_from_subedge(sub_edge)
            if key is None:
                key = current_key
            elif key != current_key:
                raise exceptions.PacmanRoutingException(
                    "the keys from this placement do not match."
                    " Please rectify and retry")
        return key

    @staticmethod
    def _check_processor(processor_ids, current_router, reached_placements,
                         placement_tuple):
        """checks for processors to be removed

        :param reached_placements: the placements to which the trace visited
        :param processor_ids: the processor ids which the last router entry
        said the trace should visit
        :param current_router: the current router being used in the trace

        :return: None
        :raise None: this method does not raise any known exceptions
        """

        dest_x, dest_y = current_router.x, current_router.y
        for processor_id in processor_ids:
            reached_placements.add(placement_tuple(dest_x, dest_y,
                                                   processor_id))


    @staticmethod
    def _locate_routing_entry(current_router, key):
        """loate the entry from the router based off the subedge

        :param current_router: the current router being used in the trace
        :param key: the key being used by the source placement
        :return None:
        :raise PacmanRoutingException: when there is no entry located on this
        router.
        """
        for entry in current_router.multicast_routing_entries:
            key_combo = entry.mask & key
            if key_combo == entry.key_combo:
                return entry
        else:
            raise exceptions.PacmanRoutingException("no entry located")