<?php

use MediaWiki\Html\Html;
use MediaWiki\Linker\Linker;
use MediaWiki\MediaWikiServices;
use MediaWiki\Linker\LinkRenderer;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\Title\Title;

/**
 * Hooks for NextBestNetwork skin
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * http://www.gnu.org/copyleft/gpl.html
 *
 * @file
 * @ingroup Skins
 */

class NextBestNetworkHooks {

	protected static $anchorID = 0;
	protected static $realnames = [];


	/**
	 * Is NextBestNetwork used as a skin for the current page?
	 *
	 * @return boolean
	 */
	public static function getSkinNextBestNetworkSkin() {
		return $GLOBALS['wgOut']->getSkin()->getSkinName() === 'nextbestnetwork';
	}


	/**
	 * Expose NextBestNetworkSkinUseTooltips configuration variable
	 *
	 * @param array $vars
	 */
	public static function onResourceLoaderGetConfigVars( array &$vars ) {
		$vars['wgNextBestNetworkSkinUseTooltips'] = $GLOBALS['wgNextBestNetworkSkinUseTooltips'];
	}


	/**
	 * Setting up parser functions
	 *
	 * @param Parser $parser Parser object being initialized
	 */
	public static function onParserFirstCallInit( Parser $parser ) {
		$parser->setHook( 'TOC', 'NextBestNetworkHooks::TOC' );
		$parser->setHook( 'legend', 'NextBestNetworkHooks::legend' );
		$parser->setHook( 'footer', 'NextBestNetworkHooks::footer' );
		$parser->setHook( 'accordion', 'NextBestNetworkHooks::buildAccordion' );
		$parser->setHook( 'label', 'NextBestNetworkHooks::buildLabel' );

		if ( $GLOBALS['wgNextBestNetworkSkinUseBtnParser'] === true ) {
			$parser->setHook( 'btn', 'NextBestNetworkHooks::buildButtons' );
		}

		$parser->setFunctionHook( 'nextbestnetworkhide', 'NextBestNetworkHooks::setHiddenElements' );
		$parser->setFunctionHook( 'nextbestnetworknav', 'NextBestNetworkHooks::setCustomNavElement' );
		$parser->setFunctionHook( 'nextbestnetworkhideexcept', 'NextBestNetworkHooks::setHiddenElementsGroups' );
		$parser->setFunctionHook( 'nextbestnetworkbodyclass', 'NextBestNetworkHooks::addBodyclass' );
		$parser->setFunctionHook( 'nextbestnetworkrealname', 'NextBestNetworkHooks::renderRealname' );
	}


