import logging
import numbers
from typing import Union, List, Tuple

import sys

from osgeo import ogr, osr

logger = logging.getLogger(__name__)


geometry_map = {
    "Point": ogr.wkbPoint,
    "LineString": ogr.wkbLineString,
    "LineString25D": ogr.wkbLineString25D,
    "Polygon": ogr.wkbPolygon
}


def get_ogr_type(
    ogr_type_str: str
) -> Union[None, 'ogr.OGRFieldType']:
    """
    Parse the provided textual field type to return an actual OGRFieldType.

    :param ogr_type_str: the string referring to the ogr field type.
    :return: the actual ogr type.
    :raise: Exception.
    """

    try:

        if ogr_type_str.endswith("OFTInteger"):
            return ogr.OFTInteger
        elif ogr_type_str.endswith("OFTIntegerList"):
            return ogr.OFTIntegerList
        elif ogr_type_str.endswith("OFTReal"):
            return ogr.OFTReal
        elif ogr_type_str.endswith("OFTRealList"):
            return ogr.OFTRealList
        elif ogr_type_str.endswith("OFTString"):
            return ogr.OFTString
        elif ogr_type_str.endswith("OFTStringList"):
            return ogr.OFTStringList
        elif ogr_type_str.endswith("OFTBinary"):
            return ogr.OFTBinary
        elif ogr_type_str.endswith("OFTDate"):
            return ogr.OFTDate
        elif ogr_type_str.endswith("OFTTime"):
            return ogr.OFTTime
        elif ogr_type_str.endswith("OFTDateTime"):
            return ogr.OFTDateTime
        elif ogr_type_str.endswith("OFTInteger64"):
            return ogr.OFTInteger64
        elif ogr_type_str.endswith("OFTInteger64List"):
            return ogr.OFTInteger64List
        else:
            raise Exception(f"Unrecognized OGR type: {ogr_type_str}")

    except Exception as e:

        print(f"\n{e!r}")
        return None


def create_geopackage(
    path: str,
) -> bool:

    try:

        driver = ogr.GetDriverByName("GPKG")
        dataset = driver.CreateDataSource(path)
        if dataset is None:
            print(f"GPKG dataset {path} is None\n")
            return False

        return True

    except Exception as e:

        print(f"{e!r}\n")
        return False


def add_vector_layer(
    geopackage_path: str,
    layer_name: str,
    geometry_type: str,
    fields: Union[None, list] = None,
    epsg_code: int = 4326,
) -> bool:

    try:

        if geometry_type not in geometry_map:
            print(f"\nUnsupported geometry '{geometry_type}'")
            return False

        srs = osr.SpatialReference()
        srs.ImportFromEPSG(epsg_code)

        #print(f"\nOpening '{geopackage_path}'")
        dataset = ogr.Open(geopackage_path, 1)  # 1 = write
        if dataset is None:
            print(f"\nUnable to open '{geopackage_path}'")
            return False

        if dataset.GetLayerByName(layer_name):
            print(f"\nLayer '{layer_name}' already exists")
            return False

        layer = dataset.CreateLayer(
            layer_name,
            srs,
            geometry_map[geometry_type],
        )

        if layer is None:
            print(f"\nError in layer '{layer_name}' creation")
            return False

        if fields is not None:

            for field_name, field_type, *field_width in fields:

                ogr_field_type = get_ogr_type(field_type)
                if ogr_field_type is None:
                    print(f"\nUnsupported field type '{ogr_field_type}' for field '{field_name}'")
                    continue

                field_defn = ogr.FieldDefn(field_name, ogr_field_type)
                if ogr_field_type == ogr.OFTString and len(field_width) > 0:
                    field_defn.SetWidth(int(field_width[0]))

                layer.CreateField(field_defn)

        return True

    except Exception as e:

        print(f"\n{e!r}")
        return False


