// lib/widgets/common/map/mapbox/map.dart
import 'dart:async';
import 'dart:convert';
import 'dart:math';

import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/utils/debouncer.dart';
import 'package:aves/widgets/common/basic/gestures/gesture_detector.dart';
import 'package:aves/widgets/common/map/mapbox/tracks_layer.dart';
import 'package:aves_map/aves_map.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart' as mbx;
import 'package:provider/provider.dart';

class EntryMapboxMap<T> extends StatefulWidget {
  final AvesMapController controller;
  final Listenable clusterListenable;
  final ValueNotifier<ZoomedBounds> boundsNotifier;
  final double minZoom, maxZoom;
  final EntryMapStyle style;
  final TransitionBuilder decoratorBuilder;
  final WidgetBuilder buttonPanelBuilder;
  final MarkerClusterBuilder<T> markerClusterBuilder;
  final MarkerWidgetBuilder<T> markerWidgetBuilder;
  final ValueNotifier<LatLng?>? dotLocationNotifier;
  final Size markerSize, dotMarkerSize;
  final ValueNotifier<double>? overlayOpacityNotifier;
  final MapOverlay? overlayEntry;
  final Set<List<LatLng>>? tracks;
  final UserZoomChangeCallback? onUserZoomChange;
  final MapTapCallback? onMapTap;
  final MarkerTapCallback<T>? onMarkerTap;
  final MarkerLongPressCallback<T>? onMarkerLongPress;
  final ValueChanged<Set<MarkerKey<T>>>? onVisibleMarkerKeysChanged;

  const EntryMapboxMap({
    super.key,
    required this.controller,
    required this.clusterListenable,
    required this.boundsNotifier,
    this.minZoom = 0,
    this.maxZoom = 22,
    required this.style,
    required this.decoratorBuilder,
    required this.buttonPanelBuilder,
    required this.markerClusterBuilder,
    required this.markerWidgetBuilder,
    required this.dotLocationNotifier,
    required this.markerSize,
    required this.dotMarkerSize,
    this.overlayOpacityNotifier,
    this.overlayEntry,
    this.tracks,
    this.onUserZoomChange,
    this.onMapTap,
    this.onMarkerTap,
    this.onMarkerLongPress,
    this.onVisibleMarkerKeysChanged,
  });

  @override
  State<EntryMapboxMap<T>> createState() => _EntryMapboxMapState<T>();
}

class _EntryMapboxMapState<T> extends State<EntryMapboxMap<T>> {
  static const bool _debugMapbox = true;
  static const bool _debugCameraMove = false;
  static const bool _debugOverlayBuild = true;

  final Set<StreamSubscription> _subscriptions = {};
  final Debouncer _debouncer = Debouncer(
    delay: ADurations.mapIdleDebounceDelay,
  );

  mbx.MapboxMap? _mapboxMap;

  Map<MarkerKey<T>, GeoEntry<T>> _geoEntryByMarkerKey = {};
  Map<MarkerKey<T>, Offset> _screenOffsetByMarkerKey = {};
  Map<MarkerKey<T>, Offset> _lastKnownScreenOffsetByMarkerKey = {};
  Map<String, Offset> _lastKnownScreenOffsetByStableKey = {};

  Set<MarkerKey<T>> _lastReportedVisibleMarkerKeys = {};

  Offset? _dotScreenOffset;
  Size _mapSize = Size.zero;

  mbx.Point? _lastCameraCenter;
  double? _lastCameraZoom;
  double? _lastCameraBearing;
  double? _lastCameraPitch;
  double? _lastTerrainPitch;

  bool _updatingScreenPositions = false;
  bool _updatingBoundsFromMap = false;

  int _lastScreenPositionUpdateMs = 0;
  int _screenPositionGeneration = 0;

  late final VoidCallback _clusterListenableListener = () {
    _updateMarkers(reason: 'cluster-listener');
  };

  ZoomedBounds get bounds => widget.boundsNotifier.value;

  static const _cameraAnimationDurationMs = 600;

  static const _terrainSourceId = 'aves-mapbox-dem';
  static const _terrainSourceUrl = 'mapbox://mapbox.mapbox-terrain-dem-v1';
  static const _terrainExaggeration = 1.5;

