<?php
/** @noinspection MultipleReturnStatementsInspection */

namespace WpAssetCleanUp\OptimiseAssets;

use WpAssetCleanUp\Main;
use WpAssetCleanUp\Misc;

/**
 * Class FontsGoogle
 * @package WpAssetCleanUp\OptimiseAssets
 */
class FontsGoogle
{
	/**
	 * @var string
	 */
	public static $containsStr = '//fonts.googleapis.com/';

	/**
	 * @var string
	 */
	public static $matchesStr = '//fonts.googleapis.com/(css2?|icon)\?';

	/**
	 *
	 */
	const NOSCRIPT_WEB_FONT_LOADER = '<span style="display: none;" data-name=wpacu-delimiter content="ASSET CLEANUP NOSCRIPT WEB FONT LOADER"></span>';

	/**
	 *
	 */
	const COMBINED_LINK_DEL = '<span style="display: none;" data-name=wpacu-delimiter content="ASSET CLEANUP COMBINED LINK LOCATION"></span>';

	/**
	 *
	 */
	public function init()
	{
		FontsGooglePreloadScanner::maybeInitFrontendCollector();

		if (self::preventAnyChange()) {
			return;
		}

		add_filter('wp_resource_hints', array($this, 'resourceHints'), PHP_INT_MAX, 2);

		add_action('wp_head',   array($this, 'preloadFontFiles'), 1);
		add_action('wp_footer', static function() {
			if ( OptimizeCommon::preventAnyFrontendOptimization() || Main::instance()->settings['google_fonts_remove'] ) {
				return;
			}

			echo self::NOSCRIPT_WEB_FONT_LOADER;
		}, PHP_INT_MAX);

		add_filter('wpacu_html_source_after_optimization', static function($htmlSource) {
			// Is the mark still there and wasn't replaced? Strip it
			return str_replace(FontsGoogle::NOSCRIPT_WEB_FONT_LOADER, '', $htmlSource);
		});

		add_action('init', function() {
			// don't apply any changes if not in the front-end view (e.g. Dashboard view)
			// or test mode is enabled, and a guest user is accessing the page
			if ( OptimizeCommon::preventAnyFrontendOptimization() || Main::instance()->settings['google_fonts_remove'] || ! Main::instance()->settings['google_fonts_display'] ) {
				return;
			}

			add_filter('style_loader_src', array($this, 'alterGoogleFontLink'));
		}, 20);
	}

	/**
	 * @param $urls
	 * @param $relationType
	 *
	 * @return array
	 */
	public function resourceHints($urls, $relationType)
	{
		// don't apply any changes if not in the front-end view (e.g. Dashboard view)
		// or test mode is enabled, and a guest user is accessing the page
		if (is_admin() || OptimizeCommon::preventAnyFrontendOptimization()) {
			return $urls;
		}

		// Are the Google Fonts removed? Do not add them and strip any existing
		// resource hints. WordPress accepts both string URLs and arrays such as
		// array('href' => 'https://fonts.gstatic.com/', 'crossorigin').
		if (! empty($urls) && Main::instance()->settings['google_fonts_remove']) {
			foreach ($urls as $urlKey => $urlValue) {
				$urlToCheck = '';

				if (is_string($urlValue)) {
					$urlToCheck = $urlValue;
				} elseif (is_array($urlValue) && isset($urlValue['href']) && is_string($urlValue['href'])) {
					$urlToCheck = $urlValue['href'];
				}

				if ($urlToCheck !== '' && FontsGoogleRemove::containsAnyGoogleFontsReference($urlToCheck)) {
					unset($urls[$urlKey]);
				}
			}

			return $urls; // Finally, return the list after any removals
		}

		// Google Fonts "preconnect"
		if ('preconnect' === $relationType
		    && ! Main::instance()->settings['google_fonts_remove']  // "Remove Google Fonts" has to be turned off
		    && Main::instance()->settings['google_fonts_preconnect']) { // Needs to be enabled within "Plugin Usage Preferences" in "Settings"
			$urls[] = array(
				'href' => 'https://fonts.gstatic.com/',
				'crossorigin'
			);
		}

		return $urls;
	}

