
import os
import re
from pathlib import Path


from qgis.PyQt import uic, QtWidgets
from qgis.PyQt.QtWidgets import QApplication, QDialog, QMessageBox, QWidget


import numpy as np
import pandas as pd
from pyproj import CRS
import geopandas as gpd
from typing import Optional


import common
from urbanq.logging.logging_config import logger
from urbanq.function.qss import gradient_style, default_style


from urbanq.function.file import (
    export_gdf,
    keep_columns_gdf,
    load_geojson_gdf,
    load_txt_or_csv_df,
    load_json_df_or_gdf,
    load_layer_or_shp_gdf,
    update_shapefile_layer,
    df_to_empty_geometry_gdf,
)

from urbanq.function.widgetutils import (
    show_progress,
    update_progress,
)


from urbanq.menu.autoUI.fileRread_dockwidget import fileRreadDockWidget
from urbanq.menu.autoUI.fileSave_dockwidget import fileSaveDockWidget
from urbanq.menu.autoUI.fileSetting_dockwidget import fileSettingDockWidget
from urbanq.menu.autoUI.ImageDescription_dockwidget import ImageDescriptionDockWidget



FORM_CLASS, _ = uic.loadUiType(os.path.join(
    os.path.dirname(__file__), 'DistanceAnalysis_dockwidget_base.ui'))


