import 'dart:async';
import 'dart:typed_data';

import 'package:diplomaticquarterapp/pages/conference/participant_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:twilio_programmable_video/twilio_programmable_video.dart';

class ConferenceRoom with ChangeNotifier {
  final String name;
  final String token;
  final String identity;

  final StreamController<bool> _onAudioEnabledStreamController = StreamController<bool>.broadcast();
  Stream<bool> onAudioEnabled;
  final StreamController<bool> _onVideoEnabledStreamController = StreamController<bool>.broadcast();
  Stream<bool> onVideoEnabled;
  final StreamController<Exception> _onExceptionStreamController = StreamController<Exception>.broadcast();
  Stream<Exception> onException;

  final Completer<Room> _completer = Completer<Room>();

  final List<ParticipantWidget> _participants = [];
  final List<ParticipantBuffer> _participantBuffer = [];
  final List<StreamSubscription> _streamSubscriptions = [];
  final List<RemoteDataTrack> _dataTracks = [];
  final List<String> _messages = [];

  CameraCapturer _cameraCapturer;
  Room _room;
  Timer _timer;

  ConferenceRoom({
    @required this.name,
    @required this.token,
    @required this.identity,
  }) {
    onAudioEnabled = _onAudioEnabledStreamController.stream;
    onVideoEnabled = _onVideoEnabledStreamController.stream;
    onException = _onExceptionStreamController.stream;
  }

  List<ParticipantWidget> get participants {
    return [..._participants];
  }

  Future<Room> connect() async {
    print('ConferenceRoom.connect()');
    try {
      await TwilioProgrammableVideo.debug(dart: true, native: true);
      await TwilioProgrammableVideo.setSpeakerphoneOn(true);

      _cameraCapturer = CameraCapturer(CameraSource.FRONT_CAMERA);
      var connectOptions = ConnectOptions(
        token,
        roomName: name,
        preferredAudioCodecs: [OpusCodec()],
        audioTracks: [LocalAudioTrack(true)],
        dataTracks: [LocalDataTrack()],
        videoTracks: [LocalVideoTrack(true, _cameraCapturer)],
        enableDominantSpeaker: true,
      );

      _room = await TwilioProgrammableVideo.connect(connectOptions);

      _streamSubscriptions.add(_room.onConnected.listen(_onConnected));
      _streamSubscriptions.add(_room.onConnectFailure.listen(_onConnectFailure));

      return _completer.future;
    } catch (err) {
      print(err);
      rethrow;
    }
  }

  Future<void> disconnect() async {
    print('ConferenceRoom.disconnect()');
    if (_timer != null) {
      _timer.cancel();
    }
    await _room.disconnect();
  }

  @override
  void dispose() {
    print('ConferenceRoom.dispose()');
    _disposeStreamsAndSubscriptions();
    super.dispose();
  }

  Future<void> _disposeStreamsAndSubscriptions() async {
    await _onAudioEnabledStreamController.close();
    await _onVideoEnabledStreamController.close();
    await _onExceptionStreamController.close();
    for (var streamSubscription in _streamSubscriptions) {
      await streamSubscription.cancel();
    }
  }

  Future<void> sendMessage(String message) async {
    final tracks = _room.localParticipant.localDataTracks;
    final localDataTrack = tracks.isEmpty ? null : tracks[0].localDataTrack;
    if (localDataTrack == null || _messages.isNotEmpty) {
      print('ConferenceRoom.sendMessage => Track is not available yet, buffering message.');
      _messages.add(message);
      return;
    }
    await localDataTrack.send(message);
  }

  Future<void> sendBufferMessage(ByteBuffer message) async {
    final tracks = _room.localParticipant.localDataTracks;
    final localDataTrack = tracks.isEmpty ? null : tracks[0].localDataTrack;
    if (localDataTrack == null) {
      return;
    }
    await localDataTrack.sendBuffer(message);
  }

  Future<void> toggleVideoEnabled() async {
    final tracks = _room.localParticipant.localVideoTracks;
    final localVideoTrack = tracks.isEmpty ? null : tracks[0].localVideoTrack;
    if (localVideoTrack == null) {
      print('ConferenceRoom.toggleVideoEnabled() => Track is not available yet!');
      return;
    }
    await localVideoTrack.enable(!localVideoTrack.isEnabled);

    var index = _participants.indexWhere((ParticipantWidget participant) => !participant.isRemote);
    if (index < 0) {
      return;
    }
    var participant = _participants[index];
    _participants.replaceRange(
      index,
      index + 1,
      [
        participant.copyWith(videoEnabled: localVideoTrack.isEnabled),
      ],
    );
    print('ConferenceRoom.toggleVideoEnabled() => ${localVideoTrack.isEnabled}');
    _onVideoEnabledStreamController.add(localVideoTrack.isEnabled);
    notifyListeners();
  }

