import logging

from qtpy.QtGui import QPen, QPainter, QBrush, QFont, QFontMetrics
from qtpy.QtCore import Qt, QPointF, QRectF, QUrl
from qtpy.QtWidgets import QGraphicsItem, QApplication, QAction

from . import colors
from ... import Constants
from ... import Utils
from ....core.blocks.block import Block as CoreBlock
from ....core.utils import flow_graph_complexity

from ..dialogs import PropsDialog

# Logging
log = logging.getLogger(f"grc.application.{__name__}")

LONG_VALUE = 20  # maximum length of a param string.
# if exceeded, '...' will be displayed


class Block(CoreBlock):
    """
    A block. Accesses its graphical representation with self.gui.
    """
    @classmethod
    def make_cls_with_base(cls, super_cls):
        name = super_cls.__name__
        bases = (super_cls,) + cls.__bases__[:-1]
        namespace = cls.__dict__.copy()
        return type(name, bases, namespace)

    def __init__(self, parent, **n):
        super(self.__class__, self).__init__(parent, **n)

        self.width = 50  # will change immediately after the first paint
        self.block_label = self.key

        self.old_data = None

        if "rotation" not in self.states.keys():
            self.states["rotation"] = 0.0

        self.gui = GUIBlock(self, parent)

    def import_data(self, name, states, parameters, **_):
        super(self.__class__, self).import_data(name, states, parameters, **_)
        self.gui.setPos(*self.states["coordinate"])
        self.gui.setRotation(self.states["rotation"])
        self.rewrite()
        self.gui.create_shapes_and_labels()

    def update_bus_logic(self):
        for direc in {'source', 'sink'}:
            if direc == 'source':
                ports = self.sources
                ports_gui = self.filter_bus_port(self.sources)
            else:
                ports = self.sinks
                ports_gui = self.filter_bus_port(self.sinks)
            if 'bus' in map(lambda a: a.dtype, ports):
                for port in ports_gui:
                    self.parent_flowgraph.gui.removeItem(port.gui)
        super(self.__class__, self).update_bus_logic()


