/*
 *   Famedly Matrix SDK
 *   Copyright (C) 2021 Famedly GmbH
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU Affero General Public License as
 *   published by the Free Software Foundation, either version 3 of the
 *   License, or (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *   GNU Affero General Public License for more details.
 *
 *   You should have received a copy of the GNU Affero General Public License
 *   along with this program.  If not, see .
 */
import 'dart:async';
import 'dart:core';
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:webrtc_interface/webrtc_interface.dart';
import 'package:matrix/matrix.dart';
import 'package:matrix/src/utils/cached_stream_controller.dart';
/// https://github.com/matrix-org/matrix-doc/pull/2746
/// version 1
const String voipProtoVersion = '1';
class CallTimeouts {
  /// The default life time for call events, in millisecond.
  static const defaultCallEventLifetime = Duration(seconds: 10);
  /// The length of time a call can be ringing for.
  static const callInviteLifetime = Duration(seconds: 60);
  /// The delay for ice gathering.
  static const iceGatheringDelay = Duration(milliseconds: 200);
  /// Delay before createOffer.
  static const delayBeforeOffer = Duration(milliseconds: 100);
}
class UserMediaOptions {
  static const optionalAudioConfig = {
    'echoCancellation': true,
    'googDAEchoCancellation': true,
    'googEchoCancellation': true,
    'googEchoCancellation2': true,
    'noiseSuppression': true,
    'googNoiseSuppression': true,
    'googNoiseSuppression2': true,
    'googAutoGainControl': true,
    'googHighpassFilter': true,
    'googTypingNoiseDetection': true,
  };
}
extension RTCIceCandidateExt on RTCIceCandidate {
  bool get isValid =>
      sdpMLineIndex != null &&
      sdpMid != null &&
      candidate != null &&
      candidate!.isNotEmpty;
}
/// Wrapped MediaStream, used to adapt Widget to display
class WrappedMediaStream {
  MediaStream? stream;
  final String userId;
  final Room room;
  /// Current stream type, usermedia or screen-sharing
  String purpose;
  bool audioMuted;
  bool videoMuted;
  final Client client;
  VideoRenderer renderer;
  final bool isWeb;
  final bool isGroupCall;
  final RTCPeerConnection? pc;
  /// for debug
  String get title => '$displayName:$purpose:a[$audioMuted]:v[$videoMuted]';
  bool stopped = false;
  final CachedStreamController onMuteStateChanged =
      CachedStreamController();
  void Function(MediaStream stream)? onNewStream;
  WrappedMediaStream(
      {this.stream,
      this.pc,
      required this.renderer,
      required this.room,
      required this.userId,
      required this.purpose,
      required this.client,
      required this.audioMuted,
      required this.videoMuted,
      required this.isWeb,
      required this.isGroupCall});
  /// Initialize the video renderer
  Future initialize() async {
    await renderer.initialize();
    renderer.srcObject = stream;
    renderer.onResize = () {
      Logs().i(
          'onResize [${stream!.id.substring(0, 8)}] ${renderer.videoWidth} x ${renderer.videoHeight}');
    };
  }
  Future dispose() async {
    renderer.srcObject = null;
    /// libwebrtc does not provide a way to clone MediaStreams. So stopping the
    /// local stream here would break calls with all other participants if anyone
    /// leaves. The local stream is manually disposed when user leaves. On web
    /// streams are actually cloned.
    if (!isGroupCall || isWeb) {
      await stopMediaStream(stream);
    }
    stream = null;
    await renderer.dispose();
  }
  Future disposeRenderer() async {
    renderer.srcObject = null;
    await renderer.dispose();
  }
  Uri? get avatarUrl => getUser().avatarUrl;
  String get avatarName =>
      getUser().calcDisplayname(mxidLocalPartFallback: false);
  String? get displayName => getUser().displayName;
  User getUser() {
    return room.unsafeGetUserFromMemoryOrFallback(userId);
  }
  bool isLocal() {
    return userId == client.userID;
  }
  bool isAudioMuted() {
    return (stream != null && stream!.getAudioTracks().isEmpty) || audioMuted;
  }
  bool isVideoMuted() {
    return (stream != null && stream!.getVideoTracks().isEmpty) || videoMuted;
  }
  void setNewStream(MediaStream newStream) {
    stream = newStream;
    renderer.srcObject = stream;
    if (onNewStream != null) {
      onNewStream?.call(stream!);
    }
  }
  void setAudioMuted(bool muted) {
    audioMuted = muted;
    onMuteStateChanged.add(this);
  }
  void setVideoMuted(bool muted) {
    videoMuted = muted;
    onMuteStateChanged.add(this);
  }
}
// Call state
enum CallState {
  /// The call is inilalized but not yet started
  kFledgling,
  /// The first time an invite is sent, the local has createdOffer
  kInviteSent,
  /// getUserMedia or getDisplayMedia has been called,
  /// but MediaStream has not yet been returned
  kWaitLocalMedia,
  /// The local has createdOffer
  kCreateOffer,
  /// Received a remote offer message and created a local Answer
  kCreateAnswer,
  /// Answer sdp is set, but ice is not connected
  kConnecting,
  /// WebRTC media stream is connected
  kConnected,
  /// The call was received, but no processing has been done yet.
  kRinging,
  /// End of call
  kEnded,
}
class CallErrorCode {
  /// The user chose to end the call
  static String UserHangup = 'user_hangup';
  /// An error code when the local client failed to create an offer.
  static String LocalOfferFailed = 'local_offer_failed';
  /// An error code when there is no local mic/camera to use. This may be because
  /// the hardware isn't plugged in, or the user has explicitly denied access.
  static String NoUserMedia = 'no_user_media';
  /// Error code used when a call event failed to send
  /// because unknown devices were present in the room
  static String UnknownDevices = 'unknown_devices';
  /// Error code used when we fail to send the invite
  /// for some reason other than there being unknown devices
  static String SendInvite = 'send_invite';
  /// An answer could not be created
  static String CreateAnswer = 'create_answer';
  /// Error code used when we fail to send the answer
  /// for some reason other than there being unknown devices
  static String SendAnswer = 'send_answer';
  /// The session description from the other side could not be set
  static String SetRemoteDescription = 'set_remote_description';
  /// The session description from this side could not be set
  static String SetLocalDescription = 'set_local_description';
  /// A different device answered the call
  static String AnsweredElsewhere = 'answered_elsewhere';
  /// No media connection could be established to the other party
  static String IceFailed = 'ice_failed';
  /// The invite timed out whilst waiting for an answer
  static String InviteTimeout = 'invite_timeout';
  /// The call was replaced by another call
  static String Replaced = 'replaced';
  /// Signalling for the call could not be sent (other than the initial invite)
  static String SignallingFailed = 'signalling_timeout';
  /// The remote party is busy
  static String UserBusy = 'user_busy';
  /// We transferred the call off to somewhere else
  static String Transfered = 'transferred';
}
class CallError extends Error {
  final String code;
  final String msg;
  final dynamic err;
  CallError(this.code, this.msg, this.err);
  @override
  String toString() {
    return '[$code] $msg, err: ${err.toString()}';
  }
}
enum CallEvent {
  /// The call was hangup by the local|remote user.
  kHangup,
  /// The call state has changed
  kState,
  /// The call got some error.
  kError,
  /// Call transfer
  kReplaced,
  /// The value of isLocalOnHold() has changed
  kLocalHoldUnhold,
  /// The value of isRemoteOnHold() has changed
  kRemoteHoldUnhold,
  /// Feeds have changed
  kFeedsChanged,
  /// For sip calls. support in the future.
  kAssertedIdentityChanged,
}
enum CallType { kVoice, kVideo }
enum CallDirection { kIncoming, kOutgoing }
enum CallParty { kLocal, kRemote }
/// Initialization parameters of the call session.
class CallOptions {
  late String callId;
  String? groupCallId;
  late CallType type;
  late CallDirection dir;
  late String localPartyId;
  late VoIP voip;
  late Room room;
  late List