import Foundation
import CoreLocation
import BinaryCodable

private struct TrackLocation {

    let timestamp: TimeInterval

    let latitude: Double

    let longitude: Double

    let speed: Double?

    let speedAccuracy: Double?

    let course: Double?

    let courseAccuracy: Double?

    let elevation: Double

    let horizontalAccuracy: Double?

    let verticalAccuracy: Double?

    init(
        timestamp: TimeInterval,
        latitude: Double,
        longitude: Double,
        speed: Double,
        speedAccuracy: Double? = nil,
        course: Double,
        courseAccuracy: Double? = nil,
        elevation: Double,
        horizontalAccuracy: Double? = nil,
        verticalAccuracy: Double? = nil
    ) {
        self.timestamp = timestamp
        self.latitude = latitude
        self.longitude = longitude
        self.speed = speed
        self.speedAccuracy = speedAccuracy
        self.course = course
        self.courseAccuracy = courseAccuracy
        self.elevation = elevation
        self.horizontalAccuracy = horizontalAccuracy
        self.verticalAccuracy = verticalAccuracy
    }

    init(location: CLLocation) {
        self.timestamp = location.timestamp.timeIntervalSince1970
        self.elevation = location.altitude
        self.latitude = location.coordinate.latitude
        self.longitude = location.coordinate.longitude
        self.speed = location.speed
        self.speedAccuracy = location.speedAccuracy
        self.course = location.course
        self.courseAccuracy = location.courseAccuracy
        self.horizontalAccuracy = location.horizontalAccuracy
        self.verticalAccuracy = location.verticalAccuracy
    }

    var location: CLLocation {
        .init(
            coordinate: .init(
                latitude: latitude,
                longitude: longitude),
            altitude: elevation,
            horizontalAccuracy: horizontalAccuracy ?? -1,
            verticalAccuracy: verticalAccuracy ?? -1,
            course: course ?? -1,
            courseAccuracy: courseAccuracy ?? -1,
            speed: speed ?? -1,
            speedAccuracy: speedAccuracy ?? -1,
            timestamp: .init(timeIntervalSince1970: timestamp))
    }
}

extension TrackLocation: Codable {

    enum CodingKeys: Int, CodingKey {
        case timestamp = 1
        case latitude
        case longitude
        case speed
        case speedAccuracy
        case course
        case courseAccuracy
        case elevation
        case horizontalAccuracy
        case verticalAccuracy
    }
}

extension WorkoutData {

    struct Sample {

        /// The unix time
        let timestamp: TimeInterval

        let value: Double

        init(timestamp: TimeInterval, value: Double) {
            self.timestamp = timestamp
            self.value = value
        }

        var time: Date {
            .init(timeIntervalSince1970: timestamp)
        }
    }
}

extension WorkoutData.Sample: Codable {

    enum CodingKeys: Int, CodingKey {
        case timestamp = 1
        case value
    }
}

struct WorkoutData {

    let locations: [CLLocation]

    let heartRates: [Sample]

    /// The active energy in kcal
    let energy: [Sample]

    init(locations: [CLLocation], heartRates: [Sample], energy: [Sample]) {
        self.locations = locations
        self.heartRates = heartRates
        self.energy = energy
    }

    func encoded() throws -> Data {
        let encoder = BinaryEncoder()
        return try encoder.encode(self)
    }

    init(url: URL) throws {
        let data = try Data(contentsOf: url)
        try self.init(data: data)
    }

    init(data: Data) throws {
        let decoder = BinaryDecoder()
        self = try decoder.decode(WorkoutData.self, from: data)
    }

    /// The total active energy in kcal
    var totalEnergy: Double {
        energy.reduce(0) { $0 + $1.value }
    }

    /// The total distance of the track in meters
    var totalDistance: CLLocationDistance {
        locations.minimumTraveledDistance3D()
    }

    /// The total duration in seconds
    var totalDuration: TimeInterval {
        guard let start, let end else {
            return 0
        }
        return end.timeIntervalSince(start)
    }

    /// The total ascended altitude in meters
    var totalAscendedElevation: CLLocationDistance {
        locations.minimumAscendedAltitude(threshold: 15)
    }

    var overview: RouteOverview {
        .init(energy: totalEnergy,
              distance: totalDistance,
              duration: totalDuration,
              ascendedElevation: totalAscendedElevation,
              start: start,
              end: end)
    }

    var start: Date? {
        let starts: [Date?] = [
            locations.first?.timestamp,
            heartRates.first?.time,
            energy.first?.time]
        return starts.compactMap { $0 }.min()
    }

    var end: Date? {
        let ends: [Date?] = [locations.last?.timestamp, heartRates.last?.time, energy.last?.time]
        return ends.compactMap { $0 }.max()
    }

    func encode(to encoder: any Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(locations.map(TrackLocation.init), forKey: .locations)
        try container.encode(heartRates, forKey: .heartRates)
        try container.encode(energy, forKey: .energy)
    }

    init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.locations = try container.decode([TrackLocation].self, forKey: .locations).map { $0.location }
        self.heartRates = try container.decode([Sample].self, forKey: .heartRates)
        self.energy = try container.decode([Sample].self, forKey: .energy)
    }
}

extension WorkoutData: Codable {

    enum CodingKeys: Int, CodingKey {
        case locations = 1
        case heartRates
        case energy
    }
}
