import sys
from PyQt6.QtWidgets import (
    QMessageBox,
    QMainWindow,
    QLabel,
    QComboBox,
    QPushButton,
    QFormLayout,
    QWidget,
    QLineEdit,
    QHBoxLayout,
    QLayout,
    QApplication,
)
from PyQt6.QtCore import Qt
import numpy as np

import task
import algorithm
import solution


class DisplayField(QWidget):
    label_text: str

    def __init__(self, parent, label_text="", text=""):
        super().__init__(parent)

        self._layout = QHBoxLayout(self)
        self._layout.setContentsMargins(0, 0, 0, 0)

        self.label_text = label_text
        self.text = QLabel(f"{self.label_text}{text}")
        self.text.setAlignment(Qt.AlignmentFlag.AlignLeft)

        self._layout.addWidget(self.text)

    def set_visible(self, visible):
        self.text.setVisible(visible)

    def set_text(self, text):
        self.text.setText(f"{self.label_text}{text}")


class InputField(QWidget):
    value: str
    change_func: callable = None

    def __init__(
        self,
        parent,
        label_text="",
        placeholder_text="",
        lineedit_text="",
        label_position="left",
    ):
        super().__init__(parent)

        self._layout = QHBoxLayout(self)
        self._layout.setContentsMargins(0, 0, 0, 0)
        self.label = QLabel(label_text)
        self.lineedit = QLineEdit(lineedit_text)
        self.lineedit.setPlaceholderText(placeholder_text)
        self.lineedit.setMinimumWidth(50)

        if label_position == "left":
            self._layout.addWidget(self.label)
            self._layout.addWidget(self.lineedit)
        elif label_position == "right":
            self._layout.addWidget(self.lineedit)
            self._layout.addWidget(self.label)
        elif label_position == "none":
            self._layout.addWidget(self.lineedit)

        self.value = self.get_value()
        self.lineedit.textChanged.connect(self._on_change)

    def get_value(self):
        return self.lineedit.text()

    def set_value(self, value):
        self.lineedit.setText(value)

    def on_change(self, func):
        self.change_func = func

    def _on_change(self):
        if self.change_func:
            self.change_func(self)

        self.value = self.get_value()


def on_input_float_number(widget: InputField):
    try:
        str_value = widget.get_value()
        if not (str_value == "" or str_value == "-"):
            widget.value = str(float(str_value))
    except:
        widget.set_value(str(widget.value))


def on_input_int_number(widget: InputField):
    try:
        str_value = widget.get_value()
        if not (str_value == "" or str_value == "-"):
            widget.value = str(int(str_value))
    except:
        widget.set_value(str(widget.value))


class MatrixRowWidget(QWidget):
    x_inputs: list[InputField]
    b_input: InputField

    def __init__(self, parent, i: int, n: int):
        super().__init__(parent)

        self._layout = QHBoxLayout()
        self._layout.setContentsMargins(0, 0, 0, 0)
        self.x_inputs = []
        for j in range(n):
            inp = InputField(self, f"x{j + 1}", "", "0.0", "right")
            inp.lineedit.setAlignment(Qt.AlignmentFlag.AlignRight)
            self.x_inputs.append(inp)
            self._layout.addWidget(inp)
            if j != n - 1:
                self._layout.addWidget(QLabel(" + "))

        self._layout.addWidget(QLabel(" = "))
        self.b_input = InputField(self, "", "", "0.0", "none")
        self.b_input.lineedit.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self._layout.addWidget(self.b_input)

        self.setLayout(self._layout)

    def get_value_x(self, i: int):
        return self.x_inputs[i].get_value()

    def get_value_b(self):
        return self.b_input.get_value()

    def on_change(self, func):
        for inp in self.x_inputs:
            inp.on_change(func)

        self.b_input.on_change(func)


class MatrixWidget(QWidget):
    rows: list[MatrixRowWidget]
    _layout: QFormLayout
    on_change_func = None

    def __init__(self, parent, n: int):
        super().__init__(parent)

        self._initialize()
        self._create(n)

    def _initialize(self):
        self.rows = []
        self._layout = QFormLayout()
        self._layout.setContentsMargins(0, 0, 0, 0)

    def _create(self, n: int):
        for i in range(n):
            self.rows.append(MatrixRowWidget(self, i, n))

        for row in self.rows:
            self._layout.addRow(row)
            if self.on_change_func:
                row.on_change(self.on_change_func)

        self.setLayout(self._layout)

    def recreate(self, n: int):
        for row in self.rows:
            row.deleteLater()

        self.rows = []
        self._create(n)

    def set_value_x(self, i: int, j: int, value: float):
        self.rows[i].x_inputs[j].set_value(value)

    def set_value_b(self, i: int, value: float):
        self.rows[i].b_input.set_value(value)

    def get_value_x(self, i: int, j: int):
        return self.rows[i].get_value_x(j)

    def get_value_b(self, i: int):
        return self.rows[i].get_value_b()

    def on_change(self, func):
        self.on_change_func = func

        for row in self.rows:
            if self.on_change_func:
                row.on_change(self.on_change_func)


