#!/usr/bin/env python3
"""
csv_to_gpx_integrated.py

Convert a WLTC-style CSV file into a GPX file whose structure mirrors
“trackExample.gpx”, while honouring the formatting constraints supplied by the
user (headers, indentation, bounds line, single-line <trkpt> payload, etc.).

Key features:
  • CSV delimiter is semicolon (“;”), with columns:
        Time in s;Speed in km/h
  • Latitudes are generated by integrating speed over time assuming constant
    southbound motion (longitude remains fixed unless you supply a custom value).
  • The GPX header comments, opening <gpx> tag layout, <bounds> line, blank
    lines and trackpoint formatting all match the requested template.

Usage example:
    python csv2gpx.py WLTC-class1.csv WLTC-class1.gpx \
        --track-name "WLTC class 1 profile" \
        --activity car \
        --start-time "2024-02-19T07:02:57Z" \
        --latitude 46.77882140 \
        --longitude 6.69573278 \
        --creator "CSV to GPX Converter"

Optional flags:
    --include-satellites     → append <sat>12</sat> to each <trkpt>
    --timezone-offset MIN    → offset (in minutes) if deriving start-time from epoch
"""

import argparse
import csv
from datetime import datetime, timedelta, timezone
from math import pi
from pathlib import Path


EARTH_RADIUS_M = 6_371_000  # Mean Earth radius in metres


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Convert WLTC CSV to formatted GPX.")
    parser.add_argument("input_csv", help="Path to the WLTC CSV file.")
    parser.add_argument("output_gpx", help="Destination GPX file path.")
    parser.add_argument(
        "--track-name",
        default="WLTC Class 1",
        help="Name for the GPX track (default: %(default)s).",
    )
    parser.add_argument(
        "--activity",
        default="car",
        help="Activity keyword to embed in the GPX metadata (default: %(default)s).",
    )
    parser.add_argument(
        "--start-time",
        default="2025-10-20T17:19:00Z",
        help=(
            "ISO-8601 UTC start timestamp (e.g. 2024-02-19T07:02:57Z). "
            "If omitted, the first CSV timestamp is interpreted as seconds since the Unix epoch."
        ),
    )
    parser.add_argument(
        "--timezone-offset",
        type=int,
        default=0,
        help="Timezone offset in minutes when deriving the start-time from the epoch (default: %(default)s).",
    )
    parser.add_argument(
        "--creator",
        default="CSV to GPX Converter",
        help="GPX creator attribute (default: %(default)s).",
    )
    parser.add_argument(
        "--latitude",
        type=float,
        default=46.77882140,
        help="Starting latitude in decimal degrees (default: %(default)s).",
    )
    parser.add_argument(
        "--longitude",
        type=float,
        default=6.69573278,
        help="Starting longitude in decimal degrees (default: %(default)s).",
    )
    parser.add_argument(
        "--elevation",
        type=float,
        default=500.0,
        help="Constant elevation (metres) applied to every track point (default: %(default)s).",
    )
    parser.add_argument(
        "--include-satellites",
        action="store_true",
        help="Include <sat>12</sat> within each <trkpt> line.",
    )
    return parser.parse_args()


def read_wltc_csv(path: Path) -> list[tuple[float, float]]:
    """Return a list of (time_seconds, speed_kmh) tuples extracted from the CSV."""
    samples: list[tuple[float, float]] = []
    with path.open(newline="", encoding="utf-8-sig") as csvfile:
        reader = csv.reader(csvfile, delimiter=";")
        for row in reader:
            if not row:
                continue
            if row[0].strip().lower().startswith("time"):
                continue
            try:
                time_s = float(row[0].strip())
                speed_kmh = float(row[1].strip())
            except (ValueError, IndexError):
                continue
            samples.append((time_s, speed_kmh))
    if not samples:
        raise ValueError("No valid samples found in the CSV input.")
    return samples


def resolve_start_time(args: argparse.Namespace, first_seconds: float) -> datetime:
    """Determine the datetime to use for the first track point."""
    if args.start_time:
        try:
            parsed = datetime.fromisoformat(args.start_time.replace("Z", "+00:00"))
        except ValueError as exc:
            raise ValueError(f"Invalid --start-time '{args.start_time}': {exc}") from exc
        if parsed.tzinfo is None:
            parsed = parsed.replace(tzinfo=timezone.utc)
        return parsed.astimezone(timezone.utc)

    tz = timezone(timedelta(minutes=args.timezone_offset))
    epoch = datetime(1970, 1, 1, tzinfo=timezone.utc)
    return (epoch + timedelta(seconds=first_seconds)).astimezone(tz).astimezone(timezone.utc)


