import os
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
import sys
import cv2
import numpy as np
import re
import shutil
import subprocess
from datetime import datetime
from PIL import Image

# ==========================================
# 0. 【合体版の肝】YomiTokuを安全に裏方で動かすためのセルフ判定
# ==========================================
if "--cli-mode" in sys.argv:
    # GUIを一切起動せず、上書きされた環境下でYomiTokuの解析処理だけをピンポイントで実行します。
    # メインアプリのインポートと隔離されているため、不具合を100%回避できます。
    try:
        from yomitoku import DocumentAnalyzer

        # 引数から入力画像と出力先テキストを取得
        img_idx = sys.argv.index("--input") + 1
        out_idx = sys.argv.index("--output") + 1
        input_path = sys.argv[img_idx]
        output_path = sys.argv[out_idx]

        cropped = cv2.imread(input_path)
        if cropped is not None:
            analyzer = DocumentAnalyzer(visualize=False, device="cpu")
            results, _, _ = analyzer(cropped)

            # 🚀 元プログラムにあったYomiToku専用のスマート重複排除ロジック
            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)
            unique_strings = []
            seen = set()
            for s in raw_strings:
                if s not in seen:
                    unique_strings.append(s)
                    seen.add(s)

            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

            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)

            text_out = "\n".join(clean_texts)
            if not text_out.strip(): text_out = "(文字は検出されませんでした)"

            with open(output_path, "w", encoding="utf-8") as f:
                f.write(text_out)
    except Exception as e:
        # エラーが起きた場合はテキストファイルに書き出してメイン側に報告
        if "-o" in sys.argv or "--output" in sys.argv:
            try:
                out_idx = sys.argv.index("--output") + 1
                with open(sys.argv[out_idx], "w", encoding="utf-8") as f:
                    f.write(f"YomiToku裏方実行エラー: {e}")
            except: pass
    sys.exit(0)

# --- ここから下は通常のGUIアプリ用のインポート ---
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, QRadioButton, QButtonGroup)
from PySide6.QtGui import QPixmap, QImage, QPainter, QPen, QColor, QDragEnterEvent, QDropEvent, QTextCursor
from pdf2image import convert_from_path, pdfinfo_from_path

# 🚀 各種外部エンジンの基本パス指定
POPPLER_BASE_PATH = r"C:\ut\poppler-26.02.0"
NDL_OCR_COMMAND = r"C:\ut\ndlocr-lite-master\src\ocr.py"


# ==========================================
# 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, ocr_mode):
        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
        self.ocr_mode = ocr_mode  # "NDL" または "YomiToku"

    def run(self):
        if self.cv_image is None:
            self.result_ready.emit("画像データが存在しません。")
            return

        # タイムスタンプを付与した一意のテンポラリフォルダ名を作成
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
        temp_dir_name = f"ndl_ocr_temp_{timestamp}"
        temp_dir = os.path.abspath(temp_dir_name)
        os.makedirs(temp_dir, exist_ok=True)

        # 切り抜いた画像を一時保存
        img_input_path = os.path.join(temp_dir, "crop_input.png")
        h, w = self.cv_image.shape[:2]
        rx1 = max(0, int(self.x1 / self.scale_factor))
        ry1 = max(0, int(self.y1 / self.scale_factor))
        rx2 = min(w, int(self.x2 / self.scale_factor))
        ry2 = min(h, int(self.y2 / self.scale_factor))

        cropped = self.cv_image[ry1:ry2, rx1:rx2]
        if cropped.size == 0:
            self.result_ready.emit("(有効な範囲が選択されませんでした)")
            try: shutil.rmtree(temp_dir)
            except: pass
            return

        cv2.imwrite(img_input_path, cropped)
        output_dir = os.path.join(temp_dir, "ocr_out")
        os.makedirs(output_dir, exist_ok=True)

        txt_out_path = os.path.join(output_dir, "crop_input.txt")
        text_out = ""

        # 🚀 選択されたモードによって処理を分岐
        if self.ocr_mode == "NDL":
            self.status_changed.emit("NDL用の部品に切り替え中（数秒〜十数秒かかります）...")

            # 【対策1】裏でのpip自動更新がフリーズの原因になる場合、この1行を頭に「#」をつけて無効化してください
            subprocess.run([sys.executable, "-m", "pip", "install", "-r", r"C:\ut\ndlocr-lite-master\requirements.txt", "--user", "--quiet"])

            self.status_changed.emit("NDL OCRでスキャン中...")

            # 【対策2】一時フォルダの作成場所を、安全な C:\ut の中に強制隔離
            import shutil
            safe_output_dir = os.path.join(r"C:\ut", os.path.basename(output_dir))
            os.makedirs(safe_output_dir, exist_ok=True)
            safe_img_input_path = os.path.join(safe_output_dir, "crop_input.png")

            # 元の場所に画像があれば、安全な C:\ut 側にコピーして引き渡す
            if os.path.exists(img_input_path):
                shutil.copy2(img_input_path, safe_img_input_path)

            # コマンドの引数をすべて C:\ut 側の安全なパスに書き換える
            cmd = [sys.executable, NDL_OCR_COMMAND, "--sourceimg", safe_img_input_path, "--output", safe_output_dir]
            txt_out_path = os.path.join(safe_output_dir, "crop_input.txt")

            # 【対策3】text=False にしてWindowsの日本語パス破壊を防ぐ
            result_raw = subprocess.run(cmd, capture_output=True, text=False)

            # 【対策4】PyMuPDF等の警告文（文字コードエラー）を安全に受け流す
            class DecodedResult:
                stdout = result_raw.stdout.decode('cp932', errors='replace')
                stderr = result_raw.stderr.decode('cp932', errors='replace')
                returncode = result_raw.returncode

            result = DecodedResult

            if os.path.exists(txt_out_path):
                try:
                    with open(txt_out_path, "r", encoding="utf-8") as f:
                        text_out = f.read()
                except Exception as e: text_out = f"テキスト読み込みエラー: {e}"
            else:
                if result.returncode != 0:
                    err_msg = result.stderr.strip() if result.stderr else result.stdout.strip()
                    text_out = f"NDL OCRエラー詳細:\n{err_msg}" if err_msg else "NDLの解析スクリプトの起動に失敗しました。"
                else:
                    text_out = "(文字は検出されませんでした)"

        else:
            self.status_changed.emit("YomiToku用の部品に切り替え中（数秒〜十数秒かかります）...")
            # YomiTokuの依存関係を復旧
