import re
import cPickle as Pickle

from datetime import datetime

from helper import remove_accents
from typing import (
    List,
    Optional,
    Dict,
)
from collections import OrderedDict

from ._CustomStorage import CustomStorage
from ._TagEvent import TagEvent
from ._TagEventType import TagEventType


class Item(CustomStorage):

    """ class Item(CustomStorage)
    Represents an item of a production order.
    Instances of this class can be handled and/or modified by any production-box.
    Use the methods write_data, read_data and delete_data in order to modify instances
    in a safe way (avoiding name conflicts).
    """
    # Note - NEVER use mutable objects as the default value here (they are shared by all instances)
    # Mutable objects MUST be created in the constructor

    line_number = 0             #: Number of the "sale line" that represents this item
    item_id = ""                #: The "context" of this item (n levels)
    part_code = ""              #: The product code (one level)
    item_code = ""              #: "item_id.part_code" (n+1 levels)
    description = ""            #: The product name
    qty = 0                     #: Item quantity
    qty_added = 0               #: Total quantity added of this item
    qty_voided = 0              #: Total quantity voided of this item
    qty_modified = 0            #: Quantity modified (for components only)
    default_qty = 0             #: Default quantity of this item (only for sub-items)
    min_qty = 0                 #: Minimum quantity of this item (only for sub-items)
    max_qty = 0                 #: Maximum quantity of this item (only for sub-items)
    last_order_qty = 0          #: Last quantity of this item
    multiplied_qty = 0          #: Real (multiplied) item quantity (considers parent quantity)
    item_type = ""              #: Indicates if this item is an OPTION, etc
    level = 0                   #: Item level (0, 1, 2, ...)
    pos_level = 0               #: Item level from the complete order in the POS
    modifier_label = None       #: Optional modifier label
    only = ""                   #: Only modifier
    json_tags = ""              #: product tags as json text
    product_priority = 100      #: sorting items
    voided = False              #: If this item, or a parent item, was voided
    last_updated = ''           #: last time this item was changed in the POS
    main_item_description = ''  #: name of level 0 item
    seat = ''                   #: Item seat
    manual_course = ''          #: Item new production course
    items = ()                  #: List of child items
    comments = ()               #: item comments
    properties = ()             #: List of custom properties from the Order Picture
    prep_time = None
    send_moment = None
    tags = {}
    tag_history = ()
    cost = None
    count = 0
    total_count = 0
    measure_unit = ""
    jit_lines = ""
    fire = False
    high_priority = False
    show_removed = False
    original_level = None               # type: Optional[int]
    original_qty = None                 # type: Optional[float]
    sequence = 1000
    fraction = 1
    fraction_parent = None
    grouped_comment = None
    fractionated_by = None
    created_at = None
    custom_params = {}

    def __init__(self, **kargs):
        CustomStorage.__init__(self)
        self.items = list()
        self.comments = dict()
        self.properties = dict()
        self.tags = {}              # type: OrderedDict[str, str]
        self.tag_history = []       # type: List[TagEvent]
        self.joined_items = []      # type: List[Item]
        self.prep_time = None       # type: Optional[int]
        self.send_moment = None     # type: Optional[datetime]
        self.cost = None          # type: Optional[int]
        self.displayed_jit = []     # type: List[str]
        self.jit_lines = ""         # type: str
        self.block_tags = False     # type: bool
        self.show_removed = False   # type: bool
        self.custom_params = {}     # type: Dict

        for key, val in kargs.iteritems():
            setattr(self, key, val)

        ignore_keys = ["fiscalParameters", "sessionId", "plainPrice"]
        self.properties = {key: self.properties[key] for key in self.properties if key not in ignore_keys}

        if not self.item_code:
            self.item_code = "%s.%s" % (self.item_id, self.part_code)

    def __str__(self):
        item_description = " " * int(self.level) + str(self.qty) + " " + self.description.encode("utf-8")
        if self.has_tags():
            item_description += " ["
            for tag in self.tags:
                item_description += tag.encode("utf-8") + ", "
            item_description = item_description[:-2]
            item_description += "]"

        item_description += " [" + self.get_line_id() + "]"
        item_description += " {}".format(self.item_type)
        if self.prep_time is not None:
            item_description += " PrepTime: " + str(self.prep_time)
        if self.send_moment is not None:
            item_description += " SendMoment: " + str(self.send_moment.isoformat())
        if self.cost is not None:
            item_description += " Cost: " + str(self.cost)
        if len(self.joined_items) > 0:
            item_description += "\nJoined Items:"
            for joined_item in self.joined_items:
                item_description += "\n" + str(joined_item)
        for son in self.items:
            item_description += "\n" + str(son)

        return item_description

    def __repr__(self):
        return "(%s) %s" % (object.__repr__(self), str(self))

    def get_tags(self):
        return self.tags.keys()

    def get_sons_tags(self):
        tags = []

        for son in self.items:
            tags.extend(son.get_tags())
            tags.extend(son.get_sons_tags())

        return list(set(tags))

    def has_tags(self):
        return len(self.tags) > 0

    def has_tag(self, tag):
        return tag in self.tags

    def has_some_tag(self, tags):
        for tag in tags:
            if tag in self.tags:
                break
        else:
            return False

        return True

    def does_not_have_tag(self, tag):
        return tag not in self.tags

    def add_view(self, kds_view):
        if 'kds_view' in self.tags.keys() and self.tags['kds_view'] == kds_view:
            return False

        self.tags['kds_view'] = kds_view
        self.tag_history.append(TagEvent(kds_view, TagEventType.added, datetime.now()))
        return True

    def add_tag(self, tag, line_jit_list=None):
        if not line_jit_list:
            self._add_single_tag(tag)
            return

        for jit in line_jit_list:
            jit_tag = tag + "-" + jit.lower()
            self._add_single_tag(jit_tag)

    def _add_single_tag(self, tag):
        if tag not in self.tags:
            self.tags[tag] = tag
            self.tag_history.append(TagEvent(tag, TagEventType.added, datetime.now()))

    def remove_tag(self, tag, line_jit_list=None):
        if not line_jit_list:
            self._remove_single_tag(tag)
            return

        for jit in line_jit_list:
            jit_tag = tag + "-" + jit.lower()
            self._remove_single_tag(jit_tag)

    def _remove_single_tag(self, tag):
        if tag in self.tags:
            del self.tags[tag]
            self.tag_history.append(TagEvent(tag, TagEventType.removed, datetime.now()))

    def toggle_tag(self, tag, line_jit_list=None):
        if not line_jit_list:
            self._toggle_single_tag(tag)
            return [tag]

        jit_tags = []
        for jit in line_jit_list:
            jit_tag = tag + "-" + jit.lower()
            self._toggle_single_tag(jit_tag)
            jit_tags.append(jit_tag)

        return jit_tags

    def _toggle_single_tag(self, tag):
        if tag in self.tags:
            self._remove_single_tag(tag)
        else:
            self._add_single_tag(tag)

    def is_product(self):
        return self.item_type in ("PRODUCT", "CANADD")

    def is_ingredient(self):
        return self.item_type == "INGREDIENT"

    def is_combo(self):
        return self.item_type == "COMBO"

    def is_option(self):
        return self.item_type == "OPTION"

    def has_some_product_child(self):
        for son in self.items:
            if son.is_product() or son.has_some_product_child():
                return True

        return False

    def is_prepared(self):
        return self.get_last_prepared_time() is not None

    def get_last_prepared_time(self):
        if self.is_product():
            part_code_jit = self.jit_lines
            last_jit_sequence = re.split('[><$]', part_code_jit)[-1]

            necessary_jit_lines = re.findall('(\\w+)', last_jit_sequence)
            prepared_jit_tags = self.check_all_tag_in_item(necessary_jit_lines, ["done", "served", "printed"])
            prepared_generic_tags = self.check_all_tag_in_item(None, ["served", "hold"])
            tag_list = [self.get_last_tag_time(prepared_jit_tags), self.get_last_tag_time(prepared_generic_tags)]
            filtered_tag_list = [x for x in tag_list if x is not None]

            return max(filtered_tag_list) if len(filtered_tag_list) > 0 else None

        son_times = []
        for son in self.items:
            if son.qty == 0:
                continue

            son_last_prepared_tag = son.get_last_prepared_time()
            if son_last_prepared_tag is None:
                return None

            son_times.append(son_last_prepared_tag)

        return max(son_times) if son_times else None

    def all_has_at_least_one_tag(self, tags):
        return self.all_has_at_least_one_jit_tag(None, tags)

    def at_least_one_has_at_least_one_tag(self, allowed_tags):
        if self.ignore_removed_item() or not allowed_tags:
            return True

        if self.is_product():
            if not self.tags:
                return True

            has_item = False
            for tag in self.tags:
                if tag in allowed_tags:
                    has_item = True

            return has_item

        for son in self.items:
            if son.at_least_one_has_at_least_one_tag(allowed_tags):
                return True

        return False

    def check_all_tag_in_item(self, necessary_jit_lines, tag_types):
        if necessary_jit_lines:
            tag_types = ["{}-{}".format(x, y.lower()) for x in tag_types for y in necessary_jit_lines]

        return next((x for x in tag_types if x in self.tags), None)

    def all_has_at_least_one_jit_tag(self, necessary_jit_lines, tag_types):
        if self.ignore_removed_item():
            return True

        if self.is_product():
            if self.check_all_tag_in_item(necessary_jit_lines, tag_types) is not None:
                return True

            return False

        for son in self.items:
            if not son.all_has_at_least_one_jit_tag(necessary_jit_lines, tag_types):
                return False

        return True

    def get_last_tag_time(self, tag):
        # type: (str) -> Optional[datetime]

        if self.is_product():
            for event in reversed(self.tag_history):
                if event.tag == tag and event.action == TagEventType.added:
                    return event.date
            return None
        else:
            last_time = None
            for son in self.items:
                son_last_time = son.get_last_tag_time(tag)
                if son_last_time is not None and (last_time is None or son_last_time > last_time):
                    last_time = son_last_time
            return last_time

    def get_last_tag_list_time(self, tag_list):
        time_list = [self.get_last_tag_time(x) for x in tag_list]
        filtered_tag_list = [x for x in time_list if x is not None]

        return max(filtered_tag_list) if len(filtered_tag_list) > 0 else None

    def clone(self):
        return Pickle.loads(Pickle.dumps(self, -1))

    def get_line_id(self):
        part_code = str(self.part_code)
        if hasattr(self, "original_part_code"):
            part_code = str(self.original_part_code)
        level = str(int(self.level))
        if self.original_level is not None:
            level = str(int(self.original_level))

        return str(self.line_number) + "-" + self.item_id + "-" + level + "-" + part_code

    def ignore_removed_item(self):
        return self.qty == 0 and not self.show_removed

    def handle_remove_father(
        self,
        father,                     # type: Item
        inherit_comments=False,     # type: Optional[bool]
    ):
        # type: (...) -> None

        if father is None:
            return

        if inherit_comments and father.comments:
            self.repeat_comment_from_father(father=father)

        if self.original_qty is None:
            self.original_qty = self.qty

        self.seat = father.seat

        self.adjust_level()

        father_level = father.level if father.original_level is None else father.original_level
        if father_level == 0:
            self.qty *= father.qty
            return

        if father.original_qty is not None and father.qty != father.original_qty and father.original_qty != 0:
            self.qty *= father.qty / father.original_qty

    def repeat_comment_from_father(
        self,
        father,     # type: Item
    ):
        # type: (...) -> None

        option_comment = "(C) "
        for key in father.comments:
            comment = remove_accents(father.comments.get(key).text)
            comment_prefix = "" if comment.startswith(option_comment) else option_comment
            father.comments[key].text = "{}{}".format(comment_prefix, comment)
        self.comments.update(father.comments)

    def adjust_level(
        self,   # type: Item
    ):
        # type: (...) -> None

        if self.original_level is None:
            self.original_level = self.level

        self.level -= 1

        for son in self.items:
            son.adjust_level()