class DistanceAnalysisDockWidget(QDialog, FORM_CLASS):  
    def __init__(self, parent=None):
        
        super(DistanceAnalysisDockWidget, self).__init__(parent)  
        
        
        
        
        
        self.setupUi(self)

        
        show_progress(self.progressBar, False)

        
        self.menuPushButton.setProperty("class", "boldText")
        self.nextStepPushButton.setProperty("class", "boldText")
        self.previousStepPushButton.setProperty("class", "boldText")

        
        self.menuPushButton.clicked.connect(self.go_back_to_data_conversion)

        
        self.nextStepPushButton.clicked.connect(lambda: self.next_previous_clicked(1))
        self.nextStepPushButton.clicked.connect(lambda: self.update_current_progress(self.stackedWidget.currentIndex()))
        self.nextStepPushButton.clicked.connect(lambda: self.load_menu_ui(self.stackedWidget.currentIndex()))

        
        self.previousStepPushButton.clicked.connect(lambda: self.next_previous_clicked(-1))
        self.previousStepPushButton.clicked.connect(lambda: self.update_current_progress(self.stackedWidget.currentIndex()))
        self.previousStepPushButton.clicked.connect(lambda: self.load_menu_ui(self.stackedWidget.currentIndex()))

        
        self.job_index = common.job_info.get("job_index") if common.job_info else None
        self.job_title = common.job_info.get("job_title") if common.job_info else None

        
        self.option_1, self.option_2 = self.get_widget_option(self.job_index, self.job_title)

        
        self.pages_and_files = self.configure_pages_and_files()

        
        self.update_current_progress(0)

        
        self.stackedWidget.setCurrentIndex(0)

        
        self.load_menu_ui(0)

    
    
    

    def configure_pages_and_files(self):
        
        try:
            pages = []

            
            pages.append((True, self.current_step_1, ImageDescriptionDockWidget, None, None))

            
            pages.append((True, self.current_step_2, fileRreadDockWidget, self.option_1, None))

            
            pages.append((True, self.current_step_3, fileRreadDockWidget, self.option_2, None))

            
            read_required = any([
                self.option_2["setting_by_text"],
                self.option_2["setting_by_array"],
                self.option_2["setting_by_expression"],
                self.option_2["setting_by_section"]["enabled"],
                self.option_2["setting_by_numeric"]["enabled"],
                self.option_2["setting_by_combo"]["enabled"],
            ])
            pages.append((read_required, self.current_step_4, fileSettingDockWidget, self.option_2, None))

            
            save_required = any([
                self.option_1["output_by_file"],
                self.option_1["output_by_field"],
                self.option_1["output_by_table"]
            ])
            pages.append((save_required, self.current_step_5, fileSaveDockWidget, self.option_1, None))

            return pages

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)

    def go_back_to_data_conversion(self):
        
        try:
            from urbanq.menu.spatialAnalysis.spatialAnalysis_dockwidget import spatialAnalysisDockWidget  
            parent_ui = spatialAnalysisDockWidget(self)  
            main_page_layout = self.parent().parent().findChild(QWidget, "page_spatialAnalysis").layout()
            if main_page_layout:
                
                for i in reversed(range(main_page_layout.count())):
                    main_page_layout.itemAt(i).widget().deleteLater()
                main_page_layout.addWidget(parent_ui)

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)

    def load_menu_ui(self, index):
        
        try:
            widget_enabled, widget_process, widget_class, widget_option, widget_instance = self.pages_and_files[index]
            page = self.stackedWidget.widget(index)

            option = self.option_1 if index == 1 else self.option_2

            
            if widget_instance is None:

                
                widget_instance = widget_class(self, option)
                page.layout().addWidget(widget_instance)
                self.pages_and_files[index] = (
                    self.pages_and_files[index][0],
                    self.pages_and_files[index][1],
                    self.pages_and_files[index][2],
                    self.pages_and_files[index][3],
                    widget_instance
                )

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)

    def update_current_progress(self, index):
        
        try:
            step = 1
            for i, (widget_enabled, widget_process, _, _, _) in enumerate(self.pages_and_files):
                if not widget_enabled:
                    widget_process.hide()
                    continue
                else:
                    updated_text = re.sub(r"\[\d+\]", f"[{step}]", widget_process.text())
                    widget_process.setText(updated_text)
                    step += 1

                
                widget_process.show()

                if i == index:
                    widget_process.setStyleSheet(gradient_style)
                else:
                    widget_process.setStyleSheet(default_style)

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)

    def get_safe_page_index(self, current_index: int, direction: int) -> int:
        
        try:
            new_index = current_index

            while True:
                
                new_index += direction

                
                new_index = max(0, min(new_index, len(self.pages_and_files) - 1))

                
                if self.pages_and_files[new_index][0]:
                    return new_index

                
                if new_index == 0 and direction == -1:
                    return current_index

                
                if new_index == len(self.pages_and_files) - 1 and direction == 1:
                    return current_index

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)

    def next_previous_clicked(self, direction):
        
        def get_last_valid_page_index(pages_and_files) -> int:
            
            for i in reversed(range(len(pages_and_files))):
                if pages_and_files[i][0]:
                    return i
            return -1  

        try:
            
            current_index = self.stackedWidget.currentIndex()

            
            if self.pages_and_files[current_index][0]:
                instance = self.pages_and_files[current_index][4]
                if direction > 0 and not instance.set_fileResults(1 if current_index != 2 else 2):
                    return

            
            new_index = self.get_safe_page_index(current_index, direction)

            
            last_page_index = get_last_valid_page_index(self.pages_and_files)

            
            self.nextStepPushButton.setText("실행하기 " if new_index == last_page_index else "다음 단계 ▶")

            
            self.stackedWidget.setCurrentIndex(new_index)

            
            if current_index == last_page_index and direction > 0:
                self.run_job_process()

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)

    
    
    

    def get_file_data_frame(self, source_file_type, source_file_path, file_path, file_encoding, file_delimiter, file_has_header):
        
        try:
            
            gdf = None

            
            if source_file_type == "shp":
                gdf = load_layer_or_shp_gdf(shp_path=file_path, file_encoding=file_encoding)

            
            elif source_file_type == "layer":
                qgs_project_layer = source_file_path
                gdf = load_layer_or_shp_gdf(layer=qgs_project_layer, file_encoding=file_encoding)

            
            elif source_file_type == "json":
                df, _ = load_json_df_or_gdf(file_path=file_path, file_encoding=file_encoding)
                gdf = df_to_empty_geometry_gdf(df)

            
            elif source_file_type == "geojson":
                gdf = load_geojson_gdf(file_path=file_path, file_encoding=file_encoding)

            
            elif source_file_type == "txt":
                df = load_txt_or_csv_df(file_path, file_encoding, file_delimiter, file_has_header)
                gdf = df_to_empty_geometry_gdf(df)

            
            elif source_file_type == "csv":
                df = load_txt_or_csv_df(file_path, file_encoding, file_delimiter, file_has_header)
                gdf = df_to_empty_geometry_gdf(df)

            
            elif source_file_type == "folder":
                df = load_txt_or_csv_df(file_path, file_encoding, file_delimiter, file_has_header)
                gdf = df_to_empty_geometry_gdf(df)

            if gdf is None:
                return

            return gdf

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)

    def run_job_process(self):
        
        try:
            
            show_progress(self.progressBar)

            
            total_files = len(common.fileInfo_2.file_preview) + 1  
            base_progress = 20  
            step_weight = (100 - base_progress) / total_files  
            current_step = 0  

            
            fileinfo = common.fileInfo_1
            file_preview_index = 0
            file_preview = fileinfo.file_preview[file_preview_index]

            
            source_file_type, source_file_path, _ = fileinfo.file_record.get_record()
            result_file_type, result_file_path, _ = fileinfo.result_record.get_record()

            
            file_field_selection = file_preview.get_selection_field()
            file_uid = file_preview.get_file_uid()
            file_is_field_check = file_preview.get_field_check()
            file_path, file_encoding, file_delimiter, file_has_header = file_preview.get_info()

            
            new_file_path = file_path if result_file_type == "layer" else result_file_path

            
            gdf = self.get_file_data_frame(source_file_type, source_file_path, file_path, file_encoding, file_delimiter, file_has_header)
            current_step += 1
            update_progress(self.progressBar, int(current_step * step_weight))

            
            status_flags = []
            result = self.run_job_by_index(gdf, file_uid, file_is_field_check, file_field_selection, current_step, step_weight)

            
            if result is None:
                status_flags.append(False)
            elif result is True:
                
                
                status_flags.append(True)

            if result is not None:
                try:
                    
                    if result_file_type == 'layer':

                        
                        layer_widget = self.pages_and_files[1][4].get_qgs_layer_widget()

                        
                        layer_widget_index = layer_widget.currentIndex()

                        
                        layer = source_file_path

                        
                        new_layer = update_shapefile_layer(layer, result)

                        
                        if 0 <= layer_widget_index < layer_widget.count():
                            layer_widget.setCurrentIndex(layer_widget_index)

                        
                        common.fileInfo_1.file_record.file_path[result_file_type] = new_layer

                        
                        status_flags.append(True)

                    else:
                        
                        if new_file_path:

                            
                            if isinstance(result, gpd.GeoDataFrame):
                                export_success = export_gdf(result, new_file_path)

                                
                                status_flags.append(export_success)

                            elif isinstance(result, list) and result:
                                
                                file_type, _, file_name = common.fileInfo_1.file_record.get_record()
                                base_dir = Path(new_file_path)
                                base_name = Path(file_name).stem
                                ext = f".{file_type}"

                                
                                export_success = []
                                for i, part in enumerate(result, start=1):
                                    output_path = base_dir / f"{base_name}_{i:03d}{ext}"
                                    export_success.append(export_gdf(part, output_path))

                                
                                status_flags.append(all(export_success))

                            else:
                                
                                QMessageBox.information(self, "파일 오류", "파일 저장 중 오류가 발생했습니다.", QMessageBox.Ok)
                                status_flags.append(False)

                except Exception as e:
                    
                    QMessageBox.information(self, "파일 오류", f"GeoDataFrame export 실패: {e}", QMessageBox.Ok)
                    status_flags.append(False)

                
                current_step += 1
                update_progress(self.progressBar, int(base_progress + current_step * step_weight))

            
            if status_flags and all(status_flags):
                update_progress(self.progressBar, 100)  
                QMessageBox.information(self, "알림", "축하합니다. 작업이 완료했습니다!", QMessageBox.Ok)

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)

        finally:
            show_progress(self.progressBar, False)

    
    
    

    
    def calculate_all_to_all_dist_fields(
            self,
            gdf_1: gpd.GeoDataFrame,
            gdf_2: gpd.GeoDataFrame,
            metric_epsg: int = 5179,
            prefix: str = "dist_"
    ):
        
        try:
            
            if gdf_1 is None or len(gdf_1) == 0:
                QMessageBox.information(self, "결과", "분석 대상 레이어(gdf_1)에 레코드가 없습니다.", QMessageBox.Ok)
                return None
            if gdf_2 is None or len(gdf_2) == 0:
                QMessageBox.information(self, "결과", "참조 레이어(gdf_2)에 레코드가 없습니다.", QMessageBox.Ok)
                return None

            
            if gdf_1.crs is None or str(gdf_1.crs).strip() == "":
                QMessageBox.information(
                    self, "좌표계 안내",
                    "분석 대상 레이어(gdf_1)에 좌표계(CRS)가 없습니다.\n"
                    "거리 계산은 좌표계가 필요하므로 CRS를 먼저 지정한 후 다시 실행해 주세요.",
                    QMessageBox.Ok
                )
                return None

            if gdf_2.crs is None or str(gdf_2.crs).strip() == "":
                QMessageBox.information(
                    self, "좌표계 안내",
                    "참조 레이어(gdf_2)에 좌표계(CRS)가 없습니다.\n"
                    "거리 계산은 좌표계가 필요하므로 CRS를 먼저 지정한 후 다시 실행해 주세요.",
                    QMessageBox.Ok
                )
                return None

            
            g1 = gdf_1.copy()
            g2 = gdf_2.copy()

            g1_valid = g1[~g1.geometry.isna() & ~g1.geometry.is_empty].copy()
            g2_valid = g2[~g2.geometry.isna() & ~g2.geometry.is_empty].copy()

            if g1_valid.empty:
                QMessageBox.information(self, "결과", "분석 대상 레이어(gdf_1)에 유효한 지오메트리가 없습니다.", QMessageBox.Ok)
                return None
            if g2_valid.empty:
                QMessageBox.information(self, "결과", "참조 레이어(gdf_2)에 유효한 지오메트리가 없습니다.", QMessageBox.Ok)
                return None

            
            if CRS.from_user_input(g1_valid.crs) != CRS.from_user_input(g2_valid.crs):
                g2_valid = g2_valid.to_crs(g1_valid.crs)

            
            crs_1 = CRS.from_user_input(g1_valid.crs)

            
            if crs_1.is_geographic:
                g1m = g1_valid.to_crs(epsg=metric_epsg)
                g2m = g2_valid.to_crs(epsg=metric_epsg)
            else:
                g1m = g1_valid
                g2m = g2_valid

            
            g2m = g2m.reset_index(drop=True)
            ref_geoms = list(g2m.geometry)
            ref_count = len(ref_geoms)

            if ref_count == 0:
                QMessageBox.information(self, "결과", "참조 레이어(gdf_2)에 유효한 지오메트리가 없습니다.", QMessageBox.Ok)
                return None

            
            est_ops = len(g1m) * ref_count  
            if est_ops >= 200000:
                reply = QMessageBox.question(
                    self,
                    "작업 안내",
                    "객체 수가 많아 전체 거리 계산에 시간이 오래 걸릴 수 있습니다.\n"
                    f"- 대상: {len(g1m)}개\n"
                    f"- 참조: {ref_count}개\n"
                    f"- 생성 필드 수: {ref_count}개 ({prefix}1 ~ {prefix}{ref_count})\n\n"
                    "계속 진행하시겠습니까?",
                    QMessageBox.Yes | QMessageBox.No,
                    QMessageBox.No
                )
                if reply == QMessageBox.No:
                    return None

            
            
            for i, ref in enumerate(ref_geoms, start=1):
                col = f"{prefix}{i}"
                g1m[col] = g1m.geometry.distance(ref).astype(float)

            
            out = gdf_1.copy()
            
            for i in range(1, ref_count + 1):
                col = f"{prefix}{i}"
                out[col] = np.nan

            
            for i in range(1, ref_count + 1):
                col = f"{prefix}{i}"
                out.loc[g1m.index, col] = g1m[col].values

            return out.copy()

        except Exception as e:
            QMessageBox.information(self, "작업 오류", "거리 계산 중 오류가 발생하였습니다.", QMessageBox.Ok)
            logger.error("에러 발생: %s", e, exc_info=True)
            return None

    
    def calculate_nearest_n_dist_and_attr(
            self,
            gdf_1: gpd.GeoDataFrame,
            gdf_2: gpd.GeoDataFrame,
            n: int,
            attr_field: str,
            metric_epsg: int = 5179,
            dist_prefix: str = "min_dist_",
            attr_prefix: str = "min_uid_"
    ):
        
        try:
            
            if gdf_1 is None or len(gdf_1) == 0:
                QMessageBox.information(self, "결과", "분석 대상 레이어(gdf_1)에 레코드가 없습니다.", QMessageBox.Ok)
                return None
            if gdf_2 is None or len(gdf_2) == 0:
                QMessageBox.information(self, "결과", "참조 레이어(gdf_2)에 레코드가 없습니다.", QMessageBox.Ok)
                return None

            
            if gdf_1.crs is None or str(gdf_1.crs).strip() == "":
                QMessageBox.information(
                    self, "좌표계 안내",
                    "분석 대상 레이어(gdf_1)에 좌표계(CRS)가 없습니다.\n"
                    "거리 계산은 좌표계가 필요하므로 CRS를 먼저 지정한 후 다시 실행해 주세요.",
                    QMessageBox.Ok
                )
                return None

            if gdf_2.crs is None or str(gdf_2.crs).strip() == "":
                QMessageBox.information(
                    self, "좌표계 안내",
                    "참조 레이어(gdf_2)에 좌표계(CRS)가 없습니다.\n"
                    "거리 계산은 좌표계가 필요하므로 CRS를 먼저 지정한 후 다시 실행해 주세요.",
                    QMessageBox.Ok
                )
                return None

            
            try:
                n = int(n)
            except Exception:
                QMessageBox.information(self, "입력 오류", "N 값은 정수여야 합니다.", QMessageBox.Ok)
                return None

            if n <= 0:
                QMessageBox.information(self, "입력 오류", "N 값은 1 이상이어야 합니다.", QMessageBox.Ok)
                return None

            
            if not attr_field or str(attr_field).strip() == "":
                QMessageBox.information(self, "입력 오류", "속성 필드명(attr_field)을 지정해 주세요.", QMessageBox.Ok)
                return None
            if attr_field not in gdf_2.columns:
                QMessageBox.information(
                    self, "입력 오류",
                    f"참조 레이어(gdf_2)에 '{attr_field}' 필드가 없습니다.\n"
                    f"사용 가능한 필드: {', '.join(list(gdf_2.columns)[:20])}"
                    + (" ..." if len(gdf_2.columns) > 20 else ""),
                    QMessageBox.Ok
                )
                return None

            
            g1 = gdf_1.copy()
            g2 = gdf_2.copy()

            g1_valid = g1[~g1.geometry.isna() & ~g1.geometry.is_empty].copy()
            g2_valid = g2[~g2.geometry.isna() & ~g2.geometry.is_empty].copy()

            if g1_valid.empty:
                QMessageBox.information(self, "결과", "분석 대상 레이어(gdf_1)에 유효한 지오메트리가 없습니다.", QMessageBox.Ok)
                return None
            if g2_valid.empty:
                QMessageBox.information(self, "결과", "참조 레이어(gdf_2)에 유효한 지오메트리가 없습니다.", QMessageBox.Ok)
                return None

            
            if CRS.from_user_input(g1_valid.crs) != CRS.from_user_input(g2_valid.crs):
                g2_valid = g2_valid.to_crs(g1_valid.crs)

            
            crs_1 = CRS.from_user_input(g1_valid.crs)

            
            if crs_1.is_geographic:
                g1m = g1_valid.to_crs(epsg=metric_epsg)
                g2m = g2_valid.to_crs(epsg=metric_epsg)
            else:
                g1m = g1_valid
                g2m = g2_valid

            
            ref_count = len(g2m)
            if ref_count == 0:
                QMessageBox.information(self, "결과", "참조 레이어(gdf_2)에 유효한 지오메트리가 없습니다.", QMessageBox.Ok)
                return None

            if n > ref_count:
                n = ref_count

            
            out = gdf_1.copy()
            for k in range(1, n + 1):
                out[f"{dist_prefix}{k}"] = np.nan
                out[f"{attr_prefix}{k}"] = None

            
            g2m = g2m.reset_index(drop=True)
            ref_geoms = list(g2m.geometry)
            ref_attrs = list(g2m[attr_field])

            
            est_ops = len(g1m) * ref_count
            if est_ops >= 200000:
                reply = QMessageBox.question(
                    self,
                    "작업 안내",
                    "객체 수가 많아 최근접 계산에 시간이 오래 걸릴 수 있습니다.\n"
                    f"- 대상: {len(g1m)}개\n"
                    f"- 참조: {ref_count}개\n"
                    f"- 최근접 N: {n}개\n\n"
                    "계속 진행하시겠습니까?",
                    QMessageBox.Yes | QMessageBox.No,
                    QMessageBox.No
                )
                if reply == QMessageBox.No:
                    return None

            
            for idx, geom in zip(g1m.index, g1m.geometry):
                dists = np.array([geom.distance(rg) for rg in ref_geoms], dtype=float)

                if dists.size == 0:
                    continue

                nearest_idx = np.argsort(dists)[:n]

                for rank, j in enumerate(nearest_idx, start=1):
                    out.loc[idx, f"{dist_prefix}{rank}"] = float(dists[j])
                    out.loc[idx, f"{attr_prefix}{rank}"] = ref_attrs[j]

            return out.copy()

        except Exception as e:
            QMessageBox.information(self, "작업 오류", "최근접 N개 계산 중 오류가 발생하였습니다.", QMessageBox.Ok)
            logger.error("에러 발생: %s", e, exc_info=True)
            return None

    
    def calculate_count_within_radius(
            self,
            gdf_1: gpd.GeoDataFrame,
            gdf_2: gpd.GeoDataFrame,
            radius: float,
            metric_epsg: int = 5179,
            count_field: str = "count",
    ):
        try:
            if gdf_1 is None or len(gdf_1) == 0:
                QMessageBox.information(self, "결과", "분석 대상 레이어(gdf_1)에 레코드가 없습니다.", QMessageBox.Ok)
                return None
            if gdf_2 is None or len(gdf_2) == 0:
                QMessageBox.information(self, "결과", "참조 레이어(gdf_2)에 레코드가 없습니다.", QMessageBox.Ok)
                return None

            if gdf_1.crs is None or str(gdf_1.crs).strip() == "":
                QMessageBox.information(self, "좌표계 안내",
                                        "분석 대상 레이어(gdf_1)에 좌표계(CRS)가 없습니다.\n"
                                        "거리 계산은 좌표계가 필요하므로 CRS를 먼저 지정한 후 다시 실행해 주세요.",
                                        QMessageBox.Ok)
                return None

            if gdf_2.crs is None or str(gdf_2.crs).strip() == "":
                QMessageBox.information(self, "좌표계 안내",
                                        "참조 레이어(gdf_2)에 좌표계(CRS)가 없습니다.\n"
                                        "거리 계산은 좌표계가 필요하므로 CRS를 먼저 지정한 후 다시 실행해 주세요.",
                                        QMessageBox.Ok)
                return None

            try:
                radius = float(radius)
            except Exception:
                QMessageBox.information(self, "입력 오류", "반경(radius) 값은 숫자(float)여야 합니다.", QMessageBox.Ok)
                return None

            if radius <= 0:
                QMessageBox.information(self, "입력 오류", "반경(radius) 값은 0보다 커야 합니다.", QMessageBox.Ok)
                return None

            
            g1 = gdf_1.copy()
            g2 = gdf_2.copy()

            g1_valid = g1[~g1.geometry.isna() & ~g1.geometry.is_empty].copy()
            g2_valid = g2[~g2.geometry.isna() & ~g2.geometry.is_empty].copy()

            if g1_valid.empty:
                QMessageBox.information(self, "결과", "분석 대상 레이어(gdf_1)에 유효한 지오메트리가 없습니다.", QMessageBox.Ok)
                return None
            if g2_valid.empty:
                QMessageBox.information(self, "결과", "참조 레이어(gdf_2)에 유효한 지오메트리가 없습니다.", QMessageBox.Ok)
                return None

            
            if CRS.from_user_input(g1_valid.crs) != CRS.from_user_input(g2_valid.crs):
                g2_valid = g2_valid.to_crs(g1_valid.crs)

            crs_1 = CRS.from_user_input(g1_valid.crs)

            
            if crs_1.is_geographic:
                g1m = g1_valid.to_crs(epsg=metric_epsg)
                g2m = g2_valid.to_crs(epsg=metric_epsg)
            else:
                g1m = g1_valid
                g2m = g2_valid

            
            out = gdf_1.copy()
            out[count_field] = 0

            ref_geoms = list(g2m.geometry)

            
            if len(ref_geoms) * len(g1m) >= 200000:
                reply = QMessageBox.question(
                    self,
                    "작업 안내",
                    "객체 수가 많아 반경 내 개수 계산에 시간이 오래 걸릴 수 있습니다.\n\n"
                    "계속 진행하시겠습니까?",
                    QMessageBox.Yes | QMessageBox.No,
                    QMessageBox.No
                )
                if reply == QMessageBox.No:
                    return None

            for idx, geom in zip(g1m.index, g1m.geometry):
                dists = np.array([geom.distance(rg) for rg in ref_geoms], dtype=float)
                cnt = int(np.sum(dists <= radius)) if dists.size else 0
                out.loc[idx, count_field] = cnt

            return out.copy()

        except Exception as e:
            QMessageBox.information(self, "작업 오류", "반경 내 객체 수 분석 중 오류가 발생하였습니다.", QMessageBox.Ok)
            logger.error("에러 발생: %s", e, exc_info=True)
            return None

    
    

    
    
    

    @staticmethod
    def get_widget_option(job_index, job_title):
        
        try:
            option_1 = None  
            option_2 = None  
            job_title = job_title[2:]

            if job_index == 0:
                option_1 = {
                    "apply_basic_qss": True,

                    "disable_file_type_layer": True,
                    "disable_file_type_shp": True,
                    "disable_file_type_json": False,
                    "disable_file_type_txtcsv": False,
                    "disable_file_type_fold": False,

                    "show_uid_in_file": False,
                    "show_tuid_in_file": False,
                    "show_field_in_file": False,

                    "setting_by_text": False,
                    "setting_by_array": False,
                    "setting_by_expression": False,
                    "setting_by_section": {"enabled": False, "value_type": "int"},
                    "setting_by_numeric": {"enabled": False, "value_type": "int"},
                    "setting_by_combo": {"enabled": False, "items": []},

                    "output_by_file": True,  
                    "output_by_field": False,
                    "output_by_table": False,
                }
                option_2 = {
                    "apply_basic_qss": False,

                    "disable_file_type_layer": True,
                    "disable_file_type_shp": True,
                    "disable_file_type_json": False,
                    "disable_file_type_txtcsv": False,
                    "disable_file_type_fold": False,

                    "show_uid_in_file": False,
                    "show_tuid_in_file": False,
                    "show_field_in_file": False,

                    "setting_by_text": False,
                    "setting_by_array": False,
                    "setting_by_expression": False,
                    "setting_by_section": {"enabled": False, "value_type": "int"},
                    "setting_by_numeric": {"enabled": False, "value_type": "int"},
                    "setting_by_combo": {"enabled": False, "items": []},

                    "output_by_file": True,  
                    "output_by_field": False,
                    "output_by_table": False,
                }
            if job_index == 1:
                option_1 = {
                    "apply_basic_qss": True,

                    "disable_file_type_layer": True,
                    "disable_file_type_shp": True,
                    "disable_file_type_json": False,
                    "disable_file_type_txtcsv": False,
                    "disable_file_type_fold": False,

                    "show_uid_in_file": False,
                    "show_tuid_in_file": False,
                    "show_field_in_file": False,

                    "setting_by_text": False,
                    "setting_by_array": False,
                    "setting_by_expression": False,
                    "setting_by_section": {"enabled": False, "value_type": "int"},
                    "setting_by_numeric": {"enabled": True, "value_type": "int"},
                    "setting_by_combo": {"enabled": False, "items": []},

                    "output_by_file": True,  
                    "output_by_field": False,
                    "output_by_table": False,

                    "SETTING_NUMERIC": [
                        '최근접 객체 개수(N) 설정',
                        'N 값 입력: ',
                        '참조 레이어에서 계산할 최근접 객체의 개수(N)를 입력해 주세요.'
                    ],
                }
                option_2 = {
                    "apply_basic_qss": False,

                    "disable_file_type_layer": True,
                    "disable_file_type_shp": True,
                    "disable_file_type_json": False,
                    "disable_file_type_txtcsv": False,
                    "disable_file_type_fold": False,

                    "show_uid_in_file": False,
                    "show_tuid_in_file": True,
                    "show_field_in_file": False,

                    "setting_by_text": False,
                    "setting_by_array": False,
                    "setting_by_expression": False,
                    "setting_by_section": {"enabled": False, "value_type": "int"},
                    "setting_by_numeric": {"enabled": True, "value_type": "int"},
                    "setting_by_combo": {"enabled": False, "items": []},

                    "output_by_file": True,  
                    "output_by_field": False,
                    "output_by_table": False,

                    "FILE_TUID": [
                        '참조 객체 속성 필드 선택',
                        '속성 필드 선택: ',
                        '최근접 N개 객체에서 함께 저장할 참조 레이어의 속성 필드를 선택해 주세요.'
                    ],

                    "SETTING_NUMERIC": [
                        '최근접 객체 개수(N) 설정',
                        'N 값 입력: ',
                        '참조 레이어에서 계산할 최근접 객체의 개수(N)를 입력해 주세요.'
                    ],
                }
            if job_index == 2:
                option_1 = {
                    "apply_basic_qss": True,

                    "disable_file_type_layer": True,
                    "disable_file_type_shp": True,
                    "disable_file_type_json": False,
                    "disable_file_type_txtcsv": False,
                    "disable_file_type_fold": False,

                    "show_uid_in_file": False,
                    "show_tuid_in_file": False,
                    "show_field_in_file": False,

                    "setting_by_text": False,
                    "setting_by_array": False,
                    "setting_by_expression": False,
                    "setting_by_section": {"enabled": False, "value_type": "int"},
                    "setting_by_numeric": {"enabled": True, "value_type": "float"},
                    "setting_by_combo": {"enabled": False, "items": []},

                    "output_by_file": True,  
                    "output_by_field": False,
                    "output_by_table": False,

                    "SETTING_NUMERIC": [
                        '반경(R) 설정',
                        '반경 입력 (m): ',
                        '분석 대상 객체를 기준으로 참조 레이어에서 계산할 반경(R, 미터 단위)을 입력해 주세요.'
                    ],
                }
                option_2 = {
                    "apply_basic_qss": False,

                    "disable_file_type_layer": True,
                    "disable_file_type_shp": True,
                    "disable_file_type_json": False,
                    "disable_file_type_txtcsv": False,
                    "disable_file_type_fold": False,

                    "show_uid_in_file": False,
                    "show_tuid_in_file": False,
                    "show_field_in_file": False,

                    "setting_by_text": False,
                    "setting_by_array": False,
                    "setting_by_expression": False,
                    "setting_by_section": {"enabled": False, "value_type": "int"},
                    "setting_by_numeric": {"enabled": True, "value_type": "float"},
                    "setting_by_combo": {"enabled": False, "items": []},

                    "output_by_file": True,  
                    "output_by_field": False,
                    "output_by_table": False,

                    "SETTING_NUMERIC": [
                        '반경(R) 설정',
                        '반경 입력 (m): ',
                        '분석 대상 객체를 기준으로 참조 레이어에서 계산할 반경(R, 미터 단위)을 입력해 주세요.'
                    ],
                }

            return option_1, option_2

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)

    def run_job_by_index(self, gdf_1, file_uid_1, file_is_field_check_1, file_field_selection_1, current_step, step_weight):
        
        try:
            
            file_info = common.fileInfo_1

            
            setting_text = file_info.file_setting.get_text()
            setting_numeric = file_info.file_setting.get_numeric()
            setting_section_min, setting_section_max = file_info.file_setting.get_section()
            setting_combo = file_info.file_setting.get_combo()
            setting_array_string, setting_array_integer, setting_array_float = file_info.file_setting.get_array()

            
            file_info_2 = common.fileInfo_2

            
            source_file_type, source_file_path, _ = file_info_2.file_record.get_record()

            for index, file_preview in enumerate(common.fileInfo_2.file_preview):

                
                file_preview = file_info_2.file_preview[index]
                file_field_selection = file_preview.get_selection_field()
                file_uid = file_preview.get_file_uid()
                file_tuid = file_preview.get_file_tuid()
                file_is_field_check = file_preview.get_field_check()
                file_path, file_encoding, file_delimiter, file_has_header = file_preview.get_info()

                
                gdf_2 = self.get_file_data_frame(source_file_type, source_file_path, file_path, file_encoding, file_delimiter, file_has_header)

                
                gdf_1.columns = gdf_1.columns.astype(str)
                gdf_2.columns = gdf_2.columns.astype(str)

                
                if self.job_index == 0:
                    gdf_1 = self.calculate_all_to_all_dist_fields(gdf_1, gdf_2)

                elif self.job_index == 1:
                    gdf_1 = self.calculate_nearest_n_dist_and_attr(gdf_1, gdf_2, n=setting_numeric, attr_field=file_tuid)

                elif self.job_index == 2:
                    gdf_1 = self.calculate_count_within_radius(gdf_1, gdf_2, radius=setting_numeric)

                current_step += 1
                update_progress(self.progressBar, int(current_step * step_weight))

            
            if gdf_1 is None or gdf_1 is False:
                return None

            return gdf_1

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)
            return None
