#!/usr/bin/env python3
"""
Helper script to bundle pro3_uart.py into an executable together with the
files that flash.py expects.

Usage:
    python package_pro3_exe.py

Prerequisites:
    pip install pyinstaller
"""

from __future__ import annotations

import shutil
import sys
from pathlib import Path
import re

try:
    from PyInstaller import __main__ as pyinstaller_main
except ImportError as exc:
    print("PyInstaller is required. Install it with 'pip install pyinstaller'.")
    raise SystemExit(1) from exc


APP_ROOT = Path(__file__).resolve().parent
PROJECT_BASE_NAME = "pro3_uart"


def derive_version_from_folder() -> str:
    root_name = APP_ROOT.name
    match = re.search(r"_v([0-9][0-9.\-_]*)$", root_name, re.IGNORECASE)
    if not match:
        return ""
    return match.group(1)


def read_version_file() -> tuple[str, str]:
    version_file = APP_ROOT / "version_info.py"
    if not version_file.exists():
        return "", ""
    namespace: dict[str, str] = {}
    try:
        with version_file.open("r", encoding="utf-8") as handle:
            exec(handle.read(), namespace)
    except Exception:
        return "", ""
    canonical = str(namespace.get("version", "")).strip()
    display = str(namespace.get("display_version", "")).strip()
    return canonical, display


def write_version_file(version: str, display: str) -> None:
    version_file = APP_ROOT / "version_info.py"
    template = f'''#! /usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright (c) 2024 Realtek Semiconductor Corp.
# SPDX-License-Identifier: Apache-2.0

version = "{version}"
display_version = "{display}"
'''
    with version_file.open("w", encoding="utf-8") as handle:
        handle.write(template)


def determine_version_tag() -> str:
    folder_version = derive_version_from_folder()
    stored_canonical, stored_display = read_version_file()
    chosen = folder_version or stored_display or stored_canonical
    sanitized = re.sub(r"[^0-9A-Za-z._-]", "_", chosen)
    if not sanitized:
        sanitized = "unknown"
    canonical = canonicalize_version_number(sanitized)
    if canonical != stored_canonical or sanitized != stored_display:
        write_version_file(canonical, sanitized)
    return sanitized


def canonicalize_version_number(tag: str) -> str:
    cleaned = tag.replace("-", ".").replace("_", ".")
    parts = [re.sub(r"\D+", "", chunk) or "0" for chunk in cleaned.split(".") if chunk]
    if not parts:
        parts = ["0"]
    while len(parts) < 4:
        parts.append("0")
    return ".".join(parts[:4])


VERSION_TAG = determine_version_tag()
RESOURCE_VERSION = canonicalize_version_number(VERSION_TAG)
DIST_NAME = f"{PROJECT_BASE_NAME}_v{VERSION_TAG}"
DIST_DIR = APP_ROOT / "dist" / DIST_NAME
BUILD_DIR = APP_ROOT / "build"
FLASH_SPEC = APP_ROOT / "flash.spec"
FLASH_EXE_NAME = "flash.exe"
FLASH_OUTPUT_PATHS = [
    APP_ROOT / "dist" / FLASH_EXE_NAME,
    APP_ROOT / "dist" / "flash" / FLASH_EXE_NAME,
]

RESOURCE_ITEMS = [
    ("base", "base"),
    ("devices", "devices"),
    ("fw", "fw"),
    ("Reburn.cfg", "Reburn.cfg"),
    ("Reset.cfg", "Reset.cfg"),
    ("Settings.json", "Settings.json"),
    ("pylink", "pylink"),
]

PYLINK_DIR = APP_ROOT / "pylink"
JLINK_DLL_NAME = "JLinkARM.dll"
JLINK_DLL_SOURCE = PYLINK_DIR / JLINK_DLL_NAME


def clean_path(path: Path) -> None:
    if path.exists():
        shutil.rmtree(path)


def cleanup_old_version_specs() -> None:
    pattern = f"{PROJECT_BASE_NAME}_v*.spec"
    for spec_path in APP_ROOT.glob(pattern):
        try:
            spec_path.unlink()
            print(f"Removed outdated spec: {spec_path.name}")
        except Exception as exc:
            print(f"Warning: unable to remove {spec_path}: {exc}")


def run_pyinstaller_for_pro3() -> None:
    if DIST_DIR.exists():
        shutil.rmtree(DIST_DIR)
    clean_path(BUILD_DIR)

    args = [
        "--noconfirm",
        "--clean",
        "--windowed",
        "--onedir",
        "--name",
        DIST_NAME,
        str(APP_ROOT / "pro3_uart.py"),
    ]

    print("Running PyInstaller...")
    pyinstaller_main.run(args)


def run_pyinstaller_for_flash() -> None:
    if not FLASH_SPEC.exists():
        raise SystemExit(f"flash.spec not found at {FLASH_SPEC}")
    clean_path(BUILD_DIR)
    print("Building flash.exe via flash.spec...")
    pyinstaller_main.run(["--noconfirm", str(FLASH_SPEC)])


def copy_resources() -> None:
    if not DIST_DIR.exists():
        raise SystemExit(f"PyInstaller output not found: {DIST_DIR}")

    for src, dest in RESOURCE_ITEMS:
        src_path = APP_ROOT / src
        dest_path = DIST_DIR / dest
        if not src_path.exists():
            print(f"Warning: resource '{src}' not found, skipping.")
            continue

        if src_path.is_dir():
            if dest_path.exists():
                shutil.rmtree(dest_path)
            shutil.copytree(src_path, dest_path)
        else:
            dest_path.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(src_path, dest_path)
        print(f"Copied {src_path} -> {dest_path}")
        if src == "pylink":
            dll_path = dest_path / JLINK_DLL_NAME
            if not dll_path.exists():
                raise SystemExit(f"{JLINK_DLL_NAME} was not found in '{src_path}'.")


def copy_flash_executable() -> None:
    source = None
    for candidate in FLASH_OUTPUT_PATHS:
        if candidate.exists():
            source = candidate
            break
    if not source:
        print("Warning: flash.exe build not found, skipping copy.")
        return
    dest = DIST_DIR / FLASH_EXE_NAME
    shutil.copy2(source, dest)
    print(f"Copied {source} -> {dest}")


def verify_jlink_dll() -> None:
    if not JLINK_DLL_SOURCE.exists():
        raise SystemExit(
            f"{JLINK_DLL_NAME} is required but missing from '{PYLINK_DIR}'. "
            "Copy the DLL into the pylink folder before packaging."
        )


def main() -> None:
    cleanup_old_version_specs()
    verify_jlink_dll()
    run_pyinstaller_for_pro3()
    run_pyinstaller_for_flash()
    copy_resources()
    copy_flash_executable()
    exe_path = DIST_DIR / f"{DIST_NAME}.exe"
    if exe_path.exists():
        print(f"\nBuild complete: {exe_path}")
    else:
        print("\nBuild complete. Exe is located inside:", DIST_DIR)


if __name__ == "__main__":
    main()