  Future<void> toggleAudioEnabled() async {
    final tracks = _room.localParticipant.localAudioTracks;
    final localAudioTrack = tracks.isEmpty ? null : tracks[0].localAudioTrack;
    if (localAudioTrack == null) {
      print('ConferenceRoom.toggleAudioEnabled() => Track is not available yet!');
      return;
    }
    await localAudioTrack.enable(!localAudioTrack.isEnabled);

    var index = _participants.indexWhere((ParticipantWidget participant) => !participant.isRemote);
    if (index < 0) {
      return;
    }
    var participant = _participants[index];
    _participants.replaceRange(
      index,
      index + 1,
      [
        participant.copyWith(audioEnabled: localAudioTrack.isEnabled),
      ],
    );
    print('ConferenceRoom.toggleAudioEnabled() => ${localAudioTrack.isEnabled}');
    _onAudioEnabledStreamController.add(localAudioTrack.isEnabled);
    notifyListeners();
  }

  Future<void> switchCamera() async {
    print('ConferenceRoom.switchCamera()');
    try {
      await _cameraCapturer.switchCamera();
    } on FormatException catch (e) {
      print(
        'ConferenceRoom.switchCamera() failed because of FormatException with message: ${e.message}',
      );
    }
  }

  void addDummy({Widget child}) {
    print('ConferenceRoom.addDummy()');
    if (_participants.length >= 18) {
      throw PlatformException(
        code: 'ConferenceRoom.maximumReached',
        message: 'Maximum reached',
        details: 'Currently the lay-out can only render a maximum of 18 participants',
      );
    }
    _participants.insert(
      0,
      ParticipantWidget(
        id: (_participants.length + 1).toString(),
        child: child,
        isRemote: true,
        audioEnabled: true,
        videoEnabled: true,
        isDummy: true,
      ),
    );
    notifyListeners();
  }

  void removeDummy() {
    print('ConferenceRoom.removeDummy()');
    var dummy = _participants.firstWhere((participant) => participant.isDummy, orElse: () => null);
    if (dummy != null) {
      _participants.remove(dummy);
      notifyListeners();
    }
  }

  void _onConnected(Room room) {
    print('ConferenceRoom._onConnected => state: ${room.state}');

    // When connected for the first time, add remote participant listeners
    _streamSubscriptions.add(_room.onParticipantConnected.listen(_onParticipantConnected));
    _streamSubscriptions.add(_room.onParticipantDisconnected.listen(_onParticipantDisconnected));
    _streamSubscriptions.add(_room.onDominantSpeakerChange.listen(_onDominantSpeakerChanged));
    // Only add ourselves when connected for the first time too.
    _participants.add(
      _buildParticipant(
        child: room.localParticipant.localVideoTracks[0].localVideoTrack.widget(),
        id: identity,
        audioEnabled: true,
        videoEnabled: true,
      ),
    );
    for (final remoteParticipant in room.remoteParticipants) {
      var participant = _participants.firstWhere((participant) => participant.id == remoteParticipant.sid, orElse: () => null);
      if (participant == null) {
        print('Adding participant that was already present in the room ${remoteParticipant.sid}, before I connected');
        _addRemoteParticipantListeners(remoteParticipant);
      }
    }
    // We have to listen for the [onDataTrackPublished] event on the [LocalParticipant] in
    // order to be able to use the [send] method.
    _streamSubscriptions.add(room.localParticipant.onDataTrackPublished.listen(_onLocalDataTrackPublished));
    notifyListeners();
    _completer.complete(room);

    _timer = Timer.periodic(const Duration(minutes: 1), (_) {
      // Let's see if we can send some data over the DataTrack API
      sendMessage('And another minute has passed since I connected...');
      // Also try the ByteBuffer way of sending data
      final list = 'This data has been sent over the ByteBuffer channel of the DataTrack API'.codeUnits;
      var bytes = Uint8List.fromList(list);
      sendBufferMessage(bytes.buffer);
    });
  }

  void _onLocalDataTrackPublished(LocalDataTrackPublishedEvent event) {
    // Send buffered messages, if any...
    while (_messages.isNotEmpty) {
      var message = _messages.removeAt(0);
      print('Sending buffered message: $message');
      event.localDataTrackPublication.localDataTrack.send(message);
    }
  }

