import 'dart:convert';

import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';

// OSM raster tile providers: https://wiki.openstreetmap.org/wiki/Raster_tile_providers
// OSM vector tile providers: https://wiki.openstreetmap.org/wiki/Vector_tiles#Providers
// Leaflet providers preview: https://leaflet-extras.github.io/leaflet-providers/preview/
// OpenMapTiles styles: https://openmaptiles.org/styles/
// Examples: https://github.com/deckerst/aves/wiki/Custom-maps

enum EntryMapBackend {
  leaflet,
  google,
  mapbox,
}

class EntryMapStyle extends Equatable {
  final String key;
  final String? name;
  final bool isRaster;

  // Leaflet / flutter_map tile URL.
  // Not strictly a `Uri`, as it may contain templates like `{x}`, `{y}`, `{z}`.
  final String? url;

  final List<String> subdomains;
  final String? userAgent;

  // Google/mobile backend compatibility.
  final bool needMobileService;

  final bool isHeavy;

  // Backend selector.
  final EntryMapBackend backend;

  // Mapbox native style URI, e.g.
  // mapbox://styles/mapbox/satellite-streets-v12
  final String? mapboxStyleUri;

  // Future Mapbox 3D terrain flag.
  final bool mapboxTerrain;

  // Optional Mapbox initial camera values.
  final double? initialPitch;
  final double? initialBearing;

  @override
  List<Object?> get props => [
        key,
        name,
        isRaster,
        url,
        subdomains,
        userAgent,
        needMobileService,
        isHeavy,
        backend,
        mapboxStyleUri,
        mapboxTerrain,
        initialPitch,
        initialBearing,
      ];

  const EntryMapStyle({
    required this.key,
    this.name,
    this.isRaster = true,
    this.url,
    List<String>? subdomains,
    this.userAgent,
    this.needMobileService = false,
    this.isHeavy = false,
    this.backend = EntryMapBackend.leaflet,
    this.mapboxStyleUri,
    this.mapboxTerrain = false,
    this.initialPitch,
    this.initialBearing,
  }) : subdomains = subdomains ?? const ['a', 'b', 'c'];

  static EntryMapStyle? fromJson(String? jsonString) {
    if (jsonString == null || jsonString.isEmpty) return null;

    try {
      final jsonMap = jsonDecode(jsonString);

      if (jsonMap is Map<String, Object?>) {
        return _fromMap(jsonMap);
      }

      debugPrint('failed to parse style from json=$jsonString');
    } catch (error) {
      debugPrint('failed to parse style from json=$jsonString error=$error');
    }

    return null;
  }

  String toJson() => jsonEncode(_toMap());

  static EntryMapStyle _fromMap(Map<String, Object?> jsonMap) {
    final backendName = jsonMap['backend'] as String?;

    EntryMapBackend backend = EntryMapBackend.leaflet;

    if (backendName != null) {
      try {
        backend = EntryMapBackend.values.byName(backendName);
      } catch (_) {
        backend = EntryMapBackend.leaflet;
      }
    } else if (jsonMap['needMobileService'] == true) {
      // Backward compatibility for old saved Google styles.
      backend = EntryMapBackend.google;
    }

    return EntryMapStyle(
      key: jsonMap['key'] as String,
      name: jsonMap['name'] as String?,
      isRaster: jsonMap['isRaster'] as bool? ?? true,
      url: jsonMap['url'] as String?,
      subdomains: (jsonMap['subdomains'] as List?)?.cast<String>(),
      userAgent: jsonMap['userAgent'] as String?,
      needMobileService: jsonMap['needMobileService'] as bool? ?? false,
      isHeavy: jsonMap['isHeavy'] as bool? ?? false,
      backend: backend,
      mapboxStyleUri: jsonMap['mapboxStyleUri'] as String?,
      mapboxTerrain: jsonMap['mapboxTerrain'] as bool? ?? false,
      initialPitch: (jsonMap['initialPitch'] as num?)?.toDouble(),
      initialBearing: (jsonMap['initialBearing'] as num?)?.toDouble(),
    );
  }

