const
	featuresEnabled = mw.config.get( 'wgDiscussionToolsFeaturesEnabled' ) || {},
	CommentItem = require( './CommentItem.js' ),
	HeadingItem = require( './HeadingItem.js' ),
	utils = require( './utils.js' );
let lastHighlightedPublishedComment = null;

/**
 * Draw a semi-transparent rectangle on the page to highlight the given thread item.
 *
 * @class
 * @param {ThreadItem|ThreadItem[]} items Thread item(s) to highlight
 */
function Highlight( items ) {
	const highlightNodes = [];
	const ranges = [];

	this.topmostElement = null;

	items = Array.isArray( items ) ? items : [ items ];

	this.rootNode = items[ 0 ] ? items[ 0 ].rootNode : null;

	items.forEach( ( item ) => {
		const $highlight = $( '<div>' ).addClass( 'ext-discussiontools-init-highlight' );

		// We insert the highlight in the DOM near the thead item, so that it remains positioned correctly
		// when it shifts (e.g. collapsing the table of contents), and disappears when it is hidden (e.g.
		// opening visual editor).
		const range = item.getRange();
		// Support: Firefox
		// The highlight node must be inserted after the start marker node (data-mw-comment-start), not
		// before, otherwise Node#getBoundingClientRect() returns wrong results.
		range.insertNode( $highlight[ 0 ] );

		// If the item is a top-level comment wrapped in a frame, highlight outside that frame
		if ( item.level === 1 ) {
			const coveredSiblings = utils.getFullyCoveredSiblings( item, item.rootNode );
			if ( coveredSiblings ) {
				range.setStartBefore( coveredSiblings[ 0 ] );
				range.setEndAfter( coveredSiblings[ coveredSiblings.length - 1 ] );
			}
		}

		// If the item is a heading, highlight the full extent of it (not only the text)
		if ( item instanceof HeadingItem && !item.placeholderHeading ) {
			range.selectNode(
				$highlight.closest( '.mw-heading' )[ 0 ] ||
				$highlight.closest( 'h1, h2, h3, h4, h5, h6' )[ 0 ]
			);
		}

		ranges.push( range );
		highlightNodes.push( $highlight[ 0 ] );
	} );

	this.ranges = ranges;
	this.$element = $( highlightNodes );

	// Events
	this.updateDebounced = OO.ui.debounce( this.update.bind( this ), 500 );
	window.addEventListener( 'resize', this.updateDebounced );

	if ( OO.ui.isMobile() ) {
		// In MobileFrontend, ensure the section we are highlighting within is expanded. This is
		// often the case as we add the hash fragment to our notification URLs, but not when we highlight
		// comments across multiple threads.
		// HACK: Ideally MF would expose Toggler as a public API, but for now just find the correct DOM
		// node and trigger a fake click.
		// Legacy parser markup:
		this.$element.parents( '.collapsible-block' ).prev( '.collapsible-heading:not( .open-block )' ).trigger( 'click' );
		// Parsoid markup:
		// Support: Firefox, Safari
		// The parsoid markup now uses hidden=until-found for the toggling.
		// Safari and Firefox doesn't support this yet, and so we need to
		// fake a click. We filter down to [hidden] first so that other
		// browsers won't be immediately toggled back to hidden.
		this.$element.parents( '.mf-collapsible-content[hidden]' ).prev( '.mf-collapsible-heading' ).trigger( 'click' );
	}

	this.update();
}

OO.initClass( Highlight );

/**
 * Update position of highlights, e.g. after window resize
 */
