<?php
namespace ReyCore;

if (!defined('ABSPATH')) exit; // Exit if accessed directly

final class AssetsManager
{

	public static $assets_handler;

	public $debug = false;
	public $settings = [];

	/**
	 * All registered styles and scripts.
	 */
	private $registered_styles = [];
	private $registered_scripts = [];

	private static $styles_to_remove = [];
	private static $scripts_to_remove = [
		'wp-util'
	];

	private static $deferred_page_scripts = [];
	private static $deferred_page_styles = [];
	private static $deferred_page_styles__late = [];
	private static $deferred_styles = [];

	private static $__sorted_styles;

	private static $excluded_styles;
	private static $excluded_scripts;

	/**
	 * Styles and scripts that has been added throughout the page load
	 */
	private $styles = [];
	private $scripts = [];

	private $collected_scripts = [];
	private $collected_styles = [];

	/**
	 * Should cache separately for mobiles.
	 * Causes issues invalidating cache, and regenerates data.
	 */
	public $mobile = false;

	/**
	 * Html attribute used for the lazy stylesheets
	 */
	const LAZY_ATTRIBUTE = 'data-lazy-stylesheet';

	/**
	 * Name of lazy style
	 *
	 * @var string
	 */
	const NAME_LAZY = 'ds';

	/**
	 * Name of top priority style
	 *
	 * @var string
	 */
	const NAME_HEAD = 'hs';

	/**
	 * Determines if the placeholder is added
	 *
	 * @var string
	 */
	private static $__placeholder;

	/**
	 * Head stylesheet placeholder
	 */
	const HEAD_PLACEHOLDER = '<!-- REY_CSS -->';

	const CHILD_THEME_SLUG = 'rey-wp-style-child';

	/**
	 * Actuall only rey-script (rey.js) is mandatory,
	 * however RC scrpt also contains various important scripts.
	 *
	 * @var array
	 */
	public static $mandatory_scripts = ['rey-script', 'reycore-script'];

	public function __construct()
	{
		add_action( 'init', [$this, 'init']);
		add_action( 'admin_enqueue_scripts', [ $this, 'register_assets' ], 5 );
		add_action( 'wp_enqueue_scripts', [ $this, 'register_assets' ], 5 );
		add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_mandatory']);
		add_filter( 'style_loader_tag', [$this, 'style_loader_tag'], 10, 2);
		add_filter( 'script_loader_tag', [$this, 'defer_script_tags'], 10, 2);
		add_filter( 'rey/main_script_params', [$this, 'script_params']);
		add_action( 'wp_footer', [$this, 'enqueue_footer'], 15 );
		add_action( 'wp_head', [$this, 'add_head_stylesheet_placeholder'], 100 ); // 100, before `wp_custom_css_cb`

		new BufferManager();

		self::$assets_handler  = new AssetsHandler($this);

	}

	public function init(){

		$this->settings = [
			'save_css'        => REY_CORE_ASSETS_CSS, // combines and minifies CSS
			'css_combine'     => REY_CORE_ASSETS_CSS_COMBINE, // combines and minifies CSS
			'css_head'        => REY_CORE_ASSETS_CSS_HEAD, // 'block', 'inline'
			'css_delay_mode'  => REY_CORE_ASSETS_CSS_DELAY_MODE, // 'interaction', 'defer', 'defer_late'
			'save_js'         => REY_CORE_ASSETS_JS, // combines and minifies JS
			'defer_js'        => true,
			'delay_js'        => false,
			'mobile'          => false, // should behave differently in mobile (Work in progress),
			'mid_position'    => self::NAME_LAZY,
			'deferred_styles' => [
				'child_css' => self::CHILD_THEME_SLUG, // should be deferred if no caching plugin installed
				'cf7' => 'contact-form-7',
				'classic_theme_styles' => 'classic-theme-styles'
			],
			'deferred_scripts' => [
				'mc4wp' => 'mc4wp-forms-api',
			],
		];

		// disable in debug mode or if theme/core unsynced
		if( (defined('REY_OUTDATED') && REY_OUTDATED) || is_404() || strpos($_SERVER['REQUEST_URI'], 'wc-api') !== false ){
			$this->settings['save_css'] = false;
			$this->settings['save_js'] = false;
		}

		if( reycore__js_is_delayed() || is_customize_preview() ){
			$this->settings['save_js'] = false;
		}

		// if late deferring, mid-prioritized styles
		// must go to head
		if( $this->settings['save_css'] && 'defer_late' === $this->settings['css_delay_mode'] ){
			$this->settings['mid_position'] = self::NAME_HEAD;
		}

		$this->settings = apply_filters('reycore/assets/settings', $this->settings, $this);

		$this->mobile = $this->settings['mobile'] && reycore__is_mobile();

		self::$deferred_page_styles = $this->settings['deferred_styles'];

		$this->set_combine_excludes();
		$this->set_excluded_styles();
		$this->set_excluded_scripts();

	}

