import 'dart:async';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:nearby_service/nearby_service.dart';

/// All UI components and utilities are moved out of main
/// to focus on the nearby_service plugin.
/// If you are interested to see them,
/// see https://github.com/ksenia312/nearby_service/blob/main/example/lib/.
import 'components/connected_device_view.dart';
import 'components/files_messaging_view.dart';
import 'components/ios_role_selector.dart';
import 'components/peer_widget.dart';
import 'components/text_messaging_view.dart';
import 'utils/app_snack_bar.dart';
import 'utils/file_saver.dart';

/// Short example variant for pub.dev
/// See the full example at
/// https://github.com/ksenia312/nearby_service/tree/main/example_full
Future<void> main() async {
  runApp(const App());
}

/// Simplified application states, main 3:
/// - Preparatory
/// - Device search
/// - Communicating with the connected device
enum AppState { idle, discovering, connected }

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Nearby Service Example',
      home: Scaffold(
        appBar: AppBar(title: const Text('Nearby Service Example')),
        body: Container(
          alignment: Alignment.topCenter,
          padding: const EdgeInsets.all(20),
          child: const AppBody(),
        ),
      ),
    );
  }
}

class AppBody extends StatefulWidget {
  const AppBody({super.key});

  @override
  State<AppBody> createState() => _AppBodyState();
}

class _AppBodyState extends State<AppBody> {
  /// Our service
  late final _nearbyService = NearbyService.getInstance(
    /// Define log level here
    logLevel: NearbyServiceLogLevel.debug,
  )..communicationChannelState.addListener(() => setState(() {}));

  AppState _state = AppState.idle;

  /// Browser OR Advertiser for IOS
  bool _isIosBrowser = true;

  /// List of discovered devices
  List<NearbyDevice> _peers = [];
  StreamSubscription? _peersSubscription;

  /// Temporary solution to check the connection,
  /// use [NearbyService.getConnectedDeviceStream] for this purpose
  /// in your application
  Timer? _connectionCheckTimer;
  NearbyDevice? _connectedDevice;

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

