# -*- coding: utf-8 -*-
"""
pytcper TCP调试助手 - 公共标签页基类
====================================
客户端 / 服务端标签页公共部分:日志区、发送区、编码与格式选项、
消息泵、循环发送、历史报文、收发统计与资源清理。
"""

import codecs
import os
import queue
import threading
import time
import tkinter as tk
from collections import deque
from tkinter import filedialog, messagebox, scrolledtext, ttk

from tcp_utils import (ENCODINGS, FONT, HISTORY_MAX, LOG_MAX_LINES,
                       SEND_TIMEOUT, SOCK_TIMEOUT, close_socket, decode_bytes,
                       encode_text, guess_decodable)


class BaseTab(ttk.Frame):
    """客户端 / 服务端标签页公共部分:日志区、发送区、编码与格式选项。"""

    def __init__(self, master, build_top=None):
        super().__init__(master)
        self.msg_queue = queue.Queue()   # 子线程 -> 主线程 的消息队列
        self.sock = None                 # 客户端连接 socket
        self.server_sock = None          # 服务端监听 socket
        self.clients = {}                # 服务端: socket -> "ip:port"
        self.client_list = []            # 服务端: 按连接顺序保存的 socket 列表
        self.client_times = {}           # 服务端: socket -> 连接时刻 (time.monotonic)
        self.conn_lock = threading.Lock()
        self._stats_lock = threading.Lock()   # 保护 _stats 的并发累加
        self.closed = False
        # 收发统计
        self._stats = {"tx_msgs": 0, "tx_bytes": 0, "rx_msgs": 0, "rx_bytes": 0}
        # 历史报文
        self._history = deque(maxlen=HISTORY_MAX)
        self._hist_index = None
        # 循环发送
        self._loop_running = False
        self._loop_after = None
        self._loop_left = None
        self._loop_interval = 1.0
        # 周期任务句柄(关闭时取消,避免残留回调报错)
        self._drain_after = None
        # 以下缓存由主线程 _cache_settings 同步,子线程只读这些普通属性
        self._enc = "UTF-8"
        self._hex_view = False
        self._ts = True

        if build_top is not None:
            build_top()
        self._build_log_area()
        self._build_send_area()
        self._drain_after = self.after(50, self._drain)

    # ---------- 界面 ----------
    def _build_log_area(self):
        bar = ttk.Frame(self)
        bar.pack(fill="x", padx=8, pady=(8, 2))
        self.var_timestamp = tk.BooleanVar(value=True)
        ttk.Checkbutton(bar, text="时间戳", variable=self.var_timestamp).pack(side="left")
        self.var_timestamp.trace_add("write", self._cache_settings)
        self.var_hex_view = tk.BooleanVar(value=False)
        ttk.Checkbutton(bar, text="接收显示HEX", variable=self.var_hex_view).pack(side="left", padx=10)
        self.var_hex_view.trace_add("write", self._cache_settings)
        self.stats_label = ttk.Label(bar, text="", foreground="#666666")
        self.stats_label.pack(side="left", padx=10)
        ttk.Button(bar, text="导出记录", command=self._export_log).pack(side="right")
        ttk.Button(bar, text="清空记录", command=self._clear_log).pack(side="right", padx=4)

        log_frame = ttk.LabelFrame(self, text="收发记录")
        log_frame.pack(fill="both", expand=True, padx=8, pady=2)
        self.log_text = scrolledtext.ScrolledText(log_frame, height=8, font=FONT, state="disabled")
        self.log_text.pack(fill="both", expand=True, padx=4, pady=4)
        self.log_text.tag_configure("rx", foreground="#0066CC")
        self.log_text.tag_configure("tx", foreground="#CC3300")
        self.log_text.tag_configure("sys", foreground="#888888")
        self.log_text.tag_configure("addr", foreground="#666666")

    def _build_send_area(self):
        send_frame = ttk.LabelFrame(self, text="发送")
        send_frame.pack(fill="both", padx=8, pady=(2, 8))
        self.send_text = scrolledtext.ScrolledText(send_frame, height=4, font=FONT)
        self.send_text.pack(fill="both", expand=True, padx=4, pady=4)

        self.send_row = ttk.Frame(send_frame)
        self.send_row.pack(fill="x", padx=4, pady=(0, 2))
        ttk.Label(self.send_row, text="编码:").pack(side="left")
        self.var_encoding = tk.StringVar(value="UTF-8")
        ttk.Combobox(self.send_row, textvariable=self.var_encoding, values=ENCODINGS,
                     width=10, state="readonly").pack(side="left", padx=(2, 8))
        self.var_encoding.trace_add("write", self._cache_settings)
        self.var_hex_send = tk.BooleanVar(value=False)
        ttk.Checkbutton(self.send_row, text="HEX发送", variable=self.var_hex_send).pack(side="left")
        self.var_newline = tk.BooleanVar(value=False)
        ttk.Checkbutton(self.send_row, text="自动追加换行(\\r\\n)",
                        variable=self.var_newline).pack(side="left", padx=8)
        self.send_btn = ttk.Button(self.send_row, text="发送(Alt+S)", command=self.do_send)
        self.send_btn.pack(side="right")
        self.clear_btn = ttk.Button(self.send_row, text="清除", command=self._clear_send)
        self.clear_btn.pack(side="right", padx=(0, 4))

        self.send_row2 = ttk.Frame(send_frame)
        self.send_row2.pack(fill="x", padx=4, pady=(0, 4))
        self.loop_btn = ttk.Button(self.send_row2, text="循环发送", command=self._loop_toggle)
        self.loop_btn.pack(side="left")
        ttk.Label(self.send_row2, text="间隔(秒):").pack(side="left", padx=(8, 0))
        self.var_loop_interval = tk.StringVar(value="1.0")
        ttk.Entry(self.send_row2, textvariable=self.var_loop_interval, width=6).pack(side="left", padx=(2, 8))
        ttk.Label(self.send_row2, text="次数(0=无限):").pack(side="left")
        self.var_loop_count = tk.StringVar(value="0")
        ttk.Entry(self.send_row2, textvariable=self.var_loop_count, width=6).pack(side="left", padx=(2, 8))
        ttk.Button(self.send_row2, text="发送文件", command=self._send_file).pack(side="left", padx=8)
        ttk.Button(self.send_row2, text="历史报文", command=self._open_history).pack(side="left")

        # 快捷键:Ctrl+Enter 发送;↑/↓ 切换发送历史
        self.send_text.bind("<Control-Return>", self._on_ctrl_enter)
        self.send_text.bind("<Up>", lambda e: self._nav_history(-1))
        self.send_text.bind("<Down>", lambda e: self._nav_history(1))

    # ---------- 日志 ----------
    def log_msg(self, tag, text, addr=""):
        """可从任意线程调用:投递一条日志到主线程渲染。"""
        prefix = time.strftime("[%H:%M:%S] ") if self._ts else ""
        self.msg_queue.put(("log", (prefix, addr, tag, text)))

    def sys_log(self, text):
        self.log_msg("sys", text)

    def _clear_log(self):
        self.log_text.configure(state="normal")
        self.log_text.delete("1.0", "end")
        self.log_text.configure(state="disabled")

    def _export_log(self):
        path = filedialog.asksaveasfilename(
            title="导出收发记录", defaultextension=".txt",
            filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")])
        if not path:
            return
        self._write_log_to(path)

    def _write_log_to(self, path):
        try:
            with open(path, "w", encoding="utf-8") as f:
                f.write(self.log_text.get("1.0", "end-1c"))
        except OSError as exc:
            messagebox.showerror("错误", "导出失败: %s" % exc)
            return
        self.sys_log("记录已导出到 %s" % os.path.basename(path))

    # ---------- 收发统计 ----------
    def bump_stats(self, kind, data):
        """统计一条收发消息(子线程也会调用,加锁防并发丢计数)。"""
        with self._stats_lock:
            if kind == "tx":
                self._stats["tx_msgs"] += 1
                self._stats["tx_bytes"] += len(data)
            else:
                self._stats["rx_msgs"] += 1
                self._stats["rx_bytes"] += len(data)

    def _refresh_stats_label(self):
        st = self._stats
        self.stats_label.configure(
            text="发送 %d 条 / %d 字节    接收 %d 条 / %d 字节"
                 % (st["tx_msgs"], st["tx_bytes"], st["rx_msgs"], st["rx_bytes"]))

    # ---------- 消息泵(主线程定时执行) ----------
    def _cache_settings(self, *_):
        """把界面上的编码 / 显示选项同步到普通属性,供子线程安全读取。"""
        self._enc = self.var_encoding.get()
        self._hex_view = self.var_hex_view.get()
        self._ts = self.var_timestamp.get()

    def _drain(self):
        self._cache_settings()
        self._refresh_stats_label()
        try:
            while True:
                kind, payload = self.msg_queue.get_nowait()
                if kind == "log":
                    prefix, addr, tag, text = payload
                    self.log_text.configure(state="normal")
                    self.log_text.insert("end", prefix)
                    if addr:
                        self.log_text.insert("end", addr + " ", "addr")
                    self.log_text.insert("end", text + "\n", tag)
                    self.log_text.configure(state="disabled")
                    self.log_text.see("end")
                    self._trim_log()
                else:
                    self.handle_msg(kind, payload)
        except queue.Empty:
            pass
        if not self.closed:
            self._drain_after = self.after(50, self._drain)

    def _trim_log(self):
        """限制日志区行数,避免长时间运行内存无限增长。"""
        try:
            lines = int(self.log_text.index("end-1c").split(".")[0])
        except tk.TclError:
            return
        if lines > LOG_MAX_LINES:
            self.log_text.delete("1.0", "%d.0" % (lines - LOG_MAX_LINES + 1))

    def handle_msg(self, kind, payload):
        pass

    # ---------- 发送 ----------
    def prepare_send(self, silent=False):
        """读取发送框内容并编码,返回 (raw, data);失败返回 None。"""
        raw = self.send_text.get("1.0", "end-1c")
        if not raw.strip():
            if not silent:
                messagebox.showwarning("提示", "发送内容为空")
            return None
        try:
            data = encode_text(raw, self.var_encoding.get(), self.var_hex_send.get())
        except ValueError:
            if not silent:
                messagebox.showerror("错误", "HEX 字符串不合法:需要偶数个十六进制字符,如 01 0A FF")
            return None
        if self.var_newline.get() and not self.var_hex_send.get():
            data += b"\r\n"
        return raw, data

    def _clear_send(self):
        """清空发送框内容。"""
        self.send_text.delete("1.0", "end")
        self._hist_index = None
        self.send_text.focus_set()

    def do_send(self, silent=False):
        raise NotImplementedError

    def tx_text(self, data):
        return decode_bytes(data, self._enc, self.var_hex_send.get())

    def decode_stream(self, state, data):
        """接收流增量解码:state 为 (编码, 增量解码器) 或 None。

        多字节字符(GBK/UTF-8 等)被 TCP 拆到多个数据包时,逐包独立 decode
        会把半个字符替换成乱码;这里按连接维护解码器状态,并返回更新后的
        state 供下次调用。编码或「接收显示HEX」切换时自动重建。

        当按当前编码解码出替换字符(客户端使用了不同编码)时,用
        guess_decodable 对整包字节做自动编码探测,优先显示无乱码的方案
        (仅显示用途,不改变流式解码器状态,也不会误伤后续分包)。
        """
        if self._hex_view:
            return " ".join("%02X" % b for b in data), None
        enc = self._enc
        if state is None or state[0] != enc:
            state = (enc, codecs.getincrementaldecoder(enc)(errors="replace"))
        try:
            text = state[1].decode(data)
        except UnicodeError:
            # UTF-16/32 流首包无 BOM 时,即使 errors="replace" 也会抛
            # UnicodeError(流结构错误,replace 不覆盖);小端探测成功则改用
            # 明确的小端解码器继续该连接的流式解码,避免接收线程崩溃导致
            # 连接永远留在列表里。探测失败按单包降级显示。
            state = (enc, codecs.getincrementaldecoder(enc + "-le")(errors="replace"))
            try:
                text = state[1].decode(data)
            except UnicodeError:
                state = None
                text = decode_bytes(data, enc, False)
        if "\ufffd" in text:
            guessed, repl = guess_decodable(data)
            if guessed is not None and repl == 0:
                text = guessed
        return text, state

    def sendall_safe(self, sock, data):
        """带超时地 sendall:成功返回 None,失败返回异常对象。

        在 Tk 主线程发送必须限时:对端接收窗口已满或网络中断而未被 TCP
        检测到时,无超时的 sendall 会阻塞数分钟并冻结整个界面。
        """
        try:
            sock.settimeout(SEND_TIMEOUT)
            try:
                sock.sendall(data)
            finally:
                try:
                    sock.settimeout(SOCK_TIMEOUT)
                except OSError:
                    pass
        except OSError as exc:
            return exc
        return None

    def _send_file(self):
        path = filedialog.askopenfilename(title="选择要发送的文件")
        if not path:
            return
        data = self._read_file_bytes(path)
        if data is None:
            return
        self._send_raw(data, "文件(%s)" % os.path.basename(path))

    def _read_file_bytes(self, path):
        try:
            with open(path, "rb") as f:
                data = f.read()
        except OSError as exc:
            messagebox.showerror("错误", "读取文件失败: %s" % exc)
            return None
        if not data:
            messagebox.showwarning("提示", "文件为空")
            return None
        return data

    # ---------- 历史报文 ----------
    def record_history(self, raw):
        if raw and (not self._history or self._history[-1] != raw):
            self._history.append(raw)
        self._hist_index = None

    def _nav_history(self, step):
        """↑/↓ 键在发送历史中前后切换。"""
        if not self._history:
            return None  # 无历史时保留默认光标行为
        current = self.send_text.get("1.0", "end-1c")
        if self._hist_index is None or self._hist_index >= len(self._history) \
                or self._history[self._hist_index] != current:
            # 无浏览起点或输入框被手动编辑过:从最新/最旧记录重新开始
            self._hist_index = len(self._history) - 1 if step < 0 else 0
        else:
            self._hist_index += step
        self._hist_index = max(0, min(len(self._history) - 1, self._hist_index))
        self.send_text.delete("1.0", "end")
        self.send_text.insert("1.0", self._history[self._hist_index])
        self.send_text.mark_set("insert", "end")
        return "break"

    def _open_history(self):
        if not self._history:
            messagebox.showinfo("历史报文", "暂无历史记录")
            return
        win = tk.Toplevel(self)
        win.title("历史报文(双击填入)")
        win.geometry("420x320")
        win.transient(self)
        lb = tk.Listbox(win, font=FONT)
        lb.pack(fill="both", expand=True, padx=6, pady=6)
        for item in reversed(self._history):   # 最新记录显示在最上面
            lb.insert("end", item)

        def fill():
            sel = lb.curselection()
            if not sel:
                return False
            text = self._history[len(self._history) - 1 - sel[0]]
            self.send_text.delete("1.0", "end")
            self.send_text.insert("1.0", text)
            self._hist_index = None
            return True

        lb.bind("<Double-Button-1>", lambda e: (fill(), win.destroy()))
        ttk.Button(win, text="填入并重发",
                   command=lambda: (fill() and self.do_send(), win.destroy())).pack(pady=4)

    # ---------- 循环发送 ----------
    def _loop_toggle(self):
        if self._loop_running:
            self._stop_loop()
            self.sys_log("循环发送已停止")
            return
        if not self.send_text.get("1.0", "end-1c").strip():
            messagebox.showwarning("提示", "发送内容为空")
            return
        try:
            interval = float(self.var_loop_interval.get())
            count = int(self.var_loop_count.get())
        except ValueError:
            messagebox.showerror("错误", "间隔(秒)/次数格式不正确")
            return
        if interval <= 0:
            messagebox.showerror("错误", "间隔必须大于 0")
            return
        self._loop_running = True
        self._loop_interval = interval
        self._loop_left = count if count > 0 else None   # None 表示无限循环
        self.loop_btn.configure(text="停止循环")
        self.record_history(self.send_text.get("1.0", "end-1c"))
        self.sys_log("开始循环发送(间隔 %.2fs%s)"
                      % (interval, "" if self._loop_left is None else ",共 %d 次" % count))
        self._loop_tick()

    def _loop_tick(self):
        if self.closed or not self._loop_running:
            return
        if self._loop_left is not None:
            if self._loop_left <= 0:
                self._stop_loop()
                self.sys_log("循环发送结束")
                return
            self._loop_left -= 1
        if not self.do_send(silent=True):
            self._stop_loop()
            self.sys_log("循环发送已停止(发送失败或连接断开)")
            return
        self._loop_after = self.after(int(self._loop_interval * 1000), self._loop_tick)

    def _stop_loop(self):
        self._loop_running = False
        if self._loop_after is not None:
            try:
                self.after_cancel(self._loop_after)
            except Exception:
                pass
            self._loop_after = None
        self.loop_btn.configure(text="循环发送")

    # ---------- 快捷键 ----------
    def _on_ctrl_enter(self, _event=None):
        self.do_send()
        return "break"

    # ---------- 关闭 ----------
    def shutdown(self):
        self.closed = True
        self._stop_loop()
        if self._drain_after is not None:
            try:
                self.after_cancel(self._drain_after)
            except Exception:
                pass
            self._drain_after = None
        self.close_conns()

    def close_conns(self):
        sock, self.sock = self.sock, None
        if sock is not None:
            close_socket(sock)