	public function get_settings($setting = ''){

		if( isset($this->settings[$setting]) ){
			return $this->settings[$setting];
		}

		return $this->settings;
	}

	public function set_settings($settings, $value = ''){

		if( is_array($settings) ){
			foreach ($settings as $setting => $value) {
				if( isset($this->settings[$setting]) ){
					$this->settings[$setting] = $value;
				}
			}
		}
		else {
			if( isset($this->settings[$settings]) ){
				$this->settings[$settings] = $value;
			}
		}

	}

	public function is_edit_mode(){

		static $__edit_mode;

		if( is_null($__edit_mode) ){
			$__edit_mode = reycore__elementor_edit_mode();
		}

		return $__edit_mode;
	}

	/**
	 * Register an asset
	 *
	 * @param string $type
	 * @param array $assets
	 * @return void
	 */
	public function register_asset( $type, $assets ){
		if( $type === 'styles' ){
			$this->registered_styles = array_merge($this->registered_styles, (array) $assets);
		}
		elseif( $type === 'scripts' ){
			$this->registered_scripts = array_merge($this->registered_scripts, (array) $assets);
		}
	}

	/**
	 * Register an asset
	 *
	 * @param string $type
	 * @param array $assets
	 * @return void
	 */
	public function update_asset( $type, $handle, $params = [] ){
		if( $type === 'styles' && isset($this->registered_styles[$handle]) ){
			foreach ($params as $key => $value) {
				if( isset($this->registered_styles[$handle][$key]) ){
					$this->registered_styles[$handle][$key] = $value;
				}
			}
		}
		else if( $type === 'scripts' && isset($this->registered_scripts[$handle]) ){
			foreach ($params as $key => $value) {
				if( isset($this->registered_scripts[$handle][$key]) ){
					$this->registered_scripts[$handle][$key] = $value;
				}
			}
		}
	}

	/**
	 * De-Register an asset
	 *
	 * @param string $type
	 * @param array $assets
	 * @return void
	 */
	public function deregister_asset( $type, $assets ){
		if( $type === 'styles' ){
			foreach ((array) $assets as $asset) {
				unset($this->registered_styles[$asset]);
			}
		}
		elseif( $type === 'scripts' ){
			foreach ((array) $assets as $asset) {
				unset($this->registered_scripts[$asset]);
			}
		}
	}

	/**
	 * Register and collect assets
	 *
	 * @return void
	 */
	public function register_assets(){

		/**
		 * Hook to register Rey styles.
		 * @since 2.0.0
		 */
		do_action('reycore/assets/register_scripts', $this);

		$is_admin = is_admin() && ! wp_doing_ajax();

		foreach( $this->registered_styles as $handle => $style ){
			if( $is_admin ){
				if( ! (isset($style['admin']) && $style['admin']) ){
					continue;
				}
			}
			if( ! isset($this->registered_styles[$handle]['path']) && strpos($style['src'], REY_CORE_URI) !== false ){
				$this->registered_styles[$handle]['path'] = str_replace(REY_CORE_URI, REY_CORE_DIR, $style['src']);
			}
			wp_register_style($handle, $style['src'], $style['deps'], $style['version']);
		}

		foreach( $this->registered_scripts as $handle => $script ){
			if( $is_admin ){
				if( ! (isset($script['admin']) && $script['admin']) ){
					continue;
				}
			}
			if( isset($script['src']) ){
				if( ! isset($this->registered_scripts[$handle]['path']) && strpos($script['src'], REY_CORE_URI) !== false ){
					$this->registered_scripts[$handle]['path'] = str_replace(REY_CORE_URI, REY_CORE_DIR, $script['src']);
				}
				wp_register_script(
					$handle,
					$script['src'],
					isset($script['deps']) ? $script['deps'] : [],
					isset($script['version']) ? $script['version'] : REY_CORE_VERSION,
					isset($script['in_footer']) ? $script['in_footer'] : true
				);
				if( isset($script['localize']) && is_array($script['localize']['params']) ){
					wp_localize_script($handle, $script['localize']['name'], $script['localize']['params']);
				}
			}
		}

		do_action('qm/debug', sprintf('Registered %s Styles and %s Scripts', count(array_keys($this->registered_styles)), count(array_keys($this->registered_scripts)) ) );

		// error_log(var_export( array_keys($this->registered_styles), true));
		// error_log(var_export( array_keys($this->registered_scripts), true));
	}