  @override
  void dispose() {
    _peersSubscription?.cancel();
    _connectionCheckTimer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (_state == AppState.idle) {
      return Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          if (Platform.isIOS)
            IOSRoleSelector(
              isIosBrowser: _isIosBrowser,
              onSelect: (value) => setState(() => _isIosBrowser = value),
            ),
          ElevatedButton(
            onPressed: _startProcess,
            child: const Text('Start'),
          ),
        ],
      );
    } else if (_state == AppState.discovering) {
      return ListView(
        children: [
          if (Platform.isIOS)
            Text('You are ${_isIosBrowser ? 'Browser' : 'Advertiser'}'),
          ..._peers.map(
            (e) => PeerWidget(
              device: e,
              isIosBrowser: _isIosBrowser,
              onConnect: _connect,
              communicationChannelState: _communicationChannelState,
            ),
          ),
        ],
      );
    } else if (_state == AppState.connected) {
      return SingleChildScrollView(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // Preview of the connected device
            ConnectedDeviceView(
              device: _connectedDevice!,
              onDisconnect: _disconnect,
            ),
            const SizedBox(height: 50),
            // Send messages section
            TextMessagingView(
              onSend: (message) => _send(
                NearbyMessageTextRequest.create(value: message),
              ),
            ),

            const SizedBox(height: 50),
            // Send files section
            FilesMessagingView(
              onSend: (files) {
                _send(
                  NearbyMessageFilesRequest.create(files: files),
                );
              },
            ),
          ],
        ),
      );
    }
    return Container();
  }

  CommunicationChannelState get _communicationChannelState {
    return _nearbyService.communicationChannelState.value;
  }

  Future<void> _initialize() async {
    await _nearbyService.initialize();
  }

  Future<void> _startProcess() async {
    final platformsReady = await _checkPlatforms();
    if (platformsReady) {
      await _discover();
    }
  }

  Future<bool> _checkPlatforms() async {
    if (Platform.isAndroid) {
      final isGranted = await _nearbyService.android?.requestPermissions();
      final wifiEnabled = await _nearbyService.android?.checkWifiService();
      return (isGranted ?? false) && (wifiEnabled ?? false);
    } else if (Platform.isIOS) {
      _nearbyService.ios?.setIsBrowser(value: _isIosBrowser);
      return true;
    } else {
      return false;
    }
  }

  Future<void> _discover() async {
    final result = await _nearbyService.discover();
    if (result) {
      setState(() {
        _state = AppState.discovering;
      });
      _peersSubscription = _nearbyService.getPeersStream().listen((event) {
        setState(() {
          _peers = event;
          // Check and filter your peers here
        });
      });
    }
  }

  Future<void> _connect(NearbyDevice device) async {
    // Be careful with already connected devices,
    // double connection may be unnecessary
    final result = await _nearbyService.connect(device);
    if (result || device.status.isConnected) {
      final channelStarting = _tryCommunicate(device);
      if (!channelStarting) {
        _connectionCheckTimer = Timer.periodic(
          const Duration(seconds: 3),
          (_) => _tryCommunicate(device),
        );
      }
    }
  }

  bool _tryCommunicate(NearbyDevice device) {
    NearbyDevice? selectedDevice;

    try {
      selectedDevice = _peers.firstWhere(
        (element) => element.info.id == device.info.id,
      );
    } catch (_) {
      return false;
    }

    if (selectedDevice.status.isConnected) {
      try {
        _startCommunicationChannel(device);
      } finally {
        _connectionCheckTimer?.cancel();
        _connectionCheckTimer = null;
      }
      return true;
    }
    return false;
  }

  void _startCommunicationChannel(NearbyDevice device) {
    if (!_communicationChannelState.isNotConnected) return;

    _nearbyService.startCommunicationChannel(
      NearbyCommunicationChannelData(
        device.info.id,
        filesListener: NearbyServiceFilesListener(onData: _filesListener),
        messagesListener: NearbyServiceMessagesListener(
          onData: _messagesListener,
          onCreated: () {
            setState(() {
              _connectedDevice = device;
              _state = AppState.connected;
            });
          },
          onError: (e, [StackTrace? s]) {
            setState(() {
              _connectedDevice = null;
              _state = AppState.idle;
            });
          },
          onDone: () {
            setState(() {
              _connectedDevice = null;
              _state = AppState.idle;
            });
          },
        ),
      ),
    );
  }

  Future<void> _disconnect() async {
    try {
      await _nearbyService.disconnect(_connectedDevice!);
    } finally {
      await _nearbyService.endCommunicationChannel();
      await _nearbyService.stopDiscovery();
      await _peersSubscription?.cancel();
      setState(() {
        _peers = [];
        _state = AppState.idle;
      });
    }
  }

  void _messagesListener(ReceivedNearbyMessage<NearbyMessageContent> message) {
    if (_connectedDevice == null) return;
    // Very useful stuff! Process messages according to the type of content
    message.content.byType(
      onTextRequest: (request) {
        AppSnackBar.show(
          context,
          title: request.value,
          subtitle: message.sender.displayName,
        ).whenComplete(
          () => _send(NearbyMessageTextResponse(id: request.id)),
        );
      },
      onTextResponse: (response) {
        AppSnackBar.show(
          context,
          title: 'Message ${response.id} was delivered',
          subtitle: message.sender.displayName,
        );
      },
      onFilesRequest: (request) {
        AppSnackBar.show(
          context,
          title: 'Request to receive ${request.files.length} files',
          subtitle: message.sender.displayName,
          actionName: 'Accept the files?',
          onAcceptAction: () {
            _send(
              NearbyMessageFilesResponse(id: request.id, isAccepted: true),
            );
          },
        );
      },
      onFilesResponse: (response) {
        AppSnackBar.show(
          context,
          title: 'Response ${response.id} for the files '
              '${response.isAccepted ? 'is accepted' : 'was denied'}',
          subtitle: message.sender.displayName,
        );
      },
    );
  }

  void _filesListener(ReceivedNearbyFilesPack pack) {
    FilesSaver.savePack(pack).then(
      (value) => AppSnackBar.show(context, title: 'Files pack was saved'),
    );
  }

  Future<void> _send(NearbyMessageContent content) async {
    if (_connectedDevice == null) return;
    await _nearbyService.send(
      OutgoingNearbyMessage(
        content: content,
        receiver: _connectedDevice!.info,
      ),
    );
  }
}
