#!/usr/bin/env -S uv run --script
#
# /// script
# requires-python = ">=3.14"
# dependencies = ["Pillow", "rectpack", "lark[interegular]", "numpy"]
# ///

import argparse
import ast
import json
import sys
import textwrap
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path

# pip install rectpack numpy lark Pillow
import numpy as np
import rectpack
from lark import Discard, Lark, Transformer
from PIL import Image

# HACK: Ensure the shared module is visible for this script.
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from shared.glyph_mapping import CombineSource, Glyph, get_glyph_map
from shared.paths import PROJECT_PATHS, REPO_DIR, SHARED_SRC_DIR


def pack_sprites(
    glyphs: list[Glyph],
    output_dir: Path,
    font_idx: int,
    page_size: int = 256,
    padding: int = 1,
):
    """Create packed images with the glyph sprites, and a mapping JSON for the
    injector that will later convert them into a .bin injection file.
    """
    glyph_with_sources = {
        glyph.source.index: (glyph, glyph.source)
        for glyph in glyphs
    }
    if len(glyph_with_sources) == 0:
        print(f"No glyphs for font {font_idx}, skipping sprite packing.")
        return None

    packer = rectpack.newPacker(rotation=False)
    for sprite_index, (glyph, source) in glyph_with_sources.items():
        pixels, _ = source.load(glyph)
        height, width, _ = pixels.shape
        packer.add_rect(width + padding * 2, height + padding * 2, sprite_index)
    packer.add_bin(page_size, page_size, count=float("inf"))
    packer.pack()

    injector_mappings: list[dict] = []
    for dst_page_num, bin in enumerate(packer):
        if font_idx == 0:
            output_path = output_dir / f"alpha_sprites_{dst_page_num:02d}.png"
        else:
            output_path = output_dir / (
                f"alpha_sprites_font{font_idx}_{dst_page_num:02d}.png"
            )
        dst_pixels = np.zeros((page_size, page_size, 4))

        for rect in bin:
            dst_x = rect.x + padding
            dst_y = rect.y + padding
            width = rect.width - padding * 2
            height = rect.height - padding * 2
            glyph, source = glyph_with_sources[rect.rid]

            pixels, bbox = source.load(glyph)

            dst_pixels[
                dst_y : dst_y + height,
                dst_x : dst_x + width,
            ] = pixels

            injector_mappings.append(
                {
                    "mesh_num": source.index,
                    "filename": output_path.name,
                    "x": dst_x,
                    "y": dst_y,
                    "w": width,
                    "h": height,
                    "l": bbox.x + glyph.extra_x,
                    "t": bbox.y + glyph.extra_y,
                    "b": bbox.y + glyph.extra_y + bbox.h,
                    "r": bbox.x + glyph.extra_x + bbox.w,
                }
            )

        image = Image.fromarray(dst_pixels.astype("uint8"))
        image.save(output_path)
        print("Created a sprite sheet for injector in", output_path)

    if font_idx == 0:
        info_path = output_dir / "glyph_info.json"
    else:
        info_path = output_dir / f"glyph_info_font{font_idx}.json"
    info_path.write_text(json.dumps(injector_mappings))
    print("Saved mappings for injector to", info_path)
    return info_path


def generate_def_file(output_path: Path, glyphs: list[Glyph]):
    """Generate a .def file for the game itself, with the C macros containing
    information on how to render each glyph.
    """
    header = textwrap.dedent(
        r"""
        // This file is autogenerated. See tools/glyphs/generate_defs for details.

        """
    ).strip()

    class_map = {
        "R": "GLYPH_REVIEW_MARKER",
        "T": "GLYPH_TEXT",
        "C": "GLYPH_TEXT",
        "c": "GLYPH_TEXT",
        "I": "GLYPH_ICON",
    }

    with output_path.open("w") as handle:
        print(header, file=handle)
        for glyph in glyphs:
            args = [
                json.dumps(glyph.text, ensure_ascii=False),
                class_map[glyph.glyph_class],
                str(glyph.source.index),
            ]
            print("X_GLYPH_DEFINE(" + ", ".join(args) + ")", file=handle)

    print("Saved", output_path)


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    parser.add_argument("game", type=int)
    parser.add_argument("--injector-output-dir", type=Path, default=REPO_DIR)
    return parser.parse_args()


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

    def_path = SHARED_SRC_DIR / f"game/ui/text_autogen.def"
    input_dir = PROJECT_PATHS[args.game].data_dir / "glyphs"
    injector_output_dir = args.injector_output_dir

    glyphs = get_glyph_map(input_dir)

    fonts = sorted({glyph.font_idx for glyph in glyphs})
    for font_idx in fonts:
        font_glyphs = [glyph for glyph in glyphs if glyph.font_idx == font_idx]
        pack_sprites(font_glyphs, output_dir=injector_output_dir, font_idx=font_idx)

    default_glyphs = [glyph for glyph in glyphs if glyph.font_idx == 0]
    generate_def_file(def_path, default_glyphs)


if __name__ == "__main__":
    main()
