# Copyright 2014-2020 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# GNU Radio Companion is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# GNU Radio Companion is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA

from __future__ import absolute_import, print_function

# Standard modules
import logging
import functools

from qtpy import QtGui, QtCore, QtWidgets, QT6
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QApplication

from itertools import count

# Custom modules
from ....core.base import Element
from .connection import DummyConnection
from .port import GUIPort
from ... import base
from ....core.FlowGraph import FlowGraph as CoreFlowgraph
from ... import Utils
from ...external_editor import ExternalEditor

from .block import GUIBlock
from .connection import GUIConnection

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

DEFAULT_MAX_X = 400
DEFAULT_MAX_Y = 300


class Flowgraph(CoreFlowgraph):
    def __init__(self, gui, platform, *args, **kwargs):
        self.gui = gui
        CoreFlowgraph.__init__(self, platform)


class FlowgraphScene(QtWidgets.QGraphicsScene, base.Component):
    itemMoved = QtCore.Signal([QtCore.QPointF])
    newElement = QtCore.Signal([Element])
    deleteElement = QtCore.Signal([Element])
    blockPropsChange = QtCore.Signal([Element])

    def __init__(self, view, platform, *args, **kwargs):
        self.core = Flowgraph(self, platform)
        super(FlowgraphScene, self).__init__()
        self.setParent(view)
        self.view = view
        self.isPanning = False
        self.mousePressed = False
        self.moving_blocks = False

        self.dummy_arrow = None
        self.start_port = None
        self.end_port = None
        self._elements_to_draw = []
        self._external_updaters = {}

        self.qsettings = QApplication.instance().qsettings

        if QT6:
            self.undoStack = QtGui.QUndoStack(self)
        else:
            self.undoStack = QtWidgets.QUndoStack(self)
        self.undoAction = self.undoStack.createUndoAction(self, "Undo")
        self.redoAction = self.undoStack.createRedoAction(self, "Redo")

        self.filename = None

        self.clickPos = None

        self.saved = False
        self.save_allowed = True

    def set_saved(self, saved):
        self.saved = saved

    def update(self):
        """
        Call the top level rewrite and validate.
        Call the top level create labels and shapes.
        """
        self.core.rewrite()
        self.core.validate()
        for block in self.core.blocks:
            block.gui.create_shapes_and_labels()
        self.update_elements_to_draw()
        self.app.VariableEditor.update_gui(self.core.blocks)

    def update_elements_to_draw(self):
        # hide_disabled_blocks = Actions.TOGGLE_HIDE_DISABLED_BLOCKS.get_active()
        hide_disabled_blocks = False
        # hide_variables = Actions.TOGGLE_HIDE_VARIABLES.get_active()
        hide_variables = False

        def draw_order(elem):
            return elem.gui.isSelected(), elem.is_block, elem.enabled

        elements = sorted(self.core.get_elements(), key=draw_order)
        del self._elements_to_draw[:]

        for element in elements:
            if hide_disabled_blocks and not element.enabled:
                continue  # skip hidden disabled blocks and connections
            if hide_variables and (element.is_variable or element.is_import):
                continue  # skip hidden disabled blocks and connections
            self._elements_to_draw.append(element)

    def dragEnterEvent(self, event):
        if event.mimeData().hasUrls:
            event.setDropAction(Qt.CopyAction)
            event.accept()
        else:
            event.ignore()

    def dragMoveEvent(self, event):
        if event.mimeData().hasUrls:
            event.setDropAction(Qt.CopyAction)
            event.accept()
        else:
            event.ignore()

    def decode_data(self, bytearray):
        data = []
        item = {}
        ds = QtCore.QDataStream(bytearray)
        while not ds.atEnd():
            row = ds.readInt32()
            column = ds.readInt32()
            map_items = ds.readInt32()
            for i in range(map_items):
                key = ds.readInt32()
                value = QtCore.QVariant()
                ds >> value
                item[Qt.ItemDataRole(key)] = value
            data.append(item)
        return data

    def _get_unique_id(self, base_id=""):
        """
        Get a unique id starting with the base id.

        Args:
            base_id: the id starts with this and appends a count

        Returns:
            a unique id
        """
        block_ids = set(b.name for b in self.core.blocks)
        for index in count():
            block_id = "{}_{}".format(base_id, index)
            if block_id not in block_ids:
                break
        return block_id

    def dropEvent(self, event):
        QtWidgets.QGraphicsScene.dropEvent(self, event)
        if event.mimeData().hasUrls:
            data = event.mimeData()
            if data.hasFormat("application/x-qabstractitemmodeldatalist"):
                bytearray = data.data("application/x-qabstractitemmodeldatalist")
                data_items = self.decode_data(bytearray)

                # Find block in tree so that we can pull out label
                block_key = data_items[0][QtCore.Qt.UserRole].value()

                # Add block of this key at the cursor position
                cursor_pos = event.scenePos()
                pos = (cursor_pos.x(), cursor_pos.y())

                self.add_block(block_key, pos)

                event.setDropAction(Qt.CopyAction)
                event.accept()
            else:
                return QtGui.QStandardItemModel.dropMimeData(
                    self, data, action, row, column, parent
                )
        else:
            event.ignore()

    def add_block(self, block_key, pos=(0, 0)):
        block = self.platform.blocks[block_key]
        # Pull out its params (keep in mind we still havent added the dialog box that lets you change param values so this is more for show)
        params = []
        for (
            p
        ) in (
            block.parameters_data
        ):  # block.parameters_data is a list of dicts, one per param
            if "label" in p:  # for now let's just show it as long as it has a label
                key = p["label"]
                value = p.get("default", "")  # just show default value for now
                params.append((key, value))

        id = self._get_unique_id(block_key)

        c_block = self.core.new_block(block_key)
        g_block = c_block.gui
        c_block.states["coordinate"] = pos
        g_block.setPos(*pos)
        c_block.params["id"].set_value(id)
        self.addItem(g_block)
        g_block.moveToTop()
        self.update()
        self.newElement.emit(c_block)

    def selected_blocks(self) -> list[GUIBlock]:
        blocks = []
        for item in self.selectedItems():
            if isinstance(item, GUIBlock):
                blocks.append(item)
        return blocks

    def selected_connections(self):
        conns = []
        for item in self.selectedItems():
            if isinstance(item, GUIConnection):
                conns.append(item)
        return conns

    def delete_selected(self):
        for item in self.selectedItems():
            self.remove_element(item)

    def select_all(self):
        for block in self.core.blocks:
            block.gui.setSelected(True)
        for conn in self.core.connections:
            conn.gui.setSelected(True)

    def rotate_selected(self, rotation):
        """
        Rotate the selected blocks by multiples of 90 degrees.
        Args:
            rotation: the rotation in degrees
        Returns:
            true if changed, otherwise false.
        """
        selected_blocks = self.selected_blocks()
        if not any(selected_blocks):
            return False
        # initialize min and max coordinates
        min_x, min_y = max_x, max_y = selected_blocks[0].x(), selected_blocks[0].y()
        # rotate each selected block, and find min/max coordinate
        for selected_block in selected_blocks:
            selected_block.rotate(rotation)
            # update the min/max coordinate
            x, y = selected_block.x(), selected_block.y()
            min_x, min_y = min(min_x, x), min(min_y, y)
            max_x, max_y = max(max_x, x), max(max_y, y)
        # calculate center point of selected blocks
        ctr_x, ctr_y = (max_x + min_x) / 2, (max_y + min_y) / 2
        # rotate the blocks around the center point
        for selected_block in selected_blocks:
            x, y = selected_block.x(), selected_block.y()
            x, y = Utils.get_rotated_coordinate((x - ctr_x, y - ctr_y), rotation)
            selected_block.setPos(x + ctr_x, y + ctr_y)
        return True

    def mousePressEvent(self, event):
        g_item = self.itemAt(event.scenePos(), QtGui.QTransform())
        if not g_item:  # Nothing selected
            event.ignore()
            return

        self.clickPos = event.scenePos()
        self.moving_blocks = False

        if g_item and not isinstance(g_item, DummyConnection):
            c_item = g_item.core
            if c_item.is_block:
                self.moving_blocks = True
            elif c_item.is_port:
                if c_item.is_source:
                    self.start_port = g_item
                elif c_item.is_sink:
                    self.end_port = g_item
        if event.button() == Qt.LeftButton:
            self.mousePressed = True
            super(FlowgraphScene, self).mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if self.mousePressed:
            if not self.dummy_arrow and not self.moving_blocks and self.start_port:
                self.dummy_arrow = DummyConnection(self, self.start_port.connection_point, self.clickPos)
                self.addItem(self.dummy_arrow)
        self.view.setSceneRect(self.itemsBoundingRect())
        if self.dummy_arrow:
            self.dummy_arrow.update(event.scenePos())

        if self.mousePressed and self.isPanning:
            new_pos = event.pos()
            self.dragPos = new_pos
            event.accept()
        else:
            super(FlowgraphScene, self).mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if self.dummy_arrow:  # We are currently dragging a DummyConnection
            self.removeItem(self.dummy_arrow)
            g_item = self.itemAt(event.scenePos(), QtGui.QTransform())
            if isinstance(g_item, GUIPort):
                c_item = g_item.core
                if g_item != self.start_port:
                    log.debug("Created connection (drag)")
                    new_con = self.core.connect(self.start_port.core, c_item)
                    self.addItem(new_con.gui)
                    self.newElement.emit(new_con)
                    self.update()
                    self.start_port = None
                    self.end_port = None
            self.dummy_arrow = None
        else:
            if self.clickPos != event.scenePos() and self.moving_blocks:
                self.itemMoved.emit(event.scenePos() - self.clickPos)
            elif (self.start_port != None) and (self.end_port != None):
                log.debug("Created connection (click)")
                new_con = self.core.connect(self.start_port.core, self.end_port.core)
                self.addItem(new_con.gui)
                self.newElement.emit(new_con)
                self.update()
                self.start_port = None
                self.end_port = None
        self.mousePressed = False
        super(FlowgraphScene, self).mouseReleaseEvent(event)

    def createActions(self, actions):
        log.debug("Creating actions")

        """
        # File Actions
        actions['save'] = Action(Icons("document-save"), _("save"), self,
                                shortcut=Keys.New, statusTip=_("save-tooltip"))

        actions['clear'] = Action(Icons("document-close"), _("clear"), self,
                                         shortcut=Keys.Open, statusTip=_("clear-tooltip"))
        """

    def createMenus(self, actions, menus):
        log.debug("Creating menus")

    def createToolbars(self, actions, toolbars):
        log.debug("Creating toolbars")

    def import_data(self, data):
        self.core.import_data(data)
        for conn in self.core.connections:
            self.addItem(conn.gui)
        for block in self.core.blocks:
            self.addItem(block.gui)

    def getMaxZValue(self):
        z_values = []
        for block in self.core.blocks:
            z_values.append(block.gui.zValue())
        return max(z_values)

    def remove_element(self, element: GUIBlock):
        self.removeItem(element)
        self.core.remove_element(element.core)

    def get_extents(self):
        # show_comments = Actions.TOGGLE_SHOW_BLOCK_COMMENTS.get_active()
        show_comments = True

        def sub_extents():
            for element in self._elements_to_draw:
                yield element.get_extents()
                if element.is_block and show_comments and element.enabled:
                    yield element.get_extents_comment()

        extent = 10000000, 10000000, 0, 0
        cmps = (min, min, max, max)
        for sub_extent in sub_extents():
            extent = [cmp(xy, e_xy) for cmp, xy, e_xy in zip(cmps, extent, sub_extent)]
        return tuple(extent)

    def copy_to_clipboard(self):
        """
        Copy the selected blocks and connections into the clipboard.

        Returns:
            the clipboard
        """
        # get selected blocks
        g_blocks = list(self.selected_blocks())
        if not g_blocks:
            return None
        # calc x and y min
        x_min, y_min = g_blocks[0].core.states["coordinate"]
        for g_block in g_blocks:
            x, y = g_block.core.states["coordinate"]
            x_min = min(x, x_min)
            y_min = min(y, y_min)
        # get connections between selected blocks
        connections = list(
            filter(
                lambda c: c.source_block.gui in g_blocks and c.sink_block.gui in g_blocks,
                self.core.connections,
            )
        )
        clipboard = (
            (x_min, y_min),
            [g_block.core.export_data() for g_block in g_blocks],
            [c_connection.export_data() for c_connection in connections],
        )
        return clipboard

    def paste_from_clipboard(self, clipboard):
        """
        Paste the blocks and connections from the clipboard.

        Args:
            clipboard: the nested data of blocks, connections
        """
        self.clearSelection()
        (x_min, y_min), blocks_n, connections_n = clipboard
        """
        # recalc the position
        scroll_pane = self.drawing_area.get_parent().get_parent()
        h_adj = scroll_pane.get_hadjustment()
        v_adj = scroll_pane.get_vadjustment()
        x_off = h_adj.get_value() - x_min + h_adj.get_page_size() / 4
        y_off = v_adj.get_value() - y_min + v_adj.get_page_size() / 4

        if len(self.get_elements()) <= 1:
            x_off, y_off = 0, 0
        """
        x_off, y_off = 10, 10

        # create blocks
        pasted_blocks = {}
        for block_n in blocks_n:
            block_key = block_n.get("id")
            if block_key == "options":
                continue

            block_name = block_n.get("name")
            # Verify whether a block with this name exists before adding it
            if block_name in (blk.name for blk in self.core.blocks):
                block_n = block_n.copy()
                block_n["name"] = self._get_unique_id(block_name)

            block = self.core.new_block(block_key)
            if not block:
                continue  # unknown block was pasted (e.g. dummy block)

            block.import_data(**block_n)
            pasted_blocks[block_name] = block  # that is before any rename

            block.gui.moveBy(x_off, y_off)
            self.addItem(block.gui)
            block.gui.moveToTop()
            block.gui.setSelected(True)
            """
            while any(Utils.align_to_grid(block.states['coordinate']) == Utils.align_to_grid(other.states['coordinate'])
                      for other in self.blocks if other is not block):
                block.moveBy(Constants.CANVAS_GRID_SIZE,
                           Constants.CANVAS_GRID_SIZE)
                # shift all following blocks
                x_off += Constants.CANVAS_GRID_SIZE
                y_off += Constants.CANVAS_GRID_SIZE
            """

        # update before creating connections
        self.update()
        # create connections
        for src_block, src_port, dst_block, dst_port in connections_n:
            source = pasted_blocks[src_block].get_source(src_port)
            sink = pasted_blocks[dst_block].get_sink(dst_port)
            connection = self.core.connect(source, sink)
            self.addItem(connection.gui)
            connection.gui.setSelected(True)

    def itemsBoundingRect(self):
        rect = QtWidgets.QGraphicsScene.itemsBoundingRect(self)
        return QtCore.QRectF(0.0, 0.0, rect.right(), rect.bottom())

    def install_external_editor(self, param, parent=None):
        target = (param.parent_block.name, param.key)

        if target in self._external_updaters:
            editor = self._external_updaters[target]
        else:
            editor = self.qsettings.value("grc/editor", "")
            if not editor:
                return
            updater = functools.partial(
                self.handle_external_editor_change, target=target)
            editor = self._external_updaters[target] = ExternalEditor(
                editor=editor,
                name=target[0], value=param.get_value(),
                callback=updater
            )
            editor.start()
        try:
            editor.open_editor()
        except Exception as e:
            # Problem launching the editor. Need to select a new editor.
            log.error('Error opening an external editor. Please select a different editor.\n')
            # Reset the editor to force the user to select a new one.
            self.parent_platform.config.editor = ''
            self.remove_external_editor(target=target)

    def remove_external_editor(self, target=None, param=None):
        if target is None:
            target = (param.parent_block.name, param.key)
        if target in self._external_updaters:
            self._external_updaters[target].stop()
            del self._external_updaters[target]

    def handle_external_editor_change(self, new_value, target):
        try:
            block_id, param_key = target
            self.core.get_block(block_id).params[param_key].set_value(new_value)

        except (IndexError, ValueError):  # block no longer exists
            self.remove_external_editor(target=target)
            return