Highlight.prototype.update = function () {
	this.$element.css( {
		'margin-top': '',
		'margin-left': '',
		'margin-right': '',
		width: '',
		height: ''
	} );
	const rootRect = this.rootNode.getBoundingClientRect();
	this.topmostElement = null;
	let topmostTop = Infinity;
	this.ranges.forEach( ( range, i ) => {
		const $element = this.$element.eq( i );
		const baseRect = $element[ 0 ].getBoundingClientRect();
		const rect = RangeFix.getBoundingClientRect( range );
		// rect may be null if the range is in a detached or hidden node
		if ( rect ) {
			// Draw the highlight over the full width of the page, except for very short comments
			// (less than 1/3 of the available width).
			//
			// This lets the far edges of almost all comments align nicely (T309444#7968858),
			// while still accurately highlighting comments in narrow floating boxes, such as
			// image captions or {{archive top}}.
			//
			// It seems difficult to distinguish the floating boxes from comments that are just
			// very short or very deeply indented, and this seems to work well enough in practice.
			const useFullWidth = rect.width > rootRect.width / 3;

			let headingTopAdj = 0;
			if (
				featuresEnabled.visualenhancements &&
				$element.closest( '.ext-discussiontools-init-section' ).length
			) {
				// Shift the highlight a little to avoid drawing over the separator border at the top,
				// and to cover the gap to the first comment at the bottom.
				headingTopAdj = 10;
			}

			const top = rect.top - baseRect.top;
			let width = rect.width;
			const height = rect.height;
			let left, right;
			if ( $element.css( 'direction' ) === 'ltr' ) {
				left = rect.left - baseRect.left;
				if ( useFullWidth ) {
					width = rootRect.width - ( rect.left - rootRect.left );
				}
			} else {
				right = ( baseRect.left + baseRect.width ) - ( rect.left + rect.width );
				if ( useFullWidth ) {
					width = rootRect.width - ( ( rootRect.left + rootRect.width ) - ( rect.left + rect.width ) );
				}
			}
			const padding = 5;
			$element.css( {
				'margin-top': top - padding + headingTopAdj,
				'margin-left': left !== undefined ? left - padding : '',
				'margin-right': right !== undefined ? right - padding : '',
				width: width + ( padding * 2 ),
				height: height + ( padding * 2 )
			} );

			if ( rect.top < topmostTop ) {
				this.topmostElement = $element[ 0 ];
				topmostTop = rect.top;
			}
		}
	} );
};

/**
 * Scroll the topmost highlight into view
 */
Highlight.prototype.scrollIntoView = function () {
	if ( this.topmostElement ) {
		this.topmostElement.scrollIntoView();
	}
};

/**
 * Destroy the highlight
 */
Highlight.prototype.destroy = function () {
	this.$element.remove();
	window.removeEventListener( 'resize', this.updateDebounced );
};

let highlightedTarget = null;
/**
 * Highlight the thread item(s) on the page associated with the URL hash or query string
 *
 * @param {ThreadItemSet} threadItemSet
 * @param {boolean} [noScroll] Don't scroll to the topmost highlight, e.g. on popstate
 * @return {Object} Object containing 'requested' IDs, 'requestedSince' ID, and successfully 'highligted' IDs
 */
