import logging
from logging import Logger

from production.model import (
    ProdStates,
    ProductionOrder,
    OrderChanger,
)
from typing import Dict, List
from ._ProductionBox import ProductionBox


class OrderRoundRobinBox(ProductionBox):
    def __init__(
        self,               # type: OrderRoundRobinBox
        name,               # type: str
        sons,               # type: List[ProductionBox]
        order_changer,      # type: OrderChanger
        logger=None,        # type: Logger
    ):
        super(OrderRoundRobinBox, self).__init__(
            name=name,
            sons=sons,
            logger=logger,
        )

        self.order_changer = order_changer

        self.paths_dict = {}  # type: Dict[int, OrderRoundRobinBox.Path]
        for index in range(len(sons)):
            self.paths_dict[index] = OrderRoundRobinBox.Path(index, sons[index])

        self.order_path = {}  # type: Dict[int, OrderRoundRobinBox.Path]
        self.last_used_path_index = 0
        self.stored_orders = []

    def order_modified(
        self,       # type: OrderRoundRobinBox
        order,      # type: ProductionOrder
    ):
        self.logger.debug("Box name: {} - Routing order:\n{}\n".format(self.name, order))

        if self.logger.level >= logging.DEBUG:
            paths = "Paths and priorities: "
            paths += " ---- ".join([str(x) for x in self.paths_dict.values()])
            self.logger.debug(paths)

        is_done = True
        for item in order.items:
            if not item.has_tag("done"):
                is_done = False
                break

        if order.round_robin_path or order.order_id in self.order_path:
            self.logger.debug("Order previously routed")
            if order.order_id in self.order_path:
                current_path_index = self.order_path[order.order_id].index
            else:
                current_path_index = order.round_robin_path

            current_path = self.paths_dict[current_path_index]
            if order.order_id in self.order_path:
                if order.prod_state in (ProdStates.SERVED, ProdStates.INVALID) or is_done:
                    self.logger.debug("Know SERVED order, removing from internal dictionaries")
                    del self.order_path[order.order_id]
                    current_path.height -= self._get_order_cost(current_path.orders[order.order_id])
                    del current_path.orders[order.order_id]
                else:
                    self.logger.debug("Know not SERVED order, updating internal dictionaries")
                    current_path.height -= self._get_order_cost(current_path.orders[order.order_id])
                    current_path.height += self._get_order_cost(order)
                    current_path.orders[order.order_id] = order
            elif order.prod_state not in (ProdStates.SERVED, ProdStates.INVALID) and not is_done:
                self.logger.debug("Unknown not SERVED order, adding to internal dictionaries")
                current_path.height += self._get_order_cost(order)
                self.order_path[order.order_id] = current_path
                current_path.orders[order.order_id] = order

            current_path.son.order_modified(order)
        else:
            self.logger.debug("Order never routed")
            if self.refreshing:
                self.logger.debug("Refreshing views. Storing order to process later")
                self.stored_orders.append(order)
                return

            if order.prod_state not in (ProdStates.SERVED, ProdStates.INVALID) and not is_done:
                self.logger.debug("Not served order, adding to internal dictionaries")

                lowest_height_path = self._get_lowest_height_path()
                lowest_height_path.orders[order.order_id] = order
                lowest_height_path.height += self._get_order_cost(order)

                self._add_path_to_root_order(order, lowest_height_path.index)
                self.order_path[order.order_id] = lowest_height_path
                lowest_height_path.son.order_modified(order)

            else:
                self.logger.debug("Served order, just propagating to sons")
                self._publish_order_to_all_sons(order)

    def start_refresh(self):
        self.logger.debug("{} - Start Refresh".format(self.name))

        super(OrderRoundRobinBox, self).start_refresh()
        self.order_path.clear()
        for path in list(self.paths_dict.values()):
            path.height = 0
            path.clear_orders()

    def end_refresh(self):
        self.logger.debug("{} - End Refresh".format(self.name))

        super(OrderRoundRobinBox, self).end_refresh()
        for order in self.stored_orders:
            self.order_modified(order)
        self.stored_orders = []

    def __setattr__(self, name, value):
        if name == "sons" and isinstance(value, list):
            if hasattr(self, "paths_dict"):
                for path in list(self.paths_dict.values()):
                    path.son = value[path.index]
            super(OrderRoundRobinBox, self).__setattr__(name, value)
        else:
            super(OrderRoundRobinBox, self).__setattr__(name, value)

    @staticmethod
    def _get_order_cost(
        order,      # type: ProductionOrder
    ):
        # type: (...) -> int

        return order.cost or 0

    def _get_lowest_height_path(
        self,       # type: OrderRoundRobinBox
    ):
        # type: (...) -> OrderRoundRobinBox.Path

        all_paths = list(self.paths_dict.values())
        start_index = (self.last_used_path_index + 1) % len(all_paths)

        lowest = all_paths[start_index]
        for path in all_paths[start_index:] + all_paths[0:start_index]:
            if path.height < lowest.height:
                lowest = path

        self.last_used_path_index = lowest.index
        return lowest

    def _publish_order_to_all_sons(self, order):
        for son in self.sons:
            son.order_modified(order)

    def _add_path_to_root_order(
        self,           # type: OrderRoundRobinBox
        order,          # type: ProductionOrder
        path_index,     # type: int
    ):
        # type: (...) -> None

        def change_order(root_order):
            root_order.round_robin_path = path_index
            return True

        self.order_changer.change_order(order.order_id, change_order)

    class Path(object):
        def __init__(self, index, son):
            self.index = index
            self.son = son
            self.height = 0
            self.orders = {}

        def get_order_list(self):
            return list(self.orders.values())

        def clear_orders(self):
            self.orders = {}

        def __str__(self):
            message = "{}: {{Height: {}, Order Counts: {}}}"
            return message.format(self.index, self.height, len(self.orders))

        def __repr__(self):
            return self.__str__()
