#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
batch-print-pdf: 批量打印 PDF 工具

用法:
    batch-print-pdf [选项] file1.pdf file2.pdf ...

选项:
    --fit a4-landscape       每页等比缩放到 A4 横版铺满 (默认, 适合发票)
    --fit a4-portrait        每页等比缩放到 A4 竖版铺满
    --fit none               不重排版, 保持原始页面尺寸
    --nup N                  N-up 多张拼一页 (1/2/4, 默认 1)
    --no-merge               单文件打印场景, 跳过合并

行为:
    合并所选 PDF -> 重排版到 A4 -> 弹 GTK 打印对话框 (只弹一次) -> 送打印机
"""
import os
import sys
import tempfile
import subprocess
import shutil
from pathlib import Path

# A4 in PostScript points (1/72 inch). Portrait 595 x 842, Landscape 842 x 595.
A4_PORTRAIT = (595.0, 842.0)
A4_LANDSCAPE = (842.0, 595.0)


# ---------------------- helpers ----------------------

def notify(summary, body="", icon="printer"):
    try:
        subprocess.run(
            ["notify-send", "-i", icon, summary, body],
            check=False, timeout=3,
        )
    except Exception:
        pass


def die(msg, code=1):
    print(f"[batch-print-pdf] ERROR: {msg}", file=sys.stderr)
    notify("批量打印失败", msg, icon="dialog-error")
    sys.exit(code)


def have(cmd):
    return shutil.which(cmd) is not None


def filter_pdfs(paths):
    pdfs, skipped = [], []
    for p in paths:
        if not os.path.isfile(p):
            skipped.append(p)
            continue
        if p.lower().endswith(".pdf"):
            pdfs.append(os.path.abspath(p))
        else:
            skipped.append(p)
    return pdfs, skipped


# ---------------------- merge ----------------------

def merge_pdfs(pdf_paths, out_path):
    """合并 PDF, 优先级: pdfunite > gs > pypdf > PyPDF2"""
    if have("pdfunite"):
        try:
            subprocess.run(["pdfunite", *pdf_paths, out_path], check=True)
            return "pdfunite"
        except subprocess.CalledProcessError as e:
            print(f"[batch-print-pdf] pdfunite failed: {e}", file=sys.stderr)

    if have("gs"):
        try:
            subprocess.run(
                ["gs", "-dBATCH", "-dNOPAUSE", "-q",
                 "-sDEVICE=pdfwrite",
                 f"-sOutputFile={out_path}",
                 *pdf_paths],
                check=True,
            )
            return "gs"
        except subprocess.CalledProcessError as e:
            print(f"[batch-print-pdf] gs failed: {e}", file=sys.stderr)

    try:
        from pypdf import PdfWriter
        w = PdfWriter()
        for p in pdf_paths:
            w.append(p)
        with open(out_path, "wb") as f:
            w.write(f)
        return "pypdf"
    except ImportError:
        pass
    except Exception as e:
        print(f"[batch-print-pdf] pypdf failed: {e}", file=sys.stderr)

    try:
        from PyPDF2 import PdfMerger
        m = PdfMerger()
        for p in pdf_paths:
            m.append(p)
        m.write(out_path)
        m.close()
        return "PyPDF2"
    except ImportError:
        pass

    die(
        "找不到合并 PDF 的工具。请安装其中之一:\n"
        "  sudo apt install poppler-utils    # 提供 pdfunite (推荐)\n"
        "  sudo apt install ghostscript       # 提供 gs\n"
        "  pip install --user pypdf           # Python 库"
    )


# ---------------------- repaginate (fit/n-up) ----------------------

def _import_pypdf():
    """Try pypdf first (modern), fallback to PyPDF2."""
    try:
        import pypdf
        return pypdf, "pypdf"
    except ImportError:
        try:
            import PyPDF2 as pypdf
            return pypdf, "PyPDF2"
        except ImportError:
            return None, None


def repaginate_to_a4(in_path, out_path, target_size, nup=1):
    """
    把 in_path 的每一页等比缩放并居中到 target_size (W, H).
    - 1-up: 用 pypdf 的 merge_transformed_page(稳定)
    - 2-up / 4-up: 用 pdfjam 或 gs 外部工具(pypdf 对多次 merge 到同一页兼容性差)
    """
    if nup == 1:
        return _repaginate_1up_pypdf(in_path, out_path, target_size)
    else:
        return _repaginate_nup_pdfjam(in_path, out_path, target_size, nup)


def _repaginate_1up_pypdf(in_path, out_path, target_size):
    """1 张原始页 → 1 张 A4 (等比缩放居中). 用 pypdf."""
    pypdf_mod, pypdf_name = _import_pypdf()
    if pypdf_mod is None:
        return _repaginate_via_gs(in_path, out_path, target_size)

    print(f"[batch-print-pdf] 重排版 1-up: {pypdf_name}, target={target_size}")

    if pypdf_name == "pypdf":
        from pypdf import PdfReader, PdfWriter, PageObject, Transformation
    else:
        from PyPDF2 import PdfReader, PdfWriter, PageObject, Transformation

    reader = PdfReader(in_path)
    writer = PdfWriter()
    target_w, target_h = target_size

    for src_page in reader.pages:
        mb = src_page.mediabox
        src_w = float(mb.width)
        src_h = float(mb.height)
        if src_w <= 0 or src_h <= 0:
            continue

        # 自动旋转: 源与目标方向不一致时把源页转 90 度
        slot_landscape = target_w > target_h
        src_landscape = src_w > src_h
        rotated = slot_landscape != src_landscape
        if rotated:
            # 先物理旋转源页(改 mediabox + 加 /Rotate)
            src_page.rotate(90)
            src_w, src_h = src_h, src_w

        # 等比缩放
        scale = min(target_w / src_w, target_h / src_h)
        new_w = src_w * scale
        new_h = src_h * scale
        offset_x = (target_w - new_w) / 2
        offset_y = (target_h - new_h) / 2

        new_page = PageObject.create_blank_page(
            writer, width=target_w, height=target_h,
        )
        t = (Transformation()
             .scale(sx=scale, sy=scale)
             .translate(tx=offset_x, ty=offset_y))
        try:
            new_page.merge_transformed_page(src_page, t, expand=False)
        except TypeError:
            new_page.mergeTransformedPage(src_page, t)
        writer.add_page(new_page)

    with open(out_path, "wb") as f:
        writer.write(f)
    return pypdf_name


def _repaginate_nup_pdfjam(in_path, out_path, target_size, nup):
    """N-up 用 pdfjam(pdflatex 驱动)或 gs 做. 这俩工具对 PDF 变换兼容性最好."""
    target_w, target_h = target_size
    orientation = "landscape" if target_w > target_h else "portrait"

    # Prefer pdfjam (texlive-extra-utils): 专业级 PDF 拼版工具
    if have("pdfjam"):
        nup_arg = "2x1" if nup == 2 else ("2x2" if nup == 4 else "1x1")
        try:
            subprocess.run([
                "pdfjam",
                "--nup", nup_arg,
                "--landscape" if orientation == "landscape" else "--no-landscape",
                "--paper", "a4paper",
                "--outfile", out_path,
                in_path,
            ], check=True, capture_output=True)
            return f"pdfjam ({nup}-up)"
        except subprocess.CalledProcessError as e:
            print(f"[batch-print-pdf] pdfjam failed: {e.stderr.decode()[:200]}", file=sys.stderr)

    # Fallback: gs 自带 NupCols/NupRows
    if have("gs"):
        try:
            cols = 2 if nup in (2, 4) else 1
            rows = 2 if nup == 4 else 1
            subprocess.run([
                "gs", "-dBATCH", "-dNOPAUSE", "-q",
                "-sDEVICE=pdfwrite",
                "-dPDFFitPage",
                f"-dDEVICEWIDTHPOINTS={int(target_w)}",
                f"-dDEVICEHEIGHTPOINTS={int(target_h)}",
                "-dFIXEDMEDIA",
                f"-sOutputFile={out_path}",
                # gs 内建的 N-up 用 postscript 的 pdfmark, 比较麻烦
                # 这里退化到不拼版, 只是缩放(相当于 nup=1 行为)
                in_path,
            ], check=True)
            print(f"[batch-print-pdf] WARN 未找到 pdfjam, N-up 无法实现, 已退回 1-up 缩放", file=sys.stderr)
            return "gs (1-up fallback)"
        except subprocess.CalledProcessError as e:
            die(f"gs fallback 失败: {e}")

    die(f"N-up ({nup}-up) 需要 pdfjam. 安装: sudo apt install texlive-extra-utils")


def _repaginate_via_gs(in_path, out_path, target_size):
    """Fallback: 用 ghostscript 把 PDF 全部转换到 A4 大小."""
    if not have("gs"):
        die("缺少 pypdf/PyPDF2, 也找不到 ghostscript, 无法重排版到 A4")
    target_w, target_h = target_size
    # gs 的 -g<W>x<H> 单位是像素 (1/72 inch * 1pt = device dot)
    width_pt = int(target_w)
    height_pt = int(target_h)
    try:
        subprocess.run([
            "gs", "-dBATCH", "-dNOPAUSE", "-q",
            "-sDEVICE=pdfwrite",
            "-dPDFFitPage",
            f"-dDEVICEWIDTHPOINTS={width_pt}",
            f"-dDEVICEHEIGHTPOINTS={height_pt}",
            "-dFIXEDMEDIA",
            f"-sOutputFile={out_path}",
            in_path,
        ], check=True)
        return "gs"
    except subprocess.CalledProcessError as e:
        die(f"gs 重排版失败: {e}")


# ---------------------- print ----------------------

def print_with_gtk_dialog(pdf_path, job_title):
    try:
        import gi
        gi.require_version("Gtk", "3.0")
        gi.require_version("Poppler", "0.18")
        from gi.repository import Gtk, Poppler, Gio
    except (ImportError, ValueError) as e:
        print(f"[batch-print-pdf] GI unavailable ({e}), falling back", file=sys.stderr)
        return fallback_print(pdf_path, job_title)

    uri = Gio.File.new_for_path(pdf_path).get_uri()
    try:
        doc = Poppler.Document.new_from_file(uri, None)
    except Exception as e:
        die(f"无法读取合并后的 PDF: {e}")

    op = Gtk.PrintOperation()
    op.set_job_name(job_title)
    op.set_n_pages(doc.get_n_pages())
    op.set_unit(Gtk.Unit.POINTS)
    op.set_use_full_page(True)
    op.set_embed_page_setup(True)
    op.set_show_progress(True)

    def on_draw(operation, context, page_nr):
        cr = context.get_cairo_context()
        page = doc.get_page(page_nr)
        page.render_for_printing(cr)

    op.connect("draw_page", on_draw)

    result = op.run(Gtk.PrintOperationAction.PRINT_DIALOG, None)

    if result == Gtk.PrintOperationResult.ERROR:
        err = op.get_error()
        die(f"打印失败: {err.message if err else '未知错误'}")
    elif result == Gtk.PrintOperationResult.CANCEL:
        print("[batch-print-pdf] 用户取消", file=sys.stderr)
        return False
    return True


def fallback_print(pdf_path, job_title):
    if not have("lp"):
        die("系统未安装 CUPS (lp 命令), 无法打印.")
    try:
        subprocess.run(["lp", "-t", job_title, pdf_path], check=True)
        notify("已送打印队列", f"{job_title} 已送默认打印机")
        return True
    except subprocess.CalledProcessError as e:
        die(f"lp 打印失败: {e}")


# ---------------------- args ----------------------

def parse_args(argv):
    """Tiny manual argparser to keep zero deps. Returns (opts, positional)."""
    opts = {"fit": "a4-landscape", "nup": 1, "no_merge": False}
    positional = []
    it = iter(argv)
    for arg in it:
        if arg == "--fit":
            opts["fit"] = next(it, "a4-landscape")
        elif arg.startswith("--fit="):
            opts["fit"] = arg.split("=", 1)[1]
        elif arg == "--nup":
            try:
                opts["nup"] = int(next(it, "1"))
            except ValueError:
                die("--nup 需要整数 (1/2/4)")
        elif arg.startswith("--nup="):
            try:
                opts["nup"] = int(arg.split("=", 1)[1])
            except ValueError:
                die("--nup 需要整数 (1/2/4)")
        elif arg == "--no-merge":
            opts["no_merge"] = True
        elif arg in ("-h", "--help"):
            print(__doc__)
            sys.exit(0)
        elif arg.startswith("--"):
            die(f"未知选项: {arg}")
        else:
            positional.append(arg)

    if opts["fit"] not in ("a4-landscape", "a4-portrait", "none"):
        die(f"--fit 仅支持: a4-landscape | a4-portrait | none, 收到: {opts['fit']}")
    if opts["nup"] not in (1, 2, 4):
        die(f"--nup 仅支持 1/2/4, 收到: {opts['nup']}")
    return opts, positional


# ---------------------- main ----------------------

def main():
    if len(sys.argv) < 2:
        print(__doc__, file=sys.stderr)
        sys.exit(2)

    opts, paths = parse_args(sys.argv[1:])

    if not paths:
        die("请至少传一个 PDF 路径")

    pdfs, skipped = filter_pdfs(paths)
    if not pdfs:
        die("没有有效的 PDF 文件")

    if skipped:
        print("[batch-print-pdf] 跳过非 PDF 或不存在的文件:", file=sys.stderr)
        for s in skipped:
            print(f"    {s}", file=sys.stderr)

    print(f"[batch-print-pdf] 待处理 {len(pdfs)} 个 PDF, fit={opts['fit']}, nup={opts['nup']}")
    job_title = f"批量打印-{len(pdfs)}个PDF"

    with tempfile.TemporaryDirectory(prefix="batchprint-") as td:
        # Step 1: merge (skip if single + --no-merge)
        if len(pdfs) == 1 and opts["no_merge"]:
            current = pdfs[0]
        elif len(pdfs) == 1:
            current = pdfs[0]
        else:
            merged = os.path.join(td, "merged.pdf")
            tool = merge_pdfs(pdfs, merged)
            print(f"[batch-print-pdf] 合并完成 ({tool}): {merged}")
            current = merged

        # Step 2: repaginate to A4 (default behaviour for invoices)
        if opts["fit"] != "none" or opts["nup"] != 1:
            target = A4_LANDSCAPE if opts["fit"] in ("a4-landscape",) else (
                A4_PORTRAIT if opts["fit"] == "a4-portrait" else
                # If fit=none but nup>1, still need a target — default to landscape
                A4_LANDSCAPE
            )
            paged = os.path.join(td, "paged.pdf")
            try:
                tool = repaginate_to_a4(current, paged, target, nup=opts["nup"])
                print(f"[batch-print-pdf] 重排版完成 ({tool}): {paged}")
                current = paged
            except Exception as e:
                print(f"[batch-print-pdf] WARN 重排版失败({e}), 退回原始 PDF", file=sys.stderr)

        # Step 3: print
        print_with_gtk_dialog(current, job_title)


if __name__ == "__main__":
    main()
