import json
import time
import printer_text_formatter
import printer_wrapper
import printerconstants
from qrcode import (
    QRCode,
    constants,
)

from typing import (
    Any,
    Dict,
    List,
    Optional,
    Tuple,
    Union,
)

from cStringIO import StringIO
from datetime import datetime
from logging import Logger

import sysactions
from mw_helper import get_modifier_prefixes

from helper import remove_accents
from messagebus import MessageBus
from production.model import (
    Item,
    ProductionOrder,
    InvalidOrderToPrint,
)
from production.util import (
    rollback_printer_default_font,
    REPRINT_CONSTANT,
)
from production.view.printerviewservices import (
    MwOrderStatus,
)

from xml.etree import cElementTree as eTree


small = printerconstants.TXT_FONT_A
medium = printerconstants.TXT_FONT_SELECT_C + printerconstants.TXT_FONT_C
large = printerconstants.TXT_SIZE + chr(printerconstants.TXT_WIDTH[2] + printerconstants.TXT_HEIGHT[2])
x_large = printerconstants.TXT_SIZE + chr(printerconstants.TXT_WIDTH[5] + printerconstants.TXT_HEIGHT[5])

font_sizes = {1: small, 2: medium, 3: large, 4: x_large}
X_LARGE_LINE_SIZE = 8