	/**
	 *
	 */
	public function preloadFontFiles()
	{
		// Suppress only WPACU's manual Google font-file preloads during the
		// browser-assisted audit. The site's Google stylesheets remain active.
		if ( FontsGooglePreloadScanner::isActiveRequest() ) {
			return;
		}

		// Don't apply any changes if not in the front-end view (e.g. Dashboard view)
		// or test mode is enabled, and a guest user is accessing the page
		if ( OptimizeCommon::preventAnyFrontendOptimization() ) {
			return;
		}

		if ( ! $preloadFontFiles = trim(Main::instance()->settings['google_fonts_preload_files']) ) {
			return;
		}

		$preloadFontFilesArray = array();

		foreach (preg_split('/\r\n|\r|\n/', $preloadFontFiles) as $preloadFontFile) {
			// Keep legacy compatibility with existing configurations that used this
			// field for another HTTP(S) font host. The audit reports those entries
			// for manual review, while runtime output remains safely escaped.
			$preloadFontFile = esc_url_raw(trim(wp_strip_all_tags($preloadFontFile)), array('http', 'https'));

			if ( ! $preloadFontFile ) {
				continue;
			}

			$preloadFontFilesArray[$preloadFontFile] = $preloadFontFile;
		}

		$preloadFontFilesOutput = '';

		foreach ($preloadFontFilesArray as $preloadFontFile) {
			$preloadFontFilesOutput .= '<link rel="preload" as="font" href="' . esc_url($preloadFontFile) . '" data-wpacu-preload-google-font="1" crossorigin>' . "\n";
		}

		echo apply_filters('wpacu_preload_google_font_files_output', $preloadFontFilesOutput);
	}