	/**
	 * Adding modules
	 *
	 * @param OutputPage $out The OutputPage object.
	 * @param Skin $skin Skin object that will be used to generate the page
	 */
	public static function onBeforePageDisplay( OutputPage $out, Skin $skin ) {
		if( $skin->getSkinName() == 'nextbestnetwork' ) {
			$config = \MediaWiki\MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'nextbestnetwork' );

			$styles = [];

			// load mediawiki styles
			$styles[] = 'skins.nextbestnetwork.mediawiki.styles';

			// load font awesome
			$styles[] = 'skins.nextbestnetwork.awesome.styles';

			// load externally defined style module
			if( $config->get( 'NextBestNetworkSkinCustomStyleModule' ) ) {
				$styles[] = $config->get( 'NextBestNetworkSkinCustomStyleModule' );

				// or: load modules defined by nextbestnetwork
			} else {
				if( !$config->get( 'NextBestNetworkSkinUseCustomFiles' ) ) {
					$styles[] = 'skins.nextbestnetwork.styles';
				} else {
					$styles[] = 'skins.nextbestnetwork.custom.styles';
				}
			}

			// load external link styles
			if( $config->get( 'NextBestNetworkSkinUseExternallinkStyles' ) ) {
				$styles[] = 'skins.nextbestnetwork.externallinks.styles';
			}

			// load additional modules
			foreach( $GLOBALS['wgNextBestNetworkSkinCustomCSS'] as $customstyle ) {
				$styles[] = $customstyle;
			}

			$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
			$hookContainer->run( 'SkinNextBestNetworkStyleModules', [ $skin, &$styles ] );

			$out->addModuleStyles( $styles );
		}
	}


	/**
	 * GetPreferences hook
	 *
	 * Adds NextBestNetwork-releated items to the preferences
	 *
	 * @param User $user User whose preferences are being modified
	 * @param array $preferences Preferences description array, to be fed to an HTMLForm object
	 */
	public static function onGetPreferences( User $user, array &$preferences ) {
		$preferences['nextbestnetwork-advanced'] = [
			'type' => 'toggle',
			'label-message' => 'prefs-nextbestnetwork-advanced-desc',
			'section' => 'rendering/nextbestnetwork-advanced',
			'help-message' => 'prefs-nextbestnetwork-advanced-help'
		];
	}


	/**
	 * Pages could be hidden for anonymous users or only be shown for specific groups
	 * so we put a user's group memberships into the page rendering hash
	 *
	 * @param string $confstr Reference to a hash key string which can be modified
	 * @param User $user User object that is requesting the page
	 * @param array $options Array of options used to generate the $confstr hash key
	 */
	static function onPageRenderingHash( &$confstr, User $user, array &$options ) {
		$userGroupManager = MediaWikiServices::getInstance()->getUserGroupManager();
		$groups = $userGroupManager->getUserEffectiveGroups($user);
		sort( $groups );
		$confstr .= "!groups=" . join(',', $groups );
	}


	/**
	 * Add body classes
	 *
	 * @param OutputPage $out The OutputPage which called the hook, can be used to get the real title
	 * @param Skin $sk The Skin that called OutputPage::headElement
	 * @param array $bodyAttrs An array of attributes for the body tag passed to Html::openElement
	 */
	static function onOutputPageBodyAttributes( OutputPage $out, Skin $sk, array &$bodyAttrs ) {
		if( $sk->getSkinName() == 'nextbestnetwork' ) {
			$additionalBodyClasses = [ 'nextbestnetwork-animateLayout' ];

			$user = $out->getUser();
			$userAdvanced = MediaWikiServices::getInstance()->getUserOptionsLookup()->getOption( $user, 'nextbestnetwork-advanced' );
			$additionalBodyClasses[] = $userAdvanced ? 'nextbestnetwork-advanced' : 'nextbestnetwork-non-advanced';
			$additionalBodyClasses[] = $user->isRegistered() ? 'nextbestnetwork-user-logged-in' : 'nextbestnetwork-user-anon';

			$additionalBodyClasses = array_merge( $additionalBodyClasses, $GLOBALS['wgNextBestNetworkSkinAdditionalBodyClasses'] );

			$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
			$hookContainer->run( 'SkinNextBestNetworkAdditionalBodyClasses', [ $sk, &$additionalBodyClasses ] );

			if( count( $additionalBodyClasses ) > 0 ) {
				$bodyAttrs['class'] = $bodyAttrs['class'] . ' ' . preg_replace( "/[^a-zA-Z0-9_\s-]/", "", implode( " ", $additionalBodyClasses ) );
			}
		}
	}


	/**
	 * Use real names instead of user names
	 *
	 * @param LinkRenderer $linkRenderer the LinkRenderer object
	 * @param LinkTarget $target the LinkTarget that the link is pointing to
	 * @param boolean $isKnown boolean indicating whether the page is known or not
	 * @param string|HtmlArmor $text the contents that the <a> tag should have; either a plain, unescaped string or a HtmlArmor object.
	 * @param array $attribs the final HTML attributes of the <a> tag, after processing, in associative array form.
	 * @param string $ret the value to return if your hook returns false.
	 */
	public static function onHtmlPageLinkRendererEnd(  LinkRenderer $linkRenderer, LinkTarget $target, $isKnown, &$text, array &$attribs, &$ret ) {
		if( $GLOBALS['wgNextBestNetworkSkinUseRealnames'] == true && $target->getNamespace() === 2 ) {
			$userkey = $target->getDBKey();

			// use real name if link text hadn't been set explicitly to be different from the page name
			$title = Title::newFromText( HtmlArmor::getHtml( $text ) );
			if( 
				$title && 
				( 
					$title->getPrefixedText() == $target->getPrefixedText() 
					|| $title->getText() == $target->getText()
				)
			) {
				$text = self::getRealname( $userkey );
			}
		}
	}


	/**
	 * Use real names instead of user names in selflinks
	 *
	 * @param Title $nt the title object of the page
	 * @param string $html Link text
	 * @param string $trail Text after link
	 * @param string $prefix Text before link
	 * @param string $ret Self link text to be used if the hook returns false
	 */
	public static function onSelfLinkBegin( Title $nt, &$html, &$trail, &$prefix, &$ret ) {
		if( $GLOBALS['wgNextBestNetworkSkinUseRealnames'] == true && $nt->getNamespace() === 2 ) {
			$userkey = $nt->getDBKey();

			// use real name if link text hadn't been set explicitly to be different from the page name
			$title = Title::newFromText( HtmlArmor::getHtml( $html ) );
			if( 
				$title && 
				( 
					$title->getPrefixedText() == $nt->getPrefixedText() 
					|| $title->getText() == $nt->getText()
				)
			) {
				$html = self::getRealname( $userkey );
			}
		}
	}


	/**
	 * Customize edit section links
	 *
	 * @param Skin $skin Skin object rendering the UI
	 * @param Title $title  Title object for the title being linked to
	 * @param string $section The designation of the section being pointed to, to be included in the link
	 * @param string $tooltip The default tooltip.
	 * @param array $links Array containing all link detail arrays.
	 * @param string $lang The language code to use for the link in the wfMessage function
	 *
	 * @todo: make this work with VisualEditor
	 */
	static function onSkinEditSectionLinks( Skin $skin, Title $title, $section, $tooltip, array &$links, $lang = false ) {
		if(
			$skin->getSkinName() == 'nextbestnetwork'
			&& $GLOBALS['wgNextBestNetworkSkinCustomEditSectionLink'] == true
		) {
			if ( version_compare( MW_VERSION, '1.40', '<' ) ) {
				$icon = wfMessage( 'nextbestnetwork-editsection-icon' )->inLanguage( $lang )->parse();
				$text = wfMessage( 'nextbestnetwork-editsection-text' )->inLanguage( $lang )->parse();
				$class = wfMessage( 'nextbestnetwork-editsection-class' )->inLanguage( $lang )->parse();
				$text = new HtmlArmor( $icon . ( ( $icon != '' ) ? ' ' : '' ) . $text );
				$links['editsection']['text'] = $text;
				$links['editsection']['attribs']['class'] = $class;
			} else {
				$links['editsection']['link-html'] = wfMessage( 'nextbestnetwork-editsection-icon' )->inLanguage( $lang )->parse();
				$links['editsection']['text'] = wfMessage( 'nextbestnetwork-editsection-text' )->inLanguage( $lang )->parse();
				$links['editsection']['attribs']['class'] = wfMessage( 'nextbestnetwork-editsection-class' )->inLanguage( $lang )->parse();
			}
		}

		return false;
	}


	/**
	 * Change TOC and page content of file pages to togglable tabs
	 *
	 * @param OutputPage $outputPage
	 */
	public static function onAfterFinalPageOutput( OutputPage $outputPage ) {
		if(
			$outputPage->getSkin()->getSkinName() == 'nextbestnetwork'
			&& $outputPage->getTitle()->getNamespace() == 6
			&& $GLOBALS['wgNextBestNetworkSkinImagePageTOCTabs'] == true
		) {
			$out = ob_get_clean();
			$out = str_replace( '<ul id="filetoc">', '<ul id="tw-filetoc" class="nav nav-tabs nav-justified" role="tablist">', $out );
			$out = str_replace( '<li><a href="#file">', '<li class="nav-item"><a href="#file" id="file-tab" class="nav-link active" data-bs-toggle="tab" role="tab" aria-controls="file" aria-selected="true">', $out );
			$out = str_replace( '<li><a href="#filehistory">', '<li class="nav-item"><a href="#filehistory" id="filehistory-tab" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="filehistory" aria-selected="false">', $out );
			$out = str_replace( '<li><a href="#filelinks">', '<li class="nav-item"><a href="#filelinks" id="filelinks-tab" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="filelinks" aria-selected="false">', $out );
			$out = str_replace( '<li><a href="#metadata">', '<li class="nav-item"><a href="#metadata" id="metadata-tab" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="metadata" aria-selected="false">', $out );
			$out = str_replace( '<div class="fullImageLink" id="file"', '<div class="tab-content"><div id="file" class="tab-pane fade show active" role="tabpanel" aria-labelledby="file-tab"><div class="fullImageLink"', $out );
			$out = str_replace( '<h2 id="filehistory"', '</div><div id="filehistory" class="tab-pane fade" role="tabpanel" aria-labelledby="filehistory-tab"><h2', $out );
			$out = str_replace( '<h2 id="filelinks"', '</div><div id="filelinks" class="tab-pane fade" role="tabpanel" aria-labelledby="filelinks-tab"><h2', $out );
			$out = str_replace( '<h2 id="metadata"', '</div><div id="metadata" class="tab-pane fade" role="tabpanel" aria-labelledby="metadata-tab"><h2', $out );
			$out = $out . '</div></div>';
			ob_start();
			echo $out;
		}
	}


	/**
	 * Enable use of <toc> tag
	 */
	public static function TOC( $input, array $args, Parser $parser, PPFrame $frame ) {
		if ( !self::getSkinNextBestNetworkSkin() ) {
			return [];
		}
		return [ '<div class="nextbestnetwork-toc">' . $parser->recursiveTagParse( $input ) . '</div>' ];
	}


	/**
	 * Enable use of <legend> tag
	 */
	public static function legend( $input, array $args, Parser $parser, PPFrame $frame ) {
		if ( !self::getSkinNextBestNetworkSkin() ) {
			return [];
		}
		return [ '<legend>' . $parser->recursiveTagParse( $input ) . '</legend>', "markerType" => 'nowiki' ];
	}


	/**
	 * Enable use of <footer> tag
	 */
	public static function footer( $input, array $args, Parser $parser, PPFrame $frame ) {
		if ( !self::getSkinNextBestNetworkSkin() ) {
			return [];
		}
		return [ '<footer>' . $parser->recursiveTagParse( $input ) . '</footer>', "markerType" => 'nowiki' ];
	}


	/**
	 * Enable use of <accordion> tag
	 */
	public static function buildAccordion( $input, array $args, Parser $parser, PPFrame $frame ) {
		if ( !self::getSkinNextBestNetworkSkin() ) {
			return '';
		}
		static::$anchorID++;
		$parent = $parser->recursiveTagParse( $args['parent'], $frame );
		$card = '
			<div class="accordion-item">
				<h2 class="accordion-header" id="' . $parent . static::$anchorID . 'Heading">
					<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#' . $parent . static::$anchorID . '" aria-expanded="' . ( isset( $args['class'] ) && $args['class'] == 'show' ? 'true' : 'false' ) . '" aria-controls="' . $parent . static::$anchorID . '">
							' . $parser->recursiveTagParse( $args['heading'], $frame ) . '
					</button>
				</h2>
				<div id="' . $parent . static::$anchorID . '" class="accordion-collapse collapse ' . ( isset( $args['class'] ) ? htmlentities( $args['class'] ) : '' ) . '" ' . (!isset( $args['alwaysopen'] ) ? 'data-bs-parent="#' . $parent . '"' : '' ) . '>
					<div class="accordion-body">' . $parser->recursiveTagParse( $input, $frame ) . '</div>
				</div>
			</div>';
		return $card;
	}


	/**
	 * Enable use of <label> tag
	 */
	public static function buildLabel( $input, array $args, Parser $parser, PPFrame $frame ) {
		if ( !self::getSkinNextBestNetworkSkin() ) {
			return '';
		}
		return '<label>' . $parser->recursiveTagParse( $input ) . '</label>';
	}


	/**
	 * Enable use of <btn> tag
	 */
	public static function buildButtons( $input, array $args, Parser $parser, PPFrame $frame ) {
		if ( !self::getSkinNextBestNetworkSkin() ) {
			return '';
		}

		$sizes = [
			'large' => 'btn-lg',
			'lg' => 'btn-lg',
			'small' => 'btn-sm',
			'sm' => 'btn-sm',
			'mini' => 'btn-xs',
			'xs' => 'btn-xs'
		];
		$renderedButtons = '';

		$buttongroups = preg_split( '/\n{2,}/', $input );

		// set standard classes for all buttons in the group
		if ( !isset( $args['class'] ) ) {
			$args['class'][] = 'btn btn-secondary';
		}
		else {
			$args['class'] = explode( ' ', $args['class'] );
		}
		if ( isset( $args['size'] ) ) {
			if ( isset( $sizes[$args['size']] ) ) {
				$args['class'][] = $sizes[$args['size']];
			}
		}

		foreach ( $buttongroups as $buttongroup ) {
			$buttons = [];
			$buttons = NextBestNetworkHooks::parseButtons( $buttongroup, $parser, $frame );
			$renderedButtons .= NextBestNetworkHooks::renderButtons( $buttons, $args );
		}

		// more than one buttongroup build a toolbar
		if ( count( $buttongroups ) > 1 ) {
			$renderedButtons = '<div class="btn-toolbar">' . $renderedButtons . '</div>';
		}

		return $renderedButtons;
	}


	/**
	 * Set elements that should be hidden
	 */
	static function setHiddenElements( Parser $parser ) {
		if ( !self::getSkinNextBestNetworkSkin() ) {
			return '';
		}

		$parser->getOutput()->updateCacheExpiry(0);

		for ( $i = 1; $i < func_num_args(); $i++ ) {
			if ( in_array ( func_get_arg( $i ), $GLOBALS['wgNextBestNetworkSkinHideable'] ) ) {
				$GLOBALS['wgNextBestNetworkSkinHideAll'][] = func_get_arg( $i );
			}
		}
		return '';
	}


	/**
	 * Set elements that should be hidden except for members of specific groups
	 */
	static function setHiddenElementsGroups( Parser $parser ) {
		if ( !self::getSkinNextBestNetworkSkin() ) {
			return '';
		}

		$parser->getOutput()->updateCacheExpiry(0);

		$groups_except = explode( ',', func_get_arg( 1 ) );
		$userGroupManager = MediaWikiServices::getInstance()->getUserGroupManager();
		$groups_user = $userGroupManager->getUserEffectiveGroups($parser->getUserIdentity());
		if( count( array_intersect( $groups_except, $groups_user ) ) == 0 ) {
			for ( $i = 2; $i < func_num_args(); $i++ ) {
				if ( in_array ( func_get_arg( $i ), $GLOBALS['wgNextBestNetworkSkinHideable'] ) ) {
					$GLOBALS['wgNextBestNetworkSkinHideAll'][] = func_get_arg( $i );
				}
			}
		}
		return '';
	}


	/**
	 * Customize a navigational element's content
	 */
	static function setCustomNavElement( Parser $parser, $element, ... $content ) {
		if ( !self::getSkinNextBestNetworkSkin() ) {
			return '';
		}

		$parser->getOutput()->updateCacheExpiry(0);

		$GLOBALS['wgNextBestNetworkSkinCustomNav'][$element] = join( '|', $content );

		return '';
	}


	/**
	 * Add classes to body
	 */
	static function addBodyclass( Parser $parser ) {
		if ( !self::getSkinNextBestNetworkSkin() ) {
			return '';
		}

		$parser->getOutput()->updateCacheExpiry(0);

		for ( $i = 1; $i < func_num_args(); $i++ ) {
			$GLOBALS['wgNextBestNetworkSkinAdditionalBodyClasses'][] = func_get_arg( $i );
		}
		return '';
	}


	/**
	 * Render a user's real name
	 *
	 * @param Parser $parser
	 * @param String $user User name
	 * @return array User's real name
	 */
	static function renderRealName( Parser $parser, $user ) {
		if ( !self::getSkinNextBestNetworkSkin() ) {
			return [];
		}

		$realname = self::getRealname( $user );

		return [ $realname ];
	}


	/**
	 * Parse string input into array
	 *
	 * @param string $buttongroup one or more buttons
	 * @param Parser $parser current parser
	 * @param PPFrame $frame current frame
	 * @return array
	 */
	static function parseButtons( $buttongroup, Parser $parser, $frame ) {
		$buttons = [];
		$lines = explode( "\n", $buttongroup );

		foreach ( $lines as $line ) {
			// empty line
			if ( trim( $line ) == "" ) {
				continue;
			}

			// simple buttons
			if ( strpos( $line, '*' ) !== 0 ) {
				$buttons = array_merge( $buttons, NextBestNetworkHooks::parseButtonLink( trim( $line ), $parser, $frame ) );
				end( $buttons );
				$currentparentkey = key($buttons);
			}

			// dropdown menus
			else {
				// no parent set?
				if ( count( $buttons ) == 0 ) {
					continue;
				}

				$cleanline = ltrim( $line, '*' );
				$cleanline = trim( $cleanline );
				if ( !isset( $buttons[$currentparentkey]['items'] ) ) {
					$buttons[$currentparentkey]['items'] = [];
				}

				// dropdown-headers (dropdown-lines that start with a colon)
				if ( strpos( $cleanline, ':' ) === 0 ) {
					$buttons[$currentparentkey]['items'][] = [ 'text' => ltrim( $cleanline, ':' ), 'header' => true ];
				} else {
					$buttons[$currentparentkey]['items'] = array_merge( $buttons[$currentparentkey]['items'], NextBestNetworkHooks::parseButtonLink( $cleanline, $parser, $frame ) );
				}
			}
		}
		return $buttons;
	}


	/**
	 * Parse specific link
	 *
	 * @param $line string
	 * @param Parser $parser current parser
	 * @param PPFrame $frame current frame
	 * @return array
	 */
	static function parseButtonLink( $line, Parser $parser, $frame ) {
		$extraAttribs = [];
		$href_implicit = false;
		$active = false;
		$current_title = $parser instanceof Parser ? $parser->getTitle() : null;

		// semantic queries
		if ( strpos( $line, '{{#ask:' ) === 0 ) {
			if ( !is_null( $current_title ) && $current_title instanceof Title ) {
				$semanticQuery = substr( $line, 7, -2 );
				$semanticHitNumber = $parser->recursiveTagParse( '{{#ask:' . $semanticQuery . '|format=count}}', false );
				if ( !is_numeric( $semanticHitNumber ) ) {
					return [ 
						[
							'text' => $semanticQuery, 
							'href' => 'INVALID QUERY',
						]
					];
				}
				if( $semanticHitNumber < 1 ) {
					return [
						[
							'html' => '<span class="dropdown-item-text">' . wfMessage( 'nextbestnetwork-no-entries' )->plain() . '</span>',
						]
					];
				}
				$semanticHits = $parser->recursiveTagParse( '{{#ask:' . $semanticQuery . '|link=none|format=array}}', false );
				$semanticHits = explode( ',', $semanticHits );
				$semanticLinks = [];
				foreach ( $semanticHits as $semanticHit ) {
					$semanticHit = rtrim( str_replace( '&lt;PROP&gt;', '|', $semanticHit ), '|' );
					$semanticLink = NextBestNetworkHooks::parseButtonLink( $semanticHit, $parser, $frame );
					$semanticLinks[] = $semanticLink[0];
				}
				return $semanticLinks;
			}
			else {
				$text = 'INVALID-TITLE/QUERY-BROKEN';
			}
		}

		$line = explode( '|', $line );
		foreach ( $line as &$single_line ) {
			$single_line = trim( $single_line );
		}

		// is the text explicitly set?
		$href = $line[0];
		if ( isset( $line[1] ) && $line[1] != "" ) {
			$text = $line[1];
		}
		else {
			$href_implicit = true;
			$text = $line[0];
		}

		// parse text
		$msgText = wfMessage( $text )->inContentLanguage();
		if ( $msgText->exists() ) {
			$text = $msgText->parse();
		}
		else {
			if ( !is_null( $current_title ) && $current_title instanceof Title ) {
				$text = $parser->recursiveTagParse( $text, $frame );
			}
			else {
				$text = 'INVALID-TITLE/PARSER-BROKEN';
			}
		}

		// parse href
		$msgLink = wfMessage( $href )->inContentLanguage();
		if ( $msgLink->exists() ) {
			$href = $msgLink->parse();
		}
		else {
			if ( !is_null( $current_title ) && $current_title instanceof Title ) {
				$href = trim( $parser->replaceVariables( $href, $frame ) );
			}
			else {
				$href = 'INVALID-HREF/PARSER-BROKEN';
			}
		}

		if ( preg_match( '/^(?i:' . MediaWikiServices::getInstance()->getUrlUtils()->validProtocols() . ')/', $href ) ) {
			// Parser::getExternalLinkAttribs won't work here because of the Namespace things
			if ( $GLOBALS['wgNoFollowLinks'] && !wfMatchesDomainList( $href, $GLOBALS['wgNoFollowDomainExceptions'] ) ) {
				$extraAttribs['rel'] = 'nofollow';
			}

			if ( $GLOBALS['wgExternalLinkTarget'] ) {
				$extraAttribs['target'] = $GLOBALS['wgExternalLinkTarget'];
			}
		} else {
			$title = Title::newFromText( $href );
			if ( $title ) {
				if( !is_null( $current_title ) && $current_title instanceof Title && $title->equals( $current_title ) ) {
					$active = true;
				}
				$title = $title->fixSpecialName();
				$href = $title->getLinkURL();
				if( $GLOBALS['wgNextBestNetworkSkinUseRealnames'] == true && $title->exists() && $title->getNamespace() === 2 ) {
					$userkey = $title->getDBKey();

					// use real name if link text hadn't been set explicitly
					if( $href_implicit ) {
						$title_from_text = Title::newFromText( HtmlArmor::getHtml( $text ) );
						if( 
							$title_from_text && 
							( 
								$title->getPrefixedText() == $title_from_text->getPrefixedText() 
								|| $title->getText() == $title_from_text->getText()
							)
						) {
							$text = self::getRealname( $userkey );
						}
					}
				}
			} else {
				// allow empty first argument
				if( $href != '' ) {
					$href = 'INVALID-TITLE:' . $href;
				}
			}
		}
		if ( isset( $line[2] ) && $line[2] != "" ) {
			$extraAttribs['class'] = $line[2];
		}

		$link = [
			'html' => $text,
			'href' => $href,
			'href_implicit' => $href_implicit,
			'active' => $active
		];
		if( $line[0] != '' ) {
			$link['id'] = urlencode( strtolower( strtr( $line[0], ' ', '-' ) ) );
			$link['id'] = 'n-' . Sanitizer::escapeIdForAttribute( $link['id'] );
		}
		$link = array_merge( $link, $extraAttribs );
		return [ $link ];
	}


	/**
	 * Get a user's real name
	 *
	 * @param String $user User name
	 *
	 * @return String User's real name
	 */
	static function getRealname( $userkey ) {
		if( !$userkey ) {
			return $userkey;
		}
		if( !isset( self::$realnames[$userkey] ) ) {
			$user = User::newFromName( $userkey );
			self::$realnames[$userkey] = $user->getRealName() ?: $user->getName();
		}

		return self::$realnames[$userkey];
	}

	/**
	 * Render Buttons
	 *
	 * @param $buttons array
	 * @param $options Array
	 * @return String
	 */
	static function renderButtons( $buttons, $options = [] ) {
		$renderedButtons = '';
		$groupclass = [];
		if ( isset( $options['class'] ) ) {
			if ( !is_array( $options['class'] ) ) {
				$options['class'] = explode( ' ', $options['class'] );
			}
			$groupclass = $options['class'];
		}
		$currentwrapperclass = '';

		// set wrapper
		$wrapper = 'div';
		if ( isset( $options['wrapper'] ) ) {
			$wrapper = $options['wrapper'];
		}

		foreach ( $buttons as $button ) {
			$btnoptions = [];
			// set classes for specific button
			// explicit classes for the specific line?
			if ( isset( $button['class'] ) ) {
				$button['class'] = explode( ' ', $button['class'] );
			}
			else {
				$button['class'] = $groupclass;
			}
			foreach ( $button['class'] as $btnclass ) {
				if ( strpos( $btnclass, 'btn' ) === 0 ) {
					$button['class'][] = 'btn';
					break;
				}
			}

			// set wrapper class
			if ( isset( $options['wrapperclass'] ) ) {
				$wrapperclass = $options['wrapperclass'];
			}
			else {
				if ( in_array( 'btn', $button['class'] ) === false ) {
					$wrapperclass = 'dropdown';
				}
				else {
					$wrapperclass = 'btn-group me-2';
				}
			}

			$button['class'] = implode( ' ', array_unique( $button['class'] ) );
			if( isset( $button['links'] ) ) {
				foreach( $button['links'] as &$link ) {
					$link['class'] = $button['class'];
				}
			}

			// set attributes
			$allowed_attributes = [
				'aria-controls',
				'aria-expanded',
				'aria-selected',
				'aria-label',
				'data-bs-target',
				'data-bs-dismiss',
				'data-bs-placement',
				'data-bs-slide',
				'title',
				'role',
			];

			foreach( $allowed_attributes as $attribute ) {
				if ( isset( $options[$attribute] ) ) {
					$button[$attribute] = $options[$attribute];
				}
			}

			// if data-bs-toggle attribute is set, unset wrapper and add attribute and toggle-class
			if ( isset( $options['data-bs-toggle'] ) ) {
				$wrapper = '';
				$button['data-bs-toggle'] = $options['data-bs-toggle'];
				$button['class'] .= ' ' . $options['data-bs-toggle'] . '-toggle';
			}

			// if html is not set, use text and sanitize it
			if ( !isset( $button['html'] ) ) {
				if( isset( $button['text'] ) ) {
					$button['html'] = htmlspecialchars( $button['text'] );
				}
				else {
					$button['html'] = '#';
				}
			}

			// if glyphicon, fa or icon attribute is set, add icon to buttons
			if ( isset( $options['icon'] ) ) {
				$options['fa'] = $options['icon'];
			}
			if ( isset( $options['fa'] ) ) {
				$button['html'] = '<span class="fa fa-' . $options['fa'] . '"></span> ' . $button['html'];
			}

			if ( isset( $options['glyphicon'] ) ) {
				$button['html'] = '<span class="glyphicon glyphicon-' . $options['glyphicon'] . '"></span> ' . $button['html'];
			}

			// render wrapper
			if (
				( ( $currentwrapperclass != $wrapperclass || isset( $button['items'] ) ) && $wrapper != '' )
				|| $wrapper == 'li'
			) {
				if ( $currentwrapperclass != '' ) {
					$renderedButtons .= '</' . $wrapper . '>';
				}
				$renderedButtons .= '<' . $wrapper . ' class="' . $wrapperclass;
				if ( isset( $button['active'] ) && $button['active'] === true ) {
					$renderedButtons .= ' active';
				}
				if( isset( $button['items'] ) ) {
					$renderedButtons .= ' dropdown';
				}
				if ( isset( $options['wrapperid'] ) ) {
					$renderedButtons .= '" id="' . $options['wrapperid'];
				}
				$renderedButtons .= '">';
				$currentwrapperclass = $wrapperclass;
			}

			// dropdown
			if ( isset( $button['items'] ) ) {
				if ( isset( $options['dropdownclass'] ) ) {
					$renderedButtons .= NextBestNetworkHooks::buildDropdown( $button, $options['dropdownclass'] );
				}
				else {
					$renderedButtons .= NextBestNetworkHooks::buildDropdown( $button );
				}
			}

			// simple button
			else {
				$renderedButtons .= NextBestNetworkHooks::makeLink( $button, $btnoptions );
			}
		}
		// close wrapper
		if ( $wrapper != '' ) {
			$renderedButtons .= '</' . $wrapper . '>';
		}
		return $renderedButtons;
	}


	/**
	 * Build dropdown
	 *
	 * @param array $dropdown
	 * @param string $dropdownclass
	 * @return String
	 */
	static function buildDropdown( $dropdown, $dropdownclass = '' ) {
		$renderedDropdown = '';

		// split dropdown
		if ( isset( $dropdown['href_implicit'] ) && $dropdown['href_implicit'] === false ) {
			$renderedDropdown .= NextBestNetworkHooks::makeLink( $dropdown );
			$caret = [
				'class' => 'dropdown-toggle dropdown-toggle-split ' . $dropdown['class'],
				'href' => '#',
				'data-bs-toggle' => 'dropdown',
				'html' => '<span class="sr-only">Toggle Dropdown</span>',
				'aria-haspopup' => 'true'
			];
			$renderedDropdown .= NextBestNetworkHooks::makeLink( $caret );
		}

		// ordinary dropdown
		else {
			$dropdown['class'] .= ' dropdown-toggle';
			$dropdown['data-bs-toggle'] = 'dropdown';
			$dropdown['href'] = '#';
			$dropdown['aria-haspopup'] = 'true';
			$renderedDropdown .= NextBestNetworkHooks::makeLink( $dropdown );
		}

		$renderedDropdown .= NextBestNetworkHooks::buildDropdownMenu( $dropdown['items'], $dropdownclass );
		return $renderedDropdown;
	}


	/**
	 * Build dropdown-menu (ul)
	 *
	 * @param array $dropdownmenu
	 * @param string $dropdownclass
	 * @return String
	 */
	static function buildDropdownMenu( array $dropdownmenu, $dropdownclass ) {
		$renderedMenu = '<div class="dropdown-menu ' . $dropdownclass . '">';

		foreach ( $dropdownmenu as $entry ) {
			// divider
			if ( ( !isset( $entry['text'] ) || $entry['text'] == "" ) // no 'text'
				&& ( !isset( $entry['html'] ) || $entry['html'] == "" ) // and no 'html'
			) {
				$renderedMenu .= '<div class="dropdown-divider"></div>';
			}

			// header
			elseif ( isset( $entry['text'] ) && isset( $entry['header'] ) && $entry['header'] ) {
				$renderedMenu .= '<h6 class="dropdown-header">' . $entry['text'] . '</h6>';
			}

			// standard menu entry
			else {
				$entry['tabindex'] = '-1';
				$entry['class'] = 'dropdown-item';
				$renderedMenu .= NextBestNetworkHooks::makeLink( $entry );
			}
		}

		$renderedMenu .= '</div>';
		return $renderedMenu;
	}


	/**
	 * Produce HTML for a link
	 *
	 * This is a slightly adapted copy of the makeLink function in Skin.php
	 * -> some of the changed parts are marked by comments //
	 *
	 * @param array $item
	 * @param array $options
	 */
	static function makeLink( array $item, array $options = [] ) {
		// nextbestnetwork: nested links?
		if ( isset( $item['links'] ) ) {
			if( isset( $item['links'][0] ) ) {
				$item = $item['links'][0];
			} else {
				return false;
			}
		}

		// nextbestnetwork: get text
		$text = '';
		if ( isset( $item['text'] ) ) {
			$text = $item['text'];
		} else {
			if( isset( $item['msg'] ) ) {
				$msgText = wfMessage( $item['msg'] )->inContentLanguage();
				if ( $msgText->exists() ) {
					$text = $msgText->parse();
				}
			}
		}

		$html = htmlspecialchars( $text );

		// nextbestnetwork: set raw html
		if ( isset( $item['html'] )) {
			$html = $item['html'];
		}

		// nextbestnetwork: set icons for individual buttons (used by some navigational elements)
		if ( isset( $item['icon'] )) {
			$html = '<span class="fa fa-' . $item['icon'] . '"></span> ' . $html;
		}

		if ( isset( $options['text-wrapper'] ) ) {
			$wrapper = $options['text-wrapper'];
			if ( isset( $wrapper['tag'] ) ) {
				$wrapper = [ $wrapper ];
			}
			while ( count( $wrapper ) > 0 ) {
				$element = array_pop( $wrapper );
				$html = Html::rawElement( $element['tag'], $element['attributes'] ?? null, $html );
			}
		}

		// nextbestnetwork: allow empty first argument in the <btn> tag
		if( isset( $item['href'] ) && $item['href'] == '' ) {
			unset( $item['href'] );
			$options['link-fallback'] = 'span';
		}

		if ( isset( $item['href'] ) || isset( $options['link-fallback'] ) ) {
			$attrs = $item;
			foreach ( [ 'single-id', 'text', 'msg', 'tooltiponly', 'context', 'primary', 'href_implicit', 'items', 'icon', 'html', 'tooltip-params', 'exists', 'active' ] as $k ) {
				unset( $attrs[$k] );
			}

			if ( isset( $attrs['data'] ) ) {
				foreach ( $attrs['data'] as $key => $value ) {
					$attrs[ 'data-' . $key ] = $value;
				}
				unset( $attrs[ 'data' ] );
			}

			if ( isset( $item['id'] ) && !isset( $item['single-id'] ) ) {
				$item['single-id'] = $item['id'];
			}

			$tooltipParams = [];
			if ( isset( $item['tooltip-params'] ) ) {
				$tooltipParams = $item['tooltip-params'];
			}

			if ( isset( $item['single-id'] ) ) {
				$tooltipOption = isset( $item['exists'] ) && $item['exists'] === false ? 'nonexisting' : null;

				if ( isset( $item['tooltiponly'] ) && $item['tooltiponly'] ) {
					$title = Linker::titleAttrib( $item['single-id'], $tooltipOption, $tooltipParams );
					if ( $title !== false ) {
						$attrs['title'] = $title;
					}
				} else {
					$tip = Linker::tooltipAndAccesskeyAttribs( 
						$item['single-id'],
						$tooltipParams,
						$tooltipOption
					);
					if ( isset( $tip['title'] ) && $tip['title'] !== false ) {
						$attrs['title'] = $tip['title'];
					}
					if ( isset( $tip['accesskey'] ) && $tip['accesskey'] !== false ) {
						$attrs['accesskey'] = $tip['accesskey'];
					}
				}
			}
			if ( isset( $options['link-class'] ) ) {
				if ( isset( $attrs['class'] ) ) {
					// In the future, this should accept an array of classes, not a string
					if ( is_array( $attrs['class'] ) ) {
						$attrs['class'][] = $options['link-class'];
					} else {
						$attrs['class'] .= " {$options['link-class']}";
					}
				} else {
					$attrs['class'] = $options['link-class'];
				}
			}

			// nextbestnetwork: pass on active class
			if ( isset( $item['active'] ) && $item['active'] ) {
				if ( !isset( $attrs['class'] ) ) {
					$attrs['class'] = 'active';
				} else {
					$attrs['class'] = trim( $attrs['class'] . ' active' );
				}
			}

			$html = Html::rawElement( isset( $attrs['href'] ) 
				? 'a' 
				: $options['link-fallback'], $attrs, $html );
		}

		return $html;
	}


	/**
	 * Produce HTML for a list item
	 *
	 * This is a slightly adapted copy of the makeListItem function in SkinTemplate.php
	 * -> some of the changed parts are marked by comments //
	 *
	 * @param array $item
	 * @param array $options
	 */
	static function makeListItem( array $item, array $options = [] ) {
		if ( isset( $item['links'] ) ) {
			$html = '';
			foreach ( $item['links'] as $linkKey => $link ) {
				$html .= NextBestNetworkHooks::makeLink( $link, $options );
			}
		} else {
			$link = $item;
			// These keys are used by makeListItem and shouldn't be passed on to the link
			foreach ( [ 'id', 'class', 'tag' ] as $k ) {
				unset( $link[$k] );
			}
			if ( isset( $item['id'] ) && !isset( $item['single-id'] ) ) {
				// The id goes on the <li> not on the <a> for single links
				// but makeSidebarLink still needs to know what id to use when
				// generating tooltips and accesskeys.
				$link['single-id'] = $item['id'];
			}
			$html = NextBestNetworkHooks::makeLink( $link, $options );
		}

		$attrs = [];
		foreach ( [ 'id', 'class' ] as $attr ) {
			if ( isset( $item[$attr] ) ) {
				$attrs[$attr] = $item[$attr];
			}
		}
		if ( isset( $item['active'] ) && $item['active'] ) {
			if ( !isset( $attrs['class'] ) ) {
				$attrs['class'] = '';
			}
			$attrs['class'] .= ' active';
			$attrs['class'] = trim( $attrs['class'] );
		}
		return Html::rawElement( isset( $options['tag'] ) ? $options['tag'] : 'li', $attrs, $html );
	}


	/**
	 * Implement numbered headings
	 * 
	 * code from https://www.mediawiki.org/wiki/Extension:MagicNumberedHeadings
	 */
	/**
	 * @copyright Copyright © 2007, Purodha Blissenabch.
	 * @license GPL-2.0-or-later
	 *
	 * This program is free software; you can redistribute it and/or
	 * modify it under the terms of the GNU General Public License
	 * as published by the Free Software Foundation, version 2
	 * of the License.
	 *
	 * The above copyright notice and this permission notice shall be included in
	 * all copies or substantial portions of the Software.
	 *
	 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
	 * SOFTWARE.
	 * See the GNU General Public License for more details.
	 */
	public static function onMagicWordMagicWords( &$magicWords ) {
		$magicWords[] = 'MAG_NUMBEREDHEADINGS';
		return true;
	}

	public static function onMagicWordwgVariableIDs( &$wgVariableIDs ) {
		$wgVariableIDs[] = 'MAG_NUMBEREDHEADINGS';
		return true;
	}

	public static function onInternalParseBeforeLinks( &$parser, &$text, &$strip_state ) {
		$id = 'MAG_NUMBEREDHEADINGS';
		$magicWord = MediaWikiServices::getInstance()
			->getMagicWordFactory()
			->get( $id );
		if ( $magicWord->matchAndRemove( $text ) ) {
			return true; // needs reimplementation (with JS?), feature was removed from MW
			// $parser->mOptions->setNumberHeadings( true );
		}
		return true;
	}
}