def integrate_positions(
    samples: list[tuple[float, float]],
    start_lat: float,
    start_lon: float,
) -> tuple[list[float], list[float]]:
    """
    Integrate the southward motion to derive latitude values.
    Longitude remains constant (no east/west displacement).
    """
    latitudes = [start_lat]
    longitudes = [start_lon]

    prev_time = samples[0][0]
    current_lat = start_lat

    for time_s, speed_kmh in samples[1:]:
        delta_t = max(0.0, time_s - prev_time)
        speed_m_s = speed_kmh / 3.6
        distance_m = speed_m_s * delta_t

        delta_lat_deg = (distance_m / EARTH_RADIUS_M) * (180.0 / pi)
        current_lat -= delta_lat_deg  # moving south decreases latitude

        latitudes.append(current_lat)
        longitudes.append(start_lon)

        prev_time = time_s

    return latitudes, longitudes


def format_gpx(
    samples: list[tuple[float, float]],
    latitudes: list[float],
    longitudes: list[float],
    args: argparse.Namespace,
) -> str:
    """Assemble the GPX document as a manually formatted string."""
    start_time = resolve_start_time(args, samples[0][0])
    start_seconds = samples[0][0]

    min_lat = min(latitudes)
    max_lat = max(latitudes)
    min_lon = min(longitudes)
    max_lon = max(longitudes)

    lines: list[str] = [
        "<?xml version='1.0' encoding='utf-8'?>",
        "<!-- Created with  GPS Logger for Android - ver. 3.2.3 -->",
        "<!-- Track 2 = XXXX TrackPoints + 0 Placemarks -->",
        "",
        "<!-- Track Statistics (based on Total Time | Time in Movement): -->",
        "<!--  Distance = XX km -->",
        "<!--  Duration = XX -->",
        "<!--  Altitude Gap = 0 m -->",
        "<!--  Max Speed = XX km/h -->",
        "<!--  Avg Speed = XX km/h -->",
        "<!--  Direction = S -->",
        "<!--  Activity = car -->",
        "<!--  Altitudes = Raw -->",
        "",
        '<gpx version="1.0" ',
        f'     creator="{args.creator}" ',
        '\t xmlns="http://www.topografix.com/GPX/1/0" ',
        '\t xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ',
        '\t xsi:schemaLocation="http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd">',
        f" <name>{args.track_name}</name>",
        f" <time>{start_time.strftime('%Y-%m-%dT%H:%M:%SZ')}</time>",
        f" <keywords>{args.activity}</keywords>",
        f' <bounds minlat="{min_lat:.8f}" minlon="{min_lon:.8f}" maxlat="{max_lat:.8f}" maxlon="{max_lon:.8f}" />',
        "",
        " <trk>",
        f"  <name>{args.track_name}</name>",
        "  <trkseg>",
    ]

    for (time_s, speed_kmh), lat, lon in zip(samples, latitudes, longitudes):
        timestamp = start_time + timedelta(seconds=(time_s - start_seconds))
        time_iso = timestamp.strftime("%Y-%m-%dT%H:%M:%SZ")
        speed_m_s = speed_kmh / 3.6
        trkpt_line = (
            f'    <trkpt lat="{lat:.12f}" lon="{lon:.12f}">'
            f"<ele>{args.elevation:.3f}</ele>"
            f"<time>{time_iso}</time>"
            f"<speed>{speed_m_s:.3f}</speed>"
        )
        if args.include_satellites:
            trkpt_line += "<sat>12</sat>"
        trkpt_line += "</trkpt>"
        lines.append(trkpt_line)

    lines.extend(
        [
            "  </trkseg>",
            " </trk>",
            "</gpx>",
        ]
    )

    return "\n".join(lines) + "\n"


def main() -> None:
    args = parse_args()
    input_path = Path(args.input_csv)
    samples = read_wltc_csv(input_path)
    latitudes, longitudes = integrate_positions(samples, args.latitude, args.longitude)
    gpx_content = format_gpx(samples, latitudes, longitudes, args)
    Path(args.output_gpx).write_text(gpx_content, encoding="utf-8")


if __name__ == "__main__":
    main()