	/**
	 * @param $htmlSource
	 *
	 * @return false|mixed|string|void
     * @noinspection NestedAssignmentsUsageInspection
     */
	public static function alterHtmlSource($htmlSource)
	{
		// don't apply any changes if not in the front-end view (e.g. Dashboard view)
		// or test mode is enabled and a guest user is accessing the page
		// or an AMP page is accessed
		if ( OptimizeCommon::preventAnyFrontendOptimization() ) {
			return $htmlSource;
		}

		/*
		 * Remove Google Fonts? Stop here as optimization is no longer relevant
		 */
		if (Main::instance()->settings['google_fonts_remove']) {
			return FontsGoogleRemove::cleanHtmlSource($htmlSource);
		}

		/*
		 * Optimize Google Fonts
		 */
		if (stripos($htmlSource, self::$containsStr) !== false) {
			// Cleaner HTML Source
			$altHtmlSource = preg_replace( '@<(script|style|noscript)[^>]*?>.*?</\\1>@si', '', $htmlSource ); // strip irrelevant tags for the collection
			$altHtmlSource = preg_replace( '/<!--[^>]*' . preg_quote( self::$containsStr, '/' ) . '.*?-->/', '', $altHtmlSource ); // strip any comments containing the string

			// Get all valid LINKs that have the $string within them
			preg_match_all( '#<link[^>]*' . self::$matchesStr . '.*(>)#Usmi', $altHtmlSource, $matchesFromLinkTags, PREG_SET_ORDER );

			// Needs to match at least one to carry on with the replacements
			if ( ! empty( $matchesFromLinkTags[0] ) ) {
				if ( Main::instance()->settings['google_fonts_combine'] ) {
					/*
					 * "Combine Google Fonts" IS enabled
					 */
					$finalCombinableLinks = $preloadedLinks = array();

					foreach ( $matchesFromLinkTags as $linkTagArray ) {
						$linkTag = $finalLinkTag = trim( trim( $linkTagArray[0], '"\'' ) );

						// Extra checks to make sure it's a valid LINK tag
						if ( ( strpos( $linkTag, "'" ) !== false && ( substr_count( $linkTag, "'" ) % 2 ) )
						     || ( strpos( $linkTag, '"' ) !== false && ( substr_count( $linkTag, '"' ) % 2 ) )
						     || ( trim( strip_tags( $linkTag ) ) !== '' ) ) {
							continue;
						}

						// Check if the CSS has any 'data-wpacu-skip' attribute; if it does, do not continue and leave it as it is (non-combined)
						if ( Misc::hasExactDataAttr($linkTag, 'data-wpacu-skip') ) {
							continue;
						}

						$finalLinkHref = $linkHrefOriginal = Misc::getValueFromTag($linkTag);

						// Remove malformed Google Fonts requests that declare no usable family.
						// The helper preserves repeated CSS2 family parameters instead of
						// collapsing them through parse_str().
						if ( ! self::hasNonEmptyFamilyParameter($linkHrefOriginal) ) {
							$htmlSource = str_replace($linkTag, '', $htmlSource);
							continue;
						}

						// If anything is set apart from '[none set]', proceed
						if ( Main::instance()->settings['google_fonts_display'] ) {
							$finalLinkHref = self::alterGoogleFontLink( $linkHrefOriginal );

							if ( $finalLinkHref !== $linkHrefOriginal ) {
								$finalLinkTag = str_replace( $linkHrefOriginal, $finalLinkHref, $linkTag );

								// Finally, alter the HTML source
								$htmlSource = str_replace( $linkTag, $finalLinkTag, $htmlSource );
							}
						}

						// The combine implementation below targets the legacy Google Fonts
						// CSS API v1 syntax. Leave CSS2, Material Icons and other endpoints
						// untouched apart from the safe font-display append above.
						if ( ! self::isLegacyCombinableGoogleFontsUrl($finalLinkHref) ) {
							continue;
						}

						if ( preg_match( '/rel=(["\'])preload(["\'])/i', $finalLinkTag )
						     || strpos( $finalLinkTag, 'data-wpacu-to-be-preloaded-basic' ) ) {
							$preloadedLinks[] = $finalLinkHref;
						}

						$finalCombinableLinks[] = array( 'href' => $finalLinkHref, 'tag' => $finalLinkTag );
					}

					$preloadedLinks = array_unique( $preloadedLinks );

					// Remove data for preloaded LINKs
					if ( ! empty( $preloadedLinks ) ) {
						foreach ( $finalCombinableLinks as $fclIndex => $combinableLinkData ) {
							if ( in_array( $combinableLinkData['href'], $preloadedLinks ) ) {
								unset( $finalCombinableLinks[ $fclIndex ] );
							}
						}
					}

					$finalCombinableLinks = array_values( $finalCombinableLinks );

					// Only proceed with the optimization/combine if there's obviously at least 2 combinable URL requests to Google Fonts
					// OR the loading type is different from render-blocking
					if ( Main::instance()->settings['google_fonts_combine_type'] || count( $finalCombinableLinks ) > 1 ) {
						$htmlSource = self::combineGoogleFontLinks( $finalCombinableLinks, $htmlSource );
					}
				} elseif (Main::instance()->settings['google_fonts_display']) {
					/*
					 * "Combine Google Fonts" IS NOT enabled
					 * Go through the links and apply any "font-display"
				     */
					foreach ( $matchesFromLinkTags as $linkTagArray ) {
						$linkTag = trim( trim( $linkTagArray[0], '"\'' ) );

						// Extra checks to make sure it's a valid LINK tag
						if ( ( strpos( $linkTag, "'" ) !== false && ( substr_count( $linkTag, "'" ) % 2 ) )
						     || ( strpos( $linkTag, '"' ) !== false && ( substr_count( $linkTag, '"' ) % 2 ) )
						     || ( trim( strip_tags( $linkTag ) ) !== '' ) ) {
							continue;
						}

						// Check if the CSS has any 'data-wpacu-skip' attribute; if it does, do not continue and leave it as it is (non-altered)
						if ( Misc::hasExactDataAttr($linkTag, 'data-wpacu-skip') ) {
							continue;
						}

						$linkHrefOriginal = Misc::getValueFromTag($linkTag);

						// Remove malformed Google Fonts requests that declare no usable family.
						// The helper preserves repeated CSS2 family parameters instead of
						// collapsing them through parse_str().
						if ( ! self::hasNonEmptyFamilyParameter($linkHrefOriginal) ) {
							$htmlSource = str_replace($linkTag, '', $htmlSource);
							continue;
						}

						// If anything is set apart from '[none set]', proceed
						$newLinkHref = self::alterGoogleFontLink( $linkHrefOriginal );

						if ( $newLinkHref !== $linkHrefOriginal ) {
							$finalLinkTag = str_replace( $linkHrefOriginal, $newLinkHref, $linkTag );

							// Finally, alter the HTML source
							$htmlSource = str_replace( $linkTag, $finalLinkTag, $htmlSource );
						}
					}
				}
			}
		}

		// "font-display: swap;" if enabled
		$htmlSource = self::alterGoogleFontUrlFromInlineStyleTags($htmlSource);

		// Clear any traces
		return str_replace(self::NOSCRIPT_WEB_FONT_LOADER, '', $htmlSource);
	}