class GUIBlock(QGraphicsItem):
    """
    The graphical representation of a block. Accesses the actual block with self.core.
    """

    def __init__(self, core, parent, **n):
        super(GUIBlock, self).__init__()
        self.core = core
        self.parent = self.scene()
        self.font = QFont("Helvetica", 10)

        self.create_shapes_and_labels()

        if "coordinate" not in self.core.states.keys():
            self.core.states["coordinate"] = (500, 300)
            self.setPos(*self.core.states["coordinate"])

        self.old_pos = (self.x(), self.y())
        self.new_pos = (self.x(), self.y())
        self.moving = False

        self.props_dialog = None
        self.right_click_menu = None

        self.setFlag(QGraphicsItem.ItemIsMovable)
        self.setFlag(QGraphicsItem.ItemIsSelectable)
        self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges)

        self.markups_height = 0.0
        self.markups_width = 0.0
        self.markups = []
        self.markup_text = ""

    def create_shapes_and_labels(self):
        qsettings = QApplication.instance().qsettings
        self.force_show_id = qsettings.value('grc/show_block_ids', type=bool)
        self.hide_variables = qsettings.value('grc/hide_variables', type=bool)
        self.hide_disabled_blocks = qsettings.value('grc/hide_disabled_blocks', type=bool)
        self.snap_to_grid = qsettings.value('grc/snap_to_grid', type=bool)
        self.show_complexity = qsettings.value('grc/show_complexity', type=bool)
        self.show_block_comments = qsettings.value('grc/show_block_comments', type=bool)
        self.show_param_expr = qsettings.value('grc/show_param_expr', type=bool)
        self.show_param_val = qsettings.value('grc/show_param_val', type=bool)
        self.prepareGeometryChange()
        self.font.setBold(True)

        # figure out height of block based on how many params there are
        i = 35

        # Check if we need to increase the height to fit all the ports
        for key, item in self.core.params.items():
            value = item.value
            if (value is not None and item.hide == "none") or (item.dtype == 'id' and self.force_show_id):
                i += 20

        self.height = i

        def get_min_height_for_ports(ports):
            min_height = (
                2 * Constants.PORT_BORDER_SEPARATION +
                len(ports) * Constants.PORT_SEPARATION
            )
            # If any of the ports are bus ports - make the min height larger
            if any([p.dtype == "bus" for p in ports]):
                min_height = (
                    2 * Constants.PORT_BORDER_SEPARATION +
                    sum(
                        port.gui.height + Constants.PORT_SPACING
                        for port in ports
                        if port.dtype == "bus"
                    ) - Constants.PORT_SPACING
                )

            else:
                if ports:
                    min_height -= ports[-1].gui.height
            return min_height

        self.height = max(
            self.height,
            get_min_height_for_ports(self.core.active_sinks),
            get_min_height_for_ports(self.core.active_sources),
        )

        # Figure out width of block based on widest line of text
        fm = QFontMetrics(self.font)
        largest_width = fm.width(self.core.label)
        for key, item in self.core.params.items():
            name = item.name
            value = item.value
            if (value is not None and item.hide == "none") or (item.dtype == 'id' and self.force_show_id):
                if len(value) > LONG_VALUE:
                    value = value[:LONG_VALUE - 3] + '...'

                value_label = item.options[value] if value in item.options else value
                full_line = name + ": " + value_label

                if fm.width(full_line) > largest_width:
                    largest_width = fm.width(full_line)
        self.width = largest_width + 15

        self.markups = []
        self.markup_text = ""
        self.markups_width = 0.0

        if self.show_complexity and self.core.key == "options":
            complexity = flow_graph_complexity.calculate(self.parent)
            self.markups.append('Complexity: {num} bal'.format(
                num=Utils.num_to_str(complexity)))

        if self.show_block_comments and self.core.comment:
            self.markups.append(self.core.comment)

        self.markup_text = "\n".join(self.markups).strip()

        self.markups_height = fm.height() * (self.markup_text.count("\n") + 1)
        for line in self.markup_text.split("\n"):
            if fm.width(line) > self.markups_width:
                self.markups_width = fm.width(line)

        # Update the position and size of all the ports
        bussified = (
            self.core.current_bus_structure["source"],
            self.core.current_bus_structure["sink"],
        )
        for ports, has_busses in zip(
            (self.core.active_sources, self.core.active_sinks), bussified
        ):
            if not ports:
                continue
            port_separation = (
                Constants.PORT_SEPARATION
                if not has_busses
                else ports[0].gui.height + Constants.PORT_SPACING
            )
            offset = (
                self.height - (len(ports) - 1) * port_separation - ports[0].gui.height
            ) / 2
            for port in ports:
                if port._dir == "sink":
                    port.gui.setPos(-15, offset)
                else:
                    port.gui.setPos(self.width, offset)
                port.gui.create_shapes_and_labels()

                offset += (
                    Constants.PORT_SEPARATION
                    if not has_busses
                    else port.gui.height + Constants.PORT_SPACING
                )

        self._update_colors()
        self.create_port_labels()
        self.setTransformOriginPoint(self.width / 2, self.height / 2)

    def create_port_labels(self):
        for ports in (self.core.active_sinks, self.core.active_sources):
            for port in ports:
                port.gui.create_shapes_and_labels()

    def _update_colors(self):
        def get_bg():
            """
            Get the background color for this block
            Explicit is better than a chain of if/else expressions,
            so this was extracted into a nested function.
            """
            if self.core.is_dummy_block:
                return colors.MISSING_BLOCK_BACKGROUND_COLOR
            if self.core.state == "bypassed":
                return colors.BLOCK_BYPASSED_COLOR
            if self.core.state == "enabled":
                if self.core.deprecated:
                    return colors.BLOCK_DEPRECATED_BACKGROUND_COLOR
                return colors.BLOCK_ENABLED_COLOR
            return colors.BLOCK_DISABLED_COLOR

        self._bg_color = get_bg()

    def move(self, x, y):
        self.moveBy(x, y)
        self.core.states["coordinate"] = (self.x(), self.y())

    def paint(self, painter, option, widget):
        if (self.hide_variables and (self.core.is_variable or self.core.is_import)) or (self.hide_disabled_blocks and not self.core.enabled):
            return

        painter.setRenderHint(QPainter.Antialiasing)
        self.font.setBold(True)

        # TODO: Make sure this is correct
        border_color = colors.BORDER_COLOR

        if self.isSelected():
            border_color = colors.HIGHLIGHT_COLOR
        else:
            if self.core.is_dummy_block:
                border_color = colors.MISSING_BLOCK_BORDER_COLOR
            elif self.core.deprecated:
                border_color = colors.BLOCK_DEPRECATED_BORDER_COLOR
            elif self.core.state == "disabled":
                border_color = colors.BORDER_COLOR_DISABLED

        pen = QPen(1)
        pen = QPen(border_color)

        pen.setWidth(3)
        painter.setPen(pen)
        painter.setBrush(QBrush(self._bg_color))
        rect = QRectF(0, 0, self.width, self.height)

        painter.drawRect(rect)
        painter.setPen(QPen(1))

        # Draw block label text
        painter.setFont(self.font)
        if self.core.is_valid():
            painter.setPen(Qt.black)
        else:
            painter.setPen(Qt.red)

        # Adjust the painter if parent block is 180 degrees rotated
        if self.rotation() == 180:
            painter.translate(self.width / 2, self.height / 2)
            painter.rotate(180)
            painter.translate(-self.width / 2, -self.height / 2)

        painter.drawText(
            QRectF(0, 0 - self.height / 2 + 15, self.width, self.height),
            Qt.AlignCenter,
            self.core.label,
        )

        # Draw param text
        y_offset = 30  # params start 30 down from the top of the box
        for key, item in self.core.params.items():
            name = item.name
            value = item.value
            is_evaluated = item.value != str(item.get_evaluated())

            display_value = ""

            # Include the value defined by the user (after evaluation)
            if not is_evaluated or self.show_param_val or not self.show_param_expr:
                display_value += item.options[value] if value in item.options else value  # TODO: pretty_print

            # Include the expression that was evaluated to get the value
            if is_evaluated and self.show_param_expr:
                expr_string = value  # TODO: Truncate

                if display_value:  # We are already displaying the value
                    display_value = expr_string + "=" + display_value
                else:
                    display_value = expr_string

            if len(display_value) > LONG_VALUE:
                display_value = display_value[:LONG_VALUE - 3] + '...'

            value_label = display_value
            if (value is not None and item.hide == "none") or (item.dtype == 'id' and self.force_show_id):
                if item.is_valid():
                    painter.setPen(QPen(1))
                else:
                    painter.setPen(Qt.red)

                self.font.setBold(True)
                painter.setFont(self.font)
                painter.drawText(
                    QRectF(7.5, 0 + y_offset, self.width, self.height),
                    Qt.AlignLeft,
                    name + ": ",
                )
                fm = QFontMetrics(self.font)
                self.font.setBold(False)
                painter.setFont(self.font)
                painter.drawText(
                    QRectF(
                        7.5 + fm.width(name + ": "),
                        0 + y_offset,
                        self.width,
                        self.height,
                    ),
                    Qt.AlignLeft,
                    value_label,
                )
                y_offset += 20

        if self.markup_text:
            painter.setPen(Qt.gray)
            painter.drawText(
                QRectF(0, self.height + 5, self.markups_width, self.markups_height),
                Qt.AlignLeft,
                self.markup_text,
            )

    def boundingRect(self):
        # TODO: Comments should be a separate QGraphicsItem
        return QRectF(
            -2.5, -2.5, self.width + 5 + self.markups_width, self.height + 5 + self.markups_height
        )

    def set_states(self, states):
        for k, v in states.items():
            self.core.states[k] = v

        self.setPos(*self.core.states["coordinate"])
        self.setRotation(self.core.states["rotation"])

    def mousePressEvent(self, e):
        super(self.__class__, self).mousePressEvent(e)
        log.debug(f"{self} clicked")
        url_prefix = str(self.core.parent_platform.config.wiki_block_docs_url_prefix)
        QApplication.instance().WikiTab.setURL(QUrl(url_prefix + self.core.label.replace(" ", "_")))

        self.moveToTop()

    def contextMenuEvent(self, e):
        if not self.isSelected():
            self.scene().clearSelection()
            self.setSelected(True)

        self.right_click_menu = self.scene()._app().MainWindow.menus["edit"]
        example_action = QAction("Examples...")
        example_action.triggered.connect(self.view_examples)
        self.right_click_menu.addAction(example_action)
        self.right_click_menu.exec_(e.screenPos())

    def view_examples(self):
        self.scene().app.MainWindow.example_browser_triggered(key_filter=self.core.key)

    def mouseDoubleClickEvent(self, e):
        self.open_properties()
        super(self.__class__, self).mouseDoubleClickEvent(e)

    def itemChange(self, change, value):
        if change == QGraphicsItem.ItemPositionChange and self.scene() and self.snap_to_grid:
            grid_size = 10
            value.setX(round(value.x() / grid_size) * grid_size)
            value.setY(round(value.y() / grid_size) * grid_size)
            return value
        else:
            return QGraphicsItem.itemChange(self, change, value)

    def rotate(self, rotation):
        log.debug(f"Rotating {self.core.name}")
        new_rotation = (self.rotation() + rotation) % 360
        self.setRotation(new_rotation)
        self.core.states["rotation"] = new_rotation
        self.create_shapes_and_labels()
        for con in self.core.connections():
            con.gui.set_rotation(new_rotation)
            con.gui.update()

    def moveToTop(self):
        # TODO: Is there a simpler way to do this?
        self.setZValue(self.scene().getMaxZValue() + 1)

    def center(self):
        return QPointF(self.x() + self.width / 2, self.y() + self.height / 2)

    def open_properties(self):
        self.props_dialog = PropsDialog(self.core, self.force_show_id)
        self.props_dialog.show()
