//
//  NavvoCarPlay.swift
//  Navvo Maps SDK for iOS
//
//  CarPlay support: `NavvoCarPlayController` attaches a live `NavvoMapView`
//  to the CarPlay window and drives a `CPMapTemplate`-based navigation
//  skeleton (trip preview → navigation session → maneuver/ETA updates);
//  `NavvoCarPlaySceneDelegate` is a drop-in scene delegate wired to it.
//
//  Everything is compile-guarded so the package builds unchanged for targets
//  without CarPlay (Mac Catalyst, app extensions, plain iPhone apps that
//  never request the entitlement). Shipping a CarPlay app additionally
//  requires Apple's `com.apple.developer.carplay-maps` entitlement and a
//  CarPlay scene declaration in Info.plist — see README → "CarPlay".
//
//  MapKit types (`MKMapItem`, `MKPlacemark`) appear below only because they
//  are part of Apple's own CarPlay contract: `CPTrip` requires them.
//

#if canImport(CarPlay) && canImport(UIKit) && canImport(MapLibre)
import CarPlay
import MapKit
import UIKit

// MARK: - Delegate

/// Event hooks for ``NavvoCarPlayController``. All methods have default
/// no-op implementations — adopt only what you need.
@available(iOS 13.0, *)
public protocol NavvoCarPlayControllerDelegate: AnyObject {
    /// The car map is on screen and ready (fires after the template and map
    /// view are installed in the CarPlay window).
    func carPlayController(
        _ controller: NavvoCarPlayController, didShow mapView: NavvoMapView
    )

    /// The driver picked a route in a trip preview and wants to go.
    /// Typically: fetch the route (`NavvoServices.plan`), draw it, then call
    /// ``NavvoCarPlayController/startNavigation(for:)``.
    func carPlayController(
        _ controller: NavvoCarPlayController, didSelect trip: CPTrip, routeChoice: CPRouteChoice
    )

    /// The driver cancelled navigation from the CarPlay UI.
    func carPlayControllerDidCancelNavigation(_ controller: NavvoCarPlayController)
}

@available(iOS 13.0, *)
public extension NavvoCarPlayControllerDelegate {
    func carPlayController(
        _ controller: NavvoCarPlayController, didShow mapView: NavvoMapView
    ) {}
    func carPlayController(
        _ controller: NavvoCarPlayController, didSelect trip: CPTrip, routeChoice: CPRouteChoice
    ) {}
    func carPlayControllerDidCancelNavigation(_ controller: NavvoCarPlayController) {}
}

// MARK: - NavvoCarPlayController

/// Puts the Navvo map on the CarPlay screen and exposes a template-safe
/// navigation skeleton.
///
/// The controller owns the full CarPlay stack for one scene:
/// - a ``NavvoMapView`` filling the `CPWindow` (the drawable layer — all the
///   usual SDK camera/marker/overlay APIs work on it unchanged),
/// - a `CPMapTemplate` as the root template (zoom + pan buttons installed;
///   pan gestures and pan buttons move the Navvo camera),
/// - trip preview / `CPNavigationSession` helpers for turn-by-turn chrome
///   (maneuver card + ETA), fed by your own guidance logic.
///
/// ```swift
/// let controller = NavvoCarPlayController()
/// controller.connect(interfaceController: interfaceController, to: window)
/// controller.mapView?.setCamera(
///     CameraPosition(target: LatLng(31.95, 35.91), zoom: 13), animated: false
/// )
///
/// // Navigation skeleton:
/// let trip = controller.makeTrip(
///     from: LatLng(31.95, 35.91), originName: "Start",
///     to: LatLng(31.99, 35.86), destinationName: "Sweifieh"
/// )
/// controller.previewTrips([trip])
/// // → delegate `didSelect trip` → controller.startNavigation(for: trip)
/// controller.updateManeuver(instruction: "Turn right onto Rainbow Street",
///                           distanceRemainingMeters: 320, timeRemainingSeconds: 45)
/// controller.endNavigation(arrived: true)
/// ```
@available(iOS 13.0, *)
public final class NavvoCarPlayController: NSObject {

    /// Delegate for connection/trip events.
    public weak var delegate: NavvoCarPlayControllerDelegate?

    /// The map view shown in the CarPlay window (`nil` before ``connect(interfaceController:to:)``).
    public private(set) var mapView: NavvoMapView?

    /// The root map template (`nil` before connect).
    public private(set) var mapTemplate: CPMapTemplate?

    /// The CarPlay interface controller (`nil` before connect).
    public private(set) var interfaceController: CPInterfaceController?

