import SwiftUI
import MapKit
import ExpoModulesCore

extension MKMapPoint {
  // Perpendicular distance (in metres) from `self` to the
  // line segment **AB**.
  func distance(toSegmentFrom a: MKMapPoint, to b: MKMapPoint) -> CLLocationDistance {
    let dx = b.x - a.x
    let dy = b.y - a.y

    // Degenerate segment => use point distance
    guard dx != 0 || dy != 0 else {
      return distance(to: a)
    }

    let t = ((x - a.x) * dx + (y - a.y) * dy) / (dx * dx + dy * dy)
    // Clamp the projection to the segment
    let clamped = max(0.0, min(1.0, t))

    let proj = MKMapPoint(
      x: a.x + clamped * dx,
      y: a.y + clamped * dy
    )
    return distance(to: proj)
  }
}

@available(iOS 18.0, *)
struct AppleMapsViewiOS18: View, AppleMapsViewProtocol {
  @EnvironmentObject var props: AppleMapsViewProps
  @ObservedObject private var state = AppleMapsViewiOS18State()

  func setCameraPosition(config: CameraPosition?) {
    withAnimation {
      state.mapCameraPosition = config.map(convertToMapCamera) ?? .userLocation(fallback: state.mapCameraPosition)
    }
  }

  func openLookAround(coordinate: Coordinate) async throws {
    if state.lookAroundScene != nil {
      throw LookAroundAlreadyPresentedException()
    }

    let scene = try await getLookAroundScene(from: CLLocationCoordinate2D(
      latitude: coordinate.latitude,
      longitude: coordinate.longitude
    ))

    if scene == nil {
      throw SceneUnavailableAtLocationException()
    }

    state.lookAroundScene = scene
    state.lookAroundPresented = true
  }

  func setSelection(config: SelectionConfig) {
    let updateSelection = {
      if let mapItem = config.mapItem {
        self.state.selection = MapSelection(mapItem)
        if config.moveCamera, let coordinate = config.coordinate {
          if let zoom = config.zoom {
            self.state.mapCameraPosition = convertToMapCameraPosition(coordinate: coordinate, zoom: zoom)
          } else {
            // Keep current zoom/distance, just center on coordinate
            self.state.mapCameraPosition = .camera(
              MapCamera(
                centerCoordinate: coordinate,
                distance: self.state.lastKnownDistance ?? 10000,
                heading: self.state.lastKnownHeading,
                pitch: self.state.lastKnownPitch
              )
            )
          }
        }
      } else {
        self.state.selection = nil
      }
    }

    withAnimation {
      updateSelection()
    }
  }

