import csv
import os
import time
from qgis.PyQt.QtCore import QVariant, QThread, pyqtSignal
from qgis.PyQt.QtWidgets import QFileDialog
from qgis.core import (
    QgsWkbTypes, QgsProject, QgsGeometry, QgsVectorLayer, QgsField,
    QgsFeature, QgsDistanceArea, QgsSpatialIndex, QgsPointXY
)
from qgis.utils import iface
from qgis.PyQt import QtWidgets, uic, QtCore

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


class cyanlove_calc_point_min_geometry_distince_v2(QtWidgets.QDockWidget, FORM_CLASS):
    closingPlugin = pyqtSignal()

    def __init__(self, parent=None):
        super(cyanlove_calc_point_min_geometry_distince_v2, self).__init__(parent)
        self.export_thread = None
        self.iface = iface
        self.canvas = iface.mapCanvas()
        self.setupUi(self)
        self.setFloating(True)
        self.property_dict = None
        self.is_running = False
        self.setWindowFlags(QtCore.Qt.Dialog)

        self.btn_calc_point_distinct_geometry.clicked.connect(self.calc_point_min_distinct_geometry)

    def calc_point_min_distinct_geometry(self):
        options = QFileDialog.Options()
        options |= QFileDialog.ReadOnly
        csv_file_path, _ = QFileDialog.getSaveFileName(self,
                                                       "选择文件",
                                                       "",
                                                       "CSV文件 (*.csv);;所有文件 (*)",
                                                       options=options)
        if not csv_file_path:
            return

        layer1 = self.mMapLayerComboBox.currentLayer()
        layer2 = self.mMapLayerComboBox_2.currentLayer()
        if layer1 is None or layer2 is None:
            self.label_show.setText("请选择两个图层！")
            return

        geom_type1 = layer1.geometryType()  # 0=点,1=线,2=面
        geom_type2 = layer2.geometryType()

        # 允许的组合: (点, 线), (点, 面), (线, 点), (面, 点)
        valid = False
        if geom_type1 == 0 and geom_type2 in (1, 2):  # 点 -> 线/面
            valid = True
        elif geom_type1 == 1 and geom_type2 == 0:  # 线 -> 点
            valid = True
        elif geom_type1 == 2 and geom_type2 == 0:  # 面 -> 点
            valid = True

        if not valid:
            self.label_show.setText("不支持的图层组合。请使用：点+线/面，线+点，或面+点。")
            return

        self.progressBar.setRange(0, 100)
        self.progressBar.setValue(0)
        self.progressBar.show()
        self.export_thread = ExportThread(layer1, layer2, csv_file_path)
        self.export_thread.progress.connect(self.update_progress)
        self.export_thread.updatelabel.connect(self.update_label)
        self.export_thread.finished.connect(self.export_finished)
        self.export_thread.start()

    def update_label(self, value):
        self.label_show.setText(value)

    def update_progress(self, value):
        self.progressBar.setValue(value)

    def export_finished(self):
        self.progressBar.setValue(100)


