<!DOCTYPE html>
<html lang="en">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Leadership Health</title>

    <link rel="stylesheet" href="/css/bootstrap.min.css">
    <link rel="stylesheet" href="/fontawesome-free/css/all.min.css">
    <link rel="stylesheet" href="/css/toastr.min.css">
    <script src="/js/jquery-3.5.1.min.js"></script>
    <script src="/js/toastr.min.js"></script>
    <script defer src=/js/AgoraRTC_N-4.1.0.js></script>
    <script src="/js/sockjs.min.js"></script>
    <script src="/js/stomp.min.js"></script>

    <link href="/css/call-minimal.css" rel="stylesheet">
</head>

<body class="p-0 m-0 has-mcp-queue">

<div class="d-flex px-3 border-bottom">
    <div class="py-2 font-weight-normal mcp-theme-1 d-inline-flex align-items-center">
        <i class="fa fa-user-injured small mr-2"></i>
        <a href="#" onclick="return window.top.openInLHS('/patients/view/{{$client->uid}}')">
            <span class="font-weight-bold">{{ $client->displayName() }}</span>
        </a>
    </div>
</div>

<div class="videos-container">

    {{-- start video button --}}
    <button id="btn-start-video"
            disabled
            class="btn btn-primary px-4 font-weight-bold d-block mx-auto my-3">
        Join Video
    </button>

    <div class="instruction-container">
        <div class="mic-access-chrome-desktop border bg-light rounded m-3 px-3 pt-3 pb-2 d-none">
            <p><b>We were unable to access your microphone!</b></p>
            <p>To allow access, please tap the blocked media icon in your browser's address bar (indicated in the figure below).</p>
            <img src="/img/mic-access-chrome-desktop.png" class="mw-100 mx-auto mb-3 d-block">
            <p class="mb-0">Once you have allowed access, please click <b>Join Video</b> again to retry.</p>
        </div>
        <div class="cam-access-chrome-desktop border bg-light rounded m-3 px-3 pt-3 pb-2 d-none">
            <p><b>We were unable to access your camera!</b></p>
            <p>To allow access, please tap the blocked media icon in your browser's address bar (indicated in the figure below).</p>
            <img src="/img/mic-access-chrome-desktop.png" class="mw-100 mx-auto mb-3 d-block">
            <p class="mb-0">Once you have allowed access, please click <b>Join Video</b> again to retry.</p>
        </div>
    </div>

    <div id="video-container" class="container d-none">
        <div class="main-view mx-auto">
            <div id="self-view" class="video-view" data-user-id="">
                <i class="fa fa-volume-mute text-white muted-icon"></i>
            </div>
        </div>
    </div>

    <div id="call-actions" class="d-none">
        <button class="btn btn-danger" id="btn-hang-up" title="Leave Call">
            <i class="fa fa-phone"></i>
        </button>
        <button class="ml-2 btn btn-default bg-light border" id="btn-stop-camera" title="Stop Camera">
            <i class="fa fa-video"></i>
        </button>
        <button class="ml-2 btn btn-secondary d-none" id="btn-start-camera" title="Start Camera">
            <i class="fa fa-video-slash"></i>
        </button>
        <button class="ml-2 btn btn-default bg-light border" id="btn-mute-audio" title="Mute">
            <i class="fa fa-microphone"></i>
        </button>
        <button class="ml-2 btn btn-secondary d-none" id="btn-unmute-audio" title="Unmute">
            <i class="fa fa-microphone-slash"></i>
        </button>
    </div>

</div>