	/**
	 * @param string $linkHrefOriginal
	 * @param bool $escHtml
	 * @param string $alterFor
	 *
	 * @return string
	 */
	public static function alterGoogleFontLink($linkHrefOriginal, $escHtml = true, $alterFor = 'css')
	{
		$isInVar = false; // The link is inside a variable with a JSON format

		// Some special filtering here as some hosting environments (at least staging) behave funny with // inside SCRIPT tags
		if ($alterFor === 'js') {
			$containsStrNoSlashes = str_replace('/', '', self::$containsStr);
			$conditionOne = stripos($linkHrefOriginal, $containsStrNoSlashes) === false;

			if (strpos($linkHrefOriginal, '\\/') !== false) {
				$isInVar = true;
			}
		} else { // css (default)
			$conditionOne = stripos($linkHrefOriginal, self::$containsStr) === false;
		}

		if ($conditionOne || ! self::hasNonEmptyFamilyParameter($linkHrefOriginal) || ! Main::instance()->settings['google_fonts_display']) {
			return $linkHrefOriginal;
		}

		$displayValue = rawurlencode((string) Main::instance()->settings['google_fonts_display']);
		$displayOverwrite = ! empty(Main::instance()->settings['google_fonts_display_overwrite']);

		if ($displayValue === '') {
			return $linkHrefOriginal;
		}

		$analysisHref = str_replace(array('&#038;', '&amp;'), '&', $linkHrefOriginal);

		if ($isInVar) {
			$analysisHref = str_replace('\\/', '/', $analysisHref);
		}

		$query = parse_url($analysisHref, PHP_URL_QUERY);
		$query = is_string($query) ? $query : '';

		if (preg_match('/(?:^|&)display=([^&]*)/i', $query, $displayMatch)) {
			if (isset($displayMatch[1]) && $displayMatch[1] !== '' && ! $displayOverwrite) {
				return $linkHrefOriginal;
			}

			// Preserve the original query byte-for-byte. Fill an empty display
			// value by default, or replace any existing value when requested.
			return preg_replace(
				'~((?:\?|&|&amp;|&#038;)display=)[^&]*~i',
				'$1' . $displayValue,
				$linkHrefOriginal,
				1
			);
		}

		if (strpos($linkHrefOriginal, '?') === false) {
			return $linkHrefOriginal . '?display=' . $displayValue;
		}

		if (strpos($linkHrefOriginal, '&#038;') !== false) {
			$separator = '&#038;';
		} elseif (strpos($linkHrefOriginal, '&amp;') !== false) {
			$separator = '&amp;';
		} else {
			$separator = $escHtml ? '&#038;' : '&';
		}

		return $linkHrefOriginal . $separator . 'display=' . $displayValue;
	}

	/**
	 * @param $htmlSource
	 *
	 * @return mixed
	 */
	public static function alterGoogleFontUrlFromInlineStyleTags($htmlSource)
	{
		if (! preg_match('/@import(\s+)url\(/i', $htmlSource)) {
			return $htmlSource;
		}

		preg_match_all('#<\s*?style\b[^>]*>(.*?)</style\b[^>]*>#s', $htmlSource, $styleMatches, PREG_SET_ORDER);

		if (empty($styleMatches)) {
			return $htmlSource;
		}

		// Go through each STYLE tag
		foreach ($styleMatches as $styleInlineArray) {
			list($styleInlineTag, $styleInlineContent) = $styleInlineArray;

			// Check if the STYLE tag has any 'data-wpacu-skip' attribute; if it does, do not continue
			if (Misc::hasExactDataAttr($styleInlineTag, 'data-wpacu-skip')) {
				continue;
			}

			// Is the content relevant?
			if (! preg_match('/@import(\s+|)(url|\(|\'|")/i', $styleInlineContent)
			    || stripos($styleInlineContent, 'fonts.googleapis.com') === false) {
				continue;
			}

			// Do any alteration to the URL of the Google Font
			$newCssOutput = self::alterGoogleFontUrlFromCssContent($styleInlineTag);

			$htmlSource = str_replace($styleInlineTag, $newCssOutput, $htmlSource);
		}

		return $htmlSource;
	}