class ExportThread(QThread):
    progress = pyqtSignal(int)
    updatelabel = pyqtSignal(str)

    def __init__(self, layer1, layer2, csv_file_path):
        super().__init__()
        self.layer1 = layer1
        self.layer2 = layer2
        self.csv_file_path = csv_file_path

    def run(self):
        try:
            geom_type1 = self.layer1.geometryType()
            geom_type2 = self.layer2.geometryType()

            if geom_type1 == 0 and geom_type2 in (1, 2):
                self._run_point_to_line_or_polygon()
            elif geom_type1 == 1 and geom_type2 == 0:
                self._run_line_to_point()
            elif geom_type1 == 2 and geom_type2 == 0:
                self._run_polygon_to_point()
            else:
                self.updatelabel.emit(f"不支持的组合: 图层1类型={geom_type1}, 图层2类型={geom_type2}")
        except Exception as e:
            self.updatelabel.emit(f"运行错误: {str(e)}")
            import traceback
            traceback.print_exc()

    # ================= 点 → 线/面（保持不变） =================
    def _run_point_to_line_or_polygon(self):
        start_time = time.time()
        layer1 = self.layer1  # 点
        layer2 = self.layer2  # 线或面

        if layer1.featureCount() == 0:
            self.updatelabel.emit("错误：点图层没有要素！")
            return
        if layer2.featureCount() == 0:
            self.updatelabel.emit("错误：线/面图层没有要素！")
            return

        fields1 = [f.name() for f in layer1.fields()]
        fields2 = [f.name() for f in layer2.fields()]
        out_fields = fields1 + fields2 + ["最短距离(米)", "最近点经度", "最近点纬度"]

        total_points = layer1.featureCount()
        is_polygon = (layer2.geometryType() == 2)

        self.updatelabel.emit(f"开始计算，共 {total_points} 个点，目标要素 {layer2.featureCount()} 个...")

        distance_area = QgsDistanceArea()
        distance_area.setEllipsoid('WGS84')
        distance_area.setSourceCrs(layer1.crs(), QgsProject.instance().transformContext())

        target_index = QgsSpatialIndex(layer2.getFeatures())
        target_features = {f.id(): f for f in layer2.getFeatures()}

        line_layer = QgsVectorLayer(f"LineString?crs={layer1.crs().authid()}", "最短连接线", "memory")
        line_provider = line_layer.dataProvider()
        line_provider.addAttributes([
            QgsField("source_id", QVariant.Int),
            QgsField("distance_m", QVariant.Double),
            QgsField("nearest_x", QVariant.Double),
            QgsField("nearest_y", QVariant.Double)
        ])
        line_layer.updateFields()
        line_feat_list = []

        found_count = 0
        candidate_count = min(10, len(target_features))
        if candidate_count == 0:
            self.updatelabel.emit("错误：目标图层没有有效要素")
            return

        with open(self.csv_file_path, 'w', newline='', encoding='utf-8-sig') as csv_file:
            writer = csv.writer(csv_file)
            writer.writerow(out_fields)

            for idx, point_feat in enumerate(layer1.getFeatures()):
                point_geom = point_feat.geometry()
                row = [str(point_feat[f]) if point_feat[f] is not None else "" for f in fields1]

                if point_geom is None or point_geom.isEmpty():
                    row.extend([""] * (len(fields2) + 3))
                    writer.writerow(row)
                    self._update_progress(idx, total_points)
                    continue

                containing_feat = None
                if is_polygon:
                    candidate_ids = target_index.intersects(point_geom.boundingBox())
                    for fid in candidate_ids:
                        feat = target_features.get(fid)
                        if feat and feat.geometry() and feat.geometry().contains(point_geom):
                            containing_feat = feat
                            break

                if containing_feat is not None:
                    for fname in fields2:
                        val = containing_feat[fname]
                        row.append(str(val) if val is not None else "")
                    row.append("0")
                    row.append("")
                    row.append("")
                    writer.writerow(row)
                    found_count += 1
                    self._update_progress(idx, total_points)
                    continue

                nearest_ids = target_index.nearestNeighbor(point_geom.asPoint(), candidate_count)
                best_feat = None
                best_dist = float('inf')
                best_line = None
                best_pt = None

                for fid in nearest_ids:
                    feat = target_features.get(fid)
                    if not feat:
                        continue
                    geom = feat.geometry()
                    if not geom or geom.isEmpty():
                        continue
                    line = point_geom.shortestLine(geom)
                    if line is None or line.isEmpty():
                        continue
                    dist = distance_area.measureLength(line)
                    pts = line.asPolyline()
                    nearest_pt = pts[-1] if pts else point_geom.asPoint()
                    if dist < best_dist:
                        best_dist = dist
                        best_feat = feat
                        best_line = line
                        best_pt = nearest_pt

                if best_feat is None:
                    row.extend([""] * (len(fields2) + 3))
                    writer.writerow(row)
                    self._update_progress(idx, total_points)
                    continue

                found_count += 1
                for fname in fields2:
                    val = best_feat[fname]
                    row.append(str(val) if val is not None else "")
                row.append(f"{best_dist:.2f}")
                row.append(f"{best_pt.x():.6f}" if best_pt else "")
                row.append(f"{best_pt.y():.6f}" if best_pt else "")
                writer.writerow(row)

                if best_line and not best_line.isEmpty():
                    line_feat = QgsFeature()
                    line_feat.setGeometry(best_line)
                    line_feat.setAttributes([point_feat.id(), best_dist,
                                             best_pt.x() if best_pt else None,
                                             best_pt.y() if best_pt else None])
                    line_feat_list.append(line_feat)

                self._update_progress(idx, total_points)
                if (idx + 1) % max(1, total_points // 10) == 0:
                    self.updatelabel.emit(f"已处理 {idx + 1}/{total_points} 点，成功匹配 {found_count} 个")

        if line_feat_list:
            line_provider.addFeatures(line_feat_list)
            line_layer.updateExtents()
            QgsProject.instance().addMapLayer(line_layer)
            self.updatelabel.emit(f"已生成最短连接线图层，共 {len(line_feat_list)} 条线")

        self.updatelabel.emit(
            f"计算完成！共 {total_points} 点，成功匹配 {found_count} 点。"
        )

    # ================= 线 → 点（保持不变） =================
    def _run_line_to_point(self):
        start_time = time.time()
        line_layer = self.layer1  # 线
        point_layer = self.layer2  # 点

        if line_layer.featureCount() == 0:
            self.updatelabel.emit("错误：线图层没有要素！")
            return
        if point_layer.featureCount() == 0:
            self.updatelabel.emit("错误：点图层没有要素！")
            return

        line_fields = [f.name() for f in line_layer.fields()]
        point_fields = [f.name() for f in point_layer.fields()]
        out_fields = line_fields + point_fields + ["最短距离(米)", "线上最近点经度", "线上最近点纬度"]

        total_lines = line_layer.featureCount()
        self.updatelabel.emit(f"开始计算，共 {total_lines} 条线，点要素 {point_layer.featureCount()} 个...")

        distance_area = QgsDistanceArea()
        distance_area.setEllipsoid('WGS84')
        distance_area.setSourceCrs(line_layer.crs(), QgsProject.instance().transformContext())

        point_index = QgsSpatialIndex(point_layer.getFeatures())
        point_features = {f.id(): f for f in point_layer.getFeatures()}
        self.updatelabel.emit(f"已加载 {len(point_features)} 个点，索引建立完成")

        conn_layer = QgsVectorLayer(f"LineString?crs={line_layer.crs().authid()}", "线到点连接线", "memory")
        conn_provider = conn_layer.dataProvider()
        conn_provider.addAttributes([
            QgsField("line_id", QVariant.Int),
            QgsField("point_id", QVariant.Int),
            QgsField("distance_m", QVariant.Double),
            QgsField("nearest_on_line_x", QVariant.Double),
            QgsField("nearest_on_line_y", QVariant.Double)
        ])
        conn_layer.updateFields()
        conn_feat_list = []

        found_count = 0
        candidate_count = min(20, len(point_features))
        if candidate_count == 0:
            self.updatelabel.emit("错误：点图层没有有效要素")
            return

        with open(self.csv_file_path, 'w', newline='', encoding='utf-8-sig') as csv_file:
            writer = csv.writer(csv_file)
            writer.writerow(out_fields)

            for idx, line_feat in enumerate(line_layer.getFeatures()):
                line_geom = line_feat.geometry()
                row = [str(line_feat[f]) if line_feat[f] is not None else "" for f in line_fields]

                if line_geom is None or line_geom.isEmpty():
                    row.extend([""] * (len(point_fields) + 3))
                    writer.writerow(row)
                    self._update_progress(idx, total_lines)
                    continue

                bbox = line_geom.boundingBox()
                bbox = bbox.buffered(0.0001)
                candidate_point_ids = point_index.intersects(bbox)
                if not candidate_point_ids:
                    nearest_ids = point_index.nearestNeighbor(line_geom.asPoint(), 1)
                    candidate_point_ids = nearest_ids

                if len(candidate_point_ids) > 100:
                    candidate_point_ids = candidate_point_ids[:100]

                best_point_feat = None
                best_distance = float('inf')
                best_line_on_line = None
                best_nearest_on_line = None

                for pid in candidate_point_ids:
                    point_feat = point_features.get(pid)
                    if not point_feat:
                        continue
                    point_geom = point_feat.geometry()
                    if point_geom is None or point_geom.isEmpty():
                        continue

                    line_to_point = point_geom.shortestLine(line_geom)
                    if line_to_point is None or line_to_point.isEmpty():
                        continue
                    dist = distance_area.measureLength(line_to_point)
                    pts = line_to_point.asPolyline()
                    if len(pts) >= 2:
                        nearest_on_line = pts[1]
                    else:
                        nearest_on_line = pts[0] if pts else None

                    if dist < best_distance:
                        best_distance = dist
                        best_point_feat = point_feat
                        best_line_on_line = line_to_point
                        best_nearest_on_line = nearest_on_line

                if best_point_feat is None:
                    row.extend([""] * (len(point_fields) + 3))
                    writer.writerow(row)
                    self._update_progress(idx, total_lines)
                    continue

                found_count += 1
                for fname in point_fields:
                    val = best_point_feat[fname]
                    row.append(str(val) if val is not None else "")
                row.append(f"{best_distance:.2f}")
                row.append(f"{best_nearest_on_line.x():.6f}" if best_nearest_on_line else "")
                row.append(f"{best_nearest_on_line.y():.6f}" if best_nearest_on_line else "")
                writer.writerow(row)

                if best_line_on_line and not best_line_on_line.isEmpty():
                    conn_feat = QgsFeature()
                    conn_feat.setGeometry(best_line_on_line)
                    conn_feat.setAttributes([
                        line_feat.id(),
                        best_point_feat.id(),
                        best_distance,
                        best_nearest_on_line.x() if best_nearest_on_line else None,
                        best_nearest_on_line.y() if best_nearest_on_line else None
                    ])
                    conn_feat_list.append(conn_feat)

                self._update_progress(idx, total_lines)
                if (idx + 1) % max(1, total_lines // 10) == 0:
                    self.updatelabel.emit(f"已处理 {idx + 1}/{total_lines} 条线，成功匹配 {found_count} 个")

        if conn_feat_list:
            conn_provider.addFeatures(conn_feat_list)
            conn_layer.updateExtents()
            QgsProject.instance().addMapLayer(conn_layer)
            self.updatelabel.emit(f"已生成线到点连接线图层，共 {len(conn_feat_list)} 条线")

        self.updatelabel.emit(
            f"计算完成！共 {total_lines} 条线，成功匹配 {found_count} 条。"
        )

    # ================= 面 → 点（优化版：自适应扩展缓冲） =================
    def _run_polygon_to_point(self):
        """
        计算每个面到最近点的距离（最终版：矩形缓冲 + 最近邻双重保障）
        """
        start_time = time.time()
        polygon_layer = self.layer1
        point_layer = self.layer2

        try:
            self.updatelabel.emit("开始检查图层有效性...")

            if polygon_layer.featureCount() == 0:
                self.updatelabel.emit("错误：面图层没有要素！")
                return
            if point_layer.featureCount() == 0:
                self.updatelabel.emit("错误：点图层没有要素！")
                return

            polygon_fields = [f.name() for f in polygon_layer.fields()]
            point_fields = [f.name() for f in point_layer.fields()]
            out_fields = polygon_fields + point_fields + [
                "最短距离(米)", "面上最近点经度", "面上最近点纬度", "是否在面内"
            ]

            total_polygons = polygon_layer.featureCount()
            self.updatelabel.emit(f"开始计算，共 {total_polygons} 个面，点要素 {point_layer.featureCount()} 个...")

            distance_area = QgsDistanceArea()
            distance_area.setEllipsoid('WGS84')
            distance_area.setSourceCrs(polygon_layer.crs(), QgsProject.instance().transformContext())

            crs = polygon_layer.crs()
            is_geographic = crs.isGeographic()

            # 建立点的空间索引
            self.updatelabel.emit("正在建立点的空间索引...")
            point_index = QgsSpatialIndex(point_layer.getFeatures())
            point_features = {f.id(): f for f in point_layer.getFeatures()}
            total_points = len(point_features)
            self.updatelabel.emit(f"已加载 {total_points} 个点，索引建立完成")

            # 创建内存线图层
            conn_layer = QgsVectorLayer(f"LineString?crs={polygon_layer.crs().authid()}", "面到点连接线", "memory")
            conn_provider = conn_layer.dataProvider()
            conn_provider.addAttributes([
                QgsField("polygon_id", QVariant.Int),
                QgsField("point_id", QVariant.Int),
                QgsField("distance_m", QVariant.Double),
                QgsField("nearest_on_polygon_x", QVariant.Double),
                QgsField("nearest_on_polygon_y", QVariant.Double),
                QgsField("inside", QVariant.Int)
            ])
            conn_layer.updateFields()
            conn_feat_list = []

            found_count = 0
            inside_count = 0

            METERS_PER_DEGREE = 111000.0
            SAFETY_FACTOR = 1.5
            STOP_THRESHOLD = 0.9
            MIN_CANDIDATES = 20
            NEAREST_NEIGHBOR_COUNT = 50  # 每次补充的最近邻数量

            with open(self.csv_file_path, 'w', newline='', encoding='utf-8-sig') as csv_file:
                writer = csv.writer(csv_file)
                writer.writerow(out_fields)

                for idx, polygon_feat in enumerate(polygon_layer.getFeatures()):
                    polygon_geom = polygon_feat.geometry()
                    row = [str(polygon_feat[f]) if polygon_feat[f] is not None else "" for f in polygon_fields]

                    if polygon_geom is None or polygon_geom.isEmpty():
                        self.updatelabel.emit(f"警告: 第 {idx + 1} 个面几何无效，已跳过")
                        row.extend([""] * (len(point_fields) + 4))
                        writer.writerow(row)
                        self._update_progress(idx, total_polygons)
                        continue

                    # 如果总点数很少，直接全量
                    if total_points <= MIN_CANDIDATES * 2:
                        candidate_ids = list(point_features.keys())
                    else:
                        bbox = polygon_geom.boundingBox()
                        search_radius_m = 1000
                        max_radius_m = 100000
                        step_m = 1000
                        candidate_ids = []
                        best_dist = float('inf')
                        best_point = None
                        best_line = None
                        best_nearest = None
                        point_inside = False

                        while search_radius_m <= max_radius_m:
                            # 1. 矩形缓冲
                            if is_geographic:
                                radius_deg = (search_radius_m / METERS_PER_DEGREE) * SAFETY_FACTOR
                            else:
                                radius_deg = search_radius_m * SAFETY_FACTOR
                            expanded_bbox = bbox.buffered(radius_deg)
                            buffer_ids = point_index.intersects(expanded_bbox)

                            # 2. 最近邻补充（基于面中心点）
                            centroid = polygon_geom.centroid().asPoint()
                            nearest_ids = point_index.nearestNeighbor(centroid, NEAREST_NEIGHBOR_COUNT)

                            # 合并去重
                            combined_ids = list(set(buffer_ids) | set(nearest_ids))

                            if combined_ids:
                                # 计算当前候选集的最近点
                                cur_best_dist = float('inf')
                                cur_best_point = None
                                cur_best_line = None
                                cur_best_nearest = None
                                cur_inside = False

                                for pid in combined_ids:
                                    point_feat = point_features.get(pid)
                                    if not point_feat:
                                        continue
                                    point_geom = point_feat.geometry()
                                    if point_geom is None or point_geom.isEmpty():
                                        continue
                                    if polygon_geom.contains(point_geom):
                                        cur_inside = True
                                        cur_best_point = point_feat
                                        cur_best_dist = 0
                                        cur_best_nearest = point_geom.asPoint()
                                        cur_best_line = point_geom
                                        break
                                    line = point_geom.shortestLine(polygon_geom)
                                    if line is None or line.isEmpty():
                                        continue
                                    dist = distance_area.measureLength(line)
                                    pts = line.asPolyline()
                                    nearest_pt = pts[-1] if len(pts) >= 2 else (pts[0] if pts else None)
                                    if dist < cur_best_dist:
                                        cur_best_dist = dist
                                        cur_best_point = point_feat
                                        cur_best_line = line
                                        cur_best_nearest = nearest_pt

                                if cur_best_point is not None:
                                    if cur_best_dist < best_dist:
                                        best_dist = cur_best_dist
                                        best_point = cur_best_point
                                        best_line = cur_best_line
                                        best_nearest = cur_best_nearest
                                        point_inside = cur_inside

                                    # 停止条件：候选点数量足够 且 当前最佳距离 < 半径*阈值
                                    if (len(combined_ids) >= MIN_CANDIDATES) and (
                                            cur_best_dist < search_radius_m * STOP_THRESHOLD):
                                        # 但还要确保没有更近的点在半径外（理论上不会，因为满足距离条件）
                                        # 为了保险，如果候选点数量很大（>100），直接停止
                                        if len(combined_ids) > 100:
                                            break
                                        # 否则，继续扩展一点点，确保覆盖
                                        # 这里我们选择继续扩展到下一个步长，但为了避免无限，先检查是否达到最大
                                        pass

                            # 如果未停止，继续扩展
                            search_radius_m += step_m

                        # 如果循环结束仍未找到点，用最近邻兜底（取100个）
                        if best_point is None:
                            centroid = polygon_geom.centroid().asPoint()
                            nearest_ids = point_index.nearestNeighbor(centroid, 100)
                            for pid in nearest_ids:
                                point_feat = point_features.get(pid)
                                if not point_feat:
                                    continue
                                point_geom = point_feat.geometry()
                                if point_geom is None or point_geom.isEmpty():
                                    continue
                                if polygon_geom.contains(point_geom):
                                    point_inside = True
                                    best_point = point_feat
                                    best_dist = 0
                                    best_nearest = point_geom.asPoint()
                                    best_line = point_geom
                                    break
                                line = point_geom.shortestLine(polygon_geom)
                                if line is None or line.isEmpty():
                                    continue
                                dist = distance_area.measureLength(line)
                                pts = line.asPolyline()
                                nearest_pt = pts[-1] if len(pts) >= 2 else (pts[0] if pts else None)
                                if dist < best_dist:
                                    best_dist = dist
                                    best_point = point_feat
                                    best_line = line
                                    best_nearest = nearest_pt
                                    point_inside = False

                    # ---------- 最终处理 ----------
                    if best_point is None:
                        row.extend([""] * (len(point_fields) + 4))
                        writer.writerow(row)
                        self._update_progress(idx, total_polygons)
                        continue

                    found_count += 1
                    if point_inside:
                        inside_count += 1

                    for fname in point_fields:
                        val = best_point[fname]
                        row.append(str(val) if val is not None else "")

                    row.append(f"{best_dist:.2f}")
                    row.append(f"{best_nearest.x():.6f}" if best_nearest else "")
                    row.append(f"{best_nearest.y():.6f}" if best_nearest else "")
                    row.append("是" if point_inside else "否")
                    writer.writerow(row)

                    if best_line and not best_line.isEmpty() and best_dist > 0:
                        conn_feat = QgsFeature()
                        conn_feat.setGeometry(best_line)
                        conn_feat.setAttributes([
                            polygon_feat.id(),
                            best_point.id(),
                            best_dist,
                            best_nearest.x() if best_nearest else None,
                            best_nearest.y() if best_nearest else None,
                            1 if point_inside else 0
                        ])
                        conn_feat_list.append(conn_feat)

                    self._update_progress(idx, total_polygons)

                    if (idx + 1) % max(1, total_polygons // 10) == 0:
                        self.updatelabel.emit(
                            f"已处理 {idx + 1}/{total_polygons} 个面，"
                            f"成功匹配 {found_count} 个，其中面内点 {inside_count} 个"
                        )

            # 添加连接线图层
            if conn_feat_list:
                conn_provider.addFeatures(conn_feat_list)
                conn_layer.updateExtents()
                QgsProject.instance().addMapLayer(conn_layer)
                self.updatelabel.emit(f"已生成面到点连接线图层，共 {len(conn_feat_list)} 条线")
            else:
                self.updatelabel.emit("未生成连接线图层（所有点都在面内或距离为0）")

            self.updatelabel.emit(
                f"计算完成！共 {total_polygons} 个面，成功匹配 {found_count} 个面。"
                f"其中点在面内的情况: {inside_count} 个。"
            )

        except Exception as e:
            error_msg = f"面到点计算错误: {str(e)}"
            self.updatelabel.emit(error_msg)
            import traceback
            traceback.print_exc()
            raise

    def _update_progress(self, current, total):
        progress_val = int((current + 1) / total * 100)
        self.progress.emit(progress_val)