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

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

    //SYNC VARS
    let _libraryInstance = null;
    let _videoElement = 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, realPosition, isBufferSufficient} = calculateSyncDifferenceTime(syncData.masterFragmentSn, syncData.masterFragmentPos, syncData.ping);
                    const currentPosition = getCurrentSegmentPosition() / 1000;


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

                    if(position !== null) {
                        return seekTo(position)
                            .then(() => {
                                const seekDuration = (Date.now() - syncStartTime) / 1000;
                                const syncPrecision =  Math.abs((realPosition + seekDuration) - (getCurrentSegmentPosition() / 1000))

                                room._log(`Insufficient buffer: `, !isBufferSufficient);
                                room._log(`Seek duration is ${seekDuration}`);
                                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) => {
        if(event.mediaType === 'video' || event.mediaType === 'audio') {
            if(event.state === 'bufferStalled') {
                handleStalledWaiting();
            }
        }
    };

    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 position = _videoElement && _videoElement.currentTime;
        return isNaN(position) ? 0 : position * 1000
    };

    const calculateSyncDifferenceTime = (fragmentSn, fragmentPos, ping) => {
        let seekRanges = _videoElement.buffered;
        let seekRange = {};
        for (let i = 0; i < seekRanges.length; i++) {
            let _c_start = seekRanges.start(i);
            let _c_end = seekRanges.end(i);
            if(!seekRange.start || _c_start < seekRange.start) {
                seekRange.start = _c_start;
            }
            if(!seekRange.end || _c_end > seekRange.end) {
                seekRange.end = _c_end;
            }
        }
        let position = (fragmentPos + (ping / 2)) / 1000;
        if(position > seekRange.start && position < seekRange.end) {
            return { position, realPosition:position, isBufferSufficient: true };
        } else if(position < seekRange.start) {
            room._log(`Syncing to ${seekRange.start} instead of ${position} due to lack of buffered data`);
            return { position: seekRange.start + 0.5, realPosition:position, isBufferSufficient: false};
        } else if(position > seekRange.end) {
            room._log(`Syncing to ${seekRange.end} instead of ${position} due to lack of buffered data`);
            return { position: seekRange.end - 0.5, realPosition:position, isBufferSufficient: false};
        } else
            return {position: null, realPosition:position, isBufferSufficient: false}
    };

    const seekTo = (time) => {
        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 fragmentPosition = parseInt(getCurrentSegmentPosition());

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

        return room.sendMessage(room.handleId, {
            body : {
                request: "sync_response",
                room: room.roomId,
                timestamp: new Date().getTime(),
                fragment: String("0"),
                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 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("0"),
                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: ({dashInstance, libraryInstance, propagateMaster = false} = {}) => {

            _libraryInstance = dashInstance || libraryInstance; // backwards comp;
            _videoElement = _libraryInstance.getVideoElement();

            if(!_libraryInstance) {
                room._log('No dash 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) {
                _videoElement.addEventListener('progress', () => {
                    restartSyncLoop();
                }, {once:true});
            }

            _libraryInstance.on('bufferStateChanged', buffering);
            _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('bufferStateChanged', buffering);
            }

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

            _libraryInstance = null;
        }
    };

};

export default syncDashJs;
