<?php if ( ! defined( 'FW' ) ) {
	die( 'Forbidden' );
}
// Useful functions

/**
 * Convert to Unix style directory separators
 */
function fw_fix_path( $path ) {
	$windows_network_path = isset( $_SERVER['windir'] ) && in_array( substr( $path, 0, 2 ),
			array( '//', '\\\\' ),
			true );
	$fixed_path           = untrailingslashit( str_replace( array( '//', '\\' ), array( '/', '/' ), $path ) );

	if ( empty( $fixed_path ) && ! empty( $path ) ) {
		$fixed_path = '/';
	}

	if ( $windows_network_path ) {
		$fixed_path = '//' . ltrim( $fixed_path, '/' );
	}

	return $fixed_path;
}

/**
 * Relative path of the framework customizations directory
 *
 * @param string $append
 *
 * @return string
 */
function fw_get_framework_customizations_dir_rel_path( $append = '' ) {
	try {
		$dir = FW_Cache::get( $cache_key = 'fw_customizations_dir_rel_path' );
	} catch ( FW_Cache_Not_Found_Exception $e ) {
		FW_Cache::set(
			$cache_key,
			$dir = apply_filters( 'fw_framework_customizations_dir_rel_path', '/framework-customizations' )
		);
	}

	return $dir . $append;
}

/** Child theme related functions */
{
	/**
	 * Full path to the child-theme framework customizations directory
	 *
	 * @param string $rel_path
	 *
	 * @return null|string
	 */
	function fw_get_stylesheet_customizations_directory( $rel_path = '' ) {
		if ( is_child_theme() ) {
			return get_stylesheet_directory() . fw_get_framework_customizations_dir_rel_path( $rel_path );
		} else {
			// check is_child_theme() before using this function
			return null;
		}
	}

	/**
	 * URI to the child-theme framework customizations directory
	 *
	 * @param string $rel_path
	 *
	 * @return null|string
	 */
	function fw_get_stylesheet_customizations_directory_uri( $rel_path = '' ) {
		if ( is_child_theme() ) {
			return get_stylesheet_directory_uri() . fw_get_framework_customizations_dir_rel_path( $rel_path );
		} else {
			// check is_child_theme() before using this function
			return null;
		}
	}
}

/** Parent theme related functions */
{
	/**
	 * Full path to the parent-theme framework customizations directory
	 *
	 * @param string $rel_path
	 *
	 * @return string
	 */
	function fw_get_template_customizations_directory( $rel_path = '' ) {
		try {
			$dir = FW_Cache::get( $cache_key = 'fw_template_customizations_dir' );
		} catch ( FW_Cache_Not_Found_Exception $e ) {
			FW_Cache::set(
				$cache_key,
				$dir = get_template_directory() . fw_get_framework_customizations_dir_rel_path()
			);
		}

		return $dir . $rel_path;
	}

	/**
	 * URI to the parent-theme framework customizations directory
	 *
	 * @param string $rel_path
	 *
	 * @return string
	 */
	function fw_get_template_customizations_directory_uri( $rel_path = '' ) {
		try {
			$dir = FW_Cache::get( $cache_key = 'fw_template_customizations_dir_uri' );
		} catch ( FW_Cache_Not_Found_Exception $e ) {
			FW_Cache::set(
				$cache_key,
				$dir = get_template_directory_uri() . fw_get_framework_customizations_dir_rel_path()
			);
		}

		return $dir . $rel_path;
	}
}

/** Framework related functions */
{
	/**
	 * Full path to the parent-theme/framework directory
	 *
	 * @param string $rel_path
	 *
	 * @return string
	 */
	function fw_get_framework_directory( $rel_path = '' ) {
		try {
			$dir = FW_Cache::get( $cache_key = 'fw_framework_dir' );
		} catch ( FW_Cache_Not_Found_Exception $e ) {
			FW_Cache::set(
				$cache_key,
				$dir = apply_filters(
					'fw_framework_directory',
					fw_fix_path( dirname( dirname( __FILE__ ) ) ) // double dirname() to remove '/helpers', use parent dir
				)
			);
		}

		return $dir . $rel_path;
	}

	/**
	 * URI to the parent-theme/framework directory
	 *
	 * @param string $rel_path
	 *
	 * @return string
	 */
	function fw_get_framework_directory_uri( $rel_path = '' ) {

		$uri = str_replace('/helpers', '', plugins_url( "", __FILE__ ));
		return $uri . $rel_path;

		try {
			$uri = FW_Cache::get( $cache_key = 'fw_framework_dir_uri' );
		} catch ( FW_Cache_Not_Found_Exception $e ) {
			FW_Cache::set(
				$cache_key,
				$uri = apply_filters(
					'fw_framework_directory_uri',
					( $uri = fw_get_path_url( fw_get_framework_directory() ) )
						? $uri
						: get_template_directory_uri() . '/framework'
				)
			);
		}

		return $uri . $rel_path;
	}
}

/**
 * Recursively find a key's value in array
 *
 * @param string $keys 'a/b/c'
 * @param array|object $array_or_object
 * @param null|mixed $default_value
 * @param string $keys_delimiter
 *
 * @return null|mixed
 */
function fw_akg( $keys, $array_or_object, $default_value = null, $keys_delimiter = '/' ) {
	if ( ! is_array( $keys ) ) {
		$keys = explode( $keys_delimiter, (string) $keys );
	}

	$array_or_object = fw_call( $array_or_object );

	$key_or_property = array_shift( $keys );
	if ( $key_or_property === null ) {
		return fw_call( $default_value );
	}

	$is_object = is_object( $array_or_object );

	if ( $is_object ) {
		if ( ! property_exists( $array_or_object, $key_or_property ) ) {
			return fw_call( $default_value );
		}
	} else {
		if ( ! is_array( $array_or_object ) || ! array_key_exists( $key_or_property, $array_or_object ) ) {
			return fw_call( $default_value );
		}
	}

	if ( isset( $keys[0] ) ) { // not used count() for performance reasons
		if ( $is_object ) {
			return fw_akg( $keys, $array_or_object->{$key_or_property}, $default_value );
		} else {
			return fw_akg( $keys, $array_or_object[ $key_or_property ], $default_value );
		}
	} else {
		if ( $is_object ) {
			return $array_or_object->{$key_or_property};
		} else {
			return $array_or_object[ $key_or_property ];
		}
	}
}

/**
 * Set (or create if not exists) value for specified key in some array level
 *
 * @param string $keys 'a/b/c', or 'a/b/c/' equivalent to: $arr['a']['b']['c'][] = $val;
 * @param mixed $value
 * @param array|object $array_or_object
 * @param string $keys_delimiter
 *
 * @return array|object
 */
