import 'dart:convert';
import 'dart:math';

import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:html/parser.dart';
import 'package:inspireui/inspireui.dart';
import 'package:inspireui/utils/regex_utils.dart';
import 'package:intl/intl.dart';
import 'package:responsive_builder/responsive_builder.dart';
import 'package:share_plus/share_plus.dart';
import 'package:universal_platform/universal_platform.dart';
import 'package:url_launcher/url_launcher.dart';

import '../../entities/currency.dart';
import '../constants/country_phone_codes.dart';
import '../constants/general.dart';
import '../extensions/string_ext.dart';
import '../localization/localization.dart';

class Tools {
  static bool isBuilder = false;
  static List<String> externalAppURLs = [];
  static List<String> externalNonBrowserAppURLs = [];
  static List<Currency> currenciesData = [];

  /// Setup the tools with the given parameters.
  /// Required to call back if the application changes the config in env.dart
  static void setup({
    required List<String> externalAppURLs,
    required List<String> externalNonBrowserAppURLs,
    required List<Currency> currenciesData,
    required bool isBuilder,
  }) {
    Tools.externalAppURLs = externalAppURLs;
    Tools.externalNonBrowserAppURLs = externalNonBrowserAppURLs;
    Tools.currenciesData = currenciesData;
    Tools.isBuilder = isBuilder;
  }

  static double? formatDouble(num? value) => value == null ? null : value * 1.0;
  static bool isDisplayDesktop(BuildContext context) {
    if (isBuilder) {
      return false;
    }

    final deviceType = getDeviceType(MediaQuery.sizeOf(context));
    return !isMobile &&
        (deviceType == DeviceScreenType.desktop ||
            (deviceType == DeviceScreenType.tablet &&
                MediaQuery.orientationOf(context) == Orientation.landscape));
  }

  static bool isDisplayTablet(BuildContext context) {
    final deviceType = getDeviceType(MediaQuery.sizeOf(context));
    return isMobile &&
        ((deviceType == DeviceScreenType.desktop ||
                deviceType == DeviceScreenType.tablet) &&
            MediaQuery.orientationOf(context) == Orientation.landscape);
  }

  /// check tablet screen
  static bool isTablet(MediaQueryData query) {
    if (isBuilder) {
      return false;
    }

    if (kIsWeb) {
      return true;
    }

    if (UniversalPlatform.isWindows || UniversalPlatform.isMacOS) {
      return false;
    }

    var size = query.size;
    var diagonal = sqrt(
      (size.width * size.width) + (size.height * size.height),
    );
    var isTablet = diagonal > 1100.0;
    return isTablet;
  }

  static bool isPhone(MediaQueryData query) {
    return isMobile && !isTablet(query);
  }

  static Future<List<dynamic>> loadStatesByCountry(String country) async {
    try {
      // load local config
      var path = 'lib/config/states/state_${country.toLowerCase()}.json';
      //if use loadString can't catch file is not exists
      final data = await rootBundle.load(path);
      String? appJson;
      if (data.lengthInBytes < 50 * 1024) {
        appJson = utf8.decode(data.buffer.asUint8List());
      } else {
        String utf8decode(ByteData data) {
          return utf8.decode(data.buffer.asUint8List());
        }

        appJson = utf8decode(data);
      }
      return List<dynamic>.from(jsonDecode(appJson));
    } catch (e) {
      return [];
    }
  }

  static dynamic getValueByKey(Map<String, dynamic>? json, String? key) {
    if (key == null) return null;
    try {
      List keys = key.split('.');
      Map<String, dynamic>? data = Map<String, dynamic>.from(json!);
      if (keys[0] == '_links') {
        var links = json['listing_data']['_links'] ?? [];
        for (var item in links) {
          if (item['network'] == keys[keys.length - 1]) return item['url'];
        }
      }
      for (var i = 0; i < keys.length - 1; i++) {
        if (data![keys[i]] is Map) {
          data = data[keys[i]];
        } else {
          return null;
        }
      }
      if (data![keys[keys.length - 1]].toString().isEmpty) return null;
      return data[keys[keys.length - 1]];
    } catch (err, trace) {
      printError(err, trace);
      return 'Error when mapping $key';
    }
  }

  static void showSnackBar(
    ScaffoldMessengerState? state,
    String? message, {
    Duration duration = const Duration(milliseconds: 4000),
  }) {
    if (state != null && message != null) {
      state.showSnackBar(SnackBar(content: Text(message), duration: duration));
    }
  }

  static Future<void> launchMaps({
    dynamic lat,
    dynamic long,
    dynamic address,
  }) async {
    if (address != null && '$address'.trim().isNotEmpty) {
      /// Launch map query using address.
      final url =
          'www.google.com/maps/search/?api=1&'
          'query=${Uri.encodeQueryComponent('$address'.trim())}';

      var appUrl = 'comgooglemapsurl://$url'.toUri()!;
      try {
        if (await canLaunchUrl(appUrl)) {
          await launchUrl(appUrl, mode: LaunchMode.externalApplication);

          // Stop here if we successfully launched Google Maps.
          return;
        }
      } catch (_) {}

      await Tools.launchURL(
        'https://$url',
        mode: LaunchMode.externalApplication,
      );

      return;
    }

    return launchMapsURL(lat, long);
  }

