from __future__ import annotations
import os
from typing import TYPE_CHECKING

import re as _re
from qgis.PyQt.QtCore import QVariant, Qt
from qgis.PyQt.QtWidgets import QFileDialog, QTableWidgetItem, QTableWidget
from qgis._core import QgsVectorDataProvider
from qgis.core import (QgsProject, QgsVectorLayer, QgsFeature, QgsGeometry, QgsField,
                       QgsFillSymbol, QgsGraduatedSymbolRenderer, QgsRendererRange,
                       QgsCoordinateReferenceSystem, QgsCoordinateTransform)
from qgis.PyQt.QtGui import QColor
import requests
import tempfile


if TYPE_CHECKING:
    from omrat import OMRAT


def _wkt_summary(wkt: str) -> str:
    """Return a short label for a WKT geometry (e.g. 'Polygon (42 pts)')."""
    geom_type = wkt.split('(')[0].strip() if '(' in wkt else wkt[:30]
    n = len(_re.findall(r'[-\d.]+\s+[-\d.]+', wkt))
    if n > 5:
        return f"{geom_type}({n} pts)"
    return wkt if len(wkt) <= 60 else wkt[:57] + '...'


_USER_ROLE = getattr(Qt, 'UserRole', None) or Qt.ItemDataRole.UserRole


def _wkt_table_item(wkt: str) -> QTableWidgetItem:
    """QTableWidgetItem that shows a compact summary but stores the full WKT in UserRole."""
    item = QTableWidgetItem(_wkt_summary(wkt))
    item.setData(_USER_ROLE, wkt)
    item.setToolTip(wkt[:400] + ('...' if len(wkt) > 400 else ''))
    return item


def get_leg_coordinates(tbl: QTableWidget) -> list[tuple[float, float]]:
    """Extract all start/end coordinates from the route table."""
    coords: list[tuple[float, float]] = []
    for row in range(tbl.rowCount()):
        start_str = tbl.item(row, 3)
        end_str = tbl.item(row, 4)
        if start_str is None or end_str is None:
            continue
        for coord_str in [start_str.text(), end_str.text()]:
            try:
                # WKT: 'POINT (lon lat)'
                coord = coord_str.split(' ')
                lon, lat = float(coord[0]), float(coord[1])
                coords.append((lon, lat))
            except Exception:  # nosec B110 B112
                continue
    return coords


def get_bbox(coords: list[tuple[float, float]]) -> tuple[float, float, float, float]:
    """Return min/max lat/lon from a list of (lon, lat) tuples."""
    lats = [lat for _, lat in coords]
    lons = [lon for lon, _ in coords]
    return min(lats), max(lats), min(lons), max(lons)


def get_depth_color(depth: float, max_depth: float = 50.0) -> QColor:
    """Get a blue color for depth: 0m = dark blue (danger), max_depth = light/white (safe).

    Args:
        depth: The depth value in meters (shallower = darker blue = more danger)
        max_depth: The depth at which color is lightest (default 50m)

    Returns:
        QColor ranging from dark blue (shallow/danger) to light blue/white (deep/safe)
    """
    # Clamp depth to 0-max_depth range
    depth = max(0, min(depth, max_depth))
    # Calculate ratio (0 = shallow/dark, 1 = deep/light)
    ratio = depth / max_depth

    # Interpolate from dark blue (0, 0, 139) to light blue/white (200, 220, 255)
    # Shallow (0m) = dark blue (danger), Deep (max_depth) = light blue (safe)
    r = int(0 + 200 * ratio)      # 0 -> 200
    g = int(0 + 220 * ratio)      # 0 -> 220
    b = int(139 + (255 - 139) * ratio)  # 139 -> 255

    return QColor(r, g, b)


def build_gebco_url(min_lat: float, max_lat: float, min_lon: float, max_lon: float, api_key: str) -> str:
    return (
        "https://portal.opentopography.org/API/globaldem?"
        f"demtype=GEBCOIceTopo&south={min_lat}&north={max_lat}&west={min_lon}&east={max_lon}"
        f"&outputFormat=GTiff&API_Key={api_key}"
    )