	public function get_register_assets( $type = 'styles' ){
		if( 'styles' === $type ){
			return $this->registered_styles;
		}
		else if( 'scripts' === $type ){
			return $this->registered_scripts;
		}
	}

	/**
	 * Add style but deferred. To be enqueued later.
	 *
	 * @param array $handlers
	 * @return void
	 */
	public function add_deferred_styles($handlers){

		foreach ((array) $handlers as $handler) {

			if( in_array($handler, self::$styles_to_remove, true) ){
				continue;
			}

			if( ! $this->style_is($handler) ){
				continue;
			}

			// maybe it was requested before, so don't defer it
			if( ! in_array($handler, $this->styles, true) ){
				self::$deferred_styles[ $handler ] = $this->registered_styles[ $handler ];
			}

			$this->styles[] = $handler;

			if( ! $this->settings['save_css'] ){
				wp_enqueue_style($handler);
			}
		}
	}

	/**
	 * Enqueue and add styles to enqueue collection
	 *
	 * @param array $handlers
	 * @return void
	 */
	public function add_styles( $handlers ){

		$is_admin = is_admin() && ! wp_doing_ajax();

		foreach ((array) $handlers as $key => $handler) {

			if( in_array($handler, self::$styles_to_remove, true) ){
				continue;
			}

			// already enqueued
			if( in_array($handler, $this->styles, true) ){
				if(
					// don't stop in case it's an ajax filter / pagination, because `$this->styles`
					// holds several styles which are not loaded during collection.
					! ( isset($_REQUEST['reynotemplate']) && absint( $_REQUEST['reynotemplate'] ) === 1)
				){
					continue;
				}
			}

			// add dependencies as styles
			if($deps = $this->style_is($handler, 'deps')){
				foreach ( (array) $deps as $dep_handler) {

					// not a rey style
					if( ! $this->style_is($dep_handler) ){
						continue;
					}

					// include in low priority styles
					if( $this->is_low_priority($dep_handler) ){
						self::$deferred_styles[ $dep_handler ] = $this->registered_styles[$dep_handler];
					}

					$this->styles[] = $dep_handler;

					if( $is_admin ){
						wp_enqueue_style($dep_handler);
					}
				}
			}

			if( $is_admin ){
				wp_enqueue_style($handler);
				continue;
			}

			// debug
			// if( in_array($handler, [ '' ]) ){
			// 	error_log(var_export( wp_debug_backtrace_summary(), true));
			// }

			foreach ($this->collected_styles as $key => $value) {
				$this->collected_styles[$key][] = $handler;
			}

			// non Rey styles, check if already enqueued
			if( ! $this->style_is($handler) ){
				if( wp_style_is($handler, 'enqueued') ){
					continue;
				}
			}

			if( $this->is_low_priority($handler) ){
				self::$deferred_styles[ $handler ] = $this->registered_styles[$handler];
			}

			$this->styles[] = $handler;

			if( ! $this->settings['save_css'] ){
				wp_enqueue_style($handler);
			}
		}
	}

	public function is_low_priority($handler){
		return 'low' === $this->style_is($handler, 'priority');
	}

	public function remove_styles($handlers){
		foreach ((array) $handlers as $handler) {
			self::$styles_to_remove[$handler] = $handler;
		}
	}

	public function remove_scripts($handlers){
		foreach ((array) $handlers as $handler) {
			self::$scripts_to_remove[$handler] = $handler;
		}
	}

