# Window.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 logging

from tools import timer, trace

from numpy import sqrt

from View.Tools.PamhyrWindow import PamhyrWindow
from View.Tools.PamhyrWidget import PamhyrWidget
from View.Tools.PamhyrDelegate import PamhyrExTimeDelegate

from PyQt5.QtGui import (
    QKeySequence,
)

from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import (
    Qt, QVariant, QAbstractTableModel, QCoreApplication,
    pyqtSlot, pyqtSignal, QSettings,
)

from PyQt5.QtWidgets import (
    QDialogButtonBox, QPushButton, QLineEdit,
    QFileDialog, QTableView, QAbstractItemView,
    QUndoStack, QShortcut, QAction, QItemDelegate,
    QHeaderView, QDoubleSpinBox, QVBoxLayout,
)

from View.Tools.Plot.PamhyrCanvas import MplCanvas
from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar

from View.BoundaryCondition.Edit.translate import BCETranslate
from View.BoundaryCondition.Edit.UndoCommand import SetMetaDataCommand
from View.BoundaryCondition.Edit.Table import TableModel
from View.BoundaryCondition.Edit.Plot import Plot
from View.BoundaryCondition.Edit.GenerateDialog import GenerateDialog

_translate = QCoreApplication.translate

logger = logging.getLogger()


class WD50Sigma(PamhyrWidget):
    _pamhyr_ui = "d50sigma"

    d50Changed = pyqtSignal(float)
    sigmaChanged = pyqtSignal(float)

    def __init__(self, parent=None):
        super(WD50Sigma, self).__init__(
            parent=parent
        )

        self.spinBox_d50 = self.find(QDoubleSpinBox, "doubleSpinBox_d50")
        self.spinBox_sigma = self.find(QDoubleSpinBox, "doubleSpinBox_sigma")

        self.spinBox_d50.valueChanged.connect(self.valueChangedD50)
        self.spinBox_sigma.valueChanged.connect(self.valueChangedSigma)

    def set_d50(self, d50):
        self.spinBox_d50.valueChanged.disconnect(self.valueChangedD50)
        self.spinBox_d50.setValue(float(d50))
        self.spinBox_d50.valueChanged.connect(self.valueChangedD50)

    def get_d50(self):
        return float(self.spinBox_d50.value())

    def set_sigma(self, sigma):
        self.spinBox_sigma.valueChanged.disconnect(self.valueChangedSigma)
        self.spinBox_sigma.setValue(float(sigma))
        self.spinBox_sigma.valueChanged.connect(self.valueChangedSigma)

    def get_sigma(self):
        return float(self.spinBox_sigma.value())

    @QtCore.pyqtSlot(float)
    def valueChangedD50(self, value):
        self.d50Changed.emit(value)

    @QtCore.pyqtSlot(float)
    def valueChangedSigma(self, value):
        self.sigmaChanged.emit(value)


