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

const syncModule = function ({room, emitter} = {}) {

	//SYNC VARS
	let _emitter = localEmitter();

	let _playerInterface = null;
	let _propagateMaster = false;
	let _playerInterfaceOptions = {
		type: 'none',
		ignoreBufferedTimeRanges: true,
		disableFastSeek: false
	};
	const syncDefaultWaitTime = 60000;
	const syncShortWaitTime = 10000;
	const maxSyncThreshold = 0.5;
	const maxSyncTries = 3;
	const waitForPlayingEventAfterSeek = 5000;
	const fastForwardThreshold = 4
	let   currentSyncRetry = 0;
	let   syncWaitId = null;
	let   syncNextWaitTime = null;
	let   stopFlag = false;
	let   isSyncing = false;
	let safeSpace = 2;
	let playbackRate = 2;
	let isPlaying = false;
	let isPreloading = true;
	let isProgrammaticallySeeked = false;


	const startSyncLoop = () => {

		if(!isConnected() && _playerInterfaceOptions.type !== 'push') {
			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;

		if(_playerInterfaceOptions.type !== 'push') {
			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();
		}
		else {
			const loop2 = () => {
				seekForward()
					.then(() => {
						setNextWaitTime(false);
					})
					.catch((e) => {
						setNextWaitTime(true);
						console.log('Sync failed', e);
					});
				syncWaitId = setTimeout(loop2, syncNextWaitTime);
			};
			loop2();
		}

	};

	const stopSyncLoop = (doNotResetRetryCount) => {
		room._log('--- Sync disabled ---');
		clearTimeout(syncWaitId);
		syncWaitId = null;
		if(!doNotResetRetryCount) {
			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(_propagateMaster) {
					setNextWaitTime(false);
					return propagateMasterFunc();
				}

				else {

					const syncStartTime = Date.now();
					const {position, realPosition, isBufferSufficient} = _playerInterface.getTimeDifference
						? _playerInterface.getTimeDifference(syncData.masterFragmentSn, syncData.masterFragmentPos, syncData.ping)
						:  getTimeDifference(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 && !isNaN(position)) {
						return seekBy(position)
							.then(() => {
								const seekDuration = (Date.now() - syncStartTime);
								const {position, realPosition, isBufferSufficient} = _playerInterface.getTimeDifference
									? _playerInterface.getTimeDifference(syncData.masterFragmentSn, syncData.masterFragmentPos + seekDuration, syncData.ping)
									:  getTimeDifference(syncData.masterFragmentSn, syncData.masterFragmentPos + seekDuration, syncData.ping);

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

								const didSyncFail = syncPrecision > maxSyncThreshold;
								setNextWaitTime(didSyncFail);
								return Promise.resolve();
							})
							.catch((e) => {
								room._log(`Seek failed: `, e);
								setNextWaitTime(true);
								return Promise.reject(e);
							});
					} else {
						room._log(`Position in not a number: `, position);
						setNextWaitTime(true);
						return Promise.reject();
					}
				}
			}).catch((e) => {
				room._log(`getSyncData() failed: `, e);
				setNextWaitTime(true);
				return Promise.reject();
			})
	}

	const handleAddLocalParticipant = () => {
		if(_propagateMaster) {
			propagateMasterFunc();
		}
		if(_playerInterface.isPaused === false) {
			_emitter.once('timeupdate', restartSyncLoop);
		}
	};

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

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

	const parseDataEvents = (msg = {}) => {
		if(msg.videoroom === 'sync_request') {
			roomSyncSend(msg.sync_slave_id).catch(()=>{});
		}
	};

	const getCurrentPlayerPosition = () => {
		let position = _playerInterface.currentTime;
		return isNaN(position) ? 0 : position * 1000
	};

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

		let seekRanges = _playerInterface.buffered;
		let position = (fragmentPos + (ping / 2)) / 1000;
		let currentPlayerPosition = getCurrentPlayerPosition() / 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: position - currentPlayerPosition, realPosition: position - currentPlayerPosition, 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 - currentPlayerPosition, realPosition: position - currentPlayerPosition, 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 - currentPlayerPosition, realPosition: position - currentPlayerPosition, isBufferSufficient: false};
			} else
				return {position: null, isBufferSufficient: false}
		} else {
			return { position: position - currentPlayerPosition, realPosition: position - currentPlayerPosition, isBufferSufficient: true };
		}
	};

	const seekForward = () => {
		return new Promise((resolve, reject) => {
			let diff = _playerInterface.buffered.end(0) - _playerInterface.currentTime;
			let diff_target = diff - safeSpace;

			let __ = () => {
				_playerInterface.setPlaybackRate(1);
				isProgrammaticallySeeked = false;
				isSyncing = false;
				reject('Stalled');
			};

			console.log('Users stream is behind behind live by:', diff);
			if(diff_target > 0) {
				_emitter.once('stalled', __);
				setExactTimeout(() => {
					_playerInterface.setPlaybackRate(1);
					isProgrammaticallySeeked = false;
					isSyncing = false;
					_emitter.off('stalled', __);
					resolve();
				}, (diff_target * 1000) / (playbackRate - 1), 20);
				isProgrammaticallySeeked = true;
				isSyncing = true;
				_playerInterface.setPlaybackRate(playbackRate);
			} else {
				resolve();
			}
		});
	};

	const seekBy = (time) => {
		return new Promise((resolve, reject) => {
			if(_playerInterface.currentTime !== time) {
				if(time > 0 && time < fastForwardThreshold && !_playerInterfaceOptions.disableFastSeek) {
					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 && _playerInterface.isVod) {
								_playerInterface.pause();
							}
							_playerInterface.setPlaybackRate(1);
							isProgrammaticallySeeked = false;
							resolve();
						}, (time * 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(_playerInterface.currentTime + time);
				}
			} else resolve()
		});
	};

	const seekTo = (time) => {
		return new Promise((resolve, reject) => {
			if(_playerInterface.currentTime !== time) {
				let diff = time - _playerInterface.currentTime;
				if(_playerInterface.currentTime < time && diff < fastForwardThreshold && !_playerInterfaceOptions.disableFastSeek) {
					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 && _playerInterface.isVod) {
								_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(true);
			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();
		}

		var fragmentData = _playerInterface?.currentFragment || {
			fragment: String("0"),
			fragment_pos: Number(parseInt(getCurrentPlayerPosition()))
		}

		room._log(`Sending my position to ${slaveId}`);
		room._log(`Current time: ${fragmentData.fragment} # ${fragmentData.fragment_pos}`);

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

	const getSyncData = () => {

		room._log('Sending roomSync request');
		let roomId = room.roomId;
		var fragmentData = _playerInterface?.currentFragment || {
			fragment: String("0"),
			fragment_pos: Number(parseInt(getCurrentPlayerPosition()))
		}

		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(),
				...fragmentData
			};

			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: ({playerInterface, propagateMaster = false} = {}) => {

			_playerInterface = playerInterface;
			_playerInterfaceOptions = {..._playerInterfaceOptions, ...(_playerInterface.syncSettings || {})}
			_propagateMaster = propagateMaster;

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

			if(_playerInterfaceOptions.type === 'none') {
				room._log('Sync module has been disabled via type:none');
				return;
			}

			// if(_playerInterfaceOptions.type === 'push') {
			// 	room._log('Push sync is not implemented in this sync module yet');
			// 	return;
			// }

			if(_playerInterface.isVod) {
				room._log('VOD sync is not implemented in this sync module yet');
				return;
			}

			playerInterface.addHandlers({
				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);
					_emitter.emit('stalled', event);
				},
				handleTimeupdate: (event) => {
					room._log('handleTimeupdate');
					_emitter.emit('timeupdate', event);
				}
			});

			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(_propagateMaster) {
				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);
			_playerInterface = null;

			_emitter.clear();
		}
	};

};

export default syncModule;
