content: /*
   *   Famedly
   *   Copyright (C) 2019, 2020, 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 <https://www.gnu.org/licenses/>.
   */
  
  import 'dart:async';
  import 'dart:math';
  
  import 'package:flutter/foundation.dart';
  import 'package:flutter/material.dart';
  import 'package:flutter/services.dart';
  
  import 'package:flutter_foreground_task/flutter_foreground_task.dart';
  import 'package:flutter_webrtc/flutter_webrtc.dart' hide VideoRenderer;
  import 'package:just_audio/just_audio.dart';
  import 'package:matrix/matrix.dart';
  import 'package:wakelock_plus/wakelock_plus.dart';
  
  import 'package:fluffychat/l10n/l10n.dart';
  import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
  import 'package:fluffychat/utils/platform_infos.dart';
  import 'package:fluffychat/utils/voip/video_renderer.dart';
  import 'package:fluffychat/widgets/avatar.dart';
  import 'pip/pip_view.dart';
  
  class _StreamView extends StatelessWidget {
    const _StreamView(
      this.wrappedStream, {
      this.mainView = false,
      required this.matrixClient,
    });
  
    final WrappedMediaStream wrappedStream;
    final Client matrixClient;
  
    final bool mainView;
  
    Uri? get avatarUrl => wrappedStream.getUser().avatarUrl;
  
    String? get displayName => wrappedStream.displayName;
  
    String get avatarName => wrappedStream.avatarName;
  
    bool get isLocal => wrappedStream.isLocal();
  
    bool get mirrored =>
        wrappedStream.isLocal() &&
        wrappedStream.purpose == SDPStreamMetadataPurpose.Usermedia;
  
    bool get audioMuted => wrappedStream.audioMuted;
  
    bool get videoMuted => wrappedStream.videoMuted;
  
    bool get isScreenSharing =>
        wrappedStream.purpose == SDPStreamMetadataPurpose.Screenshare;
  
    @override
    Widget build(BuildContext context) {
      return Container(
        decoration: const BoxDecoration(
          color: Colors.black54,
        ),
        child: Stack(
          alignment: Alignment.center,
          children: <Widget>[
            VideoRenderer(
              wrappedStream,
              mirror: mirrored,
              fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
            ),
            if (videoMuted) ...[
              Container(color: Colors.black54),
              Positioned(
                child: Avatar(
                  mxContent: avatarUrl,
                  name: displayName,
                  size: mainView ? 96 : 48,
                  client: matrixClient,
                  // textSize: mainView ? 36 : 24,
                  // matrixClient: matrixClient,
                ),
              ),
            ],
            if (!isScreenSharing)
              Positioned(
                left: 4.0,
                bottom: 4.0,
                child: Icon(
                  audioMuted ? Icons.mic_off : Icons.mic,
                  color: Colors.white,
                  size: 18.0,
                ),
              ),
          ],
        ),
      );
    }
  }
  
  class Calling extends StatefulWidget {
    final VoidCallback? onClear;
    final BuildContext context;
    final String callId;
    final CallSession call;
    final Client client;
  
    const Calling({
      required this.context,
      required this.call,
      required this.client,
      required this.callId,
      this.onClear,
      super.key,
    });
  
    @override
    MyCallingPage createState() => MyCallingPage();
  }
  
  class MyCallingPage extends State<Calling> {
    Room? get room => call.room;
  
    String get displayName => call.room.getLocalizedDisplayname(
          MatrixLocals(L10n.of(widget.context)),
        );
  
    String get callId => widget.callId;
  
    CallSession get call => widget.call;
  
    MediaStream? get localStream {
      if (call.localUserMediaStream != null) {
        return call.localUserMediaStream!.stream!;
      }
      return null;
    }
  
    MediaStream? get remoteStream {
      if (call.getRemoteStreams.isNotEmpty) {
        return call.getRemoteStreams[0].stream!;
      }
      return null;
    }
  
    bool get isMicrophoneMuted => call.isMicrophoneMuted;
  
    bool get isLocalVideoMuted => call.isLocalVideoMuted;
  
    bool get isScreensharingEnabled => call.screensharingEnabled;
  
    bool get isRemoteOnHold => call.remoteOnHold;
  
    bool get voiceonly => call.type == CallType.kVoice;
  
    bool get connecting => call.state == CallState.kConnecting;
  
    bool get connected => call.state == CallState.kConnected;
  
    double? _localVideoHeight;
    double? _localVideoWidth;
    EdgeInsetsGeometry? _localVideoMargin;
    CallState? _state;
  
    void _playCallSound() async {
      const path = 'assets/sounds/call.ogg';
      if (kIsWeb || PlatformInfos.isMobile || PlatformInfos.isMacOS) {
        final player = AudioPlayer();
        await player.setAsset(path);
        player.play();
      } else {
        Logs().w('Playing sound not implemented for this platform!');
      }
    }
  
    @override
    void initState() {
      super.initState();
      initialize();
      _playCallSound();
    }
  
    void initialize() async {
      final call = this.call;
      call.onCallStateChanged.stream.listen(_handleCallState);
      call.onCallEventChanged.stream.listen((event) {
        if (event == CallStateChange.kFeedsChanged) {
          setState(() {
            call.tryRemoveStopedStreams();
          });
        } else if (event == CallStateChange.kLocalHoldUnhold ||
            event == CallStateChange.kRemoteHoldUnhold) {
          setState(() {});
          Logs().i(
            'Call hold event: local ${call.localHold}, remote ${call.remoteOnHold}',
          );
        }
      });
      _state = call.state;
  
      if (call.type == CallType.kVideo) {
        try {
          // Enable wakelock (keep screen on)
          unawaited(WakelockPlus.enable());
        } catch (_) {}
      }
    }
  
    void cleanUp() {
      Timer(
        const Duration(seconds: 2),
        () => widget.onClear?.call(),
      );
      if (call.type == CallType.kVideo) {
        try {
          unawaited(WakelockPlus.disable());
        } catch (_) {}
      }
    }
  
    @override
    void dispose() {
      super.dispose();
      call.cleanUp.call();
    }
  
    void _resizeLocalVideo(Orientation orientation) {
      final shortSide = min(
        MediaQuery.sizeOf(widget.context).width,
        MediaQuery.sizeOf(widget.context).height,
      );
      _localVideoMargin = remoteStream != null
          ? const EdgeInsets.only(top: 20.0, right: 20.0)
          : EdgeInsets.zero;
      _localVideoWidth = remoteStream != null
          ? shortSide / 3
          : MediaQuery.sizeOf(widget.context).width;
      _localVideoHeight = remoteStream != null
          ? shortSide / 4
          : MediaQuery.sizeOf(widget.context).height;
    }
  
    void _handleCallState(CallState state) {
      Logs().v('CallingPage::handleCallState: ${state.toString()}');
      if ({CallState.kConnected, CallState.kEnded}.contains(state)) {
        HapticFeedback.heavyImpact();
      }
  
      if (mounted) {
        setState(() {
          _state = state;
          if (_state == CallState.kEnded) cleanUp();
        });
      }
    }
  
    void _answerCall() {
      setState(() {
        call.answer();
      });
    }
  
    void _hangUp() {
      setState(() {
        if (call.isRinging) {
          call.reject();
        } else {
          call.hangup(reason: CallErrorCode.userHangup);
        }
      });
    }
  
    void _muteMic() {
      setState(() {
        call.setMicrophoneMuted(!call.isMicrophoneMuted);
      });
    }
  
    void _screenSharing() async {
      if (PlatformInfos.isAndroid) {
        if (!call.screensharingEnabled) {
          FlutterForegroundTask.init(
            androidNotificationOptions: AndroidNotificationOptions(
              channelId: 'notification_channel_id',
              channelName: 'Foreground Notification',
              channelDescription:
                  L10n.of(widget.context).foregroundServiceRunning,
            ),
            iosNotificationOptions: const IOSNotificationOptions(),
            foregroundTaskOptions: ForegroundTaskOptions(
              eventAction: ForegroundTaskEventAction.nothing(),
            ),
          );
          FlutterForegroundTask.startService(
            notificationTitle: L10n.of(widget.context).screenSharingTitle,
            notificationText: L10n.of(widget.context).screenSharingDetail,
          );
        } else {
          FlutterForegroundTask.stopService();
        }
      }
  
      setState(() {
        call.setScreensharingEnabled(!call.screensharingEnabled);
      });
    }
  
    void _remoteOnHold() {
      setState(() {
        call.setRemoteOnHold(!call.remoteOnHold);
      });
    }
  
    void _muteCamera() {
      setState(() {
        call.setLocalVideoMuted(!call.isLocalVideoMuted);
      });
    }
  
    void _switchCamera() async {
      if (call.localUserMediaStream != null) {
        await Helper.switchCamera(
          call.localUserMediaStream!.stream!.getVideoTracks()[0],
        );
      }
      setState(() {});
    }
  
    /*
    void _switchSpeaker() {
      setState(() {
        session.setSpeakerOn();
      });
    }
    */
  
    List<Widget> _buildActionButtons(bool isFloating) {
      if (isFloating) {
        return [];
      }
  
      final switchCameraButton = FloatingActionButton(
        heroTag: 'switchCamera',
        onPressed: _switchCamera,
        backgroundColor: Colors.black45,
        child: const Icon(Icons.switch_camera),
      );
      /*
      var switchSpeakerButton = FloatingActionButton(
        heroTag: 'switchSpeaker',
        child: Icon(_speakerOn ? Icons.volume_up : Icons.volume_off),
        onPressed: _switchSpeaker,
        foregroundColor: Colors.black54,
        backgroundColor: Theme.of(widget.context).backgroundColor,
      );
      */
      final hangupButton = FloatingActionButton(
        heroTag: 'hangup',
        onPressed: _hangUp,
        tooltip: 'Hangup',
        backgroundColor: _state == CallState.kEnded ? Colors.black45 : Colors.red,
        child: const Icon(Icons.call_end),
      );
  
      final answerButton = FloatingActionButton(
        heroTag: 'answer',
        onPressed: _answerCall,
        tooltip: 'Answer',
        backgroundColor: Colors.green,
        child: const Icon(Icons.phone),
      );
  
      final muteMicButton = FloatingActionButton(
        heroTag: 'muteMic',
        onPressed: _muteMic,
        foregroundColor: isMicrophoneMuted ? Colors.black26 : Colors.white,
        backgroundColor: isMicrophoneMuted ? Colors.white : Colors.black45,
        child: Icon(isMicrophoneMuted ? Icons.mic_off : Icons.mic),
      );
  
      final screenSharingButton = FloatingActionButton(
        heroTag: 'screenSharing',
        onPressed: _screenSharing,
        foregroundColor: isScreensharingEnabled ? Colors.black26 : Colors.white,
        backgroundColor: isScreensharingEnabled ? Colors.white : Colors.black45,
        child: const Icon(Icons.desktop_mac),
      );
  
      final holdButton = FloatingActionButton(
        heroTag: 'hold',
        onPressed: _remoteOnHold,
        foregroundColor: isRemoteOnHold ? Colors.black26 : Colors.white,
        backgroundColor: isRemoteOnHold ? Colors.white : Colors.black45,
        child: const Icon(Icons.pause),
      );
  
      final muteCameraButton = FloatingActionButton(
        heroTag: 'muteCam',
        onPressed: _muteCamera,
        foregroundColor: isLocalVideoMuted ? Colors.black26 : Colors.white,
        backgroundColor: isLocalVideoMuted ? Colors.white : Colors.black45,
        child: Icon(isLocalVideoMuted ? Icons.videocam_off : Icons.videocam),
      );
  
      switch (_state) {
        case CallState.kRinging:
        case CallState.kInviteSent:
        case CallState.kCreateAnswer:
        case CallState.kConnecting:
          return call.isOutgoing
              ? <Widget>[hangupButton]
              : <Widget>[answerButton, hangupButton];
        case CallState.kConnected:
          return <Widget>[
            muteMicButton,
            //switchSpeakerButton,
            if (!voiceonly && !kIsWeb) switchCameraButton,
            if (!voiceonly) muteCameraButton,
            if (PlatformInfos.isMobile || PlatformInfos.isWeb)
              screenSharingButton,
            holdButton,
            hangupButton,
          ];
        case CallState.kEnded:
          return <Widget>[
            hangupButton,
          ];
        case CallState.kFledgling:
        case CallState.kWaitLocalMedia:
        case CallState.kCreateOffer:
        case CallState.kEnding:
        case null:
          break;
      }
      return <Widget>[];
    }
  
    List<Widget> _buildContent(Orientation orientation, bool isFloating) {
      final stackWidgets = <Widget>[];
  
      final call = this.call;
      if (call.callHasEnded) {
        return stackWidgets;
      }
  
      if (call.localHold || call.remoteOnHold) {
        var title = '';
        if (call.localHold) {
          title = '${call.room.getLocalizedDisplayname(
            MatrixLocals(L10n.of(widget.context)),
          )} held the call.';
        } else if (call.remoteOnHold) {
          title = 'You held the call.';
        }
        stackWidgets.add(
          Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Icon(
                  Icons.pause,
                  size: 48.0,
                  color: Colors.white,
                ),
                Text(
                  title,
                  style: const TextStyle(
                    color: Colors.white,
                    fontSize: 24.0,
                  ),
                ),
              ],
            ),
          ),
        );
        return stackWidgets;
      }
  
      var primaryStream = call.remoteScreenSharingStream ??
          call.localScreenSharingStream ??
          call.remoteUserMediaStream ??
          call.localUserMediaStream;
  
      if (!connected) {
        primaryStream = call.localUserMediaStream;
      }
  
      if (primaryStream != null) {
        stackWidgets.add(
          Center(
            child: _StreamView(
              primaryStream,
              mainView: true,
              matrixClient: widget.client,
            ),
          ),
        );
      }
  
      if (isFloating || !connected) {
        return stackWidgets;
      }
  
      _resizeLocalVideo(orientation);
  
      if (call.getRemoteStreams.isEmpty) {
        return stackWidgets;
      }
  
      final secondaryStreamViews = <Widget>[];
  
      if (call.remoteScreenSharingStream != null) {
        final remoteUserMediaStream = call.remoteUserMediaStream;
        secondaryStreamViews.add(
          SizedBox(
            width: _localVideoWidth,
            height: _localVideoHeight,
            child:
                _StreamView(remoteUserMediaStream!, matrixClient: widget.client),
          ),
        );
        secondaryStreamViews.add(const SizedBox(height: 10));
      }
  
      final localStream =
          call.localUserMediaStream ?? call.localScreenSharingStream;
      if (localStream != null && !isFloating) {
        secondaryStreamViews.add(
          SizedBox(
            width: _localVideoWidth,
            height: _localVideoHeight,
            child: _StreamView(localStream, matrixClient: widget.client),
          ),
        );
        secondaryStreamViews.add(const SizedBox(height: 10));
      }
  
      if (call.localScreenSharingStream != null && !isFloating) {
        secondaryStreamViews.add(
          SizedBox(
            width: _localVideoWidth,
            height: _localVideoHeight,
            child: _StreamView(
              call.remoteUserMediaStream!,
              matrixClient: widget.client,
            ),
          ),
        );
        secondaryStreamViews.add(const SizedBox(height: 10));
      }
  
      if (secondaryStreamViews.isNotEmpty) {
        stackWidgets.add(
          Container(
            padding: const EdgeInsets.fromLTRB(0, 20, 0, 120),
            alignment: Alignment.bottomRight,
            child: Container(
              width: _localVideoWidth,
              margin: _localVideoMargin,
              child: Column(
                children: secondaryStreamViews,
              ),
            ),
          ),
        );
      }
  
      return stackWidgets;
    }
  
    @override
    Widget build(BuildContext context) {
      return PIPView(
        builder: (context, isFloating) {
          return Scaffold(
            resizeToAvoidBottomInset: !isFloating,
            floatingActionButtonLocation:
                FloatingActionButtonLocation.centerFloat,
            floatingActionButton: SizedBox(
              width: 320.0,
              height: 150.0,
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: _buildActionButtons(isFloating),
              ),
            ),
            body: OrientationBuilder(
              builder: (BuildContext context, Orientation orientation) {
                return Container(
                  decoration: const BoxDecoration(
                    color: Colors.black87,
                  ),
                  child: Stack(
                    children: [
                      ..._buildContent(orientation, isFloating),
                      if (!isFloating)
                        Positioned(
                          top: 24.0,
                          left: 24.0,
                          child: IconButton(
                            color: Colors.black45,
                            icon: const Icon(Icons.arrow_back),
                            onPressed: () {
                              PIPView.of(context)?.setFloating(true);
                            },
                          ),
                        ),
                    ],
                  ),
                );
              },
            ),
          );
        },
      );
    }
  }
  