"""G-code 文件处理工具集。

本模块提供了 G-code 文本的读取、换行符标准化、时间/耗材格式化、
M73 剩余时间偏移计算、换板代码块构建与插入等核心功能。
所有对换板 G-code 的文本操作均通过本模块完成。"""

from __future__ import annotations

import re
from dataclasses import dataclass
from pathlib import Path

from .models import DEFAULT_INSERT_BEFORE_MARKER, LineEnding
from .paths import default_swap_gcode_dir

# 匹配 M73 剩余时间指令，捕获 P/R 参数中的分钟数值
# 示例: M73 P10 R120 ; 表示当前进度 10%，剩余 120 分钟
M73_RE = re.compile(r"^(M73\s+P\d+\s+R)(\d+)(.*)$", re.MULTILINE)


def normalize_newlines(text: str) -> str:
    """将文本中的 CRLF 和 CR 统一转换为 LF 换行符。

    参数:
        text: 待处理的原始文本。

    返回:
        使用 LF 换行的规范化文本。
    """
    return text.replace("\r\n", "\n").replace("\r", "\n")


def apply_line_ending(text: str, line_ending: LineEnding) -> bytes:
    """将文本按指定的换行符风格编码为字节串。

    参数:
        text:       待编码的文本。
        line_ending: 目标换行风格，"lf" 或 "crlf"。

    返回:
        使用指定换行符编码的 UTF-8 字节串。
    """
    normalized = normalize_newlines(text)
    if line_ending == "crlf":
        normalized = normalized.replace("\n", "\r\n")
    return normalized.encode("utf-8")


def read_swap_gcode_file(path: Path) -> str:
    """读取换板 G-code 文件内容，自动处理 BOM 头。

    参数:
        path: G-code 文件路径。

    返回:
        文件的文本内容。
    """
    return path.read_text(encoding="utf-8-sig", errors="replace")


def resolve_swap_gcode_path(value: Path | str, swap_gcode_dir: Path | None = None) -> Path:
    """将用户输入的文件名或路径解析为实际存在的换板 G-code 文件路径。

    解析策略（按优先级）:
    1. 直接作为路径查找。
    2. 在 swap_gcode_dir 目录下查找。
    3. 无后缀名时依次尝试 .gcode, .nc, .ngc, .txt 后缀。

    参数:
        value:          文件名字符串或路径。
        swap_gcode_dir: 默认搜索目录，为 None 时使用内置默认目录。

    返回:
        解析后的文件路径。

    异常:
        FileNotFoundError: 无法在任何位置找到该文件。
    """
    raw = Path(value)
    if raw.exists():
        return raw
    search_dir = swap_gcode_dir or default_swap_gcode_dir()
    candidate = search_dir / str(value)
    if candidate.exists():
        return candidate
    # 无后缀名时自动尝试常见 G-code 后缀
    if raw.suffix:
        raise FileNotFoundError(f"Swap G-code file not found: {value}")
    for suffix in (".gcode", ".nc", ".ngc", ".txt"):
        candidate = search_dir / f"{value}{suffix}"
        if candidate.exists():
            return candidate
    raise FileNotFoundError(f"Swap G-code file not found: {value}")


def list_swap_gcode_files(directory: Path | None = None) -> list[Path]:
    """列出指定目录下所有可用的换板 G-code 文件。

    参数:
        directory: 要扫描的目录，为 None 时使用内置默认目录。

    返回:
        按名称排序的 G-code 文件路径列表。
    """
    root = directory or default_swap_gcode_dir()
    if not root.exists():
        return []
    allowed = {".gcode", ".nc", ".ngc", ".txt"}
    # 过滤掉隐藏文件（以 . 开头），只保留允许的后缀名
    return sorted(path for path in root.iterdir() if path.is_file() and not path.name.startswith(".") and path.suffix.lower() in allowed)


def format_duration(seconds: float | None) -> str:
    """将秒数格式化为易读的时间字符串。

    参数:
        seconds: 秒数，可为 None。

    返回:
        格式化后的时间字符串，如 "2h 30m"、"5m 20s"、"45s"，未知则返回 "未知"。
    """
    if seconds is None:
        return "0s"
    total = max(0, int(round(seconds)))
    hours, rem = divmod(total, 3600)
    minutes, secs = divmod(rem, 60)
    if hours:
        return f"{hours}h {minutes:02d}m"
    if minutes:
        return f"{minutes}m {secs:02d}s"
    return f"{secs}s"


def format_filament(weight_grams: float | None, used_m: float | None = None) -> str:
    """将耗材重量和长度格式化为易读字符串。

    参数:
        weight_grams: 耗材重量（克），可为 None。
        used_m:       耗材使用长度（米），可为 None。

    返回:
        格式化后的耗材信息字符串，未知则返回 "未知"。
    """
    if weight_grams is None and used_m is None:
        return "0.00 g"
    parts: list[str] = []
    if weight_grams is not None:
        parts.append(f"{weight_grams:.2f} g")
    if used_m is not None:
        parts.append(f"{used_m:.2f} m")
    return " / ".join(parts)


