import Foundation
import CoreLocation

struct GPXTrack {
    let name: String?
    let points: [GPXPoint]

    var coordinates: [CLLocationCoordinate2D] {
        points.map { CLLocationCoordinate2D(latitude: $0.lat, longitude: $0.lon) }
    }

    /// Total length in meters along the track using great-circle distance.
    var distanceMeters: Double {
        guard points.count > 1 else { return 0 }
        var sum: Double = 0
        for i in 1..<points.count {
            let a = CLLocation(latitude: points[i-1].lat, longitude: points[i-1].lon)
            let b = CLLocation(latitude: points[i].lat,   longitude: points[i].lon)
            sum += b.distance(from: a)
        }
        return sum
    }

    /// Duration in seconds derived from first/last <time> if available.
    var durationSeconds: TimeInterval? {
        guard let first = points.first?.time, let last = points.last?.time else { return nil }
        let delta = last.timeIntervalSince(first)
        return delta > 0 ? delta : nil
    }
}

struct GPXPoint {
    let lat: Double
    let lon: Double
    let ele: Double?
    let time: Date?
}

enum GPXParseError: Error, LocalizedError {
    case noTrack
    case invalid(String)

    var errorDescription: String? {
        switch self {
        case .noTrack:           return "Keine Trackpunkte gefunden."
        case .invalid(let why):  return "GPX ungültig: \(why)"
        }
    }
}

/// Minimaler SAX-Parser für GPX.  Liest <trkpt>, <wpt> und <rtept> als Punkte;
/// Track-Name aus <name> innerhalb des ersten <trk>.
final class GPXParser: NSObject, XMLParserDelegate {
    private var points: [GPXPoint] = []
    private var trackName: String?

    private var currentLat: Double?
    private var currentLon: Double?
    private var currentEle: Double?
    private var currentTime: Date?
    private var currentElement: String?
    private var inTrk = false
    private var trkNameCaptured = false
    private var textBuffer = ""

    private static let isoFormatter: ISO8601DateFormatter = {
        let f = ISO8601DateFormatter()
        f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        return f
    }()
    private static let isoFormatterNoFraction: ISO8601DateFormatter = {
        let f = ISO8601DateFormatter()
        f.formatOptions = [.withInternetDateTime]
        return f
    }()

    static func parse(data: Data) throws -> GPXTrack {
        let parser = GPXParser()
        let xml = XMLParser(data: data)
        xml.delegate = parser
        if !xml.parse() {
            if let err = xml.parserError {
                throw GPXParseError.invalid(err.localizedDescription)
            }
        }
        guard !parser.points.isEmpty else { throw GPXParseError.noTrack }
        return GPXTrack(name: parser.trackName, points: parser.points)
    }

    func parser(_ parser: XMLParser, didStartElement elementName: String,
                namespaceURI: String?, qualifiedName qName: String?,
                attributes attributeDict: [String : String] = [:]) {
        currentElement = elementName
        textBuffer = ""
        switch elementName {
        case "trk":
            inTrk = true
            trkNameCaptured = false
        case "trkpt", "wpt", "rtept":
            currentLat = attributeDict["lat"].flatMap { Double($0) }
            currentLon = attributeDict["lon"].flatMap { Double($0) }
            currentEle = nil
            currentTime = nil
        default:
            break
        }
    }

    func parser(_ parser: XMLParser, foundCharacters string: String) {
        textBuffer += string
    }

    func parser(_ parser: XMLParser, didEndElement elementName: String,
                namespaceURI: String?, qualifiedName qName: String?) {
        let text = textBuffer.trimmingCharacters(in: .whitespacesAndNewlines)
        switch elementName {
        case "ele":
            currentEle = Double(text)
        case "time":
            currentTime = Self.isoFormatter.date(from: text)
                ?? Self.isoFormatterNoFraction.date(from: text)
        case "name":
            if inTrk && !trkNameCaptured {
                trackName = text
                trkNameCaptured = true
            }
        case "trkpt", "wpt", "rtept":
            if let lat = currentLat, let lon = currentLon {
                points.append(GPXPoint(lat: lat, lon: lon, ele: currentEle, time: currentTime))
            }
        case "trk":
            inTrk = false
        default:
            break
        }
        textBuffer = ""
    }
}
