import zlib
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

from PIL import Image

from .settings import BuildConfig, PAGES_DIR, PDF_COMPRESSION_LEVEL, PRINT_DIR, worker_count


def write_pdf(config: BuildConfig) -> Path | None:
    pages = sorted(PAGES_DIR.glob("page_*.png"))
    if not pages:
        return None
    out_name = config.output_pdf_name.strip() or "blockminers_printable.pdf"
    if not out_name.lower().endswith(".pdf"):
        out_name += ".pdf"
    PRINT_DIR.mkdir(parents=True, exist_ok=True)
    out = PRINT_DIR / Path(out_name).name
    write_lossless_image_pdf(pages, out, config.dpi)
    return out


def write_lossless_image_pdf(page_paths: list[Path], out_path: Path, dpi: int) -> None:
    max_obj = 2 + len(page_paths) * 3
    offsets = [0] * (max_obj + 1)
    page_objects: list[int] = []

    def pdf_num(value: float) -> str:
        return f"{value:.4f}".rstrip("0").rstrip(".")

    def write_obj(f, obj_id: int, body: bytes) -> None:
        offsets[obj_id] = f.tell()
        f.write(f"{obj_id} 0 obj\n".encode("ascii"))
        f.write(body)
        f.write(b"\nendobj\n")

    def write_stream_obj(f, obj_id: int, dictionary: str, stream: bytes) -> None:
        header = f"<< {dictionary} /Length {len(stream)} >>\nstream\n".encode("ascii")
        write_obj(f, obj_id, header + stream + b"\nendstream")

    def compress_page(path: Path) -> tuple[int, int, float, float, bytes]:
        with Image.open(path).convert("RGB") as img:
            width_px, height_px = img.size
            width_pt = width_px / dpi * 72.0
            height_pt = height_px / dpi * 72.0
            compressed = zlib.compress(img.tobytes(), level=PDF_COMPRESSION_LEVEL)
        return width_px, height_px, width_pt, height_pt, compressed

    with ThreadPoolExecutor(max_workers=worker_count(len(page_paths))) as executor:
        compressed_pages = list(executor.map(compress_page, page_paths))

    with out_path.open("wb") as f:
        f.write(b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n")

        for index, (width_px, height_px, width_pt, height_pt, compressed) in enumerate(compressed_pages):
            page_obj = 3 + index * 3
            image_obj = page_obj + 1
            content_obj = page_obj + 2
            page_objects.append(page_obj)

            image_dict = (
                f"/Type /XObject /Subtype /Image /Width {width_px} /Height {height_px} "
                "/ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode"
            )
            write_stream_obj(f, image_obj, image_dict, compressed)

            content = (
                f"q\n{pdf_num(width_pt)} 0 0 {pdf_num(height_pt)} 0 0 cm\n/Im0 Do\nQ\n"
            ).encode("ascii")
            write_stream_obj(f, content_obj, "", content)

            page_body = (
                f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {pdf_num(width_pt)} {pdf_num(height_pt)}] "
                f"/Resources << /XObject << /Im0 {image_obj} 0 R >> >> "
                f"/Contents {content_obj} 0 R >>"
            ).encode("ascii")
            write_obj(f, page_obj, page_body)

        kids = " ".join(f"{obj_id} 0 R" for obj_id in page_objects)
        write_obj(f, 2, f"<< /Type /Pages /Kids [{kids}] /Count {len(page_objects)} >>".encode("ascii"))
        write_obj(f, 1, b"<< /Type /Catalog /Pages 2 0 R >>")

        xref_offset = f.tell()
        f.write(f"xref\n0 {max_obj + 1}\n".encode("ascii"))
        f.write(b"0000000000 65535 f \n")
        for obj_id in range(1, max_obj + 1):
            f.write(f"{offsets[obj_id]:010d} 00000 n \n".encode("ascii"))
        f.write(
            f"trailer\n<< /Size {max_obj + 1} /Root 1 0 R >>\nstartxref\n{xref_offset}\n%%EOF\n".encode(
                "ascii"
            )
        )