  void _onConnectFailure(RoomConnectFailureEvent event) {
    print('ConferenceRoom._onConnectFailure: ${event.exception}');
    _completer.completeError(event.exception);
  }

  void _onDominantSpeakerChanged(DominantSpeakerChangedEvent event) {
    print('ConferenceRoom._onDominantSpeakerChanged: ${event.remoteParticipant.identity}');
    var oldDominantParticipant = _participants.firstWhere((p) => p.isDominant, orElse: () => null);
    if (oldDominantParticipant != null) {
      var oldDominantParticipantIndex = _participants.indexOf(oldDominantParticipant);
      _participants.replaceRange(oldDominantParticipantIndex, oldDominantParticipantIndex + 1, [oldDominantParticipant.copyWith(isDominant: false)]);
    }

    var newDominantParticipant = _participants.firstWhere((p) => p.id == event.remoteParticipant.sid);
    var newDominantParticipantIndex = _participants.indexOf(newDominantParticipant);
    _participants.replaceRange(newDominantParticipantIndex, newDominantParticipantIndex + 1, [newDominantParticipant.copyWith(isDominant: true)]);
    notifyListeners();
  }

  void _onParticipantConnected(RoomParticipantConnectedEvent event) {
    print('ConferenceRoom._onParticipantConnected, ${event.remoteParticipant.sid}');
    _addRemoteParticipantListeners(event.remoteParticipant);
  }

  void _onParticipantDisconnected(RoomParticipantDisconnectedEvent event) {
    print('ConferenceRoom._onParticipantDisconnected: ${event.remoteParticipant.sid}');
    _participants.removeWhere((ParticipantWidget p) => p.id == event.remoteParticipant.sid);
    notifyListeners();
  }

  ParticipantWidget _buildParticipant({
    @required Widget child,
    @required String id,
    @required bool audioEnabled,
    @required bool videoEnabled,
    RemoteParticipant remoteParticipant,
  }) {
    return ParticipantWidget(
      id: remoteParticipant?.sid,
      isRemote: remoteParticipant != null,
      child: child,
      audioEnabled: audioEnabled,
      videoEnabled: videoEnabled,
    );
  }

  void _addRemoteParticipantListeners(RemoteParticipant remoteParticipant) {
    print('ConferenceRoom._addRemoteParticipantListeners() => Adding listeners to remoteParticipant ${remoteParticipant.sid}');
    _streamSubscriptions.add(remoteParticipant.onAudioTrackDisabled.listen(_onAudioTrackDisabled));
    _streamSubscriptions.add(remoteParticipant.onAudioTrackEnabled.listen(_onAudioTrackEnabled));
    _streamSubscriptions.add(remoteParticipant.onAudioTrackPublished.listen(_onAudioTrackPublished));
    _streamSubscriptions.add(remoteParticipant.onAudioTrackSubscribed.listen(_onAudioTrackSubscribed));
    _streamSubscriptions.add(remoteParticipant.onAudioTrackSubscriptionFailed.listen(_onAudioTrackSubscriptionFailed));
    _streamSubscriptions.add(remoteParticipant.onAudioTrackUnpublished.listen(_onAudioTrackUnpublished));
    _streamSubscriptions.add(remoteParticipant.onAudioTrackUnsubscribed.listen(_onAudioTrackUnsubscribed));

    _streamSubscriptions.add(remoteParticipant.onDataTrackPublished.listen(_onDataTrackPublished));
    _streamSubscriptions.add(remoteParticipant.onDataTrackSubscribed.listen(_onDataTrackSubscribed));
    _streamSubscriptions.add(remoteParticipant.onDataTrackSubscriptionFailed.listen(_onDataTrackSubscriptionFailed));
    _streamSubscriptions.add(remoteParticipant.onDataTrackUnpublished.listen(_onDataTrackUnpublished));
    _streamSubscriptions.add(remoteParticipant.onDataTrackUnsubscribed.listen(_onDataTrackUnsubscribed));

    _streamSubscriptions.add(remoteParticipant.onVideoTrackDisabled.listen(_onVideoTrackDisabled));
    _streamSubscriptions.add(remoteParticipant.onVideoTrackEnabled.listen(_onVideoTrackEnabled));
    _streamSubscriptions.add(remoteParticipant.onVideoTrackPublished.listen(_onVideoTrackPublished));
    _streamSubscriptions.add(remoteParticipant.onVideoTrackSubscribed.listen(_onVideoTrackSubscribed));
    _streamSubscriptions.add(remoteParticipant.onVideoTrackSubscriptionFailed.listen(_onVideoTrackSubscriptionFailed));
    _streamSubscriptions.add(remoteParticipant.onVideoTrackUnpublished.listen(_onVideoTrackUnpublished));
    _streamSubscriptions.add(remoteParticipant.onVideoTrackUnsubscribed.listen(_onVideoTrackUnsubscribed));
  }