	/**
	 * Determine if an asset should be enqueued without demand.
	 *
	 * @param array $asset
	 * @param string $handle
	 * @return bool
	 */
	public function maybe_enqueue_mandatory( $asset, $handle = '' ){

		$enqueue = false;

		// always enqueue
		if( isset($asset['enqueue']) && $asset['enqueue'] ){

			$enqueue = true;

			if( in_array($handle, \ReyCore\Assets::get_excludes(), true) ){
				$enqueue = false;
			}

		}

		else {
			// check callback
			if( isset($asset['callback']) ){
				if( is_callable($asset['callback']) && call_user_func($asset['callback']) ){
					$enqueue = true;
				}
			}
		}

		return $enqueue;
	}

	/**
	 * Enqueue Mandatory scripts and styles
	 *
	 * @return void
	 */
	public function enqueue_mandatory(){

		do_action('reycore/assets/enqueue', $this);

		// Just load everything in elementor mode
		if( $this->is_edit_mode() ){
			foreach (array_keys($this->registered_styles) as $style) {
				wp_enqueue_style($style);
			}
			return;
		}

		foreach( $this->registered_styles as $handle => $style ){

			if( in_array($handle, self::$styles_to_remove, true) ){
				continue;
			}

			if( $this->maybe_enqueue_mandatory( $style, $handle ) ){
				$this->add_styles($handle);
			}
		}

		foreach( $this->registered_scripts as $handle => $script ){

			if( in_array($handle, self::$scripts_to_remove, true) ){
				continue;
			}

			if( $this->maybe_enqueue_mandatory( $script, $handle ) ){
				$this->add_scripts($handle);
			}
		}

	}

	/**
	 * Make a page stylesheet load deferred
	 * Used for late GSs.
	 *
	 * @param array $handlers
	 * @return void
	 */
	public function defer_page_styles($handlers, $late = false)
	{
		foreach ( (array) $handlers as $handle) {
			self::$deferred_page_styles[$handle] = $handle;
			if( $late ){
				self::$deferred_page_styles__late[$handle] = $handle;
			}
		}
	}

	/**
	 * Make a page stylesheet load deferred
	 * Used for late GSs.
	 *
	 * @param array $handlers
	 * @return void
	 */
	public function defer_page_scripts($handlers)
	{
		foreach ( (array) $handlers as $handle) {
			self::$deferred_page_scripts[$handle] = $handle;
		}
	}

	public static function get_defer_method( $early = true ){
		if( $early ){
			return ' rel="preload" as="style" onload="this.onload=null;this.rel=\'stylesheet\';" media="all" ';
		}
		return ' rel="stylesheet" onload="this.onload=null;this.media=\'all\';" media="print" ';
	}

	public function replace_style_attribute($tag, $handle = ''){

		$no_opt = reycore__css_no_opt_attr();

		if( $handle === self::CHILD_THEME_SLUG ){
			$no_opt = '';
		}

		// defer the stylesheet
		if( in_array($this->settings['css_delay_mode'], ['defer', 'defer_late'], true) ){

			$tag = str_replace( ' media=', ' data-media=', $tag);
			$no_script_tag = sprintf('<noscript>%s</noscript>', str_replace( [' id=', ' type=', self::HEAD_PLACEHOLDER], [ ' ' . $no_opt . ' data-id=', ' data-type=', ''], $tag));

			$search_for = ' rel=';
			$replace_with = implode(' ', [
				self::get_defer_method( ! in_array($handle, self::$deferred_page_styles__late, true) ),
				$no_opt,
				'data-no-rel=',
			]);

			return str_replace( $search_for, $replace_with, $tag) . $no_script_tag;
		}

		// set a lazy attribute
		return str_replace(' href=', sprintf(' %s %s=', $no_opt, self::LAZY_ATTRIBUTE), $tag);

	}

