/**
 * Wrapper class for the Instascan.js library.
 * Gain access to the browsers's cameras, create a video stream and provide options to
 * view or switch the active camera.
 * @see initialise
 *
 * @typedef {{id: string, name: string, _stream: MediaStream}} Camera
 */
class QrScanner {

	/**
	 * @param video HTML video element.
	 */
	constructor(video, sfxCount = 0, sfxElement = null) {
		if (video instanceof jQuery) {
			video = video[0];
		}

		this.cameras = {};
		this.activeCamId = null;

		this.myInstascan = new Instascan.Scanner({ video: video, mirror: false });

		if (sfxCount && sfxElement) {
			this.onScan((qrContent) => {
				// Play hashed SFX
				var sfx_id = (Math.abs(qrContent.hashCode()) % sfxCount) + 1;
				sfxElement.setAttribute('src','/Static/Audio/QRScanner/'+sfx_id+'.wav');
				sfxElement.volume = 0.8;
				sfxElement.play();
			})
		}
	}

	/**
	 * Request browser camera access and index the available cameras.
	 * @param {string|number|null} camId
	 * @returns {Promise} Promise, if fulfilled, containing the active camera hash as an argument.
	 *          May reject on: DOMException (permission denied), 'NoCameraError'
	 */
	initialise(camId = null) {
		return Instascan.Camera.getCameras()
		.then((currCams) => {

			if (currCams.length < 1) {
				throw 'NoCameraError';
			}

			// Re-structure the iterator returned by getCameras to make it indexable.
			// Index the cameras by hashCode (Note: the id property may contain characters unsuitable for a html id)
			// We use a hashCode instead of an arbitrary number so the hash can be re-used on seperate page requests.
			for (let i = 0; i < currCams.length; i++) {
				this.cameras[currCams[i].id.hashCode(true)] = currCams[i];
			}

			this.numCameras = currCams.length;

			// If a camera is specified but it isn't in our set:
			if (camId !== null && !(camId in this.cameras)) {
				console.warn(
					'Supplied camId ' + camId + ' does not correspond to any cameras in this environment.'
					+ ' The default camera will be used instead.'
				);
				camId = null;
			}

			// If camId is unset then use the first available camera
			return this.activateCamera(camId || currCams[0].id.hashCode(true));
		});
	}

	/**
	 * @param cb Callback. Param 1: Scan data.
	 */
	onScan(cb) {
		this.myInstascan.addListener('scan', cb);
	}

	/**
	 * Set a particular camera as current camera used for the video stream.
	 * @param {string|number} camId Id to activate. Strings and Numeric strings are identical in JSON properties.
	 */
	activateCamera(camId) {
		this.activeCamId = camId;
		return this.myInstascan.stop()
			.then(() => this.myInstascan.start(this.cameras[camId]))
			.then(() => camId);
	}

	/**
	 * @return {Camera}
	 */
	getActiveCamera() {
		// `_camera` is the property storing the current cam details
		return {shortId: this.activeCamId, camData: this.cameras[this.activeCamId]};
	}

	/**
	 * @return {MediaStreamTrack}
	 */
	getActiveMediaTrack() {
		return this.getActiveCamera().camData._stream.getVideoTracks()[0];
	}

	getCameras() {
		return this.cameras;
	}

	setMirroring(m) {
		this.myInstascan.mirror = m;
	}

	isMirroring() {
		return this.myInstascan.mirror;
	}
}