#!/usr/bin/env python3
import argparse
import re
import shutil
from pathlib import Path
from subprocess import check_output, run

# Configuration
APP_NAME = "TR1X"
APP_BUNDLE_PATH = Path(f"/tmp/{APP_NAME}.app")
APP_BINARY_PATH = APP_BUNDLE_PATH / f"Contents/MacOS/{APP_NAME}"
FRAMEWORKS_PATH = APP_BUNDLE_PATH / "Contents/Frameworks"
IGNORE_LIB_PREFIXES = ("/usr/lib/", "/System/", "@executable_path")


# Other global state
LIBRARY_PATHS: set[Path] = set()


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description=(
            f"Copies shared libraries into the macOS app bundle "
            "for {APP_NAME}."
        )
    )
    parser.add_argument(
        "--copy-only",
        action="store_true",
        help="Only copy libraries, do not update links.",
    )
    parser.add_argument(
        "--links-only",
        action="store_true",
        help="Only update links, do not copy libraries.",
    )
    return parser.parse_args()


def should_ignore_lib(lib_path: str) -> bool:
    return any(lib_path.startswith(prefix) for prefix in IGNORE_LIB_PREFIXES)


def gather_libs(binary_path: Path) -> None:
    parent_path = binary_path.parent
    output = check_output(["otool", "-L", str(binary_path)], text=True)
    libs = [line.split()[0] for line in output.split("\n")[1:] if line]

    rpath_pattern = re.compile(r"@rpath/(.*)")

    for lib in libs:
        match = rpath_pattern.match(lib)
        lib_path = parent_path / (match.group(1) if match else lib)

        if (
            should_ignore_lib(str(lib_path))
            or lib_path in LIBRARY_PATHS
            or lib_path == binary_path
        ):
            continue

        LIBRARY_PATHS.add(lib_path)
        gather_libs(lib_path)


def copy_libs() -> None:
    FRAMEWORKS_PATH.mkdir(parents=True, exist_ok=True)
    for lib_path in LIBRARY_PATHS:
        target_path = FRAMEWORKS_PATH / lib_path.name
        if not target_path.exists():
            print(f"Copying {lib_path} to {target_path}")
            shutil.copy2(lib_path, target_path)


def update_links(binary_path: Path) -> None:
    output = check_output(["otool", "-L", str(binary_path)], text=True)
    libs = [line.split()[0] for line in output.split("\n")[1:] if line]

    for lib in libs:
        if should_ignore_lib(lib):
            continue

        lib_name = Path(lib).name
        target = f"@executable_path/../Frameworks/{lib_name}"
        print(f"Updating link for {lib_name} in {binary_path}")
        run(["install_name_tool", "-change", lib, target, str(binary_path)])

        if lib_name == binary_path.name:
            print(f"Updating id for {lib_name} in {binary_path}")
            run(["install_name_tool", "-id", target, str(binary_path)])


def main() -> None:
    args = parse_args()

    if args.copy_only or not args.links_only:
        gather_libs(APP_BINARY_PATH)
        copy_libs()

    if args.links_only or not args.copy_only:
        for lib_path in FRAMEWORKS_PATH.glob("*"):
            update_links(lib_path)

        update_links(APP_BINARY_PATH)

    print(f"Libraries for {APP_NAME} copied and updated.")


if __name__ == "__main__":
    main()