  void _log(String message) {
    if (_debugMapbox) {
      debugPrint('[mapbox] $message');
    }
  }

  String _stableScreenOffsetKey(
    MarkerKey<T> markerKey,
    GeoEntry<T> geoEntry,
  ) {
    final markerId = geoEntry.markerId;
    if (markerId != null) {
      return 'entry:$markerId';
    }

    final childMarkerId = geoEntry.childMarkerId;
    if (childMarkerId != null) {
      return 'cluster-child:$childMarkerId';
    }

    final lat = geoEntry.latitude?.toStringAsFixed(3) ?? 'null';
    final lng = geoEntry.longitude?.toStringAsFixed(3) ?? 'null';

    return 'location:$lat:$lng';
  }

  bool _shouldReportVisibleMarkerKeys({required String reason}) {
    return reason == 'load-style' ||
        reason.startsWith('idle-') ||
        reason.startsWith('update-markers-idle-') ||
        reason.startsWith('pitch-clamp-') ||
        reason == 'pitch-change';
  }

  void _notifyVisibleMarkerKeys(Set<MarkerKey<T>> markerKeys) {
    final oldKeys = _lastReportedVisibleMarkerKeys;

    final unchanged =
        oldKeys.length == markerKeys.length && oldKeys.containsAll(markerKeys);

    if (unchanged) return;

    _lastReportedVisibleMarkerKeys = Set.unmodifiable(markerKeys);

    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (!mounted) return;

      widget.onVisibleMarkerKeysChanged?.call(_lastReportedVisibleMarkerKeys);
    });
  }

  void _rememberCameraState(
    mbx.CameraState camera, {
    bool rememberTerrainPitch = true,
  }) {
    _lastCameraCenter = camera.center;
    _lastCameraZoom = camera.zoom;
    _lastCameraBearing = camera.bearing;
    _lastCameraPitch = camera.pitch;

    if (rememberTerrainPitch && widget.style.mapboxTerrain) {
      _lastTerrainPitch = camera.pitch;
    }
  }

  Future<void> _captureTerrainPitch({required String reason}) async {
    final mapboxMap = _mapboxMap;
    if (mapboxMap == null) return;

    final camera = await mapboxMap.getCameraState();

    _lastTerrainPitch = camera.pitch;

    _log(
      'capture terrain pitch '
      'reason=$reason '
      'pitch=${camera.pitch}',
    );

    _rememberCameraState(
      camera,
      rememberTerrainPitch: false,
    );
  }

