import {setExactTimeout} from "../../models/utils";
import localEmitter from "../wt-emitter";

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

	//SYNC VARS
	let _emitter = localEmitter();

	let _playerInterface = null;
	let _playerInterfaceOptions = {
		isLive: true,
		ignoreBufferedTimeRanges: true,
		useSeekBy: false,
		syncBySegments: false,
		disableFastSeek: false
	};
	const syncDefaultWaitTime = 60000;
	const syncShortWaitTime = 10000;
	const maxSyncThreshold = 0.5;
	const maxSyncTries = 3;
	const waitForPlayingEventAfterSeek = 5000;
	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(_playerInterface.isPaused) _playerInterface.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(realPosition, seekDuration, getCurrentSegmentPosition());
								room._log(`Insufficient buffer: `, !isBufferSufficient)
								room._log(`Seek duration is ${seekDuration} seconds`);
								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(_playerInterface.isPaused === false) {
			_emitter.once('timeupdate', restartSyncLoop);
		}
	};

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

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

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

	const calculateSyncDifferenceTime = (fragmentSn, fragmentPos, ping) => {

		let seekRanges = _playerInterface.buffered;
		let position = (fragmentPos + (ping / 2)) / 1000;

		if(seekRanges && !_playerInterfaceOptions.ignoreBufferedTimeRanges) {
			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;
				}
			}
			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, isBufferSufficient: false}
		} else {
			return { position, realPosition: position, isBufferSufficient: true };
		}
	};

	const seekTo = (time) => {
		return new Promise((resolve, reject) => {
			if(_playerInterface.currentTime !== time) {
				let diff = time - _playerInterface.currentTime;
				if(_playerInterface.currentTime < time && diff < 4) {
					room._log(`Fast forward to seek...`);
					let wasPaused = false;
					let _handleFailed = () => {
						if(wasPaused) {
							_playerInterface.pause();
						}
						_playerInterface.setPlaybackRate(1);
						isProgrammaticallySeeked = false;
						reject('Stalled');
					};
					let _fastForward = () => {
						isProgrammaticallySeeked = true;
						setExactTimeout(() => {
							_emitter.off('stalled', _handleFailed);
							if(wasPaused && !_playerInterfaceOptions.isLive) {
								_playerInterface.pause();
							}
							_playerInterface.setPlaybackRate(1);
							isProgrammaticallySeeked = false;
							resolve();
						}, (diff * 1000) / (playbackRate - 1), 20);
						_playerInterface.setPlaybackRate(playbackRate);
					};

					_emitter.once('stalled', _handleFailed);

					if(_playerInterface.isPaused) {
						wasPaused = true;
						_playerInterface.play()
							.then(() => {
								_fastForward();
							})
							.catch(_handleFailed);
					} else {
						_fastForward();
					}
				}
				else {
					room._log(`Jump to seek...`);
					let currentTimestamp = Date.now();
					let __failsafeId = null
					let _resolve = () => {
						clearTimeout(__failsafeId);
						__failsafeId = null;
						isProgrammaticallySeeked = false;
						room._log(`It took the player ${(Date.now() - currentTimestamp) / 1000} seconds to seek `);
						resolve();
					};
					isProgrammaticallySeeked = true; // we need to ignore stall events since those are false alarm
					_emitter.once('playing', _resolve);
					__failsafeId = setTimeout(_resolve, waitForPlayingEventAfterSeek);
					_playerInterface.seek(time);
				}
			} else resolve()
		});
	};

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

	const handlePause = () => {
		stopSyncLoop()
		clientPaused().catch(() => {});
		isPlaying = !_playerInterface.isPaused;
	};

	const handleBuffering = () => {
		room._log('handleBuffering');
		if(!isProgrammaticallySeeked) {
			stopSyncLoop();
			clientPaused().catch(() => {});
			isPreloading = _playerInterface.isPaused;
		}
	};

	const roomSyncSend = (slaveId) => {

		if(!_playerInterface) {
			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'],

		getHandlers: () => ({
			handlePause: (event) => {
				room._log('handlePause');
				_emitter.emit('pause', event);
			},
			handlePlaying: (event) => {
				room._log('handlePlaying');
				_emitter.emit('playing', event);
			},
			handleBuffering: (event) => {
				room._log('handleBuffering');
				_emitter.emit('buffering', event);
			},
			handleStalled: (event) => {
				room._log('handleBuffering');
				room._log('handleStalled');
				_emitter.emit('buffering', event);
				_emitter.emit('stalled', event);
			},
			handleTimeupdate: (event) => {
				room._log('handleTimeupdate');
				_emitter.emit('timeupdate', event);
			}
		}),

		initialize: ({playerInterface} = {}) => {

			_playerInterface = playerInterface;
			_playerInterfaceOptions = {..._playerInterfaceOptions, ...(_playerInterface.options || {})}

			room._log('Interface options passed: ', (_playerInterface.options || {}));
			room._log('All interface options: ', _playerInterfaceOptions );

			shouldPropagateMaster = false;

			isPlaying = _playerInterface.isPaused === 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(_playerInterface.isPaused === false) {
				_emitter.once('timeupdate', restartSyncLoop);
			}

			_emitter.on('buffering', handleBuffering);
			_emitter.on('playing', handlePlaying);
			_emitter.on('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(typeof _playerInterface?.destroy === 'function') {
				_playerInterface?.destroy()
			}

			_playerInterface = null;

			_emitter.clear();
		}
	};

};

export default syncDazn;