	/**
	 * Override style tag output
	 *
	 * @param string $tag
	 * @param string $handle
	 * @return string
	 */
	public function style_loader_tag($tag, $handle){

		if( ! $this->can_run_loader_tag() ){
			return $tag;
		}

		if( self::$excluded_styles ){
			if(
				in_array($handle, self::$excluded_styles, true)
				|| (($words = implode('|', self::$excluded_styles)) && preg_match("~\b({$words})\b~i", $tag, $tag_matches))
			){
				if( isset($tag_matches) && ! empty($tag_matches) ){
					unset(self::$excluded_styles[$tag_matches[0]]);
				}
				unset(self::$excluded_styles[$handle]);
				return '';
			}
		}

		if( $this->can_add_placeholder() && 'elementor-frontend' === $handle ){
			$this->added_placeholder();
			$tag .= self::HEAD_PLACEHOLDER;
		}

		// Remove woocommerce general request. It's empty.
		// Just need the handle as dependency target
		if( in_array($handle, ['woocommerce-general'], true) ){
			return '';
		}

		if( in_array($handle, self::$styles_to_remove, true) ){
			return '';
		}

		if(
			// must have lazy enabled
			in_array($this->settings['css_delay_mode'], ['defer', 'defer_late'], true)
			// it's a low priority stylesheet
			&& isset( self::$deferred_styles[$handle] )
			// purposely deferring a stylesheet (not necesarily Rey)
			|| in_array($handle, self::$deferred_page_styles, true)
		){
			return $this->replace_style_attribute($tag, $handle);
		}

		return $tag;
	}

	/**
	 * Sort the css stylesheets by priorities
	 *
	 * @param array $data
	 * @return array
	 */
	public function sort_css_priorities($data){

		$high = $mid = $low = $others = [];

		foreach($data as $key => $handle){

			if( ! isset( $this->registered_styles[ $handle ] ) ){
				continue;
			}

			$style = $this->registered_styles[$handle];

			if ( isset(self::$deferred_styles[$handle]) ) {
				$low[] = $handle;
			}

			else if( isset($style['priority']) )
			{
				switch($style['priority']):
					case"high":
						$high[] = $handle;
					break;
					case"mid":
						$mid[] = $handle;
					break;
					case"low":
						$low[] = $handle;
					break;
				endswitch;
			}
			// whatever this may be
			else{
				$others[] = $handle;
			}
		}

		if( self::NAME_LAZY === $this->settings['mid_position'] ){
			$low = array_merge($low, $mid);
		}

		else if( self::NAME_HEAD === $this->settings['mid_position'] ){
			$high = array_merge($high, $mid);
		}

		return [
			self::NAME_HEAD => array_merge($high, $others),
			self::NAME_LAZY => $low,
		];
	}

	/**
	 * Get the page styles. Used for collecting enqueued styles.
	 *
	 * @return array
	 */
	public function get_styles(){

		if( self::$__sorted_styles ){
			return self::$__sorted_styles;
		}

		self::$__sorted_styles = $this->sort_css_priorities( array_unique($this->styles) );

		return self::$__sorted_styles;
	}

	/**
	 * Get the deferred page styles.
	 *
	 * @return array
	 */
	public static function get_deferred_styles(){
		return array_unique(array_keys(self::$deferred_styles));
	}

	/**
	 * Create an object with the styles which have been added to the page.
	 *
	 * @param array $styles
	 * @return void
	 */
	public function output_inserted_styles($styles = []){

		if( empty($styles) ){
			$styles = $this->get_styles();
		}

		printf("<script type='text/javascript' id='reystyles-loaded'>\n window.reyStyles=%s; \n</script>", wp_json_encode(array_values($styles)));
	}

	/**
	 * Retrive scripts that have been requested in the page
	 *
	 * @return array
	 */
	public function get_scripts(){
		return array_unique($this->scripts);
	}

	/**
	 * Retrive scripts that have been requested in the page
	 *
	 * @return array
	 */
	public function get_all_scripts(){
		if( empty($this->get_scripts()) ){
			return [];
		}
		return array_merge(self::$mandatory_scripts, $this->get_scripts());
	}