def insert_3d_line(
    file_path: str,
    layer_name: str,
    line_coordinates: List[Tuple[numbers.Real, numbers.Real, numbers.Real]]
) -> bool:
    """
    Inserisce una linea 3D in un layer esistente all'interno di un GeoPackage.

    :param file_path: Percorso al file GeoPackage.
    :param layer_name: Nome del layer in cui inserire la linea.
    :param line_coordinates: Lista di tuple (x, y, z) che definiscono la linea.
    """

    try:

        # Apri il GeoPackage in modalità di scrittura

        dataset = ogr.Open(file_path, 1)  # 1 significa modalità di scrittura
        if dataset is None:
            raise Exception(f"Unable to open {file_path}")

        # Ottieni il layer

        layer = dataset.GetLayerByName(layer_name)
        if layer is None:
            raise Exception(f"Unable to find layer {layer_name} in file {file_path}")

        # Controlla se il layer supporta la geometria 3D

        layer_defn = layer.GetLayerDefn()
        geom_type = layer_defn.GetGeomType()
        if geom_type != ogr.wkbLineString25D:
            raise Exception(f"Layer {layer_name} does not support 3D geometries (found type: {geom_type})")

        # Crea una nuova feature con una linea 3D
        import math

        line = ogr.Geometry(ogr.wkbLineString25D)
        points_added = 0
        points_without_z = 0

        for x, y, z in line_coordinates:
            # Validate and convert coordinates to float
            try:
                x_f = float(x)
                y_f = float(y)
                z_f = float(z) if z is not None else None
            except (TypeError, ValueError) as e:
                print(
                    f"Invalid coordinate values: x={x}, y={y}, z={z}, error: {e!r}",
                )
                continue

            # Validate that coordinates are finite
            if not (math.isfinite(x_f) and math.isfinite(y_f)):
                print(
                    f"Non-finite coordinates: x={x_f}, y={y_f}",
                )
                continue

            # Add point with appropriate number of coordinates.
            # A wkbLineString25D vertex always carries a z, so adding it
            # without one (line.AddPoint(x, y)) does not mean 'no elevation':
            # it stores a fictitious 0 m elevation, indistinguishable from a
            # real sea-level sample. The missing value is written as NaN
            # instead, the no-data marker that the rest of the code already
            # expects (np.nanmin / np.nanmax on the elevations, matplotlib
            # leaving a gap in the plotted profile).

            if z_f is None or not math.isfinite(z_f):
                z_f = float("nan")
                points_without_z += 1

            line.AddPoint(x_f, y_f, z_f)
            points_added += 1

        # Skip empty geometries
        if points_added == 0:
            raise Exception("No valid points found in line_coordinates")

        if points_without_z > 0:
            print(
                f"Warning: {points_without_z} of {points_added} points of the line written to "
                f"{layer_name} have no elevation (written as NaN): check that the profile lies "
                f"within the DEM extent and over valid (non nodata) cells",
            )

        # Crea una nuova feature e assegna la geometria

        feature = ogr.Feature(layer_defn)
        feature.SetGeometry(line)

        # Aggiungi la feature al layer
        result = layer.CreateFeature(feature)
        if result != ogr.OGRERR_NONE:
            raise Exception(f"Error in adding feature to layer: OGR error code {result}")

        # Libera la memoria
        try:
            feature.Destroy()
        except Exception:
            logger.debug("feature.Destroy() failed", exc_info=True)
        feature = None
        dataset = None

        return True

    except Exception as e:

        print(f"\n{e!r}")
        return False


def read_lines_3d_from_layer(
    gpkg_path: str,
    layer_name: str,
) -> Union[None, List[Tuple[numbers.Real, numbers.Real, numbers.Real]]]:

    try:

        # Carica il geopackage

        driver = ogr.GetDriverByName('GPKG')
        data_source = driver.Open(gpkg_path, 0)  # 0 significa solo lettura

        if data_source is None:
            print("Error while opening the geopackage")
            return None

        # Accedi al layer
        layer = data_source.GetLayerByName(layer_name)

        # Itera attraverso le feature del layer

        features_xyz = []

        for feature in layer:

            # Ottieni la geometria della feature
            geometry = feature.GetGeometryRef()

            # Verifica che la geometria sia una LineString

            if geometry.GetGeometryType() == ogr.wkbLineString:

                # Estrai le coordinate (x, y, z) della LineString

                points = []
                for i in range(geometry.GetPointCount()):
                    x, y, z = geometry.GetPoint(i)  # x, y, z sono le coordinate del punto
                    points.append((x, y, z))  # Salva le coordinate

                features_xyz.append(points)

        # Chiudi il datasource

        data_source = None

        return features_xyz

    except Exception as e:

        print(f"\n{e!r}")
        return None