def download_geotiff(url: str, save_path: str = "gebco_download.ti", timeout: float = 60.0) -> str:
    """
    Download a GeoTIFF file from the given URL and save it to disk.
    Returns the path to the saved file.

    A connect/read timeout (default 60s) is enforced so a stalled GEBCO
    response can't hang the QGIS UI thread indefinitely.
    """
    response = requests.get(url, stream=True, timeout=timeout)
    response.raise_for_status()  # Raises an error for bad responses

    with open(save_path, "wb") as f:
        for chunk in response.iter_content(chunk_size=8192):
            f.write(chunk)
    return save_path


class OObject:
    def __init__(self, parent: "OMRAT") -> None:
        self.p = parent
        self.deph_id = 0
        self.area: QgsVectorLayer | None = None
        self.object_id = 0
        self.area_type = ''
        # Single consolidated depth layer (all depth polygons as features)
        self.depth_layer: QgsVectorLayer | None = None
        self.depth_feature_row: dict[int, int] = {}  # feature ID -> table row
        self._depth_edit_buffer = None
        # Object layers remain per-layer (not consolidated)
        self.loaded_object_areas: list[QgsVectorLayer] = []
        self.object_layer_row: dict[str, int] = {}
        self.object_buffer_edits = []

    # ------------------------------------------------------------------
    # Consolidated depth layer helpers
    # ------------------------------------------------------------------

    def _ensure_depth_layer(self) -> QgsVectorLayer:
        """Create the consolidated 'Depth Areas' layer if it doesn't exist, or return it."""
        if self.depth_layer is not None:
            return self.depth_layer

        layer = QgsVectorLayer("Polygon?crs=epsg:4326", "Depth Areas", "memory")
        pr = layer.dataProvider()
        if pr is not None:
            pr.addAttributes([
                QgsField("id", QVariant.Int),      # type: ignore[arg-type]
                QgsField("depth", QVariant.Double),  # type: ignore[arg-type]
            ])
            layer.updateFields()

        QgsProject.instance().addMapLayer(layer)

        if not layer.isEditable():
            layer.startEditing()

        buf = layer.editBuffer()
        if buf is not None:
            buf.geometryChanged.connect(self._on_depth_geometry_changed)
            self._depth_edit_buffer = buf

        self.depth_layer = layer
        return layer

    def _on_depth_geometry_changed(self, fid: int, geom: QgsGeometry) -> None:
        """Sync a geometry edit on a consolidated depth feature back to twDepthList."""
        wkt = geom.asWkt(precision=5)
        row = self.depth_feature_row.get(fid)
        if row is not None and 0 <= row < self.p.main_widget.twDepthList.rowCount():
            self.p.main_widget.twDepthList.setItem(row, 2, _wkt_table_item(wkt))

    def _add_depth_feature(self, depth_id: int, depth_value: float, wkt: str,
                           row: int, defer_style: bool = False) -> None:
        """Add a single depth polygon as a feature in the consolidated layer."""
        layer = self._ensure_depth_layer()
        pr = layer.dataProvider()
        if pr is None:
            return

        feat = QgsFeature(layer.fields())
        feat.setGeometry(QgsGeometry.fromWkt(wkt))
        feat.setAttribute("id", depth_id)
        feat.setAttribute("depth", depth_value)

        success, added_features = pr.addFeatures([feat])
        if success and added_features:
            fid = added_features[0].id()
            self.depth_feature_row[fid] = row

        layer.updateExtents()
        if not defer_style:
            self._apply_depth_graduated_style()

    def _apply_depth_graduated_style(self) -> None:
        """Apply graduated blue symbology based on the 'depth' field."""
        if self.depth_layer is None:
            return

        depths: list[float] = []
        for feat in self.depth_layer.getFeatures():
            d = feat.attribute("depth")
            if d is not None:
                try:
                    depths.append(float(d))
                except (ValueError, TypeError):
                    pass

        if not depths:
            return

        min_d = min(depths)
        max_d = max(depths) if max(depths) > min_d else min_d + 1.0
        unique_depths = sorted(set(depths))
        num_classes = min(10, len(unique_depths))
        step = (max_d - min_d) / num_classes

        ranges = []
        for i in range(num_classes):
            lower = min_d + i * step
            upper = min_d + (i + 1) * step
            mid = (lower + upper) / 2
            color = get_depth_color(mid, max_d)
            symbol = QgsFillSymbol.createSimple({
                'color': color.name(),
                'outline_color': '#000080',
                'outline_width': '0.26'
            })
            label = f"{lower:.1f} - {upper:.1f}m"
            rng = QgsRendererRange(lower, upper, symbol, label)
            ranges.append(rng)

        renderer = QgsGraduatedSymbolRenderer('depth', ranges)
        renderer.setMode(QgsGraduatedSymbolRenderer.Mode.Custom)
        self.depth_layer.setRenderer(renderer)
        self.depth_layer.triggerRepaint()

    def _rebuild_depth_feature_row_map(self) -> None:
        """Re-establish depth_feature_row by matching feature 'id' attrs to table row IDs."""
        self.depth_feature_row.clear()
        if self.depth_layer is None:
            return
        table = self.p.main_widget.twDepthList
        for feat in self.depth_layer.getFeatures():
            feat_id_attr = str(feat.attribute("id"))
            for row in range(table.rowCount()):
                id_item = table.item(row, 0)
                if id_item is not None and id_item.text() == feat_id_attr:
                    self.depth_feature_row[feat.id()] = row
                    break

    def add_area(self, name: str = 'area', value_field: str | None = None) -> None:
        if self.area is not None:
            try:
                self.area.featureAdded.disconnect(self.on_feature_added)
            except Exception:  # nosec B110 B112
                pass
        self.area = QgsVectorLayer("Polygon?crs=epsg:4326", name, "memory")

        # Add value field (Depth or Height) if specified
        if value_field:
            pr = self.area.dataProvider()
            if pr is not None:
                pr.addAttributes([QgsField(value_field, QVariant.Double)])  # type: ignore[arg-type]
                self.area.updateFields()

        self.area.startEditing()
        self.area.featureAdded.connect(self.on_feature_added)
        self.p.iface.actionAddFeature().trigger()
        QgsProject.instance().addMapLayer(self.area)

    def on_feature_added(self, fid):
        """This will be called after the user right-clicks to finish the polygon"""
        try:
            self.area.featureAdded.disconnect(self.on_feature_added)
            print('dictonnected')
        except Exception as e:
            print(e)
        if self.area_type == 'depth':
            self.add_simple_depth()
        if self.area_type == 'object':
            self.add_simple_object()

    def load_area(self, name: str, wkt: str, row: int | None = None,
                  value: str | None = None, value_field: str | None = None,
                  defer_style: bool = False):
        # Depth areas: add as feature to the consolidated depth layer
        if self.area_type == 'depth':
            if row is None:
                row = self.p.main_widget.twDepthList.rowCount() - 1
            depth_val = 0.0
            if value is not None:
                try:
                    depth_val = float(value.split('-')[-1]) if '-' in str(value) else float(value)
                except (ValueError, AttributeError):
                    pass
            depth_id = int(row + 1)
            self._add_depth_feature(depth_id, depth_val, wkt, row, defer_style=defer_style)
            return

        # Object areas: keep per-layer behaviour (unchanged)
        area = QgsVectorLayer("Polygon?crs=epsg:4326", name, "memory")
        pr: QgsVectorDataProvider | None = area.dataProvider()

        if pr is not None and value_field:
            pr.addAttributes([QgsField(value_field, QVariant.Double)])  # type: ignore[arg-type]
            area.updateFields()

        fet = QgsFeature(area.fields())
        fet.setGeometry(QgsGeometry.fromWkt(wkt))

        if value_field and value is not None:
            try:
                fet.setAttribute(value_field, float(value.split('-')[-1]) if '-' in value else float(value))
            except (ValueError, AttributeError):
                pass

        if pr is not None:
            pr.addFeature(fet)
        QgsProject.instance().addMapLayer(area)
        if not area.isEditable():
            area.startEditing()
        buf = area.editBuffer()
        layer_id = area.id()
        if self.area_type == 'object':
            if row is None:
                row = self.p.main_widget.twObjectList.rowCount() - 1
            self.object_layer_row[layer_id] = row
            if buf is not None:
                buf.geometryChanged.connect(
                    lambda fid, geom, lid=layer_id: self.on_area_geometry_changed_wrapper(lid, 'object', fid, geom)
                )
                self.object_buffer_edits.append(buf)
        self.p.iface.actionSaveActiveLayerEdits().trigger()
        if self.area_type == 'object':
            self.loaded_object_areas.append(area)

    def on_area_geometry_changed_wrapper(self, layer_id: str, kind: str, fid: int, geom: QgsGeometry):
        # Update WKT in the object table when geometry changes (depth handled by _on_depth_geometry_changed)
        wkt = geom.asWkt(precision=5)
        row = self.object_layer_row.get(layer_id)
        if row is not None and 0 <= row < self.p.main_widget.twObjectList.rowCount():
            self.p.main_widget.twObjectList.setItem(row, 2, _wkt_table_item(wkt))

    def update_depth_intervals(self) -> None:
        """Update TWDepthIntervals with intervals from 0 to LEMaxDepth using SBDepthInterval."""
        try:
            interval = int(self.p.main_widget.SBDepthInterval.value())
            max_depth = int(float(self.p.main_widget.LEMaxDepth.text()))
        except Exception:  # nosec B110 B112
            self.p.show_error_popup("Please enter valid numbers for interval and max depth.", "update_depth_intervals")
            return

        self.p.main_widget.TWDepthIntervals.setRowCount(0)
        value = 0
        row = 0
        while value <= max_depth:
            self.p.main_widget.TWDepthIntervals.insertRow(row)
            self.p.main_widget.TWDepthIntervals.setItem(row, 0, QTableWidgetItem(str(value)))
            value += interval
            row += 1
        # If last value is less than max_depth, add max_depth as final interval
        if value - interval < max_depth:
            self.p.main_widget.TWDepthIntervals.insertRow(row)
            self.p.main_widget.TWDepthIntervals.setItem(row, 0, QTableWidgetItem(str(max_depth)))

    def obtain_gebco_data(self):
        w = self.p.main_widget
        try:
            min_lat = float(w.LEBboxLLLat.text())
            min_lon = float(w.LEBboxLLLon.text())
            max_lat = float(w.LEBboxURLat.text())
            max_lon = float(w.LEBboxURLon.text())
        except ValueError:
            self.p.show_error_popup(
                "Enter valid lower-left and upper-right coordinates before fetching.",
                "obtain_gebco_data"
            )
            return

        if min_lat >= max_lat or min_lon >= max_lon:
            self.p.show_error_popup(
                "Lower-left must be south-west of upper-right (lat_ll < lat_ur, lon_ll < lon_ur).",
                "obtain_gebco_data"
            )
            return

        api_key = w.LEOpenTopoAPIKey.text()
        url = build_gebco_url(min_lat, max_lat, min_lon, max_lon, api_key)

        temp_dir = tempfile.gettempdir()
        geotiff_path = download_geotiff(url, save_path=os.path.join(temp_dir, 'gebco_download.tif'))
        tw = w.TWDepthIntervals
        intervals = [float(tw.item(i, 0).text()) for i in range(tw.rowCount())]
        self.vectorize_and_add_geotiff(geotiff_path, intervals)

    def vectorize_and_add_geotiff(
        self,
        geotiff_path: str,
        intervals: list[float],
    ) -> None:
        """Vectorize a GEBCO GeoTIFF by depth interval and add to QGIS.

        Performance strategy:
        - Reclassify the raster into N interval bins with numpy (in-memory,
          fast) before polygonizing.  Raw GEBCO has thousands of distinct
          integer depth values; after reclassification there are only N
          distinct values (one per interval), so polygonize produces orders
          of magnitude fewer polygons.
        - Polygonize with GDAL directly (no QGIS processing framework
          overhead) into an OGR in-memory layer.
        - Merge features per interval with QgsGeometry.unaryUnion (C++ GEOS)
          instead of a sequential Python combine loop.
        """
        from osgeo import gdal, ogr, osr
        import numpy as np

        # ── 1. Read raster and reclassify into interval bins ──────────────
        ds = gdal.Open(geotiff_path)
        if ds is None:
            self.p.show_error_popup("Could not open GeoTIFF.", "vectorize_and_add_geotiff")
            return
        band = ds.GetRasterBand(1)
        data = band.ReadAsArray().astype(float)
        nodata = band.GetNoDataValue()
        geotransform = ds.GetGeoTransform()
        projection = ds.GetProjection()
        ny, nx = data.shape
        ds = None

        # GEBCO stores depth below sea level as negative values.
        # Interval [low, high] metres → raster range (-high, -low].
        # Bin index starts at 1 so 0 can serve as "outside all intervals".
        classified = np.zeros((ny, nx), dtype=np.int32)
        for idx in range(len(intervals) - 1):
            low, high = intervals[idx], intervals[idx + 1]
            mask = (data > -high) & (data <= -low)
            if nodata is not None:
                mask &= (data != nodata)
            classified[mask] = idx + 1

        if not classified.any():
            return

        # ── 2. Write classified raster to GDAL virtual filesystem ─────────
        # /vsimem/ keeps everything in RAM so there is no disk round-trip.
        vsimem_raster = '/vsimem/gebco_classified.tif'
        drv = gdal.GetDriverByName("GTiff")
        out_ds = drv.Create(vsimem_raster, nx, ny, 1, gdal.GDT_Int32)
        out_ds.SetGeoTransform(geotransform)
        out_ds.SetProjection(projection)
        out_band = out_ds.GetRasterBand(1)
        out_band.WriteArray(classified)
        out_band.SetNoDataValue(0)
        out_ds.FlushCache()
        out_ds = None

        # Build a mask band (non-zero = valid pixel to polygonize).
        vsimem_mask = '/vsimem/gebco_mask.tif'
        msk_ds = drv.Create(vsimem_mask, nx, ny, 1, gdal.GDT_Byte)
        msk_ds.SetGeoTransform(geotransform)
        msk_ds.SetProjection(projection)
        msk_ds.GetRasterBand(1).WriteArray((classified != 0).astype(np.uint8) * 255)
        msk_ds.FlushCache()
        msk_ds = None

        # ── 3. Polygonize directly into an OGR memory layer ───────────────
        src_ds = gdal.Open(vsimem_raster)
        src_band = src_ds.GetRasterBand(1)
        msk_src = gdal.Open(vsimem_mask)
        mask_band = msk_src.GetRasterBand(1)

        mem_drv = ogr.GetDriverByName("Memory")
        mem_ds = mem_drv.CreateDataSource("polys")
        srs = osr.SpatialReference()
        srs.ImportFromWkt(projection)
        dst_layer = mem_ds.CreateLayer("polygons", srs=srs)
        dst_layer.CreateField(ogr.FieldDefn("VALUE", ogr.OFTInteger))
        val_idx = dst_layer.GetLayerDefn().GetFieldIndex("VALUE")

        gdal.Polygonize(src_band, mask_band, dst_layer, val_idx, [], callback=None)

        src_ds = None
        msk_src = None
        gdal.Unlink(vsimem_raster)
        gdal.Unlink(vsimem_mask)

        # ── 4. Merge per interval with unaryUnion and add to table/layer ──
        for idx in range(len(intervals) - 1):
            dst_layer.SetAttributeFilter(f"VALUE = {idx + 1}")
            qgs_geoms = []
            for feat in dst_layer:
                geom_ref = feat.GetGeometryRef()
                if geom_ref is not None:
                    qgs_geoms.append(QgsGeometry.fromWkt(geom_ref.ExportToWkt()))
            dst_layer.ResetReading()

            if not qgs_geoms:
                continue

            merged_geom = QgsGeometry.unaryUnion(qgs_geoms)
            wkt = merged_geom.asWkt(precision=5)

            interval = intervals[idx]
            row = self.p.main_widget.twDepthList.rowCount()
            self.p.main_widget.twDepthList.insertRow(row)
            self.p.main_widget.twDepthList.setItem(row, 0, QTableWidgetItem(str(row + 1)))
            self.p.main_widget.twDepthList.setItem(row, 1, QTableWidgetItem(f"{interval}-{intervals[idx + 1]}"))
            self.p.main_widget.twDepthList.setItem(row, 2, _wkt_table_item(wkt))

            depth_val = intervals[idx + 1]
            self._add_depth_feature(row + 1, depth_val, wkt, row, defer_style=True)

        self._apply_depth_graduated_style()

    def add_simple_depth(self):
        self.area_type = 'depth'
        if self.p.main_widget.pbAddSimpleDepth.text() == 'Save':
            self.store_depth()
            self.p.main_widget.pbAddSimpleDepth.setText('Add manual')
            # Add drawn polygon to consolidated depth layer
            assert self.area is not None  # nosec B101
            row = self.deph_id - 1
            wkt_item = self.p.main_widget.twDepthList.item(row, 2)
            depth_item = self.p.main_widget.twDepthList.item(row, 1)
            if wkt_item is not None:
                wkt = wkt_item.text()
                depth_val = float(depth_item.text()) if depth_item else 10.0
                self._add_depth_feature(self.deph_id, depth_val, wkt, row)
            # Remove the temporary drawing layer from the project
            QgsProject.instance().removeMapLayer(self.area.id())
            self.area = None
        else:
            self.deph_id += 1
            # Create layer with Depth field - will be renamed after user enters depth value
            self.add_area(f'Depth - {self.deph_id} (enter depth)', value_field='Depth')
            self.p.main_widget.pbAddSimpleDepth.setText('Save')

    def store_depth(self):
        # Default depth value - user can edit in table
        default_depth = 10.0

        self.p.main_widget.twDepthList.setRowCount(self.deph_id)
        item1 = QTableWidgetItem(f'{self.deph_id}')
        item2 = QTableWidgetItem(f'{default_depth}')
        polies = self.area.getFeatures()
        for poly in polies:
            wkt_poly = poly.geometry().asWkt(precision=5)
            self.p.main_widget.twDepthList.setItem(self.deph_id - 1, 0, item1)
            self.p.main_widget.twDepthList.setItem(self.deph_id - 1, 1, item2)
            self.p.main_widget.twDepthList.setItem(self.deph_id - 1, 2, _wkt_table_item(wkt_poly))

        self.p.iface.actionSaveActiveLayerEdits().trigger()
        self.p.iface.actionToggleEditing().trigger()

    def add_simple_object(self):
        self.area_type = 'object'
        if self.p.main_widget.pbAddSimpleObject.text() == 'Save':
            self.store_object()
            self.p.main_widget.pbAddSimpleObject.setText('Add manual')
            self.loaded_object_areas.append(self.area)
        else:
            self.object_id += 1
            # Create layer with Height field
            self.add_area(f'Structure - {self.object_id} (enter height)', value_field='Height')
            self.p.main_widget.pbAddSimpleObject.setText('Save')

    def store_object(self):
        # Default height value - user can edit in table
        default_height = 10.0

        self.p.main_widget.twObjectList.setRowCount(self.object_id)
        item1 = QTableWidgetItem(f'{self.object_id}')
        item2 = QTableWidgetItem(f'{default_height}')
        polies = self.area.getFeatures()
        for poly in polies:
            item3 = QTableWidgetItem(f'{poly.geometry().asWkt(precision=5)}')
            self.p.main_widget.twObjectList.setItem(self.object_id - 1, 0, item1)
            self.p.main_widget.twObjectList.setItem(self.object_id - 1, 1, item2)
            self.p.main_widget.twObjectList.setItem(self.object_id - 1, 2, item3)

        # Rename the layer to show the height value
        if self.area is not None:
            self.area.setName(f'Structure - {default_height}m')

        self.p.iface.actionSaveActiveLayerEdits().trigger()
        self.p.iface.actionToggleEditing().trigger()

    def _select_file(self, title: str) -> str | None:
        file_path, _ = QFileDialog.getOpenFileName(
            self.p.main_widget,
            title,
            "",
            "Shapefiler (*.shp)"
        )
        return file_path if file_path else None

    def _load_layer(self, file_path: str, layer_name: str, target_list: list) -> QgsVectorLayer | None:
        layer = QgsVectorLayer(file_path, layer_name, "ogr")
        if not layer.isValid():
            print(f"Ogiltigt lager: {layer_name}")
            return None
        QgsProject.instance().addMapLayer(layer)
        target_list.append(layer)
        return layer

    def _wgs84_transform(self, layer: QgsVectorLayer) -> QgsCoordinateTransform | None:
        # Object/depth WKTs are stored as EPSG:4326 throughout OMRAT, so a
        # shapefile in any other CRS must be reprojected before its WKT is
        # extracted -- otherwise projected meters land in the table and the
        # calculation later fails the forward-transform check.
        src = layer.crs()
        if not src.isValid() or src.authid() == 'EPSG:4326':
            return None
        wgs84 = QgsCoordinateReferenceSystem('EPSG:4326')
        return QgsCoordinateTransform(src, wgs84, QgsProject.instance().transformContext())

    def _populate_table(self, layer: QgsVectorLayer, table_widget, attr_name: str) -> None:
        row_index = table_widget.rowCount()
        tr = self._wgs84_transform(layer)
        for feature in layer.getFeatures():
            geom = feature.geometry()
            if tr is not None:
                geom = QgsGeometry(geom)
                geom.transform(tr)
            wkt: str = geom.asWkt(precision=5)
            value = feature.attribute(attr_name) if attr_name in layer.fields().names() else 0.0

            table_widget.insertRow(row_index)
            table_widget.setItem(row_index, 0, QTableWidgetItem(str(row_index + 1)))
            table_widget.setItem(row_index, 1, QTableWidgetItem(str(value)))
            table_widget.setItem(row_index, 2, QTableWidgetItem(wkt))
            row_index += 1

    def load_objects(self) -> None:
        file_path = self._select_file("Välj shapefil med objektområden")
        if file_path is None:
            return

        layer = self._load_layer(file_path, "Loaded Objects", self.loaded_object_areas)
        if layer is None:
            return

        # Use the correct attribute name if different
        self._populate_table(layer, self.p.main_widget.twObjectList, "object")

    def load_depths(self) -> None:
        file_path = self._select_file("Välj shapefil med djupområden")
        if file_path is None:
            return

        # Load shapefile as temporary layer (not added to project)
        temp_layer = QgsVectorLayer(file_path, "temp_depths", "ogr")
        if not temp_layer.isValid():
            print("Ogiltigt lager: Loaded Depths")
            return

        tr = self._wgs84_transform(temp_layer)

        # Extract features and add to consolidated layer + table
        row_index = self.p.main_widget.twDepthList.rowCount()
        for feature in temp_layer.getFeatures():
            geom = feature.geometry()
            if tr is not None:
                geom = QgsGeometry(geom)
                geom.transform(tr)
            wkt = geom.asWkt(precision=5)
            value = feature.attribute("depth") if "depth" in temp_layer.fields().names() else 0.0

            self.p.main_widget.twDepthList.insertRow(row_index)
            self.p.main_widget.twDepthList.setItem(row_index, 0, QTableWidgetItem(str(row_index + 1)))
            self.p.main_widget.twDepthList.setItem(row_index, 1, QTableWidgetItem(str(value)))
            self.p.main_widget.twDepthList.setItem(row_index, 2, _wkt_table_item(wkt))

            depth_val = float(value) if value else 0.0
            self._add_depth_feature(row_index + 1, depth_val, wkt, row_index, defer_style=True)
            row_index += 1

        self._apply_depth_graduated_style()

    def remove_depth(self) -> None:
        table = self.p.main_widget.twDepthList
        selected_rows: set[int] = {item.row() for item in table.selectedItems()}
        if not selected_rows:
            return

        # Build reverse map: row -> feature ID
        row_to_fid: dict[int, int] = {row: fid for fid, row in self.depth_feature_row.items()}

        # Collect feature IDs to remove
        fids_to_remove = [row_to_fid[row] for row in selected_rows if row in row_to_fid]

        # Remove features from the consolidated layer
        if self.depth_layer is not None and fids_to_remove:
            pr = self.depth_layer.dataProvider()
            if pr is not None:
                pr.deleteFeatures(fids_to_remove)
            self.depth_layer.updateExtents()

        # Remove rows from table (reverse order to maintain indices)
        for row in sorted(selected_rows, reverse=True):
            table.removeRow(row)

        # Rebuild the feature-to-row mapping after deletion
        self._rebuild_depth_feature_row_map()
        self._apply_depth_graduated_style()

    def remove_object(self) -> None:
        table = self.p.main_widget.twObjectList
        selected_rows: set[int] = {item.row() for item in table.selectedItems()}

        for row in sorted(selected_rows, reverse=True):
            table.removeRow(row)

            # Remove the layers if they are selected
            if row < len(self.loaded_object_areas):
                layer = self.loaded_object_areas.pop(row)
                QgsProject.instance().removeMapLayer(layer.id())

    def _cleanup_depth_layer(self) -> None:
        """Disconnect signals and remove the consolidated depth layer."""
        if self._depth_edit_buffer is not None:
            try:
                self._depth_edit_buffer.geometryChanged.disconnect()
            except Exception:  # nosec B110 B112
                pass
            self._depth_edit_buffer = None
        if self.depth_layer is not None:
            try:
                QgsProject.instance().removeMapLayer(self.depth_layer.id())
            except Exception:  # nosec B110 B112
                pass
            self.depth_layer = None
        self.depth_feature_row = {}

    def unload(self):
        if self.area is not None:
            try:
                self.area.featureAdded.disconnect(self.on_feature_added)
            except TypeError:
                pass
        self.area = None
        # Clean up consolidated depth layer
        self._cleanup_depth_layer()
        # Clean up object layers
        try:
            for buf in self.object_buffer_edits:
                buf.geometryChanged.disconnect()
        except Exception:  # nosec B110 B112
            pass
        self.object_buffer_edits = []
        self.object_layer_row = {}
        for layer in self.loaded_object_areas:
            QgsProject.instance().removeMapLayer(layer.id())
        self.loaded_object_areas = []

    def clear(self) -> None:
        """Remove all loaded depth/object layers and reset state.

        Unlike unload(), this keeps the plugin operational so new data
        can be loaded afterwards.
        """
        # Disconnect featureAdded signal from current area layer
        if self.area is not None:
            try:
                self.area.featureAdded.disconnect(self.on_feature_added)
            except (TypeError, RuntimeError):
                pass
        self.area = None

        # Clean up consolidated depth layer
        self._cleanup_depth_layer()

        # Disconnect object geometry-changed signals
        for buf in self.object_buffer_edits:
            try:
                buf.geometryChanged.disconnect()
            except Exception:  # nosec B110 B112
                pass
        self.object_buffer_edits = []

        # Remove object layers from QGIS
        for layer in self.loaded_object_areas:
            try:
                QgsProject.instance().removeMapLayer(layer.id())
            except Exception:  # nosec B110 B112
                pass
        self.loaded_object_areas = []
        self.object_layer_row = {}

        # Reset counters
        self.deph_id = 0
        self.object_id = 0
        self.area_type = ''

        # Reset button text in case user was mid-add
        try:
            self.p.main_widget.pbAddSimpleDepth.setText('Add manual')
            self.p.main_widget.pbAddSimpleObject.setText('Add manual')
        except Exception:  # nosec B110 B112
            pass
