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

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


    //SYNC VARS
    let _videoElement = null;
    let _roomId = null;
    let _syncDisabled = false;

    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   seekingDebounceId = null
    let   seekingDebounceTimeout = 300;

    let isPlaying = false;
    let isProgrammaticallySeeked = false;
    let isForcedMaster = false;


    const startSyncLoop = () => {

        if(_syncDisabled) {
            room._log('--- Sync loop will not start due to sync force disabled ---');
            return
        }

        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 || stopFlag) {
                    setNextWaitTime(false);
                    return Promise.resolve();
                }

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

                else {

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

                    if(syncData.masterFragmentSn) {
                        playMedia()
                    } else {
                        pauseMedia()
                    }

                    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((position + (isPlaying ? seekDuration : 0)) - (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);
            })
    }

    const handleAddLocalParticipant = () => {
        if(isForcedMaster) {
            propagateMasterFunc();
        }
        if(_videoElement) {
            //TODO: shit fix ... data channel with other participant is not opened yet so we wait
            setTimeout(() => {
                restartSyncLoop();
            }, 1000);
        }
    };

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

    const handleRemoveRemoteParticipant = () => {
        if(isForcedMaster) {
            propagateMasterFunc();
        }
    };

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

    const executePlayerIotEvents = (data) => {
        let rid = _roomId || room.roomId;
        if(data.attributeName === 'playerReplay' && (!rid || data?.roomId === rid)) {
            seekTo(0).then(() => playMedia());
        }

        if(data.attributeName === 'playerPlay' && (!rid || data?.roomId === rid)) {
            playMedia();
        }

        if(data.attributeName === 'playerPause' && (!rid || data?.roomId === rid)) {
            pauseMedia();
        }
    };

    const parseIotEvents = (data) => {
        if(data.event === 'template_updated') {
            if(data.operation === 'multi') {
                (data.value || []).forEach(event => {
                    if (event.operation === 'sendMessage') {
                        executePlayerIotEvents({...event, roomId: data.roomId, userId: data.userId});
                    }
                })
            }
            if(data.operation === 'sendMessage') {
                executePlayerIotEvents(data);
            }
        }
    };

    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(()=>{});
        }
        else if(msg.videoroom === 'message') {

            // ignoring non studio commands if master
            if(isForcedMaster && msg.from !== null) {
                return;
            }

            if(_syncDisabled) {
                return;
            }

            if(msg.user_action === 'sync_seeking') {
                seekMedia(msg)
                    .catch(() => {

                    });
            }
            else if(msg.user_action === 'sync_seeked') {

            }
            else if(msg.user_action === 'sync_play' || msg.action === 'sync_play') {
                playMedia();
            }
            else if(msg.user_action === 'sync_pause' || msg.action === 'sync_pause') {
                pauseMedia();
            }
        }
    };

    const getCurrentSegmentPosition = () => {
        let position = _videoElement && _videoElement.currentTime;
        return isNaN(position) ? 0 : position * 1000
    };

    const calculateSyncDifferenceTime = (fragmentSn, fragmentPos = 0, ping) => {
        return {position: (fragmentPos + (ping / 2)) / 1000, isBufferSufficient: true};
    };

    const seekMedia = (msg) => {
        msg = {userId: msg.from, ...(msg.text && JSON.parse(msg.text))};
        if(msg.handleId !== room.handleId) {
            const {position, isBufferSufficient} = calculateSyncDifferenceTime(msg.fragment, msg.fragment_pos, 0); //TODO: right ping
            if(position) {
                return seekTo(position).then(() => {
                    if(isForcedMaster) {
                        propagateMasterFunc();
                    }
                });
            } else {
                return Promise.resolve();
            }
        } else {
            return Promise.resolve();
        }
    };

    const prePlay = () => {

        if(_videoElement.currentTime > 0 || _videoElement.paused === false) {
            return Promise.resolve();
        }

        isProgrammaticallySeeked = true;
        let wasMuted = _videoElement.muted;
        _videoElement.muted = true;
        return _videoElement.play()
            .then(() => {
                _videoElement.pause()
                _videoElement.currentTime = 0;
                return wait(100);
            })
			.catch(() => true)
            .finally(() => {
                _videoElement.muted = wasMuted
                isProgrammaticallySeeked = false;
                return true;
            })
    };

    const playMedia = () => {
        if(_videoElement && _videoElement.paused) {
            isProgrammaticallySeeked = true;
            _videoElement.play().finally(() => {
                isProgrammaticallySeeked = false;
            });
        }
    };

    const pauseMedia = () => {
        if(_videoElement && !_videoElement.paused) {
            isProgrammaticallySeeked = true;
            _videoElement.addEventListener('pause', () => {
                isProgrammaticallySeeked = false;
            }, {once:true});
            _videoElement.pause();
        }
    };

    const seekTo = (time) => {
        return new Promise((resolve) => {
            if(_videoElement.currentTime !== time) {
                isProgrammaticallySeeked = true; // we need to ignore stall events since those are false alarm
                _videoElement.addEventListener('seeked', () => {
                    isProgrammaticallySeeked = false;
                    resolve();
                }, {once:true});
                _videoElement.currentTime = time;
            } else resolve()
        });
    };

    const handlePlaying =  () => {
        if(!isProgrammaticallySeeked) {
            roomSession.sendSystemMessage('sync_play', {
                timestamp: new Date().getTime(),
                handleId: room.handleId,
            }, undefined, null).catch(() => {});
        }
        isPlaying = !_videoElement.paused;
    };

    const handlePause = () => {
        if(!isProgrammaticallySeeked) {
            roomSession.sendSystemMessage('sync_pause', {
                timestamp: new Date().getTime(),
                handleId: room.handleId,
            }, undefined, null).catch(() => {});
        }
        isPlaying = !_videoElement.paused;
    };

    const handleStalledWaiting = () => {
        if(!isProgrammaticallySeeked) {
            clientPaused().catch(() => {});
        }
    };

    const handleSeeking = () => {
        clearTimeout(seekingDebounceId);
        if(!isProgrammaticallySeeked) {
            seekingDebounceId = setTimeout(() => {
                let fragmentPosition = parseInt(getCurrentSegmentPosition());
                roomSession.sendSystemMessage('sync_seeking', {
                    timestamp: new Date().getTime(),
                    handleId: room.handleId,
                    fragment: "0", //isPlaying ? "1":"0",
                    fragment_pos: Number(fragmentPosition)
                }, undefined, null).catch(() => {});
                if(!isForcedMaster && !_syncDisabled) {
                    pauseMedia();
                }
            }, seekingDebounceTimeout);
        }
    };

    const handleSeeked = () => {

    };

    const handleEnded = () => {

    };

    const roomSyncSend = (slaveId) => {

        if(!_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: isPlaying ? "1":"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: isPlaying ? "1":"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: ({videoElement , propagateMaster = false, isStudio = false, roomId, syncDisabled} = {}) => {

            _videoElement = videoElement;
            _roomId = roomId;
            _syncDisabled = syncDisabled;

            if(!_videoElement) {
                room._log('No video element present!');
                return;
            }

            emitter.emit('playerSyncing', false);

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

            room.on('disconnect', stopSyncLoop);
            room.on('addLocalParticipant', handleAddLocalParticipant);
            room.on('removeLocalParticipant', stopSyncLoop);
            room.on('addRemoteParticipant', handleAddRemoteParticipant);
            room.on('removeRemoteParticipant', handleRemoveRemoteParticipant);
            room.on('data', parseDataEvents);
            wt.iot.$on('message', parseIotEvents);

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

            if(_videoElement) {
                (
                    propagateMaster || isStudio ?
                    Promise.resolve() :
                    prePlay()
                ).then(() => {
                    _videoElement.addEventListener('stalled', handleStalledWaiting);
                    _videoElement.addEventListener('playing', handlePlaying);
                    _videoElement.addEventListener('pause', handlePause);
                    _videoElement.addEventListener('seeking', handleSeeking);
                    _videoElement.addEventListener('seeked', handleSeeked);
                    _videoElement.addEventListener('ended', handleEnded);
                    startSyncLoop();
                })
            }
        },

        destroy: () => {

            stopSyncLoop();

            room.off('disconnect', stopSyncLoop);
            room.off('addLocalParticipant', handleAddLocalParticipant);
            room.off('removeLocalParticipant', stopSyncLoop);
            room.off('addRemoteParticipant', handleAddRemoteParticipant);
            room.off('removeRemoteParticipant', handleRemoveRemoteParticipant);
            room.off('data', parseDataEvents);
            wt.iot.$off('message', parseIotEvents);


            if(_videoElement) {
                _videoElement.removeEventListener('stalled', handleStalledWaiting);
                _videoElement.removeEventListener('playing', handlePlaying);
                _videoElement.removeEventListener('pause', handlePause);
                _videoElement.removeEventListener('seeking', handleSeeking);
                _videoElement.removeEventListener('seeked', handleSeeked);
                _videoElement.removeEventListener('ended', handleEnded);
            }

            _videoElement = null;
        }
    };

};

export default syncVodHlsJs;
