$( () => {

	/**
	 * Parse a line ID, e.g. "L-18"
	 *
	 * @param {string} id Line ID fragment
	 * @return {Object} Object with a string prefix and number
	 */
	function parseId( id ) {
		const matches = id.match( /(.*)-([0-9]+)/ );
		return {
			prefix: matches[ 1 ],
			number: +matches[ 2 ]
		};
	}

	/**
	 * Build a line ID from a parsed ID
	 *
	 * @param {Object} parsedId See #parseId
	 * @return {string} ID fragment
	 */
	function buildId( parsedId ) {
		return parsedId.prefix + '-' + parsedId.number;
	}

	/**
	 * Get a line element from an ID
	 *
	 * @param {string} id ID
	 * @return {HTMLElement|null} Line element, or null if not found (or the element is not a line)
	 */
	function getLineElement( id ) {
		const line = mw.util.getTargetFromFragment( id );
		// Support IE 11, Edge 14, Safari 7: Can't use unprefixed Element.matches('… *') yet.
		if ( !$( line ).closest( '.mw-highlight' ).length ) {
			// Element not in a highlight block
			return null;
		}
		return line;
	}

	let lastLines, lastAnchorLine;

	/**
	 * Handle hash change events
	 *
	 * @param {boolean} scrollIntoView Scroll the selected lines into view
	 */
	function onHashChange( scrollIntoView ) {
		const hash = location.hash.slice( 1 );

		const lines = [];
		let anchorLine, focusLine;
		const parts = hash.split( '--' );
		if ( parts.length === 2 ) {
			anchorLine = getLineElement( parts[ 0 ] );
			focusLine = getLineElement( parts[ 1 ] );
			if ( anchorLine && focusLine ) {
				const anchorId = parseId( parts[ 0 ] );
				const focusId = parseId( parts[ 1 ] );
				if ( anchorId.prefix === focusId.prefix ) {
					for ( let i = Math.min( anchorId.number, focusId.number ); i <= Math.max( anchorId.number, focusId.number ); i++ ) {
						lines.push( mw.util.getTargetFromFragment( buildId( { prefix: anchorId.prefix, number: i } ) ) );
					}
					if ( scrollIntoView ) {
						// A line range will not automatically scroll into view
						lines[ 0 ].scrollIntoView();
					}
				}
			}
		} else {
			anchorLine = getLineElement();
			if ( anchorLine ) {
				lines.push( anchorLine );
			}
		}

		lastAnchorLine = anchorLine;

		if ( lastLines ) {
			lastLines.forEach( ( line ) => line.classList.remove( 'hll' ) );
		}
		lines.forEach( ( line ) => line.classList.add( 'hll' ) );

		lastLines = lines;
	}

	window.addEventListener( 'hashchange', onHashChange );

	$( document.body ).on( 'click', '.mw-highlight .linenos', ( e ) => {
		e.preventDefault();

		const targetUrl = new URL( e.target.parentNode.href );

		if ( e.shiftKey && lastAnchorLine ) {
			const anchorId = parseId( lastAnchorLine.getAttribute( 'id' ) );
			const focusId = parseId( targetUrl.hash.slice( 1 ) );
			if ( anchorId.prefix === focusId.prefix ) {
				const hash = buildId( anchorId ) + '--' + buildId( focusId );
				history.replaceState( null, '', '#' + hash );
			} else {
				history.replaceState( null, '', targetUrl );
			}
		} else {
			history.replaceState( null, '', targetUrl );
		}

		onHashChange();
	} );

	// Check hash on load
	onHashChange( true );

} );