
from qgis.PyQt.QtCore import QVariant
from qgis.core import (
    QgsFeature, QgsField, QgsGeometry, QgsPointXY, QgsProject, QgsVectorLayer,
    QgsPalLayerSettings, QgsTextFormat, QgsVectorLayerSimpleLabeling,
    QgsUnitTypes, QgsMarkerSymbol, QgsWkbTypes,
)


class SplitOutputTools:
    POINT_LAYER_NAME = "SGS Vertex Numbers"
    SEGMENT_LAYER_NAME = "SGS Segment Lengths"

    @staticmethod
    def _dist(a, b):
        dx = b.x() - a.x()
        dy = b.y() - a.y()
        return (dx * dx + dy * dy) ** 0.5

    @staticmethod
    def _point_key(p, precision=6):
        return (round(p.x(), precision), round(p.y(), precision))

    @staticmethod
    def _segment_key(a, b, precision=6):
        pa = SplitOutputTools._point_key(a, precision)
        pb = SplitOutputTools._point_key(b, precision)
        return tuple(sorted([pa, pb]))

    @staticmethod
    def _point_on_segment(p, a, b, tol=1e-6):
        ax, ay = a.x(), a.y()
        bx, by = b.x(), b.y()
        px, py = p.x(), p.y()
        dx = bx - ax
        dy = by - ay
        length2 = dx * dx + dy * dy
        if length2 <= tol * tol:
            return False, 0.0
        t = ((px - ax) * dx + (py - ay) * dy) / length2
        if t < -tol or t > 1.0 + tol:
            return False, t
        projx = ax + t * dx
        projy = ay + t * dy
        d = ((px - projx) ** 2 + (py - projy) ** 2) ** 0.5
        return d <= tol, max(0.0, min(1.0, t))

    @staticmethod
    def _get_layer_by_name(name):
        for lyr in QgsProject.instance().mapLayers().values():
            if lyr.name() == name:
                return lyr
        return None

    @staticmethod
    def _get_or_create_point_layer(source_layer=None):
        vl = SplitOutputTools._get_layer_by_name(SplitOutputTools.POINT_LAYER_NAME)
        if vl is not None and vl.isValid():
            return vl
        crs_authid = source_layer.crs().authid() if source_layer and source_layer.crs().isValid() else "EPSG:4326"
        vl = QgsVectorLayer(f"Point?crs={crs_authid}", SplitOutputTools.POINT_LAYER_NAME, "memory")
        pr = vl.dataProvider()
        pr.addAttributes([
            QgsField("no", QVariant.Int),
            QgsField("x", QVariant.Double),
            QgsField("y", QVariant.Double),
            QgsField("label", QVariant.String),
        ])
        vl.updateFields()
        QgsProject.instance().addMapLayer(vl)
        SplitOutputTools._style_point_layer(vl)
        return vl

    @staticmethod
    def _get_or_create_segment_layer(source_layer=None):
        vl = SplitOutputTools._get_layer_by_name(SplitOutputTools.SEGMENT_LAYER_NAME)
        if vl is not None and vl.isValid():
            return vl
        crs_authid = source_layer.crs().authid() if source_layer and source_layer.crs().isValid() else "EPSG:4326"
        vl = QgsVectorLayer(f"LineString?crs={crs_authid}", SplitOutputTools.SEGMENT_LAYER_NAME, "memory")
        pr = vl.dataProvider()
        pr.addAttributes([
            QgsField("seg_no", QVariant.Int),
            QgsField("length_m", QVariant.Double),
            QgsField("label", QVariant.String),
        ])
        vl.updateFields()
        QgsProject.instance().addMapLayer(vl)
        SplitOutputTools._style_segment_layer(vl)
        return vl

    @staticmethod
    def _style_point_layer(vl):
        try:
            symbol = QgsMarkerSymbol.createSimple({
                "name": "circle",
                "color": "255,255,255,255",
                "outline_color": "0,0,0,255",
                "outline_width": "0.2",
                "size": "1.0"
            })
            symbol.setSize(1.0)
            symbol.setSizeUnit(QgsUnitTypes.RenderMillimeters)
            vl.renderer().setSymbol(symbol)
        except Exception:
            pass
        try:
            settings = QgsPalLayerSettings()
            settings.fieldName = "label"
            settings.enabled = True
            try:
                settings.placement = QgsPalLayerSettings.OverPoint
                settings.xOffset = 1.0
                settings.yOffset = 1.0
                settings.offsetUnits = QgsUnitTypes.RenderMillimeters
            except Exception:
                pass
            fmt = QgsTextFormat()
            fmt.setSize(2)
            fmt.setSizeUnit(QgsUnitTypes.RenderMillimeters)
            settings.setFormat(fmt)
            vl.setLabelsEnabled(True)
            vl.setLabeling(QgsVectorLayerSimpleLabeling(settings))
            vl.triggerRepaint()
        except Exception:
            pass

    @staticmethod
    def _style_segment_layer(vl):
        try:
            settings = QgsPalLayerSettings()
            settings.fieldName = "label"
            settings.enabled = True
            settings.placement = QgsPalLayerSettings.Line
            try:
                settings.linePlacementFlags = QgsPalLayerSettings.OnLine
            except Exception:
                pass
            fmt = QgsTextFormat()
            fmt.setSize(2)
            fmt.setSizeUnit(QgsUnitTypes.RenderMillimeters)
            settings.setFormat(fmt)
            vl.setLabelsEnabled(True)
            vl.setLabeling(QgsVectorLayerSimpleLabeling(settings))
            vl.triggerRepaint()
        except Exception:
            pass

    @staticmethod
    def _geometry_vertex_sequences(geom):
        if geom is None or geom.isEmpty():
            return []
        gtype = QgsWkbTypes.geometryType(geom.wkbType())
        sequences = []
        if gtype == QgsWkbTypes.PolygonGeometry:
            if geom.isMultipart():
                for poly in geom.asMultiPolygon():
                    if poly and poly[0]:
                        pts = [QgsPointXY(p) for p in poly[0]]
                        if len(pts) > 1 and SplitOutputTools._dist(pts[0], pts[-1]) <= 1e-9:
                            pts = pts[:-1]
                        sequences.append(("polygon", pts))
            else:
                poly = geom.asPolygon()
                if poly and poly[0]:
                    pts = [QgsPointXY(p) for p in poly[0]]
                    if len(pts) > 1 and SplitOutputTools._dist(pts[0], pts[-1]) <= 1e-9:
                        pts = pts[:-1]
                    sequences.append(("polygon", pts))
        elif gtype == QgsWkbTypes.LineGeometry:
            if geom.isMultipart():
                for line in geom.asMultiPolyline():
                    pts = [QgsPointXY(p) for p in line]
                    if len(pts) >= 2:
                        sequences.append(("line", pts))
            else:
                pts = [QgsPointXY(p) for p in geom.asPolyline()]
                if len(pts) >= 2:
                    sequences.append(("line", pts))
        return sequences

    @staticmethod
    def _vertices_from_geometries(geometries):
        pts = []
        for geom in geometries or []:
            for _kind, seq in SplitOutputTools._geometry_vertex_sequences(geom):
                pts.extend(seq)
        return pts

    @staticmethod
    def _raw_segments_from_geometries(geometries):
        segments = []
        for geom in geometries or []:
            for kind, pts in SplitOutputTools._geometry_vertex_sequences(geom):
                if len(pts) < 2:
                    continue
                rng = range(len(pts)) if kind == "polygon" else range(len(pts) - 1)
                for i in rng:
                    p1 = pts[i]
                    p2 = pts[(i + 1) % len(pts)]
                    if SplitOutputTools._dist(p1, p2) > 1e-9:
                        segments.append((QgsPointXY(p1), QgsPointXY(p2)))
        return segments

    @staticmethod
    def _load_existing_points(point_layer):
        seen = {}
        max_no = 0
        if point_layer is None or not point_layer.isValid():
            return seen, max_no
        for f in point_layer.getFeatures():
            geom = f.geometry()
            if geom is None or geom.isEmpty():
                continue
            p = geom.asPoint()
            key = SplitOutputTools._point_key(QgsPointXY(p))
            no = f["no"] if "no" in f.fields().names() else None
            try:
                no_int = int(no)
            except Exception:
                no_int = max_no + 1
            seen[key] = no_int
            max_no = max(max_no, no_int)
        return seen, max_no

    @staticmethod
    def _existing_segment_geometries(segment_layer):
        segs = []
        if segment_layer is None or not segment_layer.isValid():
            return segs
        for f in segment_layer.getFeatures():
            geom = f.geometry()
            if geom is None or geom.isEmpty():
                continue
            if geom.isMultipart():
                for line in geom.asMultiPolyline():
                    pts = [QgsPointXY(p) for p in line]
                    if len(pts) >= 2:
                        for i in range(len(pts)-1):
                            segs.append((pts[i], pts[i+1]))
            else:
                pts = [QgsPointXY(p) for p in geom.asPolyline()]
                if len(pts) >= 2:
                    for i in range(len(pts)-1):
                        segs.append((pts[i], pts[i+1]))
        return segs

    @staticmethod
    def _atomic_segments(raw_segments, all_vertices):
        unique_segments = []
        seen_segments = set()
        for p1, p2 in raw_segments:
            split_items = [(0.0, QgsPointXY(p1)), (1.0, QgsPointXY(p2))]
            for p in all_vertices:
                ok, t = SplitOutputTools._point_on_segment(p, p1, p2)
                if ok and 1e-8 < t < 1.0 - 1e-8:
                    split_items.append((t, QgsPointXY(p)))
            split_items.sort(key=lambda item: item[0])
            cleaned = []
            for t, p in split_items:
                if not cleaned or SplitOutputTools._dist(cleaned[-1][1], p) > 1e-7:
                    cleaned.append((t, p))
            for i in range(len(cleaned)-1):
                a = cleaned[i][1]
                b = cleaned[i+1][1]
                if SplitOutputTools._dist(a, b) <= 1e-7:
                    continue
                key = SplitOutputTools._segment_key(a, b)
                if key in seen_segments:
                    continue
                seen_segments.add(key)
                unique_segments.append((QgsPointXY(a), QgsPointXY(b)))
        return unique_segments

    @staticmethod
    def update_vertex_number_layer(iface, source_layer, geometries):
        point_layer = SplitOutputTools._get_or_create_point_layer(source_layer)
        pr = point_layer.dataProvider()
        seen, max_no = SplitOutputTools._load_existing_points(point_layer)
        new_features = []
        next_no = max_no + 1
        for p in SplitOutputTools._vertices_from_geometries(geometries):
            key = SplitOutputTools._point_key(p)
            if key in seen:
                continue
            seen[key] = next_no
            f = QgsFeature(point_layer.fields())
            f.setGeometry(QgsGeometry.fromPointXY(p))
            f.setAttributes([next_no, round(p.x(), 2), round(p.y(), 2), str(next_no)])
            new_features.append(f)
            next_no += 1
        if new_features:
            pr.addFeatures(new_features)
            point_layer.updateExtents()
        SplitOutputTools._style_point_layer(point_layer)
        if iface:
            iface.messageBar().pushSuccess("Smart Geometry Splitter", "Vertex number layer updated.")
        return point_layer

    @staticmethod
    def update_segment_length_layer(iface, source_layer, geometries):
        seg_layer = SplitOutputTools._get_or_create_segment_layer(source_layer)
        point_layer = SplitOutputTools._get_or_create_point_layer(source_layer)

        # Existing + new segments, then re-atomize by existing + new vertices.
        raw_segments = SplitOutputTools._existing_segment_geometries(seg_layer)
        raw_segments.extend(SplitOutputTools._raw_segments_from_geometries(geometries))

        all_vertices = []
        if point_layer and point_layer.isValid():
            for f in point_layer.getFeatures():
                g = f.geometry()
                if g and not g.isEmpty():
                    all_vertices.append(QgsPointXY(g.asPoint()))
        all_vertices.extend(SplitOutputTools._vertices_from_geometries(geometries))

        segments = SplitOutputTools._atomic_segments(raw_segments, all_vertices)

        pr = seg_layer.dataProvider()
        ids = [f.id() for f in seg_layer.getFeatures()]
        if ids:
            pr.deleteFeatures(ids)

        features = []
        for seg_no, (p1, p2) in enumerate(segments, start=1):
            f = QgsFeature(seg_layer.fields())
            f.setGeometry(QgsGeometry.fromPolylineXY([p1, p2]))
            length = f.geometry().length()
            f.setAttributes([seg_no, length, f"{length:.2f} m"])
            features.append(f)
        if features:
            pr.addFeatures(features)
        seg_layer.updateExtents()
        SplitOutputTools._style_segment_layer(seg_layer)
        if iface:
            iface.messageBar().pushSuccess("Smart Geometry Splitter", "Segment length layer updated.")
        return seg_layer

    # Backward compatible names used by existing dialog code.
    @staticmethod
    def create_vertex_number_layer(iface, source_layer, geometries=None, name=None):
        return SplitOutputTools.update_vertex_number_layer(iface, source_layer, geometries or [])

    @staticmethod
    def create_segment_length_layer(iface, source_layer, geometries=None, name=None):
        return SplitOutputTools.update_segment_length_layer(iface, source_layer, geometries or [])

    @staticmethod
    def export_vertices_txt(path, geometries=None, source_layer=None):
        # Export current SGS vertex layer if it exists; otherwise export supplied geometries.
        point_layer = SplitOutputTools._get_layer_by_name(SplitOutputTools.POINT_LAYER_NAME)
        rows = []
        if point_layer is not None and point_layer.isValid():
            for f in point_layer.getFeatures():
                try:
                    no = int(f["no"])
                except Exception:
                    no = len(rows) + 1
                geom = f.geometry()
                if geom is None or geom.isEmpty():
                    continue
                p = QgsPointXY(geom.asPoint())
                rows.append((no, p.x(), p.y()))
            rows.sort(key=lambda r: r[0])
        else:
            seen = set()
            for no, p in enumerate(SplitOutputTools._vertices_from_geometries(geometries or []), start=1):
                key = SplitOutputTools._point_key(p)
                if key in seen:
                    continue
                seen.add(key)
                rows.append((no, p.x(), p.y()))

        lines = ["No\tY\tX"]
        for no, x, y in rows:
            lines.append(f"{no}\t{x:.2f}\t{y:.2f}")
        with open(path, "w", encoding="utf-8") as f:
            f.write("\n".join(lines))
        return path
