import 'dart:async';
import 'dart:math' as math;

import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_map/plugin_api.dart';
import 'package:flutter_map_heatmap/flutter_map_heatmap.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:geolocator/geolocator.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/map/models/map_page_event.model.dart';
import 'package:immich_mobile/modules/map/providers/map_marker.provider.dart';
import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
import 'package:immich_mobile/modules/map/ui/asset_marker_icon.dart';
import 'package:immich_mobile/modules/map/ui/location_dialog.dart';
import 'package:immich_mobile/modules/map/ui/map_page_bottom_sheet.dart';
import 'package:immich_mobile/modules/map/ui/map_page_app_bar.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:immich_mobile/utils/debounce.dart';
import 'package:immich_mobile/extensions/flutter_map_extensions.dart';
import 'package:immich_mobile/utils/immich_app_theme.dart';
import 'package:immich_mobile/utils/selection_handlers.dart';
import 'package:latlong2/latlong.dart';
import 'package:logging/logging.dart';

class MapPage extends StatefulHookConsumerWidget {
  const MapPage({super.key});

  @override
  MapPageState createState() => MapPageState();
}

class MapPageState extends ConsumerState<MapPage> {
  // Non-State variables
  late final MapController mapController;
  // Streams are used instead of callbacks to prevent unnecessary rebuilds on events
  final StreamController mapPageEventSC =
      StreamController<MapPageEventBase>.broadcast();
  final StreamController bottomSheetEventSC =
      StreamController<MapPageEventBase>.broadcast();
  late final Stream bottomSheetEventStream;
  // Making assets in bounds as a state variable will result in un-necessary rebuilds of the bottom sheet
  // resulting in it getting reloaded each time a map move occurs
  Set<AssetMarkerData> assetsInBounds = {};
  // TODO: Migrate the handling to MapEventMove#id when flutter_map is upgraded
  // https://github.com/fleaflet/flutter_map/issues/1542
  // The below is used instead of MapEventMove#id to handle event from controller
  // in onMapEvent() since MapEventMove#id is not populated properly in the
  // current version of flutter_map(4.0.0) used
  bool forceAssetUpdate = false;
  bool isMapReady = false;
  late final Debounce debounce;

  @override
  void initState() {
    super.initState();
    mapController = MapController();
    bottomSheetEventStream = bottomSheetEventSC.stream;
    // Map zoom events and move events are triggered often. Throttle the call to limit rebuilds
    debounce = Debounce(
      const Duration(milliseconds: 300),
    );
  }

  @override
  void dispose() {
    debounce.dispose();
    super.dispose();
  }

  void reloadAssetsInBound(
    Set<AssetMarkerData>? assetMarkers, {
    bool forceReload = false,
  }) {
    try {
      final bounds = isMapReady ? mapController.bounds : null;
      if (bounds != null) {
        final oldAssetsInBounds = assetsInBounds.toSet();
        assetsInBounds =
            assetMarkers?.where((e) => bounds.contains(e.point)).toSet() ?? {};
        final shouldReload = forceReload ||
            assetsInBounds.difference(oldAssetsInBounds).isNotEmpty ||
            assetsInBounds.length != oldAssetsInBounds.length;
        if (shouldReload) {
          mapPageEventSC.add(
            MapPageAssetsInBoundUpdated(
              assetsInBounds.map((e) => e.asset).toList(),
            ),
          );
        }
      }
    } finally {
      // Consume all error
    }
  }

