
from __future__ import annotations

import logging
import os

try:
    from osgeo import ogr
except ImportError:
    import ogr

import os
import json
import sqlite3
from datetime import datetime, timezone
from typing import Any, Dict, Iterable, List, Optional, Tuple

from qgis.core import Qgis

from ..validation import safe_float
from ...tools.config.constants import DatabaseConstants

logger = logging.getLogger(__name__)


def _connect(db_path: str) -> sqlite3.Connection:
    os.makedirs(os.path.dirname(db_path) or ".", exist_ok=True)
    con = sqlite3.connect(db_path)
    con.execute("PRAGMA foreign_keys = ON;")
    con.execute("PRAGMA journal_mode = WAL;")
    return con


def ensure_schema(db_path: str) -> None:
    """
    Create the SQLite schema if missing (schema v1).
    Works for .sqlite/.db and also .gpkg (treated as SQLite container).
    """
    con = _connect(db_path)
    try:
        cur = con.cursor()
        
        # Create all tables
        _create_meta_table(cur)
        _create_result_sets_table(cur)
        _create_profiles_table(cur)
        _create_profile_samples_table(cur)
        _create_projected_points_table(cur)
        _create_projected_attitudes_table(cur)
        _create_projected_focal_mechanisms_table(cur)
        _create_intersected_lines_table(cur)
        _create_intersected_polygons_table(cur)
        _create_sources_table(cur)
        
        con.commit()
    finally:
        con.close()


