import argparse
import os
import sys
import torch
import torch.backends
from exp.exp_long_term_forecasting import Exp_Long_Term_Forecast
from exp.exp_imputation import Exp_Imputation
from exp.exp_short_term_forecasting import Exp_Short_Term_Forecast
from exp.exp_anomaly_detection import Exp_Anomaly_Detection
from exp.exp_classification import Exp_Classification
from exp.exp_zero_shot_forecasting import Exp_Zero_Shot_Forecast
from utils.print_args import print_args
import random
import numpy as np
import threading
import queue
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import matplotlib
matplotlib.use('TkAgg')
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import re
import io
from contextlib import redirect_stdout

class QueueWriter:
    def __init__(self, queue):
        self.queue = queue
    
    def write(self, text):
        self.queue.put(text)
    
    def flush(self):
        pass

class TrainingThread(threading.Thread):
    def __init__(self, args, log_queue, loss_queue, dataset_info_queue):
        super().__init__()
        self.args = args
        self.log_queue = log_queue
        self.loss_queue = loss_queue
        self.dataset_info_queue = dataset_info_queue
        self.daemon = True
        
    def run(self):
        # 重定向stdout到队列
        original_stdout = sys.stdout
        queue_writer = QueueWriter(self.log_queue)
        sys.stdout = queue_writer
        
        try:
            fix_seed = 2021
            random.seed(fix_seed)
            torch.manual_seed(fix_seed)
            np.random.seed(fix_seed)
            
            # 设备设置
            if torch.cuda.is_available() and self.args.use_gpu:
                self.args.device = torch.device('cuda:{}'.format(self.args.gpu))
            else:
                if hasattr(torch.backends, "mps"):
                    self.args.device = torch.device("mps") if torch.backends.mps.is_available() else torch.device("cpu")
                else:
                    self.args.device = torch.device("cpu")
            
            if self.args.use_gpu and self.args.use_multi_gpu:
                self.args.devices = self.args.devices.replace(' ', '')
                device_ids = self.args.devices.split(',')
                self.args.device_ids = [int(id_) for id_ in device_ids]
                self.args.gpu = self.args.device_ids[0]
            
            # 选择实验类
            if self.args.task_name == 'long_term_forecast':
                Exp = Exp_Long_Term_Forecast
            elif self.args.task_name == 'short_term_forecast':
                Exp = Exp_Short_Term_Forecast
            elif self.args.task_name == 'imputation':
                Exp = Exp_Imputation
            elif self.args.task_name == 'anomaly_detection':
                Exp = Exp_Anomaly_Detection
            elif self.args.task_name == 'classification':
                Exp = Exp_Classification
            elif self.args.task_name == 'zero_shot_forecast':
                Exp = Exp_Zero_Shot_Forecast
            else:
                Exp = Exp_Long_Term_Forecast
            
            # 创建实验实例
            exp = Exp(self.args)
            
            # 获取数据集信息
            try:
                train_data, train_loader = exp._get_data(flag='train')
                test_data, test_loader = exp._get_data(flag='test')
                val_data, val_loader = exp._get_data(flag='val')
                
                train_size = len(train_data)
                test_size = len(test_data)
                val_size = len(val_data) if val_data else 0
                
                dataset_info = {
                    'train_size': train_size,
                    'test_size': test_size,
                    'val_size': val_size,
                    'train_batches': len(train_loader),
                    'test_batches': len(test_loader),
                    'val_batches': len(val_loader) if val_loader else 0
                }
                self.dataset_info_queue.put(dataset_info)
            except Exception as e:
                self.log_queue.put(f"获取数据集信息失败: {str(e)}\n")
            
            if self.args.is_training:
                for ii in range(self.args.itr):
                    setting = '{}_{}_{}_{}_ft{}_sl{}_ll{}_pl{}_dm{}_nh{}_el{}_dl{}_df{}_expand{}_dc{}_fc{}_eb{}_dt{}_{}_{}'.format(
                        self.args.task_name,
                        self.args.model_id,
                        self.args.model,
                        self.args.data,
                        self.args.features,
                        self.args.seq_len,
                        self.args.label_len,
                        self.args.pred_len,
                        self.args.d_model,
                        self.args.n_heads,
                        self.args.e_layers,
                        self.args.d_layers,
                        self.args.d_ff,
                        self.args.expand,
                        self.args.d_conv,
                        self.args.factor,
                        self.args.embed,
                        self.args.distil,
                        self.args.des, ii)
                    
                    print(f'开始训练: {setting}')
                    
                    # 训练过程
                    exp.train(setting)
                    
                    print(f'开始测试: {setting}')
                    exp.test(setting)
                    
                    if self.args.gpu_type == 'mps':
                        torch.backends.mps.empty_cache()
                    elif self.args.gpu_type == 'cuda':
                        torch.cuda.empty_cache()
            
            print("训练完成！")
            
        except Exception as e:
            self.log_queue.put(f"训练过程中出错: {str(e)}\n")
            import traceback
            self.log_queue.put(traceback.format_exc() + "\n")
        finally:
            sys.stdout = original_stdout


class TrainingGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("时间序列预测模型训练界面")
        self.root.geometry("1400x900")
        
        # 训练线程
        self.training_thread = None
        self.is_training = False
        
        # 队列
        self.log_queue = queue.Queue()
        self.loss_queue = queue.Queue()
        self.dataset_info_queue = queue.Queue()
        
        # Loss数据
        self.train_losses = []
        self.val_losses = []
        self.test_losses = []
        self.epochs = []
        
        self.create_widgets()
        self.process_queue()
        
    def create_widgets(self):
        # 创建主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)
        main_frame.columnconfigure(1, weight=1)
        main_frame.rowconfigure(1, weight=1)
        
        # 左侧参数面板
        left_frame = ttk.Frame(main_frame)
        left_frame.grid(row=0, column=0, rowspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 10))
        left_frame.columnconfigure(1, weight=1)
        
        # 创建滚动框架
        canvas = tk.Canvas(left_frame, width=400, height=700)
        scrollbar = ttk.Scrollbar(left_frame, orient="vertical", command=canvas.yview)
        scrollable_frame = ttk.Frame(canvas)
        
        scrollable_frame.bind(
            "<Configure>",
            lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
        )
        
        canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)
        
        canvas.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
        left_frame.rowconfigure(0, weight=1)
        
        # 参数输入
        self.params = {}
        row = 0
        
        # 基本配置
        ttk.Label(scrollable_frame, text="基本配置", font=('Arial', 12, 'bold')).grid(row=row, column=0, columnspan=2, pady=10, sticky=tk.W)
        row += 1
        
        self.create_param_row(scrollable_frame, "任务类型 (task_name)", "long_term_forecast", row)
        row += 1
        self.create_param_row(scrollable_frame, "模型 (model)", "TimesNet", row)
        row += 1
        self.create_param_row(scrollable_frame, "模型ID (model_id)", "test", row)
        row += 1
        
        # 数据配置
        ttk.Label(scrollable_frame, text="数据配置", font=('Arial', 12, 'bold')).grid(row=row, column=0, columnspan=2, pady=10, sticky=tk.W)
        row += 1
        
        self.create_param_row(scrollable_frame, "数据集 (data)", "ETTh1", row)
        row += 1
        self.create_param_row(scrollable_frame, "数据路径 (root_path)", "./dataset/ETT-small/", row)
        row += 1
        self.create_param_row(scrollable_frame, "数据文件 (data_path)", "ETTh1.csv", row)
        row += 1
        self.create_param_row(scrollable_frame, "特征类型 (features)", "M", row)
        row += 1
        self.create_param_row(scrollable_frame, "频率 (freq)", "h", row)
        row += 1
        
        # 预测配置
        ttk.Label(scrollable_frame, text="预测配置", font=('Arial', 12, 'bold')).grid(row=row, column=0, columnspan=2, pady=10, sticky=tk.W)
        row += 1
        
        self.create_param_row(scrollable_frame, "序列长度 (seq_len)", "96", row, int)
        row += 1
        self.create_param_row(scrollable_frame, "标签长度 (label_len)", "48", row, int)
        row += 1
        self.create_param_row(scrollable_frame, "预测长度 (pred_len)", "96", row, int)
        row += 1
        
        # 模型配置
        ttk.Label(scrollable_frame, text="模型配置", font=('Arial', 12, 'bold')).grid(row=row, column=0, columnspan=2, pady=10, sticky=tk.W)
        row += 1
        
        self.create_param_row(scrollable_frame, "模型维度 (d_model)", "512", row, int)
        row += 1
        self.create_param_row(scrollable_frame, "注意力头数 (n_heads)", "8", row, int)
        row += 1
        self.create_param_row(scrollable_frame, "编码器层数 (e_layers)", "2", row, int)
        row += 1
        self.create_param_row(scrollable_frame, "解码器层数 (d_layers)", "1", row, int)
        row += 1
        self.create_param_row(scrollable_frame, "FFN维度 (d_ff)", "2048", row, int)
        row += 1
        self.create_param_row(scrollable_frame, "Dropout", "0.1", row, float)
        row += 1
        
        # 训练配置
        ttk.Label(scrollable_frame, text="训练配置", font=('Arial', 12, 'bold')).grid(row=row, column=0, columnspan=2, pady=10, sticky=tk.W)
        row += 1
        
        self.create_param_row(scrollable_frame, "训练轮数 (train_epochs)", "10", row, int)
        row += 1
        self.create_param_row(scrollable_frame, "批次大小 (batch_size)", "32", row, int)
        row += 1
        self.create_param_row(scrollable_frame, "学习率 (learning_rate)", "0.0001", row, float)
        row += 1
        self.create_param_row(scrollable_frame, "早停耐心值 (patience)", "3", row, int)
        row += 1
        self.create_param_row(scrollable_frame, "实验次数 (itr)", "1", row, int)
        row += 1
        
        # GPU配置
        ttk.Label(scrollable_frame, text="GPU配置", font=('Arial', 12, 'bold')).grid(row=row, column=0, columnspan=2, pady=10, sticky=tk.W)
        row += 1
        
        self.create_param_row(scrollable_frame, "使用GPU (use_gpu)", "True", row, bool)
        row += 1
        self.create_param_row(scrollable_frame, "GPU设备 (gpu)", "0", row, int)
        row += 1
        
        # 按钮
        button_frame = ttk.Frame(scrollable_frame)
        button_frame.grid(row=row, column=0, columnspan=2, pady=20)
        row += 1
        
        self.train_button = ttk.Button(button_frame, text="开始训练", command=self.start_training, width=20)
        self.train_button.pack(side=tk.LEFT, padx=5)
        
        self.stop_button = ttk.Button(button_frame, text="停止训练", command=self.stop_training, width=20, state=tk.DISABLED)
        self.stop_button.pack(side=tk.LEFT, padx=5)
        
        # 右侧显示面板
        right_frame = ttk.Frame(main_frame)
        right_frame.grid(row=0, column=1, sticky=(tk.W, tk.E, tk.N, tk.S))
        right_frame.columnconfigure(0, weight=1)
        right_frame.rowconfigure(1, weight=1)
        
        # 数据集信息
        info_frame = ttk.LabelFrame(right_frame, text="数据集信息", padding="10")
        info_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
        info_frame.columnconfigure(0, weight=1)
        
        self.dataset_info_text = tk.Text(info_frame, height=6, wrap=tk.WORD)
        self.dataset_info_text.pack(fill=tk.BOTH, expand=True)
        self.dataset_info_text.insert("1.0", "等待训练开始...\n")
        self.dataset_info_text.config(state=tk.DISABLED)
        
        # 创建标签页
        notebook = ttk.Notebook(right_frame)
        notebook.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # 日志标签页
        log_frame = ttk.Frame(notebook)
        notebook.add(log_frame, text="训练日志")
        log_frame.columnconfigure(0, weight=1)
        log_frame.rowconfigure(0, weight=1)
        
        self.log_text = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, height=20)
        self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # Loss可视化标签页
        loss_frame = ttk.Frame(notebook)
        notebook.add(loss_frame, text="Loss可视化")
        loss_frame.columnconfigure(0, weight=1)
        loss_frame.rowconfigure(0, weight=1)
        
        self.fig = Figure(figsize=(10, 6), dpi=100)
        self.ax = self.fig.add_subplot(111)
        self.ax.set_xlabel('Epoch')
        self.ax.set_ylabel('Loss')
        self.ax.set_title('训练过程Loss曲线')
        self.ax.grid(True)
        
        self.canvas = FigureCanvasTkAgg(self.fig, loss_frame)
        self.canvas.get_tk_widget().grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
    def create_param_row(self, parent, label, default, row, param_type=str):
        ttk.Label(parent, text=label, width=20).grid(row=row, column=0, sticky=tk.W, padx=5, pady=2)
        
        entry = ttk.Entry(parent, width=25)
        entry.insert(0, str(default))
        entry.grid(row=row, column=1, sticky=(tk.W, tk.E), padx=5, pady=2)
        
        # 提取参数名：如果标签包含括号，提取括号内的内容；否则使用标签的小写形式
        if '(' in label and ')' in label:
            param_name = label.split('(')[1].split(')')[0]
        else:
            # 没有括号的标签，使用标签的小写形式作为参数名
            param_name = label.lower()
        
        self.params[param_name] = (entry, param_type)
        
    def get_args(self):
        args = argparse.Namespace()
        
        # 基本配置
        args.task_name = self.params['task_name'][0].get()
        args.model = self.params['model'][0].get()
        args.model_id = self.params['model_id'][0].get()
        args.is_training = 1
        
        # 数据配置
        args.data = self.params['data'][0].get()
        args.root_path = self.params['root_path'][0].get()
        args.data_path = self.params['data_path'][0].get()
        args.features = self.params['features'][0].get()
        args.target = 'OT'
        args.freq = self.params['freq'][0].get()
        args.checkpoints = './checkpoints/'
        
        # 预测配置
        args.seq_len = int(self.params['seq_len'][0].get())
        args.label_len = int(self.params['label_len'][0].get())
        args.pred_len = int(self.params['pred_len'][0].get())
        args.seasonal_patterns = 'Monthly'
        args.inverse = False
        
        # 输入任务
        args.mask_rate = 0.25
        
        # 异常检测任务
        args.anomaly_ratio = 0.25
        
        # 模型配置
        args.expand = 2
        args.d_conv = 4
        args.top_k = 5
        args.num_kernels = 6
        args.enc_in = 7
        args.dec_in = 7
        args.c_out = 7
        args.d_model = int(self.params['d_model'][0].get())
        args.n_heads = int(self.params['n_heads'][0].get())
        args.e_layers = int(self.params['e_layers'][0].get())
        args.d_layers = int(self.params['d_layers'][0].get())
        args.d_ff = int(self.params['d_ff'][0].get())
        args.moving_avg = 25
        args.factor = 1
        args.distil = True
        args.dropout = float(self.params['dropout'][0].get())
        args.embed = 'timeF'
        args.activation = 'gelu'
        args.channel_independence = 1
        args.decomp_method = 'moving_avg'
        args.use_norm = 1
        args.down_sampling_layers = 0
        args.down_sampling_window = 1
        args.down_sampling_method = None
        args.seg_len = 96
        
        # 优化配置
        args.num_workers = 10
        args.itr = int(self.params['itr'][0].get())
        args.train_epochs = int(self.params['train_epochs'][0].get())
        args.batch_size = int(self.params['batch_size'][0].get())
        args.patience = int(self.params['patience'][0].get())
        args.learning_rate = float(self.params['learning_rate'][0].get())
        args.des = 'test'
        args.loss = 'MSE'
        args.lradj = 'type1'
        args.use_amp = False
        
        # GPU配置
        use_gpu_str = self.params['use_gpu'][0].get().lower()
        args.use_gpu = use_gpu_str in ['true', '1', 'yes']
        args.gpu = int(self.params['gpu'][0].get())
        args.gpu_type = 'cuda'
        args.use_multi_gpu = False
        args.devices = '0,1,2,3'
        
        # 其他参数
        args.p_hidden_dims = [128, 128]
        args.p_hidden_layers = 2
        args.use_dtw = False
        args.augmentation_ratio = 0
        args.seed = 2
        args.jitter = False
        args.scaling = False
        args.permutation = False
        args.randompermutation = False
        args.magwarp = False
        args.timewarp = False
        args.windowslice = False
        args.windowwarp = False
        args.rotation = False
        args.spawner = False
        args.dtwwarp = False
        args.shapedtwwarp = False
        args.wdba = False
        args.discdtw = False
        args.discsdtw = False
        args.extra_tag = ""
        args.patch_len = 16
        args.node_dim = 10
        args.gcn_depth = 2
        args.gcn_dropout = 0.3
        args.propalpha = 0.3
        args.conv_channel = 32
        args.skip_channel = 32
        args.individual = False
        args.alpha = 0.1
        args.top_p = 0.5
        args.pos = 1
        
        return args
    
    def start_training(self):
        if self.is_training:
            messagebox.showwarning("警告", "训练已在进行中！")
            return
        
        try:
            args = self.get_args()
            self.is_training = True
            self.train_button.config(state=tk.DISABLED)
            self.stop_button.config(state=tk.NORMAL)
            
            # 清空日志和loss
            self.log_text.delete("1.0", tk.END)
            self.train_losses = []
            self.val_losses = []
            self.test_losses = []
            self.epochs = []
            self.ax.clear()
            self.ax.set_xlabel('Epoch')
            self.ax.set_ylabel('Loss')
            self.ax.set_title('训练过程Loss曲线')
            self.ax.grid(True)
            self.canvas.draw()
            
            # 启动训练线程
            self.training_thread = TrainingThread(args, self.log_queue, self.loss_queue, self.dataset_info_queue)
            self.training_thread.start()
            
            self.log_text.insert(tk.END, "训练线程已启动...\n")
            self.log_text.see(tk.END)
            
        except Exception as e:
            messagebox.showerror("错误", f"启动训练失败: {str(e)}")
            self.is_training = False
            self.train_button.config(state=tk.NORMAL)
            self.stop_button.config(state=tk.DISABLED)
    
    def stop_training(self):
        # 注意：这个实现可能无法立即停止训练
        # 更好的实现需要修改训练代码以支持中断
        if self.training_thread and self.training_thread.is_alive():
            messagebox.showwarning("警告", "训练无法立即停止，请等待当前epoch完成")
            # 可以设置一个标志位，让训练代码检查
    
    def process_queue(self):
        # 处理日志队列
        try:
            while True:
                msg = self.log_queue.get_nowait()
                self.log_text.insert(tk.END, msg)
                self.log_text.see(tk.END)
                
                # 检测训练完成
                if "训练完成" in msg or "训练过程中出错" in msg:
                    if self.training_thread and not self.training_thread.is_alive():
                        self.is_training = False
                        self.train_button.config(state=tk.NORMAL)
                        self.stop_button.config(state=tk.DISABLED)
                
                # 从日志中提取loss信息
                # 匹配格式: "Epoch: {0}, Steps: {1} | Train Loss: {2:.7f} Vali Loss: {3:.7f} Test Loss: {4:.7f}"
                loss_pattern = r'Epoch:\s+(\d+).*?Train Loss:\s+([\d.]+).*?Vali Loss:\s+([\d.]+).*?Test Loss:\s+([\d.]+)'
                match = re.search(loss_pattern, msg)
                if match:
                    epoch = int(match.group(1))
                    train_loss = float(match.group(2))
                    val_loss = float(match.group(3))
                    test_loss = float(match.group(4))
                    
                    self.loss_queue.put({
                        'epoch': epoch,
                        'train_loss': train_loss,
                        'val_loss': val_loss,
                        'test_loss': test_loss
                    })
        except queue.Empty:
            pass
        
        # 检查训练线程是否结束
        if self.is_training and self.training_thread and not self.training_thread.is_alive():
            self.is_training = False
            self.train_button.config(state=tk.NORMAL)
            self.stop_button.config(state=tk.DISABLED)
        
        # 处理数据集信息队列
        try:
            while True:
                info = self.dataset_info_queue.get_nowait()
                self.dataset_info_text.config(state=tk.NORMAL)
                self.dataset_info_text.delete("1.0", tk.END)
                self.dataset_info_text.insert("1.0", 
                    f"训练集大小: {info['train_size']}\n"
                    f"测试集大小: {info['test_size']}\n"
                    f"验证集大小: {info['val_size']}\n"
                    f"训练批次数: {info['train_batches']}\n"
                    f"测试批次数: {info['test_batches']}\n"
                    f"验证批次数: {info['val_batches']}\n"
                )
                self.dataset_info_text.config(state=tk.DISABLED)
        except queue.Empty:
            pass
        
        # 处理loss队列
        try:
            while True:
                loss_data = self.loss_queue.get_nowait()
                epoch = loss_data.get('epoch', 0)
                train_loss = loss_data.get('train_loss', 0)
                val_loss = loss_data.get('val_loss', 0)
                test_loss = loss_data.get('test_loss', 0)
                
                self.epochs.append(epoch)
                self.train_losses.append(train_loss)
                self.val_losses.append(val_loss)
                self.test_losses.append(test_loss)
                
                self.update_loss_plot()
        except queue.Empty:
            pass
        
        self.root.after(100, self.process_queue)
    
    def update_loss_plot(self):
        if len(self.epochs) > 0:
            self.ax.clear()
            self.ax.set_xlabel('Epoch')
            self.ax.set_ylabel('Loss')
            self.ax.set_title('训练过程Loss曲线')
            self.ax.grid(True)
            
            if len(self.train_losses) > 0:
                self.ax.plot(self.epochs, self.train_losses, label='Train Loss', marker='o')
            if len(self.val_losses) > 0:
                self.ax.plot(self.epochs, self.val_losses, label='Val Loss', marker='s')
            if len(self.test_losses) > 0:
                self.ax.plot(self.epochs, self.test_losses, label='Test Loss', marker='^')
            
            self.ax.legend()
            self.canvas.draw()


def main():
    root = tk.Tk()
    app = TrainingGUI(root)
    root.mainloop()


if __name__ == '__main__':
    main()