	/**
	 * Add script to collection of to be enqueued
	 *
	 * @param array $handlers
	 * @return void
	 */
	public function add_scripts( $handlers ){

		$is_admin = is_admin() && ! wp_doing_ajax();

		foreach ((array) $handlers as $key => $handler) {

			if( in_array($handler, self::$scripts_to_remove, true) ){
				continue;
			}

			$collectable_handlers = [];

			// add dependencies as scripts
			if( $deps = $this->script_is($handler, 'deps') ){

				foreach ( (array) $deps as $dep_handler)
				{
					// not a rey script (eg jQuery)
					if( ! $this->script_is($dep_handler) ){
						// but make sure it's loaded
						wp_enqueue_script($dep_handler);
						continue;
					}

					// it's an external script, so should just load
					if( $this->script_is($dep_handler, 'external') ){
						wp_enqueue_script($dep_handler);
						continue;
					}

					// make sure it's collected
					$collectable_handlers[] = $dep_handler;

					if( $is_admin ){
						wp_enqueue_script($dep_handler);
					}

					// It's a JS plugin script.
					// To play well with other wp extensions if they use these librraries,
					// they should load outside of the bundle
					if( $this->settings['save_js'] && $this->script_is($dep_handler, 'plugin') ){
						wp_enqueue_script($dep_handler);
						continue;
					}

					$this->scripts[] = $dep_handler;
				}
			}

			if( $is_admin ){
				wp_enqueue_script($handler);
				continue;
			}

			$collectable_handlers[] = $handler;

			foreach ($this->collected_scripts as $key => $value) {
				foreach ($collectable_handlers as $item) {
					$this->collected_scripts[$key][] = $item;
				}
			}

			// It's a JS plugin script.
			// To play well with other wp extensions if they use these librraries,
			// they should load outside of the bundle
			if( $this->settings['save_js'] && $this->script_is($handler, 'plugin') ){
				wp_enqueue_script($handler);
				continue;
			}

			$this->scripts[] = $handler;

		}
	}

	public function script_is($handler, $type = ''){
		if( ! $type ){
			return isset($this->registered_scripts[$handler]);
		}
		return isset($this->registered_scripts[$handler]) && isset($this->registered_scripts[$handler][$type]) ? $this->registered_scripts[$handler][$type] : '';
	}

	public function style_is($handler, $type = ''){
		if( ! $type ){
			return isset($this->registered_styles[$handler]);
		}
		return isset($this->registered_styles[$handler]) && isset($this->registered_styles[$handler][$type]) ? $this->registered_styles[$handler][$type] : '';
	}

	/**
	 * Run late footer scripts
	 *
	 * @return void
	 */
	public function enqueue_footer(){

		do_action('reycore/assets/enqueue_footer', $this);

		$this->output_inserted_styles();

		$this->enqueue_js();

		do_action('qm/debug', sprintf('Enqueued %s Styles and %s Scripts', count(array_unique($this->styles)), count($this->get_all_scripts()) ));

	}

	/**
	 * Render a JS global object containing all scripts that have been added to the page
	 *
	 * @param array $scripts
	 * @return void
	 */
	public function output_inserted_scripts($scripts){
		printf("<script type='text/javascript' id='reyscripts-loaded'>\n window.reyScripts=%s; \n</script>", wp_json_encode(array_values($scripts)));
	}

	/**
	 * Enqueue JS scripts
	 *
	 * @return void
	 */
	public function enqueue_js(){

		if( $this->is_edit_mode() ){
			if( $registered_scripts = array_keys($this->registered_scripts) ){
				foreach (array_merge($registered_scripts, self::$mandatory_scripts) as $script) {
					wp_enqueue_script($script);
				}
			}
			return;
		}

		if( ! ($scripts = $this->get_scripts()) ){
			return;
		}

		$scripts = array_merge(self::$mandatory_scripts, $scripts);

		$this->output_inserted_scripts($scripts);

		// error_log(var_export( $scripts, true));

		// when bundle is enabled
		// scripts having localized data need to be added
		// but cleaned up afterwards
		if( $this->settings['save_js'] ){
			global $wp_scripts;
			foreach ($scripts as $handle) {
				if( $script_data = $wp_scripts->get_data( $handle, 'data' ) ){
					printf('<script type="text/javascript" id="%1$s-js-extra" %3$s>%2$s</script>', $handle, $script_data, reycore__js_no_opt_attr()) . "\n";
				}
				$this->load_icons($handle);
			}
		}
		// just enqueue scripts
		else {
			foreach ($scripts as $script) {
				wp_enqueue_script($script);
				$this->load_icons($script);
			}
		}

	}

	public function load_icons( $handle ){
		if( isset($this->registered_scripts[$handle]['icons']) ){
			\ReyCore\Plugin::instance()->js_icons->include_icons( (array) $this->registered_scripts[$handle]['icons'] );
		}
	}