class EditBoundaryConditionWindow(PamhyrWindow):
    _pamhyr_ui = "EditBoundaryConditions"
    _pamhyr_name = "Edit Boundary Conditions"

    def __init__(self, data=None, study=None, config=None, parent=None):
        self._data = data
        trad = BCETranslate()
        self._long_types = trad.get_dict("long_types")
        self._study = study

        name = trad[self._pamhyr_name]
        if self._data is not None:
            node_name = (self._data.node.name if self._data.node is not None
                         else trad['not_associated'])
            name += (
                f" - {study.name} " +
                f" - {self._data.name} ({self._data.id}) " +
                f"({self._long_types[self._data.bctype]} - {node_name})"
            )

        super(EditBoundaryConditionWindow, self).__init__(
            title=name,
            study=study,
            config=config,
            trad=trad,
            parent=parent
        )

        self._hash_data.append(data)

        self.setup_table()
        self.setup_plot()
        self.setup_data()
        self.setup_connections()
        self.setup_dialog()

    def setup_data(self):
        self._is_solid = self._data.bctype == "SL"

        if self._is_solid:
            layout = self.find(QVBoxLayout, "verticalLayout_table")
            self._d50sigma = WD50Sigma(parent=self)
            layout.addWidget(self._d50sigma)

            self._d50sigma.set_d50(self._data.d50)
            self._d50sigma.set_sigma(self._data.sigma)

    def setup_table(self):
        headers = {}
        table_headers = self._trad.get_dict("table_headers")
        for h in self._data.header:
            headers[h] = table_headers[h]

        if self._study.is_editable():
            editable_headers = self._data.header
        else:
            editable_headers = []

        self._delegate_time = PamhyrExTimeDelegate(
            data=self._data,
            mode=self._study.time_system,
            parent=self
        )

        table = self.find(QTableView, "tableView")
        self._table = TableModel(
            table_view=table,
            table_headers=headers,
            editable_headers=editable_headers,
            delegates={
                # "time": self._delegate_time,
            },
            data=self._data,
            undo=self._undo_stack,
            opt_data=self._study.time_system
        )

        table.setModel(self._table)
        table.setSelectionBehavior(QAbstractItemView.SelectRows)
        table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        table.setAlternatingRowColors(True)

    def setup_plot(self):
        self.canvas = MplCanvas(width=5, height=4, dpi=100)
        self.canvas.setObjectName("canvas")
        self.toolbar = PamhyrPlotToolbar(
            self.canvas, self
        )
        self.verticalLayout.addWidget(self.toolbar)
        self.verticalLayout.addWidget(self.canvas)

        self.plot = Plot(
            canvas=self.canvas,
            data=self._data,
            mode=self._study.time_system,
            trad=self._trad,
            toolbar=self.toolbar,
        )
        self.plot.draw()

    def setup_connections(self):
        if self._study.is_editable():
            self.find(QAction, "action_add").triggered.connect(self.add)
            self.find(QAction, "action_del").triggered.connect(self.delete)
            self.find(QAction, "action_sort").triggered.connect(self.sort)
            self.find(QAction, "action_import").triggered\
                                               .connect(self.import_from_file)

            self.find(QAction, "action_generate_uniform")\
                .triggered.connect(self.generate_uniform)
            self.find(QAction, "action_generate_critical")\
                .triggered.connect(self.generate_critical)
            self.find(QAction, "action_increasing")\
                .triggered.connect(self.make_increasing)

        if self._data.bctype != "ZD" or not self._data.has_node:
            self.find(QAction, "action_generate_uniform").setVisible(False)
            self.find(QAction, "action_generate_critical").setVisible(False)
            self.find(QAction, "action_increasing").setVisible(False)
        else:
            self.find(QAction, "action_generate_uniform").setVisible(True)
            self.find(QAction, "action_generate_critical").setVisible(True)
            self.find(QAction, "action_increasing").setVisible(True)

        self._table.dataChanged.connect(self.update)
        self._table.layoutChanged.connect(self.update)

        if self._is_solid:
            if self._study.is_editable():
                self._d50sigma.d50Changed.connect(self.d50_changed)
                self._d50sigma.sigmaChanged.connect(self.sigma_changed)
            else:
                self._d50sigma.setEnabled(False)

    def setup_dialog(self):
        if len(self._data.reach(self._study.river)) > 0:
            reach = self._data.reach(self._study.river)[0]
            self.slope_value = abs(reach.get_incline_median_mean())
        else:
            self.slope_value = 0.0

    def d50_changed(self, value):
        self._undo_stack.push(
            SetMetaDataCommand(
                self._data,
                "d50", value
            )
        )

    def sigma_changed(self, value):
        self._undo_stack.push(
            SetMetaDataCommand(
                self._data,
                "sigma", value
            )
        )

    def widget_update(self):
        if self._is_solid:
            self._d50sigma.set_d50(self._data.d50)
            self._d50sigma.set_sigma(self._data.sigma)

    def update(self):
        self.plot.update()

    def index_selected_row(self):
        table = self.find(QTableView, "tableView")
        return table.selectionModel()\
                    .selectedRows()[0]\
                    .row()

    def index_selected_rows(self):
        table = self.find(QTableView, "tableView")
        return list(
            # Delete duplicate
            set(
                map(
                    lambda i: i.row(),
                    table.selectedIndexes()
                )
            )
        )

    def add(self):
        rows = self.index_selected_rows()
        if len(self._data) == 0 or len(rows) == 0:
            self._table.add(0)
        else:
            self._table.add(rows[0])

        self.plot.update()

    def delete(self):
        rows = self.index_selected_rows()
        if len(rows) == 0:
            return

        self._table.delete(rows)
        self.plot.update()

    def sort(self):
        self._table.sort(False)
        self.plot.update()

    def import_from_file(self):
        options = QFileDialog.Options()
        settings = QSettings(QSettings.IniFormat,
                             QSettings.UserScope, 'MyOrg', )
        options |= QFileDialog.DontUseNativeDialog

        if self._data.bctype == "TD":
            file_types = [self._trad["file_hyd"]]
        if self._data.bctype == "TZ":
            file_types = [self._trad["file_lim"]]
        if self._data.bctype == "ZD":
            file_types = [self._trad["file_ava"]]
        file_types.append(self._trad["file_all"])

        file_name, _ = QtWidgets.QFileDialog.getOpenFileName(
            self,
            self._trad["open_file"],
            "",
            ";; ".join(file_types),
            options=options
        )

        if file_name != "":
            self._table.read_from_file(file_name, self._data.bctype)

    def move_up(self):
        row = self.index_selected_row()
        self._table.move_up(row)
        self.plot.update()

    def move_down(self):
        row = self.index_selected_row()
        self._table.move_down(row)
        self.plot.update()

    def _copy(self):
        rows = self.index_selected_rows()

        table = []
        table.append(self._data.header)

        data = self._data.data
        for row in rows:
            table.append(list(data[row]))

        self.copyTableIntoClipboard(table)

    def _paste(self):
        header, data = self.parseClipboardTable()

        logger.debug(f"paste: h:{header}, d:{data}")

        if len(data) == 0:
            return

        row = 0
        rows = self.index_selected_rows()
        if len(rows) != 0:
            row = rows[0]

        self._table.paste(row, header, data)
        self.plot.update()

    def _undo(self):
        self._table.undo()
        self.plot.update()
        self.widget_update()

    def _redo(self):
        self._table.redo()
        self.plot.update()
        self.widget_update()

    def generate_uniform(self):
        if self._data.has_node:
            node = self._data.node
            if node is None:
                return
            reach = self._data.reach(self._study.river)[0]
            if len(reach.profiles) == 0:
                self.message_box(
                    window_title=self._trad["Warning"],
                    text=self._trad["title_need_geometry"],
                    informative_text=self._trad["msg_need_geometry"]
                )
                return

            profile = reach.profiles[-1]
            dlg = GenerateDialog(self.slope_value,
                                 reach,
                                 trad=self._trad,
                                 parent=self)
            if dlg.exec():
                self.slope_value = dlg.value
            frictions = reach._parent.frictions.frictions
            z_min = profile.z_min()
            z_max = profile.z_max()
            strickler = None
            for f in frictions:
                if f.contains_rk(profile.rk):
                    strickler = f.get_friction(profile.rk)[0]
            if strickler is None:
                strickler = 25.0
            height = [(i)*(z_max-z_min)/50 for i in range(51)]
            # q = [((profile.wet_width(z_min + h) * 0.8) * strickler
            #      * (h ** (5/3)) * (abs(self.slope_value) ** (0.5)))
            #      for h in height]
            q = [profile.wet_area(z_min + h) * strickler
                 * (profile.wet_radius(z_min + h) ** (2/3))
                 * (abs(self.slope_value) ** (0.5))
                 for h in height]
            for i in range(len(height)):
                height[i] += z_min
            self._table.replace_data(height, q)

        return

    def generate_critical(self):
        if self._data.has_node:
            node = self._data.node
            if node is None:
                return
            reach = self._data.reach(self._study.river)[0]
            if len(reach.profiles) == 0:
                self.message_box(
                    window_title=self._trad["Warning"],
                    text=self._trad["title_need_geometry"],
                    informative_text=self._trad["msg_need_geometry"]
                )
                return

            profile = reach.profiles[-1]
            z_min = profile.z_min()
            z_max = profile.z_max()
            height = [(i)*(z_max-z_min)/50 for i in range(51)]
            q = [sqrt(9.81 * (profile.wet_area(z_min + h) ** 3)
                      / profile.wet_width(z_min + h))
                 for h in height]
            for i in range(len(height)):
                height[i] += z_min
            self._table.replace_data(height, q)
        return

    def make_increasing(self):
        if self._data.has_node:
            node = self._data.node
            if node is None:
                return
        if len(self._table._data) < 2:
            return
        h = [self._data.get_i(0)[0]]
        q = [self._data.get_i(0)[1]]
        for i in range(len(self._table._data)):
            if i == 0:
                continue
            row = self._data.get_i(i)
            if row[1] > q[-1]:
                h.append(row[0])
                q.append(row[1])
        self._table.replace_data(h, q)
