# GraphWidget.py -- Pamhyr
# Copyright (C) 2023-2026  INRAE
#
# This program 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 3 of the License, or
# (at your option) any later version.
#
# This program 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, see <https://www.gnu.org/licenses/>.

# -*- coding: utf-8 -*-

import math
import logging

from tools import timer

from PyQt5.QtCore import (
    Qt, QPoint, QPointF, QSizeF, QLineF, QRectF,
    pyqtSlot, pyqtSignal, QCoreApplication,
)
from PyQt5.QtGui import (
    QPainter, QColor, QBrush, QPen, QPainterPath,
    QPolygonF,
)
from PyQt5.QtWidgets import (
    QApplication, QGraphicsScene, QGraphicsView,
    QGraphicsItem, QGraphicsTextItem, QMenu, QMessageBox,
)

from View.WaitingDialog import WaitingDialog

from View.Scenarios.UndoCommand import *
from View.Scenarios.ContextMenu import (
    DefaultMenu, ScenarioMenu,
)

logger = logging.getLogger()

_translate = QCoreApplication.translate


class ScenarioItem(QGraphicsTextItem):
    Type = QGraphicsItem.UserType + 10

    def __init__(self, scenario, graph_widget):
        super(ScenarioItem, self).__init__()

        self.graph = graph_widget
        self.scenario = scenario

        self.setPos(QPointF(self.scenario.x, self.scenario.y))

        tag = " "
        if self.graph.scenario_has_child(self.scenario):
            tag = "⛔ "

        self.setHtml(tag + self.scenario.name)
        self.setDefaultTextColor(Qt.black)

        self.setFlag(QGraphicsItem.ItemIsMovable)
        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
        self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
        self.setZValue(1)

    def type(self):
        return ScenarioItem.Type

    def boundingRect(self):
        extra = 6
        return super(ScenarioItem, self)\
            .boundingRect()\
            .normalized()\
            .adjusted(
                -extra, -extra,
                extra, extra
            )

    @timer
    def paint(self, painter, option, widget):
        extra = 2
        pcolor = QColor(Qt.black)
        color = QColor("#aac3d7")
        if self.graph._study.status.scenario is self.scenario:
            color = QColor("#eeaba5")
        elif self.scenario.id == 0:
            color = QColor("#d3c1d8")
        elif self.graph.scenario_has_child(self.scenario):
            color = QColor("#e6e6e6")

        painter.setPen(
            QPen(
                pcolor, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin
            )
        )
        painter.setBrush(QBrush(color))
        painter.drawEllipse(self.boundingRect())

        super(ScenarioItem, self).paint(painter, option, widget)

    def itemChange(self, change, value):
        if change == QGraphicsItem.ItemPositionHasChanged:
            self.scenario.set_pos(value.x(), value.y())

        self.graph.edges_display_update()
        return super(ScenarioItem, self).itemChange(change, value)


class EdgeItem(QGraphicsItem):
    Type = QGraphicsItem.UserType + 11

    def __init__(self, scenario_item, parent_item, graph_widget):
        super(EdgeItem, self).__init__()

        self.item = scenario_item
        self.parent_item = parent_item
        self.graph = graph_widget

        self.setAcceptedMouseButtons(Qt.NoButton)

    def type(self):
        return EdgeItem.Type

    def boundingRect(self):
        extra = 0.0
        for item in [self.item, self.parent_item]:
            rect = item.boundingRect()
            extra = max(
                [rect.width(), rect.height(), extra]
            )

        extra /= 2
        return QRectF(
            self.parent_item.pos(),
            QSizeF(
                self.item.pos().x() - self.parent_item.pos().x(),
                self.item.pos().y() - self.parent_item.pos().y()
            )
        ).normalized().adjusted(-extra, -extra, extra, extra)

    @timer
    def paint(self, painter, option, widget):
        line = QLineF(
            self.get_line_item_point(self.parent_item),
            self.get_line_item_point(self.item)
        )

        if line.length() == 0.0:
            return

        color = Qt.black
        arrow = self.get_arrow_path(line, color)

        brush = QBrush()
        path = QPainterPath()
        brush.setColor(color)
        brush.setStyle(Qt.SolidPattern)

        painter.setPen(
            QPen(
                color, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin
            )
        )
        painter.drawLine(line)
        path.addPolygon(arrow)

        painter.drawPolygon(arrow)
        painter.fillPath(path, brush)

    def get_arrow_path(self, line, color):
        pp = self.get_line_item_point(self.parent_item)
        p = self.get_line_item_point(self.item)

        line_center = QPointF(
            (pp.x() + p.x()) / 2,
            (pp.y() + p.y()) / 2,
        )

        angle = math.acos(line.dx() / line.length())
        if line.dy() >= 0:
            angle = (math.pi * 2.0) - angle

        size = 10.0
        arrow_p1 = line_center + QPointF(
            math.sin(angle - math.pi / 3) * size,
            math.cos(angle - math.pi / 3) * size
        )
        arrow_p2 = line_center + QPointF(
            math.sin(angle - math.pi + math.pi / 3) * size,
            math.cos(angle - math.pi + math.pi / 3) * size
        )
        poly = QPolygonF([line_center, arrow_p1, arrow_p2])

        return poly

    def get_line_item_point(self, item):
        item_extra = 6
        rect = item.boundingRect()
        return QPointF(
            item.pos().x() + ((rect.width() - item_extra) / 2.0),
            item.pos().y() + ((rect.height() - item_extra) / 2.0)
        )


