#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
pdf2pptx.py

PDFの各ページを画像としてPowerPointスライドに貼り込む変換モジュール。
convert.py から import して使えるように、convert(input_path, output_path, **kwargs)
を公開しています。

必要ライブラリ:
    pip install pymupdf python-pptx

単独実行例:
    python pdf2pptx.py input.pdf output.pptx
    python pdf2pptx.py input.pdf output.pptx --dpi 250 --slide-size wide
    python pdf2pptx.py input.pdf output.pptx --split-vertical-half

convert.py 経由の例:
    python convert.py . pptx --target="*.pdf" --pdf-dpi 250 --pdf-slide-size wide
    python convert.py . pptx --target="*.pdf" --pdf-split-vertical-half
"""

from __future__ import annotations

import argparse
from io import BytesIO
from pathlib import Path
from typing import Iterable, List, Tuple

import fitz  # PyMuPDF
from pptx import Presentation
from pptx.util import Inches


SLIDE_SIZE_CHOICES = ("pdf", "wide", "standard")


def _as_bool(value) -> bool:
    """文字列/数値/真偽値を bool に変換する。"""
    if isinstance(value, bool):
        return value
    if value is None:
        return False
    if isinstance(value, (int, float)):
        return bool(value)
    text = str(value).strip().lower()
    return text in ("1", "true", "yes", "y", "on")


def _get_first(kwargs, names, default=None):
    """kwargs から複数候補名のうち最初に存在する値を取り出す。"""
    for name in names:
        if name in kwargs and kwargs[name] is not None:
            return kwargs[name]
    return default


def fit_image_to_slide(img_w: int, img_h: int, slide_w: int, slide_h: int, margin: int = 0):
    """
    画像をスライド内にアスペクト比を保って収める位置とサイズを返す。

    img_w, img_h は pixel 単位、slide_w, slide_h, margin は PowerPoint の EMU 単位。
    戻り値 left, top, width, height も EMU 単位。
    """
    available_w = slide_w - 2 * margin
    available_h = slide_h - 2 * margin

    if available_w <= 0 or available_h <= 0:
        raise ValueError("Margin is too large for the selected slide size.")

    img_ratio = img_w / img_h
    slide_ratio = available_w / available_h

    if img_ratio > slide_ratio:
        width = available_w
        height = int(width / img_ratio)
    else:
        height = available_h
        width = int(height * img_ratio)

    left = int((slide_w - width) / 2)
    top = int((slide_h - height) / 2)

    return left, top, width, height


def set_slide_size(
    prs: Presentation,
    pdf_doc: fitz.Document,
    slide_size: str = "pdf",
    split_vertical_half: bool = False,
):
    """Presentation のスライドサイズを設定する。"""
    slide_size = str(slide_size).lower()

    if slide_size == "wide":
        # 16:9 widescreen
        prs.slide_width = Inches(13.333)
        prs.slide_height = Inches(7.5)
        return

    if slide_size == "standard":
        # 4:3 standard
        prs.slide_width = Inches(10)
        prs.slide_height = Inches(7.5)
        return

    if slide_size == "pdf":
        page = pdf_doc[0]
        rect = page.rect

        width_pt = rect.width
        height_pt = rect.height

        # 縦長PDFを上下半分に分割するときは、スライド高さも半分にする。
        if split_vertical_half:
            height_pt /= 2.0

        prs.slide_width = Inches(width_pt / 72.0)
        prs.slide_height = Inches(height_pt / 72.0)
        return

    raise ValueError(f"Unknown slide_size: {slide_size}. Choose from {SLIDE_SIZE_CHOICES}.")


def get_page_clips(page: fitz.Page, split_vertical_half: bool = False) -> List[Tuple[fitz.Rect, str]]:
    """
    1ページをそのまま使うか、上下2分割して使うかを返す。

    split_vertical_half=True の場合、戻り値は [(上半分, 'upper'), (下半分, 'lower')]。
    """
    rect = page.rect

    if not split_vertical_half:
        return [(rect, "full")]

    mid_y = (rect.y0 + rect.y1) / 2.0
    upper = fitz.Rect(rect.x0, rect.y0, rect.x1, mid_y)
    lower = fitz.Rect(rect.x0, mid_y, rect.x1, rect.y1)

    return [(upper, "upper"), (lower, "lower")]


def convert_pdf_to_pptx(
    pdf_path: str | Path,
    pptx_path: str | Path,
    dpi: int = 200,
    slide_size: str = "pdf",
    margin_inch: float = 0.0,
    split_vertical_half: bool = False,
    verbose: bool = True,
) -> bool:
    """
    PDFをPPTXへ変換する本体関数。

    Parameters
    ----------
    pdf_path : str | Path
        入力PDF。
    pptx_path : str | Path
        出力PPTX。
    dpi : int
        PDFを画像化するときのDPI。
    slide_size : {'pdf', 'wide', 'standard'}
        PPTXのスライドサイズ。
    margin_inch : float
        スライド内の余白。inch単位。
    split_vertical_half : bool
        Trueなら、各PDFページを上半分・下半分に分けて2枚のスライドにする。
    verbose : bool
        進捗表示するか。

    Returns
    -------
    bool
        変換に成功したら True。
    """
    pdf_path = Path(pdf_path)
    pptx_path = Path(pptx_path)

    if not pdf_path.exists():
        raise FileNotFoundError(f"PDF file not found: {pdf_path}")
    if pdf_path.suffix.lower() != ".pdf":
        raise ValueError(f"Input file is not a PDF: {pdf_path}")
    if dpi <= 0:
        raise ValueError("dpi must be a positive integer.")

    slide_size = str(slide_size).lower()
    if slide_size not in SLIDE_SIZE_CHOICES:
        raise ValueError(f"slide_size must be one of {SLIDE_SIZE_CHOICES}: {slide_size}")

    pptx_path.parent.mkdir(parents=True, exist_ok=True)

    doc = fitz.open(str(pdf_path))
    try:
        if len(doc) == 0:
            raise ValueError("PDF has no pages.")

        prs = Presentation()
        set_slide_size(
            prs,
            doc,
            slide_size=slide_size,
            split_vertical_half=split_vertical_half,
        )

        blank_layout = prs.slide_layouts[6]
        zoom = dpi / 72.0
        matrix = fitz.Matrix(zoom, zoom)
        margin = Inches(margin_inch)

        slide_count = 0

        for page_index, page in enumerate(doc):
            clips = get_page_clips(page, split_vertical_half=split_vertical_half)

            for clip, part_name in clips:
                slide_count += 1
                if verbose:
                    print(
                        f"Rendering PDF page {page_index + 1}/{len(doc)} "
                        f"({part_name}) -> slide {slide_count}"
                    )

                pix = page.get_pixmap(matrix=matrix, clip=clip, alpha=False)
                png_bytes = pix.tobytes("png")

                image_stream = BytesIO(png_bytes)
                image_stream.seek(0)

                slide = prs.slides.add_slide(blank_layout)
                left, top, width, height = fit_image_to_slide(
                    img_w=pix.width,
                    img_h=pix.height,
                    slide_w=prs.slide_width,
                    slide_h=prs.slide_height,
                    margin=margin,
                )

                slide.shapes.add_picture(
                    image_stream,
                    left,
                    top,
                    width=width,
                    height=height,
                )

        prs.save(str(pptx_path))
    finally:
        doc.close()

    if verbose:
        print(f"Saved: {pptx_path}")

    return True


def convert(input_path: str, output_path: str | None = None, **kwargs) -> bool:
    """
    convert.py から呼ばれる importable wrapper。

    convert.py の ConverterSpec では converter(input_path, output_path, **kwargs)
    として呼ばれるため、この関数名とシグネチャにしてあります。

    受け付ける主な kwargs:
        pdf_dpi / dpi
        pdf_slide_size / slide_size
        pdf_margin / margin / margin_inch
        pdf_split_vertical_half / split_vertical_half
        verbose
    """
    input_path = Path(input_path)
    if output_path is None:
        output_path = input_path.with_suffix(".pptx")

    dpi = int(_get_first(kwargs, ("pdf_dpi", "dpi"), 200))
    slide_size = _get_first(kwargs, ("pdf_slide_size", "slide_size"), "pdf")
    margin_inch = float(_get_first(kwargs, ("pdf_margin", "margin", "margin_inch"), 0.0))
    split_vertical_half = _as_bool(_get_first(
        kwargs,
        ("pdf_split_vertical_half", "split_vertical_half"),
        False,
    ))
    verbose = _as_bool(_get_first(kwargs, ("verbose",), True))

    return convert_pdf_to_pptx(
        pdf_path=input_path,
        pptx_path=output_path,
        dpi=dpi,
        slide_size=slide_size,
        margin_inch=margin_inch,
        split_vertical_half=split_vertical_half,
        verbose=verbose,
    )


def parse_args():
    parser = argparse.ArgumentParser(
        description="Convert PDF pages to PowerPoint slides as images."
    )
    parser.add_argument("pdf_path", help="Input PDF file")
    parser.add_argument("pptx_path", nargs="?", default=None, help="Output PPTX file")
    parser.add_argument("--dpi", type=int, default=200, help="Rendering DPI. Default: 200")
    parser.add_argument(
        "--slide-size",
        choices=SLIDE_SIZE_CHOICES,
        default="pdf",
        help="Slide size: pdf, wide, or standard. Default: pdf",
    )
    parser.add_argument("--margin", type=float, default=0.0, help="Margin in inches. Default: 0.0")
    parser.add_argument(
        "--split-vertical-half",
        action="store_true",
        help="Split each PDF page into upper and lower halves.",
    )
    parser.add_argument("--quiet", action="store_true", help="Suppress progress messages")
    parser.add_argument("--pause", action="store_true", help="Pause before terminate")
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    output_path = args.pptx_path or str(Path(args.pdf_path).with_suffix(".pptx"))
    convert_pdf_to_pptx(
        pdf_path=args.pdf_path,
        pptx_path=output_path,
        dpi=args.dpi,
        slide_size=args.slide_size,
        margin_inch=args.margin,
        split_vertical_half=args.split_vertical_half,
        verbose=not args.quiet,
    )
    
    if args.pause:
        input("\nPress ENTER to terminate>>\n")
        
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