function fw_aks( $keys, $value, &$array_or_object, $keys_delimiter = '/' ) {
	if ( ! is_array( $keys ) ) {
		$keys = explode( $keys_delimiter, (string) $keys );
	}

	$key_or_property = array_shift( $keys );
	if ( $key_or_property === null ) {
		return $array_or_object;
	}

	$is_object = is_object( $array_or_object );

	if ( $is_object ) {
		if ( ! property_exists( $array_or_object, $key_or_property )
		     || ! ( is_array( $array_or_object->{$key_or_property} ) || is_object( $array_or_object->{$key_or_property} ) )
		) {
			if ( $key_or_property === '' ) {
				// this happens when use 'empty keys' like: abc/d/e////i/j//foo/
				trigger_error( 'Cannot push value to object like in array ($arr[] = $val)', E_USER_WARNING );
			} else {
				$array_or_object->{$key_or_property} = array();
			}
		}
	} else {
		if ( ! is_array( $array_or_object ) ) {
			$array_or_object = array();
		}

		if ( ! array_key_exists( $key_or_property,
				$array_or_object ) || ! is_array( $array_or_object[ $key_or_property ] )
		) {
			if ( $key_or_property === '' ) {
				// this happens when use 'empty keys' like: abc.d.e....i.j..foo.
				$array_or_object[] = array();

				// get auto created key (last)
				end( $array_or_object );
				$key_or_property = key( $array_or_object );
			} else {
				$array_or_object[ $key_or_property ] = array();
			}
		}
	}

	if ( isset( $keys[0] ) ) { // not used count() for performance reasons
		if ( $is_object ) {
			fw_aks( $keys, $value, $array_or_object->{$key_or_property} );
		} else {
			fw_aks( $keys, $value, $array_or_object[ $key_or_property ] );
		}
	} else {
		if ( $is_object ) {
			$array_or_object->{$key_or_property} = $value;
		} else {
			$array_or_object[ $key_or_property ] = $value;
		}
	}

	return $array_or_object;
}

/**
 * Unset specified key in some array level
 *
 * @param string $keys 'a/b/c' -> unset($arr['a']['b']['c']);
 * @param array|object $array_or_object
 * @param string $keys_delimiter
 *
 * @return array|object
 */
function fw_aku( $keys, &$array_or_object, $keys_delimiter = '/' ) {
	if ( ! is_array( $keys ) ) {
		$keys = explode( $keys_delimiter, (string) $keys );
	}

	$key_or_property = array_shift( $keys );
	if ( $key_or_property === null || $key_or_property === '' ) {
		return $array_or_object;
	}

	$is_object = is_object( $array_or_object );

	if ( $is_object ) {
		if ( ! property_exists( $array_or_object, $key_or_property ) ) {
			return $array_or_object;
		}
	} else {
		if ( ! is_array( $array_or_object ) || ! array_key_exists( $key_or_property, $array_or_object ) ) {
			return $array_or_object;
		}
	}

	if ( isset( $keys[0] ) ) { // not used count() for performance reasons
		if ( $is_object ) {
			fw_aku( $keys, $array_or_object->{$key_or_property} );
		} else {
			fw_aku( $keys, $array_or_object[ $key_or_property ] );
		}
	} else {
		if ( $is_object ) {
			unset( $array_or_object->{$key_or_property} );
		} else {
			unset( $array_or_object[ $key_or_property ] );
		}
	}

	return $array_or_object;
}

/**
 * Generate random unique md5
 */
function fw_rand_md5() {
	return md5( time() . '-' . uniqid( rand(), true ) . '-' . mt_rand( 1, 1000 ) );
}

function fw_unique_increment() {
	static $i = 0;

	return ++ $i;
}

/**
 * print_r() alternative
 *
 * @param mixed $value Value to debug
 */
function fw_print( $value ) {
	static $first_time = true;

	if ( $first_time ) {
		ob_start();
		echo '<style type="text/css">
		div.fw_print_r {
			max-height: 500px;
			overflow-y: scroll;
			background: #23282d;
			margin: 10px 30px;
			padding: 0;
			border: 1px solid #F5F5F5;
			border-radius: 3px;
			position: relative;
			z-index: 11111;
		}

		div.fw_print_r pre {
			color: #78FF5B;
			background: #23282d;
			text-shadow: 1px 1px 0 #000;
			font-family: Consolas, monospace;
			font-size: 12px;
			margin: 0;
			padding: 5px;
			display: block;
			line-height: 16px;
			text-align: left;
		}

		div.fw_print_r_group {
			background: #f1f1f1;
			margin: 10px 30px;
			padding: 1px;
			border-radius: 5px;
			position: relative;
			z-index: 11110;
		}
		div.fw_print_r_group div.fw_print_r {
			margin: 9px;
			border-width: 0;
		}
		</style>';
		echo str_replace( array( '  ', "\n" ), '', ob_get_clean() );

		$first_time = false;
	}

	if ( func_num_args() == 1 ) {
		echo '<div class="fw_print_r"><pre>';
		echo fw_htmlspecialchars( FW_Dumper::dump( $value ) );
		echo '</pre></div>';
	} else {
		echo '<div class="fw_print_r_group">';
		foreach ( func_get_args() as $param ) {
			fw_print( $param );
		}
		echo '</div>';
	}
}

/**
 * Alias for fw_print
 *
 * @see fw_print()
 */
if ( ! function_exists( 'debug' ) ) {
	function debug() {
		call_user_func_array( 'fw_print', func_get_args() );
	}
}

/**
 * Generate html tag
 *
 * @param string $tag Tag name
 * @param array $attr Tag attributes
 * @param bool|string $end Append closing tag. Also accepts body content
 *
 * @return string The tag's html
 */
function fw_html_tag( $tag, $attr = array(), $end = false ) {
	$html = '<' . $tag . ' ' . fw_attr_to_html( $attr );

	if ( $end === true ) {
		# <script></script>
		$html .= '></' . $tag . '>';
	} else if ( $end === false ) {
		# <br/>
		$html .= '/>';
	} else {
		# <div>content</div>
		$html .= '>' . $end . '</' . $tag . '>';
	}

	return $html;
}

/**
 * Generate attributes string for html tag
 *
 * @param array $attr_array array('href' => '/', 'title' => 'Test')
 *
 * @return string 'href="/" title="Test"'
 */
function fw_attr_to_html( array $attr_array ) {
	$html_attr = '';

	foreach ( $attr_array as $attr_name => $attr_val ) {
		if ( $attr_val === false ) {
			continue;
		}

		$html_attr .= $attr_name . '="' . fw_htmlspecialchars( $attr_val ) . '" ';
	}

	return $html_attr;
}

/**
 * Strip slashes from values, and from keys if magic_quotes_gpc = On
 */
function fw_stripslashes_deep_keys( $value ) {
	static $magic_quotes = null;
	if ( $magic_quotes === null ) {
		$magic_quotes = false; //https://www.php.net/manual/en/function.get-magic-quotes-gpc.php - always returns FALSE as of PHP 5.4.0. false fixes https://github.com/ThemeFuse/Unyson/issues/3915
	}

	if ( is_array( $value ) ) {
		if ( $magic_quotes ) {
			$new_value = array();
			foreach ( $value as $key => $val ) {
				$new_value[ is_string( $key ) ? stripslashes( $key ) : $key ] = fw_stripslashes_deep_keys( $val );
			}
			$value = $new_value;
			unset( $new_value );
		} else {
			$value = array_map( 'fw_stripslashes_deep_keys', $value );
		}
	} elseif ( is_object( $value ) ) {
		$vars = get_object_vars( $value );
		foreach ( $vars as $key => $data ) {
			$value->{$key} = fw_stripslashes_deep_keys( $data );
		}
	} elseif ( is_string( $value ) ) {
		$value = stripslashes( $value );
	}

	return $value;
}