  Map<String, Object?> _toMap() {
    return {
      'key': key,
      'name': name,
      'isRaster': isRaster,
      'url': url,
      'subdomains': subdomains,
      'userAgent': userAgent,
      'needMobileService': needMobileService,
      'isHeavy': isHeavy,
      'backend': backend.name,
      'mapboxStyleUri': mapboxStyleUri,
      'mapboxTerrain': mapboxTerrain,
      'initialPitch': initialPitch,
      'initialBearing': initialBearing,
    };
  }
}

class EntryMapStyles {
  // ------------------------------------------------------------
  // Google
  // ------------------------------------------------------------

  static const googleNormal = EntryMapStyle(
    key: 'googleNormal',
    needMobileService: true,
    isHeavy: true,
    backend: EntryMapBackend.google,
  );

  static const googleHybrid = EntryMapStyle(
    key: 'googleHybrid',
    needMobileService: true,
    isHeavy: true,
    backend: EntryMapBackend.google,
  );

  static const googleTerrain = EntryMapStyle(
    key: 'googleTerrain',
    needMobileService: true,
    isHeavy: true,
    backend: EntryMapBackend.google,
  );

  // ------------------------------------------------------------
  // Vector / OpenMapTiles
  // ------------------------------------------------------------

  static const osmLiberty = EntryMapStyle(
    key: 'osmLiberty',
    isRaster: false,
    backend: EntryMapBackend.leaflet,
  );

  // ------------------------------------------------------------
  // Raster / Leaflet
  // ------------------------------------------------------------

  static const openTopoMap = EntryMapStyle(
    key: 'openTopoMap',
    url: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
    backend: EntryMapBackend.leaflet,
  );

  static const osmHot = EntryMapStyle(
    key: 'osmHot',
    url: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
    backend: EntryMapBackend.leaflet,
  );

  static const stamenWatercolor = EntryMapStyle(
    key: 'stamenWatercolor',
    url: 'https://watercolormaps.collection.cooperhewitt.org/tile/watercolor/{z}/{x}/{y}.jpg',
    backend: EntryMapBackend.leaflet,
  );

  // ------------------------------------------------------------
  // Mapbox
  // ------------------------------------------------------------

  static const mapboxSatelliteStreets = EntryMapStyle(
    key: 'mapboxSatelliteStreets',
    backend: EntryMapBackend.mapbox,
    mapboxStyleUri: 'mapbox://styles/mapbox/satellite-streets-v12',
    isHeavy: true,
  );

  static const mapboxSatellite = EntryMapStyle(
    key: 'mapboxSatellite',
    backend: EntryMapBackend.mapbox,
    mapboxStyleUri: 'mapbox://styles/mapbox/satellite-v9',
    isHeavy: true,
  );

  static const mapboxStreets = EntryMapStyle(
    key: 'mapboxStreets',
    backend: EntryMapBackend.mapbox,
    mapboxStyleUri: 'mapbox://styles/mapbox/streets-v12',
    isHeavy: true,
  );

  static const mapboxOutdoors = EntryMapStyle(
    key: 'mapboxOutdoors',
    backend: EntryMapBackend.mapbox,
    mapboxStyleUri: 'mapbox://styles/mapbox/outdoors-v12',
    isHeavy: true,
  );

  static const mapboxTerrain3d = EntryMapStyle(
    key: 'mapboxTerrain3d',
    backend: EntryMapBackend.mapbox,
    mapboxStyleUri: 'mapbox://styles/mapbox/satellite-streets-v12',
    mapboxTerrain: true,
    initialPitch: 65,
    initialBearing: 0,
    isHeavy: true,
  );

  static List<EntryMapStyle> mapboxStyles = [
    mapboxSatelliteStreets,
    mapboxSatellite,
    mapboxStreets,
    mapboxOutdoors,
    mapboxTerrain3d,
  ];

  // Default styles available in the standard map style selector.
  //
  // Important:
  // Mapbox styles are included here so `settings.mapStyle` validation/fallback
  // can recognize them as valid default styles.
  static List<EntryMapStyle> baseStyles = [
    // Vector / OpenMapTiles
    osmLiberty,

    // Raster / Leaflet
    openTopoMap,
    osmHot,
    stamenWatercolor,

    // Mapbox
    ...mapboxStyles,
  ];
}
