import sys
import os
import cv2
import numpy as np
import re
from PIL import Image

from PySide6.QtCore import QThread, Signal, Slot, QTimer, QRect, QPoint, Qt
from PySide6.QtWidgets import (QApplication, QMainWindow, QPushButton, QTextEdit,
                             QVBoxLayout, QHBoxLayout, QWidget, QFileDialog, QLabel,
                             QScrollArea, QSplitter)
from PySide6.QtGui import QPixmap, QImage, QPainter, QPen, QColor, QDragEnterEvent, QDropEvent, QTextCursor
from pdf2image import convert_from_path, pdfinfo_from_path

# 🚀 【修正】正しいクラス名 DocumentAnalyzer をインポート
from yomitoku import DocumentAnalyzer

# 🚀 ベースとなるPopplerのフォルダを指定
POPPLER_BASE_PATH = r"C:\ut\poppler-26.02.0"

# 【劇的高速化】YomiTokuのAIモデルを保持する変数
_ocr_instance = None

def get_ocr_instance():
    global _ocr_instance
    if _ocr_instance is None:
        # 🚀 【修正】DocumentAnalyzerクラスで初期化
        # CPU環境でも動くよう device="cpu" を指定（GPUをお持ちなら "cuda" に変更可能です）
        _ocr_instance = DocumentAnalyzer(visualize=False, device="cpu")
    return _ocr_instance


# ==========================================
# 1. 範囲指定可能な画像表示用カスタムラベル
# ==========================================
class ImageLabel(QLabel):
    rect_selected = Signal(int, int, int, int)

    def __init__(self):
        super().__init__()
        self.start_pos = QPoint()
        self.end_pos = QPoint()
        self.is_drawing = False
        self.orig_pixmap = None
        self.setCursor(Qt.CrossCursor)

        self.setSizePolicy(self.sizePolicy().Policy.Fixed, self.sizePolicy().Policy.Fixed)
        self.setScaledContents(False)

    def set_pixmap(self, pixmap):
        self.orig_pixmap = pixmap
        self.setPixmap(pixmap)
        if pixmap:
            self.resize(pixmap.size())
            self.setMinimumSize(pixmap.size())

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton and self.orig_pixmap:
            self.start_pos = event.position().toPoint()
            self.end_pos = self.start_pos
            self.is_drawing = True
            self.update()

    def mouseMoveEvent(self, event):
        if self.is_drawing:
            self.end_pos = event.position().toPoint()
            self.update()

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton and self.is_drawing:
            self.is_drawing = False
            self.update()

            x1 = min(self.start_pos.x(), self.end_pos.x())
            y1 = min(self.start_pos.y(), self.end_pos.y())
            x2 = max(self.start_pos.x(), self.end_pos.x())
            y2 = max(self.start_pos.y(), self.end_pos.y())

            if (x2 - x1) > 5 and (y2 - y1) > 5:
                self.rect_selected.emit(x1, y1, x2, y2)

    def paintEvent(self, event):
        super().paintEvent(event)
        if self.is_drawing:
            painter = QPainter(self)
            pen = QPen(QColor(255, 0, 0), 2, Qt.DashLine)
            painter.setPen(pen)
            rect = QRect(self.start_pos, self.end_pos)
            painter.drawRect(rect)