  var body: some View {
    let properties = props.properties
    let uiSettings = props.uiSettings

    // swiftlint:disable:next closure_body_length
    MapReader { reader in
      // swiftlint:disable:next closure_body_length
      Map(position: $state.mapCameraPosition, selection: $state.selection) {
        ForEach(props.markers) { marker in
          if marker.hasMonogram {
            Marker(
              marker.title,
              monogram: Text(marker.monogram),
              coordinate: marker.clLocationCoordinate2D
            )
            .tint(marker.tintColor)
            .tag(MapSelection(marker.mapItem))
          } else {
            Marker(
              marker.title,
              systemImage: marker.systemImage,
              coordinate: marker.clLocationCoordinate2D
            )
            .tint(marker.tintColor)
            .tag(MapSelection(marker.mapItem))
          }
        }

        ForEach(props.polylines) { polyline in
          MapPolyline(
            coordinates: polyline.clLocationCoordinates2D,
            contourStyle: polyline.contourStyle.toContourStyle()
          )
            .stroke(polyline.color, lineWidth: polyline.width)
            .tag(MapSelection<MKMapItem>(polyline.mapItem))
        }

        ForEach(props.polygons) { polygon in
          renderPolygon(polygon)
        }

        ForEach(props.circles) { circle in
          renderCircle(circle)
        }

        ForEach(props.annotations) { annotation in
          Annotation(
            annotation.title,
            coordinate: annotation.clLocationCoordinate2D
          ) {
            ZStack {
              if let icon = annotation.icon {
                Image(uiImage: icon.ref)
                  .resizable()
                  .frame(width: 50, height: 50)
              } else {
                RoundedRectangle(cornerRadius: 5)
                  .fill(annotation.backgroundColor)
              }
              Text(annotation.text)
                .foregroundStyle(annotation.textColor)
                .padding(5)
            }
          }
          .tag(MapSelection(annotation.mapItem))
        }

        if props.properties.isMyLocationEnabled {
          UserAnnotation()
        }
      }
      // https://developer.apple.com/forums/thread/795909?answerId=855111022#855111022
      // onTapGesture with Map is broken on iOS 26 so we use this workaround
      // https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-26-release-notes#Maps
      .simultaneousGesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
          .onEnded { value in
            // ignore pans: only a near-stationary touch counts as a tap
            let movement = hypot(value.translation.width, value.translation.height)
            guard movement < 10 else { return }
            let location = value.location
            if let coordinate = reader.convert(location, from: .local) {
              // marker press is handled via selection -> onMarkerClick; don't also fire onMapClick
              if nearestMarker(to: location, in: props.markers, using: reader) != nil {
                return
              }
              // check if we hit a polygon and send an event
              if let hit = props.polygons.first(where: { polygon in
                isTapInsidePolygon(tapCoordinate: coordinate, polygonCoordinates: polygon.clLocationCoordinates2D)
              }) {
                let coords = hit.coordinates.map {
                  [
                    "latitude": $0.latitude,
                    "longitude": $0.longitude
                  ]
                }
                props.onPolygonClick([
                  "id": hit.id,
                  "color": hit.color,
                  "lineColor": hit.lineColor as Any,
                  "lineWidth": hit.lineWidth as Any,
                  "coordinates": coords
                ])
              } else if let hit = props.circles.first(where: { circle in
                isTapInsideCircle(
                  tapCoordinate: coordinate,
                  circleCenter: circle.clLocationCoordinate2D,
                  radius: circle.radius
                )}) {
                  props.onCircleClick([
                    "id": hit.id,
                    "color": hit.color,
                    "lineColor": hit.lineColor as Any,
                    "lineWidth": hit.lineWidth as Any,
                    "radius": hit.radius,
                    "coordinates": [
                      "latitude": hit.center.latitude,
                      "longitude": hit.center.longitude
                    ]
                  ])
                }
              // Then check if we hit a polyline and send an event
              else if let hit = polyline(at: coordinate) {
                let coords = hit.coordinates.map {
                  [
                    "latitude": $0.latitude,
                    "longitude": $0.longitude
                  ]
                }
                props.onPolylineClick([
                  "id": hit.id,
                  "color": hit.color,
                  "width": hit.width,
                  "contourStyle": hit.contourStyle.rawValue,
                  "coordinates": coords
                ])
              }

              // Send an event of map click (empty-map tap only)
              props.onMapClick([
                "coordinates": [
                  "latitude": coordinate.latitude,
                  "longitude": coordinate.longitude
                ]
              ])
            }
          }
      )
      .mapControls {
        if uiSettings.compassEnabled {
          MapCompass()
        }
        if uiSettings.scaleBarEnabled {
          MapScaleView()
        }
        if uiSettings.togglePitchEnabled {
          MapPitchToggle()
        }
        if uiSettings.myLocationButtonEnabled {
          MapUserLocationButton()
        }
      }
      .onChange(of: props.cameraPosition) { _, newValue in
        state.mapCameraPosition = convertToMapCamera(position: newValue)
      }
      .onChange(of: state.selection, perform: handleSelectionChange)
      .onMapCameraChange(frequency: .onEnd) { context in
        state.lastKnownDistance = context.camera.distance
        state.lastKnownHeading = context.camera.heading
        state.lastKnownPitch = context.camera.pitch

        let cameraPosition = context.region.center
        let longitudeDelta = context.region.span.longitudeDelta
        let latitudeDelta = context.region.span.latitudeDelta
        let zoomLevel = log2(360 / longitudeDelta)

        props.onCameraMove([
          "coordinates": [
            "latitude": cameraPosition.latitude,
            "longitude": cameraPosition.longitude
          ],
          "longitudeDelta": longitudeDelta,
          "latitudeDelta": latitudeDelta,
          "zoom": zoomLevel,
          "tilt": context.camera.pitch,
          "bearing": context.camera.heading
        ])
      }
      .mapFeatureSelectionAccessory(props.properties.selectionEnabled ? .automatic : nil)
      .mapStyle(properties.mapType.toMapStyle(properties))
      .lookAroundViewer(
        isPresented: $state.lookAroundPresented,
        initialScene: state.lookAroundScene,
        allowsNavigation: true,
        showsRoadLabels: true,
        pointsOfInterest: .all,
        onDismiss: {
          state.lookAroundScene = nil
          state.lookAroundPresented = false
        }
      )
      .onAppear {
        if !state.hasInitializedCamera {
          state.mapCameraPosition = convertToMapCamera(position: props.cameraPosition)
          state.hasInitializedCamera = true
        }
      }
      .let(props.colorScheme.toColorScheme()) { view, colorScheme in
        view.environment(\.colorScheme, colorScheme)
      }
    }
  }

  private func handleSelectionChange(_ newSelection: MapSelection<MKMapItem>?) {
    guard let item = newSelection?.value else {
      return
    }

    if let marker = props.markers.first(where: { $0.mapItem == item }) {
      props.onMarkerClick(markerClickPayload(marker))
      return
    }

    if let annotation = props.annotations.first(where: { $0.mapItem == item }) {
      props.onAnnotationClick([
        "id": annotation.id,
        "title": annotation.title,
        "text": annotation.text,
        "coordinates": [
          "latitude": annotation.coordinates.latitude,
          "longitude": annotation.coordinates.longitude
        ]
      ])
      return
    }
  }

  private func polyline(at tap: CLLocationCoordinate2D) -> ExpoAppleMapPolyline? {
    let tapPoint = MKMapPoint(tap)
    let threshold = props.properties.polylineTapThreshold

    return props.polylines.first { line in
      let coords = line.hitTestCoordinates
      guard var prev = coords.first.map(MKMapPoint.init) else { return false }

      for coord in coords.dropFirst() {
        let curr = MKMapPoint(coord)
        if tapPoint.distance(toSegmentFrom: prev, to: curr) < threshold {
          return true
        }
        prev = curr
      }
      return false
    }
  }

  // Point-in-polygon algorithm (Ray-casting)
  // See: https://rosettacode.org/wiki/Ray-casting_algorithm
  func isTapInsidePolygon(tapCoordinate: CLLocationCoordinate2D, polygonCoordinates: [CLLocationCoordinate2D]) -> Bool {
    var inside = false
    let n = polygonCoordinates.count
    var j = n - 1

    for i in 0..<n {
      let vi = polygonCoordinates[i]
      let vj = polygonCoordinates[j]

      // Check if the point's latitude is between the y-coordinates (latitude) of the edge
      // and if the point's longitude is to the left of the intersection with the edge
      if ((vi.latitude > tapCoordinate.latitude) != (vj.latitude > tapCoordinate.latitude)) &&
        (tapCoordinate.longitude < (vj.longitude - vi.longitude) * (tapCoordinate.latitude - vi.latitude) / (vj.latitude - vi.latitude) + vi.longitude) {
        inside.toggle()
      }
      j = i
    }

    return inside
  }

  func isTapInsideCircle(
    tapCoordinate: CLLocationCoordinate2D, circleCenter: CLLocationCoordinate2D, radius: Double
  ) -> Bool {
    let tapPoint = MKMapPoint(tapCoordinate)
    let centerPoint = MKMapPoint(circleCenter)
    return tapPoint.distance(to: centerPoint) <= radius
  }
}