  void _onAudioTrackDisabled(RemoteAudioTrackEvent event) {
    print('ConferenceRoom._onAudioTrackDisabled(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}, isEnabled: ${event.remoteAudioTrackPublication.isTrackEnabled}');
    _setRemoteAudioEnabled(event);
  }

  void _onAudioTrackEnabled(RemoteAudioTrackEvent event) {
    print('ConferenceRoom._onAudioTrackEnabled(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}, isEnabled: ${event.remoteAudioTrackPublication.isTrackEnabled}');
    _setRemoteAudioEnabled(event);
  }

  void _onAudioTrackPublished(RemoteAudioTrackEvent event) {
    print('ConferenceRoom._onAudioTrackPublished(), ${event.remoteParticipant.sid}}');
  }

  void _onAudioTrackSubscribed(RemoteAudioTrackSubscriptionEvent event) {
    print('ConferenceRoom._onAudioTrackSubscribed(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}');
    _addOrUpdateParticipant(event);
  }

  void _onAudioTrackSubscriptionFailed(RemoteAudioTrackSubscriptionFailedEvent event) {
    print('ConferenceRoom._onAudioTrackSubscriptionFailed(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}');
    _onExceptionStreamController.add(
      PlatformException(
        code: 'ConferenceRoom.audioTrackSubscriptionFailed',
        message: 'AudioTrack Subscription Failed',
        details: event.exception.toString(),
      ),
    );
  }

  void _onAudioTrackUnpublished(RemoteAudioTrackEvent event) {
    print('ConferenceRoom._onAudioTrackUnpublished(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}');
  }

  void _onAudioTrackUnsubscribed(RemoteAudioTrackSubscriptionEvent event) {
    print('ConferenceRoom._onAudioTrackUnsubscribed(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrack.sid}');
  }

  void _onDataTrackPublished(RemoteDataTrackEvent event) {
    print('ConferenceRoom._onDataTrackPublished(), ${event.remoteParticipant.sid}}');
  }

  void _onDataTrackSubscribed(RemoteDataTrackSubscriptionEvent event) {
    print('ConferenceRoom._onDataTrackSubscribed(), ${event.remoteParticipant.sid}, ${event.remoteDataTrackPublication.trackSid}');
    final dataTrack = event.remoteDataTrackPublication.remoteDataTrack;
    _dataTracks.add(dataTrack);
    _streamSubscriptions.add(dataTrack.onMessage.listen(_onMessage));
    _streamSubscriptions.add(dataTrack.onBufferMessage.listen(_onBufferMessage));
  }

  void _onDataTrackSubscriptionFailed(RemoteDataTrackSubscriptionFailedEvent event) {
    print('ConferenceRoom._onDataTrackSubscriptionFailed(), ${event.remoteParticipant.sid}, ${event.remoteDataTrackPublication.trackSid}');
    _onExceptionStreamController.add(
      PlatformException(
        code: 'ConferenceRoom.dataTrackSubscriptionFailed',
        message: 'DataTrack Subscription Failed',
        details: event.exception.toString(),
      ),
    );
  }

  void _onDataTrackUnpublished(RemoteDataTrackEvent event) {
    print('ConferenceRoom._onDataTrackUnpublished(), ${event.remoteParticipant.sid}, ${event.remoteDataTrackPublication.trackSid}');
  }

  void _onDataTrackUnsubscribed(RemoteDataTrackSubscriptionEvent event) {
    print('ConferenceRoom._onDataTrackUnsubscribed(), ${event.remoteParticipant.sid}, ${event.remoteDataTrack.sid}');
  }

  void _onVideoTrackDisabled(RemoteVideoTrackEvent event) {
    print('ConferenceRoom._onVideoTrackDisabled(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}, isEnabled: ${event.remoteVideoTrackPublication.isTrackEnabled}');
    _setRemoteVideoEnabled(event);
  }

  void _onVideoTrackEnabled(RemoteVideoTrackEvent event) {
    print('ConferenceRoom._onVideoTrackEnabled(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}, isEnabled: ${event.remoteVideoTrackPublication.isTrackEnabled}');
    _setRemoteVideoEnabled(event);
  }