class MainWindow(QMainWindow):
    initialized = False

    matrix_a: np.matrix
    matrix_b: np.matrix

    det_display: DisplayField
    cond_display: DisplayField

    def __init__(self):
        super().__init__()

        self.setupUI()

        self.initialized = True
        self.update_stats()

    def setupUI(self):
        self.setWindowTitle("Лабораторная работа №3")
        self.central_widget = QWidget()
        self.setCentralWidget(self.central_widget)

        layout = QFormLayout()

        label1 = QLabel("Вариант 6", self)
        layout.addWidget(label1)

        self.n_input = InputField(self, " n: ", "число строк", str(4), "left")
        self.n_input.on_change(lambda w: self.on_n_input_changed(w))
        layout.addWidget(self.n_input)

        buttons_row = QWidget()
        buttons_row_layout = QHBoxLayout()
        buttons_row_layout.setContentsMargins(0, 0, 0, 0)

        self.reset_button = QPushButton("Сбросить", self)
        self.reset_button.clicked.connect(self.on_reset_button_clicked)
        buttons_row_layout.addWidget(self.reset_button)

        self.paste_button = QPushButton("Вставить", self)
        self.paste_button.clicked.connect(self.on_paste_button_clicked)
        buttons_row_layout.addWidget(self.paste_button)

        buttons_row.setLayout(buttons_row_layout)
        layout.addWidget(buttons_row)

        self.matrix = MatrixWidget(self, 4)
        self.matrix.on_change(lambda w: self.on_matrix_changed(w))
        self.set_matrix(task.A(), task.B())
        layout.addWidget(self.matrix)

        self.det_display = DisplayField(self, "Определитель: ", "")
        layout.addWidget(self.det_display)

        self.cond_display = DisplayField(self, "Число обусловленности: ", "")
        layout.addWidget(self.cond_display)

        find_solution_button = QPushButton("Решить", self)
        find_solution_button.clicked.connect(self.on_find_solution_clicked)
        layout.addWidget(find_solution_button)

        self.solution_display = DisplayField(self, "Решение: ", "")
        layout.addWidget(self.solution_display)
        self.solution_display.set_visible(False)

        self.central_widget.setLayout(layout)

    def on_find_solution_clicked(self):
        x = solution.iterative_method(self.matrix_a, self.matrix_b)

        if x is None:
            self.solution_display.set_visible(True)
            self.solution_display.set_text("Нет решения")
        else:
            self.solution_display.set_visible(True)
            solution_text = "\n"
            for i in range(x.shape[0]):
                solution_text += f"x{i} = {x[i, 0]:.5f}"
                if i < x.shape[0] - 1:
                    solution_text += "\n"
            self.solution_display.set_text(solution_text)

    def on_paste_button_clicked(self):
        text = QApplication.clipboard().text()
        try:
            input_np = np.fromstring(text, dtype=np.float64, sep=" ")

            n = 0.5 * (np.sqrt(4.0 * input_np.shape[0] + 1.0) - 1.0)
            if n % 1.0 != 0.0:
                return
            n = int(n)

            input_np = np.reshape(input_np, (n, n + 1))

            self.n_input.set_value(str(n))

            self.matrix_a = np.copy(input_np[:n, :n])
            self.matrix_b = np.copy(input_np[:n, n:])

            self.set_matrix(self.matrix_a, self.matrix_b)

        except Exception as e:
            print(e)
            return

    def on_n_input_changed(self, widget: InputField):
        MAX_N = 8

        on_input_int_number(widget)
        try:
            n = int(widget.get_value())
            if n < 1:
                widget.set_value(str(1))
                return
            if n > MAX_N:
                widget.set_value(str(MAX_N))
                return

            self.matrix.recreate(int(self.n_input.get_value()))
            self.input_matrix_from_widget()
            self.update_stats()
        except:
            return

    def on_reset_button_clicked(self):
        self.n_input.set_value(str(4))
        self.set_matrix(task.A(), task.B())

    def on_matrix_changed(self, widget: InputField):
        on_input_float_number(widget)

        try:
            self.input_matrix_from_widget()
            self.update_stats()
        except:
            return

    def input_matrix_from_widget(self):
        try:
            self.matrix_a = np.matrix(
                [
                    [
                        self.matrix.get_value_x(i, j)
                        for j in range(self.matrix.rows[0].x_inputs.__len__())
                    ]
                    for i in range(self.matrix.rows.__len__())
                ],
                dtype=np.float64,
            )
            self.matrix_b = np.matrix(
                [
                    [self.matrix.get_value_b(i)]
                    for i in range(self.matrix.rows.__len__())
                ],
                dtype=np.float64,
            )
        except:
            return

    def update_stats(self):
        if not self.initialized:
            return

        self.solution_display.set_visible(False)

        try:
            self.det_display.set_text(f"{algorithm.det(self.matrix_a):.5f}")
            self.cond_display.set_text(f"{algorithm.cond(self.matrix_a):.5f}")
        except:
            self.det_display.set_text("Невозможно вычислить")
            self.cond_display.set_text("Невозможно вычислить")

    def set_matrix(self, A: np.matrix, B: np.matrix):
        for i in range(A.shape[0]):
            for j in range(A.shape[1]):
                self.matrix.set_value_x(i, j, str(A[i, j]))

        for i in range(B.shape[0]):
            self.matrix.set_value_b(i, str(B[i, 0]))

        self.update_stats()