    /// The active turn-by-turn session (`nil` while not navigating).
    public private(set) var navigationSession: CPNavigationSession?

    private weak var window: CPWindow?
    private var options: NavvoMapView.Options
    private var currentManeuver: CPManeuver?
    private var lastPanTranslation: CGPoint = .zero

    /// Create a controller. `options` configure the car map (style, camera,
    /// terrain) exactly like a phone ``NavvoMapView``.
    public init(options: NavvoMapView.Options = NavvoMapView.Options()) {
        self.options = options
        super.init()
    }

    // MARK: Connect / disconnect

    /// Install the map view and root template into the CarPlay scene. Call
    /// from `templateApplicationScene(_:didConnect:to:)` (or use
    /// ``NavvoCarPlaySceneDelegate``, which does this for you).
    public func connect(interfaceController: CPInterfaceController, to window: CPWindow) {
        let host = NavvoCarWindowRootViewController(options: options)
        window.rootViewController = host
        self.window = window
        self.interfaceController = interfaceController
        self.mapView = host.mapView

        let template = CPMapTemplate()
        template.mapDelegate = self
        template.automaticallyHidesNavigationBar = true
        template.mapButtons = [makeZoomInButton(), makeZoomOutButton(), makePanButton()]
        self.mapTemplate = template

        if #available(iOS 14.0, *) {
            interfaceController.setRootTemplate(template, animated: false, completion: nil)
        } else {
            interfaceController.setRootTemplate(template, animated: false)
        }