class GraphWidget(QGraphicsView):
    changeScenario = pyqtSignal(object)

    def __init__(self, study, parent=None,
                 min_size=(400, 400), max_size=None,
                 size=None, undo=None, trad=None):
        super(GraphWidget, self).__init__(parent=parent)

        self._study = study

        self.parent = parent
        self._undo = undo
        self._trad = trad

        self.m_origin_x = 0.0
        self.m_origin_y = 0.0

        self.scenarios_items = {}
        self.edge_items = []

        self.setup_scene(min_size, max_size, size)

    def setup_scene(self, min_size, max_size, size):
        scene = QGraphicsScene(self)
        scene.setItemIndexMethod(QGraphicsScene.NoIndex)
        scene.setSceneRect(0, 0, 2000, 2000)

        self.setScene(scene)
        self.setCacheMode(QGraphicsView.CacheBackground)
        self.setViewportUpdateMode(QGraphicsView.BoundingRectViewportUpdate)
        self.setRenderHint(QPainter.Antialiasing)
        self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
        self.setResizeAnchor(QGraphicsView.AnchorViewCenter)

        self.scale(1, 1)
        self.previousScale = 1

        if min_size:
            self.setMinimumSize(*min_size)
        if max_size:
            self.setMaximumSize(*max_size)
        if size:
            self.resize(*size)

        self.create_items()

    @timer
    def create_items(self):
        """Create all items and draw its

        Returns:
            Nothing
        """
        self.scenarios_items = {}
        self.edge_items = []

        for scenar in self._study.scenarios.lst:
            if scenar.is_deleted():
                continue

            iscenar = ScenarioItem(scenar, self)
            self.scene().addItem(iscenar)
            self.scenarios_items[scenar] = iscenar

            if scenar.parent is not None:
                iedge = EdgeItem(
                    self.scenarios_items[scenar],
                    self.scenarios_items[scenar.parent],
                    self
                )
                self.scene().addItem(iedge)
                self.edge_items.append(iedge)

    def edges_display_update(self):
        for e in self.edge_items:
            e.update()

    def display_update(self):
        """Clear the scene and redraw it

        Returns:
            Nothing
        """
        self.scene().clear()
        self.create_items()

    def scenario_has_child(self, scenario):
        for scenar in self._study.scenarios.lst:
            if scenar.parent is scenario:
                if not scenar.is_deleted():
                    return True

        return False

    def drawBackground(self, painter, rect):
        sceneRect = self.sceneRect()
        painter.fillRect(
            rect.intersected(sceneRect),
            QBrush(QColor("#e6e6e6"))
        )
        painter.setBrush(Qt.NoBrush)
        painter.drawRect(sceneRect)

    def wheelEvent(self, event):
        factor = math.pow(2.0, event.angleDelta().y() / 240.0)

        old_pos = self.mapToScene(event.pos())

        self.scaleView(factor)

        new_pos = self.mapToScene(event.pos())

        delta = old_pos - new_pos

        # Compensation pour garder le point sous la souris fixe
        self.translate(delta.x(), delta.y())

    def keyPressEvent(self, event):
        key = event.key()

        if key == Qt.Key_Plus:
            self.scaleView(1.2)
        elif key == Qt.Key_Minus:
            self.scaleView(1 / 1.2)
        else:
            super(GraphWidget, self).keyPressEvent(event)

    def scaleView(self, scaleFactor):
        factor = self.transform().scale(
            scaleFactor, scaleFactor
        ).mapRect(QRectF(0, 0, 1, 1)).width()

        if factor < 0.05 or factor > 1.5:
            return

        self.scale(scaleFactor, scaleFactor)

    def mousePressEvent(self, event):
        # pos = self.mapToScene(event.pos())
        # items = self.items(event.pos())

        self.update()
        super(GraphWidget, self).mousePressEvent(event)

    def mouseMoveEvent(self, event):
        pos = self.mapToScene(event.pos())

        items = self.items(event.pos())
        selectable_items = list(
            filter(
                lambda i: (type(i) is ScenarioItem or
                           type(i) is EdgeItem),
                items
            )
        )

        # Move on scene
        if len(selectable_items) == 0:
            if event.buttons() & Qt.LeftButton:
                old_p = self.mapToScene(
                    QPoint(
                        int(self.m_origin_x),
                        int(self.m_origin_y)
                    )
                )
                new_p = self.mapToScene(event.pos())
                translation = new_p - old_p

                self.translate(translation.x(), translation.y())

                self.m_origin_x = event.x()
                self.m_origin_y = event.y()

        # Propagate event
        self.update()
        super(GraphWidget, self).mouseMoveEvent(event)

    def exec_with_waiting_window(self, fn, action_str):
        dlg = WaitingDialog(
            payload_fn=fn,
            title=action_str,
            trad=self._trad,
            parent=self.parent
        )
        dlg.exec_()

    # Contextual menu

    def contextMenuEvent(self, event):
        pos = self.mapToScene(event.pos())
        items = self.items(event.pos())

        items = list(
            filter(
                lambda i: (type(i) is ScenarioItem or
                           type(i) is EdgeItem),
                items
            )
        )

        m_type = DefaultMenu
        if len(items) != 0:
            if type(items[0]) is ScenarioItem:
                m_type = ScenarioMenu

        # Create and exec menu
        m = m_type(
            event=event, pos=pos, items=items,
            graph=self._study, trad=self._trad,
            parent=self
        )
        m.run()

    def dialog_save(self):
        dlg = QMessageBox(self)

        dlg.setWindowTitle(self._trad["mb_save_title"])
        dlg.setText(self._trad["mb_save_msg"])
        opt = QMessageBox.Cancel | QMessageBox.Save | QMessageBox.Discard

        dlg.setStandardButtons(opt)
        dlg.setIcon(QMessageBox.Warning)
        dlg.button(QMessageBox.Save).setText(self._trad["Save"])
        dlg.button(QMessageBox.Discard).setText(self._trad["Don't save"])
        dlg.button(QMessageBox.Cancel).setText(self._trad["Cancel"])

        res = dlg.exec()

        if res == QMessageBox.Save:
            return "Save"
        elif res == QMessageBox.Cancel:
            return "Cancel"
        else:
            return "Discard"

    def select_scenario(self, item):
        if type(item) is not ScenarioItem:
            return

        must_save = self.dialog_save()
        if must_save == "Cancel":
            return

        def fn():
            self._close_other_window()

            if must_save == "Save":
                self._study.save()

            self._undo.push(
                SelectScenariosCommand(
                    self._study,
                    item.scenario
                )
            )
            # self._study.reload_from_scenario(item.scenario)

        self.exec_with_waiting_window(fn, "select_scenario")

        self.changeScenario.emit(self.sender())

    def new_scenario(self, pos):
        must_save = self.dialog_save()
        if must_save == "Cancel":
            return

        def fn():
            self._close_other_window()

            if must_save == "Save":
                self._study.save()

            self._undo.push(
                AddScenariosCommand(
                    self._study,
                    pos
                )
            )
            # scenario = self._study.new_scenario_from_current()
            # scenario.set_pos(pos.x(), pos.y())

        self.exec_with_waiting_window(fn, "new_scenario")
        self.changeScenario.emit(self.sender())

    def delete_scenario(self, item):
        def fn():
            self._close_other_window()
            # self._study.save()
            self._undo.push(
                DeleteScenariosCommand(
                    self._study,
                    item.scenario
                )
            )

        self.exec_with_waiting_window(fn, "delete_scenario")
        self.changeScenario.emit(self.sender())

    def duplicate_scenario(self, item):
        must_save = self.dialog_save()
        if must_save == "Cancel":
            return

        def fn():
            self._close_other_window()

            if must_save == "Save":
                self._study.save()

            self._undo.push(
                DuplicateScenariosCommand(
                    self._study,
                )
            )

        self.exec_with_waiting_window(fn, "duplicate_scenario")
        self.changeScenario.emit(self.sender())

    def _close_other_window(self):
        self.parent\
            .parent\
            ._close_sub_window(
                exceptions=["Scenarios", "Debug REPL"]
            )