Future<void> _applyMapboxOrnamentSettings() async {
  final mapboxMap = _mapboxMap;
  if (mapboxMap == null) return;

  _log(
    'apply ornament settings '
    'style=${widget.style.key} '
    'hideCompass=true',
  );

  await mapboxMap.compass.updateSettings(
    mbx.CompassSettings(
      enabled: false,
      visibility: false,
    ),
  );
}

  @override
  void initState() {
    super.initState();

    _setMapboxToken();
    _registerWidget(widget);
    _updateMarkers(reason: 'init');
  }

  @override
  void didUpdateWidget(covariant EntryMapboxMap<T> oldWidget) {
    super.didUpdateWidget(oldWidget);

    _unregisterWidget(oldWidget);
    _registerWidget(widget);

    final styleChanged =
        oldWidget.style.key != widget.style.key ||
        oldWidget.style.mapboxStyleUri != widget.style.mapboxStyleUri ||
        oldWidget.style.mapboxTerrain != widget.style.mapboxTerrain;

    if (styleChanged) {
      _log(
        'style changed ${oldWidget.style.key} -> ${widget.style.key}',
      );

      unawaited(() async {
        if (oldWidget.style.mapboxTerrain && !widget.style.mapboxTerrain) {
          await _captureTerrainPitch(reason: 'before-leaving-terrain');
        }

        await _applyMapboxGestureSettings();
        await _applyMapboxOrnamentSettings();
        await _loadSelectedStyle();
      }());
    }

    if (oldWidget.tracks != widget.tracks) {
      unawaited(_updateTracksLayer(reason: 'widget-update'));
    }
  }

  @override
  void dispose() {
    _unregisterWidget(widget);
    super.dispose();
  }

  void _setMapboxToken() {
    const token = String.fromEnvironment('MAPBOX_ACCESS_TOKEN');

    _log(
      'token present=${token.isNotEmpty} '
      'length=${token.length}',
    );

    if (token.isNotEmpty) {
      mbx.MapboxOptions.setAccessToken(token);
    }
  }

  void _registerWidget(EntryMapboxMap<T> widget) {
    final avesMapController = widget.controller;

    _subscriptions.add(
      avesMapController.moveCommands.listen(
        (event) => _moveTo(event.latLng),
      ),
    );

    _subscriptions.add(
      avesMapController.zoomCommands.listen(
        (event) => _zoomBy(event.delta),
      ),
    );

    _subscriptions.add(
      avesMapController.rotationResetCommands.listen(
        (_) => _resetRotation(),
      ),
    );

    widget.clusterListenable.addListener(_clusterListenableListener);
    widget.boundsNotifier.addListener(_onBoundsChanged);
    widget.dotLocationNotifier?.addListener(_updateDotScreenPosition);
  }

  void _unregisterWidget(EntryMapboxMap<T> widget) {
    widget.clusterListenable.removeListener(_clusterListenableListener);
    widget.boundsNotifier.removeListener(_onBoundsChanged);
    widget.dotLocationNotifier?.removeListener(_updateDotScreenPosition);

    _subscriptions
      ..forEach((sub) => sub.cancel())
      ..clear();
  }

  Future<void> _applyMapboxGestureSettings() async {
    final mapboxMap = _mapboxMap;
    if (mapboxMap == null) return;

    final pitchEnabled = widget.style.mapboxTerrain;

    _log(
      'apply gesture settings '
      'style=${widget.style.key} '
      'pitchEnabled=$pitchEnabled',
    );

    await mapboxMap.gestures.updateSettings(
      mbx.GesturesSettings(
        pitchEnabled: pitchEnabled,
        rotateEnabled: true,
        pinchToZoomEnabled: true,
        scrollEnabled: true,
        simultaneousRotateAndPinchToZoomEnabled: true,
      ),
    );

    // In 2D il pitch corrente deve essere 0, ma NON cancelliamo _lastTerrainPitch.
    // Così, tornando in 3D, ripristiniamo l'ultima inclinazione usata.
    if (!pitchEnabled) {
      _lastCameraPitch = 0.0;
    }
  }

  Future<void> _updateTracksLayer({required String reason}) async {
    final mapboxMap = _mapboxMap;
    if (mapboxMap == null) return;
    if (!mounted) return;

    await MapboxTracksLayer.update(
      mapboxMap: mapboxMap,
      tracks: widget.tracks,
      color: Theme.of(context).colorScheme.primary,
      width: MapThemeData.trackWidth.toDouble(),
      log: _log,
      reason: reason,
    );
  }

  Future<void> _loadSelectedStyle() async {
    final mapboxMap = _mapboxMap;
    if (mapboxMap == null) return;

    final styleUri = widget.style.mapboxStyleUri ??
        'mapbox://styles/mapbox/satellite-streets-v12';

    _log(
      'loadStyleURI '
      'key=${widget.style.key} '
      'uri=$styleUri '
      'terrain=${widget.style.mapboxTerrain}',
    );

    await mapboxMap.loadStyleURI(styleUri);

    await _applyMapboxTerrainIfNeeded();
    await _applyCameraForSelectedStyle();
    await _updateTracksLayer(reason: 'load-style');

    await _updateVisibleRegion(reason: 'load-style');

    _updateMarkers(reason: 'load-style');

    await _updateMarkerScreenPositions(
      reason: 'load-style',
      keepPreviousOnEmpty: true,
    );

    await _updateDotScreenPosition();
  }

  Future<void> _applyMapboxTerrainIfNeeded() async {
    final mapboxMap = _mapboxMap;
    if (mapboxMap == null) return;

    if (!widget.style.mapboxTerrain) {
      _log('terrain disabled style=${widget.style.key}');
      return;
    }

    _log('applying 3D terrain');

    try {
      await mapboxMap.style.addSource(
        mbx.RasterDemSource(
          id: _terrainSourceId,
          url: _terrainSourceUrl,
          tileSize: 512.0,
          maxzoom: 14.0,
        ),
      );

      _log('terrain source added');
    } catch (e) {
      _log('terrain source add skipped/error: $e');
    }

    final terrainProperties = jsonEncode({
      'source': _terrainSourceId,
      'exaggeration': _terrainExaggeration,
    });

    final style = mapboxMap.style as dynamic;

    try {
      await style.setStyleTerrain(terrainProperties);
      _log('terrain applied via setStyleTerrain');
      return;
    } catch (e) {
      _log('setStyleTerrain failed: $e');
    }

    try {
      await style.setTerrain(terrainProperties);
      _log('terrain applied via setTerrain');
      return;
    } catch (e) {
      _log('setTerrain failed: $e');
    }

    _log(
      'terrain API not available in this plugin version. '
      'Map will keep pitch but real DEM terrain may not be enabled.',
    );
  }

  Future<void> _applyCameraForSelectedStyle() async {
    final mapboxMap = _mapboxMap;
    if (mapboxMap == null) return;

    final camera = await mapboxMap.getCameraState();

    _rememberCameraState(
      camera,
      rememberTerrainPitch: false,
    );

    final hasRememberedCamera =
        _lastCameraCenter != null && _lastCameraZoom != null;

    final targetCenter = hasRememberedCamera
        ? _lastCameraCenter
        : camera.center;

    final targetZoom = hasRememberedCamera
        ? _lastCameraZoom
        : camera.zoom;

    final targetBearing = hasRememberedCamera
        ? (_lastCameraBearing ?? camera.bearing)
        : bounds.rotation;

    final targetPitch = widget.style.mapboxTerrain
        ? (_lastTerrainPitch ?? widget.style.initialPitch ?? camera.pitch)
        : 0.0;

    _log(
      'apply camera '
      'style=${widget.style.key} '
      'terrain=${widget.style.mapboxTerrain} '
      'hasRememberedCamera=$hasRememberedCamera '
      'pitch=$targetPitch '
      'bearing=$targetBearing',
    );

    await mapboxMap.flyTo(
      mbx.CameraOptions(
        center: targetCenter,
        zoom: targetZoom,
        pitch: targetPitch,
        bearing: targetBearing,
      ),
      mbx.MapAnimationOptions(
        duration: _cameraAnimationDurationMs,
      ),
    );

    final updatedCamera = await mapboxMap.getCameraState();

    _rememberCameraState(
      updatedCamera,
      rememberTerrainPitch: widget.style.mapboxTerrain,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        widget.decoratorBuilder(context, _buildMap()),
        _buildMarkerOverlay(),
        widget.buttonPanelBuilder(context),
      ],
    );
  }

  Widget _buildMap() {
    final center = bounds.projectedCenter;

    final styleUri = widget.style.mapboxStyleUri ??
        'mapbox://styles/mapbox/satellite-streets-v12';

    final hasRememberedCamera =
        _lastCameraCenter != null && _lastCameraZoom != null;

    final cameraOptions = mbx.CameraOptions(
      center: hasRememberedCamera
          ? _lastCameraCenter
          : mbx.Point(
              coordinates: mbx.Position(
                center.longitude,
                center.latitude,
              ),
            ),
      zoom: hasRememberedCamera ? _lastCameraZoom : bounds.zoom,
      bearing: hasRememberedCamera
          ? (_lastCameraBearing ?? 0.0)
          : bounds.rotation,
      pitch: widget.style.mapboxTerrain
          ? (_lastTerrainPitch ??
              widget.style.initialPitch ??
              _lastCameraPitch ??
              0.0)
          : 0.0,
    );

    return LayoutBuilder(
      builder: (context, constraints) {
        final newMapSize = Size(
          constraints.maxWidth,
          constraints.maxHeight,
        );

        if (_mapSize != newMapSize) {
          _mapSize = newMapSize;

          WidgetsBinding.instance.addPostFrameCallback((_) {
            if (!mounted) return;

            unawaited(_onIdle(reason: 'map-size-change'));
          });
        }

        return mbx.MapWidget(
          key: const ValueKey('mapbox_map_widget'),
          styleUri: styleUri,
          cameraOptions: cameraOptions,
          onMapCreated: (mapboxMap) async {
            _log('onMapCreated');

            _mapboxMap = mapboxMap;

            await _applyMapboxGestureSettings();
            await _applyMapboxOrnamentSettings();
            await _loadSelectedStyle();

            _log('initial bounds=${widget.boundsNotifier.value}');
          },
          onTapListener: (context) {
            final coordinate = context.point.coordinates;

            widget.onMapTap?.call(
              LatLng(
                coordinate.lat.toDouble(),
                coordinate.lng.toDouble(),
              ),
            );
          },
          onCameraChangeListener: (_) {
            if (_debugCameraMove) {
              _log('camera change style=${widget.style.key}');
            }

            unawaited(_updateScreenPositionsDuringCameraMove());

            _debouncer(() => _onIdle(reason: 'camera-debounced'));
          },
          onMapIdleListener: (_) {
            _onIdle(reason: 'map-idle');
          },
        );
      },
    );
  }

  Widget _buildMarkerOverlay() {
    final markerSize = widget.markerSize;
    final dotMarkerSize = widget.dotMarkerSize;

    var overlayTotal = 0;
    var overlayWithDirectOffset = 0;
    var overlayWithStableOffset = 0;
    var overlayWithoutOffset = 0;

    final interactive = context.select<MapThemeData, bool>(
      (v) => v.interactive,
    );

    final markerWidgets = _geoEntryByMarkerKey.entries.map((kv) {
      final markerKey = kv.key;
      final geoEntry = kv.value;

      overlayTotal++;

      final latLng = LatLng(
        geoEntry.latitude!,
        geoEntry.longitude!,
      );

      final stableKey = _stableScreenOffsetKey(markerKey, geoEntry);

      final directOffset = _screenOffsetByMarkerKey[markerKey];
      final stableOffset = _lastKnownScreenOffsetByStableKey[stableKey];

      if (directOffset != null) {
        overlayWithDirectOffset++;
      } else if (stableOffset != null) {
        overlayWithStableOffset++;
      } else {
        overlayWithoutOffset++;
      }

      final offset = directOffset ?? stableOffset;

      if (offset == null) return const SizedBox();

      final onMarkerLongPress = widget.onMarkerLongPress;
      final onLongPress = onMarkerLongPress != null
          ? Feedback.wrapForLongPress(
              () => onMarkerLongPress.call(geoEntry, latLng),
              context,
            )
          : null;

      return Positioned(
        left: offset.dx - markerSize.width / 2,
        top: offset.dy - markerSize.height,
        width: markerSize.width,
        height: markerSize.height,
        child: AGestureDetector(
          onTap: () => widget.onMarkerTap?.call(geoEntry),
          onDoubleTap: interactive
              ? () => _zoomBy(
                    1,
                    focalPoint: latLng,
                  )
              : null,
          onLongPress: onLongPress,
          longPressTimeout: Duration(
            milliseconds: min(
              settings.longPressTimeout.inMilliseconds,
              kLongPressTimeout.inMilliseconds,
            ),
          ),
          child: widget.markerWidgetBuilder(markerKey),
        ),
      );
    }).toList();

    if (_debugOverlayBuild) {
      _log(
        'overlay build '
        'total=$overlayTotal '
        'direct=$overlayWithDirectOffset '
        'stable=$overlayWithStableOffset '
        'missing=$overlayWithoutOffset '
        'screenOffsets=${_screenOffsetByMarkerKey.length} '
        'stableOffsets=${_lastKnownScreenOffsetByStableKey.length} '
        'style=${widget.style.key} '
        'terrain=${widget.style.mapboxTerrain}',
      );
    }

    final dotLocationNotifier = widget.dotLocationNotifier;

    return IgnorePointer(
      ignoring: false,
      child: Stack(
        children: [
          ...markerWidgets,
          if (dotLocationNotifier != null)
            ValueListenableBuilder<LatLng?>(
              valueListenable: dotLocationNotifier,
              builder: (context, dotLocation, child) {
                if (dotLocation == null) return const SizedBox();

                final offset = _dotScreenOffset;
                if (offset == null) return const SizedBox();

                return Positioned(
                  left: offset.dx - dotMarkerSize.width / 2,
                  top: offset.dy - dotMarkerSize.height / 2,
                  width: dotMarkerSize.width,
                  height: dotMarkerSize.height,
                  child: const DotMarker(),
                );
              },
            ),
        ],
      ),
    );
  }

  Future<Offset?> _screenOffsetForLatLng(LatLng location) async {
    final mapboxMap = _mapboxMap;
    if (mapboxMap == null) return null;
    if (_mapSize.width <= 0 || _mapSize.height <= 0) return null;

    final screenCoordinate = await mapboxMap.pixelForCoordinate(
      mbx.Point(
        coordinates: mbx.Position(
          location.longitude,
          location.latitude,
        ),
      ),
    );

    final x = screenCoordinate.x;
    final y = screenCoordinate.y;

    final margin = max(
      widget.markerSize.width,
      widget.markerSize.height,
    );

    if (x < -margin ||
        y < -margin ||
        x > _mapSize.width + margin ||
        y > _mapSize.height + margin) {
      final camera = await mapboxMap.getCameraState();

      _log(
        'projection outside viewport '
        'lat=${location.latitude} '
        'lng=${location.longitude} '
        'x=$x '
        'y=$y '
        'mapSize=$_mapSize '
        'pitch=${camera.pitch} '
        'zoom=${camera.zoom} '
        'bearing=${camera.bearing} '
        'style=${widget.style.key} '
        'terrain=${widget.style.mapboxTerrain}',
      );

      return null;
    }

    return Offset(x, y);
  }

  Future<LatLng?> _latLngForScreenOffset(Offset offset) async {
    final mapboxMap = _mapboxMap;
    if (mapboxMap == null) return null;

    final point = await mapboxMap.coordinateForPixel(
      mbx.ScreenCoordinate(
        x: offset.dx,
        y: offset.dy,
      ),
    );

    final coordinates = point.coordinates;

    return LatLng(
      coordinates.lat.toDouble(),
      coordinates.lng.toDouble(),
    );
  }

  void _onBoundsChanged() {
    if (_updatingBoundsFromMap) return;

    _debouncer(
      () => _onIdle(reason: 'bounds-changed-external'),
    );
  }

  Future<void> _onIdle({required String reason}) async {
    if (!mounted) return;

    _log(
      'onIdle '
      'reason=$reason '
      'style=${widget.style.key} '
      'terrain=${widget.style.mapboxTerrain}',
    );

    await _updateVisibleRegion(reason: reason);

    widget.controller.notifyIdle(widget.boundsNotifier.value);

    _updateMarkers(reason: 'idle-$reason');

    await _updateMarkerScreenPositions(
      reason: 'idle-$reason',
      keepPreviousOnEmpty: true,
    );

    await _updateDotScreenPosition();
  }

  Future<void> _updateScreenPositionsDuringCameraMove() async {
    if (_updatingScreenPositions) return;

    final now = DateTime.now().millisecondsSinceEpoch;

    if (now - _lastScreenPositionUpdateMs < 50) return;

    _lastScreenPositionUpdateMs = now;
    _updatingScreenPositions = true;

    try {
      await _updateMarkerScreenPositions(
        reason: 'camera-move',
        keepPreviousOnEmpty: true,
      );

      await _updateDotScreenPosition();
    } finally {
      _updatingScreenPositions = false;
    }
  }

  void _updateMarkers({required String reason}) {
    if (!mounted) return;

    final markers = widget.markerClusterBuilder();

    _log(
      'updateMarkers '
      'reason=$reason '
      'count=${markers.length} '
      'style=${widget.style.key} '
      'terrain=${widget.style.mapboxTerrain}',
    );

    setState(() {
      _geoEntryByMarkerKey = markers;
    });

    unawaited(
      _updateMarkerScreenPositions(
        reason: 'update-markers-$reason',
        keepPreviousOnEmpty: true,
      ),
    );

    unawaited(_updateDotScreenPosition());
  }

  Future<void> _updateMarkerScreenPositions({
    required String reason,
    required bool keepPreviousOnEmpty,
  }) async {
    final mapboxMap = _mapboxMap;
    if (mapboxMap == null) return;

    final generation = ++_screenPositionGeneration;
    final result = <MarkerKey<T>, Offset>{};

    var total = 0;
    var visible = 0;
    var invalid = 0;
    var retained = 0;

    for (final kv in _geoEntryByMarkerKey.entries) {
      total++;

      final geoEntry = kv.value;
      final stableKey = _stableScreenOffsetKey(kv.key, geoEntry);

      final latLng = LatLng(
        geoEntry.latitude!,
        geoEntry.longitude!,
      );

      final offset = await _screenOffsetForLatLng(latLng);

      if (offset != null) {
        visible++;
        result[kv.key] = offset;

        _lastKnownScreenOffsetByMarkerKey[kv.key] = offset;
        _lastKnownScreenOffsetByStableKey[stableKey] = offset;
      } else {
        invalid++;

        final previousOffset =
            _lastKnownScreenOffsetByMarkerKey[kv.key] ??
            _lastKnownScreenOffsetByStableKey[stableKey] ??
            _screenOffsetByMarkerKey[kv.key];

        if (previousOffset != null) {
          retained++;
          result[kv.key] = previousOffset;
        }
      }
    }

    if (!mounted) return;

    if (generation != _screenPositionGeneration) {
      _log(
        'marker screen positions skipped stale '
        'reason=$reason '
        'generation=$generation current=$_screenPositionGeneration',
      );
      return;
    }

    _log(
      'marker screen positions '
      'reason=$reason '
      'total=$total '
      'visible=$visible '
      'invalid=$invalid '
      'retained=$retained '
      'keepPreviousOnEmpty=$keepPreviousOnEmpty '
      'style=${widget.style.key} '
      'terrain=${widget.style.mapboxTerrain} '
      'zoom=${widget.boundsNotifier.value.zoom} '
      'rotation=${widget.boundsNotifier.value.rotation}',
    );

    if (keepPreviousOnEmpty &&
        total > 0 &&
        visible == 0 &&
        retained == 0 &&
        _screenOffsetByMarkerKey.isNotEmpty) {
      _log(
        'keeping previous marker positions because current projection is empty '
        'reason=$reason '
        'previous=${_screenOffsetByMarkerKey.length}',
      );
      return;
    }

    final previousCount = _screenOffsetByMarkerKey.length;
    final newCount = result.length;

    if (previousCount > 0 && newCount < previousCount && invalid > 0) {
      _log(
        'merging previous marker positions '
        'reason=$reason '
        'previous=$previousCount '
        'new=$newCount '
        'invalid=$invalid',
      );

      for (final entry in _screenOffsetByMarkerKey.entries) {
        result.putIfAbsent(entry.key, () => entry.value);
      }
    }

    setState(() {
      _screenOffsetByMarkerKey = result;
    });

    if (_shouldReportVisibleMarkerKeys(reason: reason)) {
      final visibleMarkerKeys = result.keys
          .where(_geoEntryByMarkerKey.containsKey)
          .toSet();

      _notifyVisibleMarkerKeys(visibleMarkerKeys);
    } else {
      _log(
        'visible marker keys report skipped '
        'reason=$reason',
      );
    }
  }

  Future<void> _updateDotScreenPosition() async {
    final dotLocation = widget.dotLocationNotifier?.value;

    if (dotLocation == null) {
      if (!mounted) return;

      setState(() {
        _dotScreenOffset = null;
      });

      return;
    }

    final offset = await _screenOffsetForLatLng(dotLocation);

    if (!mounted) return;

    setState(() {
      _dotScreenOffset = offset;
    });
  }

  Future<ZoomedBounds?> _queryBoundsForCamera(mbx.CameraState camera) async {
    final mapboxMap = _mapboxMap;
    if (mapboxMap == null) return null;

    try {
      final bounds = await (mapboxMap as dynamic).coordinateBoundsForCamera(
        mbx.CameraOptions(
          center: camera.center,
          zoom: camera.zoom,
          bearing: camera.bearing,
          // Use a top-down query footprint, while keeping the real camera pitched.
          pitch: 0.0,
        ),
      );

      final swCoordinates = bounds.southwest.coordinates;
      final neCoordinates = bounds.northeast.coordinates;

      return ZoomedBounds(
        sw: LatLng(
          swCoordinates.lat.toDouble(),
          swCoordinates.lng.toDouble(),
        ),
        ne: LatLng(
          neCoordinates.lat.toDouble(),
          neCoordinates.lng.toDouble(),
        ),
        zoom: camera.zoom,
        rotation: camera.bearing,
      );
    } catch (e) {
      _log('coordinateBoundsForCamera unavailable/error: $e');
      return null;
    }
  }

  Future<void> _updateVisibleRegion({required String reason}) async {
    final mapboxMap = _mapboxMap;
    if (mapboxMap == null) return;
    if (_mapSize.width <= 0 || _mapSize.height <= 0) return;

    final camera = await mapboxMap.getCameraState();
    _rememberCameraState(camera);

    final queryBounds = await _queryBoundsForCamera(camera);

    if (queryBounds != null) {
      _updatingBoundsFromMap = true;

      try {
        widget.boundsNotifier.value = queryBounds;
      } finally {
        _updatingBoundsFromMap = false;
      }

      _log(
        'visible bounds native-query '
        'reason=$reason '
        'sw=${queryBounds.sw} '
        'ne=${queryBounds.ne} '
        'zoom=${camera.zoom} '
        'bearing=${camera.bearing} '
        'pitch=${camera.pitch}',
      );

      return;
    }

    final corners = [
      Offset.zero,
      Offset(_mapSize.width, 0),
      Offset(_mapSize.width, _mapSize.height),
      Offset(0, _mapSize.height),
    ];

    final points = <LatLng>[];

    for (final corner in corners) {
      final point = await _latLngForScreenOffset(corner);

      if (point != null) {
        points.add(point);
      }
    }

    if (points.isEmpty) {
      _log('visible bounds skipped no corner points reason=$reason');
      return;
    }

    final south = points.map((v) => v.latitude).reduce(min);
    final north = points.map((v) => v.latitude).reduce(max);
    final west = points.map((v) => v.longitude).reduce(min);
    final east = points.map((v) => v.longitude).reduce(max);

    _updatingBoundsFromMap = true;

    try {
      widget.boundsNotifier.value = ZoomedBounds(
        sw: LatLng(south, west),
        ne: LatLng(north, east),
        zoom: camera.zoom,
        rotation: camera.bearing,
      );
    } finally {
      _updatingBoundsFromMap = false;
    }

    _log(
      'visible bounds '
      'reason=$reason '
      'sw=${widget.boundsNotifier.value.sw} '
      'ne=${widget.boundsNotifier.value.ne} '
      'zoom=${camera.zoom} '
      'bearing=${camera.bearing} '
      'pitch=${camera.pitch}',
    );
  }

  Future<void> _moveTo(LatLng point) async {
    final mapboxMap = _mapboxMap;
    if (mapboxMap == null) return;

    await mapboxMap.flyTo(
      mbx.CameraOptions(
        center: mbx.Point(
          coordinates: mbx.Position(
            point.longitude,
            point.latitude,
          ),
        ),
      ),
      mbx.MapAnimationOptions(
        duration: _cameraAnimationDurationMs,
      ),
    );
  }

  Future<void> _zoomBy(
    double amount, {
    LatLng? focalPoint,
  }) async {
    final mapboxMap = _mapboxMap;
    if (mapboxMap == null) return;

    final camera = await mapboxMap.getCameraState();

    final endZoom = (camera.zoom + amount).clamp(
      widget.minZoom,
      widget.maxZoom,
    );

    widget.onUserZoomChange?.call(endZoom);

    await mapboxMap.flyTo(
      mbx.CameraOptions(
        center: focalPoint != null
            ? mbx.Point(
                coordinates: mbx.Position(
                  focalPoint.longitude,
                  focalPoint.latitude,
                ),
              )
            : null,
        zoom: endZoom,
      ),
      mbx.MapAnimationOptions(
        duration: _cameraAnimationDurationMs,
      ),
    );
  }

  Future<void> _resetRotation() async {
    final mapboxMap = _mapboxMap;
    if (mapboxMap == null) return;

    await mapboxMap.flyTo(
      mbx.CameraOptions(
        bearing: 0,
      ),
      mbx.MapAnimationOptions(
        duration: _cameraAnimationDurationMs,
      ),
    );
  }
}
