Загрузка данных


# -*- coding: utf-8 -*-
# gui/forecast_window.py

import tkinter as tk
from tkinter import ttk
from datetime import datetime, timedelta
import matplotlib.dates as mdates
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg


class SettingsAccordionPanel(ttk.Frame):
    """Класс для правой панели настроек."""
    def __init__(self, parent, main_window_ref=None):
        super().__init__(parent)
        self.main_window = main_window_ref

        s = ttk.Style()
        # Стиль делает табы похожими на кнопки слева
        s.layout('Accordion.TNotebook.Tab', [
            ('Accordion.Toolbutton', {'children': [('Label', {'side': 'left'})], 'sticky': 'nswe'})
        ])
        s.configure('Accordion.TNotebook', tabposition='wn')
        s.map('Accordion.TNotebook', background=[('selected', '!focus', '#f0f0f0')])
        s.configure('Accordion.TNotebook.Tab', padding=[12, 6, 12, 6], relief="flat") # <-- Выравнивание заголовков

        self.frame = ttk.Notebook(self, style='Accordion.TNotebook')
        self.frame.pack(expand=1, fill="both")

        for title in ["Общие настройки", "Настройки симуляции", "Настройки агрегирования",
                      "Настройка статистики", "Настройка печати"]:
            frame = ttk.Frame(self.frame, padding=10)
            self.frame.add(frame, text=title)
        
        # --- Виджеты ТОЛЬКО для "Настроек симуляции" ---
        sim_tab = self.frame.tabs()[1]
        sim_frame = self.frame.nametowidget(sim_tab)
        sim_frame.columnconfigure(0, weight=3)
        sim_frame.columnconfigure(1, weight=2)

        row_num = 0
        ttk.Label(sim_frame, text="Интенсивность событий (в день):").grid(row=row_num, column=0, sticky="e", padx=(0,5), pady=2)
        self.lambda_entry = ttk.Entry(sim_frame)
        self.lambda_entry.insert(0, "5")
        self.lambda_entry.grid(row=row_num, column=1, sticky="we", pady=2)
        row_num += 1

        ttk.Label(sim_frame, text="Шаг (дни):").grid(row=row_num, column=0, sticky="e", padx=(0,5), pady=2)
        self.step_entry = ttk.Entry(sim_frame)
        self.step_entry.insert(0, "1")
        self.step_entry.grid(row=row_num, column=1, sticky="we", pady=2)
        row_num += 1

        period_frame = ttk.Frame(sim_frame)
        period_frame.grid(row=row_num, column=0, columnspan=2, sticky="we", pady=2)
        ttk.Label(period_frame, text="Прогноз на:").pack(side="left", anchor="w", padx=(0, 5))
        self.period_combobox = ttk.Combobox(period_frame, state="readonly",
                                          values=["Месяц (30 дней)", "Квартал (90 дней)", "Полгода (180 дней)"],
                                          width=18)
        self.period_combobox.current(0)
        self.period_combobox.pack(side="left", anchor="w")
        row_num += 1

        ttk.Label(sim_frame, text="Кол-во итераций:\n(в симуляции)").grid(row=row_num, column=0, sticky="ne", pady=2)
        self.iterations_entry = ttk.Entry(sim_frame)
        self.iterations_entry.insert(0, "10000")
        self.iterations_entry.grid(row=row_num, column=1, sticky="we", pady=2)
        row_num += 1

        # Кнопка "Рассчитать"
        self.build_button = ttk.Button(sim_frame, text="Рассчитать", command=self.on_build_button_clicked)
        self.build_button.grid(row=row_num, column=0, columnspan=2, sticky="we", pady=20, ipadx=10, ipady=5)

    def on_build_button_clicked(self):
        if self.main_window is not None:
            self.main_window.run_forecast()