/**
 * Add slashes to values, and to keys if magic_quotes_gpc = On
 */
function fw_addslashes_deep_keys( $value ) {
	static $magic_quotes = null;
	if ( $magic_quotes === null ) {
		$magic_quotes = get_magic_quotes_gpc();
	}

	if ( is_array( $value ) ) {
		if ( $magic_quotes ) {
			$new_value = array();
			foreach ( $value as $key => $value ) {
				$new_value[ is_string( $key ) ? addslashes( $key ) : $key ] = fw_addslashes_deep_keys( $value );
			}
			$value = $new_value;
			unset( $new_value );
		} else {
			$value = array_map( 'fw_addslashes_deep_keys', $value );
		}
	} elseif ( is_object( $value ) ) {
		$vars = get_object_vars( $value );
		foreach ( $vars as $key => $data ) {
			$value->{$key} = fw_addslashes_deep_keys( $data );
		}
	} elseif ( is_string( $value ) ) {
		$value = addslashes( $value );
	}

	return $value;
}

/**
 * Check if current screen pass/match give rules
 *
 * @param array $rules Rules for current screen
 *
 * @return bool
 */
function fw_current_screen_match( array $rules ) {
	$available_options = array(
		'action'      => true,
		'base'        => true,
		'id'          => true,
		'is_network'  => true,
		'is_user'     => true,
		'parent_base' => true,
		'parent_file' => true,
		'post_type'   => true,
		'taxonomy'    => true,
	);

	if ( empty( $rules ) ) {
		return true;
	}

	$rules = array_merge(
		array(
			'exclude' => array(), // array of arrays or array with keys from $available_options
			'only'    => array(), // same as in 'exclude'
		),
		$rules
	);

	if ( empty( $rules['exclude'] ) && empty( $rules['only'] ) ) {
		return true;
	}

	global $current_screen;

	if ( gettype( $current_screen ) != 'object' ) {
		return false;
	}

	// check if current screen passes the "only" rules
	do {
		$only = $rules['only'];

		if ( empty( $only ) ) {
			break;
		}

		if ( ! isset( $only[0] ) ) { // if not array of arrays
			$only = array( $only );
		}

		$found_one = false;
		$counter   = 0;
		foreach ( $only as $rule ) {
			if ( ! count( $rule ) ) {
				continue;
			}

			$match = true;

			foreach ( $rule as $r_key => $r_val ) {
				if ( ! isset( $available_options[ $r_key ] ) ) {
					continue;
				}

				if ( gettype( $r_val ) != 'array' ) {
					$r_val = array( $r_val );
				}

				$counter ++;

				if ( ! in_array( $current_screen->{$r_key}, $r_val ) ) {
					$match = false;
					break;
				}
			}

			if ( $match ) {
				$found_one = true;
				break;
			}
		}

		if ( ! $found_one && $counter ) {
			return false;
		}
	} while ( false );

	// check if current screen passes the "exclude" rules
	do {
		$exclude = $rules['exclude'];

		if ( empty( $exclude ) ) {
			break;
		}

		if ( ! isset( $exclude[0] ) ) { // if not array of arrays
			$exclude = array( $exclude );
		}

		foreach ( $exclude as $rule ) {
			if ( ! count( $rule ) ) {
				continue;
			}

			$match   = true;
			$counter = 0;

			foreach ( $rule as $r_key => $r_val ) {
				if ( ! isset( $available_options[ $r_key ] ) ) {
					continue;
				}

				if ( gettype( $r_val ) != 'array' ) {
					$r_val = array( $r_val );
				}

				$counter ++;

				if ( ! in_array( $current_screen->{$r_key}, $r_val ) ) {
					$match = false;
					break;
				}
			}

			if ( $match && $counter ) {
				return false;
			}
		}
	} while ( false );

	return true;
}

/**
 * Search relative path in child then in parent theme directory and return URI
 *
 * @param  string $rel_path '/some/path_to_dir' or '/some/path_to_file.php'
 *
 * @return string URI
 */
function fw_locate_theme_path_uri( $rel_path ) {
	if ( is_child_theme() && file_exists( get_stylesheet_directory() . $rel_path ) ) {
		return get_stylesheet_directory_uri() . $rel_path;
	} elseif ( file_exists( get_template_directory() . $rel_path ) ) {
		return get_template_directory_uri() . $rel_path;
	} else {
		return 'about:blank#theme-file-not-found:' . $rel_path;
	}
}

/**
 * Search relative path in child then in parent theme directory and return full path
 *
 * @param  string $rel_path '/some/path_to_dir' or '/some/path_to_file.php'
 *
 * @return string URI
 */
function fw_locate_theme_path( $rel_path ) {
	if ( is_child_theme() && file_exists( get_stylesheet_directory() . $rel_path ) ) {
		return get_stylesheet_directory() . $rel_path;
	} elseif ( file_exists( get_template_directory() . $rel_path ) ) {
		return get_template_directory() . $rel_path;
	} else {
		return false;
	}
}

/**
 * There is a theme which does: if (!defined('FW')): function fw_render_view() { ... } endif;
 * It works fine, except in this case
 * https://github.com/ThemeFuse/Unyson/commit/07be8b1f4b50eaf0f1f7e85ea1c6912a0415d241#diff-cf866bf08b8f747e3120221a6b1b07cfR48
 * it throws fatal error because this function here is defined after that
 */
if ( ! function_exists( 'fw_render_view' ) ):
	/**
	 * Safe render a view and return html
	 * In view will be accessible only passed variables
	 * Use this function to not include files directly and to not give access to current context variables (like $this)
	 *
	 * @param string $file_path
	 * @param array $view_variables
	 * @param bool $return In some cases, for memory saving reasons, you can disable the use of output buffering
	 *
	 * @return string HTML
	 */
	function fw_render_view( $file_path, $view_variables = array(), $return = true ) {

		if ( ! is_file( $file_path ) ) {
			return '';
		}

		extract( $view_variables, EXTR_REFS );
		unset( $view_variables );

		if ( $return ) {
			ob_start();
			require $file_path;

			return ob_get_clean();
		} else {
			require $file_path;
		}

		return '';
	}
endif;

/**
 * Safe load variables from an file
 * Use this function to not include files directly and to not give access to current context variables (like $this)
 *
 * @param string $file_path
 * @param array $_extract_variables Extract these from file array('variable_name' => 'default_value')
 * @param array $_set_variables Set these to be available in file (like variables in view)
 *
 * @return array
 */
