import speakerViewHTML from './speaker-view.html'

import { marked } from 'marked';

/**
 * Handles opening of and synchronization with the reveal.js
 * notes window.
 *
 * Handshake process:
 * 1. This window posts 'connect' to notes window
 *    - Includes URL of presentation to show
 * 2. Notes window responds with 'connected' when it is available
 * 3. This window proceeds to send the current presentation state
 *    to the notes window
 */
const Plugin = () => {

	let connectInterval;
	let speakerWindow = null;
	let deck;

	/**
	 * Opens a new speaker view window.
	 */
	function openSpeakerWindow() {

		// If a window is already open, focus it
		if( speakerWindow && !speakerWindow.closed ) {
			speakerWindow.focus();
		}
		else {
			speakerWindow = window.open( 'about:blank', 'reveal.js - Notes', 'width=1100,height=700' );
			speakerWindow.marked = marked;
			speakerWindow.document.write( speakerViewHTML );

			if( !speakerWindow ) {
				alert( 'Speaker view popup failed to open. Please make sure popups are allowed and reopen the speaker view.' );
				return;
			}

			connect();
		}

	}

	/**
	 * Reconnect with an existing speaker view window.
	 */
	function reconnectSpeakerWindow( reconnectWindow ) {

		if( speakerWindow && !speakerWindow.closed ) {
			speakerWindow.focus();
		}
		else {
			speakerWindow = reconnectWindow;
			window.addEventListener( 'message', onPostMessage );
			onConnected();
		}

	}

	/**
		* Connect to the notes window through a postmessage handshake.
		* Using postmessage enables us to work in situations where the
		* origins differ, such as a presentation being opened from the
		* file system.
		*/
	function connect() {

		const presentationURL = deck.getConfig().url;

		const url = typeof presentationURL === 'string' ? presentationURL :
								window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search;

		// Keep trying to connect until we get a 'connected' message back
		connectInterval = setInterval( function() {
			speakerWindow.postMessage( JSON.stringify( {
				namespace: 'reveal-notes',
				type: 'connect',
				state: deck.getState(),
				url
			} ), '*' );
		}, 500 );

		window.addEventListener( 'message', onPostMessage );

	}

	/**
	 * Calls the specified Reveal.js method with the provided argument
	 * and then pushes the result to the notes frame.
	 */
	function callRevealApi( methodName, methodArguments, callId ) {

		let result = deck[methodName].apply( deck, methodArguments );
		speakerWindow.postMessage( JSON.stringify( {
			namespace: 'reveal-notes',
			type: 'return',
			result,
			callId
		} ), '*' );

	}

	/**
	 * Posts the current slide data to the notes window.
	 */
	function post( event ) {

		let slideElement = deck.getCurrentSlide(),
			notesElements = slideElement.querySelectorAll( 'aside.notes' ),
			fragmentElement = slideElement.querySelector( '.current-fragment' );

		let messageData = {
			namespace: 'reveal-notes',
			type: 'state',
			notes: '',
			markdown: false,
			whitespace: 'normal',
			state: deck.getState()
		};

		// Look for notes defined in a slide attribute
		if( slideElement.hasAttribute( 'data-notes' ) ) {
			messageData.notes = slideElement.getAttribute( 'data-notes' );
			messageData.whitespace = 'pre-wrap';
		}

		// Look for notes defined in a fragment
		if( fragmentElement ) {
			let fragmentNotes = fragmentElement.querySelector( 'aside.notes' );
			if( fragmentNotes ) {
				messageData.notes = fragmentNotes.innerHTML;
				messageData.markdown = typeof fragmentNotes.getAttribute( 'data-markdown' ) === 'string';

				// Ignore other slide notes
				notesElements = null;
			}
			else if( fragmentElement.hasAttribute( 'data-notes' ) ) {
				messageData.notes = fragmentElement.getAttribute( 'data-notes' );
				messageData.whitespace = 'pre-wrap';

				// In case there are slide notes
				notesElements = null;
			}
		}

		// Look for notes defined in an aside element
		if( notesElements && notesElements.length ) {
			// Ignore notes inside of fragments since those are shown
			// individually when stepping through fragments
			notesElements = Array.from( notesElements ).filter( notesElement => notesElement.closest( '.fragment' ) === null );

			messageData.notes = notesElements.map( notesElement => notesElement.innerHTML ).join( '\n' );
			messageData.markdown = notesElements[0] && typeof notesElements[0].getAttribute( 'data-markdown' ) === 'string';
		}

		speakerWindow.postMessage( JSON.stringify( messageData ), '*' );

	}

	/**
	 * Check if the given event is from the same origin as the
	 * current window.
	 */
	function isSameOriginEvent( event ) {

		try {
			return window.location.origin === event.source.location.origin;
		}
		catch ( error ) {
			return false;
		}

	}

	function onPostMessage( event ) {

		// Only allow same-origin messages
		// (added 12/5/22 as a XSS safeguard)
		if( isSameOriginEvent( event ) ) {

			try {
				let data = JSON.parse( event.data );
				if( data && data.namespace === 'reveal-notes' && data.type === 'connected' ) {
					clearInterval( connectInterval );
					onConnected();
				}
				else if( data && data.namespace === 'reveal-notes' && data.type === 'call' ) {
					callRevealApi( data.methodName, data.arguments, data.callId );
				}
		  } catch (e) {}

		}

	}

	/**
	 * Called once we have established a connection to the notes
	 * window.
	 */
	function onConnected() {

		// Monitor events that trigger a change in state
		deck.on( 'slidechanged', post );
		deck.on( 'fragmentshown', post );
		deck.on( 'fragmenthidden', post );
		deck.on( 'overviewhidden', post );
		deck.on( 'overviewshown', post );
		deck.on( 'paused', post );
		deck.on( 'resumed', post );

		// Post the initial state
		post();

	}

	return {
		id: 'notes',

		init: function( reveal ) {

			deck = reveal;

			if( !/receiver/i.test( window.location.search ) ) {

				// If the there's a 'notes' query set, open directly
				if( window.location.search.match( /(\?|\&)notes/gi ) !== null ) {
					openSpeakerWindow();
				}
				else {
					// Keep listening for speaker view hearbeats. If we receive a
					// heartbeat from an orphaned window, reconnect it. This ensures
					// that we remain connected to the notes even if the presentation
					// is reloaded.
					window.addEventListener( 'message', event => {

						if( !speakerWindow && typeof event.data === 'string' ) {
							let data;

							try {
								data = JSON.parse( event.data );
							}
							catch( error ) {}

							if( data && data.namespace === 'reveal-notes' && data.type === 'heartbeat' ) {
								reconnectSpeakerWindow( event.source );
							}
						}
					});
				}

				// Open the notes when the 's' key is hit
				deck.addKeyBinding({keyCode: 83, key: 'S', description: 'Speaker notes view'}, function() {
					openSpeakerWindow();
				} );

			}

		},

		open: openSpeakerWindow
	};

};

export default Plugin;
