#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
概要:
    PDFの各ページを画像としてPowerPointスライドに貼り込む変換モジュールです。
詳細説明:
    convert.pyからインポートして使えるように、convert関数を公開しています。
    必要ライブラリとして pymupdf と python-pptx が必要です。
主な機能:
    単独実行例として python pdf2pptx.py input.pdf output.pptx や、
    オプション付きで python pdf2pptx.py input.pdf output.pptx --dpi 250 のように実行可能です。
"""

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型に変換します。
    引数:
        :param value: 変換対象の値
        :type value: Any
    戻り値:
        :returns: 変換後の真偽値
        :rtype: 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):
    """
    概要:
        キーワード引数から複数候補名のうち最初に存在する値を取り出します。
    引数:
        :param kwargs: キーワード引数を格納した辞書
        :type kwargs: dict
        :param names: 検索するキー名のリストまたはタプル
        :type names: Iterable
        :param default: 見つからなかった場合に返すデフォルト値
        :type default: Any
    戻り値:
        :returns: 見つかった値またはデフォルト値
        :rtype: Any
    """
    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 はピクセル単位、slide_w と slide_h と margin は PowerPoint の EMU 単位として計算します。
        戻り値の left と top と width と height も EMU 単位となります。
    引数:
        :param img_w: 画像の幅
        :type img_w: int
        :param img_h: 画像の高さ
        :type img_h: int
        :param slide_w: スライドの幅
        :type slide_w: int
        :param slide_h: スライドの高さ
        :type slide_h: int
        :param margin: スライドの余白
        :type margin: int
    戻り値:
        :returns: left、top、width、heightのタプル
        :rtype: tuple
    例外:
        :raises ValueError: 選択されたスライドサイズに対してmarginが大きすぎる場合に発生します。
    """
    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 に応じてサイズを設定し、縦長PDFを上下半分に分割する場合はスライド高さも半分に調整します。
    引数:
        :param prs: PowerPointのPresentationオブジェクト
        :type prs: pptx.presentation.Presentation
        :param pdf_doc: PyMuPDFのDocumentオブジェクト
        :type pdf_doc: fitz.Document
        :param slide_size: スライドサイズを指定する文字列
        :type slide_size: str
        :param split_vertical_half: 縦長PDFを上下半分に分割する場合はTrue
        :type split_vertical_half: bool
    戻り値:
        :returns: なし
        :rtype: None
    例外:
        :raises ValueError: 不明なslide_sizeが指定された場合に発生します。
    """
    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 の場合、戻り値は上半分と下半分の矩形およびそれぞれの名称のタプルになります。
    引数:
        :param page: 対象のPDFページオブジェクト
        :type page: fitz.Page
        :param split_vertical_half: 上下2分割するかどうかを指定するフラグ
        :type split_vertical_half: bool
    戻り値:
        :returns: 矩形オブジェクトと名称のタプルのリスト
        :rtype: list
    """
    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へ変換する本体関数です。
    引数:
        :param pdf_path: 入力PDFのファイルパス
        :type pdf_path: str または pathlib.Path
        :param pptx_path: 出力PPTXのファイルパス
        :type pptx_path: str または pathlib.Path
        :param dpi: PDFを画像化するときの解像度
        :type dpi: int
        :param slide_size: PPTXのスライドサイズ指定文字列
        :type slide_size: str
        :param margin_inch: スライド内のインチ単位の余白
        :type margin_inch: float
        :param split_vertical_half: 各PDFページを上半分と下半分に分けて2枚のスライドにする場合はTrue
        :type split_vertical_half: bool
        :param verbose: 変換中の進捗メッセージを表示する場合はTrue
        :type verbose: bool
    戻り値:
        :returns: 変換に成功した場合はTrue
        :rtype: bool
    例外:
        :raises FileNotFoundError: 入力PDFファイルが見つからない場合に発生します。
        :raises ValueError: 入力ファイルがPDFでない場合やDPIが正の整数でない場合などに発生します。
    """
    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:
    """
    概要:
        外部スクリプトから呼ばれるためのラッパー関数です。
    詳細説明:
        キーワード引数 kwargs として、pdf_dpi や pdf_slide_size や pdf_margin や pdf_split_vertical_half などを受け付けます。
    引数:
        :param input_path: 入力ファイルのパス
        :type input_path: str
        :param output_path: 出力ファイルのパス
        :type output_path: str または None
        :param kwargs: オプション引数を格納する辞書
        :type kwargs: dict
    戻り値:
        :returns: 変換に成功した場合はTrue
        :rtype: bool
    """
    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():
    """
    概要:
        コマンドライン引数をパースします。
    戻り値:
        :returns: パースされたコマンドライン引数を格納したオブジェクト
        :rtype: argparse.Namespace
    """
    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:
    """
    概要:
        コマンドラインからの実行時のメイン処理を行います。
    戻り値:
        :returns: 終了コード
        :rtype: 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())