def _create_meta_table(cur: sqlite3.Cursor) -> None:
    """Create gp_meta table for schema versioning."""
    cur.execute("""
        CREATE TABLE IF NOT EXISTS gp_meta (
            key TEXT PRIMARY KEY,
            value TEXT
        );
    """)
    
    # Basic schema versioning
    cur.execute("INSERT OR IGNORE INTO gp_meta(key, value) VALUES('schema_version', '1');")
    cur.execute("INSERT OR IGNORE INTO gp_meta(key, value) VALUES('created_utc', ?);",
                (datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z"),))


def _create_result_sets_table(cur: sqlite3.Cursor) -> None:
    """Create gp_result_sets table and indexes."""
    cur.execute("""
        CREATE TABLE IF NOT EXISTS gp_result_sets (
            result_set_id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            description TEXT DEFAULT '',
            created_utc TEXT NOT NULL,
            schema_version INTEGER NOT NULL DEFAULT 1,
            distance_units TEXT DEFAULT 'project',
            extra_json TEXT DEFAULT NULL
        );
    """)
    
    # Make result set name unique to simplify overwrite logic
    cur.execute("""
        CREATE UNIQUE INDEX IF NOT EXISTS ux_gp_result_sets_name
        ON gp_result_sets(name);
    """)


def _create_profiles_table(cur: sqlite3.Cursor) -> None:
    """Create gp_profiles table and indexes."""
    cur.execute("""
        CREATE TABLE IF NOT EXISTS gp_profiles (
            profile_id INTEGER PRIMARY KEY AUTOINCREMENT,
            result_set_id INTEGER NOT NULL,
            source_profile_fid INTEGER,
            profile_name TEXT DEFAULT '',
            wkt_crs TEXT DEFAULT NULL,
            s_max REAL DEFAULT NULL,
            z_min REAL DEFAULT NULL,
            z_max REAL DEFAULT NULL,
            extra_json TEXT DEFAULT NULL,
            FOREIGN KEY(result_set_id) REFERENCES gp_result_sets(result_set_id) ON DELETE CASCADE
        );
    """)
    
    cur.execute("""
        CREATE INDEX IF NOT EXISTS ix_gp_profiles_result_set
        ON gp_profiles(result_set_id);
    """)


def _create_profile_samples_table(cur: sqlite3.Cursor) -> None:
    """Create gp_profile_samples table and indexes."""
    cur.execute("""
        CREATE TABLE IF NOT EXISTS gp_profile_samples (
            sample_id INTEGER PRIMARY KEY AUTOINCREMENT,
            profile_id INTEGER NOT NULL,
            idx INTEGER NOT NULL,
            s REAL NOT NULL,
            z REAL NOT NULL,
            x REAL DEFAULT NULL,
            y REAL DEFAULT NULL,
            extra_json TEXT DEFAULT NULL,
            FOREIGN KEY(profile_id) REFERENCES gp_profiles(profile_id) ON DELETE CASCADE
        );
    """)
    
    cur.execute("""
        CREATE INDEX IF NOT EXISTS ix_gp_profile_samples_profile_idx
        ON gp_profile_samples(profile_id, idx);
    """)
    
    # Enforce idempotency: one sample per (profile, idx)
    cur.execute("""
        CREATE UNIQUE INDEX IF NOT EXISTS ux_gp_profile_samples_profile_idx
        ON gp_profile_samples(profile_id, idx);
    """)


def _create_projected_points_table(cur: sqlite3.Cursor) -> None:
    """Create gp_projected_points table and indexes."""
    cur.execute("""
        CREATE TABLE IF NOT EXISTS gp_projected_points (
            rec_id INTEGER PRIMARY KEY AUTOINCREMENT,
            profile_id INTEGER NOT NULL,
            label TEXT DEFAULT '',
            s REAL NOT NULL,
            z REAL NOT NULL,
            dist_to_profile REAL DEFAULT NULL,
            src_x REAL DEFAULT NULL,
            src_y REAL DEFAULT NULL,
            src_z REAL DEFAULT NULL,
            src_fid INTEGER DEFAULT NULL,
            extra_json TEXT DEFAULT NULL,
            FOREIGN KEY(profile_id) REFERENCES gp_profiles(profile_id) ON DELETE CASCADE
        );
    """)
    
    cur.execute("""
        CREATE INDEX IF NOT EXISTS ix_gp_projected_points_profile_s
        ON gp_projected_points(profile_id, s);
    """)


def _create_projected_attitudes_table(cur: sqlite3.Cursor) -> None:
    """Create gp_projected_attitudes table and indexes."""
    cur.execute("""
        CREATE TABLE IF NOT EXISTS gp_projected_attitudes (
            rec_id INTEGER PRIMARY KEY AUTOINCREMENT,
            profile_id INTEGER NOT NULL,
            label TEXT DEFAULT '',
            s REAL NOT NULL,
            z REAL NOT NULL,
            slope_degr REAL NOT NULL,
            down_sense TEXT NOT NULL,
            src_dip_dir REAL NOT NULL,
            src_dip_ang REAL NOT NULL, 
            dist_to_profile REAL DEFAULT NULL,
            src_x REAL DEFAULT NULL,
            src_y REAL DEFAULT NULL,
            src_z REAL DEFAULT NULL,
            src_fid INTEGER DEFAULT NULL,
            extra_json TEXT DEFAULT NULL,
            FOREIGN KEY(profile_id) REFERENCES gp_profiles(profile_id) ON DELETE CASCADE
        );
    """)
    
    cur.execute("""
        CREATE INDEX IF NOT EXISTS ix_gp_projected_attitudes_profile_s
        ON gp_projected_attitudes(profile_id, s);
    """)


def _create_projected_focal_mechanisms_table(cur: sqlite3.Cursor) -> None:
    """Create gp_projected_focal_mechanisms table and indexes."""
    cur.execute("""
        CREATE TABLE IF NOT EXISTS gp_projected_focal_mechanisms (
            rec_id INTEGER PRIMARY KEY AUTOINCREMENT,
            profile_id INTEGER NOT NULL,
            label TEXT DEFAULT '',
            s REAL NOT NULL,
            z REAL NOT NULL,
            strike REAL NOT NULL,
            dip REAL NOT NULL,
            rake REAL NOT NULL,
            profile_azimuth REAL NOT NULL,
            dist_to_profile REAL DEFAULT NULL,
            src_x REAL DEFAULT NULL,
            src_y REAL DEFAULT NULL,
            src_z REAL DEFAULT NULL,
            src_fid INTEGER DEFAULT NULL,
            extra_json TEXT DEFAULT NULL,
            FOREIGN KEY(profile_id) REFERENCES gp_profiles(profile_id) ON DELETE CASCADE
        );
    """)

    cur.execute("""
        CREATE INDEX IF NOT EXISTS ix_gp_projected_focal_mechanisms_profile_s
        ON gp_projected_focal_mechanisms(profile_id, s);
    """)


def _create_intersected_lines_table(cur: sqlite3.Cursor) -> None:
    """Create gp_intersected_lines table and indexes."""
    cur.execute("""
        CREATE TABLE IF NOT EXISTS gp_intersected_lines (
            rec_id INTEGER PRIMARY KEY AUTOINCREMENT,
            profile_id INTEGER NOT NULL,
            feat_category TEXT DEFAULT '',
            s REAL NOT NULL,
            extra_json TEXT DEFAULT NULL,
            FOREIGN KEY(profile_id) REFERENCES gp_profiles(profile_id) ON DELETE CASCADE
        );
    """)
    
    cur.execute("""
        CREATE INDEX IF NOT EXISTS ix_gp_intersected_lines_profile_s
        ON gp_intersected_lines(profile_id, s);
    """)
    
    cur.execute("""
        CREATE INDEX IF NOT EXISTS ix_gp_intersected_lines_profile_category
        ON gp_intersected_lines(profile_id, feat_category);
    """)


def _create_intersected_polygons_table(cur: sqlite3.Cursor) -> None:
    """Create gp_intersected_polygons table and indexes."""
    cur.execute("""
        CREATE TABLE IF NOT EXISTS gp_intersected_polygons (
            rec_id INTEGER PRIMARY KEY AUTOINCREMENT,
            profile_id INTEGER NOT NULL,
            unit_name TEXT NOT NULL,
            s_from REAL NOT NULL,
            s_to REAL NOT NULL,
            extra_json TEXT DEFAULT NULL,
            FOREIGN KEY(profile_id) REFERENCES gp_profiles(profile_id) ON DELETE CASCADE
        );
    """)
    
    cur.execute("""
        CREATE INDEX IF NOT EXISTS ix_gp_intersected_polygons_profile_s
        ON gp_intersected_polygons(profile_id, s_from, s_to);
    """)


def _create_sources_table(cur: sqlite3.Cursor) -> None:
    """Create gp_sources table and indexes."""
    cur.execute("""
        CREATE TABLE IF NOT EXISTS gp_sources (
            source_id INTEGER PRIMARY KEY AUTOINCREMENT,
            result_set_id INTEGER NOT NULL,
            source_type TEXT NOT NULL,          -- e.g. 'profiles_layer', 'points_layer', 'attitudes_layer', ...
            source_name TEXT DEFAULT '',
            source_uri TEXT DEFAULT '',
            wkt_crs TEXT DEFAULT NULL,
            parameters_json TEXT DEFAULT NULL,
            FOREIGN KEY(result_set_id) REFERENCES gp_result_sets(result_set_id) ON DELETE CASCADE
        );
    """)
    
    cur.execute("""
        CREATE INDEX IF NOT EXISTS ix_gp_sources_result_set
        ON gp_sources(result_set_id);
    """)


def upsert_result_set(db_path: str, name: str, description: str,
                      overwrite: bool, schema_version: int = 1,
                      distance_units: str = "project",
                      extra: Optional[Dict[str, Any]] = None) -> int:
    """
    Create a result set. If overwrite=True and same name exists, it gets deleted (cascade).
    Returns result_set_id.
    """
    ensure_schema(db_path)
    con = _connect(db_path)
    try:
        cur = con.cursor()

        cur.execute("SELECT result_set_id FROM gp_result_sets WHERE name = ?;", (name,))
        row = cur.fetchone()
        if row and overwrite:
            cur.execute("DELETE FROM gp_result_sets WHERE result_set_id = ?;", (row[0],))
            row = None

        if row:
            # exists and overwrite=False
            raise ValueError(f"Result set name '{name}' already exists (overwrite disabled).")

        cur.execute("""
            INSERT INTO gp_result_sets(name, description, created_utc, schema_version, distance_units, extra_json)
            VALUES (?, ?, ?, ?, ?, ?);
        """, (
            name,
            description or "",
            datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z"),
            int(schema_version),
            distance_units or "project",
            json.dumps(extra) if extra else None
        ))

        rs_id = int(cur.lastrowid)
        con.commit()
        return rs_id
    finally:
        con.close()


def insert_source(db_path: str, result_set_id: int, source_type: str,
                  source_name: str = "", source_uri: str = "", wkt_crs: Optional[str] = None,
                  parameters: Optional[Dict[str, Any]] = None) -> None:
    con = _connect(db_path)
    try:
        con.execute("""
            INSERT INTO gp_sources(result_set_id, source_type, source_name, source_uri, wkt_crs, parameters_json)
            VALUES (?, ?, ?, ?, ?, ?);
        """, (result_set_id, source_type, source_name, source_uri, wkt_crs, json.dumps(parameters) if parameters else None))
        con.commit()
    finally:
        con.close()


def insert_profile(db_path: str, result_set_id: int, source_profile_fid: Optional[int],
                   profile_name: str, wkt_crs: Optional[str],
                   s_max: Optional[float], z_min: Optional[float], z_max: Optional[float],
                   extra: Optional[Dict[str, Any]] = None) -> int:
    import sqlite3
    from qgis.core import QgsMessageLog
    
    ensure_schema(db_path)
    con = _connect(db_path)
    try:
        cur = con.cursor()
        cur.execute("""
            INSERT INTO gp_profiles(result_set_id, source_profile_fid, profile_name, wkt_crs, s_max, z_min, z_max, extra_json)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?);
        """, (
            result_set_id,
            source_profile_fid,
            profile_name or "",
            wkt_crs,
            s_max,
            z_min,
            z_max,
            json.dumps(extra) if extra else None
        ))
        pid = int(cur.lastrowid)
        con.commit()
        return pid
    except sqlite3.Error as e:
        con.rollback()
        QgsMessageLog.logMessage(
            f"Database error inserting profile: {e!r}",
            "qgSurf",
            Qgis.MessageLevel.Critical,
        )
        raise
    finally:
        con.close()


def insert_profile_samples(
    db_path: str,
    profile_id: int,
    samples: Iterable[Tuple[int, float, float, Optional[float], Optional[float]]],
    *,
    replace: bool = False,
    use_upsert: bool = False,
    chunk_size: int = DatabaseConstants.DEFAULT_CHUNK_SIZE,
) -> int:
    """
    samples: iterable di (idx, s, z, x, y)

    replace=True  -> cancella prima i campioni di quel profilo e reinserisce.
    use_upsert=True -> usa INSERT OR REPLACE (richiede UNIQUE su (profile_id, idx)).
    chunk_size -> inserisce a blocchi (utile per profili molto densi).

    Returns: numero righe inserite (tentate).
    """
    import sqlite3
    from qgis.core import QgsMessageLog
    
    ensure_schema(db_path)
    con = _connect(db_path)
    try:
        cur = con.cursor()

        if replace:
            cur.execute("DELETE FROM gp_profile_samples WHERE profile_id = ?;", (profile_id,))

        if use_upsert:
            sql = """
                INSERT OR REPLACE INTO gp_profile_samples(profile_id, idx, s, z, x, y)
                VALUES (?, ?, ?, ?, ?, ?);
            """
        else:
            sql = """
                INSERT INTO gp_profile_samples(profile_id, idx, s, z, x, y)
                VALUES (?, ?, ?, ?, ?, ?);
            """

        # materializzo una volta se samples è un generatore (per chunk)
        samples_list = list(samples)
        if not samples_list:
            return 0

        # Validate and filter samples
        total = 0
        for i in range(0, len(samples_list), chunk_size):
            chunk = samples_list[i:i + chunk_size]
            validated_chunk = []
            for idx, s, z, x, y in chunk:
                s_val = safe_float(s)
                z_val = safe_float(z)
                x_val = safe_float(x)
                y_val = safe_float(y)
                if s_val is not None and z_val is not None:
                    validated_chunk.append((profile_id, idx, s_val, z_val, x_val, y_val))
                else:
                    QgsMessageLog.logMessage(
                        f"Profile {profile_id}: skipping sample {idx} with invalid values (s={s}, z={z})",
                        "qgSurf",
                        Qgis.MessageLevel.Warning
                    )
            
            if validated_chunk:
                cur.executemany(sql, validated_chunk)
                total += len(validated_chunk)

        con.commit()
        return total

    except sqlite3.Error as e:
        con.rollback()
        QgsMessageLog.logMessage(
            f"Database error inserting profile samples: {e!r}",
            "qgSurf",
            Qgis.MessageLevel.Critical
        )
        raise
    finally:
        con.close()


def insert_projected_points(db_path: str, profile_id: int,
                            rows: Iterable[Dict[str, Any]]) -> None:
    import sqlite3
    from qgis.core import QgsMessageLog
    
    ensure_schema(db_path)
    con = _connect(db_path)
    try:

        con.execute("""
         DELETE FROM gp_projected_points WHERE profile_id = ?;
         """, (profile_id,))

        validated_rows = []
        for r in rows:
            s_val = safe_float(r.get("s"))
            z_val = safe_float(r.get("z"))
            if s_val is None or z_val is None:
                QgsMessageLog.logMessage(
                    f"Profile {profile_id}: skipping projected point with invalid coordinates (s={r.get('s')}, z={r.get('z')})",
                    "qgSurf",
                    Qgis.MessageLevel.Warning
                )
                continue
            validated_rows.append((
                profile_id,
                r.get("label", ""),
                s_val,
                z_val,
                safe_float(r.get("dist_to_profile")),
                safe_float(r.get("src_x")),
                safe_float(r.get("src_y")),
                safe_float(r.get("src_z")),
                r.get("src_fid"),
                json.dumps(r.get("extra")) if r.get("extra") else None
            ))

        if validated_rows:
            con.executemany("""
                INSERT INTO gp_projected_points(
                    profile_id, label, s, z, dist_to_profile, src_x, src_y, src_z, src_fid, extra_json
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
            """, validated_rows)
        con.commit()
    except sqlite3.Error as e:
        con.rollback()
        QgsMessageLog.logMessage(
            f"Database error inserting projected points: {e!r}",
            "qgSurf",
            Qgis.MessageLevel.Critical,
        )
        raise
    finally:
        con.close()


def insert_projected_attitudes(
        db_path: str,
        profile_id: int,
        rows: Iterable[Dict[str, Any]],
) -> None:
    import sqlite3
    from qgis.core import QgsMessageLog
    
    ensure_schema(db_path)
    con = _connect(db_path)

    try:
        validated_rows = []
        for r in rows:
            s_val = safe_float(r.get("s"))
            z_val = safe_float(r.get("z"))
            slope_val = safe_float(r.get("slope_degr"))
            dip_dir_val = safe_float(r.get("src_dip_dir"))
            dip_ang_val = safe_float(r.get("src_dip_ang"))
            
            if s_val is None or z_val is None or slope_val is None or dip_dir_val is None or dip_ang_val is None:
                QgsMessageLog.logMessage(
                    f"Profile {profile_id}: skipping projected attitude with invalid values (s={r.get('s')}, z={r.get('z')}, slope={r.get('slope_degr')})",
                    "qgSurf",
                    Qgis.MessageLevel.Warning
                )
                continue
            
            validated_rows.append((
                profile_id,
                r.get("label", ""),
                s_val,
                z_val,
                slope_val,
                r.get("down_sense"),
                dip_dir_val,
                dip_ang_val,
                safe_float(r.get("dist_to_profile")),
                safe_float(r.get("src_x")),
                safe_float(r.get("src_y")),
                safe_float(r.get("src_z")),
                r.get("src_fid"),
                json.dumps(r.get("extra")) if r.get("extra") else None
            ))

        if validated_rows:
            con.executemany("""
                INSERT INTO gp_projected_attitudes(
                    profile_id, 
                    label, 
                    s, 
                    z, 
                    slope_degr, 
                    down_sense, 
                    src_dip_dir, 
                    src_dip_ang,
                    dist_to_profile,
                    src_x, src_y, src_z, src_fid, extra_json
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
            """, validated_rows)
        con.commit()
    except sqlite3.Error as e:
        con.rollback()
        QgsMessageLog.logMessage(
            f"Database error inserting projected attitudes: {e!r}",
            "qgSurf",
            Qgis.MessageLevel.Critical,
        )
        raise
    finally:
        con.close()


def insert_projected_focal_mechanisms(
        db_path: str,
        profile_id: int,
        rows: Iterable[Dict[str, Any]],
) -> None:
    import sqlite3
    from qgis.core import QgsMessageLog

    ensure_schema(db_path)
    con = _connect(db_path)

    try:
        validated_rows = []
        for r in rows:
            s_val = safe_float(r.get("s"))
            z_val = safe_float(r.get("z"))
            strike_val = safe_float(r.get("strike"))
            dip_val = safe_float(r.get("dip"))
            rake_val = safe_float(r.get("rake"))
            profile_azimuth_val = safe_float(r.get("profile_azimuth"))

            if None in (s_val, z_val, strike_val, dip_val, rake_val, profile_azimuth_val):
                QgsMessageLog.logMessage(
                    f"Profile {profile_id}: skipping projected focal mechanism with invalid values "
                    f"(s={r.get('s')}, z={r.get('z')}, strike={r.get('strike')}, "
                    f"dip={r.get('dip')}, rake={r.get('rake')})",
                    "qgSurf",
                    Qgis.MessageLevel.Warning
                )
                continue

            validated_rows.append((
                profile_id,
                r.get("label", ""),
                s_val,
                z_val,
                strike_val,
                dip_val,
                rake_val,
                profile_azimuth_val,
                safe_float(r.get("dist_to_profile")),
                safe_float(r.get("src_x")),
                safe_float(r.get("src_y")),
                safe_float(r.get("src_z")),
                r.get("src_fid"),
                json.dumps(r.get("extra")) if r.get("extra") else None
            ))

        if validated_rows:
            con.executemany("""
                INSERT INTO gp_projected_focal_mechanisms(
                    profile_id,
                    label,
                    s,
                    z,
                    strike,
                    dip,
                    rake,
                    profile_azimuth,
                    dist_to_profile,
                    src_x, src_y, src_z, src_fid, extra_json
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
            """, validated_rows)
        con.commit()
    except sqlite3.Error as e:
        con.rollback()
        QgsMessageLog.logMessage(
            f"Database error inserting projected focal mechanisms: {e!r}",
            "qgSurf",
            Qgis.MessageLevel.Critical,
        )
        raise
    finally:
        con.close()


def insert_intersected_lines(
    db_path: str,
    profile_id: int,
    rows: Iterable[Dict[str, Any]]
) -> None:
    import sqlite3
    import math
    from qgis.core import QgsMessageLog
    
    ensure_schema(db_path)
    con = _connect(db_path)

    try:
        # idempotenza per profilo: evita duplicati se riesporti
        con.execute("DELETE FROM gp_intersected_lines WHERE profile_id = ?;", (profile_id,))

        validated_rows = []
        for r in rows:
            s_val = safe_float(r.get("s"))
            if s_val is None:
                QgsMessageLog.logMessage(
                    f"Profile {profile_id}: skipping intersected line with invalid s value: {r.get('s')}",
                    "qgSurf",
                    Qgis.MessageLevel.Warning
                )
                continue
            validated_rows.append((
                profile_id,
                r["feature_id"],
                s_val,
                json.dumps(r.get("extra")) if r.get("extra") else None
            ))

        if validated_rows:
            con.executemany("""
                INSERT INTO gp_intersected_lines(
                    profile_id, feat_category, s, extra_json
                ) VALUES (?, ?, ?, ?);
            """, validated_rows)
        con.commit()
    except sqlite3.Error as e:
        con.rollback()
        QgsMessageLog.logMessage(
            f"Database error inserting intersected lines: {e!r}",
            "qgSurf",
            QgsMessageLog.CRITICAL
        )
        raise
    finally:
        con.close()


def insert_intersected_polygons(db_path: str, profile_id: int,
                                rows: Iterable[Dict[str, Any]]) -> None:
    import sqlite3
    import math
    from qgis.core import QgsMessageLog
    
    ensure_schema(db_path)
    con = _connect(db_path)
    try:
        # idempotenza per profilo
        con.execute("DELETE FROM gp_intersected_polygons WHERE profile_id = ?;", (profile_id,))

        validated_rows = []
        for r in rows:
            s_from_val = safe_float(r.get("s_from"))
            s_to_val = safe_float(r.get("s_to"))
            if s_from_val is None or s_to_val is None:
                QgsMessageLog.logMessage(
                    f"Profile {profile_id}: skipping intersected polygon with invalid s values (from={r.get('s_from')}, to={r.get('s_to')})",
                    "qgSurf",
                    Qgis.MessageLevel.Warning
                )
                continue
            validated_rows.append((
                profile_id,
                r["unit_name"],
                s_from_val,
                s_to_val,
                json.dumps(r.get("extra")) if r.get("extra") else None
            ))

        if validated_rows:
            con.executemany("""
                INSERT INTO gp_intersected_polygons(
                    profile_id, unit_name, s_from, s_to, extra_json
                ) VALUES (?, ?, ?, ?, ?);
            """, validated_rows)
        con.commit()
    except sqlite3.Error as e:
        con.rollback()
        QgsMessageLog.logMessage(
            f"Database error inserting intersected polygons: {e!r}",
            "qgSurf",
            QgsMessageLog.CRITICAL
        )
        raise
    finally:
        con.close()

def preprocess_labels(
        labels,
        orders,
        data
):

    if labels is None:

        labels = [''] * len(data)

    if orders is None:

        orders = list(range(1, len(data) + 1))

    return labels, orders


def write_rubberband_profile_lnshp(
        fileName,
        header_list,
        points,
        sr
):

    shape_driver_name = "ESRI Shapefile"
    shape_driver = ogr.GetDriverByName(shape_driver_name)
    if shape_driver is None:
        return False, "%s driver is not available" % shape_driver_name

    datasource = shape_driver.CreateDataSource(str(fileName))

    if datasource is None:
        return False, "Creation of %s shapefile failed" % os.path.split(fileName)[1]

    layer = datasource.CreateLayer('profile', sr, geom_type=ogr.wkbLineString)
    if layer is None:
        return False, "Output layer creation failed"

    # creates required fields
    layer.CreateField(ogr.FieldDefn(header_list[0], ogr.OFTInteger))
    featureDefn = layer.GetLayerDefn()

    # loops through output records

    import math
    from qgis.core import QgsMessageLog

    for ndx in range(len(points)-1):

        try:
            _, x0, y0 = points[ndx]
            _, x1, y1 = points[ndx+1]
        except (ValueError, IndexError, TypeError) as e:
            QgsMessageLog.logMessage(
                f"Invalid point data at index {ndx}: {points[ndx] if ndx < len(points) else 'N/A'}, error: {e!r}",
                "qgSurf",
                Qgis.MessageLevel.Warning
            )
            continue

        # Validate and convert to float
        try:
            x0_f = float(x0)
            y0_f = float(y0)
            x1_f = float(x1)
            y1_f = float(y1)
        except (TypeError, ValueError) as e:
            QgsMessageLog.logMessage(
                f"Invalid coordinate values at index {ndx}: x0={x0}, y0={y0}, x1={x1}, y1={y1}, error: {e!r}",
                "qgSurf",
                Qgis.MessageLevel.Warning
            )
            continue

        # Validate that coordinates are finite
        if not (math.isfinite(x0_f) and math.isfinite(y0_f) and math.isfinite(x1_f) and math.isfinite(y1_f)):
            QgsMessageLog.logMessage(
                f"Non-finite coordinates at index {ndx}: x0={x0_f}, y0={y0_f}, x1={x1_f}, y1={y1_f}",
                "qgSurf",
                Qgis.MessageLevel.Warning
            )
            continue

        ln_feature = ogr.Feature(featureDefn)
        segment_2d = ogr.CreateGeometryFromWkt('LINESTRING(%f %f, %f %f)' % (x0_f, y0_f, x1_f, y1_f))
        ln_feature.SetGeometry(segment_2d)

        ln_feature.SetField(header_list[0], 1)
        result = layer.CreateFeature(ln_feature)
        if result != ogr.OGRERR_NONE:
            QgsMessageLog.logMessage(
                f"Error creating line feature at index {ndx}: OGR error code {result}",
                "qgSurf",
                Qgis.MessageLevel.Warning
            )

        try:
            ln_feature.Destroy()
        except Exception:
            logger.debug("ln_feature.Destroy() failed", exc_info=True)

    datasource.Destroy()

    return True, "done"


def write_generic_csv(
        output_filepath,
        header_list,
        parsed_results,
        sep: str = ","
):

    try:
        with open(str(output_filepath), 'w') as f:
            f.write(sep.join(header_list) + '\n')
            for rec in parsed_results:
                out_rec_string = ''
                for val in rec:
                    out_rec_string += str(val) + sep
                f.write(out_rec_string[:-1] + '\n')
        return True, "done"
    except Exception as e:
        return False, e


def write_topography_singledem_csv(
        fileName,
        header_list,
        labels,
        orders,
        multiprofile_dem_data,
        current_dem_ndx,
        sep: str = ","
):

    labels, orders = preprocess_labels(
        labels,
        orders,
        multiprofile_dem_data
    )

    try:
        with open(str(fileName), 'w') as f:
            f.write(sep.join(header_list) + '\n')
            for prof_ndx, profile_label, profile_data in zip(orders, labels, multiprofile_dem_data):
                for rec in profile_data:
                    rec_id, x, y, cum2ddist = rec[:4]
                    z = rec[3 + current_dem_ndx * 3 + 1]
                    cum3ddist = rec[3 + current_dem_ndx * 3 + 2]
                    slope = rec[3 + current_dem_ndx * 3 + 3]
                    outdata_list = list(map(str, [prof_ndx, profile_label, rec_id, x, y, cum2ddist, z, cum3ddist, slope]))
                    f.write(sep.join(outdata_list) + "\n")
        return True, "done"
    except Exception as e:
        return False, e


def write_topography_singledem_ptshp(
        out_file_path,
        header_list,
        labels,
        orders,
        multiprofile_dem_data,
        current_dem_ndx,
        sr
):

    labels, orders = preprocess_labels(
        labels,
        orders,
        multiprofile_dem_data
    )

    shape_driver_name = "ESRI Shapefile"
    shape_driver = ogr.GetDriverByName(shape_driver_name)
    if shape_driver is None:
        return False, "%s driver is not available" % shape_driver_name

    try:
        datasource = shape_driver.CreateDataSource(str(out_file_path))
    except TypeError:
        datasource = shape_driver.CreateDataSource(str(out_file_path))

    if datasource is None:
        return False, "Creation of %s shapefile failed" % os.path.split(out_file_path)[1]

    layer = datasource.CreateLayer('profile', sr, geom_type=ogr.wkbPoint)
    if layer is None:
        return False, "Output layer creation failed"

        # creates required fields
    layer.CreateField(ogr.FieldDefn(header_list[0], ogr.OFTInteger))
    labelsFldDef = ogr.FieldDefn(header_list[1], ogr.OFTString)
    labelsFldDef.SetWidth(255)
    layer.CreateField(labelsFldDef)
    layer.CreateField(ogr.FieldDefn(header_list[2], ogr.OFTInteger))
    layer.CreateField(ogr.FieldDefn(header_list[3], ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn(header_list[4], ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn(header_list[5], ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn(header_list[6], ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn(header_list[7], ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn(header_list[8], ogr.OFTReal))

    featureDefn = layer.GetLayerDefn()

    # loops through geoprofiles and output records

    for prof_ndx, profile_label, profile_data in zip(orders, labels, multiprofile_dem_data):
        for rec in profile_data:
            rec_id, x, y, cumdist2D = rec[0], rec[1], rec[2], rec[3]
            z = rec[3 + current_dem_ndx * 3 + 1]
            cumdist3D = rec[3 + current_dem_ndx * 3 + 2]
            slopedegr = rec[3 + current_dem_ndx * 3 + 3]

            if z == "":
                continue

            pt_feature = ogr.Feature(featureDefn)

            pt = ogr.Geometry(ogr.wkbPoint25D)
            pt.SetPoint(0, x, y, z)
            pt_feature.SetGeometry(pt)

            pt_feature.SetField(header_list[0], prof_ndx)
            pt_feature.SetField(header_list[1], profile_label)
            pt_feature.SetField(header_list[2], rec_id)
            pt_feature.SetField(header_list[3], x)
            pt_feature.SetField(header_list[4], y)
            pt_feature.SetField(header_list[5], cumdist2D)
            pt_feature.SetField(header_list[6], z)
            if cumdist3D != '':
                pt_feature.SetField(header_list[7], cumdist3D)
            if slopedegr != '':
                pt_feature.SetField(header_list[8], slopedegr)

            layer.CreateFeature(pt_feature)

            pt_feature.Destroy()

    datasource.Destroy()

    return True, "done"


def write_topography_singledem_lnshp(
        fileName,
        header_list,
        labels,
        orders,
        multiprofile_dem_data,
        current_dem_ndx,
        sr
):

    labels, orders = preprocess_labels(
        labels,
        orders,
        multiprofile_dem_data
    )

    shape_driver_name = "ESRI Shapefile"
    shape_driver = ogr.GetDriverByName(shape_driver_name)
    if shape_driver is None:
        return False, "%s driver is not available" % shape_driver_name

    datasource = shape_driver.CreateDataSource(str(fileName))

    if datasource is None:
        return False, "Creation of %s shapefile failed" % os.path.split(fileName)[1]

    layer = datasource.CreateLayer('profile', sr, geom_type=ogr.wkbLineString25D)
    if layer is None:
        return False, "Output layer creation failed"

        # creates required fields
    layer.CreateField(ogr.FieldDefn(header_list[0], ogr.OFTInteger))
    labelsFldDef = ogr.FieldDefn(header_list[1], ogr.OFTString)
    labelsFldDef.SetWidth(255)
    layer.CreateField(labelsFldDef)
    layer.CreateField(ogr.FieldDefn(header_list[2], ogr.OFTInteger))
    layer.CreateField(ogr.FieldDefn(header_list[5], ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn(header_list[7], ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn(header_list[8], ogr.OFTReal))

    featureDefn = layer.GetLayerDefn()

    # loops through output records

    for prof_ndx, profile_label, profile_data in zip(orders, labels, multiprofile_dem_data):

        for ndx in range(len(profile_data) - 1):

            rec_a = profile_data[ndx]
            rec_b = profile_data[ndx + 1]

            rec_id = rec_a[0]
            x0, y0, z0 = rec_a[1], rec_a[2], rec_a[3 + current_dem_ndx * 3 + 1]
            x1, y1, z1 = rec_b[1], rec_b[2], rec_b[3 + current_dem_ndx * 3 + 1]
            cum3ddist = rec_b[3 + current_dem_ndx * 3 + 2]
            slope_degr = rec_b[3 + current_dem_ndx * 3 + 3]

            if z0 == '' or z1 == '':
                continue

            ln_feature = ogr.Feature(featureDefn)
            segment_3d = ogr.CreateGeometryFromWkt('LINESTRING(%f %f %f, %f %f %f)' % (x0, y0, z0, x1, y1, z1))
            ln_feature.SetGeometry(segment_3d)

            ln_feature.SetField(header_list[0], prof_ndx)
            ln_feature.SetField(header_list[1], profile_label)
            ln_feature.SetField(header_list[2], rec_id)
            ln_feature.SetField(header_list[5], rec_b[3])

            if cum3ddist != '':
                ln_feature.SetField(header_list[7], cum3ddist)

            if slope_degr != '':
                ln_feature.SetField(header_list[8], slope_degr)

            layer.CreateFeature(ln_feature)

            ln_feature.Destroy()

    datasource.Destroy()

    return True, "done"


def write_topography_multidems_csv(
        fileName,
        multi_dems_headers,
        labels,
        orders,
        multiprofile_dem_data,
        sep: str = ","
):

    labels, orders = preprocess_labels(
        labels,
        orders,
        multiprofile_dem_data
    )

    try:
        with open(str(fileName), 'w') as f:
            f.write(sep.join(multi_dems_headers) + '\n')
            for prof_ndx, profile_label, profile_data in zip(orders, labels, multiprofile_dem_data):
                for rec in profile_data:
                    out_rec_string = sep.join(map(str, [prof_ndx, profile_label]+rec))
                    f.write(out_rec_string + '\n')
        return True, "done"
    except Exception as e:
        return False, e


def write_topography_multidems_ptshp(
        fileName,
        multidems_headers,
        dem_names,
        labels,
        orders,
        multiprofile_dem_data,
        sr
):

    labels, orders = preprocess_labels(
        labels,
        orders,
        multiprofile_dem_data
    )

    shape_driver_name = "ESRI Shapefile"
    shape_driver = ogr.GetDriverByName(shape_driver_name)
    if shape_driver is None:
        return False, "%s driver is not available" % shape_driver_name

    datasource = shape_driver.CreateDataSource(str(fileName))

    if datasource is None:
        return False, "Creation of %s shapefile failed" % os.path.split(fileName)[1]

    layer = datasource.CreateLayer('profile', sr, geom_type=ogr.wkbPoint)
    if layer is None:
        return False, "Output layer creation failed"

    # creates required fields
    layer.CreateField(ogr.FieldDefn(multidems_headers[0], ogr.OFTInteger))
    labelsFldDef = ogr.FieldDefn(multidems_headers[1], ogr.OFTString)
    labelsFldDef.SetWidth(255)
    layer.CreateField(labelsFldDef)
    layer.CreateField(ogr.FieldDefn(multidems_headers[2], ogr.OFTInteger))
    layer.CreateField(ogr.FieldDefn(multidems_headers[3], ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn(multidems_headers[4], ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn(multidems_headers[5], ogr.OFTReal))

    for dem_ndx in range(len(dem_names)):
        layer.CreateField(ogr.FieldDefn(multidems_headers[5 + dem_ndx * 3 + 1], ogr.OFTReal))
        layer.CreateField(ogr.FieldDefn(multidems_headers[5 + dem_ndx * 3 + 2], ogr.OFTReal))
        layer.CreateField(ogr.FieldDefn(multidems_headers[5 + dem_ndx * 3 + 3], ogr.OFTReal))

    featureDefn = layer.GetLayerDefn()

    field_names = []
    for i in range(featureDefn.GetFieldCount()):
        field_names.append(featureDefn.GetFieldDefn(i).GetName())

    if len(multidems_headers) != len(field_names):
        raise ValueError("multidems_headers and field_names must have the same length")

    # loops through output records

    for prof_ndx, profile_label, profile_data in zip(orders, labels, multiprofile_dem_data):

        for rec in profile_data:

            pt_feature = ogr.Feature(featureDefn)

            pt = ogr.Geometry(ogr.wkbPoint)
            pt.SetPoint(0, rec[1], rec[2])
            pt_feature.SetGeometry(pt)

            pt_feature.SetField(field_names[0], prof_ndx)
            pt_feature.SetField(field_names[1], profile_label)
            pt_feature.SetField(field_names[2], rec[0])
            pt_feature.SetField(field_names[3], rec[1])
            pt_feature.SetField(field_names[4], rec[2])
            pt_feature.SetField(field_names[5], rec[3])
            for dem_ndx in range(len(dem_names)):
                dem_height = rec[3 + dem_ndx * 3 + 1]
                if dem_height != '':
                    pt_feature.SetField(field_names[5 + dem_ndx * 3 + 1], dem_height)
                cum3ddist = rec[3 + dem_ndx * 3 + 2]
                if cum3ddist != '':
                    pt_feature.SetField(field_names[5 + dem_ndx * 3 + 2], cum3ddist)
                slope = rec[3 + dem_ndx * 3 + 3]
                if slope != '':
                    pt_feature.SetField(field_names[5 + dem_ndx * 3 + 3], slope)

            layer.CreateFeature(pt_feature)

            pt_feature.Destroy()

    datasource.Destroy()

    return True, "Done"


def write_topography_multidems_lnshp(
        fileName,
        header_list,
        dem_names,
        labels,
        orders,
        multiprofile_dem_data,
        sr
):

    labels, orders = preprocess_labels(
        labels,
        orders,
        multiprofile_dem_data
    )

    shape_driver_name = "ESRI Shapefile"
    shape_driver = ogr.GetDriverByName(shape_driver_name)
    if shape_driver is None:
        return False, "%s driver is not available" % shape_driver_name

    datasource = shape_driver.CreateDataSource(str(fileName))

    if datasource is None:
        return False, "Creation of %s shapefile failed" % os.path.split(fileName)[1]

    layer = datasource.CreateLayer('profile', sr, geom_type=ogr.wkbLineString)
    if layer is None:
        return False, "Output layer creation failed"

    # creates required fields

    layer.CreateField(ogr.FieldDefn(header_list[0], ogr.OFTInteger))  # prof ndx
    labelsFldDef = ogr.FieldDefn(header_list[1], ogr.OFTString)
    labelsFldDef.SetWidth(255)
    layer.CreateField(labelsFldDef)
    layer.CreateField(ogr.FieldDefn(header_list[2], ogr.OFTInteger))  # rec ndx
    layer.CreateField(ogr.FieldDefn(header_list[5], ogr.OFTReal))  # cum dist 2d
    for dem_ndx in range(len(dem_names)):
        layer.CreateField(ogr.FieldDefn(header_list[5 + dem_ndx * 3 + 1], ogr.OFTReal))
        layer.CreateField(ogr.FieldDefn(header_list[5 + dem_ndx * 3 + 2], ogr.OFTReal))
        layer.CreateField(ogr.FieldDefn(header_list[5 + dem_ndx * 3 + 3], ogr.OFTReal))

    featureDefn = layer.GetLayerDefn()

    field_names = []
    for i in range(featureDefn.GetFieldCount()):
        field_names.append(featureDefn.GetFieldDefn(i).GetName())

    # loops through output records

    for prof_ndx, profile_label, profile_data in zip(orders, labels, multiprofile_dem_data):

        for ndx in range(len(profile_data) - 1):

            rec_a = profile_data[ndx]
            rec_b = profile_data[ndx + 1]

            rec_a_x, rec_a_y = rec_a[1], rec_a[2]
            rec_b_x, rec_b_y = rec_b[1], rec_b[2]

            ln_feature = ogr.Feature(featureDefn)

            segment_2d = ogr.CreateGeometryFromWkt('LINESTRING(%f %f, %f %f)' % (rec_a_x, rec_a_y, rec_b_x, rec_b_y))
            ln_feature.SetGeometry(segment_2d)

            ln_feature.SetField(field_names[0], prof_ndx)
            ln_feature.SetField(field_names[1], profile_label)
            ln_feature.SetField(field_names[2], rec_a[0])
            ln_feature.SetField(field_names[5], rec_b[3])
            for dem_ndx, dem_name in enumerate(dem_names):
                dem_height = rec_b[3 + dem_ndx * 3 + 1]
                if dem_height != '':
                    ln_feature.SetField(field_names[3 + dem_ndx * 3 + 1], dem_height)
                cum3ddist = rec_b[3 + dem_ndx * 3 + 2]
                if cum3ddist != '':
                    ln_feature.SetField(field_names[3 + dem_ndx * 3 + 2], cum3ddist)
                slope = rec_b[3 + dem_ndx * 3 + 3]
                if slope != '':
                    ln_feature.SetField(field_names[3 + dem_ndx * 3 + 3], slope)

            layer.CreateFeature(ln_feature)
            ln_feature.Destroy()

    datasource.Destroy()

    return True, "Done"


def write_topography_gpx_ptshp(
        output_filepath,
        header_list,
        gpx_parsed_results,
        sr
):

    shape_driver_name = "ESRI Shapefile"
    shape_driver = ogr.GetDriverByName(shape_driver_name)
    if shape_driver is None:
        return False, "%s driver is not available" % shape_driver_name

    try:
        datasource = shape_driver.CreateDataSource(str(output_filepath))
    except TypeError:
        datasource = shape_driver.CreateDataSource(str(output_filepath))

    if datasource is None:
        return False, "Creation of %s shapefile failed" % os.path.split(output_filepath)[1]

    layer = datasource.CreateLayer('profile', sr, geom_type=ogr.wkbPoint)
    if layer is None:
        return False, "Point layer creation failed"

        # creates required fields
    layer.CreateField(ogr.FieldDefn(header_list[0], ogr.OFTInteger))
    layer.CreateField(ogr.FieldDefn(header_list[1], ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn(header_list[2], ogr.OFTReal))
    time_field = ogr.FieldDefn(header_list[3], ogr.OFTString)
    time_field.SetWidth(20)
    layer.CreateField(time_field)
    layer.CreateField(ogr.FieldDefn(header_list[4], ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn(header_list[5], ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn(header_list[6], ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn(header_list[7], ogr.OFTReal))

    featureDefn = layer.GetLayerDefn()

    # loops through output records
    for rec in gpx_parsed_results:

        pt_feature = ogr.Feature(featureDefn)

        pt = ogr.Geometry(ogr.wkbPoint)
        pt.SetPoint(0, rec[2], rec[1])
        pt_feature.SetGeometry(pt)

        pt_feature.SetField(header_list[0], rec[0])
        pt_feature.SetField(header_list[1], rec[1])
        pt_feature.SetField(header_list[2], rec[2])

        pt_feature.SetField(header_list[3], str(rec[3]))
        if rec[4] != '':
            pt_feature.SetField(header_list[4], str(rec[4]))
        pt_feature.SetField(header_list[5], rec[5])
        if rec[6] != '':
            pt_feature.SetField(header_list[6], rec[6])
        if rec[7] != '':
            pt_feature.SetField(header_list[7], rec[7])

        layer.CreateFeature(pt_feature)

        pt_feature.Destroy()

    datasource.Destroy()

    return True, "done"


def write_topography_gpx_lnshp(
        output_filepath,
        header_list,
        gpx_parsed_results,
        sr
):

    shape_driver_name = "ESRI Shapefile"
    shape_driver = ogr.GetDriverByName(shape_driver_name)
    if shape_driver is None:
        return False, "%s driver is not available" % shape_driver_name

    try:
        datasource = shape_driver.CreateDataSource(str(output_filepath))
    except TypeError:
        datasource = shape_driver.CreateDataSource(str(output_filepath))

    if datasource is None:
        return False, "Creation of %s shapefile failed" % os.path.split(output_filepath)[1]

    layer = datasource.CreateLayer('profile', sr, geom_type=ogr.wkbLineString25D)
    if layer is None:
        return False, "Output layer creation failed"

        # creates required fields
    layer.CreateField(ogr.FieldDefn(header_list[0], ogr.OFTInteger))
    time_beg_field = ogr.FieldDefn('time_beg', ogr.OFTString)
    time_beg_field.SetWidth(20)
    layer.CreateField(time_beg_field)
    time_end_field = ogr.FieldDefn('time_end', ogr.OFTString)
    time_end_field.SetWidth(20)
    layer.CreateField(time_end_field)
    layer.CreateField(ogr.FieldDefn(header_list[5], ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn(header_list[6], ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn(header_list[7], ogr.OFTReal))

    featureDefn = layer.GetLayerDefn()

    # loops through output records
    for ndx in range(len(gpx_parsed_results) - 1):

        rec_a = gpx_parsed_results[ndx]
        rec_b = gpx_parsed_results[ndx + 1]

        lon0, lat0, z0 = rec_a[2], rec_a[1], rec_a[4]
        lon1, lat1, z1 = rec_b[2], rec_b[1], rec_b[4]

        if z0 == '' or z1 == '':
            continue

        ln_feature = ogr.Feature(featureDefn)
        segment_3d = ogr.CreateGeometryFromWkt('LINESTRING(%f %f %f, %f %f %f)' % (lon0, lat0, z0, lon1, lat1, z1))
        ln_feature.SetGeometry(segment_3d)

        ln_feature.SetField(header_list[0], rec_a[0])
        ln_feature.SetField('time_beg', str(rec_a[3]))
        ln_feature.SetField('time_end', str(rec_b[3]))
        ln_feature.SetField(header_list[5], rec_b[5])
        if rec_b[6] != '':
            ln_feature.SetField(header_list[6], rec_b[6])
        if rec_b[7] != '':
            ln_feature.SetField(header_list[7], rec_b[7])

        layer.CreateFeature(ln_feature)

        ln_feature.Destroy()

    datasource.Destroy()

    return True, "done"


def write_geological_attitudes_ptshp(
        fileName,
        parsed_crosssect_results,
        sr
):

    shape_driver_name = "ESRI Shapefile"
    shape_driver = ogr.GetDriverByName(shape_driver_name)
    if shape_driver is None:
        return False, "%s driver is not available" % shape_driver_name

    datasource = shape_driver.CreateDataSource(str(fileName))

    if datasource is None:
        return False, "Creation of %s shapefile failed" % os.path.split(fileName)[1]

    layer = datasource.CreateLayer('profile', sr, geom_type=ogr.wkbPoint25D)
    if layer is None:
        return False, "Output layer creation failed"

    # creates required fields
    layer.CreateField(ogr.FieldDefn('id', ogr.OFTString))
    layer.CreateField(ogr.FieldDefn('or_pt_x', ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn('or_pt_y', ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn('or_pt_z', ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn('prj_pt_x', ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn('prj_pt_y', ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn('prj_pt_z', ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn('s', ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn('or_dpdir', ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn('or_dpang', ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn('tr_dpang', ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn('tr_dpdir', ogr.OFTString))

    featureDefn = layer.GetLayerDefn()

    # loops through output records
    for rec in parsed_crosssect_results:
        pt_id, or_pt_x, or_pt_y, or_pt_z, pr_pt_x, pr_pt_y, pr_pt_z, s, or_dipdir, or_dipangle, tr_dipangle, tr_dipdir = rec

        pt_feature = ogr.Feature(featureDefn)

        pt = ogr.Geometry(ogr.wkbPoint25D)
        pt.SetPoint(0, pr_pt_x, pr_pt_y, pr_pt_z)
        pt_feature.SetGeometry(pt)

        pt_feature.SetField('id', str(pt_id))
        pt_feature.SetField('or_pt_x', or_pt_x)
        pt_feature.SetField('or_pt_y', or_pt_y)
        pt_feature.SetField('or_pt_z', or_pt_z)
        pt_feature.SetField('prj_pt_x', pr_pt_x)
        pt_feature.SetField('prj_pt_y', pr_pt_y)
        pt_feature.SetField('prj_pt_z', pr_pt_z)
        pt_feature.SetField('s', s)
        pt_feature.SetField('or_dpdir', or_dipdir)
        pt_feature.SetField('or_dpang', or_dipangle)
        pt_feature.SetField('tr_dpang', tr_dipangle)
        pt_feature.SetField('tr_dpdir', str(tr_dipdir))

        layer.CreateFeature(pt_feature)

        pt_feature.Destroy()

    datasource.Destroy()

    return True, "done"


def write_intersection_line_csv(
        output_filepath,
        header_list,
        parsed_results,
        sep: str = ","
):

    try:
        with open(str(output_filepath), 'w') as f:
            f.write(sep.join(header_list) + '\n')
            for classification, line3d, s_list in parsed_results:
                for pt, s in zip(line3d.pts, s_list):
                    out_values = [classification, s, pt.x, pt.y, pt.y]
                    out_val_strings = [str(val) for val in out_values]
                    f.write(sep.join(out_val_strings) + '\n')
        return True, "done"
    except Exception as e:
        return False, e


def write_intersection_line_ptshp(
        fileName,
        header_list,
        intersline_results,
        sr
):

    shape_driver_name = "ESRI Shapefile"
    shape_driver = ogr.GetDriverByName(shape_driver_name)
    if shape_driver is None:
        return False, "%s driver is not available" % shape_driver_name

    datasource = shape_driver.CreateDataSource(str(fileName))

    if datasource is None:
        return False, "Creation of %s shapefile failed" % os.path.split(fileName)[1]

    layer = datasource.CreateLayer('profile', sr, geom_type=ogr.wkbPoint25D)
    if layer is None:
        return False, "Output layer creation failed"

    # creates required fields
    layer.CreateField(ogr.FieldDefn(header_list[0], ogr.OFTString))
    layer.CreateField(ogr.FieldDefn(header_list[1], ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn(header_list[2], ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn(header_list[3], ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn(header_list[4], ogr.OFTReal))

    featureDefn = layer.GetLayerDefn()

    # loops through output records
    for rec_id, s, x, y, z in intersline_results:
        pt_feature = ogr.Feature(featureDefn)

        pt = ogr.Geometry(ogr.wkbPoint25D)
        pt.SetPoint(0, x, y, z)
        pt_feature.SetGeometry(pt)

        pt_feature.SetField(header_list[0], str(rec_id))
        pt_feature.SetField(header_list[1], s)
        pt_feature.SetField(header_list[2], x)
        pt_feature.SetField(header_list[3], y)
        pt_feature.SetField(header_list[4], z)

        layer.CreateFeature(pt_feature)

        pt_feature.Destroy()

    datasource.Destroy()

    return True, "done"


def write_intersection_polygon_lnshp(
        fileName,
        header_list,
        intersline_results,
        sr
):

    shape_driver_name = "ESRI Shapefile"
    shape_driver = ogr.GetDriverByName(shape_driver_name)
    if shape_driver is None:
        return False, "%s driver is not available" % shape_driver_name

    datasource = shape_driver.CreateDataSource(str(fileName))

    if datasource is None:
        return False, "Creation of %s shapefile failed" % os.path.split(fileName)[1]

    layer = datasource.CreateLayer('profile', sr, geom_type=ogr.wkbLineString25D)
    if layer is None:
        return False, "Output layer creation failed"

    # creates required fields
    layer.CreateField(ogr.FieldDefn(header_list[0], ogr.OFTString))
    layer.CreateField(ogr.FieldDefn(header_list[1], ogr.OFTReal))

    featureDefn = layer.GetLayerDefn()

    # loops through output records

    for classification, line3d, s_list in intersline_results:

        if len(line3d.pts) != len(s_list):
            raise ValueError("line3d.pts and s_list must have the same length")

        # loops through output records

        for ndx in range(len(line3d.pts) - 1):
            rec_a = line3d.pts[ndx]
            rec_b = line3d.pts[ndx + 1]

            x0, y0, z0 = rec_a.x, rec_a.y, rec_a.y
            x1, y1, z1 = rec_b.x, rec_b.y, rec_b.y
            s = s_list[ndx + 1]

            ln_feature = ogr.Feature(featureDefn)
            segment_3d = ogr.CreateGeometryFromWkt('LINESTRING(%f %f %f, %f %f %f)' % (x0, y0, z0, x1, y1, z1))
            ln_feature.SetGeometry(segment_3d)

            ln_feature.SetField(header_list[0], str(classification))
            ln_feature.SetField(header_list[1], s)

            layer.CreateFeature(ln_feature)

            ln_feature.Destroy()

    datasource.Destroy()

    return True, "Output layer created"

