//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import CoreLocation

protocol LocationManagerDelegate: AnyObject {
    func locationManager(_ manager: LocationManager, didUpdateLocation location: CLLocation)
}

/// Location accuracy
enum LocationManagerAccuracy {
    case full
    case reduced
}

/// LocationManager handles device geolocalization
class LocationManager: NSObject {
    
    // MARK: - Constants
    
    private enum Constants {
        static let distanceFiler: CLLocationDistance = 200.0
        static let waitForAuthorizationStatusDelay: TimeInterval = 0.5
    }
    
    // MARK: - Properties
    
    // MARK: Private
    
    private let locationManager: CLLocationManager
    private var authorizationHandler: LocationAuthorizationHandler?
    private var authorizationReturnedSinceRequestingAlways = false
    
    // MARK: Public
    
    class var isLocationEnabled: Bool {
        return CLLocationManager.locationServicesEnabled()
    }
    
    private(set) var accuracy: LocationManagerAccuracy
    
    var isUpdatingLocation = false
    
    var lastLocation: CLLocation?
    
    weak var delegate: LocationManagerDelegate?
    
    // MARK: - Setup
        
    init(accuracy: LocationManagerAccuracy, allowsBackgroundLocationUpdates: Bool) {

        self.accuracy = accuracy
        
        let locationManager = CLLocationManager()
        locationManager.distanceFilter = Constants.distanceFiler
        
        let desiredLocationAccuracy: CLLocationAccuracy
        
        switch accuracy {
        case .full:
            desiredLocationAccuracy = kCLLocationAccuracyNearestTenMeters
        case .reduced:
            desiredLocationAccuracy = kCLLocationAccuracyHundredMeters
        }
        
        locationManager.desiredAccuracy = desiredLocationAccuracy
        locationManager.allowsBackgroundLocationUpdates = allowsBackgroundLocationUpdates
        
        // Indicate to change status bar appearance when the app uses location services in the background
        locationManager.showsBackgroundLocationIndicator = true
        
        self.locationManager = locationManager
        
        super.init()
    }
    
    // MARK: - Public
    
    /// Start monitoring user location
    func start() {
        
        self.locationManager.delegate = self
        
        switch accuracy {
        case .full:
            self.locationManager.startUpdatingLocation()
        case .reduced:
            // Only listen to significant changes
            // roughly after 500 meters moves or every 5 minutes minimum
            // as mentioned in the Apple documentation https://developer.apple.com/documentation/corelocation/cllocationmanager/1423531-startmonitoringsignificantlocati
            self.locationManager.startMonitoringSignificantLocationChanges()
        }

        self.isUpdatingLocation = true
    }
    
    /// Stop monitoring user location
    func stop() {
        
        switch accuracy {
        case .full:
            self.locationManager.stopUpdatingLocation()
        case .reduced:
            self.locationManager.stopMonitoringSignificantLocationChanges()
        }

        self.locationManager.delegate = nil
        self.isUpdatingLocation = false
    }
    
    /// Request location authorization
    func requestAuthorization(_ handler: @escaping LocationAuthorizationHandler) {
        
        let status = self.locationManager.authorizationStatus
                
        switch status {
        case .notDetermined, .authorizedWhenInUse:
            // Try to resquest always authorization
            self.tryToRequestAlwaysAuthorization(handler: handler)
        default:
            handler(self.locationAuthorizationStatus(from: status))
        }
    }
    
    // MARK: - Private
    
    // Try to request always authorization and if `locationManagerDidChangeAuthorization` is not called within `Constants.waitForAuthorizationStatusDelay` call the input handler.
    // NOTE: As pointed in the Apple doc:
    // - Core Location limits calls to requestAlwaysAuthorization(). After your app calls this method, further calls have no effect.
    // - If the user responded to requestWhenInUseAuthorization() with Allow Once, then Core Location ignores further calls to requestAlwaysAuthorization() due to the temporary authorization.
    // See https://developer.apple.com/documentation/corelocation/cllocationmanager/1620551-requestalwaysauthorization?changes=_6_6
    private func tryToRequestAlwaysAuthorization(handler: @escaping LocationAuthorizationHandler) {
        self.authorizationHandler = handler
        self.authorizationReturnedSinceRequestingAlways = false
        self.locationManager.delegate = self
        self.locationManager.requestAlwaysAuthorization()
        
        Timer.scheduledTimer(withTimeInterval: Constants.waitForAuthorizationStatusDelay, repeats: false) { [weak self] _ in
            guard let self = self, !self.authorizationReturnedSinceRequestingAlways else {
                return
            }
            
            self.authorizationAlwaysRequestDidComplete(with: self.locationManager.authorizationStatus)
        }
    }
    
    private func locationAuthorizationStatus(from clLocationAuthorizationStatus: CLAuthorizationStatus) -> LocationAuthorizationStatus {
        
        let status: LocationAuthorizationStatus
        
        switch clLocationAuthorizationStatus {
        case .notDetermined:
            status = .unknown
        case .restricted, .denied:
            status = .denied
        case .authorizedAlways:
            status = .authorizedAlways
        case .authorizedWhenInUse:
            status = .authorizedInForeground
        @unknown default:
            status = .unknown
        }
        
        return status
    }
    private func authorizationAlwaysRequestDidComplete(with status: CLAuthorizationStatus) {
        guard let authorizationHandler = self.authorizationHandler else {
            return
        }

        authorizationHandler(self.locationAuthorizationStatus(from: status))
        self.authorizationHandler = nil
    }
}

// MARK: - CLLocationManagerDelegate
extension LocationManager: CLLocationManagerDelegate {

    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        
        let status = self.locationManager.authorizationStatus
        authorizationReturnedSinceRequestingAlways = true
        if status == .authorizedAlways {
            // LocationManager can call locationManagerDidChangeAuthorization multiple times.
            // For example it calls it at initialisation of LocationManager manager and we are also seeing it called
            // after requestAlwaysAuthorization but before the user has actually selected on option on the prompt.
            // Therefore we should only call `authorizationAlwaysRequestDidComplete` once on the success of authorizedAlways being granted.
            self.authorizationAlwaysRequestDidComplete(with: status)
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        
        guard let lastLocation = locations.last else {
            return
        }
        
        self.lastLocation = lastLocation
        
        self.delegate?.locationManager(self, didUpdateLocation: lastLocation)
    }
    
    func locationManagerDidResumeLocationUpdates(_ manager: CLLocationManager) {
        MXLog.debug("[LocationManager] Did resume location updates")
    }
    
    func locationManagerDidPauseLocationUpdates(_ manager: CLLocationManager) {
        MXLog.debug("[LocationManager] Did pause location updates")
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        MXLog.error("[LocationManager] Did failed", context: error)
    }
}

extension CLLocationManager {
    func requestAuthorizationIfNeeded() -> Bool {
        switch authorizationStatus {
        case .notDetermined:
            requestWhenInUseAuthorization()
            return false
        case .restricted, .denied:
            return false
        case .authorizedAlways, .authorizedWhenInUse, .authorized:
            return true
        @unknown default:
            return false
        }
    }
}