function fw_get_variables_from_file( $file_path, array $_extract_variables, array $_set_variables = array() ) {
	extract( $_set_variables, EXTR_REFS );
	unset( $_set_variables );

	require $file_path;

	foreach ( $_extract_variables as $variable_name => $default_value ) {
		if ( isset( $$variable_name ) ) {
			$_extract_variables[ $variable_name ] = $$variable_name;
		}
	}

	return $_extract_variables;
}

/**
 * Use this function to not include files directly and to not give access to current context variables (like $this)
 *
 * @param string $file_path
 * @param bool $once
 *
 * @return bool If was included or not
 */
function fw_include_file_isolated( $file_path, $once = false ) {
	if ( file_exists( $file_path ) ) {
		if ( (bool) $once ) {
			include_once $file_path;
		} else {
			include $file_path;
		}

		return true;
	} else {
		return false;
	}
}

/**
 * Extract only input options (without containers)
 *
 * @param array $options
 *
 * @return array {option_id => option}
 */
function fw_extract_only_options( array $options ) {
	$collected = array();

	fw_collect_options( $collected, $options );

	return $collected;
}

/**
 * Collect correct options from the first level of the array and group them
 *
 * @param array $collected Will be filled with found correct options
 * @param array $options
 *
 * @deprecated
 * It is deprecated since 2.4 because container types were added and there can be any type of containers
 * but this function is hardcoded only for tab,box,group.
 * Use fw_collect_options()
 */
function fw_collect_first_level_options( &$collected, &$options ) {
	if ( empty( $options ) ) {
		return;
	}

	if ( empty( $collected ) ) {
		$collected['tabs'] =
		$collected['boxes'] =
		$collected['groups'] =
		$collected['options'] =
		$collected['groups_and_options'] =
		$collected['all'] = array();
	}

	foreach ( $options as $option_id => &$option ) {
		if ( isset( $option['options'] ) ) {
			// this is container for other options

			switch ( $option['type'] ) {
				case 'tab':
					$collected['tabs'][ $option_id ] =& $option;
					break;
				case 'box':
					$collected['boxes'][ $option_id ] =& $option;
					break;
				case 'group':
					$collected['groups'][ $option_id ]             =& $option;
					$collected['groups_and_options'][ $option_id ] =& $option;
					break;
				default:
					trigger_error( 'Invalid option container type: ' . $option['type'], E_USER_WARNING );
					continue 2;
			}

			$collected['all'][ $option['type'] . ':~:' . $option_id ] = array(
				'type'   => $option['type'],
				'id'     => $option_id,
				'option' => &$option,
			);
		} elseif (
			is_int( $option_id )
			&&
			is_array( $option )
			&&
			/**
			 * make sure the array key was generated automatically
			 * and it's not an associative array with numeric keys created like this: $options[1] = array();
			 */
			isset( $options[0] )
		) {
			/**
			 * Array "without key" containing options.
			 *
			 * This happens when options are returned into array from a function:
			 * $options = array(
			 *  'foo' => array('type' => 'text'),
			 *  'bar' => array('type' => 'textarea'),
			 *
			 *  // this is our case
			 *  // go inside this array and extract the options as they are on the same array level
			 *  array(
			 *      'hello' => array('type' => 'text'),
			 *  ),
			 *
			 *  // there can be any nested arrays
			 *  array(
			 *      array(
			 *          array(
			 *              'h1' => array('type' => 'text'),
			 *          ),
			 *      ),
			 *  ),
			 * )
			 */
			fw_collect_first_level_options( $collected, $option );
		} elseif ( isset( $option['type'] ) ) {
			// simple option, last possible level in options array
			$collected['options'][ $option_id ]            =& $option;
			$collected['groups_and_options'][ $option_id ] =& $option;

			$collected['all'][ 'option' . ':~:' . $option_id ] = array(
				'type'   => 'option',
				'id'     => $option_id,
				'option' => &$option,
			);
		} else {
			trigger_error( 'Invalid option: ' . $option_id, E_USER_WARNING );
		}
	}
	unset( $option );
}

/**
 * @param array $result
 * @param array $options
 * @param array $settings
 * @param array $_recursion_data (private) for internal use
 */
function fw_collect_options( &$result, &$options, $settings = array(), $_recursion_data = array() ) {
	static $default_settings = array(
		/**
		 * @type bool Wrap the result/collected options in arrays will useful info
		 *
		 * If true:
		 * $result = array(
		 *   '(container|option):{id}' => array(
		 *      'id' => '{id}',
		 *      'level' => int, // from which nested level this option is
		 *      'group' => 'container|option',
		 *      'option' => array(...),
		 *   )
		 * )
		 *
		 * If false:
		 * $result = array(
		 *   '{id}' => array(...),
		 *   // Warning: There can be options and containers with the same id (array key will be replaced)
		 * )
		 */
		'info_wrapper'          => false,
		/**
		 * @type int Nested options level limit. For e.g. use 1 to collect only first level. 0 is for unlimited.
		 */
		'limit_level'           => 0,
		/**
		 * @type false|array('option-type', ...) Empty array will skip all types
		 */
		'limit_option_types'    => false,
		/**
		 * @type false|array('container-type', ...) Empty array will skip all types
		 */
		'limit_container_types' => array(),
		/**
		 * @type int Limit the number of options that will be collected
		 */
		'limit'                 => 0,
		/**
		 * @type callable Executed on each collected option
		 * @since 2.6.0
		 */
		'callback'              => null,
	);

	static $access_key = null;

	if ( empty( $options ) ) {
		return;
	}

	if ( empty( $_recursion_data ) ) {
		if ( is_null( $access_key ) ) {
			$access_key = new FW_Access_Key( 'fw_collect_options' );
		}

		$settings = array_merge( $default_settings, $settings );

		$_recursion_data = array(
			'level'      => 1,
			'access_key' => $access_key,
			// todo: maybe add 'parent' => array('id' => '{id}', 'type' => 'container|option') ?
		);
	} elseif ( ! (
		isset( $_recursion_data['access_key'] )
		&&
		( $_recursion_data['access_key'] instanceof FW_Access_Key )
		&&
		( $_recursion_data['access_key']->get_key() === 'fw_collect_options' )
	)
	) {
		trigger_error( 'Call not allowed', E_USER_ERROR );
	}

	if (
		$settings['limit_level']
		&&
		$_recursion_data['level'] > $settings['limit_level']
	) {
		return;
	}

	foreach ( $options as $option_id => &$option ) {
		if ( isset( $option['options'] ) ) { // this is a container
			do {
				if (
					is_array( $settings['limit_container_types'] )
					&&
					(
						// Customizer options can contain options with not existing or empty $option['type']
						empty( $option['type'] )
						||
						! in_array( $option['type'], $settings['limit_container_types'] )
					)
				) {
					break;
				}

				if (
					$settings['limit']
					&&
					count( $result ) >= $settings['limit']
				) {
					return;
				}

				if ( $settings['info_wrapper'] ) {
					$result[ 'container:' . $option_id ] = array(
						'group'  => 'container',
						'id'     => $option_id,
						'option' => &$option,
						'level'  => $_recursion_data['level'],
					);
				} else {
					$result[ $option_id ] = &$option;
				}

				if ( $settings['callback'] ) {
					call_user_func_array( $settings['callback'],
						array(
							array(
								'group'  => 'container',
								'id'     => $option_id,
								'option' => &$option,
							)
						) );
				}
			} while ( false );

			fw_collect_options(
				$result,
				$option['options'],
				$settings,
				array_merge( $_recursion_data, array( 'level' => $_recursion_data['level'] + 1 ) )
			);
		} elseif (
			is_int( $option_id )
			&&
			is_array( $option )
			&&
			/**
			 * make sure the array key was generated automatically
			 * and it's not an associative array with numeric keys created like this: $options[1] = array();
			 */
			isset( $options[0] )
		) {
			/**
			 * Array "without key" containing options.
			 *
			 * This happens when options are returned into array from a function:
			 * $options = array(
			 *  'foo' => array('type' => 'text'),
			 *  'bar' => array('type' => 'textarea'),
			 *
			 *  // this is our case
			 *  // go inside this array and extract the options as they are on the same array level
			 *  array(
			 *      'hello' => array('type' => 'text'),
			 *  ),
			 *
			 *  // there can be any nested arrays
			 *  array(
			 *      array(
			 *          array(
			 *              'h1' => array('type' => 'text'),
			 *          ),
			 *      ),
			 *  ),
			 * )
			 */
			fw_collect_options( $result, $option, $settings, $_recursion_data );
		} elseif ( isset( $option['type'] ) ) { // option
			if (
				is_array( $settings['limit_option_types'] )
				&&
				! in_array( $option['type'], $settings['limit_option_types'] )
			) {
				continue;
			}

			if (
				$settings['limit']
				&&
				count( $result ) >= $settings['limit']
			) {
				return;
			}

			if ( $settings['info_wrapper'] ) {
				$result[ 'option:' . $option_id ] = array(
					'group'  => 'option',
					'id'     => $option_id,
					'option' => &$option,
					'level'  => $_recursion_data['level'],
				);
			} else {
				$result[ $option_id ] = &$option;
			}

			if ( $settings['callback'] ) {
				call_user_func_array( $settings['callback'],
					array(
						array(
							'group'  => 'option',
							'id'     => $option_id,
							'option' => &$option,
						)
					) );
			}
		} else {
			trigger_error( 'Invalid option: ' . $option_id, E_USER_WARNING );
		}
	}
}

