import sys
import os
import cv2
import numpy as np
import re
from PIL import Image

# Windows環境でのIntel CPU最適化バグを完全に回避するおまじない
os.environ["FLAGS_use_onednn"] = "0"
os.environ["FLAGS_use_mkldnn"] = "0"

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
from paddleocr import PaddleOCR

# 🚀 ベースとなるPopplerのフォルダを指定
POPPLER_BASE_PATH = r"C:\ut\poppler-26.02.0"

# 【劇的高速化】AIモデルを保持する変数
_ocr_instance = None

def get_ocr_instance():
    global _ocr_instance
    if _ocr_instance is None:
        # 縦書きの認識精度を安定させるため、use_angle_cls=True に変更しています
        _ocr_instance = PaddleOCR(use_angle_cls=True, lang='japan', enable_mkldnn=False)
    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:
            # 1. OCRの実行
            ocr = get_ocr_instance()
            result = ocr.ocr(cropped)

            if not result:
                self.result_ready.emit("(文字は検出されませんでした)")
                return

            # 💡 元のコードの強みである「どんな構造からでも文字列をすべて引っこ抜く」ロジックを完全再現
            def extract_all_strings(d):
                res = []
                if isinstance(d, str):
                    res.append(d.strip())
                elif isinstance(d, dict):
                    for v in d.values():
                        res.extend(extract_all_strings(v))
                elif isinstance(d, (list, tuple)):
                    for i in d:
                        res.extend(extract_all_strings(i))
                return res

            raw_strings = extract_all_strings(result)

            # 元の日本語判定フィルターの適用
            jp_pattern = re.compile(r'[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\u3000-\u303F]')
            clean_texts = []

            for s in raw_strings:
                if s.replace('.', '', 1).isdigit() or s in ['[', ']', '(', ')', ',', '.']:
                    continue
                if jp_pattern.search(s):
                    clean_texts.append(s)

            # 💡【解決策】取り出された行の順番を、出力の直前で丸ごとひっくり返します（右から左へ）
            # もしPaddleOCRが左の列から順に文字を拾っていても、これで右から左へ直ります
            reversed_texts = clean_texts[::-1]

            text_out = "\n".join(reversed_texts)

            if not text_out.strip():
                text_out = "(文字は検出されませんでした)"

            # 結果を画面に送信
            self.result_ready.emit(text_out)

        except Exception as e:
            import traceback
            error_details = traceback.format_exc()
            self.result_ready.emit(f"❌ エラーが発生しました:\n{error_details}")

# ==========================================
# 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)
        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("選択範囲 of 改行コードを削除して文章を繋げました。")
        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:
            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)

        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())