	/**
	 * @param $cssContent
	 *
	 * @return mixed
	 */
	public static function alterGoogleFontUrlFromCssContent($cssContent)
	{
		if (stripos($cssContent, 'fonts.googleapis.com') === false || ! Main::instance()->settings['google_fonts_display']) {
			return $cssContent;
		}

		$regExps = array('/@import(\s+)url\((.*?)\)(|\s+)\;/i', '/@import(\s+|)(\(|\'|")(.*?)(\'|"|\))\;/i');

		$newCssOutput = $cssContent;

		foreach ($regExps as $regExpIndex => $regExpPattern) {
			preg_match_all($regExpPattern, $cssContent, $matchesFromInlineCode, PREG_SET_ORDER);

			if (! empty($matchesFromInlineCode)) {
				foreach ($matchesFromInlineCode as $matchesFromInlineCodeArray) {
					$cssImportRule = $matchesFromInlineCodeArray[0];

					if ($regExpIndex === 0) {
						$googleApisUrl = trim($matchesFromInlineCodeArray[2], '"\' ');
					} else {
						$googleApisUrl = trim($matchesFromInlineCodeArray[3], '"\' ');
					}

					// It has to be a Google Fonts API link
					if (stripos($googleApisUrl, 'fonts.googleapis.com') === false) {
						continue;
					}

					$newGoogleApisUrl = self::alterGoogleFontLink($googleApisUrl, false);

					if ($newGoogleApisUrl !== $googleApisUrl) {
						$newCssImportRule = str_replace($googleApisUrl, $newGoogleApisUrl, $cssImportRule);
						$newCssOutput = str_replace($cssImportRule, $newCssImportRule, $newCssOutput);
					}
				}
			}
		}

		return $newCssOutput;
	}

	/**
	 * @param $jsContent
	 *
	 * @return mixed
	 */
	public static function alterGoogleFontUrlFromJsContent($jsContent)
	{
		$newJsOutput = $jsContent;

        if (stripos($jsContent, 'fonts.googleapis.com') !== false) {
            preg_match_all('#fonts.googleapis.com(.*?)(["\'])#si', $jsContent, $matchesFromJsCode);

            if ( ! empty($matchesFromJsCode[0]) ) {
                foreach ($matchesFromJsCode[0] as $match) {
                    $matchRule     = $match;
                    $googleApisUrl = trim($match, '"\' ');

                    $newGoogleApisUrl = self::alterGoogleFontLink($googleApisUrl, false, 'js');
                    if ($newGoogleApisUrl !== $googleApisUrl) {
                        $newJsMatchOutput = str_replace($googleApisUrl, $newGoogleApisUrl, $matchRule);
                        $newJsOutput      = str_replace($matchRule, $newJsMatchOutput, $newJsOutput);
                    }
                }
            }
        }

        foreach (array('WebFontConfig', 'WebFont') as $webFontStr) {
            if (stripos($jsContent, $webFontStr) !== false) {
                // Case 1:
                // Look for any "WebFontConfig = { google: { families: ['font-one', 'font-two'] } }" patterns
                // and for any "WebFont = { google: { families: ['font-one', 'font-two'] } }" patterns
                preg_match_all(
                    '#'.$webFontStr.'(.*?)google(\s+|):(\s+|){(\s+|)families(\s+|):(?<families>.*?)]#s',
                    $jsContent,
                    $webFontConfigMatches
                );

                if ( ! empty($webFontConfigMatches['families']) ) {
                    foreach ($webFontConfigMatches['families'] as $webFontConfigKey => $webFontConfigMatch) {
                        $originalWholeMatch  = $webFontConfigMatches[0][$webFontConfigKey];
                        $familiesMatchOutput = trim($webFontConfigMatch);

                        $hasDisplayParameter = stripos($familiesMatchOutput, 'display=') !== false;

                        // Keep an existing value unless overwriting was explicitly requested.
                        if ( ! $familiesMatchOutput || ($hasDisplayParameter && empty(Main::instance()->settings['google_fonts_display_overwrite'])) ) {
                            continue;
                        }

                        $familiesNewOutput = $hasDisplayParameter
                            ? preg_replace('/([?&]display=)[^&\'\"]*/i', '$1' . Main::instance()->settings['google_fonts_display'], $familiesMatchOutput, 1)
                            : preg_replace('/([\'"])$/', '&display=' . Main::instance()->settings['google_fonts_display'] . '\\1', $familiesMatchOutput);

                        $newWebFontConfigOutput = str_replace($familiesMatchOutput, $familiesNewOutput, $originalWholeMatch);

                        // Finally, do the replacement
                        $newJsOutput = str_replace($originalWholeMatch, $newWebFontConfigOutput, $newJsOutput);
                    }
                }

                // Case 2
                // Look for any "WebFontConfig['google'] = {families: ['font-one', 'font-two']};" / "WebFontConfig.google = {families: ['font-one', 'font-two']};"
                // and for any "WebFont['google'] = {families: ['font-one', 'font-two']};" / "WebFont.google = {families: ['font-one', 'font-two']};"
                preg_match_all(
                    '#'.$webFontStr.'(.*?)google(.*?)=(.*?)families(\s+|):(?<families>.*?)]#si',
                    $jsContent,
                    $webFontConfigMatchesTwo
                );

                if ( ! empty($webFontConfigMatchesTwo['families']) ) {
                    foreach ($webFontConfigMatchesTwo['families'] as $webFontConfigKey => $webFontConfigMatch) {
                        $originalWholeMatch  = $webFontConfigMatchesTwo[0][$webFontConfigKey];
                        $familiesMatchOutput = trim($webFontConfigMatch);

                        $hasDisplayParameter = stripos($familiesMatchOutput, 'display=') !== false;

                        // Keep an existing value unless overwriting was explicitly requested.
                        if ( ! $familiesMatchOutput || ($hasDisplayParameter && empty(Main::instance()->settings['google_fonts_display_overwrite'])) ) {
                            continue;
                        }

                        $familiesNewOutput = $hasDisplayParameter
                            ? preg_replace('/([?&]display=)[^&\'\"]*/i', '$1' . Main::instance()->settings['google_fonts_display'], $familiesMatchOutput, 1)
                            : preg_replace('/([\'"])$/', '&display=' . Main::instance()->settings['google_fonts_display'] . '\\1', $familiesMatchOutput);

                        $newWebFontConfigOutput = str_replace($familiesMatchOutput, $familiesNewOutput, $originalWholeMatch);

                        // Finally, do the replacement
                        $newJsOutput = str_replace($originalWholeMatch, $newWebFontConfigOutput, $newJsOutput);
                    }
                }
            }
        }

		return $newJsOutput;
	}