  static Future<void> launchMapsURL(dynamic lat, dynamic long) async {
    final url =
        'www.google.com/maps/search/?api=1&'
        'query=${Uri.encodeQueryComponent('$lat'.trim())},'
        '${Uri.encodeQueryComponent('$long'.trim())}';

    /// iOS only: Attempt to launch Google Maps instead of Apple Maps.
    if (isIos) {
      try {
        var appUrl = 'comgooglemapsurl://$url'.toUri()!;
        if (await canLaunchUrl(appUrl)) {
          await launchUrl(appUrl, mode: LaunchMode.externalApplication);

          // Stop here if we successfully launched Google Maps.
          return;
        }
      } catch (_) {}
    }

    await Tools.launchURL('https://$url', mode: LaunchMode.externalApplication);
  }

  /// Launches the given url.
  ///
  /// Returns `true` if the url was launched, `false` otherwise.
  static Future<bool> launchURL(
    String? originUrl, {
    LaunchMode? mode,
    Map<String, String> headers = const <String, String>{},
  }) async {
    final url = prepareURL(originUrl) ?? '';
    if (url.isEmpty) {
      return false;
    }
    if (url.startsWith('about:')) {
      return false;
    }

    final uri = Uri.tryParse(url);
    if (uri == null) {
      return false;
    }

    if (mode == null && needToOpenExternalNonBrowserApp(url)) {
      mode = LaunchMode.externalNonBrowserApplication;
    }

    if (mode == null && needToOpenExternalApp(url)) {
      mode = LaunchMode.externalApplication;
    }

    // Check if can launch url in external app, not external browser.
    // If not be able to launch, try to open it in external app
    if (mode == LaunchMode.externalNonBrowserApplication) {
      try {
        final canLaunch = await canLaunchUrl(uri);
        if (!canLaunch) {
          mode = LaunchMode.externalApplication;
        }
      } catch (_) {
        mode = LaunchMode.externalApplication;
      }
    }

    var isLaunched = await launchUrl(
      uri,
      mode: mode ?? LaunchMode.platformDefault,
      webViewConfiguration: WebViewConfiguration(headers: headers),
    );

    if (!isLaunched) {
      // Try again if the previous mode was `externalNonBrowserApplication`
      if (mode == LaunchMode.externalNonBrowserApplication) {
        printError(
          'Could not launch $originUrl with mode ${mode?.name}. Try again!',
        );
        mode = LaunchMode.externalApplication;
        isLaunched = await launchUrl(
          uri,
          mode: mode,
          webViewConfiguration: WebViewConfiguration(headers: headers),
        );
      }
    }

    if (!isLaunched) {
      // Try again if the previous mode was `externalApplication`
      if (mode == LaunchMode.externalApplication) {
        printError(
          'Could not launch $originUrl with mode ${mode?.name}. Try again!',
        );
        mode = LaunchMode.platformDefault;
        isLaunched = await launchUrl(
          uri,
          mode: mode,
          webViewConfiguration: WebViewConfiguration(headers: headers),
        );
      }
    }

    if (!isLaunched) {
      printError('Could not launch $originUrl');
    }

    return isLaunched;
  }

  /// If this url can open external app, it will redirect to external app
  static bool needToOpenExternalNonBrowserApp(String? url) {
    if (url == null || url.isEmpty) {
      return false;
    }

    for (var app in externalNonBrowserAppURLs) {
      if (url.startsWith(app)) {
        return true;
      }
    }
    return false;
  }

  /// If this url can open external app/browser, it will redirect to external
  /// app/browser
  static bool needToOpenExternalApp(String? url) {
    if (url == null || url.isEmpty) {
      return false;
    }

    final isNeedToOpenExternalNonBrowserApp = needToOpenExternalNonBrowserApp(
      url,
    );

    if (isNeedToOpenExternalNonBrowserApp) {
      return true;
    }

    for (var app in externalAppURLs) {
      if (url.startsWith('http') && url.isTheSameDomain(app)) {
        return true;
      }
    }
    return false;
  }

  static String? prepareURL(String? url) {
    if (url == null) {
      return null;
    }

    final uri = Uri.tryParse(url);
    if (uri == null) {
      return null;
    }

    if (!url.startsWith('http') && uri.scheme.isEmpty) {
      return 'https://$url';
    }

    if (url.startsWith('intent://') && url.contains('scheme=')) {
      final intentInfo = url.substring(url.indexOf('scheme='));
      final scheme = intentInfo.substring(
        intentInfo.indexOf('scheme=') + 7,
        intentInfo.indexOf(';'),
      );
      return url.replaceFirst('intent://', '$scheme://');
    }

    return url;
  }

  static Future<dynamic> parseJsonFromAssets(String assetsPath) async {
    return rootBundle.loadString(assetsPath).then(jsonDecode);
  }