class PickListReportGenerator:
    def __init__(
        self,
        message_bus,                # type: MessageBus
        configuration_json_path,    # type: str
        picklist_name,              # type: str
        logger                      # type: Logger
    ):
        # type: (...) -> None

        sysactions.mbcontext = message_bus.mbcontext
        self.configuration_json_path = configuration_json_path
        self.logger = logger
        self.font_size = "small"
        self.picklist_font_size_items = None
        self.picklist_name = picklist_name

        self.item_level_separator_stack = []
        self.last_item_level = 0
        self.configuration_json = self._get_configuration_json()
        self.line_sizes_by_font_size = self.configuration_json["lineSize"]
        self.modifiers_prefixes = get_modifier_prefixes()

    def _get_configuration_json(self):
        try:
            with open(self.configuration_json_path) as json_file:
                return json.loads(json_file.read())
        except Exception:  # noqa
            self.logger.exception(
                "Error opening picklist config file: %r",
                self.configuration_json_path,
            )
            with open("../src/new-production/static/default_picklist_config.json") as json_file:
                return json.loads(json_file.read())

    def production_pick_list(
        self,
        production_order,   # type: ProductionOrder
        is_reprint,         # type: bool
    ):
        try:
            if self._order_is_voided(production_order=production_order):
                self.logger.warning("ignoring voided order")
                raise InvalidOrderToPrint()

            if self._order_is_kiosk(pod_type=production_order.pod_type):
                self.logger.warning("ignoring kiosk order")
                raise InvalidOrderToPrint()

            report_header = self._get_report_header(
                production_order=production_order,
                picklist_name=self.picklist_name,
            )
            order_comment = self._generate_comment(
                production_order=production_order,
                key='COMMENT',
            )
            tab_add_info_comment = self._generate_comment(
                production_order=production_order,
                key='TAB_ADD_INFO',
                top_separator=True,
            )

            sale_line_by_seat_dict = self._get_sale_lines_by_seat(production_order)
            report_sale_type = self._generate_report_sale_type(production_order)
            report_footer = self._get_report_footer(
                production_order=production_order,
            )

            return self.generate_report(
                sale_line_by_seat_dict=sale_line_by_seat_dict,
                order_comment=order_comment,
                report_header=report_header,
                report_footer=report_footer,
                report_sale_type=report_sale_type,
                tab_add_info_comment=tab_add_info_comment,
                is_reprint=is_reprint,
            )
        except (Exception,):
            self.logger.exception("Error building PickList for Order {}".format(production_order.order_id))

    def generate_report(
        self,
        sale_line_by_seat_dict,     # type: dict
        order_comment,              # type: str
        report_header,              # type: str
        report_footer,              # type: str
        report_sale_type,           # type: str
        tab_add_info_comment,       # type: str
        is_reprint,                 # type: bool
    ):
        # type: (...) -> str

        report = StringIO()
        seat_separator = self.configuration_json["body"]["separatorBySeat"]
        first_seat = True
        sale_line_counter = 0
        if "cutMainItems" in self.configuration_json and self.configuration_json["cutMainItems"]:
            for seat_text in sorted(sale_line_by_seat_dict):
                list_sale_line_by_level = sale_line_by_seat_dict[seat_text]

                self._write_header(order_comment, report, report_header)
                items = [item for item in list_sale_line_by_level]

                report.write(self.get_report_body(
                    seat_text,
                    items,
                ))

                report.write(tab_add_info_comment)
                report.write(report_sale_type)
                report.write(rollback_printer_default_font())
                report.write("\n\n")

                report.write(report_footer)
                report.write(rollback_printer_default_font())

                sale_line_counter += 1
                if sale_line_counter < len(sale_line_by_seat_dict):
                    if is_reprint:
                        report.write("\n\n{}".format(REPRINT_CONSTANT))

                    report.write("\n\n\n\n\n")
                    report.write(printerconstants.PAPER_FULL_CUT)
                else:
                    if is_reprint:
                        report.write("\n\n{}\n".format(REPRINT_CONSTANT))
        else:
            self._write_header(order_comment, report, report_header)

            for seat_text in sorted(sale_line_by_seat_dict):
                if first_seat:
                    first_seat = False
                else:
                    if seat_separator:
                        report.write(seat_separator * printerconstants.COLS + "\n")
                list_sale_line_by_level = sale_line_by_seat_dict[seat_text]
                report.write(
                    self.get_report_body(
                        sale_lines=list_sale_line_by_level,
                        seat_text=seat_text
                    )
                )
            report.write(tab_add_info_comment)
            report.write(report_sale_type)

            report.write(report_footer)
            if is_reprint:
                report.write("\n\n{}\n".format(REPRINT_CONSTANT))
        return report.getvalue()

    @staticmethod
    def _write_header(order_comment, report, report_header):
        report.write(printerconstants.TXT_ALIGN_LT)
        report.write(printerconstants.TXT_BOLD_ON)
        report.write(report_header)
        report.write(printerconstants.TXT_BOLD_OFF)
        report.write(order_comment)

    def _get_report_header(
        self,
        production_order,   # type: ProductionOrder
        picklist_name       # type: str
    ):
        # type: (...) -> str

        header_pod_type = production_order.pod_type
        header_sale_type = production_order.sale_type
        totem_print = header_pod_type == "TT" or header_sale_type == "TOTEM"
        drive_print = header_pod_type == "DT" or header_sale_type == "DRIVE_THRU"
        delivery_print = header_pod_type == "DL" or header_sale_type == "DELIVERY"
        sale_type_text = self._get_sale_type(
            delivery_print=delivery_print,
            drive_print=drive_print,
            production_order=production_order,
            totem_print=totem_print
        )

        header_config = self.configuration_json.get("header")
        if not header_config:
            return ""

        header_separator = (header_config.get("separator", "") * printerconstants.COLS)
        produced_datetime = ""
        timestamp = self._get_first_printed_time(production_order)
        if timestamp is not None:
            produced_datetime = time.strftime(printerconstants.DATE_TIME_FMT, timestamp.timetuple())

        pos_id = int(production_order.originator[-2:])
        user_info = "{} / {}".format(production_order.operator_id, production_order.operator_name)

        order_id = int(production_order.order_id.split("-")[0]) % 10000
        parent_table_id = None
        table_name = self._get_property_value(
            production_order=production_order,
            key="TAB_ID"
        )
        order_parent_descr = None
        order_parent_descr_id = None
        if table_name is not None:
            order_descr = "Comanda"
            order_descr_id = printer_text_formatter.get_text_ellipsis(
                text_id=order_descr,
                text=table_name,
                padding_space=3
            )
            parent_table_id = self._get_property_value(
                production_order=production_order,
                key="PARENT_ID"
            )
            if parent_table_id is not None:
                order_parent_descr = "Mesa"
                order_parent_descr_id = printer_text_formatter.get_text_ellipsis(
                    text_id=order_parent_descr,
                    text=parent_table_id,
                    padding_space=3
                )
        else:
            table_name = self._get_property_value(
                production_order=production_order,
                key="TABLE_NAME"
            )
            if table_name is not None:
                order_descr = "Mesa"
                order_descr_id = printer_text_formatter.get_text_ellipsis(
                    text_id=order_descr,
                    text=table_name,
                    padding_space=3
                )
            else:
                order_descr = "Pedido"
                order_descr_id = "{}".format(order_id)

        created_at = time.strptime(production_order.created_at, "%Y-%m-%dT%H:%M:%S.%f")
        created_at = time.strftime(printerconstants.DATE_TIME_FMT, created_at)

        header = self._get_free_label_text(header_config)

        header += font_sizes[header_config["fontSize"]]
        line_size = self.line_sizes_by_font_size.get(str(header_config["fontSize"]), 42)
        if header_config["blackBackground"]:
            header += printerconstants.TXT_INVERT_ON

        if parent_table_id:
            header += printer_text_formatter.center_text(
                text="{}: {}".format(order_parent_descr, order_parent_descr_id),
                line_size=line_size
            )
            header += "\n"

        is_customer_name_used = False
        customer_name = self._get_property_value(
            production_order=production_order,
            key="CUSTOMER_NAME",
        )

        is_pager_number_used = False
        pager_number = self._get_property_value(
            production_order=production_order,
            key="PAGER_NUMBER",
        )

        order_partner_id = self._get_property_value(
            production_order=production_order,
            key="SHORT_REFERENCE",
        )

        header += self._get_reference_number(
            customer_name=customer_name,
            header_config=header_config,
            order_descr=order_descr,
            order_descr_id=order_descr_id,
            pager_number=pager_number,
            order_partner_id=order_partner_id,
        )

        header += self._get_origin_order(
            header_config=header_config,
            production_order=production_order,
        )

        header += self._get_delivery_brand(
            header_config=header_config,
            production_order=production_order,
        )

        header += "\n{}".format(header_separator)

        header += "\n{}".format(self._get_formatted_kds_title(
            picklist_name,
            sale_type_text,
        ))

        header += font_sizes[header_config["fontSize"]]
        if header_config["enabledLabels"]["orderNumber"]:
            header += "\nCodigo........: %s" % order_id

        if header_config["enabledLabels"]["posNumber"]:
            header += "\nPOS...........: %s" % str(pos_id).zfill(2)

        if header_config.get("enabledLabels", {}).get("partnerId", None):
            header += self._print_delivery_partner(
                delivery_print=delivery_print,
                production_order=production_order,
            )

        if pager_number and not is_pager_number_used:
            header += "\nPager.........: %s" % pager_number

        if customer_name and not is_customer_name_used:
            header += "\nCliente.......: %s" % customer_name

        current_datetime = time.strftime(printerconstants.DATE_TIME_FMT, time.localtime())
        if header_config["enabledLabels"]["orderDatetime"]:
            if produced_datetime:
                header += "\nHora Pedido...: %s" % created_at
            else:
                header += "\nImpresso em...: {}".format(current_datetime)

        if header_config["enabledLabels"]["productionDatetime"] and produced_datetime:
            header += "\nHora Produzido: %s" % produced_datetime

        if header_config["enabledLabels"]["orderType"]:
            pos_id = int(production_order.originator[-2:])
            model = sysactions.get_model(posid=pos_id)
            order_type_translation = sysactions.translate_message(
                model=model,
                message_key=production_order.order_type,
            )
            header += "\nTipo de Pedido: %s" % order_type_translation or production_order.order_type

        if header_config["enabledLabels"]["operator"]:
            header += "\nOperador......: %s" % user_info

        header += "\n{}".format(header_separator)

        if header_config.get("printAddress"):
            header += self._print_address(
                delivery_print=delivery_print,
                production_order=production_order,
                separator=header_separator
            )

        header += rollback_printer_default_font()

        return header

    def _get_origin_order(
        self,
        header_config,      # type: dict
        production_order,   # type: ProductionOrder
    ):
        # type: (...) -> str

        has_order_origin = self.has_property_in_config(config=header_config, property_name="orderOrigin")
        if not has_order_origin:
            return ""

        model = sysactions.get_model(posid=0)
        order_reference = sysactions.translate_message(
            model=model,
            message_key=production_order.sale_type,
        )

        if production_order.sale_type == "EAT_IN":
            company_id = sysactions.get_storewide_config(
                parameter="Store.CompanyId",
                defval="",
            )

            store_id = sysactions.get_storewide_config(
                parameter="Store.Id",
                defval="",
            )
            order_reference = "{}: {} {}".format(order_reference, company_id, store_id)
        elif production_order.sale_type == "DELIVERY":
            order_partner = self._get_property_value(
                production_order=production_order,
                key="PARTNER",
            )
            order_reference = "{} {}".format(order_reference, order_partner)

        order_reference = self._get_formatted_text(
            text=order_reference,
            style_config=header_config["orderOrigin"],
        )

        header = order_reference + "\n"
        header += rollback_printer_default_font()
        return header

    def _get_delivery_brand(
        self,               # type: PickListReportGenerator
        header_config,      # type: dict
        production_order,   # type: ProductionOrder
    ):
        # type: (...) -> str

        has_delivery_brand = self.has_property_in_config(config=header_config, property_name="useDeliveryBrand")
        if not has_delivery_brand:
            return ""

        delivery_brand = self._get_property_value(
            production_order=production_order,
            key="BRAND",
        )

        if delivery_brand in [None, '']:
            return ""

        delivery_brand = self._get_formatted_text(
            text=delivery_brand,
            style_config=header_config.get("deliveryBrand", ""),
        )

        header = delivery_brand + "\n"
        header += rollback_printer_default_font()
        return header

    def _get_reference_number(
        self,               # type: PickListReportGenerator
        customer_name,      # type: str
        header_config,      # type: dict
        order_descr,        # type: str
        order_descr_id,     # type: str
        pager_number,       # type: str
        order_partner_id,   # type: str
    ):
        # type: (...) -> str

        enabled_labels = header_config.get("enabledLabels", {})
        has_reference_number = self.has_property_in_config(config=enabled_labels, property_name="referenceNumber")
        if not has_reference_number:
            return ""

        reference_number_text = "%s: %s" % (order_descr, order_descr_id)

        if header_config.get("usePartnerReferenceNumber") and order_partner_id not in [None, '']:
            reference_number_text = order_partner_id
        elif header_config.get("usePagerAsReferenceNumber") and pager_number:
            reference_number_text = "%s: %s" % ("Pager", pager_number)
        elif header_config.get("useCustomerNameAsReferenceNumber") and customer_name:
            reference_number_text = customer_name

        header = self._get_formatted_reference_number(text=reference_number_text) + "\n"
        header += rollback_printer_default_font()
        return header

    def _get_report_footer(
        self,
        production_order,    # type: ProductionOrder
    ):
        # type: (...) -> str

        footer_config = self.configuration_json.get("footer")
        if not footer_config:
            return ""

        footer = "\n\n" + self._get_free_label_text(json_config=footer_config)

        show_qr_code = self.has_property_in_config(config=footer_config, property_name="showQrCode")
        if show_qr_code:
            footer += self._get_qr_code(production_order=production_order)

        show_debug_line = self.has_property_in_config(config=footer_config, property_name="showDebugLine")
        if show_debug_line:
            footer += self._get_debug_line(
                production_order=production_order
            )

        return footer

    def _get_debug_line(
        self,
        production_order,   # type: ProductionOrder
    ):
        # type: (...) -> str

        table_or_tab_name = self._get_table_tab_name(production_order=production_order)
        debug_line = "{}|{}|{}|{}|{}".format(
            production_order.order_id,
            production_order.pos_id,
            datetime.now().strftime("%d%m%Y%H%M%S"),
            production_order.operator_id,
            table_or_tab_name,
        )
        debug_line_text = printer_text_formatter.center_text(
            text=debug_line,
        )
        debug_line_text += rollback_printer_default_font()
        return debug_line_text

    def _get_free_label_text(self, json_config):
        free_label = json_config.get("freeLabel")
        if not free_label:
            return ""

        footer = ""
        if free_label.get("text", "") != "":
            if free_label["blackBackground"]:
                footer += printerconstants.TXT_INVERT_ON

            if free_label["bold"]:
                footer += printerconstants.TXT_BOLD_ON

            footer += font_sizes[free_label["fontSize"]]
            footer += printer_text_formatter.center_text(
                text=free_label["text"].encode("cp860"),
            ) + "\n"
            footer += rollback_printer_default_font()

        return footer

    def get_report_body(
        self,
        seat_text,  # type: str
        sale_lines  # type: list
    ):
        # type: (...) -> str

        body_config = self.configuration_json["body"]
        body = font_sizes[body_config.get("fontSize", 1)]
        body += "\n"
        body += seat_text

        sale_line_separator_char = body_config.get("saleLineSeparator")
        sale_line_separator = ""
        if sale_line_separator_char and len(sale_lines) > 1:
            sale_line_separator = sale_line_separator_char * printerconstants.COLS + "\n"

        for index, product_sale_line in enumerate(sale_lines):
            if index > 0:
                body += sale_line_separator

            if isinstance(product_sale_line, list):
                for sale_line in product_sale_line:
                    if sale_line is None:
                        self.logger.warning("Saleline is None")
                        continue
                    body += sale_line
            else:
                body += product_sale_line

        body += rollback_printer_default_font()
        return body

    def _generate_comment(
        self,
        production_order,       # type: ProductionOrder
        key,                    # type: str
        top_separator=False     # type: Optional[bool]
    ):
        # type: (...) -> str

        report = StringIO()
        comment = self._get_property_value(
            production_order=production_order,
            key=key,
        )
        if comment:
            if top_separator:
                report.write("\n{}".format(printerconstants.SEPARATOR))

            required_lines = len(comment) / printerconstants.COLS + 1
            report.write("\n")
            for line in range(required_lines):
                begin = line * printerconstants.COLS
                end = (line + 1) * printerconstants.COLS
                report.write("\n%s" % comment[begin:end])

            report.write("\n\n{}".format(printerconstants.SEPARATOR))

        return report.getvalue()

    @staticmethod
    def _get_first_printed_time(
        production_order    # type: ProductionOrder
    ):
        # type: (...) -> Optional[datetime]

        for item in production_order.items:
            for tag_history in item.tag_history:
                if tag_history.tag == "served":
                    return tag_history.date
        return None

    def _get_sale_lines_by_seat(
        self,
        production_order        # type: ProductionOrder
    ):
        # type: (...) -> Dict

        grouped_sale_lines = self._get_grouped_sale_lines_by_seat(production_order=production_order)
        if len(grouped_sale_lines) == 0:
            self.logger.warning("Order {} has no sale lines to print".format(production_order.order_id))
            raise Exception()

        return grouped_sale_lines

    def _get_formatted_sale_line(
        self,
        model,                              # type:
        item,                               # type: Item
        pre_option_level=None,              # type: Optional[int]
        preceding_removed_combo=False,      # type: Optional[bool]
        cols=printerconstants.COLS,         # type: Optional[int]
    ):
        # type: (...) -> Tuple[Optional[str], Optional[int], bool]

        item_type = item.item_type

        level = int(item.level)
        level_text = self._get_level_text(level)

        tabulation = self.configuration_json["body"]["layoutByLevel"][level_text]["tabulation"]
        font_size = font_sizes[self.configuration_json["body"]["layoutByLevel"][level_text]["fontSize"]]
        bold = self.configuration_json["body"]["layoutByLevel"][level_text]["bold"]
        black_background = self.configuration_json["body"]["layoutByLevel"][level_text]["blackBackground"]
        highlight_non_production_items = self.configuration_json["body"].get("highlightNonProductionItems", False)

        if item_type == "COMBO":
            preceding_removed_combo = False

        if level == 0:
            pre_option_level = None

        if item_type in [None, "OPTION", "COUPON"]:
            pre_option_level = level if item_type == "OPTION" else None
            return None, pre_option_level, preceding_removed_combo

        include_item_level_separator = False
        level_separator = self.configuration_json["body"]["layoutByLevel"][level_text]["separator"]

        if level_separator != '':
            if level_separator not in self.item_level_separator_stack:
                self.item_level_separator_stack.append(level_separator)

            include_item_level_separator = True

        qty = float(item.qty or 0)
        multiplied_qty = float(item.multiplied_qty or 0)
        default_qty = float(item.default_qty or 0)

        if item_type in ("INGREDIENT", "CANADD", "PRODUCT") and multiplied_qty > 0:
            qty = multiplied_qty

        if (qty == 0 and default_qty == 0) or preceding_removed_combo:
            if item_type == "COMBO":
                preceding_removed_combo = True

            return None, None, preceding_removed_combo

        qty_description = ""
        if qty < default_qty and item.item_type == 'INGREDIENT':
            qty_description = "{} ".format(sysactions.translate_message(
                model=model,
                message_key=self.modifiers_prefixes['[Without]'].replace("$", ""),
            ))

        fraction_parent = item.fraction_parent
        if qty > default_qty and item.level > 0 and item.item_type == 'INGREDIENT':
            qty_description = "{} ".format(sysactions.translate_message(
                model=model,
                message_key=self.modifiers_prefixes['[Additional]'].replace("$", ""),
            ))

        if qty != 0:
            measure_unit = (item.measure_unit or "UN").upper()
            if fraction_parent:
                numerator = int(round(abs(multiplied_qty - default_qty) * fraction_parent))
                formatted_qty = "{}/{}".format(numerator, fraction_parent)
                qty_description += formatted_qty if numerator != fraction_parent else "1".zfill(2)
            else:
                decimal_places = 3 if measure_unit == "KG" else 0
                quantity_format = "{{:.{}f}}".format(decimal_places)
                qty_description = quantity_format.format(qty)
                qty_description = qty_description.zfill(2) if decimal_places == 0 else qty_description

        indentation = tabulation * ((pre_option_level or level) + 1)
        product_name = item.description
        manual_course = item.manual_course
        if manual_course and level == 0:
            manual_course = "[" + manual_course + "]"

        formatted_sale_line = font_size
        if bold:
            formatted_sale_line += printerconstants.TXT_BOLD_ON
        if black_background:
            formatted_sale_line += printerconstants.TXT_INVERT_ON

        if highlight_non_production_items and not self.item_has_jit_line(item=item):
            formatted_sale_line += printerconstants.TXT_INVERT_ON

        description_length = cols - len(manual_course)
        if len(item.comments) > 0:
            for comment in item.comments.values():
                comment_description = ""
                jumped_comment = ""
                if comment.text:
                    if comment.text.startswith("["):
                        comment_description = self._get_configured_comment_description(
                            model=model,
                            comment=comment.text,
                        )
                    else:
                        description = "* " + comment.text
                        indentation_comment_size = len(indentation) + int(round(qty, 1))
                        jumped_comment = self._split_comment_in_lines(
                            comment=description,
                            indentation_size=indentation_comment_size,
                            cols=cols
                        )

                product_description = str(qty_description) + " " + product_name
                if str(comment_description):
                    product_description = str(comment_description) + " " + product_description

                line_rjust = len(product_description) + len(indentation)

                product_description = product_description.rjust(line_rjust)
                if manual_course:
                    product_description = product_description.ljust(description_length)[:description_length]

                description = remove_accents(text=product_description + manual_course)

                formatted_sale_line += description + "\n"
                if jumped_comment:
                    formatted_sale_line += jumped_comment + "\n"
        else:
            product_description = indentation + qty_description + " " + product_name
            if manual_course:
                product_description = product_description.ljust(description_length)[:description_length]
            formatted_sale_line += remove_accents(text=product_description + manual_course) + "\n"

        if include_item_level_separator:
            formatted_sale_line += rollback_printer_default_font()
            formatted_sale_line += level_separator * cols + "\n"

        if level <= pre_option_level:
            pre_option_level = None
        self.last_item_level = level
        formatted_sale_line = self._clean_rollback_default_font(formatted_sale_line)
        return formatted_sale_line, pre_option_level, preceding_removed_combo

    def _get_configured_comment_description(
        self,       # type: PickListReportGenerator
        model,      # type: eTree.Element
        comment,    # type: str
    ):
        # type: (...) -> str

        comment_description = self.modifiers_prefixes.get(comment, comment)
        if "[" in comment_description or "$" in comment_description:
            comment_description = comment_description.replace("[", "").replace("]", "").replace("$", "").upper()
            comment_description = sysactions.translate_message(
                model=model,
                message_key=str(comment_description),
            )
        return comment_description

    @staticmethod
    def _clean_rollback_default_font(
        content,  # type: str
    ):
        # type: (...) -> str

        sequence = rollback_printer_default_font()
        while True:
            if content.endswith("\n"):
                content = content.rstrip("\n")
            elif content.endswith(sequence):
                content = content.rstrip(sequence)
            else:
                break
        return "{}{}\n".format(content, sequence)

    def _get_level_text(self, level):
        current_level = level
        level_text = "level" + str(current_level)
        while level_text not in self.configuration_json["body"]["layoutByLevel"]:
            current_level = current_level - 1
            level_text = "level" + str(current_level)

        return level_text

    def _get_formatted_kds_title(
        self,            # type: PickListReportGenerator
        pick_list_name,  # type: str
        sale_type_text,  # type: str
    ):
        # type: (...) -> str

        header_config = self.configuration_json.get("header", {})
        kds_title_config = header_config.get("kdsTitle", {})
        text = "{}{}".format(pick_list_name, sale_type_text)

        if not kds_title_config:
            return printer_text_formatter.center_text(text=text)

        kds_title = self._get_formatted_text(
            text=text,
            style_config=kds_title_config,
        )
        return kds_title

    def _get_formatted_reference_number(
        self,
        text,  # type: str
    ):
        # type: (...) -> str

        header_config = self.configuration_json.get("header", {})
        reference_number_config = header_config.get("referenceNumber", {})
        reference_number = self._get_formatted_text(
            text=text,
            style_config=reference_number_config,
        )
        return reference_number

    def _get_formatted_text(
        self,           # type: PickListReportGenerator
        text,           # type: str
        style_config,   # type: Dict
    ):
        # type: (...) -> str

        parts = []
        is_rollback_needed = False

        if style_config:
            line_size = printerconstants.COLS
            if style_config.get("blackBackground"):
                parts.append(printerconstants.TXT_INVERT_ON)

            parts.append(printerconstants.TXT_BOLD_OFF)
            if style_config.get("bold"):
                parts.append(printerconstants.TXT_BOLD_ON)

            font_size = style_config.get("fontSize")
            if font_size:
                line_size = self.line_sizes_by_font_size.get(str(font_size), printerconstants.COLS)
                parts.append(font_sizes[font_size])
            is_rollback_needed = bool(parts)

            centered = style_config.get("centered")
            if centered:
                text = printer_text_formatter.center_text(
                    text=text,
                    line_size=line_size,
                )

                if line_size == X_LARGE_LINE_SIZE:
                    parts.append(printerconstants.TXT_ALIGN_CT)

        parts.append(text)

        if is_rollback_needed:
            parts.append(rollback_printer_default_font())

        return "".join(parts)

    @staticmethod
    def _split_comment_in_lines(
        comment,            # type: str
        indentation_size,   # type: int
        cols                # type: int
    ):
        # type: (...) -> str

        line_rjust = len(comment) + indentation_size
        if line_rjust > cols:
            comment = remove_accents(text=comment)
            remaining_size = cols - indentation_size
            lines = [comment[i: i + remaining_size] for i in range(0, len(comment), remaining_size)]
            comment = "\n".join([(" " * indentation_size) + line for line in lines])
        else:
            comment = remove_accents(text=comment.rjust(line_rjust))

        return comment

    @staticmethod
    def _get_title_sale_lines_seats(
        seat,           # type: str
        table_seats,    # type: str
    ):
        # type: (...) -> str

        seat_title = "Assento {}"

        if seat != "":
            title = seat_title.format(seat)
        else:
            title = "Mesa ({} assentos)".format(table_seats) if table_seats else "Sem Assento"

        return "{}\n".format(title)

    def _generate_report_sale_type(
        self,
        production_order    # type: ProductionOrder
    ):
        # type: (...) -> str

        report = StringIO()
        sale_type = ""
        if production_order.pod_type == "DT":
            sale_type = "DRIVE-THRU"
        else:
            if production_order.sale_type != 'EAT_IN':
                sale_type = production_order.sale_type.replace('_', ' ').upper()
                if sale_type == "APP":
                    pickup_type = self._get_property_value(
                        production_order=production_order,
                        key="PICKUP_TYPE"
                    )

                    if pickup_type.upper() == "TAKE_OUT":
                        sale_type = "VIAGEM"

                    if pickup_type.upper() == "EAT_IN":
                        sale_type = ""
                if "TAKE" in sale_type:
                    sale_type = "VIAGEM"

        if sale_type != "":
            sale_type_text = printer_text_formatter.center_text(
                text=sale_type,
                sep='*',
                line_size=printerconstants.COLS / 2,
            )
            sale_type_text = printerconstants.TXT_4SQUARE + sale_type_text + printerconstants.TXT_NORMAL
            report.write("\n%s" % sale_type_text)

        return report.getvalue()

    @staticmethod
    def _create_qrcode(
        qr_data,        # type: str
    ):
        # type: (...) -> str

        qr = QRCode(
            version=2,
            error_correction=constants.ERROR_CORRECT_H,
            box_size=10,
            border=4,
        )

        qr.add_data(qr_data)
        qr.make(fit=True)

        img = qr.make_image()
        img = img.convert("RGB")
        img = printer_wrapper.image(img)

        return img.strip()

    def _print_delivery_partner(
        self,
        delivery_print,     # type: str
        production_order    # type: ProductionOrder
    ):
        # type: (...) -> str

        text = ""
        order_partner = self._get_property_value(
            production_order=production_order,
            key="PARTNER"
        )
        order_partner_id = self._get_property_value(
            production_order=production_order,
            key="SHORT_REFERENCE"
        )
        if delivery_print and order_partner and order_partner != "" and order_partner_id != "":
            text += """\nParceiro......: {0}\nID Parceiro...: {1}""".format(order_partner.upper(), order_partner_id)

        return text

    def _print_address(
        self,
        delivery_print,     # type: bool
        production_order,   # type: ProductionOrder
        separator,          # type: str
    ):
        # type: (...) -> str
        text = ""
        valid_sale_type = production_order.sale_type.upper() not in ["TAKE_OUT", "EAT_IN"]
        if not delivery_print or not valid_sale_type:
            return text

        try:
            street_name_element = self._get_property_value(
                production_order=production_order,
                key="STREET_NAME",
            )
            street_number_element = self._get_property_value(
                production_order=production_order,
                key="STREET_NUMBER",
            )
            if street_name_element:
                text += """\nEndereco......: {0}, {1}\n""".format(street_name_element, street_number_element)
            default_format = """{}: {}\n"""
            text += default_format.format(
                "Complemento...",
                self._get_property_value(
                    production_order=production_order,
                    key="COMPLEMENT",
                )
            )
            reference = self._get_property_value(
                production_order=production_order,
                key="REFERENCE",
            )
            if reference:
                text += default_format.format(
                    "Referencia....",
                    reference,
                )
            text += default_format.format(
                "Bairro........",
                self._get_property_value(
                    production_order=production_order,
                    key="NEIGHBORHOOD",
                )
            )
            text += default_format.format(
                "CEP...........",
                self._get_property_value(
                    production_order=production_order,
                    key="POSTAL_CODE",
                )
            )
            text += default_format.format(
                "Cidade........",
                self._get_property_value(
                    production_order=production_order,
                    key="CITY",
                )
            )
            state = self._get_property_value(
                production_order=production_order,
                key="STATE",
            )

            if state not in ['', None]:
                text += default_format.format(
                    "Estado........",
                    state,
                )

            if text != "":
                text += separator

        except (Exception,):
            self.logger.exception("Error in get address on Delivery picklist")

        return text

    def _get_picklist_font_size_items(self):
        # type: () -> str

        if self.picklist_font_size_items is None:
            try:
                sizes = {"small": small, "medium": medium, "large": large}
                font_size_items = self.font_size
                self.picklist_font_size_items = sizes[font_size_items]
            except (Exception,):
                self.picklist_font_size_items = small

        return self.picklist_font_size_items

    def _get_grouped_sale_lines_by_seat(
        self,
        production_order    # type: ProductionOrder
    ):
        # type: (...) -> Dict

        grouped_sale_lines = {}
        pre_option_level = None
        preceding_removed_combo = False
        self.generate_sale_line_of_all_items(
            production_order=production_order,
            grouped_sale_lines=grouped_sale_lines,
            pre_option_level=pre_option_level,
            preceding_removed_combo=preceding_removed_combo,
            items=production_order.items
        )
        return grouped_sale_lines

    def generate_sale_line_of_all_items(
        self,                       # type: PickListReportGenerator
        production_order,           # type: ProductionOrder
        grouped_sale_lines,         # type: Dict
        pre_option_level,           # type: Optional[int]
        preceding_removed_combo,    # type: bool
        items                       # type: List
    ):
        # type: (...) -> Dict

        comments_by_sale_line = self._comments_by_sale_line(items=items)
        items = self._create_grouped_comments(
            items=items,
            comments_by_sale_line=comments_by_sale_line,
        )
        items_by_seat_and_sale_line = self._items_by_seat_and_sale_line(
            items=items,
            production_order=production_order,
        )

        for seat, items_by_sale_line in items_by_seat_and_sale_line.items():
            grouped_sale_lines[seat] = []
            for sale_line, items in items_by_sale_line.items():
                if items[0].grouped_comment:
                    grouped_sale_lines[seat].extend(self._get_formatted_line_with_surrounded_grouped_comment(
                        items=items,
                        preceding_removed_combo=preceding_removed_combo,
                        production_order=production_order,
                        pre_option_level=pre_option_level,
                    ))
                else:
                    grouped_sale_lines[seat].append(self._get_formatted_items_to_print(
                        items=items,
                        preceding_removed_combo=preceding_removed_combo,
                        production_order=production_order,
                        pre_option_level=pre_option_level,
                    ))

                if seat is not None:
                    self._flush_pending_item_level_separators(
                        seat_text=seat,
                        grouped_sale_lines=grouped_sale_lines,
                    )

                self.item_level_separator_stack[:] = []
                self.last_item_level = 0

        return grouped_sale_lines

    def _flush_pending_item_level_separators(
        self,
        seat_text,           # type: str
        grouped_sale_lines,  # type: Dict[str, List[List[Optional[str]]]]
    ):
        # type: (...) -> None

        if self.item_level_separator_stack:
            item_sale_lines = grouped_sale_lines[seat_text]
            last_item_sale_lines = self._get_last_non_none_sublist(item_sale_lines)
            if last_item_sale_lines:
                sale_line_buf = last_item_sale_lines[-1] or ""
                self.item_level_separator_stack.reverse()
                for sep in self.item_level_separator_stack:
                    sale_line_buf += sep * printerconstants.COLS + "\n"
                last_item_sale_lines[-1] = sale_line_buf

    def generate_sale_line_of_all_items_sons(
        self,
        model,                      # type: object
        production_order,           # type: ProductionOrder
        item_sale_lines,            # type: List[str]
        pre_option_level,           # type: int
        preceding_removed_combo,    # type: bool
        items                       # type: List[Item]
    ):
        # type: (...) -> None

        for item in items:
            if item.is_ingredient() and item.default_qty == item.qty and not item.comments:
                continue

            self.generate_sale_line_by_item(
                model=model,
                grouped_sale_lines=item_sale_lines,
                item=item,
                pre_option_level=pre_option_level,
                preceding_removed_combo=preceding_removed_combo
            )
            if len(item.items) > 0:
                self.generate_sale_line_of_all_items_sons(
                    model=model,
                    production_order=production_order,
                    item_sale_lines=item_sale_lines,
                    pre_option_level=pre_option_level,
                    preceding_removed_combo=preceding_removed_combo,
                    items=item.items
                )

    def generate_sale_line_by_item(
        self,
        model,                      # type: object
        grouped_sale_lines,         # type: List[str]
        item,                       # type: Item
        pre_option_level,           # type: int
        preceding_removed_combo     # type: bool
    ):
        # type: (...) -> None

        sale_line, pre_option_level, preceding_removed_combo = self._get_formatted_sale_line(
            model=model,
            item=item,
            pre_option_level=pre_option_level,
            preceding_removed_combo=preceding_removed_combo,
        )
        grouped_sale_lines.append(sale_line)

    @staticmethod
    def _order_is_voided(
        production_order    # type: ProductionOrder
    ):
        # type: (...) -> bool

        return MwOrderStatus.VOIDED.name == production_order.state

    @staticmethod
    def _join(
        dic1,   # type: Dict
        dic2    # type: Dict
    ):
        # type: (...) -> Dict

        d = dict(dic1)
        d.update(dic2)
        return d

    def _get_sale_type(
        self,
        delivery_print,     # type: bool
        drive_print,        # type: bool
        production_order,   # type: ProductionOrder
        totem_print         # type: bool
    ):
        # type: (...) -> str

        if totem_print:
            return "(TOTEM)"
        if drive_print:
            return "(DRIVE-THRU)"
        if delivery_print:
            sale_type = self._get_sale_type_by_pickup_type(production_order=production_order)
            return sale_type

        sale_type = self._get_by_sale_type(order=production_order)
        return sale_type

    def _get_sale_type_by_pickup_type(
        self,
        production_order,   # type: ProductionOrder
    ):
        # type: (...) -> str

        sale_type = " (DELIVERY)"
        pickup_type = self._get_property_value(
            production_order=production_order,
            key="PICKUP_TYPE"
        )
        if pickup_type == "TAKE_OUT":
            sale_type = " (DELIVERY - RETIRADA VIAGEM)"
        elif pickup_type == "EAT_IN":
            sale_type = " (DELIVERY - RETIRADA LOJA)"

        return sale_type

    @staticmethod
    def _get_by_sale_type(
        order   # type: ProductionOrder
    ):
        # type: (...) -> str

        if order.sale_type == "TAKE_OUT":
            return " (VIAGEM)"

        return " (LOJA)"

    @staticmethod
    def _get_property_value(
        production_order,   # type: ProductionOrder
        key                 # type: str
    ):
        # type: (...) -> Any

        if key in production_order.properties:
            return remove_accents(production_order.properties[key])
        return None

    @staticmethod
    def _get_last_non_none_sublist(
        main_list,  # type: List[Union[List[str], None]]
    ):
        # type: (...) -> Optional[List]

        for sublist in list(reversed(main_list)):
            if sublist is not None:
                return sublist

        return None

    def _get_qr_code(
        self,               # type: PickListReportGenerator
        production_order,   # type: ProductionOrder
    ):
        # type: (...) -> str

        qr_code_data = "QuickService|{}".format(
            production_order.order_id,
        )
        if production_order.sale_type == "DELIVERY":
            partner = self._get_property_value(
                production_order=production_order,
                key="PARTNER",
            )
            short_reference = self._get_property_value(
                production_order=production_order,
                key="SHORT_REFERENCE",
            )

            delivery_brand = self._get_property_value(
                production_order=production_order,
                key="BRAND",
            )
            delivery_brand = delivery_brand if delivery_brand is not None else ""

            originator = self._get_property_value(
                production_order=production_order,
                key="ORIGINATOR",
            )
            originator = originator.split('|')[0] if originator is not None else ""

            qr_code_data = "{}|{}|{}|{}|{}".format(
                production_order.order_id,
                partner,
                short_reference,
                delivery_brand,
                originator
            )

        qr_code = printerconstants.TXT_ALIGN_CT
        qr_code += self._create_qrcode(qr_data=qr_code_data)
        qr_code += rollback_printer_default_font()
        return qr_code

    def _get_table_tab_name(
        self,
        production_order,   # type: ProductionOrder
    ):
        # type: (...) -> str

        tab_id = self._get_property_value(
            production_order=production_order,
            key="TAB_ID",
        )
        if tab_id is not None:
            return "C{}".format(tab_id)

        table_or_tab_name = ""
        table_name = self._get_property_value(
            production_order=production_order,
            key="TABLE_NAME",
        )
        if table_name is not None:
            table_or_tab_name = "M{}".format(table_name)

        return table_or_tab_name

    @staticmethod
    def item_has_jit_line(
        item,  # type: Item
    ):
        # type: (...) -> bool

        return item.jit_lines != ""

    @staticmethod
    def has_property_in_config(
        config,         # type: Dict
        property_name,  # type: str
    ):
        # type: (...) -> bool

        return config.get(property_name, False)

    @staticmethod
    def _comments_by_sale_line(
        items,  # type: List
    ):
        # type: (...) -> Dict

        comments_by_sale_line = {}
        for item in items:
            line_number = item.line_number
            if line_number not in comments_by_sale_line:
                comments_by_sale_line.update({
                    line_number: {
                        "items_qty": 1,
                    }
                })
            else:
                comments_by_sale_line[line_number]["items_qty"] += 1

            for comment in item.comments.values():
                if comment.text not in comments_by_sale_line[line_number]:
                    comments_by_sale_line[line_number].update({
                        comment.text: {
                            "comment_qty": 1,
                        }
                    })
                else:
                    comments_by_sale_line[line_number][comment.text]["comment_qty"] += 1

        return comments_by_sale_line

    @staticmethod
    def _create_grouped_comments(
        items,                  # type: List
        comments_by_sale_line,  # type: Dict
    ):
        # type: (...) -> List

        for item in items:
            line_number = item.line_number
            for comment_id, comment in item.comments.items():
                total_items_in_line_number = comments_by_sale_line[line_number]["items_qty"]
                if total_items_in_line_number == 1:
                    continue

                if comments_by_sale_line[line_number][comment.text]["comment_qty"] == total_items_in_line_number:
                    del item.comments[comment_id]
                    item.grouped_comment = remove_accents(comment.text)

        return items

    def get_seat_key(
        self,              # type: PickListReportGenerator
        item,              # type: Item
        production_order,  # type: ProductionOrder
    ):
        seat_key = ""
        should_separate_by_seat = self.configuration_json["body"]["separateBySeat"]
        if should_separate_by_seat:
            table_id = self._get_property_value(
                production_order=production_order,
                key="TABLE_ID",
            )
            if table_id is not None:
                table_seats = self._get_property_value(
                    production_order=production_order,
                    key="TABLE_SEATS",
                )
                seat_key = self._get_title_sale_lines_seats(
                    seat=item.seat,
                    table_seats=table_seats,
                )
        return seat_key

    def _items_by_seat_and_sale_line(
        self,              # type: PickListReportGenerator
        items,             # type: List
        production_order,  # type: ProductionOrder
    ):
        # type: (...) -> Dict

        items_by_seat_and_sale_line = {}
        for item in items:
            seat_key = self.get_seat_key(
                item=item,
                production_order=production_order,
            )
            if seat_key not in items_by_seat_and_sale_line:
                items_by_seat_and_sale_line[seat_key] = {}

            sale_line = item.line_number
            if sale_line not in items_by_seat_and_sale_line[seat_key]:
                items_by_seat_and_sale_line[seat_key][sale_line] = [item]
            else:
                items_by_seat_and_sale_line[seat_key][sale_line].append(item)

        return items_by_seat_and_sale_line

    def _get_formatted_line_with_surrounded_grouped_comment(
        self,                       # type: PickListReportGenerator
        items,                      # type: List
        preceding_removed_combo,    # type: bool
        production_order,           # type: ProductionOrder
        pre_option_level,           # type: Optional[int]
    ):
        # type: (...) -> List

        formatted_lines_to_print = []
        grouped_comment_separator = "{}{}".format("=" * printerconstants.COLS, "\n")
        formatted_lines_to_print.append([grouped_comment_separator])

        grouped_comment_text = "{}{}".format(items[0].grouped_comment, "\n")
        formatted_lines_to_print.append([grouped_comment_text])

        formatted_lines_to_print.append(self._get_formatted_items_to_print(
            items=items,
            preceding_removed_combo=preceding_removed_combo,
            production_order=production_order,
            pre_option_level=pre_option_level,
        ))

        formatted_lines_to_print.append([grouped_comment_separator])

        return formatted_lines_to_print

    def _get_formatted_items_to_print(
        self,                       # type: PickListReportGenerator
        items,                      # type: List[Item]
        preceding_removed_combo,    # type: bool
        production_order,           # type: ProductionOrder
        pre_option_level,           # type: Optional[int]
    ):
        # type: (...) -> List

        model = sysactions.get_model(0)

        item_sale_lines = []
        for item in items:
            self.generate_sale_line_by_item(
                model=model,
                grouped_sale_lines=item_sale_lines,
                item=item,
                pre_option_level=pre_option_level,
                preceding_removed_combo=preceding_removed_combo,
            )
            if len(item.items) > 0:
                self.generate_sale_line_of_all_items_sons(
                    model=model,
                    production_order=production_order,
                    item_sale_lines=item_sale_lines,
                    pre_option_level=pre_option_level,
                    preceding_removed_combo=preceding_removed_combo,
                    items=item.items,
                )

        return item_sale_lines

    @staticmethod
    def _order_is_kiosk(
        pod_type,   # type: str
    ):
        # type: (...) -> bool

        return pod_type == "KK"