/**
 * Get correct values from input (POST) for given options
 * This values can be saved in db then replaced with $option['value'] for each option
 *
 * @param array $options
 * @param array $input_array
 *
 * @return array Values
 */
function fw_get_options_values_from_input( array $options, $input_array = null ) {
	if ( ! is_array( $input_array ) ) {
		$input_array = FW_Request::POST( fw()->backend->get_options_name_attr_prefix() );
	}

	$values = array();

	$maybe_new_values = apply_filters(
		'fw:get_options_values_from_input:before',
		null,
		$options, $input_array
	);

	if ($maybe_new_values) {
		return $maybe_new_values;
	}

	foreach ( fw_extract_only_options( $options ) as $id => $option ) {
		$values[ $id ] = fw()->backend->option_type( $option['type'] )->get_value_from_input(
			$option,
			isset( $input_array[ $id ] ) ? $input_array[ $id ] : null
		);

		if ( is_null( $values[ $id ] ) ) {
			// do not save null values
			unset( $values[ $id ] );
		}
	}

	return $values;
}

/**
 * @param $attr_name
 * @param bool $set_mode
 *
 * @return mixed
 */
function fw_html_attr_name_to_array_multi_key( $attr_name, $set_mode = false ) {
	if ( $set_mode ) {
		/**
		 * The key will be used to set value in array
		 * 'hello[world][]' -> 'hello/world/'
		 * $array['hello']['world'][] = $value;
		 */
		$attr_name = str_replace( '[]', '/', $attr_name );
	} else {
		/**
		 * The key will be used to get value from array
		 * 'hello[world][]' -> 'hello/world'
		 * $value = $array['hello']['world'];
		 */
		$attr_name = str_replace( '[]', '', $attr_name );
	}

	$attr_name = str_replace( '][', '/', $attr_name );
	$attr_name = str_replace( '[', '/', $attr_name );
	$attr_name = str_replace( ']', '', $attr_name );

	return $attr_name;
}

/**
 * Used when getting some option value from serialized array saved in a custom place
 * and that option is unreachable for standard WordPress filters by other plugins
 * For e.g. that option cannot be translated by plugins, so we pass its value through this function and do the fixes
 *
 * @param $value
 *
 * @return array
 */
function fw_prepare_option_value( $value ) {
	if ( empty( $value ) ) {
		return $value;
	}

	if ( function_exists( 'qtrans_useCurrentLanguageIfNotFoundUseDefaultLanguage' ) ) {
		$value = qtrans_useCurrentLanguageIfNotFoundUseDefaultLanguage( $value );
	}

	return $value;
}

/**
 * This function is used in 'save_post' action
 *
 * Used to check if current post save is a regular "Save" button press
 * not a revision, auto-save or something else
 *
 * @param $post_id
 *
 * @return bool
 *
 * @deprecated
 * save_post action happens also happens on Preview, Revision, Auto-save Restore, ...
 * the verifications in this function simplifies too much the save process,
 * the developers should study and understand better how it works
 * and handle different save cases in their scripts using wp functions
 */
function fw_is_real_post_save( $post_id ) {
	return ! (
		wp_is_post_revision( $post_id )
		|| wp_is_post_autosave( $post_id )
		|| ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
		|| ( defined( 'DOING_AJAX' ) && DOING_AJAX )
		|| empty( $_POST )
		|| empty( $_POST['post_ID'] )
		|| $_POST['post_ID'] != $post_id
	);
}

/**
 * @return Array with Google fonts
 */
function fw_get_google_fonts() {
	$cache_key = 'fw_google_fonts';

	try {
		return FW_Cache::get( $cache_key );
	} catch ( FW_Cache_Not_Found_Exception $e ) {
		$g_fonts   = json_decode( fw_get_google_fonts_v2(), true );
		$old_fonts = include( dirname( __FILE__ ) . '/fw-google-fonts.json.php' );
		$fonts     = array();

		foreach ( $g_fonts['items'] as $font ) {
			$fonts[ $font['family'] ] = array(
				'family'   => $font['family'],
				'variants' => $font['variants'],
				'position' => isset( $old_fonts[ $font['family'] ] )
					? $old_fonts[ $font['family'] ]['position']
					: 99999
			);
		}

		$fonts = apply_filters( 'fw_google_fonts', $fonts );

		FW_Cache::set( $cache_key, $fonts );

		return $fonts;
	}
}

/**
 * @return string JSON encoded array with Google fonts
 */
