//
//  LocationProvider.swift
//  Go Map!!
//
//  Created by Bryce Cogswell on 1/13/26.
//  Copyright © 2026 Bryce Cogswell. All rights reserved.
//

import CoreLocation
import UIKit

final class LocationProvider: NSObject, CLLocationManagerDelegate {

	static let shared = LocationProvider()
	private let locationManager = CLLocationManager()

	var ignoreInitialStatusUpdate = false

	private(set) var currentLocation: CLLocation? {
		didSet {
			if let currentLocation {
				onChangeLocation.notify(currentLocation)
			}
		}
	}

	struct Heading {
		var heading: Double // radians, north up
		var accuracy: Double // radians
	}

	private(set) var currentHeading: Heading? {
		didSet {
			if let currentHeading {
				onChangeHeading.notify(currentHeading)
			}
		}
	}

	let onChangeHeading = NotificationService<Heading>()
	let onChangeLocation = NotificationService<CLLocation>()

	var allowsBackgroundLocationUpdates: Bool {
		get {
			locationManager.allowsBackgroundLocationUpdates
		}
		set {
			locationManager.allowsBackgroundLocationUpdates = newValue
		}
	}

	override init() {
		super.init()

		ignoreInitialStatusUpdate = true // flag that we're going to receive an extra notification from CL
		locationManager.delegate = self
		locationManager.pausesLocationUpdatesAutomatically = false
		locationManager.allowsBackgroundLocationUpdates =
			AppState.shared.gpxTracks.recordTracksInBackground && AppDelegate.shared.mainView.settings.displayGpxTracks
		if #available(iOS 11.0, *) {
			locationManager.showsBackgroundLocationIndicator = true
		}
		locationManager.activityType = .other

		locationManager.delegate = self

		NotificationCenter.default.addObserver(
			self,
			selector: #selector(backgroundCollectionSettingChanged(_:)),
			name: NSNotification.Name("CollectGpxTracksInBackgroundChanged"),
			object: nil)
	}

	@objc func backgroundCollectionSettingChanged(_ notification: Notification) {
		if AppState.shared.gpxTracks.recordTracksInBackground,
		   AppDelegate.shared.mainView.settings.displayGpxTracks
		{
			allowsBackgroundLocationUpdates = true
			locationManager.requestAlwaysAuthorization()
		} else {
			allowsBackgroundLocationUpdates = false
		}
	}

	func start() {
		let mainView = AppDelegate.shared.mainView!

		let status: CLAuthorizationStatus
		if #available(iOS 14.0, *) {
			status = locationManager.authorizationStatus
		} else {
			status = CLLocationManager.authorizationStatus()
		}
		switch status {
		case .notDetermined:
			// we haven't asked user before, so have iOS pop up the question
			locationManager.requestWhenInUseAuthorization()
			mainView.gpsState = .NONE
			return
		case .restricted, .denied:
			// user denied permission previously, so ask if they want to open Settings
			AppDelegate.askUserToAllowLocationAccess()
			mainView.gpsState = .NONE
			return
		case .authorizedAlways, .authorizedWhenInUse:
			break
		default:
			break
		}

		locationManager.startUpdatingLocation()
		locationManager.startUpdatingHeading()
	}

	func stop() {
		locationManager.stopUpdatingLocation()
		locationManager.stopUpdatingHeading()
		currentLocation = nil
	}

	func updateToLocation(_ newLocation: CLLocation) {
		guard AppDelegate.shared.mainView.gpsState != .NONE else {
			// sometimes we get a notification after turning off notifications
			DLog("discard location notification")
			return
		}

		guard newLocation.timestamp >= Date(timeIntervalSinceNow: -10.0) else {
			// its old data
			DLog("discard old GPS data: \(newLocation.timestamp), \(Date())\n")
			return
		}

		if let currentLocation,
		   GreatCircleDistance(newLocation.coordinate, currentLocation.coordinate) < 0.1,
		   abs(newLocation.horizontalAccuracy - currentLocation.horizontalAccuracy) < 1.0
		{
			// didn't move far, and the accuracy didn't change either, so ignore it
			return
		}

		self.currentLocation = newLocation
	}

	private static func headingAdjustedForInterfaceOrientation(_ clHeading: CLHeading) -> Double {
		var heading = clHeading.trueHeading * .pi / 180
		if let scene = UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }).first {
			switch scene.interfaceOrientation {
			case .portraitUpsideDown:
				heading += .pi
			case .landscapeLeft:
				heading -= .pi / 2
			case .landscapeRight:
				heading += .pi / 2
			case .portrait:
				fallthrough
			default:
				break
			}
		}
		return heading
	}

	// MARK: CLLocationManagerDelegate

	func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
		self.currentHeading = Heading(heading: Self.headingAdjustedForInterfaceOrientation(newHeading),
		                              accuracy: newHeading.headingAccuracy * .pi / 180)
	}

	func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
		for location in locations {
			updateToLocation(location)
		}
	}

	func locationManagerDidPauseLocationUpdates(_ manager: CLLocationManager) {
		print("GPS paused by iOS\n")
	}

	func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
		let mainView = AppDelegate.shared.mainView!

		var error = error
		if (error as? CLError)?.code == CLError.Code.denied {
			mainView.gpsState = .NONE

			var text = String.localizedStringWithFormat(
				NSLocalizedString(
					"Ensure Location Services is enabled and you have granted this application access.\n\nError: %@",
					comment: ""),
				error.localizedDescription)
			text = NSLocalizedString("The current location cannot be determined: ", comment: "") + text
			error = NSError(domain: "Location", code: 100, userInfo: [
				NSLocalizedDescriptionKey: text
			])
			MessageDisplay.shared.presentError(title: nil, error: error, flash: false)
		} else {
			// driving through a tunnel or something
			let text = NSLocalizedString("Location unavailable", comment: "")
			error = NSError(domain: "Location", code: 100, userInfo: [
				NSLocalizedDescriptionKey: text
			])
			MessageDisplay.shared.presentError(title: nil, error: error, flash: true)
		}
	}

	func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
		if ignoreInitialStatusUpdate {
			// filter out extraneous notification we get when initializing CL
			ignoreInitialStatusUpdate = false
			return
		}

		switch status {
		case .authorizedAlways, .authorizedWhenInUse:
			AppDelegate.shared.mainView.gpsState = .LOCATION
		case .notDetermined, .restricted, .denied:
			fallthrough
		default:
			AppDelegate.shared.mainView.gpsState = .NONE
		}
	}
}