	public function can_run_loader_tag(){

		static $wont_run;

		if( is_null($wont_run) ){
			$wont_run = is_admin() || $this->is_edit_mode();
		}

		return ! $wont_run;
	}

	/**
	 * Filter scripts output to append a defer tag
	 *
	 * @param string $tag
	 * @param string $handle
	 * @return string
	 */
	public function defer_script_tags($tag, $handle){

		if( ! $this->can_run_loader_tag() ){
			return $tag;
		}

		if( self::$excluded_scripts ){
			if(
				in_array($handle, self::$excluded_scripts, true)
				|| (($words = implode('|', self::$excluded_scripts)) && preg_match("~\b({$words})\b~i", $tag, $tag_matches))
			){
				if( isset($tag_matches) && ! empty($tag_matches) ){
					unset(self::$excluded_scripts[$tag_matches[0]]);
				}
				unset(self::$excluded_scripts[$handle]);
				return '';
			}
		}

		if( in_array($handle, self::$scripts_to_remove, true) ){
			return $tag;
		}

		// Stop if no deferring
		if( ! $this->settings['defer_js'] ){
			return $tag;
		}

		// already deferred
		if( strpos($tag, ' defer') !== false ){
			return $tag;
		}

		if(
			// pre-defined deferred scripts list
			in_array($handle, $this->settings['deferred_scripts'], true)
			// manually deferred page scripts
			|| in_array($handle, self::$deferred_page_scripts, true)
			// Specified scripts
			|| $this->script_is($handle, 'defer')
			// Rey JS plugins
			|| $this->script_is($handle, 'plugin')
		){
			return str_replace( ' src', ' defer src', $tag );
		}

		// no need to continue going through all scripts
		// if bundle is enabled.
		if( $this->settings['save_js'] ){

			// Rey Script already loaded in bundle
			if( 'rey-script' === $handle
				&& ! is_admin()
				&& current_user_can('administrator')
				&& ! is_customize_preview()
				&& ! $this->is_edit_mode()
			){
				return '';
			}

			return $tag;
		}

		if( is_admin() || isset($_REQUEST['elementor-preview']) && ! empty($_REQUEST['elementor-preview']) ){
			return $tag;
		}

		if( in_array($handle, $this->get_all_scripts(), true) ){
			return str_replace( ' src', ' defer src', $tag );
		}

		return $tag;
	}

	/**
	 * Adds a placeholder in the HEAD tag
	 * to be replaced by the main stylesheet's tag
	 *
	 * @return void
	 */
	public function add_head_stylesheet_placeholder()
	{
		if( ! $this->can_run_loader_tag() ){
			return;
		}
		if( $this->can_add_placeholder() ){
			echo self::HEAD_PLACEHOLDER;
			$this->added_placeholder();
		}
	}

	public function added_placeholder(){
		self::$__placeholder = true;
	}

	public function can_add_placeholder(){
		return is_null(self::$__placeholder);
	}


	/**
	 * Retrieve RTL stylesheet suffix
	 *
	 * @return string
	 */
	public static function rtl(){
		return is_rtl() ? '-rtl' : '';
	}

	public function collect_start( $name ){
		$this->collected_styles[$name] = [];
		$this->collected_scripts[$name] = [];
	}

	public function collect_end($name, $src = false, $type = '' ){

		$collected = [
			'scripts' => [],
			'styles' => [],
		];

		if( $this->collected_scripts[$name] ){
			$collected['scripts'] = array_unique( $this->collected_scripts[$name] );
		}
		if( $this->collected_styles[$name] ){
			$collected['styles'] = array_unique( $this->collected_styles[$name] );
		}

		do_action('qm/debug', sprintf('Collected for `%s` %s Styles and %s Scripts', $name, count($collected['styles']), count($collected['scripts']) ) );

		unset($this->collected_styles[$name]);
		unset($this->collected_scripts[$name]);

		if( $src ){
			return [
				'scripts' => $this->get_assets_uri($collected, 'scripts'),
				'styles' => $this->get_assets_uri($collected, 'styles'),
			];
		}

		if( $type && isset($collected[$type]) ){
			return $collected[$type];
		}

		return $collected;
	}

