import numpy as np
import tkinter as tk
from tkinter import messagebox
from typing import List, Tuple


class TransportProblemApp:
    def __init__(self, root: tk.Tk) -> None:
        """
        Инициализация приложения.
        
        :param root: Основное окно приложения (Tkinter).
        """
        self.root: tk.Tk = root
        self.root.title("Транспортная задача")

        self.suppliers: int = 3
        self.consumers: int = 3

        self.setup_ui()

    def setup_ui(self) -> None:
        """
        Создание интерфейса приложения, включая поля для ввода данных и кнопки.
        """
        self.frame_controls: tk.Frame = tk.Frame(self.root)
        self.frame_controls.pack(pady=10)

        tk.Label(self.frame_controls, text="Количество поставщиков:").grid(row=0, column=0)
        self.entry_suppliers: tk.Entry = tk.Entry(self.frame_controls, width=5)
        self.entry_suppliers.insert(0, str(self.suppliers))
        self.entry_suppliers.grid(row=0, column=1)

        tk.Label(self.frame_controls, text="Количество потребителей:").grid(row=0, column=2)
        self.entry_consumers: tk.Entry = tk.Entry(self.frame_controls, width=5)
        self.entry_consumers.insert(0, str(self.consumers))
        self.entry_consumers.grid(row=0, column=3)

        tk.Button(self.frame_controls, text="Применить", command=self.update_table).grid(row=0, column=4, padx=10)

        self.frame_table: tk.Frame = tk.Frame(self.root)
        self.frame_table.pack(pady=10)
        self.update_table()

        self.frame_buttons: tk.Frame = tk.Frame(self.root)
        self.frame_buttons.pack(pady=10)

        tk.Button(self.frame_buttons, text="Рассчитать", command=self.solve).pack()

    def update_table(self) -> None:
        """
        Обновление таблицы для ввода данных о поставщиках, потребителях, запасах и потребностях.
        """
        self.suppliers = int(self.entry_suppliers.get())
        self.consumers = int(self.entry_consumers.get())

        for widget in self.frame_table.winfo_children():
            widget.destroy()

        self.entries_costs: List[List[tk.Entry]] = []
        self.entries_supplies: List[tk.Entry] = []
        self.entries_demands: List[tk.Entry] = []

        # Заголовок "Пункт назначения"
        tk.Label(self.frame_table, text="Пункт назначения").grid(row=0, column=1, columnspan=self.consumers)
        tk.Label(self.frame_table, text="Пункт отправления").grid(row=1, column=0)
        for j in range(self.consumers):
            tk.Label(self.frame_table, text=f"B{j+1}").grid(row=1, column=j+1)
        tk.Label(self.frame_table, text="Запас груза").grid(row=1, column=self.consumers+1)

        # Данные для поставщиков
        for i in range(self.suppliers):
            tk.Label(self.frame_table, text=f"A{i+1}").grid(row=i+2, column=0)
            row_entries: List[tk.Entry] = []
            for j in range(self.consumers):
                entry: tk.Entry = tk.Entry(self.frame_table, width=5)
                entry.grid(row=i+2, column=j+1)
                row_entries.append(entry)
            self.entries_costs.append(row_entries)

            entry_supply: tk.Entry = tk.Entry(self.frame_table, width=5)
            entry_supply.grid(row=i+2, column=self.consumers+1)
            self.entries_supplies.append(entry_supply)

        # Данные для потребителей
        tk.Label(self.frame_table, text="Потребность в грузе").grid(row=self.suppliers+2, column=0)
        for j in range(self.consumers):
            entry_demand: tk.Entry = tk.Entry(self.frame_table, width=5)
            entry_demand.grid(row=self.suppliers+2, column=j+1)
            self.entries_demands.append(entry_demand)

    def solve(self) -> None:
        """
        Решение транспортной задачи методом минимальных элементов.
        Вычисляет и отображает результаты.
        """
        try:
            supplies: np.ndarray = np.array([int(entry.get()) for entry in self.entries_supplies])
            demands: np.ndarray = np.array([int(entry.get()) for entry in self.entries_demands])
            costs: np.ndarray = np.array([[int(entry.get()) for entry in row] for row in self.entries_costs], dtype=float)

            if supplies.sum() != demands.sum():
                messagebox.showerror("Ошибка", "Сумма запасов должна равняться сумме потребностей.")
                return

            # Создание копии массива для расчета итоговой стоимости
            original_costs: np.ndarray = costs.copy()

            allocation: np.ndarray = self.minimum_cost_method(supplies, demands, costs)
            total_cost, abs_errors = self.calculate_results(allocation, original_costs)

            self.show_results(allocation, total_cost, abs_errors)

        except ValueError:
            messagebox.showerror("Ошибка", "Все значения должны быть целыми числами.")

    def minimum_cost_method(self, supplies: np.ndarray, demands: np.ndarray, costs: np.ndarray) -> np.ndarray:
        """
        Метод минимальных элементов для решения транспортной задачи.
        
        :param supplies: Запасы для поставщиков.
        :param demands: Потребности для потребителей.
        :param costs: Стоимость перевозки между поставщиками и потребителями.
        :return: Распределение грузов между поставщиками и потребителями.
        """
        allocation: np.ndarray = np.zeros_like(costs)

        while supplies.sum() > 0 and demands.sum() > 0:
            min_index: Tuple[int, int] = np.unravel_index(np.argmin(costs, axis=None), costs.shape)  # type: ignore
            i, j = min_index

            qty: int = min(supplies[i], demands[j])
            allocation[i, j] = qty

            supplies[i] -= qty
            demands[j] -= qty

            costs[i, j] = np.inf  # Исключить обработанную клетку

        return allocation

    def calculate_results(self, allocation: np.ndarray, costs: np.ndarray) -> Tuple[float, np.ndarray]:
        """
        Подсчет общей стоимости перевозок и абсолютных погрешностей для решения задачи.
        
        :param allocation: Распределение грузов между поставщиками и потребителями.
        :param costs: Стоимость перевозки между поставщиками и потребителями.
        :return: Общая стоимость перевозок и массив абсолютных погрешностей.
        """
        # Замена np.inf на 0 для расчётов
        costs_no_inf: np.ndarray = np.where(costs == np.inf, 0, costs)
        total_cost: float = np.sum(allocation * costs_no_inf)
        abs_errors: np.ndarray = allocation * 0.05  # 5% допустимая погрешность
        return total_cost, abs_errors

    def show_results(self, allocation: np.ndarray, total_cost: float, abs_errors: np.ndarray) -> None:
        """
        Отображение результатов решения задачи.
        
        :param allocation: Распределение грузов между поставщиками и потребителями.
        :param total_cost: Общая стоимость перевозок.
        :param abs_errors: Абсолютные погрешности для каждого элемента распределения.
        """
        results_window: tk.Toplevel = tk.Toplevel(self.root)
        results_window.title("Результаты")

        tk.Label(results_window, text="Распределение грузов:").pack(pady=5)
        allocation_text: tk.Text = tk.Text(results_window, height=10, width=50)
        allocation_text.insert(tk.END, str(allocation))
        allocation_text.config(state=tk.DISABLED)
        allocation_text.pack()

        tk.Label(results_window, text=f"Общая стоимость перевозок: {total_cost}").pack(pady=5)

        tk.Label(results_window, text="Абсолютные погрешности:").pack(pady=5)
        errors_text: tk.Text = tk.Text(results_window, height=10, width=50)
        errors_text.insert(tk.END, str(abs_errors))
        errors_text.config(state=tk.DISABLED)
        errors_text.pack()


if __name__ == "__main__":
    root: tk.Tk = tk.Tk()
    app: TransportProblemApp = TransportProblemApp(root)
    root.mainloop()