# ==========================================
# 2. バックグラウンドでOCRを実行するスレッド
# ==========================================
class CropOCRWorker(QThread):
    result_ready = Signal(str)
    status_changed = Signal(str)

    def __init__(self, cv_image, x1, y1, x2, y2, scale_factor):
        super().__init__()
        self.cv_image = cv_image.copy() if cv_image is not None else None
        self.x1 = x1
        self.y1 = y1
        self.x2 = x2
        self.y2 = y2
        self.scale_factor = scale_factor

    def run(self):
        if self.cv_image is None:
            self.result_ready.emit("画像データが存在しません。")
            return

        self.status_changed.emit("選択エリアを解析中...")

        # 表示スケールから実際の画像座標に変換
        rx1 = int(self.x1 / self.scale_factor)
        ry1 = int(self.y1 / self.scale_factor)
        rx2 = int(self.x2 / self.scale_factor)
        ry2 = int(self.y2 / self.scale_factor)

        h, w = self.cv_image.shape[:2]
        rx1, rx2 = max(0, rx1), min(w, rx2)
        ry1, ry2 = max(0, ry1), min(h, ry2)

        cropped = self.cv_image[ry1:ry2, rx1:rx2]

        if cropped.size == 0:
            self.result_ready.emit("(有効な範囲が選択されませんでした)")
            return

        try:
            # DocumentAnalyzerを使って解析を実行
            ocr = get_ocr_instance()
            results, ocr_vis, layout_vis = ocr(cropped)

            # 🚀 すべてのデータ階層から文字列を漏らさず自動探索
            def deep_extract_strings(obj):
                res = []
                if isinstance(obj, str):
                    if obj.strip():
                        res.append(obj.strip())
                elif isinstance(obj, dict):
                    for v in obj.values():
                        res.extend(deep_extract_strings(v))
                elif isinstance(obj, (list, tuple, set)):
                    for item in obj:
                        res.extend(deep_extract_strings(item))
                else:
                    if hasattr(obj, "to_dict"):
                        try: res.extend(deep_extract_strings(obj.to_dict()))
                        except: pass
                    elif hasattr(obj, "dict"):
                        try: res.extend(deep_extract_strings(obj.dict()))
                        except: pass
                    elif hasattr(obj, "__dict__"):
                        try: res.extend(deep_extract_strings(obj.__dict__))
                        except: pass
                return res

            raw_strings = deep_extract_strings(results)

            # 🚀 【画期的解決策：スマート重複排除】
            # 1. まず完全に同じ文字列の重複を、読む順番（出現順）を維持したまま削る
            unique_strings = []
            seen = set()
            for s in raw_strings:
                if s not in seen:
                    unique_strings.append(s)
                    seen.add(s)

            # 2. 部分一致（重複した部屋に入っている細切れのゴミデータ）を徹底排除
            # 長い文章から順に判定し、すでに他の長い文章に含まれている短い一節は除外対象にする
            sorted_strings = sorted(unique_strings, key=len, reverse=True)
            exclude_set = set()
            joined_check_text = ""

            for s in sorted_strings:
                if s in joined_check_text:
                    exclude_set.add(s)
                else:
                    joined_check_text += "  " + s

            # 3. YomiTokuが並び替えた元の正しい読む順番（unique_strings）のまま、除外を免れた文言だけを最終採用
            clean_texts = []
            jp_pattern = re.compile(r'[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\u3000-\u303F]')

            for s in unique_strings:
                if s in exclude_set:
                    continue
                if s.replace('.', '', 1).isdigit() or s in ['[', ']', '(', ')', ',', '.']:
                    continue
                if jp_pattern.search(s):
                    clean_texts.append(s)

        except Exception as e:
            self.result_ready.emit(f"OCR起動エラー: {e}")
            return

        # テキストを行ごとに改行で結合
        text_out = "\n".join(clean_texts)

        if not text_out.strip():
            text_out = "(文字は検出されませんでした)"

        self.result_ready.emit(text_out)


