content: import 'dart:async';
  import 'dart:io';
  
  import 'package:flutter/foundation.dart';
  import 'package:nearby_service/nearby_service.dart';
  import 'package:nearby_service_example_full/utils/files_saver.dart';
  
  import 'app_state.dart';
  
  class AppService extends ChangeNotifier {
    late final _nearbyService = NearbyService.getInstance();
  
    AppState state = AppState.idle;
    List<NearbyDevice>? peers;
    NearbyDevice? connectedDevice;
    NearbyDeviceInfo? currentDeviceInfo;
    NearbyConnectionAndroidInfo? _connectionAndroidInfo;
    CommunicationChannelState _communicationChannelState =
        CommunicationChannelState.notConnected;
    bool _isDarwinBrowser = true;
  
    String platformVersion = 'Unknown';
    String platformModel = 'Unknown';
  
    StreamSubscription? _peersSubscription;
    StreamSubscription? _connectedDeviceSubscription;
    StreamSubscription? _connectionInfoSubscription;
  
    final filesLoadings = <String, int>{};
  
    @override
    void dispose() {
      stopListeningAll();
      super.dispose();
    }
  
    Future<void> getPlatformInfo() async {
      platformVersion = await _nearbyService.getPlatformVersion() ?? 'Unknown';
      platformModel = await _nearbyService.getPlatformModel() ?? 'Unknown';
      notifyListeners();
    }
  
    Future<String> getSavedDarwinDeviceName() async {
      return (await _nearbyService.darwin?.getSavedDeviceName()) ?? platformModel;
    }
  
    Future<void> initialize(String? darwinDeviceName) async {
      try {
        await _nearbyService.initialize(
          data: NearbyInitializeData(darwinDeviceName: darwinDeviceName),
        );
        _nearbyService.darwin?.getIsBrowserStream().listen((event) {
          _isDarwinBrowser = event;
        });
        startListeningCommunicationChannelState();
        updateState(
          Platform.isAndroid ? AppState.permissions : AppState.selectClientType,
        );
      } catch (e, s) {
        _log(e, s);
      } finally {
        notifyListeners();
      }
    }
  
    Future<void> getCurrentDeviceInfo() async {
      try {
        currentDeviceInfo = await _nearbyService.getCurrentDeviceInfo();
      } catch (e, s) {
        _log(e, s);
      }
    }
  
    Future<void> requestPermissions() async {
      try {
        final result = await _nearbyService.android?.requestPermissions();
        if (result ?? false) {
          updateState(AppState.checkServices);
        }
      } catch (e, s) {
        _log(e, s);
      }
    }
  
    Future<bool> checkWifiService() async {
      final result = await _nearbyService.android?.checkWifiService();
      if (result ?? false) {
        updateState(AppState.readyToDiscover);
        startListeningConnectionInfo();
        return true;
      }
      return false;
    }
  
    Future<void> openServicesSettings() async {
      await _nearbyService.openServicesSettings();
    }
  
    void setIsBrowser({required bool value}) {
      _nearbyService.darwin?.setIsBrowser(value: value);
      updateState(AppState.readyToDiscover);
    }
  
    Future<bool> hasRunningJobs() async {
      try {
        final result = await _nearbyService.getPeers();
        // if one of devices is connecting, service is busy (android only)
        if (result.any(
          (element) => element.status == NearbyDeviceStatus.connecting,
        )) {
          if (kDebugMode) {
            print('Service has already running jobs');
          }
          return true;
        }
        return false;
      } catch (e, s) {
        _log(e, s);
        return false;
      }
    }
  
    Future<void> discover() async {
      try {
        await getCurrentDeviceInfo();
        final hasRunning = await hasRunningJobs();
        if (hasRunning) {
          updateState(AppState.discoveringPeers);
        } else {
          final result = await _nearbyService.discover();
          if (result) {
            updateState(AppState.discoveringPeers);
          }
        }
      } on NearbyServiceBusyException catch (_) {
        _logBusyException();
      } catch (e, s) {
        _log(e, s);
      }
    }
  
    Future<void> stopDiscovery() async {
      try {
        final hasRunning = await hasRunningJobs();
        if (hasRunning) {
          await cancelConnect();
        }
        final result = await _nearbyService.stopDiscovery();
        if (result) {
          updateState(AppState.readyToDiscover);
        }
      } on NearbyServiceBusyException catch (_) {
        _logBusyException();
      } catch (e, s) {
        _log(e, s);
      }
    }
  
    Future<void> connect(NearbyDevice device) async {
      try {
        await _nearbyService.connectById(device.info.id);
      } on NearbyServiceBusyException catch (_) {
        _logBusyException();
      } catch (e, s) {
        _log(e, s);
      }
      notifyListeners();
    }
  
    Future<void> disconnect([NearbyDevice? device]) async {
      try {
        await _nearbyService.disconnectById(device?.info.id);
      } on NearbyServiceBusyException catch (_) {
        _logBusyException();
      } catch (e, s) {
        _log(e, s);
      } finally {
        await stopListeningAll();
      }
      notifyListeners();
    }
  
    Future<void> cancelConnect() async {
      try {
        await _nearbyService.android?.cancelLastConnectionProcess();
      } on NearbyServiceBusyException catch (_) {
        _logBusyException();
      } catch (e, s) {
        _log(e, s);
      }
      notifyListeners();
    }
  
    Future<void> stopListeningAll() async {
      await endCommunicationChannel();
      await stopListeningConnectedDevice();
      await stopListeningPeers();
      await stopListeningConnectionInfo();
      await stopDiscovery();
    }
  
    void updateState(AppState state, {bool shouldNotify = true}) {
      this.state = state;
      if (shouldNotify) {
        notifyListeners();
      }
    }
  
    void _notify() => notifyListeners();
  }
  
  extension GettersExtension on AppService {
    CommunicationChannelState get communicationChannelState =>
        _communicationChannelState;
  
    bool get isDarwinBrowser {
      return _isDarwinBrowser;
    }
  
    bool? get isAndroidGroupOwner {
      return _connectionAndroidInfo?.isGroupOwner;
    }
  }
  
  extension ConnectionInfoExtension on AppService {
    void startListeningConnectionInfo() {
      try {
        _connectionInfoSubscription =
            _nearbyService.android?.getConnectionInfoStream().listen(
          (event) async {
            _connectionAndroidInfo = event;
            _notify();
          },
        );
      } catch (e, s) {
        _log(e, s);
      }
      _notify();
    }
  
    void startListeningCommunicationChannelState() {
      try {
        _connectionInfoSubscription =
            _nearbyService.getCommunicationChannelStateStream().listen(
          (event) async {
            _communicationChannelState = event;
            _notify();
          },
        );
      } catch (e, s) {
        _log(e, s);
      }
      _notify();
    }
  
    Future<void> stopListeningConnectionInfo() async {
      await _connectionInfoSubscription?.cancel();
      _connectionInfoSubscription = null;
    }
  }
  
  extension PeersExtension on AppService {
    Future<void> startListeningPeers() async {
      try {
        _peersSubscription = _nearbyService.getPeersStream().listen(
          (event) {
            peers = event;
            _notify();
          },
        );
        updateState(AppState.streamingPeers);
      } catch (e, s) {
        _log(e, s);
      }
    }
  
    Future<void> stopListeningPeers() async {
      await _peersSubscription?.cancel();
      peers = null;
      updateState(AppState.discoveringPeers);
    }
  }
  
  extension ConnectedDeviceExtension on AppService {
    Future<void> startListeningConnectedDevice(NearbyDevice device) async {
      updateState(AppState.loadingConnection);
      try {
        _connectedDeviceSubscription =
            _nearbyService.getConnectedDeviceStreamById(device.info.id).listen(
          (event) async {
            final wasConnected = connectedDevice?.status.isConnected ?? false;
            final nowConnected = event?.status.isConnected ?? false;
            if (wasConnected && !nowConnected) {
              stopListeningAll();
              return;
            }
            connectedDevice = event;
            if (connectedDevice != null &&
                state != AppState.connected &&
                state != AppState.communicationChannelCreated) {
              updateState(AppState.connected);
            }
            _notify();
          },
        );
      } catch (e) {
        updateState(AppState.streamingPeers, shouldNotify: false);
      }
      _notify();
    }
  
    Future<void> stopListeningConnectedDevice() async {
      await _connectedDeviceSubscription?.cancel();
      await _nearbyService.endCommunicationChannel();
      _connectedDeviceSubscription = null;
      connectedDevice = null;
      _notify();
    }
  }
  
  extension CommunicationChannelExtension on AppService {
    Future<void> startCommunicationChannel({
      ValueChanged<ReceivedNearbyMessage>? listener,
      ValueChanged<ReceivedNearbyFilesPack>? onFilesSaved,
    }) async {
      final messagesListener = NearbyServiceMessagesListener(
        onCreated: () {
          updateState(AppState.communicationChannelCreated);
        },
        onData: (event) {
          listener?.call(event);
        },
        onError: (e, [StackTrace? s]) {
          stopListeningAll();
        },
      );
      final filesListener = NearbyServiceFilesListener(
        onData: (event) async {
          final files = await FilesSaver.savePack(event);
          onFilesSaved?.call(
            ReceivedNearbyFilesPack(
              id: event.id,
              sender: event.sender,
              files: files,
            ),
          );
        },
      );
  
      await _nearbyService.startCommunicationChannel(
        NearbyCommunicationChannelData(
          connectedDevice!.info.id,
          messagesListener: messagesListener,
          filesListener: filesListener,
        ),
      );
    }
  
    Future<void> endCommunicationChannel() async {
      try {
        await _nearbyService.endCommunicationChannel();
      } catch (e, s) {
        _log(e, s);
      }
      _notify();
    }
  }
  
  extension MessagingExtension on AppService {
    void sendTextRequest(String message) {
      if (connectedDevice == null) return;
      _nearbyService.send(
        OutgoingNearbyMessage(
          content: NearbyMessageTextRequest.create(value: message),
          receiver: connectedDevice!.info,
        ),
      );
    }
  
    void sendFilesRequest(List<String> paths) {
      if (connectedDevice == null) return;
      _nearbyService.send(
        OutgoingNearbyMessage(
          content: NearbyMessageFilesRequest.create(
            files: [
              ...paths.map((e) => NearbyFileInfo(path: e)),
            ],
          ),
          receiver: connectedDevice!.info,
        ),
      );
    }
  
    void sendTextResponse(String requestId) {
      if (connectedDevice == null) return;
  
      _nearbyService.send(
        OutgoingNearbyMessage(
          receiver: connectedDevice!.info,
          content: NearbyMessageTextResponse(id: requestId),
        ),
      );
    }
  
    void sendFilesResponse(
      NearbyMessageFilesRequest request, {
      required bool isAccepted,
    }) {
      if (connectedDevice == null) return;
      _nearbyService.send(
        OutgoingNearbyMessage(
          receiver: connectedDevice!.info,
          content: NearbyMessageFilesResponse(
            id: request.id,
            isAccepted: isAccepted,
          ),
        ),
      );
      if (isAccepted) {
        setFilesLoading(request);
      }
    }
  
    void setFilesLoading(NearbyMessageFilesRequest request) {
      filesLoadings[request.id] = request.files.length;
      _notify();
    }
  
    void endFilesLoading(ReceivedNearbyFilesPack pack) {
      filesLoadings.remove(pack.id);
      _notify();
    }
  }
  
  extension LoggingExtension on AppService {
    void _log(e, StackTrace s) {
      if (kDebugMode) {
        print('$e, \nStacktrace: $s');
      }
    }
  
    void _logBusyException() {
      if (kDebugMode) {
        print(
          'Nearby service is busy, wait a little and retry (You can implement retry in your code)',
        );
      }
    }
  }
  