import math

from abc import ABCMeta
from typing import List, Any

from ._OrderChangerProductionBox import OrderChangerProductionBox
from production.model import ProductionOrder, Item


class OrderSplitterBox(OrderChangerProductionBox):
    __metaclass__ = ABCMeta

    def __init__(self, name, sons, order_changer, split_type, split_value, logger=None):
        super(OrderSplitterBox, self).__init__(name, sons, logger)
        self.order_changer = order_changer
        self.split_type = split_type
        self.split_value = split_value

    def change_order(self, order):
        # type: (ProductionOrder) -> List[ProductionOrder]

        if not self._possible_to_split_order(order) or self.split_type is None:
            return [order]

        if self.split_type.upper() == 'PREP_TIME':
            orders = self.split_prep_time(order)
        elif self.split_type.upper() == 'SALE_LINES':
            if self.split_value is None:
                return [order]
            orders = self.split_sale_line(order, float(self.split_value))

        elif self.split_type.upper() in 'FIRE_HOLD_PRIORITY':
            orders = self.split_fire_hold_priority(order)
        else:
            orders = [order]

        return orders

    @staticmethod
    def split_prep_time(order):
        # type: (ProductionOrder) -> List[ProductionOrder]

        orders = []
        split_index = 0

        for item in order.items:
            for temp_order in orders:
                if len(temp_order.items) > 0 and item.prep_time == temp_order.items[0].prep_time:
                    temp_order.items.append(item)
                    break
            else:
                new_order = order.clone([item], split_index)
                orders.append(new_order)
                split_index += 1

        return orders

    def split_sale_line(self, order, limit):
        # type: (ProductionOrder, float) -> List[ProductionOrder]

        temp_items = [x for x in order.items if not self._has_fire_hold_or_priority(x)]
        if len(temp_items) < limit:
            return [order]

        orders = []
        split_index = 0
        box_limit = math.ceil(len(temp_items) / limit)
        items_per_box = math.ceil(len(temp_items) / box_limit)
        for item in temp_items:
            if len(orders) <= split_index:
                new_order = order.clone([], split_index)
                orders.append(new_order)

            orders[split_index].items.append(item)
            if len(orders[split_index].items) >= items_per_box:
                split_index += 1

        orders[0].items.extend([x for x in order.items if self._has_fire_hold_or_priority(x)])
        return orders

    def split_fire_hold_priority(self, order):
        # type: (ProductionOrder) -> List[ProductionOrder]

        if not self._possible_to_split_order(order):
            return [order]

        orders = []
        main_order_items = []
        split_index = 1
        for item in order.items:
            if not self._has_fire_hold_or_priority(item):
                main_order_items.append(item)
                continue

            hold_item = self._get_item_with_property(item.clone(), self._hold_in_item)
            if hold_item:
                new_hold_order = order.clone([hold_item], split_index)
                clear_tag_time = item.get_last_tag_time("clear_hold")
                if clear_tag_time is not None:
                    new_hold_order.display_time = clear_tag_time.strftime("%Y-%m-%dT%H:%M:%S")
                orders.append(new_hold_order)
                split_index += 1

            fire_item = self._get_item_with_property(item.clone(), self._fire_in_item)
            if fire_item:
                new_fire_order = order.clone([fire_item], split_index)
                orders.append(new_fire_order)
                split_index += 1

            priority_item = self._get_item_with_property(item.clone(), self._priority_in_item)
            if priority_item:
                new_priority_order = order.clone([priority_item], split_index)
                orders.append(new_priority_order)
                split_index += 1

            if hold_item or fire_item or priority_item:
                property_list = [self._hold_in_item, self._fire_in_item, self._priority_in_item]
                clear_item = self._get_item_without_properties(item.clone(), property_list)
                if clear_item:
                    main_order_items.append(clear_item)

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

        new_order = order
        if len(orders) > 0:
            new_order = order.clone()
            new_order.order_id = str(order.order_id) + "-0"
        new_order.items = main_order_items
        orders.append(new_order)

        return orders

    def _mark_items_as_split(self, root):
        # type: (Any[ProductionOrder, Item]) -> bool

        need_to_save_order = False
        child_need_to_save_order = False
        for item in root.items:
            need_to_save_order = self._mark_items_as_split(item)

            if "fire" in item.properties and "had_fire" not in item.tags:
                item.add_tag("had_fire")
                need_to_save_order = True

            if "hold" in item.properties and "had_hold" not in item.tags:
                item.add_tag("had_hold")
                need_to_save_order = True

            if "hold" not in item.properties and "had_hold" in item.tags and "clear_hold" not in item.tags:
                item.add_tag("clear_hold")
                need_to_save_order = True

            if "highPriority" in item.properties and "had_high_priority" not in item.tags:
                item.add_tag("had_high_priority")
                need_to_save_order = True

        return need_to_save_order or child_need_to_save_order

    @staticmethod
    def _possible_to_split_order(order):
        lines = 0
        has_combo = False
        for item in order.items:
            lines += 1 if item.qty > 0 else 0
            has_combo = True if item.item_type == 'COMBO' else has_combo
        return lines > 1 or has_combo

    @staticmethod
    def _hold_in_item(item):
        return "hold" in item.properties or "had_hold" in item.tags

    @staticmethod
    def _fire_in_item(item):
        return "fire" in item.properties or "had_fire" in item.tags

    @staticmethod
    def _priority_in_item(item):
        return "highPriority" in item.properties or "had_high_priority" in item.tags

    def _has_fire_hold_or_priority(self, item):
        has_fire = self._fire_in_item(item)
        has_hold = self._hold_in_item(item)
        has_high_priority = self._priority_in_item(item)

        children_has_split = False
        for child in item.items:
            children_has_split = children_has_split or self._has_fire_hold_or_priority(child)
            if children_has_split:
                break

        return has_fire or has_hold or has_high_priority or children_has_split

    def _get_item_with_property(self, item, item_has_property):

        if item_has_property(item):
            return item

        if not item.items:
            return None

        children = []
        for child in item.items:
            tag_child = self._get_item_with_property(child, item_has_property)
            if tag_child:
                children.append(tag_child)
        item.items = children
        return item if children else None

    def _get_item_without_properties(self, item, item_has_properties):
        if any([f(item) for f in item_has_properties]):
            return None

        if not item.items:
            return item

        children = []
        for child in item.items:
            tag_child = self._get_item_without_properties(child, item_has_properties)
            if tag_child:
                children.append(tag_child)
        item.items = children
        return item if children else None