# ==========================================
# 3. メインの画面クラス（改行削除・ページめくり対応）
# ==========================================
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("高精度クロップスキャン OCRツール (マルチページ対応)")
        self.resize(1200, 750)
        self.setAcceptDrops(True)

        # ページ管理用の変数
        self.current_file_path = None
        self.current_page = 1
        self.total_pages = 1

        self.current_cv_image = None
        self.scale_factor = 1.0
        self.elapsed_seconds = 0.0

        self.timer = QTimer()
        self.timeout_count = 0
        self.timer.timeout.connect(self.update_timer_label)

        # UI部品の作成
        self.btn_select = QPushButton("画像またはPDFを開く")

        # ページめくりボタンとページ番号表示
        self.btn_prev = QPushButton("◀ 前のページ")
        self.btn_next = QPushButton("次のページ ▶")
        self.lbl_page_num = QLabel("ページ: 0 / 0")
        self.lbl_page_num.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.btn_prev.setEnabled(False)
        self.btn_next.setEnabled(False)

        self.btn_reverse = QPushButton("🔄 行順を反転")

        # 選択範囲の改行を削除して文章を綺麗に繋げるボタン
        self.btn_remove_nl = QPushButton("✂️ 選択範囲の改行を削除")

        self.btn_save = QPushButton("テキスト保存")
        self.btn_save.setEnabled(False)
        self.lbl_status = QLabel("画像・PDFを開くか、ここにファイルをドラッグ＆ドロップしてください。")

        # 左右分割用スプリッター
        self.splitter = QSplitter(Qt.Orientation.Horizontal)
        self.splitter.setChildrenCollapsible(False)

        self.scroll_area = QScrollArea()
        self.img_label = ImageLabel()
        self.scroll_area.setWidget(self.img_label)
        self.scroll_area.setWidgetResizable(False)

        self.text_edit = QTextEdit()
        self.text_edit.setPlaceholderText("マウスで囲んだ部分のテキストがここに表示されます...")

        self.splitter.addWidget(self.scroll_area)
        self.splitter.addWidget(self.text_edit)

        # ページめくり用のコントロール用水平レイアウト
        page_layout = QHBoxLayout()
        page_layout.addWidget(self.btn_prev)
        page_layout.addWidget(self.lbl_page_num, stretch=1)
        page_layout.addWidget(self.btn_next)

        # トップナビゲーション全体のレイアウト
        top_layout = QHBoxLayout()
        top_layout.addWidget(self.btn_select)
        top_layout.addLayout(page_layout, stretch=1)
        top_layout.addWidget(self.btn_reverse)
        top_layout.addWidget(self.btn_remove_nl)
        top_layout.addWidget(self.btn_save)

        root_layout = QVBoxLayout()
        root_layout.addLayout(top_layout)
        root_layout.addWidget(self.lbl_status)
        root_layout.addWidget(self.splitter, stretch=1)

        container = QWidget()
        container.setLayout(root_layout)
        self.setCentralWidget(container)

        # イベントの接続
        self.btn_select.clicked.connect(self.select_file)
        self.btn_prev.clicked.connect(self.prev_page)
        self.btn_next.clicked.connect(self.next_page)
        self.btn_reverse.clicked.connect(self.reverse_text)
        self.btn_remove_nl.clicked.connect(self.remove_newlines)
        self.btn_save.clicked.connect(self.save_file)
        self.img_label.rect_selected.connect(self.on_rect_selected)

    def showEvent(self, event):
        super().showEvent(event)
        # 分割幅（左の画像エリア700px、右の文字エリア500px）を正しく再設定
        self.splitter.setSizes([700, 500])

    def reverse_text(self):
        cursor = self.text_edit.textCursor()
        if cursor.hasSelection():
            selected_text = cursor.selectedText()
            lines = selected_text.replace('\u2029', '\n').split('\n')
            reversed_text = '\n'.join(lines[::-1])
            cursor.insertText(reversed_text)
            self.lbl_status.setText("選択された部分の行順を反転しました。")
        else:
            current_text = self.text_edit.toPlainText()
            if current_text:
                lines = current_text.split('\n')
                self.text_edit.setText('\n'.join(lines[::-1]))
                self.lbl_status.setText("すべての行順を反転しました。")

    # 選択されたテキストの改行を取り除く機能
    def remove_newlines(self):
        cursor = self.text_edit.textCursor()

        # マウスで文字がハイライト選択されている場合のみ実行
        if cursor.hasSelection():
            selected_text = cursor.selectedText()

            # QtのQTextEdit特有の段落区切り文字（\u2029）と通常の改行をすべて空文字に置換
            cleaned_text = selected_text.replace('\u2029', '').replace('\n', '').replace('\r', '')

            # 選択中のテキストを、改行を除去したクリーンな文章に置き換える
            cursor.insertText(cleaned_text)
            self.lbl_status.setText("選択範囲の改行コードを削除して文章を繋げました。")
        else:
            self.lbl_status.setText("エラー: 改行を消したい文章の範囲を、右側のテキストエリアでマウス選択してください。")

    def dragEnterEvent(self, event: QDragEnterEvent):
        if event.mimeData().hasUrls():
            event.acceptProposedAction()

    def dropEvent(self, event: QDropEvent):
        urls = event.mimeData().urls()
        if urls:
            raw_path = urls[0].toLocalFile()
            if os.path.exists(raw_path):
                self.current_file_path = raw_path
                self.current_page = 1
                self.load_document(raw_path)

    def select_file(self):
        file_path, _ = QFileDialog.getOpenFileName(
            self, "ファイルを開く", "",
            "対応ファイル (*.pdf *.png *.jpg *.jpeg *.bmp)"
        )
        if file_path:
            self.current_file_path = file_path
            self.current_page = 1
            self.load_document(file_path)

    def prev_page(self):
        if self.current_page > 1:
            self.current_page -= 1
            self.load_document(self.current_file_path)

    def next_page(self):
        if self.current_page < self.total_pages:
            self.current_page += 1
            self.load_document(self.current_file_path)

    def update_page_buttons(self):
        self.lbl_page_num.setText(f"ページ: {self.current_page} / {self.total_pages}")
        self.btn_prev.setEnabled(self.current_page > 1)
        self.btn_next.setEnabled(self.current_page < self.total_pages)

    def load_document(self, file_path):
        if isinstance(file_path, (tuple, list)):
            file_path = file_path[0]

        ext = os.path.splitext(file_path)[1].lower()
        pil_img = None

        self.lbl_status.setText(f"ドキュメントの {self.current_page} ページ目を読み込み中...")
        QApplication.processEvents()

        possible_bins = [
            POPPLER_BASE_PATH,
            os.path.join(POPPLER_BASE_PATH, "bin"),
            os.path.join(POPPLER_BASE_PATH, "Library", "bin")
        ]
        actual_poppler_bin = None
        for p in possible_bins:
            if os.path.exists(os.path.join(p, "pdftoppm.exe")):
                actual_poppler_bin = p
                break

        if ext == '.pdf':
            if not actual_poppler_bin:
                self.lbl_status.setText(f"エラー: {POPPLER_BASE_PATH} 内に pdftoppm.exe が見つかりません。")
                return
            try:
                info = pdfinfo_from_path(file_path, poppler_path=actual_poppler_bin)
                self.total_pages = int(info.get("Pages", 1))

                images = convert_from_path(
                    file_path,
                    first_page=self.current_page,
                    last_page=self.current_page,
                    poppler_path=actual_poppler_bin
                )
                if images:
                    pil_img = images[0]
            except Exception as e:
                self.lbl_status.setText(f"PDF読み込みエラー: {e}")
                return
        elif ext in ['.png', '.jpg', '.jpeg', '.bmp']:
            self.total_pages = 1
            self.current_page = 1
            try:
                pil_img = Image.open(file_path)
            except Exception as e:
                self.lbl_status.setText(f"画像読み込みエラー: {e}")
                return

        self.update_page_buttons()

        if pil_img:
            # 画像をRGBに統一し、OpenCV形式のBGR画像に変換
            pil_img = pil_img.convert('RGB')
            self.current_cv_image = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)

            # 画面表示用に縮小処理
            display_height = 650
            w, h = pil_img.size
            self.scale_factor = display_height / h
            nw, nh = int(w * self.scale_factor), int(h * self.scale_factor)

            rgb_image = cv2.cvtColor(self.current_cv_image, cv2.COLOR_BGR2RGB)
            bytes_per_line = 3 * w
            qimg = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888)
            safe_qimg = qimg.copy()

            pixmap = QPixmap.fromImage(safe_qimg).scaled(nw, nh, Qt.KeepAspectRatio, Qt.SmoothTransformation)
            self.img_label.set_pixmap(pixmap)
            self.lbl_status.setText(f"{self.current_page} ページ目の読み込み成功！ 範囲をドラッグしてスキャンしてください。")
            self.btn_save.setEnabled(True)

    def on_rect_selected(self, x1, y1, x2, y2):
        if self.current_cv_image is None:
            return

        self.elapsed_seconds = 0.0
        self.lbl_status.setText("スキャン中... (0.0秒経過)")
        self.timer.start(100)

        # YomiToku仕様にアップデートしたOCRワーカー（スレッド）を起動
        self.worker = CropOCRWorker(self.current_cv_image, x1, y1, x2, y2, self.scale_factor)
        self.worker.status_changed.connect(self.lbl_status.setText)
        self.worker.result_ready.connect(self.on_ocr_finished)
        self.worker.start()

    @Slot(str)
    def on_ocr_finished(self, text):
        self.timer.stop()

        current_text = self.text_edit.toPlainText()
        if current_text:
            current_text += "\n\n"

        self.text_edit.setText(current_text + text)
        self.text_edit.moveCursor(QTextCursor.MoveOperation.End)
        self.lbl_status.setText(f"{self.current_page} ページ目のスキャン完了！ ({self.elapsed_seconds:.1f}秒) 必要に応じて右側で文章を選択し「改行を削除」を押してください。")

    def update_timer_label(self):
        self.elapsed_seconds += 0.1
        self.lbl_status.setText(f"スキャン中... ({self.elapsed_seconds:.1f}秒経過)")

    def save_file(self):
        file_path, _ = QFileDialog.getSaveFileName(
            self, "テキストを保存", "ocr_result.txt", "Text Files (*.txt)"
        )
        if file_path:
            with open(file_path, "w", encoding="utf-8") as f:
                f.write(self.text_edit.toPlainText())
            self.lbl_status.setText(f"保存しました: {file_path}")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())
