import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import fitz
from PIL import Image, ImageTk
import PIL.ImageOps


class PDFCropperApp:

    def __init__(self, root):
        self.root = root
        self.root.title("PDF 視覚的トリミングツール (OCR最適化版)")
        self.root.geometry("1100x750")

        # 状態管理変数
        self.doc = None
        self.pdf_path = ""
        self.current_page_idx = 0
        self.zoom = 1.0

        # トリミング範囲（左、上、右、下 の余白 pt）
        self.lm_pt = 40.0
        self.tm_pt = 40.0
        self.rm_pt = 40.0
        self.bm_pt = 40.0

        # マウス操作用
        self.drag_target = None
        self.offset_x = 0
        self.offset_y = 0
        self.pix_w = 0
        self.pix_h = 0

        # 適用範囲フラグ (all: 全ページ, select: 選択ページ, odd: 奇数のみ, even: 偶数のみ)
        self.apply_scope = tk.StringVar(value="all")
        self.selected_pages_str = tk.StringVar(value="1")

        self.create_widgets()

    def create_widgets(self):
        # ─── 上部コントロールパネル ───
        top_frame = tk.Frame(self.root, pady=10)
        top_frame.pack(side=tk.TOP, fill=tk.X)

        btn_open = tk.Button(
            top_frame, text="PDFファイルを開く", command=self.open_pdf
        )
        btn_open.pack(side=tk.LEFT, padx=10)

        self.lbl_info = tk.Label(top_frame, text="PDFを選択してください")
        self.lbl_info.pack(side=tk.LEFT, padx=10)

        # ─── メインエリア（左: 設定 / 右: プレビュー） ───
        main_frame = tk.Frame(self.root)
        main_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        # 左側：設定パネル
        side_panel = tk.LabelFrame(
            main_frame, text=" 操作・適用設定 (PowerPDF機能内蔵) ", padx=10, pady=10
        )
        side_panel.pack(side=tk.LEFT, fill=tk.Y, padx=10, pady=10)

        # ★PowerPDF機能1: 自動余白カットボタン
        btn_auto = tk.Button(
            side_panel,
            text="✨ 白い余白を自動検出してカット",
            bg="#0288d1",
            fg="white",
            font=("", 10, "bold"),
            command=self.auto_detect_margins,
        )
        btn_auto.pack(fill=tk.X, pady=5)

        ttk.Separator(side_panel, orient="horizontal").pack(
            fill=tk.X, pady=10
        )

        # 適用範囲の選択
        tk.Label(side_panel, text="適用対象（ページ範囲）:", font=("", 10, "bold")).pack(
            anchor="w"
        )
        tk.Radiobutton(
            side_panel, text="すべてのページに適用", variable=self.apply_scope, value="all"
        ).pack(anchor="w", pady=2)
        tk.Radiobutton(
            side_panel,
            text="奇数ページのみに適用 (1, 3, 5...)",
            variable=self.apply_scope,
            value="odd",
        ).pack(anchor="w", pady=2)
        tk.Radiobutton(
            side_panel,
            text="偶数ページのみに適用 (2, 4, 6...)",
            variable=self.apply_scope,
            value="even",
        ).pack(anchor="w", pady=2)
        tk.Radiobutton(
            side_panel,
            text="ページを個別指定して適用",
            variable=self.apply_scope,
            value="select",
        ).pack(anchor="w", pady=2)

        # ページ指定用テキストボックス
        self.ent_pages = tk.Entry(
            side_panel, textvariable=self.selected_pages_str, width=18
        )
        self.ent_pages.pack(anchor="w", padx=20, pady=2)
        tk.Label(
            side_panel,
            text="例: 1, 3, 5-8\n(カンマ区切り、ハイフン範囲)",
            fg="gray",
            justify="left",
        ).pack(anchor="w", padx=20)

        ttk.Separator(side_panel, orient="horizontal").pack(
            fill=tk.X, pady=10
        )

        # ページめくりと情報表示
        page_nav = tk.Frame(side_panel)
        page_nav.pack(fill=tk.X, pady=5)
        tk.Button(page_nav, text="◀ 前へ", command=self.prev_page).pack(
            side=tk.LEFT, expand=True
        )
        self.lbl_page_num = tk.Label(page_nav, text="0 / 0")
        self.lbl_page_num.pack(side=tk.LEFT, expand=True)
        tk.Button(page_nav, text="次へ ▶", command=self.next_page).pack(
            side=tk.LEFT, expand=True
        )

        # 現在の枠線数値の表示
        self.lbl_status_pt = tk.Label(
            side_panel, text="現在の余白:\n左:0pt 上:0pt 右:0pt 下:0pt", justify="left", fg="#555"
        )
        self.lbl_status_pt.pack(fill=tk.X, pady=10)

        # 出力ボタン
        btn_save = tk.Button(
            side_panel,
            text="トリミングして保存",
            bg="#4CAF50",
            fg="white",
            font=("", 11, "bold"),
            command=self.save_cropped_pdf,
        )
        btn_save.pack(fill=tk.X, side=tk.BOTTOM, pady=10)

        # 右側：プレビュー画面（キャンバス）
        self.canvas = tk.Canvas(main_frame, bg="darkgray")
        self.canvas.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=10, pady=10)

        # マウスイベントのバインド
        self.canvas.bind("<Motion>", self.on_mouse_move)
        self.canvas.bind("<ButtonPress-1>", self.on_button_press)
        self.canvas.bind("<B1-Motion>", self.on_mouse_drag)
        self.canvas.bind("<ButtonRelease-1>", self.on_button_release)

    def open_pdf(self):
        file_path = filedialog.askopenfilename(
            filetypes=[("PDF Files", "*.pdf")]
        )
        if not file_path:
            return

        self.pdf_path = file_path
        self.doc = fitz.open(file_path)
        self.current_page_idx = 0
        self.lbl_info.config(text=f"選択中: {file_path.split('/')[-1]}")

        # 初期枠線をページサイズの10%にする
        rect = self.doc[0].rect
        self.lm_pt = rect.width * 0.1
        self.tm_pt = rect.height * 0.1
        self.rm_pt = rect.width * 0.1
        self.bm_pt = rect.height * 0.1

        self.update_preview()

    def auto_detect_margins(self):
        """【PowerPDF機能】文字データだけでなく、スキャン画像（写真）の余白も自動検出する"""
        if not self.doc:
            messagebox.showwarning("警告", "PDFファイルが開かれていません。")
            return

        page = self.doc[self.current_page_idx]
        rect = page.rect

        # ─── 1. まずは通常の文字データやベクトル図形を探す ───
        bbox = fitz.Rect()

        # テキストの配置範囲
        blocks = page.get_text("blocks")
        for b in blocks:
            bbox = bbox | fitz.Rect(b[0], b[1], b[2], b[3])

        # 図形の配置範囲
        drawings = page.get_drawings()
        for d in drawings:
            if "rect" in d:
                bbox = bbox | d["rect"]

        # ─── 2. 文字データがない場合、スキャン画像の「色」から範囲を探す ───
        if bbox.is_empty or bbox.width < 10 or bbox.height < 10:
            mat = fitz.Matrix(2.0, 2.0)
            pix = page.get_pixmap(matrix=mat)

            # PyMuPDFの画像をPillow画像に安全に変換
            img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)

            # 確実なグレースケール変換と反転処理
            gray_img = img.convert("L")
            inv_img = PIL.ImageOps.invert(gray_img)

            # 20より明るい場所（何かが写っている場所）の境界ボックスを取得
            img_bbox = inv_img.getbbox()

            if img_bbox:
                # 元のPDFサイズ（pt）の座標に逆変換
                bbox = fitz.Rect(
                    img_bbox[0] / 2.0,
                    img_bbox[1] / 2.0,
                    img_bbox[2] / 2.0,
                    img_bbox[3] / 2.0
                )

        # ─── 3. 最終的な余白の計算 ───
        if not bbox or bbox.is_empty:
            messagebox.showinfo("情報", "このページからコンテンツを検出できませんでした。")
            return

        # 【OCR最適化】文字の端（ルビやハネ）の物理的な欠けを防ぐため、マージンを12.0pt（約4mm）に強化
        padding = 12.0
        self.lm_pt = max(0.0, bbox.x0 - padding)
        self.tm_pt = max(0.0, bbox.y0 - padding)
        self.rm_pt = max(0.0, rect.width - bbox.x1 - padding)
        self.bm_pt = max(0.0, rect.height - bbox.y1 - padding)

        # 画面の赤枠と影をリアルタイムに更新
        self.update_preview()

    def update_preview(self, rebuild_image=True):
        if not self.doc:
            return

        page = self.doc[self.current_page_idx]
        rect = page.rect
        self.lbl_page_num.config(
            text=f"{self.current_page_idx + 1} / {len(self.doc)}"
        )

        # 画面下のステータス文字（現在の余白pt）を更新
        self.lbl_status_pt.config(
            text=(
                f"現在の余白設定:\n"
                f"左: {self.lm_pt:.1f}pt | 上: {self.tm_pt:.1f}pt\n"
                f"右: {self.rm_pt:.1f}pt | 下: {self.bm_pt:.1f}pt"
            )
        )

        canvas_width = self.canvas.winfo_width()
        canvas_height = self.canvas.winfo_height()
        if canvas_width < 10:
            canvas_width, canvas_height = 700, 600

        if rebuild_image:
            zoom_w = (canvas_width - 40) / rect.width
            zoom_h = (canvas_height - 40) / rect.height
            self.zoom = min(zoom_w, zoom_h, 2.0)

            mat = fitz.Matrix(self.zoom, self.zoom)
            pix = page.get_pixmap(matrix=mat)

            img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
            self.photo_img = ImageTk.PhotoImage(img)

            self.pix_w = pix.width
            self.pix_h = pix.height
            self.offset_x = (canvas_width - self.pix_w) // 2
            self.offset_y = (canvas_height - self.pix_h) // 2

            # メモリ蓄積防止
            pix = None

        self.canvas.delete("all")
        self.canvas.create_image(
            self.offset_x, self.offset_y, anchor="nw", image=self.photo_img
        )

        # 赤枠の画面座標
        self.x0 = self.offset_x + (self.lm_pt * self.zoom)
        self.y0 = self.offset_y + (self.tm_pt * self.zoom)
        self.x1 = self.offset_x + self.pix_w - (self.rm_pt * self.zoom)
        self.y1 = self.offset_y + self.pix_h - (self.bm_pt * self.zoom)

        # 暗いマスク（外側の影）の描画
        self.canvas.create_rectangle(
            self.offset_x, self.offset_y, self.offset_x + self.pix_w, self.y0,
            fill="black", stipple="gray25", width=0
        )
        self.canvas.create_rectangle(
            self.offset_x, self.y0, self.x0, self.y1,
            fill="black", stipple="gray25", width=0
        )
        self.canvas.create_rectangle(
            self.x1, self.y0, self.offset_x + self.pix_w, self.y1,
            fill="black", stipple="gray25", width=0
        )
        self.canvas.create_rectangle(
            self.offset_x, self.y1, self.offset_x + self.pix_w, self.offset_y + self.pix_h,
            fill="black", stipple="gray25", width=0
        )

        # 赤枠の描画
        self.canvas.create_rectangle(
            self.x0, self.y0, self.x1, self.y1, outline="red", width=2
        )

    def get_mouse_target(self, x, y):
        tol = 8
        if not self.doc:
            return None

        in_x_range = (self.x0 - tol) <= x <= (self.x1 + tol)
        in_y_range = (self.y0 - tol) <= y <= (self.y1 + tol)

        if in_x_range and abs(y - self.y0) <= tol:
            return "T"
        if in_x_range and abs(y - self.y1) <= tol:
            return "B"
        if in_y_range and abs(x - self.x0) <= tol:
            return "L"
        if in_y_range and abs(x - self.x1) <= tol:
            return "R"
        return None

    def on_mouse_move(self, event):
        target = self.get_mouse_target(event.x, event.y)
        if target in ["T", "B"]:
            self.canvas.config(cursor="sb_v_double_arrow")
        elif target in ["L", "R"]:
            self.canvas.config(cursor="sb_h_double_arrow")
        else:
            self.canvas.config(cursor="")

    def on_button_press(self, event):
        self.drag_target = self.get_mouse_target(event.x, event.y)

    def on_mouse_drag(self, event):
        if not self.drag_target:
            return

        x, y = event.x, event.y
        x = max(self.offset_x, min(x, self.offset_x + self.pix_w))
        y = max(self.offset_y, min(y, self.offset_y + self.pix_h))

        if self.drag_target == "L":
            new_lm = (x - self.offset_x) / self.zoom
            if new_lm < (self.pix_w / self.zoom) - self.rm_pt - 10:
                self.lm_pt = max(0, new_lm)
        elif self.drag_target == "R":
            new_rm = (self.offset_x + self.pix_w - x) / self.zoom
            if new_rm < (self.pix_w / self.zoom) - self.lm_pt - 10:
                self.rm_pt = max(0, new_rm)
        elif self.drag_target == "T":
            new_tm = (y - self.offset_y) / self.zoom
            if new_tm < (self.pix_h / self.zoom) - self.bm_pt - 10:
                self.tm_pt = max(0, new_tm)
        elif self.drag_target == "B":
            new_bm = (self.offset_y + self.pix_h - y) / self.zoom
            if new_bm < (self.pix_h / self.zoom) - self.tm_pt - 10:
                self.bm_pt = max(0, new_bm)

        self.update_preview(rebuild_image=False)

    def on_button_release(self, event):
        self.drag_target = None
        self.update_preview(rebuild_image=True)

    def prev_page(self):
        if self.doc and self.current_page_idx > 0:
            self.current_page_idx -= 1
            self.update_preview()

    def next_page(self):
        if self.doc and self.current_page_idx < len(self.doc) - 1:
            self.current_page_idx += 1
            self.update_preview()

    def parse_pages_string(self, max_pages):
        pages = set()
        parts = self.selected_pages_str.get().split(",")
        for part in parts:
            part = part.strip()
            if not part:
                continue
            if "-" in part:
                try:
                    start, end = map(int, part.split("-"))
                    for p in range(start, end + 1):
                        if 1 <= p <= max_pages:
                            pages.add(p - 1)
                except ValueError:
                    continue
            else:
                try:
                    p = int(part)
                    if 1 <= p <= max_pages:
                        pages.add(p - 1)
                except ValueError:
                    continue
        return sorted(list(pages))

    def save_cropped_pdf(self):
        if not self.doc:
            messagebox.showwarning("警告", "PDFファイルが開かれていません。")
            return

        save_path = filedialog.asksaveasfilename(
            defaultextension=".pdf", filetypes=[("PDF Files", "*.pdf")]
        )
        if not save_path:
            return

        out_doc = fitz.open(self.pdf_path)
        total_pages = len(out_doc)

        # ─── 適用ページ範囲の絞り込み ───
        scope = self.apply_scope.get()
        if scope == "all":
            target_pages = list(range(total_pages))
        elif scope == "odd":
            target_pages = [i for i in range(total_pages) if (i + 1) % 2 != 0]
        elif scope == "even":
            target_pages = [i for i in range(total_pages) if (i + 1) % 2 == 0]
        else:
            target_pages = self.parse_pages_string(total_pages)

        if not target_pages:
            messagebox.showerror("エラー", "適用対象のページ指定が正しくありません。")
            out_doc.close()
            return

        # 各対象ページにトリミングを適用
        for idx in target_pages:
            page = out_doc[idx]
            rect = page.rect
            new_rect = fitz.Rect(
                rect.x0 + self.lm_pt,
                rect.y0 + self.tm_pt,
                rect.x1 - self.rm_pt,
                rect.y1 - self.bm_pt
            )
            # 【OCR最適化】CropBoxだけでなく、物理的な基準点であるMediaBoxも完全に同期させる
            # これにより、YomiTokuやNDL OCRが元の外枠データを誤って読みに行くのを完全にシャットアウトします
            page.set_cropbox(new_rect)
            page.set_mediabox(new_rect)

        # 保存処理
        try:
            out_doc.save(save_path, garbage=3, deflate=True)
            messagebox.showinfo("成功", f"トリミングが完了しました！\nOCRツールに安心して読み込ませることができます。\n\n保存先: {save_path}")
        except Exception as e:
            messagebox.showerror("エラー", f"ファイルの保存に失敗しました。\n{str(e)}")
        finally:
            out_doc.close()


if __name__ == "__main__":
    root = tk.Tk()
    app = PDFCropperApp(root)

    # 【バグ修正】Configureイベントが自分自身のサイズ変更（ウィンドウ変形）の時だけ動くように制御
    # これにより、ラベルテキスト変更などの内部イベントによる無限プレビュー更新ループを防ぎます
    def on_window_resize(event):
        if event.widget == root:
            app.update_preview()

    root.bind("<Configure>", on_window_resize)
    root.mainloop()