	/**
	 * @param $finalLinks
	 * @param $htmlSource
	 *
	 * @return false|mixed|string|void
	 */
	public static function combineGoogleFontLinks($finalLinks, $htmlSource)
	{
		$fontsArray = array();

		foreach ($finalLinks as $finalLinkIndex => $finalLinkData) {
			$finalLinkHref = $finalLinkData['href'];

			if ( ! self::isLegacyCombinableGoogleFontsUrl($finalLinkHref) ) {
				continue;
			}

            // Make sure all have the same common delimiters
            // They will be restored later on for "W3 Validator Compatibility"
			$finalLinkHref = str_replace(array('&#038;', '%7C'), array('&', '|'), $finalLinkHref);

			$queries = parse_url($finalLinkHref, PHP_URL_QUERY);
			parse_str($queries, $fontQueries);

			if (! array_key_exists('family', $fontQueries) || array_key_exists('text', $fontQueries)) {
				continue;
			}

			// Strip the existing tag, leave a mark where the final combined LINK will be placed
			$stripTagWith = ($finalLinkIndex === 0) ? self::COMBINED_LINK_DEL : '';
			$finalLinkTag = $finalLinkData['tag'];

			$htmlSource = str_ireplace(array($finalLinkTag."\n", $finalLinkTag), $stripTagWith, $htmlSource);

			$family = trim($fontQueries['family']);
			$family = trim($family, '|');

			if (! $family) {
				continue;
			}

			if (strpos($family, '|') !== false) {
				// More than one family per request?
				foreach (explode('|', $family) as $familyOne) {
					if (strpos($familyOne, ':') !== false) {
						// They have types
						list ($familyRaw, $familyTypes) = explode(':', $familyOne);
						$fontsArray['families'][$familyRaw]['types'] = self::buildSortTypesList($familyTypes);
					} else {
						// They do not have types
						$familyRaw = $familyOne;
						$fontsArray['families'][$familyRaw]['types'] = false;
					}
				}
			} elseif (strpos($family, ':') !== false) {
				list ($familyRaw, $familyTypes) = explode(':', $family);
				$fontsArray['families'][$familyRaw]['types'] = self::buildSortTypesList($familyTypes);
			} else {
				$familyRaw = $family;
				$fontsArray['families'][$familyRaw]['types'] = false;
			}

			if (array_key_exists('subset', $fontQueries)) {
				// More than one subset per request?
				if (strpos($fontQueries['subset'], ',') !== false) {
					$multipleSubsets = explode(',', trim($fontQueries['subset'], ','));

					foreach ($multipleSubsets as $subset) {
						$fontsArray['subsets'][] = trim($subset);
					}
				} else {
					// Only one subset
					$fontsArray['subsets'][] = $fontQueries['subset'];
				}
			}

			if (array_key_exists('effect', $fontQueries)) {
				// More than one subset per request?
				if (strpos($fontQueries['effect'], '|') !== false) {
					$multipleSubsets = explode('|', trim($fontQueries['effect'], '|'));

					foreach ($multipleSubsets as $subset) {
						$fontsArray['effect'][] = trim($subset);
					}
				} else {
					// Only one subset
					$fontsArray['effect'][] = $fontQueries['effect'];
				}
			}
		}

		if ( ! empty($fontsArray) ) {
			$finalCombinedParameters = '';
			ksort($fontsArray['families']);

			// Families
			foreach ($fontsArray['families'] as $familyRaw => $fontValues) {
				$finalCombinedParameters .= str_replace(' ', '+', $familyRaw);

				// Any types? e.g. 400, 400italic, bold, etc.
				if (isset($fontValues['types']) && $fontValues['types'] !== false) {
					$finalCombinedParameters .= ':' . $fontValues['types'];
				}

				$finalCombinedParameters .= '|';
			}

			$finalCombinedParameters = trim($finalCombinedParameters, '|');

			// Subsets
			if ( ! empty($fontsArray['subsets']) ) {
				sort($fontsArray['subsets']);
				$finalCombinedParameters .= '&subset=' . implode(',', array_unique($fontsArray['subsets']));
			}

			// Effect(s)
			if ( ! empty($fontsArray['effect']) ) {
				sort($fontsArray['effect']);
				$finalCombinedParameters .= '&effect=' . implode('|', array_unique($fontsArray['effect']));
			}

			if ($fontDisplay = Main::instance()->settings['google_fonts_display']) {
				$finalCombinedParameters .= '&display=' . $fontDisplay;
			}

            // For "W3 Validator"
            $finalCombinedParameters = str_replace('|', '%7C', $finalCombinedParameters);
			$finalCombinedParameters = esc_attr($finalCombinedParameters);

			// This is needed for both render-blocking and async (within NOSCRIPT tag as a fallback)
			$finalCombinedLink = <<<LINK
<link rel='stylesheet' id='wpacu-combined-google-fonts-css' href='https://fonts.googleapis.com/css?family={$finalCombinedParameters}' type='text/css' media='all' />
LINK;
			/*
			 * Loading Type: Render-Blocking (Default)
			 */
			if ( ! Main::instance()->settings['google_fonts_combine_type'] ) {
				$finalCombinedLink .= "\n";
				$htmlSource = str_replace(self::COMBINED_LINK_DEL, apply_filters('wpacu_combined_google_fonts_link_tag', $finalCombinedLink), $htmlSource);
			}

			/*
			 * Loading Type: Asynchronous via LINK preload with fallback
			 */
			if (Main::instance()->settings['google_fonts_combine_type'] === 'async_preload') {
				$finalPreloadCombinedLink = <<<LINK
<link rel='preload' as="style" onload="this.onload=null;this.rel='stylesheet'" data-wpacu-preload-it-async='1' id='wpacu-combined-google-fonts-css-async-preload' href='https://fonts.googleapis.com/css?family={$finalCombinedParameters}' type='text/css' media='all' />
LINK;
				$finalPreloadCombinedLink .= "\n".Misc::preloadAsyncCssFallbackOutput();

				$htmlSource = str_replace(self::COMBINED_LINK_DEL, apply_filters('wpacu_combined_google_fonts_async_preload_link_tag', $finalPreloadCombinedLink), $htmlSource);
			}

			/*
			 * Loading Type: Asynchronous via Web Font Loader (webfont.js) with fallback
			 */
			if (Main::instance()->settings['google_fonts_combine_type'] === 'async') { // Async via Web Font Loader
				$subSetsStr = '';

				if ( ! empty($fontsArray['subsets']) ) {
					sort($fontsArray['subsets']);
					$subSetsStr = implode(',', array_unique($fontsArray['subsets']));
				}

				$wfConfigGoogleFamilies = array();

				// Families
				$iCount = 0;

				foreach ($fontsArray['families'] as $familyRaw => $fontValues) {
					$wfConfigGoogleFamily = str_replace(' ', '+', $familyRaw);

					// Any types? e.g. 400, 400italic, bold, etc.
					$hasTypes = false;
					if (isset($fontValues['types']) && $fontValues['types']) {
						$wfConfigGoogleFamily .= ':'.$fontValues['types'];
						$hasTypes = true;
					}

					if ($subSetsStr) {
						// If there are types, continue to use the comma delimiter
						$wfConfigGoogleFamily .= ($hasTypes ? ',' : ':') . $subSetsStr;
					}

					// Append extra parameters to the last family from the list
					if ($iCount === count($fontsArray['families']) - 1) {
						// Effect(s)
						if ( ! empty($fontsArray['effect']) ) {
							sort($fontsArray['effect']);
							$wfConfigGoogleFamily .= '&effect=' . implode('%7C', array_unique($fontsArray['effect']));
						}

						if ($fontDisplay = Main::instance()->settings['google_fonts_display']) {
							$wfConfigGoogleFamily .= '&display=' . $fontDisplay;
						}
					}

					$wfConfigGoogleFamilies[] = "'".$wfConfigGoogleFamily."'";

					$iCount++;
				}

				$wfConfigGoogleFamiliesStr = '['.implode(',', $wfConfigGoogleFamilies).']';

				$scriptType = Misc::getScriptTypeAttribute();

				$finalInlineTagWebFontConfig = '<script id=\'wpacu-google-fonts-async-load\' '.$scriptType.'>'."\n".'WebFontConfig={google:{families:'.$wfConfigGoogleFamiliesStr.'}};(function(wpacuD){var wpacuWf=wpacuD.createElement(\'script\'),wpacuS=wpacuD.scripts[0];wpacuWf.src=(\'https:\'===document.location.protocol?\'https\':\'http\')+\'://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js\';wpacuWf.async=!0;wpacuS.parentNode.insertBefore(wpacuWf,wpacuS)})(document);'."\n".'</script>';

				$htmlSource = str_replace(
					array(
						self::COMBINED_LINK_DEL,
						self::NOSCRIPT_WEB_FONT_LOADER
					),
					array(
						apply_filters( 'wpacu_combined_google_fonts_inline_script_tag', $finalInlineTagWebFontConfig ),
						'<noscript>' . apply_filters( 'wpacu_combined_google_fonts_link_tag', $finalCombinedLink ) . '</noscript>' . "\n"
					),
					$htmlSource
				);
			}
		}

		return $htmlSource;
	}

