#!/usr/bin/env python3
"""
upconvert-python.py

Utility that:
* shows a static pixel‑width reference table,
* asks for an up‑scaling factor (‑R) – default 3× (valid values: 2‑8),
* optionally enables the ice‑colors palette (‑i),
* finds the newest .ans (or any) file in a given directory,
* **strips every character outside ASCII 33‑126 from that file’s name**,
* deletes all other files in that directory,
* runs ansilove with the selected options,
* zips the produced PNG into ansiupconverted.zip.

All user‑visible output is forced to pure ASCII.
"""

import argparse
import pathlib
import shutil
import subprocess
import sys
import zipfile
from datetime import datetime

# ----------------------------------------------------------------------
# Configuration ---------------------------------------------------------
# Allowed scaling factors – 1× removed because ansilove cannot handle it
ALLOWED_SCALES = list(range(2, 9))   # 2‑8 are valid
DEFAULT_SCALE = 3                    # Default scaling factor (still 3×)

# Static mapping of scale → pixel width (displayed to the user)
PIXEL_MAP = {
    2: 1280,
    3: 1920,
    4: 2560,
    5: 3200,
    6: 3840,
    7: 4480,
    8: 5120,
}
# ----------------------------------------------------------------------


def _a(text: str) -> None:
    """
    Print *text* after stripping any non‑ASCII characters.
    Guarantees that only ASCII reaches the terminal.
    """
    ascii_text = text.encode('ascii', errors='ignore').decode('ascii')
    print(ascii_text)


def show_pixel_reference() -> None:
    """Display the static table that maps scale factors to pixel widths."""
    _a("\n--- Render-prompt pixel reference ---")
    _a("| Scale | Pixel width |")
    _a("|-------|-------------|")
    for scale in sorted(PIXEL_MAP):
        _a(f"| {scale}x    | {PIXEL_MAP[scale]} px |")
    _a("------------------------------------\n")


def ask_for_scale() -> int:
    """Prompt for a scaling factor and return a validated integer."""
    _a("\n=== ansilove up-scaling selection ===")
    _a("Available scale factors (default = 3):")
    for s in ALLOWED_SCALES:
        marker = " (default)" if s == DEFAULT_SCALE else ""
        _a(f"  {s}{marker}")

    while True:
        resp = input(f"\nEnter desired scale [{DEFAULT_SCALE}]: ").strip()
        if resp == "":
            return DEFAULT_SCALE
        if resp.isdigit() and int(resp) in ALLOWED_SCALES:
            return int(resp)
        _a("Please type a number from the list above (or press Enter for the default).")


def ask_for_ice_colors() -> bool:
    """Prompt whether to enable the ice‑colors palette."""
    _a("\n=== Ice-colors option ===")
    _a(
        "Ansilove can render the image using the special \"ice-colors\" palette.\n"
        "Controlled by the short flag -i."
    )
    while True:
        resp = input("Enable ice-colors? (y/N): ").strip().lower()
        if resp in ("y", "yes"):
            return True
        if resp in ("n", "no", ""):
            return False
        _a("Please answer with 'y' (yes) or 'n' (no).")


def find_newest_file(dir_path: pathlib.Path) -> pathlib.Path:
    """Return the newest regular file in *dir_path*."""
    files = [p for p in dir_path.iterdir() if p.is_file()]
    if not files:
        raise FileNotFoundError(f"No regular files found in {dir_path}")
    return max(files, key=lambda p: p.stat().st_mtime)


def delete_all_except(dir_path: pathlib.Path, keep: pathlib.Path) -> None:
    """Delete everything in *dir_path* except the *keep* path."""
    for p in dir_path.iterdir():
        if p == keep:
            continue
        try:
            if p.is_dir():
                shutil.rmtree(p)
            else:
                p.unlink()
        except Exception as exc:
            _a(f"[WARN] Could not delete {p}: {exc}")