  void openAssetInViewer(Asset asset) {
    context.pushRoute(
      GalleryViewerRoute(
        initialIndex: 0,
        loadAsset: (index) => asset,
        totalAssets: 1,
        heroOffset: 0,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final log = Logger("MapService");
    final isDarkTheme =
        ref.watch(mapStateNotifier.select((state) => state.isDarkTheme));
    final ValueNotifier<Set<AssetMarkerData>> mapMarkerData =
        useState(<AssetMarkerData>{});
    final ValueNotifier<AssetMarkerData?> closestAssetMarker = useState(null);
    final selectionEnabledHook = useState(false);
    final selectedAssets = useState(<Asset>{});
    final showLoadingIndicator = useState(false);
    final refetchMarkers = useState(true);
    final isLoading =
        ref.watch(mapStateNotifier.select((state) => state.isLoading));
    final maxZoom = ref.read(mapStateNotifier.notifier).maxZoom;
    final zoomLevel = math.min(maxZoom, 14.0);

    if (refetchMarkers.value) {
      mapMarkerData.value = ref.watch(mapMarkersProvider).when(
            skipLoadingOnRefresh: false,
            error: (error, stackTrace) {
              log.warning(
                "Cannot get map markers ${error.toString()}",
                error,
                stackTrace,
              );
              showLoadingIndicator.value = false;
              return {};
            },
            loading: () {
              showLoadingIndicator.value = true;
              return {};
            },
            data: (data) {
              showLoadingIndicator.value = false;
              refetchMarkers.value = false;
              closestAssetMarker.value = null;
              debounce(
                () => reloadAssetsInBound(
                  mapMarkerData.value,
                  forceReload: true,
                ),
              );
              return data;
            },
          );
    }

    ref.listen(mapStateNotifier, (previous, next) {
      bool shouldRefetch =
          previous?.showFavoriteOnly != next.showFavoriteOnly ||
              previous?.relativeTime != next.relativeTime ||
              previous?.includeArchived != next.includeArchived;
      if (shouldRefetch) {
        refetchMarkers.value = shouldRefetch;
        ref.invalidate(mapMarkersProvider);
      }
    });

    void onZoomToAssetEvent(Asset? assetInBottomSheet) {
      if (assetInBottomSheet != null) {
        final mapMarker = mapMarkerData.value
            .firstWhereOrNull((e) => e.asset.id == assetInBottomSheet.id);
        if (mapMarker != null) {
          LatLng? newCenter = mapController.centerBoundsWithPadding(
            mapMarker.point,
            const Offset(0, -120),
            zoomLevel: zoomLevel,
          );
          if (newCenter != null) {
            forceAssetUpdate = true;
            mapController.move(newCenter, zoomLevel);
          }
        }
      }
    }

    void onZoomToLocation() async {
      try {
        bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
        if (!serviceEnabled) {
          showDialog(
            context: context,
            builder: (context) => Theme(
              data: isDarkTheme ? immichDarkTheme : immichLightTheme,
              child: LocationServiceDisabledDialog(),
            ),
          );
          return;
        }

        LocationPermission permission = await Geolocator.checkPermission();
        bool shouldRequestPermission = false;

        if (permission == LocationPermission.denied) {
          shouldRequestPermission = await showDialog(
            context: context,
            builder: (context) => Theme(
              data: isDarkTheme ? immichDarkTheme : immichLightTheme,
              child: LocationPermissionDisabledDialog(),
            ),
          );
          if (shouldRequestPermission) {
            permission = await Geolocator.requestPermission();
          }
        }

        if (permission == LocationPermission.denied ||
            permission == LocationPermission.deniedForever) {
          // Open app settings only if you did not request for permission before
          if (permission == LocationPermission.deniedForever &&
              !shouldRequestPermission) {
            await Geolocator.openAppSettings();
          }
          return;
        }

        Position currentUserLocation = await Geolocator.getCurrentPosition(
          desiredAccuracy: LocationAccuracy.medium,
          timeLimit: const Duration(seconds: 5),
        );

        forceAssetUpdate = true;
        mapController.move(
          LatLng(currentUserLocation.latitude, currentUserLocation.longitude),
          zoomLevel,
        );
      } catch (error) {
        log.severe(
          "Cannot get user's current location due to ${error.toString()}",
        );
        if (context.mounted) {
          ImmichToast.show(
            context: context,
            gravity: ToastGravity.BOTTOM,
            toastType: ToastType.error,
            msg: "map_cannot_get_user_location".tr(),
          );
        }
      }
    }

    void handleBottomSheetEvents(dynamic event) {
      if (event is MapPageBottomSheetScrolled) {
        final assetInBottomSheet = event.asset;
        if (assetInBottomSheet != null) {
          final mapMarker = mapMarkerData.value
              .firstWhereOrNull((e) => e.asset.id == assetInBottomSheet.id);
          closestAssetMarker.value = mapMarker;
          if (mapMarker != null && mapController.zoom >= 5) {
            LatLng? newCenter = mapController.centerBoundsWithPadding(
              mapMarker.point,
              const Offset(0, -120),
            );
            if (newCenter != null) {
              mapController.move(
                newCenter,
                mapController.zoom,
              );
            }
          }
        }
      } else if (event is MapPageZoomToAsset) {
        onZoomToAssetEvent(event.asset);
      } else if (event is MapPageZoomToLocation) {
        onZoomToLocation();
      }
    }

    useEffect(
      () {
        final bottomSheetEventSubscription =
            bottomSheetEventStream.listen(handleBottomSheetEvents);
        return bottomSheetEventSubscription.cancel;
      },
      [bottomSheetEventStream],
    );

    void handleMapTapEvent(LatLng tapPosition) {
      const d = Distance();
      final assetsInBoundsList = assetsInBounds.toList();
      assetsInBoundsList.sort(
        (a, b) => d
            .distance(a.point, tapPosition)
            .compareTo(d.distance(b.point, tapPosition)),
      );
      // First asset less than the threshold from the tap point
      final nearestAsset = assetsInBoundsList.firstWhereOrNull(
        (element) =>
            d.distance(element.point, tapPosition) <
            mapController.getTapThresholdForZoomLevel(),
      );
      // Reset marker if no assets are near the tap point
      if (nearestAsset == null && closestAssetMarker.value != null) {
        selectionEnabledHook.value = false;
        mapPageEventSC.add(
          const MapPageOnTapEvent(),
        );
      }
      closestAssetMarker.value = nearestAsset;
    }

    void onMapEvent(MapEvent mapEvent) {
      if (mapEvent is MapEventMove || mapEvent is MapEventDoubleTapZoom) {
        if (forceAssetUpdate ||
            mapEvent.source != MapEventSource.mapController) {
          debounce(() {
            if (selectionEnabledHook.value) {
              selectionEnabledHook.value = false;
            }
            reloadAssetsInBound(
              mapMarkerData.value,
              forceReload: forceAssetUpdate,
            );
            forceAssetUpdate = false;
          });
        }
      } else if (mapEvent is MapEventTap) {
        handleMapTapEvent(mapEvent.tapPosition);
      }
    }

    void onShareAsset() {
      handleShareAssets(ref, context, selectedAssets.value.toList());
      selectionEnabledHook.value = false;
    }

    void onFavoriteAsset() async {
      showLoadingIndicator.value = true;
      try {
        await handleFavoriteAssets(ref, context, selectedAssets.value.toList());
      } finally {
        showLoadingIndicator.value = false;
        selectionEnabledHook.value = false;
        refetchMarkers.value = true;
      }
    }

    void onArchiveAsset() async {
      showLoadingIndicator.value = true;
      try {
        await handleArchiveAssets(ref, context, selectedAssets.value.toList());
      } finally {
        showLoadingIndicator.value = false;
        selectionEnabledHook.value = false;
        refetchMarkers.value = true;
      }
    }

    void selectionListener(bool isMultiSelect, Set<Asset> selection) {
      selectionEnabledHook.value = isMultiSelect;
      selectedAssets.value = selection;
    }

    final markerLayer = MarkerLayer(
      markers: [
        if (closestAssetMarker.value != null)
          AssetMarker(
            remoteId: closestAssetMarker.value!.asset.remoteId!,
            anchorPos: AnchorPos.align(AnchorAlign.top),
            point: closestAssetMarker.value!.point,
            width: 100,
            height: 100,
            builder: (ctx) => GestureDetector(
              onTap: () => openAssetInViewer(closestAssetMarker.value!.asset),
              child: AssetMarkerIcon(
                key: Key(closestAssetMarker.value!.asset.remoteId!),
                isDarkTheme: isDarkTheme,
                id: closestAssetMarker.value!.asset.remoteId!,
              ),
            ),
          ),
      ],
    );

    final heatMapLayer = mapMarkerData.value.isNotEmpty
        ? HeatMapLayer(
            heatMapDataSource: InMemoryHeatMapDataSource(
              data: mapMarkerData.value
                  .map(
                    (e) => WeightedLatLng(
                      LatLng(e.point.latitude, e.point.longitude),
                      1,
                    ),
                  )
                  .toList(),
            ),
            heatMapOptions: HeatMapOptions(
              radius: 60,
              layerOpacity: 0.5,
              gradient: {
                0.20: Colors.deepPurple,
                0.40: Colors.blue,
                0.60: Colors.green,
                0.95: Colors.yellow,
                1.0: Colors.deepOrange,
              },
            ),
          )
        : const SizedBox.shrink();

    return AnnotatedRegion<SystemUiOverlayStyle>(
      value: SystemUiOverlayStyle(
        statusBarColor:
            (isDarkTheme ? Colors.black : Colors.white).withOpacity(0.5),
        statusBarIconBrightness:
            isDarkTheme ? Brightness.light : Brightness.dark,
        systemNavigationBarColor:
            isDarkTheme ? Colors.grey[900] : Colors.grey[100],
        systemNavigationBarIconBrightness:
            isDarkTheme ? Brightness.light : Brightness.dark,
        systemNavigationBarDividerColor: Colors.transparent,
      ),
      child: Theme(
        // Override app theme based on map theme
        data: isDarkTheme ? immichDarkTheme : immichLightTheme,
        child: Scaffold(
          appBar: MapAppBar(
            isDarkTheme: isDarkTheme,
            selectionEnabled: selectionEnabledHook,
            selectedAssetsLength: selectedAssets.value.length,
            onShare: onShareAsset,
            onArchive: onArchiveAsset,
            onFavorite: onFavoriteAsset,
          ),
          extendBodyBehindAppBar: true,
          body: Stack(
            children: [
              if (!isLoading)
                FlutterMap(
                  mapController: mapController,
                  options: MapOptions(
                    maxBounds:
                        LatLngBounds(LatLng(-90, -180.0), LatLng(90.0, 180.0)),
                    interactiveFlags: InteractiveFlag.doubleTapZoom |
                        InteractiveFlag.drag |
                        InteractiveFlag.flingAnimation |
                        InteractiveFlag.pinchMove |
                        InteractiveFlag.pinchZoom,
                    center: LatLng(20, 20),
                    zoom: 2,
                    minZoom: 1,
                    maxZoom: maxZoom,
                    onMapReady: () {
                      isMapReady = true;
                      mapController.mapEventStream.listen(onMapEvent);
                    },
                  ),
                  children: [
                    ref.read(mapStateNotifier.notifier).getTileLayer(),
                    heatMapLayer,
                    markerLayer,
                  ],
                ),
              if (!isLoading)
                MapPageBottomSheet(
                  mapPageEventStream: mapPageEventSC.stream,
                  bottomSheetEventSC: bottomSheetEventSC,
                  selectionEnabled: selectionEnabledHook.value,
                  selectionlistener: selectionListener,
                  isDarkTheme: isDarkTheme,
                ),
              if (showLoadingIndicator.value || isLoading)
                Positioned(
                  top: context.height * 0.35,
                  left: context.width * 0.425,
                  child: const ImmichLoadingIndicator(),
                ),
            ],
          ),
        ),
      ),
    );
  }
}

class AssetMarker extends Marker {
  String remoteId;

  AssetMarker({
    super.key,
    required this.remoteId,
    super.anchorPos,
    required super.point,
    super.width = 100.0,
    super.height = 100.0,
    required super.builder,
  });
}
