import multiprocessing

import queue

from pathfinder.core.constants import NODE_DEGREE_LIMIT, NEIGHBOR_LIMIT
from pathfinder.core.model.Node import Node
from pathfinder.core.model.Path import Path
from pathfinder.core.model.PathFinderModel import PathFinderModel
from pathfinder.core.repo.repo_factory import get_repo


def process_path(path_string):
    try:
        path_finder_model = PathFinderModel.deserialize(path_string)
        result = []
        if path_finder_model.path.path_limit > 0:
            last_link = path_finder_model.path.last()
            repo = get_repo(path_finder_model.repo_name, path_finder_model.plover_url, path_finder_model.ngd_url,
                            path_finder_model.degree_url)
            node_degree = repo.get_node_degree(last_link.id)
            if node_degree > NODE_DEGREE_LIMIT:
                return path_string, result, None
            neighbors = repo.get_neighbors(last_link, NEIGHBOR_LIMIT)
            for neighbor in neighbors:
                if neighbor not in path_finder_model.path.links:
                    new_path = path_finder_model.path.make_new_path(neighbor)
                    result.append(new_path.serialize())

        return path_string, result, None
    except Exception as e:
        return path_string, None, e


class BreadthFirstSearch:

    def __init__(self, repository_name, plover_url, ngd_url, degree_url, path_container, logger):
        self.repo_name = repository_name
        self.plover_url = plover_url
        self.ngd_url = ngd_url
        self.degree_url = degree_url
        self.path_container = path_container
        self.logger = logger

    def traverse(self, source_id, hops_numbers=1):
        self.logger.info(f"Expanding {source_id}")
        path_queue = queue.Queue()
        new_path = Path(hops_numbers, [Node(source_id, weight=1)])
        path_queue.put(new_path)
        self.path_container.add_new_path(new_path)

        if hops_numbers == 0:
            return

        num_cores = min(multiprocessing.cpu_count(), 4)

        with multiprocessing.Pool(num_cores) as pool:
            while not path_queue.empty():
                paths = []
                for _ in range(4 * num_cores):
                    if not path_queue.empty():
                        paths.append(PathFinderModel(self.repo_name, path_queue.get(), self.plover_url, self.ngd_url,
                                                     self.degree_url).serialize())

                new_paths_list = pool.map(process_path, paths)

                for path_string, new_paths, exception in new_paths_list:
                    if exception:
                        path_finder_model = PathFinderModel.deserialize(path_string)
                        self.logger.error(f"Path {path_finder_model.path} raised an exception: {exception}")
                        raise exception
                    else:
                        for new_path in new_paths:
                            p = Path.deserialize(new_path)
                            path_queue.put(p)
                            self.path_container.add_new_path(p)