class ResultsPanel(ttk.Frame):
    """Компонент для нижней части окна: результаты и кнопки."""
    def __init__(self, parent, height):
        super().__init__(parent)
        self.frame = ttk.Frame(self)
        self.frame.pack(fill="x")

        result_frame = ttk.Frame(self.frame)
        result_frame.pack(side="left", fill="both", expand=True)

        self.result_text = tk.Text(result_frame, height=height, wrap='word', state='disabled')
        scrollbar = ttk.Scrollbar(result_frame, command=self.result_text.yview)
        scrollbar.pack(side="right", fill="y")
        self.result_text.configure(yscrollcommand=scrollbar.set)
        self.result_text.pack(padx=10, pady=5, fill="x", expand=True)

        button_frame = ttk.Frame(self.frame)
        button_frame.pack(side="right", padx=10)

        self.calculate_btn = ttk.Button(button_frame, text="Рассчитать", state="disabled")
        self.print_btn = ttk.Button(button_frame, text="Печатать в файл")

        self.calculate_btn.pack(pady=5, ipadx=10, side="top")
        self.print_btn.pack(pady=5, ipadx=10, side="top")

    def set_calculate_command(self, command):
        self.calculate_btn.config(command=command)
        self.calculate_btn.config(state="normal")


class ForecastWindow:
    """Главное окно приложения."""
    def __init__(self, parent):
        self.root_window = parent
        self.window = tk.Toplevel(parent)
        self.window.title("Прогнозирование")
        self.window.geometry("1050x700")
        self.window.resizable(True, True)

        # --- ГЛАВНЫЙ КОНТЕЙНЕР ---
        main_container = ttk.Frame(self.window)
        main_container.pack(fill="both", expand=True)

        main_container.grid_rowconfigure(0, weight=4) # Верх тяжелее
        main_container.grid_rowconfigure(1, weight=1) # Низ легче

        # --- ВЕРХНИЙ БЛОК: График и Настройки рядом ---
        top_block = ttk.Frame(main_container)
        top_block.grid(row=0, column=0, sticky="nsew", padx=5, pady=(5, 0)) 

        # Конфигурация сетки верхнего блока: две колонки
        top_block.grid_columnconfigure(0, weight=3) # Колонка графика шире
        top_block.grid_columnconfigure(1, weight=2) # Колонка настроек уже

        # 1. Левая панель: График
        graph_frame = ttk.LabelFrame(top_block, text="График прогноза", padding="10")
        graph_frame.grid(row=0, column=0, sticky="nswe") 

        self.figure = Figure(figsize=(5, 4), dpi=100)
        self.ax = self.figure.add_subplot(111)
        self.canvas = FigureCanvasTkAgg(self.figure, master=graph_frame)
        self.canvas.get_tk_widget().pack(fill="both", expand=True)

        # 2. Правая панель: Аккордеон
        self.settings_accordion = SettingsAccordionPanel(top_block, main_window_ref=self)
        self.settings_accordion.grid(row=0, column=1, sticky="nswe", padx=(10, 0)) 

        # Принудительное обновление геометрии
        self.window.update_idletasks()

        # --- НИЖНИЙ БЛОК: Результаты расчета и Кнопки ---
        self.results_panel = ResultsPanel(main_container, height=4)
        self.results_panel.frame.grid(row=1, column=0, sticky="ew", pady=(5, 5))

        # Связываем кнопку из аккордеона с полем результатов
        self.results_panel.set_calculate_command(self.settings_accordion.on_build_button_clicked)

        # Устанавливаем минимальный размер окна
        min_width = max(1050, self.window.winfo_reqwidth())
        min_height = max(650, self.window.winfo_reqheight())
        self.window.minsize(min_width, min_height)

    # ... остальные методы остаются без изменений ...
    def _insert_into_result(self, text):
        self.results_panel.result_text.config(state='normal')
        self.results_panel.result_text.insert(tk.END, text + "\n")
        self.results_panel.result_text.config(state='disabled')
        self.results_panel.result_text.see(tk.END)

    def _clear_result_text(self):
        self.results_panel.result_text.config(state='normal')
        self.results_panel.result_text.delete(1.0, tk.END)
        self.results_panel.result_text.config(state='disabled')

    def run_forecast(self):
        try:
            iterations = int(self.settings_accordion.iterations_entry.get())
            lambda_val = float(self.settings_accordion.lambda_entry.get())
            selected_period = self.settings_accordion.period_combobox.get()
            days_forward_dict = {"Месяц": 30, "Квартал": 90, "Полгода": 180}
            period_key = next((k for k in days_forward_dict if k in selected_period), None)
            days_forward = days_forward_dict.get(period_key, 30)
            step_days = int(self.settings_accordion.step_entry.get())
            start_date = datetime.now().date()

            from modules.forecast_data_processing import generate_trajectory
            trajectory, daily_values_history = generate_trajectory(lambda_val, days_forward, step_days)
            if not trajectory:
                self._insert_into_result("Не удалось сгенерировать данные.")
                return

            from modules.forecast_modeling import monte_carlo_forecast
            sim_result = monte_carlo_forecast(
                pool_values=trajectory,
                num_simulations=iterations,
                p1=0.05,
                p2=0.95
            )
            final_lower = sim_result["lower_bound"]
            final_upper = sim_result["upper_bound"]
            final_median = sim_result["median"]

            output_text = (
                f"ПРОГНОЗ ДОСТИЖЕНИЯ ЦЕЛИ:\\n"
                f" Цель (Нижняя граница): {final_lower:.2f}\\n\\n"
                f" Прогнозируемый диапазон значений:\\n"
                f" Минимум: {final_lower:.2f}\\n"
                f" Максимум: {final_upper:.2f}\\n"
            )
            date_of_intersection = None
            for idx, val in enumerate(daily_values_history):
                if val >= final_lower:
                    date_of_intersection = start_date + timedelta(days=idx)
                    break
            if date_of_intersection:
                output_text += f"Достижение минимума прогнозируется: {date_of_intersection.strftime('%d.%m.%Y')}"
            else:
                output_text += "Достижение минимума в заданный период не прогнозируется."

            from modules.forecast_visualization import GraphUpdater
            graph_updater = GraphUpdater(start_date, step_days)
            plot_data = graph_updater.prepare_plot_data(sim_result, trajectory, daily_values_history)

            self._update_results_text(output_text)
            self._draw_graph(plot_data)

        except Exception as e:
            print(e)
            self._insert_into_result(f"ОШИБКА: {e}")

    def _update_results_text(self, output_text):
        self._clear_result_text()
        self._insert_into_result(output_text)

    def _draw_graph(self, plot_data):
        self.ax.clear()
        self.ax.axhline(y=plot_data["lower_bound"], color='red', linestyle='--', label=f'Нижняя граница')
        self.ax.axhline(y=plot_data["median"], color='green', linestyle='-', label=f'Медиана')
        self.ax.axhline(y=plot_data["upper_bound"], color='blue', linestyle='--', label=f'Верхняя граница')
        if plot_data["has_trajectory"]:
            self.ax.plot(plot_data["dates"], plot_data["trajectory"],
                         color='orange', marker='o', linewidth=2, markersize=6, label='Смоделированная динамика')
        self.ax.xaxis.set_major_formatter(mdates.DateFormatter('%d.%m'))
        self.ax.xaxis.set_tick_params(rotation=45)
        self.ax.set_xticks(plot_data["dates"])
        if plot_data["intersection_date"] and plot_data["has_trajectory"]:
            x_coord = plot_data["dates"][plot_data["intersection_x_idx"]]
            y_coord = plot_data["intersection_y_val"]
            self.ax.scatter(x_coord, y_coord, color='darkred', s=150,
                            zorder=5, edgecolor='black', linewidth=1, alpha=0.7)
            self.ax.annotate(f'{plot_data["intersection_date"].strftime("%d.%m")}',
                             xy=(x_coord, y_coord),
                             xytext=(5, 5), textcoords="offset points",
                             fontsize=8, backgroundcolor='white')
        handles, labels = self.ax.get_legend_handles_labels()
        if handles:
            self.ax.legend(loc='upper left')
        self.canvas.draw()