import Foundation
import CoreLocation

struct StatisticsFileGenerationJob {

    let mapCoordinates: [CGPoint]

    let workout: WorkoutData

    let outputPath: URL

    let numberOfSamples: Int

    /// The minimum pace to allow, in min/km
    let minimumPace: Double

    init(mapCoordinates: [CGPoint], workout: WorkoutData, path: URL, numberOfSamples: Int, minimumPace: Double = 60) {
        self.mapCoordinates = mapCoordinates
        self.workout = workout
        self.outputPath = path
        self.numberOfSamples = numberOfSamples
        self.minimumPace = minimumPace
    }

    var locations: [CLLocation] {
        workout.locations
    }

    var heartRateSamples: [WorkoutData.Sample] {
        workout.heartRates
    }

    var energy: [WorkoutData.Sample] {
        workout.energy
    }

    var start: Date? { workout.start }

    var end: Date? { workout.end }

    var duration: TimeInterval {
        guard let start, let end else {
            return 0
        }
        return end.timeIntervalSince(start)
    }

    /**
     Points representing the distance (`y`, in meters) against duration (`x`, in seconds)
     */
    func distances() -> [Point] {
        guard var current = locations.first else { return [] }
        var distanceSoFar = 0.0

        let start = current.timestamp
        return locations.dropFirst().map { next in
            let time = next.duration(since: start)
            distanceSoFar += next.distance(from: current)
            current = next
            return Point(x: time, y: distanceSoFar)
        }
    }

    func run() -> Bool {
        let startLocation = locations.first!
        let start = startLocation.timestamp
        let end = locations.last!.timestamp
        let totalDuration = end.timeIntervalSince(start)
        let totalDistance = locations.totalDistance // in meter
        let minimumPace = minimumPace * 60 // Convert to s/km


        let rawDistances = distances() // m
        /// In meter
        let distances: [Point] = rawDistances.resample(numberOfSamples: numberOfSamples, mode: .instantaneous)
        let times = distances.map { $0.x }

        let xValues: [Double] = locations.enumerated().map { (index, location) in
            Point(x: location.duration(since: start),
                  y: mapCoordinates[index].x)
        }.resample(numberOfSamples: numberOfSamples, mode: .instantaneous)

        let yValues: [Double] = locations.enumerated().map { (index, location) in
            Point(x: location.duration(since: start),
                  y: mapCoordinates[index].y)
        }.resample(numberOfSamples: numberOfSamples, mode: .instantaneous)

        let elevations: [Double] = locations.resample(
            numberOfSamples: numberOfSamples,
            minX: 0,
            maxX: totalDuration,
            x: { $0.duration(since: start) },
            y: { $0.altitude },
            mode: .instantaneous)

        let speeds: [Double] = rawDistances.resample(
            numberOfSamples: numberOfSamples,
            minX: 0,
            maxX: totalDuration,
            mode: .cumulative)
            .map { $0 * 3.6 } // Convert from m/s to km/h
            .medianFiltered(windowSize: 15)

        // Speed is km/h, pace is s/km
        let paces: [Double] = speeds.map {
            guard $0 > 0 else {
                return minimumPace
            }
            let converted = 3600 / $0 // s/km
            return min(minimumPace, converted)
        }

        let heartRates: [Double] = heartRateSamples.resample(
            numberOfSamples: numberOfSamples,
            minX: 0,
            maxX: totalDuration,
            x: { $0.time.timeIntervalSince(start) },
            y: { $0.value },
            mode: .instantaneous)
            .map { $0 * 60 } // from hz to bpm
            .medianFiltered(windowSize: 15)

        let energies: [Double] = energy.resample(
            numberOfSamples: numberOfSamples,
            minX: 0,
            maxX: totalDuration,
            x: { $0.time.timeIntervalSince(start) },
            y: { $0.value },
            mode: .instantaneous)
            .map { $0 * 60 } // from kcal/s to kcal/min
            .medianFiltered(windowSize: 10)

        let series = RouteSeries(
            elevation: elevations.fittingAxisLimits(minLimit: 0),
            speed: speeds.fittingAxisLimits(minLimit: 0),
            pace: paces.fittingTimeAxisLimits(maxLimit: minimumPace),
            hr: heartRates.fittingAxisLimits(minLimit: 0),
            energy: energies.fittingAxisLimits(minLimit: 0))

        let ranges = DataRanges(
            duration: .init(min: 0, max: totalDuration),
            time: .init(min: start.timeIntervalSince1970,
                        max: end.timeIntervalSince1970),
            distance: .init(min: 0, max: totalDistance / 1000)) // km

        let samples: [RouteSample] = (0..<numberOfSamples).map { index -> RouteSample in
            RouteSample(
                x: xValues[index],
                y: yValues[index],
                time: times[index] / totalDuration,
                distance: distances[index].y / totalDistance,
                elevation: series.elevation.scale(elevations[index]),
                speed: series.speed.scale(speeds[index]),
                pace: series.pace.scale(paces[index]),
                hr: series.hr.scale(heartRates[index]),
                energy: series.energy!.scale(energies[index]))
        }

        let result = RouteData(
            series: series,
            ranges: ranges,
            samples: samples)

        let data = result.encoded()

        do {
            try data.write(to: outputPath)
            return true
        } catch {
            print("Failed to write file \(outputPath.path()): \(error.localizedDescription)")
            return false
        }
    }
}