function fw_get_google_fonts_v2() {
	$saved_data = get_option( 'fw_google_fonts', false );
	$ttl        = 7 * DAY_IN_SECONDS;

	if (
		false === $saved_data
		||
		( !empty($saved_data) AND $saved_data['last_update'] + $ttl < time() )
	) {
		
		$response = wp_remote_get( apply_filters( 'fw_googleapis_webfonts_url',
			'http://updates.like-themes.com/google-fonts/' ) );
		$body     = wp_remote_retrieve_body( $response );

		if (
			200 === wp_remote_retrieve_response_code( $response )
			&&
			! is_wp_error( $body ) && ! empty( $body )
		) {
			update_option( 'fw_google_fonts',
				array(
					'last_update' => time(),
					'fonts'       => $body
				),
				false );

			return $body;
		} else {

			$old_fonts = include( dirname( __FILE__ ) . '/fw-google-fonts.json.php' );

			if ( empty( $saved_data['fonts'] ) ) {

				$saved_data = [];
				$saved_data['fonts'] = json_encode(array( 'items' => $old_fonts ));
			}

			update_option( 'fw_google_fonts',
				array(
					'last_update' => time() - $ttl + MINUTE_IN_SECONDS,
					'fonts'       => $saved_data['fonts']
				),
				false );
		}

		return $saved_data['fonts'];

	}

	if ( !empty($saved_data) ) {

		return $saved_data['fonts'];
	}
		else {

		return false;
	}

}

/**
 * @return string Current url
 */
function fw_current_url() {
	static $url = null;
	if ( $url === null ) {
		if ( is_multisite() && ! ( defined( 'SUBDOMAIN_INSTALL' ) && SUBDOMAIN_INSTALL ) ) {
			switch_to_blog( 1 );
			$url = get_option( 'home' );
			restore_current_blog();
		} else {
			$url = get_option( 'home' );
		}

		//Remove the "//" before the domain name
		$url = ltrim( fw_get_url_without_scheme( $url ), '/' );

		//Remove the ulr subdirectory in case it has one
		$split = explode( '/', $url );

		//Remove end slash
		$url = rtrim( $split[0], '/' );

		$url .= '/' . ltrim( fw_akg( 'REQUEST_URI', $_SERVER, '' ), '/' );
		$url = set_url_scheme( '//' . $url ); // https fix
	}

	return $url;
}