<script>
    (function() {

        window.StagVideo = {

            // model

            // data returned by getMyMeeting
            meetingData: {
                amIInAMeeting: false,
                awayMessage: '',
                inMeetingForClient: {
                    clientMediaServiceRoomIdentifier: '',
                    displayName: '',
                    dob: '',
                    uid: '',
                },
                inMeetingForClientUid: null,
                inMeetingForPro: null,
                inMeetingForProUid: null,
                meetingType: "CLIENT",
                myMedia: {
                    isCameraAcquired: false,
                    isCameraOn: false,
                    isMicrophoneAcquired: false,
                    isMicrophoneOn: false
                },
                myMediaServiceIdentifier: '',
                myMediaServiceToken: null,
                otherParticipants: []
            },

            // data local to StagVideo
            myName: '{{ $performer->pro->displayName() }}',
            clientUid: '{{ $client->uid }}',

            // agora
            mediaServiceClient: null, // instantiated on agora init
            appId: '{{ config('app.agora_appid') }}',

            // handle to own tracks (needed later for muting and unmuting)
            myAudio: null,
            myVideo: null,

            // sockets
            backendWsURL: '{{ config('app.backend_ws_url') }}',
            socketClient: null,

            // cache elements to avoid running selectors everytime
            // Notation: $ at the beginning for jQuery objects
            $btnStartVideo: null,
            $videoContainer: null,
            $selfView: null,
            $callActions: null,
            $btnHangUp: null,
            $btnStopCamera: null,
            $btnStartCamera: null,
            $btnMuteAudio: null,
            $btnUnmuteAudio: null,

            // methods
            init: function() {

                // to distinguish between reloads
                this.log('page refreshed ----------------------------------');

                // cache elements to avoid running selectors everytime
                this.$btnStartVideo = $('#btn-start-video');
                this.$videoContainer = $('#video-container');
                this.$selfView = $('#self-view');
                this.$callActions = $('#call-actions');
                this.$btnHangUp = $('#btn-hang-up');

                this.$btnStopCamera = $('#btn-stop-camera');
                this.$btnStartCamera = $('#btn-start-camera');
                this.$btnMuteAudio = $('#btn-mute-audio');
                this.$btnUnmuteAudio = $('#btn-unmute-audio');

                this.registerSocket();

                // event handlers
                $(document)
                    .off('click.start-video', '#btn-start-video')
                    .on('click.start-video', '#btn-start-video', () => {
                        this.initMedia();
                    });

                $(document)
                    .off('click.hang-up', '#btn-hang-up')
                    .on('click.hang-up', '#btn-hang-up', () => {
                        this.leaveRoomAsPro();
                    });

                $(document)
                    .off('click.stop-camera', '#btn-stop-camera')
                    .on('click.stop-camera', '#btn-stop-camera', () => {
                        this.stopCamera();
                    });

                $(document)
                    .off('click.start-camera', '#btn-start-camera')
                    .on('click.start-camera', '#btn-start-camera', () => {
                        this.startCamera();
                    });

                $(document)
                    .off('click.mute-audio', '#btn-mute-audio')
                    .on('click.mute-audio', '#btn-mute-audio', () => {
                        this.muteAudio();
                    });

                $(document)
                    .off('click.unmute-audio', '#btn-unmute-audio')
                    .on('click.unmute-audio', '#btn-unmute-audio', () => {
                        this.unmuteAudio();
                    });


                this.enterRoomAsPro();
            },

            // register socket
            registerSocket: function (_done) {
                let socket = new SockJS(this.backendWsURL);
                this.socketClient = Stomp.over(socket);
                this.socketClient.connect({}, (frame) => {
                    this.socketClient.send("/app/register", {},
                        JSON.stringify({
                            sessionKey: '{{$performer->session_key}}'
                        })
                    );
                    window.setInterval(() => {
                        this.socketClient.send("/app/heartbeat", {},
                            JSON.stringify({sessionKey: '{{ request()->cookie('sessionKey') }}'})
                        );
                    }, 5000);

                    this.initSocketEvents();
                });
            },

            // on pro side, handle all events
            initSocketEvents: function() {

                this.socketClient.subscribe("/user/topic/myMicrophoneIsOn", (message) => {
                    this.log("WSE myMicrophoneIsOn received: " + message.body);
                    this.handleParticipantMicrophoneMutedChangeWSEvent(message, true);
                });

                this.socketClient.subscribe("/user/topic/myMicrophoneIsOff", (message) => {
                    this.log("WSE myMicrophoneIsOff received:" + message.body);
                    this.handleParticipantMicrophoneMutedChangeWSEvent(message, false);
                });

                this.log("Initialized WS event handlers")

            },

            handleParticipantMicrophoneMutedChangeWSEvent: function(_event, _value) {
                if(_event && _event.body) {
                    let eventData = JSON.parse(_event.body);
                    if(eventData.performer !== '{{ $session->uid  }}' && eventData.data) {
                        for (let i = 0; i < this.meetingData.otherParticipants.length; i++) {
                            if(this.meetingData.otherParticipants[i].uid === eventData.performer) {
                                this.meetingData.otherParticipants[i].media.isMicrophoneOn = _value;
                                if(!_value) {
                                    $('.video-view[data-user-id="' + this.meetingData.otherParticipants[i].mediaServiceIdentifier + '"]')
                                        .attr('participant-muted', 1);
                                }
                                else {
                                    $('.video-view[data-user-id="' + this.meetingData.otherParticipants[i].mediaServiceIdentifier + '"]')
                                        .removeAttr('participant-muted');
                                }
                            }
                        }
                    }
                }
            },

            // join meeting
            enterRoomAsPro: function () {
                this.ajax('/api/meeting/enterClientRoomAsPro', {clientUid: this.clientUid}, (_data) => {
                    this.getMeetingInfo(true, () => {
                        this.$btnStartVideo.prop('disabled', false);
                    });
                });
            },

            // leave meeting
            leaveRoomAsPro: function() {

                // notify java
                this.socketClient.send("/app/leaveClientRoom",
                    {},
                    JSON.stringify({sessionKey: '{{$performer->session_key}}'})
                );

                // refresh to non-client page
                window.location.href = '/pro/meet/';

            },

            // get meeting info and populate model
            getMeetingInfo: function (_firstRun = false, _done = null) {
                this.ajax('/api/meeting/getMyMeeting', {}, (_data) => {

                    // fill model
                    this.meetingData = _data.data;

                    // set own myMediaServiceIdentifier to #selfView
                    if(_firstRun) this.$selfView.attr('data-user-id', this.meetingData.myMediaServiceIdentifier);

                    if(_done) _done.call(this);
                }, 'json');
            },

            // init media
            initMedia: function() {

                // create client
                this.mediaServiceClient = AgoraRTC.createClient({mode: 'rtc', codec: 'vp8'});

                // acquire devices
                this.attemptToAcquireMicrophone(() => {
                    this.attemptToAcquireCamera(() => {
                        this.log('Hurrah! Mic and camera both acquired :)', 'log');

                        // go to video UI
                        this.$btnStartVideo.removeClass('d-block').addClass('d-none');
                        this.$videoContainer.removeClass('d-none').addClass('d-block');

                        // show call actions
                        this.$callActions.removeClass('d-none').addClass('d-flex');

                        // show own video
                        this.$selfView.height(this.$selfView.width() * 0.75); // 4x3 mode
                        this.myVideo.play(this.$selfView[0], {fit: 'contain'});

                        // show own name-bar
                        this.$selfView.find('.name-bar').remove();
                        this.$selfView.append($('<div class="name-bar"></div>').text(this.myName));

                        // start listening to events
                        this.initMediaEvents();

                        // start publishing
                        this.initMediaPublishing();

                    })
                });
            },

            attemptToAcquireMicrophone: function(_done) {
                AgoraRTC.onMicrophoneChanged = (_info) => {
                    this.log("microphone changed! - " + JSON.stringify(_info.state), 'log');
                    if(_info.state === 'ACTIVE') {

                        if(!this.myAudio || !this.meetingData.myMedia.isMicrophoneAcquired) {
                            window.location.reload();
                        }

                        // reactive acquisition (because user later granted access)
                        this.acquireMicrophone().then(() => {
                            this.log('microphone acquisition attempt completed', 'log');

                            // if all good, allow to proceed
                            if(this.myAudio && this.meetingData.myMedia.isMicrophoneAcquired) {
                                _done.call(this);
                            }
                        });
                    }
                };

                // proactive acquisition (already allowed OR user clicked "Allow" when prompted
                this.acquireMicrophone().then(() => {
                    this.log('microphone acquisition attempt completed', 'log');

                    // if all good, allow to proceed
                    if(this.myAudio && this.meetingData.myMedia.isMicrophoneAcquired) {
                        _done.call(this);
                    }
                });
            },

            acquireMicrophone: async function() {

                // if already acquired, ignore
                if(this.myAudio && this.meetingData.myMedia.isMicrophoneAcquired) {
                    // Skipping microphone acquisition..
                    return;
                }

                this.meetingData.myMedia.isMicrophoneAcquired = false;
                this.meetingData.myMedia.isMicrophoneOn = false;
                try {
                    this.myAudio = await AgoraRTC.createMicrophoneAudioTrack({
                        AEC: true,
                        AGC: true,
                        ANS: true,
                        encoderConfig: 'speech_standard'
                    });

                    this.meetingData.myMedia.isMicrophoneAcquired = true;
                    this.log('acquired microphone :)');
                    // TODO: notify others via WS

                    this.myAudio.setEnabled(true);
                    this.myAudio.setVolume(100); // default volume, max allowed is 1000
                    this.meetingData.myMedia.isMicrophoneOn = true;
                    this.log('microphone is ON and not on mute :)');
                    // TODO: notify others via WS

                } catch (e) {
                    this.log('could not acquire microphone', 'error');

                    this.meetingData.myMedia.isMicrophoneAcquired = false;
                    this.meetingData.myMedia.isMicrophoneOn = false;
                    // TODO: notify others via WS

                    this.$btnStartVideo.removeClass('d-block').addClass('d-none');
                    // TODO: Use device/browser specific instruction & image
                    $('.mic-access-chrome-desktop').removeClass('d-none').addClass('d-block');
                }
            },

            attemptToAcquireCamera: function(_done) {
                AgoraRTC.onCameraChanged = (_info) => {
                    this.log("camera changed! - " + JSON.stringify(_info));
                    if(_info.state === 'ACTIVE') {

                        if(!this.myVideo || !this.meetingData.myMedia.isCameraAcquired) {
                            window.location.reload();
                        }

                        // reactive acquisition (because user later granted access)
                        this.acquireCamera().then(() => {
                            this.log('camera acquisition attempt completed');

                            // if all good, allow to proceed
                            if(this.myVideo && this.meetingData.myMedia.isCameraAcquired) {
                                _done.call(this);
                            }
                        });
                    }
                };

                // proactive acquisition (already allowed OR user clicked "Allow" when prompted
                this.acquireCamera().then(() => {
                    this.log('camera acquisition attempt completed');

                    // if all good, allow to proceed
                    if(this.myVideo && this.meetingData.myMedia.isCameraAcquired) {
                        _done.call(this);
                    }
                });
            },

            acquireCamera: async function() {

                // if already acquired, ignore
                if(this.myVideo && this.meetingData.myMedia.isCameraAcquired) {
                    // Skipping camera acquisition
                    return;
                }

                this.meetingData.myMedia.isCameraAcquired = false;
                this.meetingData.myMedia.isCameraOn = false;
                try {
                    this.myVideo = await AgoraRTC.createCameraVideoTrack({
                        optimizationMode: "motion"
                    });

                    this.meetingData.myMedia.isCameraAcquired = true;
                    this.log('acquired camera :)');
                    // TODO: notify others via WS

                    this.myVideo.setEnabled(true);
                    this.meetingData.myMedia.isCameraOn = true;
                    this.log('camera is ON :)');
                    // TODO: notify others via WS

                } catch (e) {
                    this.log('could not acquire camera', 'error');

                    this.meetingData.myMedia.isCameraAcquired = false;
                    this.meetingData.myMedia.isCameraOn = false;
                    // TODO: notify others via WS

                    this.$btnStartVideo.removeClass('d-block').addClass('d-none');
                    // TODO: Use device/browser specific instruction & image
                    $('.cam-access-chrome-desktop').removeClass('d-none').addClass('d-block');
                }
            },

            // join agora & start publishing
            getMyMediaServiceToken: function(_done) {
                $.post('/api/meeting/refreshMyMediaServiceToken', (_data) => {  // get new agora token
                    if (!this.hasError(_data)) {
                        this.meetingData.myMediaServiceToken = _data.data;
                        _done.call(this);
                    }
                }, 'json');
            },

            // init media events
            initMediaEvents: function() {
                this.mediaServiceClient.on('user-left', user => {
                    // remove user's video div
                    $('.video-view[data-user-id="' + user.uid + '"]').remove();

                    // refresh state using getMyMeeting
                    this.getMeetingInfo();

                    // TODO log
                });
                this.mediaServiceClient.on('user-published', async (user, mediaType) => {
                    this.log('user-published - ' + user.uid);

                    // subscribe to the stream
                    try {
                        await this.mediaServiceClient.subscribe(user, mediaType);

                        // subscription success, proceed to playing the stream
                        if(mediaType === 'video') {
                            let element = $('.video-view[data-user-id="' + user.uid + '"]');
                            if(!element.length) {
                                element = $('<div class="video-view" data-user-id="' + user.uid + '"></div>');
                                element.appendTo('.main-view');
                            }
                            element.height(element.width() * 0.75); // 4x3 mode
                            user.videoTrack.play(element[0], {fit: 'contain'});

                            // refreshState & show user's name over video
                            this.getMeetingInfo(false, () => {
                                let participant = null;
                                for (let i = 0; i < this.meetingData.otherParticipants.length; i++) {
                                    if (this.meetingData.otherParticipants[i].mediaServiceIdentifier === ('' + user.uid)) {
                                        participant = this.meetingData.otherParticipants[i];
                                        break;
                                    }
                                }
                                if (participant) {
                                    element.find('.name-bar').remove();
                                    element.append($('<div class="name-bar"></div>').text(participant.displayName));
                                    element.append('<i class="fa fa-volume-mute text-white muted-icon"></i>');
                                }
                            });

                        }
                        else if(mediaType === 'audio') {
                            user.audioTrack.play();
                        }

                        this.log('got ' + mediaType + ' stream from user: ' + user.uid);
                    }
                    catch (e) {
                        this.log('could not subscribe to ' + mediaType + ' from ' + user.uid + ' - ' + e.toString(), 'error');
                    }
                });
            },

            initMediaPublishing: function() {

                // get a new token
                this.getMyMediaServiceToken(async () => {

                    try {

                        // join
                        await this.mediaServiceClient.join(
                            this.appId,
                            this.meetingData.inMeetingForClient.clientMediaServiceRoomIdentifier,
                            this.meetingData.myMediaServiceToken,
                            +this.meetingData.myMediaServiceIdentifier
                        );

                        this.log('Joined agora channel :)');
                    }
                    catch (e) {
                        this.log('could not join the room! - ' + e.toString(), 'error');
                    }

                    try {
                        // publish
                        await this.mediaServiceClient.publish([this.myAudio, this.myVideo]);

                        this.log('Started publishing own audio and video to channel :)');
                    }
                    catch (e) {
                        this.log('could not publish media to media service! - ' + e.toString(), 'error');
                    }

                });

            },

            // mute/unmute cam/mic
            stopCamera: async function () {
                try {
                    await this.myVideo.setEnabled(false);
                    this.socketClient.send("/app/myCameraIsOff", {},
                        JSON.stringify({sessionKey: '{{$performer->session_key}}'})
                    );
                    this.$btnStopCamera.addClass('d-none');
                    this.$btnStartCamera.removeClass('d-none');
                    // TODO log
                } catch (e) {
                    this.log('could not stop camera! - ' + e.toString(), 'error');
                }
            },
            startCamera: async function () {
                try {
                    await this.myVideo.setEnabled(true);
                    this.socketClient.send("/app/myCameraIsOn", {},
                        JSON.stringify({sessionKey: '{{$performer->session_key}}'})
                    );
                    this.$btnStartCamera.addClass('d-none');
                    this.$btnStopCamera.removeClass('d-none');
                    // TODO log
                } catch (e) {
                    this.log('could not start camera! - ' + e.toString(), 'error');
                }
            },
            muteAudio: async function () {
                try {
                    await this.myAudio.setEnabled(false);
                    this.socketClient.send("/app/myMicrophoneIsOff", {},
                        JSON.stringify({sessionKey: '{{$performer->session_key}}'})
                    );
                    this.$btnMuteAudio.addClass('d-none');
                    this.$btnUnmuteAudio.removeClass('d-none');
                    this.$selfView.attr('participant-muted', 1);
                    // TODO log
                } catch (e) {
                    this.log('could not mute audio! - ' + e.toString(), 'error');
                    // TODO log
                }
            },
            unmuteAudio: async function () {
                try {
                    await this.myAudio.setEnabled(true);
                    this.socketClient.send("/app/myMicrophoneIsOn", {},
                        JSON.stringify({sessionKey: '{{$performer->session_key}}'})
                    );
                    this.$btnUnmuteAudio.addClass('d-none');
                    this.$btnMuteAudio.removeClass('d-none');
                    this.$selfView.removeAttr('participant-muted');
                    // TODO log
                } catch (e) {
                    this.log('could not unmute audio! ' + e.toString(), 'error');
                }
            },

            // utils - start ============================================================== //

            // generic logger
            log: function(_message, _type = 'log') {
                console[_type](
                    'StagVideo => ' + '[' + (new Date()).toLocaleTimeString() + '] ' + this.myName,
                    _message);
                // post to userEventLog
                $.post('/api/userEventLog/create', {
                    eventName: 'video-event',
                    dataJson: JSON.stringify({type: _type, message: _message})
                }, function () {}, 'json');
            },

            // ajax request with error checking and reporting
            ajax: function (_url, _input, _callback) {
                $.post(_url, _input, (_data) => {
                    if (!this.hasError(_data)) {
                        _callback.call(this, _data);
                    }
                }, 'json');
            },

            // check and report error
            hasError: function (_data) {
                let msg = 'Unknown error!';
                if (_data) {
                    if (_data.success) return false;
                    else if (_data.message) msg = _data.message;
                }
                toastr.error(msg);
                return true;
            }

            // utils - end   ============================================================== //

        };

        window.StagVideo.init();

    }).call(window);
</script>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
@include('app/patient/partials/mcp-queue')

</body>
</html>