  static void hideKeyboard(BuildContext context) {
    FocusScope.of(context).requestFocus(FocusNode());
  }

  static bool isRTL(BuildContext context) {
    var languageCode = Localizations.localeOf(context).languageCode;
    return Bidi.isRtlLanguage(languageCode) ||
        F.of(context).isRTL(languageCode);
  }

  static String? convertDateTime(DateTime date) {
    return DateFormat.yMd().add_jm().format(date).replaceAll(' ', ' ');
  }

  static String? getTimeWith2Digit(String time) {
    return time.length == 1 ? '0$time' : time;
  }

  static String getFileNameFromUrl(String url) {
    final nameFromUrlRegExp = RegExp(RegexUtils.fileNameFromUrl);
    if (RegexUtils.check(url, RegexUtils.url) &&
        nameFromUrlRegExp.hasMatch(url)) {
      return nameFromUrlRegExp.stringMatch(url)!;
    }
    return url;
  }

  static String removeHTMLTags(String value) {
    try {
      final document = parse(value);
      if (document.body == null) {
        return value;
      }
      final parsedString = parse(document.body!.text).documentElement!.text;
      return parsedString;
    } catch (err, trace) {
      printError(err, trace);
    }
    return value;
  }

  static double calculateDistance(
    double lat1,
    double lon1,
    double lat2,
    double lon2,
  ) {
    var p = 0.017453292519943295;
    var c = cos;
    var a =
        0.5 -
        c((lat2 - lat1) * p) / 2 +
        c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p)) / 2;
    var distance = 12742 * asin(sqrt(a));
    return distance.toInt().toDouble();
  }

  static dynamic formatDate(String date) {
    var dateFormat = DateFormat(DateFormat.YEAR_MONTH_DAY);
    return dateFormat.format(DateTime.tryParse(date) ?? DateTime.now());
  }

  static dynamic formatDateToLocal(String date) {
    var dateFormat = DateFormat(DateFormat.YEAR_MONTH_DAY);
    return dateFormat.format(
      DateTime.tryParse(date)?.toLocal() ?? DateTime.now(),
    );
  }

  static String? getCurrencyCode(String? currency) {
    var currencies = currenciesData.toList();
    if (currencies.isNotEmpty) {
      var item = currencies.firstWhere(
        (element) => element.currencyDisplay == currency,
      );
      return item.currencyCode;
    }
    return currency;
  }

  static FontWeight getFontWeight(
    dynamic fontWeight, {
    FontWeight? defaultValue,
  }) {
    var fontWeightVal = '$fontWeight';
    switch (fontWeightVal) {
      case '100':
        return FontWeight.w100;
      case '200':
        return FontWeight.w200;
      case '300':
        return FontWeight.w300;
      case '400':
        return FontWeight.w400;
      case '500':
        return FontWeight.w500;
      case '600':
        return FontWeight.w600;
      case '700':
        return FontWeight.w700;
      case '800':
        return FontWeight.w800;
      case '900':
        return FontWeight.w900;
      default:
        return defaultValue ?? FontWeight.w400;
    }
  }

  static AlignmentGeometry getAlignment(
    String? alignment, {
    AlignmentGeometry? defaultValue,
  }) {
    switch (alignment) {
      case 'left':
      case 'centerLeft':
      case 'centerStart':
      case 'start':
        return AlignmentDirectional.centerStart;
      case 'right':
      case 'centerRight':
      case 'centerEnd':
      case 'end':
        return AlignmentDirectional.centerEnd;
      case 'topLeft':
      case 'topStart':
        return AlignmentDirectional.topStart;
      case 'topRight':
      case 'topEnd':
        return AlignmentDirectional.topEnd;
      case 'bottomLeft':
      case 'bottomStart':
        return AlignmentDirectional.bottomStart;
      case 'bottomRight':
      case 'bottomEnd':
        return AlignmentDirectional.bottomEnd;
      case 'bottom':
      case 'bottomCenter':
        return AlignmentDirectional.bottomCenter;
      case 'top':
      case 'topCenter':
        return AlignmentDirectional.topCenter;
      case 'center':
        return AlignmentDirectional.center;
      default:
        return defaultValue ?? AlignmentDirectional.center;
    }
  }

  static String getPhoneCodeByPhoneNumber(String phoneNumber) {
    final item = countryPhoneCodes.firstWhereOrNull(
      (e) => phoneNumber.contains(e['dial_code']),
    );
    if (item != null) {
      return item['dial_code'];
    }
    return '';
  }

  static Future<void> share({
    String? text,
    String? subject,
    List<XFile>? files,
    required BuildContext? context,
  }) async {
    Rect? shareOrigin;

    if (context != null) {
      final renderObject = context.findRenderObject();
      if (renderObject is RenderBox) {
        final offset = renderObject.localToGlobal(Offset.zero);
        shareOrigin = offset & renderObject.size;
      }
    }

    await SharePlus.instance.share(
      ShareParams(
        text: text,
        subject: subject,
        files: files,
        sharePositionOrigin: shareOrigin,
      ),
    );
  }
}