        delegate?.carPlayController(self, didShow: host.mapView)
    }

    /// Tear everything down (scene disconnect). Safe to call repeatedly.
    public func disconnect() {
        navigationSession?.cancelTrip()
        navigationSession = nil
        currentManeuver = nil
        mapTemplate?.mapDelegate = nil
        mapTemplate = nil
        interfaceController = nil
        window?.rootViewController = nil
        window = nil
        mapView = nil
    }

    // MARK: Camera helpers (used by the map buttons; also callable directly)

    /// Zoom the car map in one level.
    public func zoomIn() { mapView?.zoomIn() }

    /// Zoom the car map out one level.
    public func zoomOut() { mapView?.zoomOut() }

    /// Show the CarPlay panning interface (chevrons / pan gestures).
    public func beginPanning(animated: Bool = true) {
        mapTemplate?.showPanningInterface(animated: animated)
    }

    /// Hide the CarPlay panning interface.
    public func endPanning(animated: Bool = true) {
        mapTemplate?.dismissPanningInterface(animated: animated)
    }

    // MARK: Trip / navigation skeleton

    /// Build a `CPTrip` between two Navvo points. `MKMapItem` is required by
    /// Apple's CarPlay API; the coordinates are yours and no external service
    /// is consulted.
    public func makeTrip(
        from origin: LatLng,
        originName: String,
        to destination: LatLng,
        destinationName: String,
        routeSummary: String = "Route",
        routeSelectionSummary: String = ""
    ) -> CPTrip {
        let originItem = MKMapItem(placemark: MKPlacemark(coordinate: origin.clCoordinate))
        originItem.name = originName
        let destinationItem = MKMapItem(
            placemark: MKPlacemark(coordinate: destination.clCoordinate)
        )
        destinationItem.name = destinationName
        let choice = CPRouteChoice(
            summaryVariants: [routeSummary],
            additionalInformationVariants: [],
            selectionSummaryVariants: [routeSelectionSummary]
        )
        return CPTrip(origin: originItem, destination: destinationItem, routeChoices: [choice])
    }

    /// Show the trip preview panel. The driver's selection arrives at
    /// ``NavvoCarPlayControllerDelegate/carPlayController(_:didSelect:routeChoice:)``.
    public func previewTrips(_ trips: [CPTrip]) {
        mapTemplate?.showTripPreviews(trips, textConfiguration: nil)
    }

    /// Dismiss the trip preview panel.
    public func hideTripPreviews() {
        mapTemplate?.hideTripPreviews()
    }

    /// Begin turn-by-turn chrome for `trip`. Returns the session (also kept
    /// on ``navigationSession``). Drive the maneuver card with
    /// ``updateManeuver(instruction:distanceRemainingMeters:timeRemainingSeconds:symbol:)``.
    @discardableResult
    public func startNavigation(for trip: CPTrip) -> CPNavigationSession? {
        guard let mapTemplate else { return nil }
        mapTemplate.hideTripPreviews()
        let session = mapTemplate.startNavigationSession(for: trip)
        navigationSession = session
        return session
    }

    /// Show/refresh the current maneuver (instruction card) and its
    /// distance/time estimates.
    public func updateManeuver(
        instruction: String,
        distanceRemainingMeters: Double,
        timeRemainingSeconds: TimeInterval,
        symbol: UIImage? = nil
    ) {
        guard let session = navigationSession else { return }
        let estimates = CPTravelEstimates(
            distanceRemaining: Measurement(value: distanceRemainingMeters, unit: UnitLength.meters),
            timeRemaining: timeRemainingSeconds
        )
        let maneuver: CPManeuver
        if let existing = currentManeuver {
            maneuver = existing
        } else {
            maneuver = CPManeuver()
            currentManeuver = maneuver
        }
        maneuver.instructionVariants = [instruction]
        if let symbol { maneuver.symbolImage = symbol }
        maneuver.initialTravelEstimates = estimates
        session.upcomingManeuvers = [maneuver]
        session.updateEstimates(estimates, for: maneuver)
    }

    /// Advance to a new maneuver (clears the reused card so the next
    /// ``updateManeuver(instruction:distanceRemainingMeters:timeRemainingSeconds:symbol:)``
    /// starts fresh).
    public func advanceManeuver() {
        currentManeuver = nil
    }

    /// Refresh the destination ETA row shown by the template.
    public func updateTripEstimates(
        for trip: CPTrip,
        distanceRemainingMeters: Double,
        timeRemainingSeconds: TimeInterval,
        color: CPTimeRemainingColor = .default
    ) {
        let estimates = CPTravelEstimates(
            distanceRemaining: Measurement(value: distanceRemainingMeters, unit: UnitLength.meters),
            timeRemaining: timeRemainingSeconds
        )
        mapTemplate?.update(estimates, for: trip, with: color)
    }

    /// End turn-by-turn: `arrived: true` finishes the trip, `false` cancels.
    public func endNavigation(arrived: Bool) {
        if arrived {
            navigationSession?.finishTrip()
        } else {
            navigationSession?.cancelTrip()
        }
        navigationSession = nil
        currentManeuver = nil
    }

    // MARK: Map buttons

    private func makeZoomInButton() -> CPMapButton {
        let button = CPMapButton { [weak self] _ in self?.zoomIn() }
        button.image = UIImage(systemName: "plus.magnifyingglass")
        return button
    }

    private func makeZoomOutButton() -> CPMapButton {
        let button = CPMapButton { [weak self] _ in self?.zoomOut() }
        button.image = UIImage(systemName: "minus.magnifyingglass")
        return button
    }

    private func makePanButton() -> CPMapButton {
        let button = CPMapButton { [weak self] _ in self?.beginPanning() }
        button.image = UIImage(systemName: "arrow.up.and.down.and.arrow.left.and.right")
        return button
    }

    // MARK: Pan math

    /// Move the camera by a screen-space content translation (pixels): a
    /// positive `y` reveals north, a positive `x` reveals west — matching
    /// drag-gesture semantics.
    fileprivate func pan(byScreenTranslation translation: CGPoint) {
        guard
            let mapView,
            mapView.bounds.width > 0, mapView.bounds.height > 0
        else { return }
        let visible = mapView.visibleBounds
        let latSpan = visible.northeast.latitude - visible.southwest.latitude
        let lngSpan = visible.northeast.longitude - visible.southwest.longitude

        var camera = mapView.camera
        let latitude = camera.target.latitude
            + Double(translation.y / mapView.bounds.height) * latSpan
        let longitude = camera.target.longitude
            - Double(translation.x / mapView.bounds.width) * lngSpan
        camera.target = LatLng(
            min(85.0, max(-85.0, latitude)),
            min(180.0, max(-180.0, longitude))
        )
        mapView.setCamera(camera, animated: false)
    }
}

// MARK: - CPMapTemplateDelegate

@available(iOS 13.0, *)
extension NavvoCarPlayController: CPMapTemplateDelegate {

    /// Pan buttons (chevrons) — step a quarter viewport per press.
    public func mapTemplate(
        _ mapTemplate: CPMapTemplate, panWith direction: CPMapTemplate.PanDirection
    ) {
        guard let mapView else { return }
        let stepX = mapView.bounds.width * 0.25
        let stepY = mapView.bounds.height * 0.25
        var translation = CGPoint.zero
        if direction.contains(.up) { translation.y += stepY }
        if direction.contains(.down) { translation.y -= stepY }
        if direction.contains(.left) { translation.x += stepX }
        if direction.contains(.right) { translation.x -= stepX }
        pan(byScreenTranslation: translation)
    }

