class sciencePreloadLinks {

	constructor( browser, config ) {
		this.browser = browser;
		this.config  = config;
		this.options = this.browser.options;

		this.prefetched = new Set;
		this.eventTime  = null;
		this.threshold  = 1111;
		this.numOnHover = 0;
	}

	/**
	 * Initializes the handler.
	 */
	init() {
		if (
			! this.browser.supportsLinkPrefetch()
			||
			this.browser.isDataSaverModeOn()
			||
			this.browser.isSlowConnection()
		) {
			return;
		}

		this.regex = {
			excludeUris: RegExp( this.config.excludeUris, 'i' ),
			images: RegExp( '.(' + this.config.imageExt + ')$', 'i' ),
			fileExt: RegExp( '.(' + this.config.fileExt + ')$', 'i' )
		};

		this._initListeners( this );
	}

	/**
	 * Initializes the event listeners.
	 *
	 * @private
	 *
	 * @param self instance of this object, used for binding "this" to the listeners.
	 */
	_initListeners( self ) {
		// Setting onHoverDelay to -1 disables the "on-hover" feature.
		if ( this.config.onHoverDelay > -1 ) {
			document.addEventListener( 'mouseover', self.listener.bind( self ), self.listenerOptions );
		}

		document.addEventListener( 'mousedown', self.listener.bind( self ), self.listenerOptions );
		document.addEventListener( 'touchstart', self.listener.bind( self ), self.listenerOptions );
	}

	/**
	 * Event listener. Processes when near or on a valid <a> hyperlink.
	 *
	 * @param Event event Event instance.
	 */
	listener( event ) {
		const linkElem = event.target.closest( 'a' );
		const url      = this._prepareUrl( linkElem );
		if ( null === url ) {
			return;
		}

		switch ( event.type ) {
			case 'mousedown':
			case 'touchstart':
				this._addPrefetchLink( url );
				break;
			case 'mouseover':
				this._earlyPrefetch( linkElem, url, 'mouseout' );
		}
	}

	/**
	 *
	 * @private
	 *
	 * @param Element|null linkElem
	 * @param object url
	 * @param string resetEvent
	 */
	_earlyPrefetch( linkElem, url, resetEvent ) {
		const doPrefetch = () => {
			falseTrigger = null;

			// Start the rate throttle: 1 sec timeout.
			if ( 0 === this.numOnHover ) {
				setTimeout( () => this.numOnHover = 0, 1000 );
			}
			// Bail out when exceeding the rate throttle.
			else if ( this.numOnHover > this.config.rateThrottle ) {
				return;
			}

			this.numOnHover++;
			this._addPrefetchLink( url );
		};

		// Delay to avoid false triggers for hover/touch/tap.
		let falseTrigger = setTimeout( doPrefetch, this.config.onHoverDelay );

		// On reset event, reset the false trigger timer.
		const reset = () => {
			linkElem.removeEventListener( resetEvent, reset, { passive: true } );
			if ( null === falseTrigger ) {
				return;
			}

			clearTimeout( falseTrigger );
			falseTrigger = null;
		};
		linkElem.addEventListener( resetEvent, reset, { passive: true } );
	}

	/**
	 * Adds a <link rel="prefetch" href="<url>"> for the given URL.
	 *
	 * @param string url The Given URL to prefetch.
	 */
	_addPrefetchLink( url ) {
		this.prefetched.add( url.href );

		return new Promise( ( resolve, reject ) => {
			const elem   = document.createElement( 'link' );
			elem.rel     = 'prefetch';
			elem.href    = url.href;
			elem.onload  = resolve;
			elem.onerror = reject;

			document.head.appendChild( elem );
		} ).catch(() => {
			// ignore and continue.
		});
	}

	/**
	 * Prepares the target link's URL.
	 *
	 * @private
	 *
	 * @param Element|null linkElem Instance of the link element.
	 * @returns {null|*}
	 */
	_prepareUrl( linkElem ) {
		if (
			null === linkElem
			||
			typeof linkElem !== 'object'
			||
			! 'href' in linkElem
			||
			// Link prefetching only works on http/https protocol.
			[ 'http:', 'https:' ].indexOf( linkElem.protocol ) === -1
		) {
			return null;
		}

		const origin   = linkElem.href.substring( 0, this.config.siteUrl.length );
		const pathname = this._getPathname( linkElem.href, origin );
		const url      = {
			original: linkElem.href,
			protocol: linkElem.protocol,
			origin: origin,
			pathname: pathname,
			href: origin + pathname
		};

		return this._isLinkOk( url ) ? url : null;
	}

	/**
	 * Gets the URL's pathname. Note: ensures the pathname matches the permalink structure.
	 *
	 * @private
	 *
	 * @param object url Instance of the URL.
	 * @param string origin The target link href's origin.
	 * @returns {string}
	 */
	_getPathname( url, origin ) {
		let pathname = origin
			? url.substring( this.config.siteUrl.length )
			: url;

		if ( ! pathname.startsWith( '/' ) ) {
			pathname = '/' + pathname;
		}

		if ( this._shouldAddTrailingSlash( pathname ) ) {
			return pathname + '/';
		}

		return pathname;
	}

	_shouldAddTrailingSlash( pathname ) {
		return (
			this.config.usesTrailingSlash
			&&
			! pathname.endsWith( '/' )
			&&
			! this.regex.fileExt.test( pathname )
		);
	}

	/**
	 * Checks if the given link element is okay to process.
	 *
	 * @private
	 *
	 * @param object url URL parts object.
	 *
	 * @returns {boolean}
	 */
	_isLinkOk( url ) {
		if ( null === url || typeof url !== 'object' ) {
			return false;
		}

		return (
			! this.prefetched.has( url.href )
			&&
			url.origin === this.config.siteUrl // is an internal document.
			&&
			url.href.indexOf( '?' ) === -1 // not a query string.
			&&
			url.href.indexOf( '#' ) === -1 // not an anchor.
			&&
			! this.regex.excludeUris.test( url.href ) // not excluded.
			&&
			! this.regex.images.test( url.href ) // not an image.
		);
	}

	/**
	 * Named static constructor to encapsulate how to create the object.
	 */
	static run() {
		// Bail out if the configuration not passed from the server.
		if ( typeof sciencePreloadLinksConfig === 'undefined' ) {
			return;
		}

		const browser  = new scienceBrowserCompatibilityChecker( {
			capture: true,
			passive: true
		} );
		const instance = new sciencePreloadLinks( browser, sciencePreloadLinksConfig );
		instance.init();
	}
}

sciencePreloadLinks.run();
