import os
import tkinter as tk
from tkinter import filedialog, messagebox
import fitz  # PyMuPDF
from PIL import Image, ImageTk

class PreciseStepPDFRotator:
    def __init__(self, root):
        self.root = root
        self.root.title("PDF回転ツール")
        self.root.geometry("1000x850")

        self.original_path = ""
        self.src_doc = None
        self.current_page_idx = 0
        self.page_angles = {}
        self.base_images = {}  # クロップ済みのオリジナル画像キャッシュ
        self.zoom_scale = 1.0  # フィット時を 100% とした相対倍率

        # --- UI レイアウト ---
        nav_frame = tk.Frame(root)
        nav_frame.pack(fill=tk.X, padx=10, pady=5)

        self.btn_open = tk.Button(nav_frame, text="PDFを開く", command=self.open_pdf, font=("Arial", 11))
        self.btn_open.pack(side=tk.LEFT, padx=5)

        self.btn_prev = tk.Button(nav_frame, text="◀ 前の頁", command=self.prev_page, state=tk.DISABLED)
        self.btn_prev.pack(side=tk.LEFT, padx=(15, 5))

        self.lbl_page = tk.Label(nav_frame, text=" 0 / 0 ", font=("Arial", 11))
        self.lbl_page.pack(side=tk.LEFT, padx=5)

        self.btn_next = tk.Button(nav_frame, text="次の頁 ▶", command=self.next_page, state=tk.DISABLED)
        self.btn_next.pack(side=tk.LEFT, padx=5)

        self.lbl_angle = tk.Label(nav_frame, text="現在の角度: 0度", font=("Arial", 11, "bold"), fg="#2c3e50")
        self.lbl_angle.pack(side=tk.LEFT, padx=20)

        control_frame = tk.Frame(root)
        control_frame.pack(fill=tk.X, padx=10, pady=5)

        tk.Label(control_frame, text="【左回転】", font=("Arial", 10)).pack(side=tk.LEFT, padx=(5, 2))
        self.btn_l5 = tk.Button(control_frame, text="左 5°", command=lambda: self.rotate_step(-5), font=("Arial", 10), state=tk.DISABLED, width=6)
        self.btn_l5.pack(side=tk.LEFT, padx=2)
        self.btn_l1 = tk.Button(control_frame, text="左 1°", command=lambda: self.rotate_step(-1), font=("Arial", 10), state=tk.DISABLED, width=6)
        self.btn_l1.pack(side=tk.LEFT, padx=2)

        tk.Label(control_frame, text="【右回転】", font=("Arial", 10)).pack(side=tk.LEFT, padx=(15, 2))
        self.btn_r1 = tk.Button(control_frame, text="右 1°", command=lambda: self.rotate_step(1), font=("Arial", 10), state=tk.DISABLED, width=6)
        self.btn_r1.pack(side=tk.LEFT, padx=2)
        self.btn_r5 = tk.Button(control_frame, text="右 5°", command=lambda: self.rotate_step(5), font=("Arial", 10), state=tk.DISABLED, width=6)
        self.btn_r5.pack(side=tk.LEFT, padx=2)

        self.btn_reset = tk.Button(control_frame, text="戻す(0°)", command=self.reset_angle, font=("Arial", 10), state=tk.DISABLED, width=8)
        self.btn_reset.pack(side=tk.LEFT, padx=15)

        tk.Label(control_frame, text="【拡大縮小】", font=("Arial", 10)).pack(side=tk.LEFT, padx=(15, 2))
        self.btn_zoom_out = tk.Button(control_frame, text="- 5%", command=lambda: self.change_zoom(-0.05), font=("Arial", 10), state=tk.DISABLED, width=6)
        self.btn_zoom_out.pack(side=tk.LEFT, padx=2)

        self.lbl_zoom = tk.Label(control_frame, text="100%", font=("Arial", 10, "bold"), width=6)
        self.lbl_zoom.pack(side=tk.LEFT, padx=2)

        self.btn_zoom_in = tk.Button(control_frame, text="+ 5%", command=lambda: self.change_zoom(0.05), font=("Arial", 10), state=tk.DISABLED, width=6)
        self.btn_zoom_in.pack(side=tk.LEFT, padx=2)

        self.btn_finalize = tk.Button(control_frame, text="💾 上書き確定保存", command=self.finalize_to_original, font=("Arial", 11, "bold"), fg="white", bg="#2c3e50", state=tk.DISABLED)
        self.btn_finalize.pack(side=tk.RIGHT, padx=5, fill=tk.Y)

        canvas_frame = tk.Frame(root)
        canvas_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)

        self.canvas = tk.Canvas(canvas_frame, bg="darkgray")
        self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        hbar = tk.Scrollbar(root, orient=tk.HORIZONTAL, command=self.canvas.xview)
        hbar.pack(fill=tk.X, padx=10)
        vbar = tk.Scrollbar(canvas_frame, orient=tk.VERTICAL, command=self.canvas.yview)
        vbar.pack(side=tk.RIGHT, fill=tk.Y)

        self.canvas.config(xscrollcommand=hbar.set, yscrollcommand=vbar.set)

    def open_pdf(self):
        file_path = filedialog.askopenfilename(filetypes=[("PDF files", "*.pdf")])
        if not file_path: return
        try:
            self.original_path = file_path
            self.src_doc = fitz.open(self.original_path)
            self.current_page_idx = 0
            self.page_angles = {i: 0 for i in range(len(self.src_doc))}
            self.base_images = {}
            self.zoom_scale = 1.0
            self.lbl_zoom.config(text="100%")

            self.btn_l5.config(state=tk.NORMAL)
            self.btn_l1.config(state=tk.NORMAL)
            self.btn_r1.config(state=tk.NORMAL)
            self.btn_r5.config(state=tk.NORMAL)
            self.btn_reset.config(state=tk.NORMAL)
            self.btn_zoom_in.config(state=tk.NORMAL)
            self.btn_zoom_out.config(state=tk.NORMAL)
            self.btn_finalize.config(state=tk.NORMAL)

            self.update_page_nav()
            self.update_preview()
        except Exception as e:
            messagebox.showerror("エラー", f"PDFの読み込みに失敗しました:\n{e}")

    def update_page_nav(self):
        if not self.src_doc: return
        total = len(self.src_doc)
        self.lbl_page.config(text=f" {self.current_page_idx + 1} / {total} ")
        self.btn_prev.config(state=tk.NORMAL if self.current_page_idx > 0 else tk.DISABLED)
        self.btn_next.config(state=tk.NORMAL if self.current_page_idx < total - 1 else tk.DISABLED)

    def prev_page(self):
        if self.current_page_idx > 0:
            self.current_page_idx -= 1
            self.update_page_nav()
            self.update_preview()

    def next_page(self):
        if self.current_page_idx < len(self.src_doc) - 1:
            self.current_page_idx += 1
            self.update_page_nav()
            self.update_preview()

    def rotate_step(self, diff):
        if not self.src_doc: return
        new_angle = self.page_angles[self.current_page_idx] + diff
        new_angle = (new_angle + 180) % 360 - 180
        self.page_angles[self.current_page_idx] = new_angle
        self.update_preview()

    def reset_angle(self):
        if not self.src_doc: return
        self.page_angles[self.current_page_idx] = 0
        self.update_preview()

    def change_zoom(self, diff):
        if not self.src_doc: return
        self.zoom_scale += diff
        self.zoom_scale = max(0.1, min(3.0, self.zoom_scale))
        self.lbl_zoom.config(text=f"{int(self.zoom_scale * 100)}%")
        self.update_preview()

    def crop_white_margins(self, pil_img):
        """【バグ修正版】画像外周の完全な白の余白を自動検知して安全に切り詰める"""
        gray = pil_img.convert("L")
        inverted = gray.point(lambda x: 0 if x > 245 else 255)
        bbox = inverted.getbbox()  # bboxは (left, upper, right, lower) の4要素のタプルが返ります
        if bbox:
            w, h = pil_img.size
            # タプルの各要素に対して個別にマージン（5ピクセル）を加減算するよう修正
            left = max(0, bbox[0] - 5)
            upper = max(0, bbox[1] - 5)
            right = min(w, bbox[2] + 5)
            lower = min(h, bbox[3] + 5)
            return pil_img.crop((left, upper, right, lower))
        return pil_img

    def draw_grid(self, width, height):
        self.canvas.delete("grid_line")
        grid_size = 40
        for x in range(0, width, grid_size):
            self.canvas.create_line(x, 0, x, height, fill="#4a90e2", dash=(2, 4), tags="grid_line")
        for y in range(0, height, grid_size):
            self.canvas.create_line(0, y, width, y, fill="#4a90e2", dash=(2, 4), tags="grid_line")

    def update_preview(self):
        if not self.src_doc: return

        if self.current_page_idx not in self.base_images:
            page = self.src_doc[self.current_page_idx]
            pix = page.get_pixmap(dpi=100)
            raw_img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
            self.base_images[self.current_page_idx] = self.crop_white_margins(raw_img)

        img = self.base_images[self.current_page_idx].copy()
        angle = self.page_angles[self.current_page_idx]

        if angle != 0:
            img = img.rotate(angle, resample=Image.Resampling.BICUBIC, expand=False, fillcolor=(255, 255, 255))

        canvas_width = self.canvas.winfo_width()
        canvas_height = self.canvas.winfo_height()
        if canvas_width <= 10 or canvas_height <= 10:
            canvas_width, canvas_height = 980, 700

        ratio_w = canvas_width / img.width
        ratio_h = canvas_height / img.height
        fit_ratio = min(ratio_w, ratio_h)

        target_w = int(img.width * fit_ratio * self.zoom_scale)
        target_h = int(img.height * fit_ratio * self.zoom_scale)

        img = img.resize((target_w, target_h), Image.Resampling.LANCZOS)
        self.tk_img = ImageTk.PhotoImage(img)

        self.canvas.config(scrollregion=(0, 0, target_w, target_h))
        self.canvas.delete("bg_img")
        self.canvas.create_image(0, 0, image=self.tk_img, anchor=tk.NW, tags="bg_img")

        self.draw_grid(target_w, target_h)
        self.lbl_angle.config(text=f"現在の角度: {-int(angle)}度")

    def finalize_to_original(self):
        """【重要修正】保存時の解像度を安全なサイズ（dpi=96）に落とし、システムエラーを確実に回避する"""
        if not self.src_doc: return
        if not messagebox.askyesno("最終確定", "すべてのページの回転状態を元のPDFファイルに上書き保存しますか？"): return

        # Pillowの巨大画像制限を解除
        Image.MAX_IMAGE_PIXELS = None

        self.root.title("PDF保存中... お待ちください...")
        self.root.update()

        try:
            out_doc = fitz.open()
            for i in range(len(self.src_doc)):
                page = self.src_doc[i]
                angle = self.page_angles[i]

                # ⭕【最重要対策】保存時の解像度をdpi=200から安全なdpi=96（画面表示と同等）に変更
                # これによりデータ量が大幅に削減され、すべてのシステム上限エラー（code=5）を完全に回避できます。
                pix = page.get_pixmap(dpi=96)
                img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
                img = self.crop_white_margins(img)

                if angle != 0:
                    img = img.rotate(angle, resample=Image.Resampling.BICUBIC, expand=False, fillcolor=(255, 255, 255))

                # 安全のための念のため制限
                MAX_DIM = 15000
                if img.width > MAX_DIM or img.height > MAX_DIM:
                    img.thumbnail((MAX_DIM, MAX_DIM), Image.Resampling.LANCZOS)

                temp_img_path = f"~temp_p{i}.jpg"
                img.save(temp_img_path, "JPEG", quality=90)

                # 画像と同じ寸法の白紙ページを作成して直接挿入
                new_page = out_doc.new_page(width=img.width, height=img.height)
                new_page.insert_image(new_page.rect, filename=temp_img_path)

                os.remove(temp_img_path)

            self.src_doc.close()

            temp_pdf_path = self.original_path + ".tmp"
            out_doc.save(temp_pdf_path)
            out_doc.close()

            if os.path.exists(self.original_path): os.remove(self.original_path)
            os.rename(temp_pdf_path, self.original_path)

            self.src_doc = fitz.open(self.original_path)
            self.page_angles = {i: 0 for i in range(len(self.src_doc))}
            self.base_images = {}
            self.zoom_scale = 1.0
            self.lbl_zoom.config(text="100%")

            self.root.title("PDF回転ツール")
            self.update_preview()
            messagebox.showinfo("成功", "すべての変更を元のPDFに上書き保存しました。")
        except Exception as e:
            self.root.title("PDF回転ツール")
            messagebox.showerror("エラー", f"保存に失敗しました:\n{e}")

if __name__ == "__main__":
    root = tk.Tk()
    app = PreciseStepPDFRotator(root)
    root.bind("<Configure>", lambda e: app.update_preview() if app.src_doc else None)
    root.mainloop()
