import Foundation
import RxSwift
import RxCocoa
import CoreLocation

class RxLocationManagerDelegateProxy: DelegateProxy<CLLocationManager, CLLocationManagerDelegate>, DelegateProxyType, CLLocationManagerDelegate {
    
    private let generalErrors: [CLError.Code] = [.locationUnknown, .denied, .network, .headingFailure, .rangingUnavailable, .rangingFailure]
    private let geocodingErrors: [CLError.Code] = [.geocodeCanceled, .geocodeFoundNoResult, .geocodeFoundPartialResult]
    
    private(set) var authSubject = PublishSubject<CLAuthorizationStatus>()
    private(set) var locationSubject = PublishSubject<CLLocation>()

    init(locationManager: ParentObject) {
        super.init(parentObject: locationManager, delegateProxy: RxLocationManagerDelegateProxy.self)
    }
    
    deinit {
        print("deinit")
    }
    
    // MARK: - DelegateProxyType
    
    static func registerKnownImplementations() {
        self.register { RxLocationManagerDelegateProxy(locationManager: $0) }
    }
    
    static func currentDelegate(for object: CLLocationManager) -> CLLocationManagerDelegate? {
        return object.delegate
    }

    static func setCurrentDelegate(_ delegate: CLLocationManagerDelegate?, to object: CLLocationManager) {
        object.delegate = delegate
    }
    
    // MARK: - CLLocationManagerDelegate
    
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        self.authSubject.onNext(status)
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        if let location = locations.first {
            self.locationSubject.onNext(location)
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        guard let err = error as? CLError else { return }
        
        if self.generalErrors.contains(err.code) {
            // Pass general errors to all existing subjects
            self.authSubject.onError(error)
            self.locationSubject.onError(error)
        } else if self.geocodingErrors.contains(err.code) {
            // TODO: pass error to geocoding subject
        } else {
            print("Unexpected CoreLocation error: \(error)")
        }
    }
    
    func getNewLocationSubject() -> PublishSubject<CLLocation> {
        self.locationSubject = PublishSubject<CLLocation>()
        return self.locationSubject
    }
}

class LocationManager {
    private static let manager: CLLocationManager = {
        let mgr = CLLocationManager()
        mgr.desiredAccuracy = kCLLocationAccuracyBest
        return mgr
    }()
    private static let bag = DisposeBag()
    private static var eventObservable: Single<VehicleEvent>?
    private(set) static var lastEvent: VehicleEvent?
    
    private static func checkPermissions() -> Single<Void> {
        return Single<Void>.create { observer in
            switch CLLocationManager.authorizationStatus() {
            case .authorizedWhenInUse, .authorizedAlways:
                observer(.success(()))
                break
            case .notDetermined:
                self.manager.requestAlwaysAuthorization()
                let proxy = RxLocationManagerDelegateProxy.proxy(for: self.manager)
                _ = proxy.authSubject.filter{ $0 != .notDetermined }.first().subscribe(onSuccess: { result in
                    if let status = result, [.authorizedWhenInUse, .authorizedAlways].contains(status) {
                        observer(.success(()))
                    } else {
                        observer(.error(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Location permission error"])))
                    }
                }, onError: { observer(.error($0)) })
            case .denied:
                observer(.error(CLError(.denied)))
                break
            default:
                observer(.error(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Location permission error"])))
                break
            }
            
            return Disposables.create()
        }
    }
    
    private static func requestLocation() -> Single<VehicleEvent> {
        let proxy = RxLocationManagerDelegateProxy.proxy(for: self.manager)
        let single = proxy.getNewLocationSubject().take(1).asSingle().map { location -> VehicleEvent in
            let event = VehicleEvent(lat: location.coordinate.latitude, lon: location.coordinate.longitude, speed: location.speed, dir: location.course)
            self.lastEvent = event
            return event
        }
        self.manager.requestLocation()
        return single
    }
    
    static func requestCurrentLocation() -> Single<VehicleEvent> {
        if let result = self.eventObservable {
            return result
        } else {
            self.eventObservable = self.checkPermissions().flatMap(self.requestLocation).do(onError: { _ in
                self.eventObservable = nil
            }, onDispose: {
                self.eventObservable = nil
                self.manager.stopUpdatingLocation()
            })
            return self.eventObservable!
        }
    }
    
    static func locationRequestInProgress() -> Bool {
        return self.eventObservable != nil
    }
    
    static func getAddressForLocation(latitude: Double, longitude: Double) -> Single<String> {
        return Single.create { observer in
            let geocoder = CLGeocoder()
            let location = CLLocation(latitude: latitude, longitude: longitude)
            geocoder.reverseGeocodeLocation(location) { placemarks, error in
                if let error = error {
                    observer(.error(error))
                } else if let placemark = placemarks?.first, let name = placemark.name {
                    observer(.success(name))
                } else {
                    observer(.error(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Reverse geolocation error"])))
                }
            }
            
            return Disposables.create {
                geocoder.cancelGeocode()
            }
        }
    }
    
    static func resetLastEvent() {
        self.lastEvent = nil
    }
}