def run_ansilove(input_file: pathlib.Path,
                 output_png: pathlib.Path,
                 scale: int,
                 ice: bool) -> None:
    """
    Call ansilove with short-form flags only:
        ansilove -R <scale> [-i] -o <output_png> <input_file>
    """
    cmd = ["ansilove", "-R", str(scale)]
    if ice:
        cmd.append("-i")
    cmd.extend(["-o", str(output_png), str(input_file)])

    result = subprocess.run(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
    )
    if result.returncode != 0:
        raise RuntimeError(
            f"ansilove failed (exit {result.returncode}):\n{result.stderr}"
        )


def create_zip(png_path: pathlib.Path, zip_path: pathlib.Path) -> None:
    """Create a zip containing *png_path*."""
    with zipfile.ZipFile(zip_path, mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
        zf.write(png_path, arcname=png_path.name)


def _sanitize_filename(name: str) -> str:
    """
    Keep only characters whose ASCII codes are 33‑126.
    Everything else (including spaces) is removed.
    """
    return "".join(ch for ch in name if 33 <= ord(ch) <= 126)


def main() -> int:
    parser = argparse.ArgumentParser(
        description=(
            "Convert the newest file in a directory to PNG with ansilove, "
            "showing a static pixel‑width reference first, then prompting for "
            "up‑scaling (-R, 2‑8) and optional ice‑colors (-i), finally zipping the result. "
            "All other files in the directory are deleted."
        )
    )
    parser.add_argument(
        "directory",
        type=pathlib.Path,
        help="Directory that holds the source files."
    )
    args = parser.parse_args()

    target_dir = args.directory.resolve()
    if not target_dir.is_dir():
        _a(f"[ERROR] Not a directory: {target_dir}")
        return 1

    # --------------------------------------------------------------
    # 1) Show pixel reference table (ASCII only)
    show_pixel_reference()

    # 2) Ask for scaling factor (2‑8 only)
    scale = ask_for_scale()

    # 3) Ask for ice‑colors preference
    use_ice = ask_for_ice_colors()
    _a(f"\n✅ Ice-colors {'enabled' if use_ice else 'disabled'}.\n")

    # 4) Locate newest file and clean the folder
    try:
        newest = find_newest_file(target_dir)

        # ----- SANITISE THE FILENAME ---------------------------------
        safe_name = _sanitize_filename(newest.name)
        if safe_name != newest.name:
            safe_path = target_dir / safe_name
            if safe_path.exists():
                _a(f"[ERROR] Sanitised filename already exists: {safe_name}")
                return 1
            newest.rename(safe_path)
            newest = safe_path
            _a(f"[INFO] Renamed file to safe name: {safe_name}")

        mod_time = datetime.fromtimestamp(newest.stat().st_mtime)
        _a(
            f"[INFO] Newest file: {newest.name} "
            f"(modified {mod_time})"
        )

        # No dimension calculations; ansilove handles sizing internally.
        delete_all_except(target_dir, newest)
        _a("[INFO] Deleted all other files.")

        # 5) Run ansilove with the selected flags
        png_path = target_dir / (newest.stem + ".png")
        run_ansilove(newest, png_path, scale, use_ice)
        _a(
            f"[INFO] ansilove rendered PNG at {scale}x scaling "
            f"{'(ice-colors)' if use_ice else ''}: {png_path.name}"
        )

        # 6) Package the PNG into a zip archive
        zip_path = target_dir / "ansiupconverted.zip"
        create_zip(png_path, zip_path)
        _a(f"[INFO] Created zip archive: {zip_path.name}")

        # (Optional) Remove the intermediate PNG if you only need the zip:
        # png_path.unlink()
        # _a("[INFO] Removed intermediate PNG file.")

        _a("\n🎉 SUCCESS – all steps completed.")
        return 0

    except Exception as exc:
        _a(f"[ERROR] {exc}")
        return 1


if __name__ == "__main__":
    sys.exit(main())