	public function downgrade_styles_priority($name){
		if( ! isset($this->collected_styles[$name]) ){
			return;
		}
		foreach ( $this->collect_end($name, false, 'styles') as $handler) {
			self::$deferred_styles[$handler] = $handler;
		}
	}

	public function get_assets_uri( $assets, $type = 'styles' ){

		$assets_to_return = [];

		$single_asset = ! isset( $assets['styles'] ) && ! isset( $assets['scripts'] );

		if( ! isset($assets[$type]) && ! $single_asset ){
			return $assets_to_return;
		}

		$wp_assets = $type === 'styles' ? wp_styles() : wp_scripts();

		$the_assets = $single_asset ? (array) $assets : $assets[$type];

		foreach ($the_assets as $key => $handler) {

			if( ! (isset($wp_assets->registered[ $handler ]) && ($script = $wp_assets->registered[ $handler ])) ){
				continue;
			}
			if( ! (isset($script->src) && ($src = $script->src)) ){
				continue;
			}

			$site_url = get_site_url();

			if ( 0 === strpos( $src, $site_url ) || 0 === strpos( $src, 'http' ) ) {
				$src_ = $src;
			} else {
				$src_ = $site_url . $src;
			}

			if( $single_asset ){
				$assets_to_return = $src_;
			}
			else {
				$assets_to_return[$handler] = $src_;
			}
		}

		return $assets_to_return;
	}

	public function script_params($params){

		foreach ($this->registered_styles as $handle => $style) {
			if( isset($style['lazy_assets']) && ($selector = $style['lazy_assets']) ){
				// add initial style
				$params['lazy_assets'][$selector]['styles'][$handle] = $style['src'];
				// check dependencies
				if( isset($style['deps']) ){
					foreach ($style['deps'] as $dep) {
						if( isset($this->registered_styles[$dep]) ){
							$params['lazy_assets'][$selector]['styles'][$dep] = $this->registered_styles[$dep]['src'];
						}
					}
				}
			}
		}

		foreach ($this->registered_scripts as $handle => $script) {
			if( isset($script['lazy_assets']) && ($selector = $script['lazy_assets']) ){
				// add initial script
				$params['lazy_assets'][$selector]['scripts'][$handle] = $script['src'];
				// check dependencies
				if( isset($script['deps']) ){
					foreach ($script['deps'] as $dep) {
						if( isset($this->registered_scripts[$dep]) ){
							$params['lazy_assets'][$selector]['scripts'][$dep] = $this->registered_scripts[$dep]['src'];
						}
					}
				}
			}
		}

		return $params;
	}

	public function combine_excludes_filter($val){

		$excludes[] = sprintf('rey-%s-css', self::NAME_LAZY);

		if( 'defer' === $this->settings['css_head'] ){
			$excludes[] = sprintf('rey-%s-css', self::NAME_HEAD);
		}

		unset(self::$deferred_page_styles['child_css']);

		return array_merge($val, $excludes, array_keys(self::$deferred_styles), self::$deferred_page_styles);
	}

	public function set_combine_excludes(){

		if( ! $this->settings['save_css'] ){
			return;
		}

		foreach ([
			'sgo_css_combine_exclude', // Siteground Optimizer
			'wp-optimize-minify-default-exclusions', // WP Optimize
			// 'breeze_filter_css_exclude', // Breeze
		] as $filter_id) {
			add_filter($filter_id, [$this, 'combine_excludes_filter']);
		}
	}

	public function set_excluded_styles(){

		if( is_null(self::$excluded_styles) ){
			self::$excluded_styles = [];
			if( $opt = get_theme_mod('perf__exclude_styles', '') ){
				$split = preg_split("/\r\n|\n|\r/", $opt);
				if( ! empty($split) ){
					self::$excluded_styles = array_combine($split, $split);
				}
			}
		}

		return self::$excluded_styles;
	}

	public function set_excluded_scripts(){

		if( is_null(self::$excluded_scripts) ){
			self::$excluded_scripts = [];
			if( $opt = get_theme_mod('perf__exclude_scripts', '') ){
				$split = preg_split("/\r\n|\n|\r/", $opt);
				if( ! empty($split) ){
					self::$excluded_scripts = array_combine($split, $split);
				}
			}
		}

		return self::$excluded_scripts;
	}
}