    /// Touch-pan begins — reset the incremental translation tracker.
    public func mapTemplateDidBeginPanGesture(_ mapTemplate: CPMapTemplate) {
        lastPanTranslation = .zero
    }

    /// Touch-pan on touchscreen head units. The host reports the cumulative
    /// gesture translation, so apply only the increment since the last tick.
    public func mapTemplate(
        _ mapTemplate: CPMapTemplate,
        didUpdatePanGestureWithTranslation translation: CGPoint,
        velocity: CGPoint
    ) {
        let delta = CGPoint(
            x: translation.x - lastPanTranslation.x,
            y: translation.y - lastPanTranslation.y
        )
        lastPanTranslation = translation
        pan(byScreenTranslation: delta)
    }

    /// Touch-pan ended.
    public func mapTemplate(
        _ mapTemplate: CPMapTemplate, didEndPanGestureWithVelocity velocity: CGPoint
    ) {
        lastPanTranslation = .zero
    }

    /// The driver tapped "Go" on a previewed trip — hand off to the app.
    public func mapTemplate(
        _ mapTemplate: CPMapTemplate, startedTrip trip: CPTrip, using routeChoice: CPRouteChoice
    ) {
        delegate?.carPlayController(self, didSelect: trip, routeChoice: routeChoice)
    }

    /// The driver cancelled navigation from the CarPlay UI.
    public func mapTemplateDidCancelNavigation(_ mapTemplate: CPMapTemplate) {
        endNavigation(arrived: false)
        delegate?.carPlayControllerDidCancelNavigation(self)
    }
}

// MARK: - Scene delegate

/// A drop-in CarPlay scene delegate.
///
/// Reference it from the CarPlay scene configuration in Info.plist (or
/// subclass it and reference the subclass) — see README → "CarPlay" for the
/// scene manifest and entitlement. Override the three `open` hooks to pick
/// map options and react to connect/disconnect:
///
/// ```swift
/// final class CarPlaySceneDelegate: NavvoCarPlaySceneDelegate {
///     override func carPlayMapOptions() -> NavvoMapView.Options {
///         var options = NavvoMapView.Options()
///         options.style = .dark
///         return options
///     }
///
///     override func carPlayDidConnect(_ controller: NavvoCarPlayController) {
///         controller.mapView?.setCamera(
///             CameraPosition(target: LatLng(31.95, 35.91), zoom: 13), animated: false
///         )
///     }
/// }
/// ```
@available(iOS 13.0, *)
open class NavvoCarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate {

    /// The controller for the connected scene (`nil` while disconnected).
    public private(set) var carPlayController: NavvoCarPlayController?

    /// Override to configure the car map (style, camera, terrain).
    open func carPlayMapOptions() -> NavvoMapView.Options {
        NavvoMapView.Options()
    }

    /// Override for post-connect setup (camera, markers, overlays, delegate).
    open func carPlayDidConnect(_ controller: NavvoCarPlayController) {}

    /// Override for teardown just before the scene disconnects.
    open func carPlayWillDisconnect(_ controller: NavvoCarPlayController) {}

    // MARK: CPTemplateApplicationSceneDelegate

    public func templateApplicationScene(
        _ templateApplicationScene: CPTemplateApplicationScene,
        didConnect interfaceController: CPInterfaceController,
        to window: CPWindow
    ) {
        let controller = NavvoCarPlayController(options: carPlayMapOptions())
        carPlayController = controller
        controller.connect(interfaceController: interfaceController, to: window)
        carPlayDidConnect(controller)
    }

    public func templateApplicationScene(
        _ templateApplicationScene: CPTemplateApplicationScene,
        didDisconnect interfaceController: CPInterfaceController,
        from window: CPWindow
    ) {
        if let controller = carPlayController {
            carPlayWillDisconnect(controller)
            controller.disconnect()
        }
        carPlayController = nil
    }
}

// MARK: - Window root

/// Internal host view controller: a ``NavvoMapView`` filling the CarPlay
/// window.
@available(iOS 13.0, *)
final class NavvoCarWindowRootViewController: UIViewController {

    let mapView: NavvoMapView

    init(options: NavvoMapView.Options) {
        self.mapView = NavvoMapView(frame: .zero, options: options)
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        self.mapView = NavvoMapView(frame: .zero)
        super.init(coder: coder)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        mapView.frame = view.bounds
        mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(mapView)
    }
}
#endif