  void _onVideoTrackPublished(RemoteVideoTrackEvent event) {
    print('ConferenceRoom._onVideoTrackPublished(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}');
  }

  void _onVideoTrackSubscribed(RemoteVideoTrackSubscriptionEvent event) {
    print('ConferenceRoom._onVideoTrackSubscribed(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrack.sid}');
    _addOrUpdateParticipant(event);
  }

  void _onVideoTrackSubscriptionFailed(RemoteVideoTrackSubscriptionFailedEvent event) {
    print('ConferenceRoom._onVideoTrackSubscriptionFailed(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}');
    _onExceptionStreamController.add(
      PlatformException(
        code: 'ConferenceRoom.videoTrackSubscriptionFailed',
        message: 'VideoTrack Subscription Failed',
        details: event.exception.toString(),
      ),
    );
  }

  void _onVideoTrackUnpublished(RemoteVideoTrackEvent event) {
    print('ConferenceRoom._onVideoTrackUnpublished(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}');
  }

  void _onVideoTrackUnsubscribed(RemoteVideoTrackSubscriptionEvent event) {
    print('ConferenceRoom._onVideoTrackUnsubscribed(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrack.sid}');
  }

  void _onMessage(RemoteDataTrackStringMessageEvent event) {
    print('onMessage => ${event.remoteDataTrack.sid}, ${event.message}');
  }

  void _onBufferMessage(RemoteDataTrackBufferMessageEvent event) {
    print('onBufferMessage => ${event.remoteDataTrack.sid}, ${String.fromCharCodes(event.message.asUint8List())}');
  }

  void _setRemoteAudioEnabled(RemoteAudioTrackEvent event) {
    if (event.remoteAudioTrackPublication == null) {
      return;
    }
    var index = _participants.indexWhere((ParticipantWidget participant) => participant.id == event.remoteParticipant.sid);
    if (index < 0) {
      return;
    }
    var participant = _participants[index];
    _participants.replaceRange(
      index,
      index + 1,
      [
        participant.copyWith(audioEnabled: event.remoteAudioTrackPublication.isTrackEnabled),
      ],
    );
    notifyListeners();
  }

  void _setRemoteVideoEnabled(RemoteVideoTrackEvent event) {
    if (event.remoteVideoTrackPublication == null) {
      return;
    }
    var index = _participants.indexWhere((ParticipantWidget participant) => participant.id == event.remoteParticipant.sid);
    if (index < 0) {
      return;
    }
    var participant = _participants[index];
    _participants.replaceRange(
      index,
      index + 1,
      [
        participant.copyWith(videoEnabled: event.remoteVideoTrackPublication.isTrackEnabled),
      ],
    );
    notifyListeners();
  }

  void _addOrUpdateParticipant(RemoteParticipantEvent event) {
    print('ConferenceRoom._addOrUpdateParticipant(), ${event.remoteParticipant.sid}');
    final participant = _participants.firstWhere(
      (ParticipantWidget participant) => participant.id == event.remoteParticipant.sid,
      orElse: () => null,
    );
    if (participant != null) {
      print('Participant found: ${participant.id}, updating A/V enabled values');
      _setRemoteVideoEnabled(event);
      _setRemoteAudioEnabled(event);
    } else {
      final bufferedParticipant = _participantBuffer.firstWhere(
        (ParticipantBuffer participant) => participant.id == event.remoteParticipant.sid,
        orElse: () => null,
      );
      if (bufferedParticipant != null) {
        _participantBuffer.remove(bufferedParticipant);
      } else if (event is RemoteAudioTrackEvent) {
        print('Audio subscription came first, waiting for the video subscription...');
        _participantBuffer.add(
          ParticipantBuffer(
            id: event.remoteParticipant.sid,
            audioEnabled: event.remoteAudioTrackPublication?.remoteAudioTrack?.isEnabled ?? true,
          ),
        );
        return;
      }
      if (event is RemoteVideoTrackSubscriptionEvent) {
        print('New participant, adding: ${event.remoteParticipant.sid}');
        _participants.insert(
          0,
          _buildParticipant(
            child: event.remoteVideoTrack.widget(),
            id: event.remoteParticipant.sid,
            remoteParticipant: event.remoteParticipant,
            audioEnabled: bufferedParticipant?.audioEnabled ?? true,
            videoEnabled: event.remoteVideoTrackPublication?.remoteVideoTrack?.isEnabled ?? true,
          ),
        );
      }
      notifyListeners();
    }
  }
}