##            subprocess.run([sys.executable, "-m", "pip", "install", "networkx>=3.4.2", "reportlab>=4.4.1", "--user", "--quiet"])

            self.status_changed.emit("YomiTokuでスキャン中...")
            # 自分自身（sys.argv[0]）を --cli-mode で裏方として呼び出す高等テクニック
            cmd = [sys.executable, sys.argv[0], "--cli-mode", "--input", img_input_path, "--output", txt_out_path]
            result = subprocess.run(cmd, capture_output=True, text=True)

            if os.path.exists(txt_out_path):
                try:
                    with open(txt_out_path, "r", encoding="utf-8") as f:
                        text_out = f.read()
                except Exception as e: text_out = f"テキスト読み込みエラー: {e}"
            else:
                err_msg = result.stderr.strip() if result.stderr else result.stdout.strip()
                text_out = f"YomiTokuエラー詳細:\n{err_msg}" if err_msg else "YomiToku裏方処理の起動に失敗しました。"

        # 安全クリーンアップ
        try: shutil.rmtree(temp_dir)
        except: pass

        self.result_ready.emit(text_out)


# ==========================================
# 3. メインの画面クラス（合体・モード切り替え対応）
# ==========================================
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("高精度クロップスキャン OCRツール (NDL / YomiToku ハイブリッド)")
        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.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.radio_ndl = QRadioButton("🤖 NDLエンジン")
        self.radio_yomi = QRadioButton("🦆 YomiToku")
        self.radio_ndl.setChecked(True)  # デフォルトはNDL

        self.mode_group = QButtonGroup(self)
        self.mode_group.addButton(self.radio_ndl)
        self.mode_group.addButton(self.radio_yomi)

        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("マウスで囲んだ部分のテキストがここに表示されます...")

        # 🚀 【追加】老眼でもクッキリ見えるよう、右側の文字サイズを大きくします
        # 16 の数字を「18」や「20」に変えることで、好みの大きさにさらに調整できます
        self.text_edit.setStyleSheet("font-size: 12pt; font-family: 'Noto Serif JP', 'MS Gothic', sans-serif;")

        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)

        # モード選択ボタンを並べるレイアウト
        mode_layout = QHBoxLayout()
        mode_layout.addWidget(self.radio_ndl)
        mode_layout.addWidget(self.radio_yomi)

        top_layout = QHBoxLayout()
        top_layout.addWidget(self.btn_select)
        top_layout.addLayout(page_layout, stretch=1)
        top_layout.addLayout(mode_layout)  # 🚀 切り替えスイッチをナビに配置
        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()
            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:
            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)

        # 🚀 画面上のラジオボタンがどちらを選択しているかを判定
        ocr_mode = "NDL" if self.radio_ndl.isChecked() else "YomiToku"

        # モード情報をワーカーに引き渡して起動
        self.worker = CropOCRWorker(self.current_cv_image, x1, y1, x2, y2, self.scale_factor, ocr_mode)
        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)

        ocr_mode = "NDL" if self.radio_ndl.isChecked() else "YomiToku"
        self.lbl_status.setText(f"[{ocr_mode}モード] スキャン完了！ ({self.elapsed_seconds:.1f}秒) 必要に応じて文章を補正してください。")

    def update_timer_label(self):
        self.elapsed_seconds += 0.1
        # ステータスラベルの末尾の時間表示だけをリアルタイム更新
        current_status = self.lbl_status.text()
        if "(" in current_status and "秒経過" in current_status:
            base_status = current_status.split("(")[0].strip()
            self.lbl_status.setText(f"{base_status} ({self.elapsed_seconds:.1f}秒経過)")
        elif "中..." in current_status:
            self.lbl_status.setText(f"{current_status} ({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())
