
/*
 *  TODO:
 *  Sync we get
 *  machineTimestamp = now
 *  ping = (endRequestTimestamp - startRequestTimestamp) / 2
 *  clockDifference = (serverTimestamp - machineTimestamp - ping)
 *
 *
 *  usable vars   : (now +- clockDifference) === currentServerTimestamp ---> when event was received
 *                : (otherMachineTimestamp +- otherMachineClockDifference) === currentServerTimestamp ---> when event was sent
 *
 *
 *  ping: (this user currentServerTimestamp a.k.a when event was received) - (remote user currentServerTimestamp a.k.a. when event was sent )
 *
 *
 * */


import {setExactTimeout} from "../../models/utils";

const syncHlsJs = function ({room, wt, roomSession, emitter} = {}) {

    //SYNC VARS
    let _libraryInstance = null;
    let _videoElement = null;
    let currentFragmentSn = null;

    const syncDefaultWaitTime = 60000;
    const syncShortWaitTime = 10000;
    const maxSyncThreshold = 0.5;
    const maxSyncTries = 3;
    let   currentSyncRetry = 0;
    let   syncWaitId = null;
    let   syncNextWaitTime = null;
    let   stopFlag = false;
    let   isSyncing = false;

    let playbackRate = 2;
    let isPlaying = false;
    let isPreloading = true;
    let isProgrammaticallySeeked = false;
    let shouldPropagateMaster = false;

    const startSyncLoop = () => {

        if(!isConnected()) {
            room._log('--- Sync loop will not start due to user not connected yet ---');
            return
        }

        if(syncWaitId) {
            room._log('--- Sync loop already running ---');
            return
        }

        room._log('--- Sync enabled ---');

        stopFlag = false;

        const loop = () => {

            isSyncing = true;
            emitter.emit('playerSyncing', true);

            sync().finally(() => {

                isSyncing = false;
                emitter.emit('playerSyncing', false);

                if(isConnected() && !stopFlag) {
                    syncWaitId = setTimeout(loop, syncNextWaitTime);
                }
                else {
                    room._log('--- Automatic stop due to user not connected or stop flag enabled ---');
                    stopSyncLoop();
                }

            })

        };

        loop();

    };

    const stopSyncLoop = () => {

        room._log('--- Sync disabled ---');

        clearTimeout(syncWaitId);
        syncWaitId = null;
        currentSyncRetry = 0
        stopFlag = true;

    };

    const restartSyncLoop = () => {

        room._log('--- Sync restarting ---');
        stopSyncLoop();
        startSyncLoop();

    };

    const setNextWaitTime = (didSyncFail = false) => {

        if(!didSyncFail) {
            syncNextWaitTime = syncDefaultWaitTime;
            currentSyncRetry = 0
        } else {
            currentSyncRetry++;
            if(currentSyncRetry > maxSyncTries) {
                syncNextWaitTime = syncDefaultWaitTime
                currentSyncRetry = 0;
            } else {
                syncNextWaitTime = syncShortWaitTime;
            }
        }

        room._log('--- Next sync will occur in ' + syncNextWaitTime / 1000 + ' seconds ---');

    }

    const sync = () => {

        return getSyncData()
            .then(syncData => {

                if(syncData.isMaster) {
                    if(_videoElement.paused) _videoElement.play();
                    setNextWaitTime(false);
                    return Promise.resolve();
                }

                else if(shouldPropagateMaster) {
                    setNextWaitTime(false);
                    return propagateMasterFunc();
                }

                else {

                    const syncStartTime = Date.now();
                    const {position, isBufferSufficient} = calculateSyncDifferenceTime(syncData.masterFragmentSn, syncData.masterFragmentPos, syncData.ping);

                    if(position && Math.abs(position) <=  maxSyncThreshold) {
                        room._log(`We're within max sync threshold, no need to resync now`);
                        setNextWaitTime(false);
                        return Promise.resolve();
                    }

                    if(position !== null) {
                        return seekBy(position)
                            .then(() => {
                                const seekDuration = (Date.now() - syncStartTime);
                                const {position, isBufferSufficient} = calculateSyncDifferenceTime(syncData.masterFragmentSn, syncData.masterFragmentPos + seekDuration, syncData.ping);
                                const syncPrecision =  Math.abs(position);

                                room._log(`Insufficient buffer: `, !isBufferSufficient);
                                room._log(`Seek duration is ${seekDuration / 1000}`);
                                room._log(`Sync precision should be ${syncPrecision}`);

                                const didSyncFail = syncPrecision > maxSyncThreshold
                                setNextWaitTime(didSyncFail);
                                return Promise.resolve();
                            })
                            .catch((e) => {
                                setNextWaitTime(true);
                                return Promise.reject(e);
                            });
                    } else {
                        setNextWaitTime(true);
                        return Promise.reject();
                    }
                }
            }).catch(() => {
                setNextWaitTime(true);
                return Promise.reject();
            })
    }

    const handleAddLocalParticipant = () => {
        if(shouldPropagateMaster) {
            propagateMasterFunc();
        }
        if(_videoElement && _videoElement.paused === false) {
            _videoElement.addEventListener('progress', () => {
                restartSyncLoop();
            }, {once:true});
        }
    };

    const handleAddRemoteParticipant = () => {
        if(shouldPropagateMaster) {
            propagateMasterFunc();
        }
    };

    const isConnected = () => {
        return room._isDataChannelOpen && room.isConnected;
    };

    const buffering = (event, data = {}) => {
        if(data.details === 'bufferStalledError') {
            handleStalledWaiting();
        }
    };

    const hlsFragChanged = (event, data) => {
        currentFragmentSn = data.frag.sn;
    };

    const parseDataEvents = (msg = {}) => {
        if(msg.videoroom === 'sync_source_set' && msg.wt_channel_id != "") {
            emitter.emit('changePlayerSource', msg.wt_channel_id);
        }
        else if(msg.videoroom === 'sync_request') {
            roomSyncSend(msg.sync_slave_id).catch(()=>{});
        }
    };

    const getCurrentSegmentPosition = () => {
        let fragments = _libraryInstance.streamController.fragmentTracker.fragments;
        if(!fragments) {
            return 0;
        }
        let currentFragment = fragments[Object.keys(fragments).find(key => key.indexOf(currentFragmentSn) > -1)];
        if(!currentFragment) {
            return 0;
        }
        return Math.max((_libraryInstance.streamController.lastCurrentTime - (currentFragment.body?.startPTS || 0)) * 1000, 0)
    };

    const calculateSyncDifferenceTime = (masterFragmentSn, masterFragmentPos, ping) => {

        let fragments = _libraryInstance.streamController.fragmentTracker.fragments;
        let fragmentNumbers = Object.keys(fragments).map(key => parseInt(key.slice(key.lastIndexOf('_') + 1))).sort(function(a, b){return a - b});

        // our position in fragment
        //let currentFragmentSn = currentFragmentSn;
        let currentFragment = fragments[Object.keys(fragments).find(key => key.indexOf(currentFragmentSn) > -1)];

        if(!currentFragment) {
            room._log('Current fragment not found, what now?', currentFragmentSn);
            return {position: null, isBufferSufficient: false}
        }

        let currentFragmentPos = Math.max((_libraryInstance.streamController.lastCurrentTime - (currentFragment.body?.startPTS || 0)) * 1000, 0);
        let currentFragmentLength = currentFragment.body.duration * 1000;

        room._log(`Our current fragment is: ${currentFragmentSn}`);
        room._log(`Our current fragment position is: ${currentFragmentPos}`);
        room._log(`Our current fragment length is: ${currentFragmentLength}`);

        // real server fragment
        let realMasterFragmentSn = masterFragmentSn;

        // real server position in fragment
        let realMasterFragmentPos = Math.max((ping / 2) + masterFragmentPos, 0);

        // simple flag
        let isBufferSufficient = false;

        /* Searching for fragment and in-fragment position when taking all delays into account */
        let relevantFragmentNumbers = fragmentNumbers.indexOf(masterFragmentSn) > -1 ? fragmentNumbers.slice(fragmentNumbers.indexOf(masterFragmentSn)) : [];

        for(let i = 0, len = relevantFragmentNumbers.length; i < len; i++) {

            let sn = relevantFragmentNumbers[i];
            let fragment = fragments[Object.keys(fragments).find(key => key.indexOf(sn) > -1)];
            let fragmentDuration = (fragment.body.duration * 1000);

            room._log(`Correction diff: ${realMasterFragmentPos - fragmentDuration}`);
            realMasterFragmentSn = sn;
            if((realMasterFragmentPos - fragmentDuration) > 0) {
                room._log(`Not enough at fragment ${sn}`);
                realMasterFragmentPos -= fragmentDuration;
            }
            else {
                room._log(`Enough at fragment ${sn}`);
                isBufferSufficient = true;
                break;
            }
        }

        // We're not ready yet
        if(!isBufferSufficient) {
            room._log(`We don't have required fragment ${realMasterFragmentSn} yet`);
            return {position: null, isBufferSufficient: false}
        }

        // We are too ahead

        if(realMasterFragmentSn < currentFragmentSn) {

            room._log(`We are ahead of master...`);
            room._log(`Real master fragment: ${realMasterFragmentSn}`);
            room._log(`Our fragment: ${currentFragmentSn}`);

            let seekTime = currentFragmentPos - realMasterFragmentPos;
            let relevantFragmentNumbers = fragmentNumbers.slice(fragmentNumbers.indexOf(realMasterFragmentSn));

            for(let i = 0, len = relevantFragmentNumbers.length; i < len; i++) {
                let sn = relevantFragmentNumbers[i];
                let fragment = fragments[Object.keys(fragments).find(key => key.indexOf(sn) > -1)];
                let fragmentDuration = (fragment.body.duration * 1000);

                if(sn >= currentFragmentSn) {
                    break;
                }

                seekTime += fragmentDuration;
            }

            room._log(`Sync difference is: ${seekTime}`);
            return {position: (seekTime / 1000) * -1, isBufferSufficient: true}
        }

        // We are behind or spot on

        else {

            room._log(`We're behind or spot on master...`);
            room._log(`Real master fragment: ${realMasterFragmentSn}`);
            room._log(`Our fragment: ${currentFragmentSn}`);

            let seekTime = realMasterFragmentPos - currentFragmentPos;
            let relevantFragmentNumbers = fragmentNumbers.slice(fragmentNumbers.indexOf(currentFragmentSn));

            for(let i = 0, len = relevantFragmentNumbers.length; i < len; i++) {
                let sn = relevantFragmentNumbers[i];
                let fragment = fragments[Object.keys(fragments).find(key => key.indexOf(sn) > -1)];
                let fragmentDuration = (fragment.body.duration * 1000);

                if(sn >= realMasterFragmentSn) {
                    break;
                }

                seekTime += fragmentDuration;
            }

            room._log(`We can proceed with seek time: ${seekTime}`);
            return {position: (seekTime / 1000), isBufferSufficient: true}
        }
    };

    const seekBy = (time) => {
        //https://mcorp.no/lib/mediasync.js
        return new Promise((resolve, reject) => {
            if(time !== 0) {
                if(time > 0 && time < 4) {
                    room._log(`Fast forward to seek...`);
                    let wasPaused = false;
                    let __ = () => {
                        if(wasPaused) {
                            _videoElement.pause();
                        }
                        _videoElement.playbackRate = 1;
                        isProgrammaticallySeeked = false;
                        reject('Stalled');
                    };
                    let diff = time;
                    // _videoElement.addEventListener('waiting', __, {once:true});
                    _videoElement.addEventListener('stalled', __, {once:true});
                    setExactTimeout(() => {
                        if(wasPaused) {
                            _videoElement.pause();
                        }
                        _videoElement.playbackRate = 1;
                        isProgrammaticallySeeked = false;
                        // _videoElement.removeEventListener('waiting', __, {once:true});
                        _videoElement.removeEventListener('stalled', __, {once:true});
                        resolve();
                    }, (diff * 1000) / (playbackRate - 1), 20);
                    isProgrammaticallySeeked = true;
                    if(_videoElement.paused) {
                        wasPaused = true;
                        _videoElement.play().then(() => {
                            _videoElement.playbackRate = playbackRate;
                        })
                    } else {
                        _videoElement.playbackRate = playbackRate;
                    }
                } else {
                    room._log(`Jump to seek...`);
                    let __ = Date.now();
                    isProgrammaticallySeeked = true; // we need to ignore stall events since those are false alarm
                    _videoElement.addEventListener('playing', () => {
                        room._log(`It took the player ${(Date.now() - __) / 1000} seconds to seek `);
                        isProgrammaticallySeeked = false;
                        resolve();
                    }, {once:true});
                    _videoElement.currentTime += time;
                }
            } else resolve()
        });
    };

    const seekTo = (time) => {
        //https://webtiming.github.io/timingsrc/ use this
        //https://mcorp.no/lib/mediasync.js
        return new Promise((resolve, reject) => {
            if(_videoElement.currentTime !== time) {
                let diff = time - _videoElement.currentTime;
                if(_videoElement.currentTime < time && diff < 4) {
                    room._log(`Fast forward to seek...`);
                    let wasPaused = false;
                    let __ = () => {
                        if(wasPaused) {
                            _videoElement.pause();
                        }
                        _videoElement.playbackRate = 1;
                        isProgrammaticallySeeked = false;
                        reject('Stalled');
                    };
                    // _videoElement.addEventListener('waiting', __, {once:true});
                    _videoElement.addEventListener('stalled', __, {once:true});
                    setExactTimeout(() => {
                        if(wasPaused) {
                            _videoElement.pause();
                        }
                        _videoElement.playbackRate = 1;
                        isProgrammaticallySeeked = false;
                        // _videoElement.removeEventListener('waiting', __, {once:true});
                        _videoElement.removeEventListener('stalled', __, {once:true});
                        resolve();
                    }, (diff * 1000) / (playbackRate - 1), 20);
                    isProgrammaticallySeeked = true;
                    if(_videoElement.paused) {
                        wasPaused = true;
                        _videoElement.play().then(() => {
                            _videoElement.playbackRate = playbackRate;
                        })
                    } else {
                        _videoElement.playbackRate = playbackRate;
                    }
                }
                else {
                    room._log(`Jump to seek...`);
                    let __ = Date.now();
                    isProgrammaticallySeeked = true; // we need to ignore stall events since those are false alarm
                    _videoElement.addEventListener('playing', () => {
                        room._log(`It took the player ${(Date.now() - __) / 1000} seconds to seek `);
                        isProgrammaticallySeeked = false;
                        resolve();
                    }, {once:true});
                    _videoElement.currentTime = time;
                }
            } else resolve()
        });
    };

    const handlePlaying =  () => {
        if(!isProgrammaticallySeeked) {
            room._log('Handle playing');
            startSyncLoop();
        }
        isProgrammaticallySeeked = false;
        isPreloading = _videoElement.paused;
        isPlaying = !_videoElement.paused;
    };

    const handlePause = () => {
        stopSyncLoop()
        clientPaused().catch(() => {});
        isPlaying = !_videoElement.paused;
    };

    const handleStalledWaiting = () => {
        room._log('handleStalledWaiting');
        if(!isProgrammaticallySeeked) {
            stopSyncLoop();
            clientPaused().catch(() => {});
            isPreloading = _videoElement.paused;
        }
    };

    const roomSyncSend = (slaveId) => {

        if(!_libraryInstance || !_videoElement) {
            room._log(
                `I've been asked for position even if we don't have player attached.
                Does it mean I'm the master?`
            );
            return Promise.resolve();
        }

        let fragment = currentFragmentSn;
        let fragmentPosition = parseInt(getCurrentSegmentPosition());

        room._log(`Sending my position to ${slaveId}`);
        room._log(`Fragment: ${fragment}`);
        room._log(`Fragment position: ${fragmentPosition}`);

        return room.sendMessage(room.handleId, {
            body : {
                request: "sync_response",
                room: room.roomId,
                timestamp: new Date().getTime(),
                fragment: String(fragment),
                fragment_pos: Number(fragmentPosition),
                slave_id: room.webrtcVersion > 1000 ? String(slaveId) : Number(slaveId)
            }});
    };

    const getSyncData = () => {

        room._log('Sending roomSync request');
        let roomId = room.roomId;
        let fragment = currentFragmentSn;
        let fragmentPosition = parseInt(getCurrentSegmentPosition());

        return new Promise((resolve, reject) => {

            let now = new Date().getTime();
            let ping = null;

            let sid = setTimeout(() => {
                room.off('data', fn, this);
                reject('Timeout');
            }, 3000);

            let body = {
                request:  "sync",
                room: roomId,
                timestamp: new Date().getTime(),
                fragment: String(fragment),
                fragment_pos: Number(fragmentPosition)
            };

            let fn = (msg) => {

                if(msg.videoroom && ['sync', 'sync_response'].includes(msg.videoroom)) {

                    if(msg.sync_master_await) {
                        room._log('Waiting for master position');
                        if(!ping) {
                            ping = (new Date().getTime() - now);
                        }
                    }

                    else if(msg.sync_master_fragment || msg.sync_master_fragment_pos) {
                        room._log('Got master position data');
                        if(!ping) {
                            ping = (new Date().getTime() - now);
                        }

                        room._log(`I'm master: ${!!msg.sync_master_self}`);
                        room._log(`Ping: ${ping}`);
                        room._log(`Master fragment: ${msg.sync_master_fragment}`);
                        room._log(`Master fragment position: ${msg.sync_master_fragment_pos}`);

                        room.off('data', fn, this);
                        clearTimeout(sid);

                        resolve({
                            isMaster: !!msg.sync_master_self,
                            ping: ping,
                            masterFragmentPos: parseInt(msg.sync_master_fragment_pos),
                            masterFragmentSn: parseInt(msg.sync_master_fragment),
                        });
                    }

                    else {
                        clearTimeout(sid);
                        reject('Master lost connection')
                    }
                }
            };
            room.on('data', fn, this);
            room.sendMessage(room.handleId, {body}).then(fn).catch(e => {
                room.off('data', fn, this);
                clearTimeout(sid);
                reject(e)
            });

        });

    };

    const clientPaused = () => {
        room._log('Sending client paused');

        if(!isConnected()) {
            return Promise.resolve();
        }

        return room.sendMessage(room.handleId, {
            body:{
                request: "sync_paused",
                room: room.roomId,
                timestamp: new Date().getTime(),
                fragment:'0',
                fragment_pos:0
            }});
    };

    const propagateMasterFunc = () => {
        room._log('Propagating master');
        if(!isConnected()) {
            return Promise.resolve();
        }
        return room.sendMessage(room.handleId, {
            body:{
                request: "sync_source_set",
                room: room.roomId,
                timestamp: new Date().getTime(),
                wt_channel_id:"",
                fragment:"0",
                fragment_pos:0
            }});
    };

    return {

        __events: ['playerSyncing'],

        initialize: ({hlsInstance, libraryInstance, propagateMaster = false} = {}) => {

            _libraryInstance = hlsInstance || libraryInstance; // backwards comp;
            _videoElement = _libraryInstance.media

			if(!_libraryInstance) {
                room._log('No hls.js player instance!');
				return;
			}

            shouldPropagateMaster = propagateMaster;
            isPlaying = _videoElement.paused === false;

            room.on('disconnect', stopSyncLoop);
            room.on('removeLocalParticipant', stopSyncLoop);
            room.on('addLocalParticipant', handleAddLocalParticipant);
            room.on('addRemoteParticipant', handleAddRemoteParticipant);
            room.on('data', parseDataEvents);

            if(shouldPropagateMaster) {
                propagateMasterFunc().catch(() => {});
            }

            if(_videoElement && _videoElement.paused === false) {
                _libraryInstance.once('hlsFragChanged', () => {
                    restartSyncLoop();
                });
            }

            _libraryInstance.on('hlsError', buffering);
            _libraryInstance.on('hlsFragChanged', hlsFragChanged);
            _videoElement.addEventListener('playing', handlePlaying);
            _videoElement.addEventListener('pause', handlePause);

        },

        destroy: () => {

            stopSyncLoop();

            room.off('disconnect', stopSyncLoop);
            room.off('removeLocalParticipant', stopSyncLoop);
            room.off('addLocalParticipant', handleAddLocalParticipant);
            room.off('addRemoteParticipant', handleAddRemoteParticipant);
            room.off('data', parseDataEvents);

            if(_libraryInstance) {
                _libraryInstance.off('hlsError', buffering);
                _libraryInstance.off('hlsFragChanged', hlsFragChanged);
            }

            if(_videoElement) {
                _videoElement.removeEventListener('playing', handlePlaying);
                _videoElement.removeEventListener('pause', handlePause);
            }

            _libraryInstance = null;

        }
    };
};

export default syncHlsJs;
