//
//  NavvoMapSwiftUI.swift
//  Navvo Maps SDK for iOS
//
//  The SwiftUI surface: `NavvoMap`, a UIViewRepresentable wrapper around
//  `NavvoMapView` with a two-way camera binding, marker diffing, and
//  `.onNavvo*` event modifiers.
//

#if canImport(SwiftUI) && canImport(UIKit) && canImport(MapLibre)
import SwiftUI

/// A SwiftUI map view rendering the Navvo basemap.
///
/// ```swift
/// struct ContentView: View {
///     @State private var camera = CameraPosition(target: LatLng(31.95, 35.91), zoom: 12)
///
///     var body: some View {
///         NavvoMap(style: .standard, camera: $camera, markers: [
///             NavvoMarker(position: LatLng(31.9539, 35.9106), title: "Downtown Amman"),
///         ])
///         .onNavvoTap { print("tapped \($0)") }
///         .ignoresSafeArea()
///     }
/// }
/// ```
@available(iOS 13.0, *)
public struct NavvoMap: UIViewRepresentable {

    private var style: NavvoMapStyle
    private var terrain: Bool
    private var initialCamera: CameraPosition
    private var cameraBinding: Binding<CameraPosition>?
    private var markers: [NavvoMarker]
    private var showsUserLocation: Bool = false
    private var tapHandler: ((LatLng) -> Void)?
    private var longPressHandler: ((LatLng) -> Void)?
    private var markerTapHandler: ((NavvoMarker) -> Void)?
    private var styleLoadedHandler: (() -> Void)?
    private var cameraIdleHandler: ((CameraPosition) -> Void)?

    /// Create a map with a fixed initial camera.
    public init(
        style: NavvoMapStyle = .standard,
        camera: CameraPosition = CameraPosition(target: LatLng(31.95, 35.91), zoom: 12),
        terrain: Bool = false,
        markers: [NavvoMarker] = []
    ) {
        self.style = style
        self.terrain = terrain
        self.initialCamera = camera
        self.cameraBinding = nil
        self.markers = markers
    }

    /// Create a map with a two-way camera binding (written on camera idle).
    public init(
        style: NavvoMapStyle = .standard,
        camera: Binding<CameraPosition>,
        terrain: Bool = false,
        markers: [NavvoMarker] = []
    ) {
        self.style = style
        self.terrain = terrain
        self.initialCamera = camera.wrappedValue
        self.cameraBinding = camera
        self.markers = markers
    }

    // MARK: Modifiers

    /// Handle basemap taps.
    public func onNavvoTap(_ handler: @escaping (LatLng) -> Void) -> NavvoMap {
        var copy = self
        copy.tapHandler = handler
        return copy
    }

    /// Handle long presses.
    public func onNavvoLongPress(_ handler: @escaping (LatLng) -> Void) -> NavvoMap {
        var copy = self
        copy.longPressHandler = handler
        return copy
    }

    /// Handle marker taps.
    public func onNavvoMarkerTap(_ handler: @escaping (NavvoMarker) -> Void) -> NavvoMap {
        var copy = self
        copy.markerTapHandler = handler
        return copy
    }

    /// Handle style-loaded events (fires again after style switches).
    public func onNavvoStyleLoaded(_ handler: @escaping () -> Void) -> NavvoMap {
        var copy = self
        copy.styleLoadedHandler = handler
        return copy
    }

    /// Handle camera-idle events.
    public func onNavvoCameraIdle(_ handler: @escaping (CameraPosition) -> Void) -> NavvoMap {
        var copy = self
        copy.cameraIdleHandler = handler
        return copy
    }

    /// Show the user-location puck.
    public func navvoUserLocation(_ shows: Bool) -> NavvoMap {
        var copy = self
        copy.showsUserLocation = shows
        return copy
    }

    // MARK: UIViewRepresentable

    public func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    public func makeUIView(context: Context) -> NavvoMapView {
        var options = NavvoMapView.Options()
        options.style = style
        options.terrain = terrain
        options.camera = initialCamera
        options.showsUserLocation = showsUserLocation
        let view = NavvoMapView(frame: .zero, options: options)
        view.delegate = context.coordinator
        context.coordinator.attach(parent: self, view: view)
        return view
    }

    public func updateUIView(_ uiView: NavvoMapView, context: Context) {
        context.coordinator.parent = self
        if uiView.style != style || uiView.terrainEnabled != terrain {
            uiView.setStyle(style, terrain: terrain)
        }
        uiView.showsUserLocation = showsUserLocation
        context.coordinator.syncMarkers(markers, on: uiView)
        if let binding = cameraBinding {
            let desired = binding.wrappedValue
            if !context.coordinator.isWritingCamera,
               !desired.isApproximately(uiView.camera)
            {
                uiView.setCamera(desired, animated: true)
            }
        }
    }

    // MARK: Coordinator

    /// Bridges `NavvoMapView` delegate callbacks into SwiftUI closures and
    /// binding writes.
    public final class Coordinator: NavvoMapViewDelegate {
        var parent: NavvoMap
        weak var view: NavvoMapView?
        var isWritingCamera = false
        private var appliedMarkers: [String: NavvoMarker] = [:]

        init(_ parent: NavvoMap) {
            self.parent = parent
        }

        func attach(parent: NavvoMap, view: NavvoMapView) {
            self.parent = parent
            self.view = view
            syncMarkers(parent.markers, on: view)
        }

        func syncMarkers(_ desired: [NavvoMarker], on view: NavvoMapView) {
            var desiredByID: [String: NavvoMarker] = [:]
            for marker in desired { desiredByID[marker.id] = marker }
            // Remove absent or moved markers.
            for (id, existing) in appliedMarkers {
                let replacement = desiredByID[id]
                if replacement == nil || replacement!.position != existing.position {
                    view.removeMarker(existing)
                    appliedMarkers.removeValue(forKey: id)
                }
            }
            // Add new (or re-add moved) markers.
            for (id, marker) in desiredByID where appliedMarkers[id] == nil {
                view.addMarker(marker)
                appliedMarkers[id] = marker
            }
        }

        // MARK: NavvoMapViewDelegate

        public func mapViewDidFinishLoadingStyle(_ mapView: NavvoMapView) {
            parent.styleLoadedHandler?()
        }

        public func mapView(_ mapView: NavvoMapView, didTapAt coordinate: LatLng) {
            parent.tapHandler?(coordinate)
        }

        public func mapView(_ mapView: NavvoMapView, didLongPressAt coordinate: LatLng) {
            parent.longPressHandler?(coordinate)
        }

        public func mapView(_ mapView: NavvoMapView, didTap marker: NavvoMarker) {
            parent.markerTapHandler?(marker)
        }

        public func mapViewDidBecomeIdle(_ mapView: NavvoMapView) {
            let camera = mapView.camera
            parent.cameraIdleHandler?(camera)
            if let binding = parent.cameraBinding {
                isWritingCamera = true
                binding.wrappedValue = camera
                isWritingCamera = false
            }
        }
    }
}
#endif