function highlightTargetComment( threadItemSet, noScroll ) {
	if ( highlightedTarget ) {
		highlightedTarget.destroy();
		highlightedTarget = null;
	}

	const targetElement = mw.util.getTargetFromFragment();

	if ( targetElement && targetElement.hasAttribute( 'data-mw-comment-start' ) ) {
		const threadItemId = targetElement.getAttribute( 'id' );
		const threadItem = threadItemSet.findCommentById( targetElement.getAttribute( 'id' ) );
		if ( threadItem ) {
			highlightedTarget = new Highlight( threadItem );
			highlightedTarget.$element.addClass( 'ext-discussiontools-init-targetcomment' );
			highlightedTarget.$element.addClass( 'ext-discussiontools-init-highlight-fadein' );
		}
		return {
			requested: [ threadItemId ],
			highlighted: [ threadItemId ]
		};
	}

	// Single thread item not highlighted yet
	if ( location.hash.match( /^#(c|h)-/ ) ) {
		return {
			requested: [ location.hash.slice( 1 ) ],
			highlighted: []
		};
	}

	const url = new URL( location.href );
	return highlightNewComments(
		threadItemSet,
		noScroll,
		url.searchParams.get( 'dtnewcomments' ) && url.searchParams.get( 'dtnewcomments' ).split( '|' ),
		{
			newCommentsSinceId: url.searchParams.get( 'dtnewcommentssince' ),
			inThread: url.searchParams.get( 'dtinthread' ),
			sinceThread: url.searchParams.get( 'dtsincethread' )
		}
	);
}

/**
 * Highlight a just-published comment/topic
 *
 * These highlights show for a short period of time then tear themselves down.
 *
 * @param {ThreadItemSet} threadItemSet Thread item set
 * @param {string} threadItemId Thread item ID (NEW_TOPIC_COMMENT_ID for the a new topic)
 */
function highlightPublishedComment( threadItemSet, threadItemId ) {
	const highlightComments = [];

	if ( threadItemId === utils.NEW_TOPIC_COMMENT_ID ) {
		// Highlight the last comment on the page
		const lastComment = threadItemSet.threadItems[ threadItemSet.threadItems.length - 1 ];
		lastHighlightedPublishedComment = lastComment;
		highlightComments.push( lastComment );

		// If it's the only comment under its heading, highlight the heading too.
		// (It might not be if the new discussion topic was posted without a title: T272666.)
		if (
			lastComment.parent &&
			lastComment.parent.type === 'heading' &&
			lastComment.parent.replies.length === 1
		) {
			highlightComments.push( lastComment.parent );
			lastHighlightedPublishedComment = lastComment.parent;
			// Change URL to point to this section, like the old section=new wikitext editor does.
			// This also expands collapsed sections on mobile (T301840).
			const sectionTitle = lastHighlightedPublishedComment.getLinkableTitle();
			const urlFragment = mw.util.escapeIdForLink( sectionTitle );
			// Navigate to fragment without scrolling
			location.hash = '#' + urlFragment + '-DoesNotExist-DiscussionToolsHack';
			history.replaceState( null, '', '#' + urlFragment );
		}
	} else {
		// Find the comment we replied to, then highlight the last reply
		const repliedToComment = threadItemSet.threadItemsById[ threadItemId ];
		highlightComments.push( repliedToComment.replies[ repliedToComment.replies.length - 1 ] );
		lastHighlightedPublishedComment = highlightComments[ 0 ];
	}

	// We may have changed the location hash on mobile, so wait for that to cause
	// the section to expand before drawing the highlight.
	setTimeout( () => {
		const highlight = new Highlight( highlightComments );
		highlight.$element.addClass( 'ext-discussiontools-init-publishedcomment' );

		// Show a highlight with the same timing as the post-edit message (mediawiki.action.view.postEdit):
		// show for 3000ms, fade out for 250ms (animation duration is defined in CSS).
		OO.ui.Element.static.scrollIntoView(
			highlight.topmostElement,
			{
				padding: {
					// Add padding to avoid overlapping the post-edit notification (above on desktop, below on mobile)
					top: OO.ui.isMobile() ? 10 : 60,
					bottom: OO.ui.isMobile() ? 85 : 10
				},
				// Specify scrollContainer for compatibility with MobileFrontend.
				// Apparently it makes `<dd>` elements scrollable and OOUI tried to scroll them instead of body.
				scrollContainer: OO.ui.Element.static.getRootScrollableElement( highlight.topmostElement )
			}
		).then( () => {
			highlight.$element.addClass( 'ext-discussiontools-init-highlight-fadein' );
			setTimeout( () => {
				highlight.$element.addClass( 'ext-discussiontools-init-highlight-fadeout' );
				setTimeout( () => {
					// Remove the node when no longer needed, because it's using CSS 'mix-blend-mode', which
					// affects the text rendering of the whole page, disabling subpixel antialiasing on Windows
					highlight.destroy();
				}, 250 );
			}, 3000 );
		} );
	} );
}

/**
 * Highlight the new comments on the page associated with the query string
 *
 * @param {ThreadItemSet} threadItemSet
 * @param {boolean} [noScroll] Don't scroll to the topmost highlighted comment, e.g. on popstate
 * @param {string[]} [newCommentIds] A list of comment IDs to highlight
 * @param {Object} [options] Extra options
 * @param {string} [options.newCommentsSinceId] Highlight all comments after the comment with this ID
 * @param {boolean} [options.inThread] When using newCommentsSinceId, only highlight comments in the same thread
 * @param {boolean} [options.sinceThread] When using newCommentsSinceId, only highlight comments in threads
 *  created since that comment was posted.
 * @return {Object} Object containing 'requested' comment IDs, 'requestedSince' ID, and successfully 'highligted' IDs
 */
function highlightNewComments( threadItemSet, noScroll, newCommentIds, options ) {
	if ( highlightedTarget ) {
		highlightedTarget.destroy();
		highlightedTarget = null;
	}

	newCommentIds = newCommentIds || [];
	options = options || {};

	if ( options.newCommentsSinceId ) {
		const newCommentsSince = threadItemSet.findCommentById( options.newCommentsSinceId );
		if ( newCommentsSince && newCommentsSince instanceof CommentItem ) {
			const sinceTimestamp = newCommentsSince.timestamp;
			let threadItems;
			if ( options.inThread ) {
				const heading = newCommentsSince.getSubscribableHeading() || newCommentsSince.getHeading();
				threadItems = heading.getThreadItemsBelow();
			} else {
				threadItems = threadItemSet.getCommentItems();
			}
			threadItems.forEach( ( threadItem ) => {
				if (
					threadItem instanceof CommentItem &&
					threadItem.timestamp >= sinceTimestamp
				) {
					if ( options.sinceThread ) {
						// Check that we are in a thread that is newer than `sinceTimestamp`.
						// Thread age is determined by looking at getOldestReply.
						const itemHeading = threadItem.getSubscribableHeading() || threadItem.getHeading();
						const oldestReply = itemHeading.getOldestReply();
						if ( !( oldestReply && oldestReply.timestamp >= sinceTimestamp ) ) {
							return;
						}
					}
					newCommentIds.push( threadItem.id );
				}
			} );
		}
	}

	let comments;
	if ( newCommentIds.length ) {
		comments = newCommentIds
			.map( ( id ) => threadItemSet.findCommentById( id ) )
			.filter( ( cmt ) => !!cmt );
		if ( comments.length ) {
			highlightedTarget = new Highlight( comments );
			highlightedTarget.$element.addClass( 'ext-discussiontools-init-targetcomment' );
			highlightedTarget.$element.addClass( 'ext-discussiontools-init-highlight-fadein' );

			if ( !noScroll ) {
				highlightedTarget.scrollIntoView();
			}
		}
	}
	return {
		requested: newCommentIds,
		requestedSince: options.newCommentsSinceId,
		highlighted: comments || []
	};
}

/**
 * Clear the highlighting of the comment in the URL hash
 *
 * @param {ThreadItemSet} threadItemSet
 */
function clearHighlightTargetComment( threadItemSet ) {
	const url = new URL( location.href );

	const targetElement = mw.util.getTargetFromFragment();

	if ( targetElement && targetElement.hasAttribute( 'data-mw-comment-start' ) ) {
		// Clear the hash from the URL, triggering the 'hashchange' event and updating the :target
		// selector (so that our code to clear our highlight works), but without scrolling anywhere.
		// This is tricky because:
		// * Using history.pushState() does not trigger 'hashchange' or update the :target selector.
		//   https://developer.mozilla.org/en-US/docs/Web/API/History/pushState#description
		//   https://github.com/whatwg/html/issues/639
		// * Using location.hash does, but it also scrolls to the target, which is the top of the
		//   page for the empty hash.
		// Instead, we first use location.hash to navigate to a *different* hash (whose target
		// doesn't exist on the page, hopefully), and then use history.pushState() to clear it.
		location.hash += '-DoesNotExist-DiscussionToolsHack';
		url.hash = '';
		history.replaceState( null, '', url );
	} else if (
		url.searchParams.has( 'dtnewcomments' ) ||
		url.searchParams.has( 'dtnewcommentssince' )
	) {
		url.searchParams.delete( 'dtnewcomments' );
		url.searchParams.delete( 'dtnewcommentssince' );
		url.searchParams.delete( 'dtinthread' );
		url.searchParams.delete( 'dtsincethread' );
		history.pushState( null, '', url );
		highlightTargetComment( threadItemSet );
	} else if ( highlightedTarget ) {
		// Highlights were applied without changing the URL, e.g. when showing
		// new comments while drafting. Just clear the highlights.
		highlightedTarget.destroy();
		highlightedTarget = null;
	}
}

/**
 * Get the last highlighted just-published comment, if any
 *
 * Used to show an auto-subscription popup to first-time users
 *
 * @return {ThreadItem|null}
 */
function getLastHighlightedPublishedComment() {
	return lastHighlightedPublishedComment;
}

module.exports = {
	highlightTargetComment: highlightTargetComment,
	highlightPublishedComment: highlightPublishedComment,
	highlightNewComments: highlightNewComments,
	clearHighlightTargetComment: clearHighlightTargetComment,
	getLastHighlightedPublishedComment: getLastHighlightedPublishedComment
};