@dataclass(frozen=True)
class M73OffsetTemplate:
    """M73 剩余时间偏移模板。

    用于在合并多板 G-code 时，将各板的 M73 剩余时间递增偏移，
    使合并后的 G-code 中每块板的剩余时间估算保持连续。

    属性:
        segments:               M73 指令之间的文本片段（含首尾）。
        base_remaining_minutes: 每个 M73 指令的原始剩余分钟数。
    """
    segments: tuple[str, ...]
    base_remaining_minutes: tuple[int, ...]

    def apply(self, plate_number: int) -> str:
        """根据板号生成偏移后的 M73 指令文本。

        偏移量 = plate_number * 100 * 60（单位：分钟），
        即每块板增加 100 小时的剩余时间偏移。

        参数:
            plate_number: 板号（从 0 开始）。

        返回:
            应用偏移后的完整 G-code 文本片段。
        """
        if not self.base_remaining_minutes:
            return self.segments[0]
        minute_offset = plate_number * 100 * 60
        parts: list[str] = []
        for index, remaining_minutes in enumerate(self.base_remaining_minutes):
            parts.append(self.segments[index])
            parts.append(str(remaining_minutes + minute_offset))
        parts.append(self.segments[-1])
        return "".join(parts)


def prepare_m73_offset_template(gcode: str) -> M73OffsetTemplate:
    """从 G-code 文本中提取 M73 剩余时间偏移模板。

    参数:
        gcode: 原始换板 G-code 文本。

    返回:
        可用于多板偏移计算的 M73OffsetTemplate 实例。
    """
    segments: list[str] = []
    base_remaining_minutes: list[int] = []
    last_index = 0
    for match in M73_RE.finditer(gcode):
        # 保存 M73 指令前的文本片段
        segments.append(gcode[last_index : match.start(2)])
        # 提取原始剩余分钟数
        base_remaining_minutes.append(int(match.group(2)))
        last_index = match.end(2)
    # 保存最后一段尾部文本
    segments.append(gcode[last_index:])
    return M73OffsetTemplate(tuple(segments), tuple(base_remaining_minutes))


def apply_plate_number_offset(gcode: str, plate_number: int) -> str:
    """对 G-code 应用板号偏移，更新所有 M73 剩余时间。

    参数:
        gcode:        原始换板 G-code 文本。
        plate_number: 板号（从 0 开始）。

    返回:
        偏移后的 G-code 文本。
    """
    return prepare_m73_offset_template(gcode).apply(plate_number)


def build_swap_block(swap_gcode: str, cool_bed_temp: int | None, wait_seconds: int) -> str:
    """构建换板代码块。

    由可选的热床降温 M190 指令、换板 G-code 主体和可选的暂停等待 G4 指令组成。

    参数:
        swap_gcode:    换板 G-code 主体内容。
        cool_bed_temp: 目标热床温度（°C），为 None 时不插入降温指令。
        wait_seconds:  换板后等待秒数，0 时不插入等待指令。

    返回:
        完整的换板代码块字符串。
    """
    parts: list[str] = []
    if cool_bed_temp is not None:
        parts.append(f"M190 S{int(cool_bed_temp)}")
    parts.append(normalize_newlines(swap_gcode).rstrip("\n"))
    if wait_seconds > 0:
        parts.append(f"G4 P{int(wait_seconds) * 1000}")
    # 过滤空字符串后拼接，末尾加换行
    return "\n".join(part for part in parts if part) + "\n"


def insert_swap_block(gcode: str, swap_block: str, insert_before_marker: str = DEFAULT_INSERT_BEFORE_MARKER) -> str:
    """在原始 G-code 中的指定标记之前插入换板代码块。

    如果找不到标记，则将换板代码块追加到 G-code 末尾。

    参数:
        gcode:                 原始 G-code 文本。
        swap_block:            要插入的换板代码块。
        insert_before_marker:  插入位置标记，在该标记所在行之前插入。

    返回:
        插入换板代码块后的 G-code 文本。
    """
    text = normalize_newlines(gcode)
    marker = insert_before_marker.strip()
    if marker:
        # 查找标记行，支持换行后的标记或文件开头的标记
        marker_index = text.find("\n" + marker)
        if marker_index == -1 and text.startswith(marker):
            marker_index = 0
        if marker_index != -1:
            # 在标记行之前插入换板代码块
            prefix = text[: marker_index + (1 if marker_index > 0 else 0)]
            suffix = text[marker_index + (1 if marker_index > 0 else 0) :]
            return prefix + swap_block + suffix
    # 未找到标记时追加到末尾
    return text.rstrip("\n") + "\n" + swap_block