	/**
	 * e.g. 300, 400, 400italic, bold, etc.
	 *
	 * @param $types
	 *
	 * @return string
	 */
	public static function buildSortTypesList($types)
	{
		$newTypes = array();

		// More than one type per family?
		if (strpos($types, ',') !== false) {
			$multipleTypes = explode(',', trim($types, ','));

			foreach ($multipleTypes as $type) {
				if (trim($type)) {
					$newTypes[] = trim($type);
				}
			}
		} else {
			// Only one type per family
			$newTypes[] = $types;
		}

		$newTypes = array_unique($newTypes);

        $newTypes = array_map(function($type) {
            return ($type === 'bold') ? '700' : $type;
        }, $newTypes);

		sort($newTypes);

		return implode(',', $newTypes);
	}

	/**
	 * Whether a Google Fonts URL contains at least one non-empty family value.
	 * Repeated CSS2 family parameters are deliberately preserved.
	 *
	 * @param string $url
	 *
	 * @return bool
	 */
	private static function hasNonEmptyFamilyParameter($url)
	{
		$analysisUrl = str_replace(array('&#038;', '&amp;', '\\/'), array('&', '&', '/'), (string) $url);
		$query       = parse_url($analysisUrl, PHP_URL_QUERY);

		if ( ! is_string($query) ) {
			return false;
		}

		preg_match_all('/(?:^|&)family=([^&]*)/i', $query, $familyMatches);

		if (empty($familyMatches[1])) {
			return false;
		}

		foreach ($familyMatches[1] as $familyValue) {
			if (trim(rawurldecode(str_replace('+', ' ', $familyValue))) !== '') {
				return true;
			}
		}

		return false;
	}

	/**
	 * The existing combiner is intentionally limited to the legacy /css API.
	 * CSS2 and icon endpoints can contain repeated parameters and variable axes
	 * that must not be reconstructed as a v1 family list.
	 *
	 * @param string $url
	 *
	 * @return bool
	 */
	private static function isLegacyCombinableGoogleFontsUrl($url)
	{
		$analysisUrl = str_replace(array('&#038;', '&amp;', '\\/'), array('&', '&', '/'), (string) $url);

		return (bool) preg_match('#(?:https?:)?//fonts\.googleapis\.com/css\?#i', $analysisUrl);
	}

	/**
	 * @return bool
	 */
	public static function preventAnyChange()
	{
		return wpacuIsDefinedConstant( 'WPACU_ALLOW_ONLY_UNLOAD_RULES' );
	}
}