function fw_is_valid_domain_name( $domain_name ) {
	return ( preg_match( "/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $domain_name ) // valid chars check
	         && preg_match( "/^.{1,253}$/", $domain_name ) // overall length check
	         && preg_match( "/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name ) ); // length of each label
}

/**
 * Use this id do not want to enter every time same last two parameters
 * Info: Cannot use default parameters because in php 5.2 encoding is not UTF-8 by default
 *
 * @param string $string
 *
 * @return string
 */
function fw_htmlspecialchars( $string ) {
	return htmlspecialchars( $string, ENT_QUOTES, 'UTF-8' );
}

/**
 * Check if current user has one capability from the given list
 *
 * @param array $capabilities list of capabilities to check
 * @param mixed $default_value
 *
 * @return string|bool|mixed
 *  Return first capability that user can.
 *  Else, return default value if it is not null, else return first capability from list.
 *  Use default value false to check if user can some of the capabilities
 */
function fw_current_user_can( $capabilities, $default_value = null ) {
	if ( is_user_logged_in() ) {
		foreach ( $capabilities as $capability ) {
			if ( current_user_can( $capability ) ) {
				return $capability;
			}
		}
	}

	return ( $default_value !== null ? fw_call( $default_value ) : array_shift( $capabilities ) );
}

/**
 * Convert number of seconds to 'X {units}'
 *
 * E.g. 123 => '2 minutes'
 * then you can use this string how you want, for e.g. append ' ago' => '2 minutes ago'
 *
 * @param int $seconds
 *
 * @return string
 */
function fw_human_time( $seconds ) {
	static $translations = null;
	if ( $translations === null ) {
		$translations = array(
			'year'  => __( 'year', 'fw' ),
			'years' => __( 'years', 'fw' ),

			'month'  => __( 'month', 'fw' ),
			'months' => __( 'months', 'fw' ),

			'week'  => __( 'week', 'fw' ),
			'weeks' => __( 'weeks', 'fw' ),

			'day'  => __( 'day', 'fw' ),
			'days' => __( 'days', 'fw' ),

			'hour'  => __( 'hour', 'fw' ),
			'hours' => __( 'hours', 'fw' ),

			'minute'  => __( 'minute', 'fw' ),
			'minutes' => __( 'minutes', 'fw' ),

			'second'  => __( 'second', 'fw' ),
			'seconds' => __( 'seconds', 'fw' ),
		);
	}

	$tokens = array(
		31536000 => 'year',
		2592000  => 'month',
		604800   => 'week',
		86400    => 'day',
		3600     => 'hour',
		60       => 'minute',
		1        => 'second'
	);

	foreach ( $tokens as $unit => $translation_key ) {
		if ( $seconds < $unit ) {
			continue;
		}

		$number_of_units = floor( $seconds / $unit );

		return $number_of_units . ' ' . $translations[ $translation_key . ( $number_of_units != 1 ? 's' : '' ) ];
	}
}

/**
 * Convert bytes to human readable format
 *
 * @param integer $bytes Size in bytes to convert
 * @param integer $precision
 *
 * @return string
 * @since 2.4.17
 */
function fw_human_bytes( $bytes, $precision = 2 ) {
	$kilobyte = 1024;
	$megabyte = $kilobyte * 1024;
	$gigabyte = $megabyte * 1024;
	$terabyte = $gigabyte * 1024;

	if ( ( $bytes >= 0 ) && ( $bytes < $kilobyte ) ) {
		return $bytes . ' B';

	} elseif ( ( $bytes >= $kilobyte ) && ( $bytes < $megabyte ) ) {
		return round( $bytes / $kilobyte, $precision ) . ' KB';

	} elseif ( ( $bytes >= $megabyte ) && ( $bytes < $gigabyte ) ) {
		return round( $bytes / $megabyte, $precision ) . ' MB';

	} elseif ( ( $bytes >= $gigabyte ) && ( $bytes < $terabyte ) ) {
		return round( $bytes / $gigabyte, $precision ) . ' GB';

	} elseif ( $bytes >= $terabyte ) {
		return round( $bytes / $terabyte, $precision ) . ' TB';
	} else {
		return $bytes . ' B';
	}
}

function fw_strlen( $string ) {
	if ( function_exists( 'mb_strlen' ) ) {
		return mb_strlen( $string, 'UTF-8' );
	} else {
		return strlen( $string );
	}
}

/**
 * If currently is a Post Edit page display/submit
 * @return bool
 */
function fw_is_post_edit() {
	static $result = null;

	if ( $result === null ) {
		$result = false;

		if ( is_admin() ) {
			if (
				empty( $_POST )
				&&
				isset( $_GET['action'] )
				&&
				$_GET['action'] === 'edit'
				&&
				isset( $_GET['post'] )
			) {
				// Display Edit Post page
				$result = true;
			} elseif (
				isset( $_POST['action'] )
				&&
				$_POST['action'] === 'editpost'
				&&
				isset( $_POST['post_type'] )
				&&
				isset( $_POST['post_ID'] )
				&&
				strpos( wp_get_referer(), 'action=edit' ) !== false
			) {
				// Submit Edit Post page
				$result = true;
			}
		}
	}

	return $result;
}

/**
 * @param string $dirname 'foo-bar'
 *
 * @return string 'Foo_Bar'
 */
function fw_dirname_to_classname( $dirname ) {
	$class_name = explode( '-', $dirname );
	$class_name = array_map( 'ucfirst', $class_name );
	$class_name = implode( '_', $class_name );

	return $class_name;
}

/**
 * This function is a wrapper function that set correct width and height for iframes from wp_oembed_get() function
 *
 * @param $url
 * @param array $args
 *
 * @return bool|string
 */
function fw_oembed_get( $url, $args = array() ) {
	$html = wp_oembed_get( $url, $args );

	if ( ! empty( $args['width'] ) and ! empty( $args['height'] ) and class_exists( 'DOMDocument' ) and ! empty( $html ) ) {
		$dom_element = new DOMDocument();
		@$dom_element->loadHTML( $html );

		if ( $obj = $dom_element->getElementsByTagName( 'iframe' )->item( 0 ) ) {
			$obj->setAttribute( 'width', $args['width'] );
			$obj->setAttribute( 'height', $args['height'] );
			//saveXml instead of SaveHTML for php version compatibility
			$html = $dom_element->saveXML( $obj, LIBXML_NOEMPTYTAG );
		}
	}

	return $html;
}

/**
 * @var $length
 * @return string
 *
 * Reference
 *
 * Strong cryptography in PHP
 * http://www.zimuel.it/en/strong-cryptography-in-php/
 * > Don't use rand() or mt_rand()
 */
function fw_secure_rand( $length ) {
	if ( function_exists( 'openssl_random_pseudo_bytes' ) ) {
		$rnd = openssl_random_pseudo_bytes( $length, $strong );
		if ( $strong ) {
			return $rnd;
		}
	}

	$sha = '';
	$rnd = '';

	if ( file_exists( '/dev/urandom' ) ) {
		$fp = fopen( '/dev/urandom', 'rb' );
		if ( $fp ) {
			if ( function_exists( 'stream_set_read_buffer' ) ) {
				stream_set_read_buffer( $fp, 0 );
			}
			$sha = fread( $fp, $length );
			fclose( $fp );
		}
	}

	for ( $i = 0; $i < $length; $i ++ ) {
		$sha  = hash( 'sha256', $sha . mt_rand() );
		$char = mt_rand( 0, 62 );
		$rnd .= chr( hexdec( $sha[ $char ] . $sha[ $char + 1 ] ) );
	}

	return $rnd;
}

/**
 * Try to make user friendly title from an id
 *
 * @param string $id 'hello-world'
 *
 * @return string 'Hello world'
 */
function fw_id_to_title( $id ) {
	// mb_ucfirst()
	if ( function_exists( 'mb_strtoupper' ) && function_exists( 'mb_substr' ) && function_exists( 'mb_strlen' ) ) {
		$id = mb_strtoupper( mb_substr( $id, 0, 1, 'UTF-8' ), 'UTF-8' ) . mb_substr( $id,
				1,
				mb_strlen( $id, 'UTF-8' ),
				'UTF-8' );
	} else {
		$id = strtoupper( substr( $id, 0, 1 ) ) . substr( $id, 1, strlen( $id ) );
	}

	return str_replace( array( '_', '-' ), ' ', $id );
}

/**
 * Alias
 *
 * @param string $extension_name
 *
 * @return FW_Extension|null
 */
function fw_ext( $extension_name ) {
	return fw()->extensions->get( $extension_name );
}

/*
 * Return URI without scheme
 */
function fw_get_url_without_scheme( $url ) {
	return preg_replace( '/^[^:]+:\/\//', '//', $url );
}

/**
 * Try to find file path by its uri and read the file contents
 *
 * @param string $file_uri
 *
 * @return bool|string false or string - the file contents
 */
function fw_read_file_by_uri( $file_uri ) {
	static $base = null;

	if ( $base === null ) {
		$base                     = array();
		$base['dir']              = WP_CONTENT_DIR;
		$base['uri']              = ltrim( content_url(), '/' );
		$base['uri_prefix_regex'] = '/^' . preg_quote( $base['uri'], '/' ) . '/';
	}

	$file_rel_path = preg_replace( $base['uri_prefix_regex'], '', $file_uri );

	if ( $base['uri'] === $file_rel_path ) {
		// the file is not inside base dir
		return false;
	}

	$file_path = $base['dir'] . '/' . $file_rel_path;

	if ( ! file_exists( $file_path ) ) {
		return false;
	}

	return file_get_contents( $file_path );
}

/**
 * Make stylesheet contents (portable) independent of directory location
 * For e.g. replace relative paths 'url(img/bg.png)' with full paths 'url(http://site.com/assets/img/bg.png)'
 *
 * @param string $href 'http://.../style.css'
 * @param null|string $contents If not specified, will try to read from $href
 *
 * @return bool|string false - on failure; string - stylesheet contents
 */
function fw_make_stylesheet_portable( $href, $contents = null ) {
	if ( is_null( $contents ) ) {
		$contents = fw_read_file_by_uri( $href );

		if ( $contents === false ) {
			return false;
		}
	}

	$dir_uri = dirname( $href );

	/**
	 * Replace relative 'url(img/bg.png)'
	 * with full 'url(http://site.com/assets/img/bg.png)'
	 *
	 * Do not touch if url starts with:
	 * - 'https://'
	 * - 'http://'
	 * - '/' (also matches '//')
	 * - '#' (for css property: "behavior: url(#behaveBinObject)")
	 * - 'data:'
	 */
	$contents = preg_replace(
		'/url\s*\((?!\s*[\'"]?(?:\/|data\:|\#|(?:https?:)?\/\/))\s*([\'"])?/',
		'url($1' . $dir_uri . '/',
		$contents
	);

	return $contents;
}

/**
 * Return all images sizes register by add_image_size() merged with
 * WordPress default image sizes.
 * @link https://codex.wordpress.org/Function_Reference/get_intermediate_image_sizes
 *
 * @param string $size
 *
 * @return array|bool
 */
function fw_get_image_sizes( $size = '' ) {
	global $_wp_additional_image_sizes;

	$sizes                        = array();
	$get_intermediate_image_sizes = get_intermediate_image_sizes();

	// Create the full array with sizes and crop info
	foreach ( $get_intermediate_image_sizes as $_size ) {
		if ( in_array( $_size, array( 'thumbnail', 'medium', 'large' ) ) ) {
			$sizes[ $_size ]['width']  = get_option( $_size . '_size_w' );
			$sizes[ $_size ]['height'] = get_option( $_size . '_size_h' );
			$sizes[ $_size ]['crop']   = (bool) get_option( $_size . '_crop' );
		} elseif ( isset( $_wp_additional_image_sizes[ $_size ] ) ) {
			$sizes[ $_size ] = array(
				'width'  => $_wp_additional_image_sizes[ $_size ]['width'],
				'height' => $_wp_additional_image_sizes[ $_size ]['height'],
				'crop'   => $_wp_additional_image_sizes[ $_size ]['crop']
			);
		}
	}

	// Get only 1 size if found
	if ( $size ) {
		if ( isset( $sizes[ $size ] ) ) {
			return $sizes[ $size ];
		} else {
			return false;
		}
	}

	return $sizes;
}

/**
 * @param string $icon A string that is meant to be an icon (an image, a font icon class, or something else)
 * @param array Additional attributes
 *
 * @return string
 */
function fw_string_to_icon_html( $icon, array $attributes = array() ) {
	if (
		preg_match( '/\.(png|jpg|jpeg|gif|svg|webp)$/', $icon )
		||
		preg_match( '/^data:image\//', $icon )
	) {
		// http://.../image.png
		$tag  = 'img';
		$attr = array(
			'src' => $icon,
			'alt' => 'icon',
		);
	} elseif ( preg_match( '/^[a-zA-Z0-9\-_ ]+$/', $icon ) ) {
		// 'font-icon font-icon-class'
		$tag  = 'span';
		$attr = array(
			'class' => trim( $icon ),
		);
	} else {
		// can't detect. maybe it's raw html '<span ...'
		return $icon;
	}

	foreach ( $attributes as $attr_name => $attr_val ) {
		if ( isset( $attr[ $attr_name ] ) ) {
			if ( $attr_name === 'class' ) {
				$attr[ $attr_name ] .= ' ' . $attr_val;
			} else {
				// ignore. do not overwrite already set attributes
			}
		} else {
			$attr[ $attr_name ] = (string) $attr_val;
		}
	}

	return fw_html_tag( $tag, $attr );
}

/**
 * @return string|null
 * @since 2.4.10
 */
function fw_get_json_last_error_message() {
	switch ( function_exists( 'json_last_error' ) ? json_last_error() : - 1 ) {
		case JSON_ERROR_NONE:
			return null; // __('No errors', 'fw');
			break;
		case JSON_ERROR_DEPTH:
			return __( 'Maximum stack depth exceeded', 'fw' );
			break;
		case JSON_ERROR_STATE_MISMATCH:
			return __( 'Underflow or the modes mismatch', 'fw' );
			break;
		case JSON_ERROR_CTRL_CHAR:
			return __( 'Unexpected control character found', 'fw' );
			break;
		case JSON_ERROR_SYNTAX:
			return __( 'Syntax error, malformed JSON', 'fw' );
			break;
		case JSON_ERROR_UTF8:
			return __( 'Malformed UTF-8 characters, possibly incorrectly encoded', 'fw' );
			break;
		default:
			return __( 'Unknown error', 'fw' );
			break;
	}
}

/**
 * Return mime_types by file extension ex : input : array( 'png', 'jpg', 'jpeg' ) => output : array( 'image/jpeg' ).
 *
 * @param array $type
 *
 * @return array
 */
function fw_get_mime_type_by_ext( $type = array() ) {
	$result = array();

	foreach ( wp_get_mime_types() as $key => $mime_type ) {
		$types = explode( '|', $key );
		foreach ( $type as $item ) {
			if ( in_array( $item, $types ) && ! in_array( $mime_type, $result ) ) {
				$result[] = $mime_type;
			}
		}
	}

	return $result;
}

/**
 * Return types from file extensions ex : input array( 'png', 'jpg', 'zip' ) => output : array( 'image', 'archive' ).
 *
 * @see wp_ext2type() function.
 *
 * @param array $ext_array
 *
 * @return array
 */
function fw_multi_ext2type( $ext_array = array() ) {
	$result = array();

	foreach ( $ext_array as $ext ) {
		if ( ! in_array( $type = wp_ext2type( $ext ), $result ) ) {
			$result[] = $type;
		}
	}

	return $result;
}

if ( ! function_exists( 'fw_resize' ) ) {
	function fw_resize( $url, $width = false, $height = false, $crop = false ) {
		$fw_resize = FW_Resize::getInstance();
		$response  = $fw_resize->process( $url, $width, $height, $crop );

		return ( ! is_wp_error( $response ) && ! empty( $response['src'] ) ) ? $response['src'] : $url;
	}
}

/**
 * fw_get_path_url( dirname(__FILE__) .'/test.css' ) --> http://site.url/path/to/test.css
 *
 * @param string $path
 *
 * @return string|null
 * @since 2.6.11
 */
function fw_get_path_url( $path ) {
	try {
		$paths_to_urls = FW_Cache::get( $cache_key = 'fw:paths_to_urls' );
	} catch ( FW_Cache_Not_Found_Exception $e ) {
		$wp_upload_dir = wp_upload_dir();

		$paths_to_urls = array(
			fw_fix_path( WP_PLUGIN_DIR )             => plugins_url(),
			fw_fix_path( get_theme_root() )          => get_theme_root_uri(),
			fw_fix_path( $wp_upload_dir['basedir'] ) => $wp_upload_dir['baseurl'],
		);

		if ( is_multisite() && WPMU_PLUGIN_DIR ) {
			$paths_to_urls[ fw_fix_path( WPMU_PLUGIN_DIR ) ] = WPMU_PLUGIN_URL;
		}

		FW_Cache::set( $cache_key, $paths_to_urls );
	}

	$path = fw_fix_path( $path );

	foreach ( $paths_to_urls as $_path => $_url ) {
		if ( preg_match( $regex = '/^' . preg_quote( $_path, '/' ) . '($|\/)/', $path ) ) {
			return $_url . '/' . preg_replace( $regex, '', $path );
		}
	}

	return null;
}

/**
 * @param string|array $callback Callback function
 * @param array $args Callback arguments
 * @param bool $cache Whenever you want to cache the function value after it's first call or not
 * Recommend when the function call may require many resources or time (database requests) , or the value is small
 * Not recommended using on very large values
 *
 * @return FW_Callback
 *
 * @since 2.6.14
 */
function fw_callback( $callback, array $args = array(), $cache = true ) {
	return new FW_Callback( $callback, $args, $cache );
}

/**
 * In the value is instance of FW_Callback class then it is executed and returns the callback value
 * In other case function returns the provided value
 *
 * @param mixed|FW_Callback $value
 *
 * @return mixed
 *
 * @since 2.6.14
 */
function fw_call( $value ) {
	if ( ! fw_is_callback( $value ) ) {
		return $value;
	}

	return ( is_object( $value ) && get_class( $value ) == 'Closure' )
		? $value()
		: $value->execute();
}

/**
 * Check is the current value is instance of FW_Callback class
 *
 * @param mixed $value
 *
 * @return bool
 */
function fw_is_callback( $value ) {
	return $value instanceof FW_Callback || ( is_object( $value ) && get_class( $value ) == 'Closure' );
}

/**
 * Check for command line interface
 *
 * @return bool
 * @since 2.6.16
 */
function fw_is_cli() {
	return ( php_sapi_name() === 'cli' ) && defined( 'WP_CLI' );
}
