<?php
/**
 * The utility class.
 *
 * @since      	1.1.5
 * @since  		1.5 Moved into /inc
 */
namespace LiteSpeed;

defined( 'WPINC' ) || exit;

class Utility extends Instance {
	protected static $_instance;
	private static $_internal_domains;

	/**
	 * Validate regex
	 *
	 * @since 1.0.9
	 * @since  3.0 Moved here from admin-settings.cls
	 * @access public
	 * @return bool True for valid rules, false otherwise.
	 */
	public static function syntax_checker( $rules ) {
		$success = true;

		set_error_handler( 'litespeed_exception_handler' );

		try {
			preg_match( self::arr2regex( $rules ), null );
		}
		catch ( \ErrorException $e ) {
			$success = false;
		}

		restore_error_handler();

		return $success;
	}

	/**
	 * Combine regex array to regex rule
	 *
	 * @since  3.0
	 */
	public static function arr2regex( $arr, $drop_delimiter = false ) {
		$arr = self::sanitize_lines( $arr );

		$new_arr = array();
		foreach ( $arr as $v ) {
			$new_arr[] = preg_quote( $v, '#' );
		}

		$regex = implode( '|', $new_arr );
		$regex = str_replace( ' ', '\\ ', $regex );

		if ( $drop_delimiter ) {
			return $regex;
		}

		return '#' . $regex . '#';
	}

	/**
	 * Replace wildcard to regex
	 *
	 * @since  3.2.2
	 */
	public static function wildcard2regex( $string ) {
		if ( is_array( $string ) ) {
			return array_map( __CLASS__ . '::wildcard2regex', $string );
		}

		if ( strpos( $string, '*' ) !== false ) {
			$string = preg_quote( $string, '#' );
			$string = str_replace( '\*', '.*', $string );
		}

		return $string;
	}

	/**
	 * Check if an URL or current page is REST req or not
	 *
	 * @since  2.9.3
	 * @deprecated 2.9.4 Moved to REST class
	 * @access public
	 */
	public static function is_rest( $url = false ) {
		return false;
	}

	/**
	 * Get current page type
	 *
	 * @since  2.9
	 */
	public static function page_type() {
		global $wp_query;
		$page_type = 'default';

		if ( $wp_query->is_page ) {
			$page_type = is_front_page() ? 'front' : 'page';
		}
		elseif ( $wp_query->is_home ) {
			$page_type = 'home';
		}
		elseif ( $wp_query->is_single ) {
			// $page_type = $wp_query->is_attachment ? 'attachment' : 'single';
			$page_type = get_post_type();
		}
		elseif ( $wp_query->is_category ) {
			$page_type = 'category';
		}
		elseif ( $wp_query->is_tag ) {
			$page_type = 'tag';
		}
		elseif ( $wp_query->is_tax ) {
			$page_type = 'tax';
			// $page_type = get_queried_object()->taxonomy;
		}
		elseif ( $wp_query->is_archive ) {
			if ( $wp_query->is_day ) {
				$page_type = 'day';
			}
			elseif ( $wp_query->is_month ) {
				$page_type = 'month';
			}
			elseif ( $wp_query->is_year ) {
				$page_type = 'year';
			}
			elseif ( $wp_query->is_author ) {
				$page_type = 'author';
			}
			else {
				$page_type = 'archive';
			}
		}
		elseif ( $wp_query->is_search ) {
			$page_type = 'search';
		}
		elseif ( $wp_query->is_404 ) {
			$page_type = '404';
		}

		return $page_type;

		// if ( is_404() ) {
		// 	$page_type = '404';
		// }
		// elseif ( is_singular() ) {
		// 	$page_type = get_post_type();
		// }
		// elseif ( is_home() && get_option( 'show_on_front' ) == 'page' ) {
		// 	$page_type = 'home';
		// }
		// elseif ( is_front_page() ) {
		// 	$page_type = 'front';
		// }
		// elseif ( is_tax() ) {
		// 	$page_type = get_queried_object()->taxonomy;
		// }
		// elseif ( is_category() ) {
		// 	$page_type = 'category';
		// }
		// elseif ( is_tag() ) {
		// 	$page_type = 'tag';
		// }

		// return $page_type;
	}

	/**
	 * Get ping speed
	 *
	 * @since  2.9
	 */
	public static function ping( $domain ) {
		if ( strpos( $domain, ':' ) ) {
			$domain = parse_url( $domain, PHP_URL_HOST );
		}
		$starttime	= microtime( true );
		$file		= fsockopen( $domain, 443, $errno, $errstr, 10 );
		$stoptime	= microtime( true );
		$status		= 0;

		if ( ! $file ) $status = 99999;// Site is down
		else {
			fclose( $file );
			$status = ( $stoptime - $starttime ) * 1000;
			$status = floor( $status );
		}

		Debug2::debug( "[Util] ping [Domain] $domain \t[Speed] $status" );

		return $status;
	}

	/**
	 * Set seconds/timestamp to readable format
	 *
	 * @since  1.6.5
	 * @access public
	 */
	public static function readable_time( $seconds_or_timestamp, $timeout = 3600, $forword = false ) {

		if ( strlen( $seconds_or_timestamp ) == 10 ) {
			$seconds = time() - $seconds_or_timestamp;
			if ( $seconds > $timeout ) {
				return date( 'm/d/Y H:i:s', $seconds_or_timestamp + LITESPEED_TIME_OFFSET );
			}
		}
		else {
			$seconds = $seconds_or_timestamp;
		}

		$res = '';
		if ( $seconds > 86400 ) {
			$num = floor( $seconds / 86400 );
			$res .= $num . 'd';
			$seconds %= 86400;
		}

		if ( $seconds > 3600 ) {
			if ( $res ) {
				$res .= ', ';
			}
			$num = floor( $seconds / 3600 );
			$res .= $num . 'h';
			$seconds %= 3600;
		}

		if ( $seconds > 60 ) {
			if ( $res ) {
				$res .= ', ';
			}
			$num = floor( $seconds / 60 );
			$res .= $num . 'm';
			$seconds %= 60;
		}

		if ( $seconds > 0 ) {
			if ( $res ) {
				$res .= ' ';
			}
			$res .= $seconds . 's';
		}

		if ( ! $res ) {
			return $forword ? __( 'right now', 'litespeed-cache' ) : __( 'just now', 'litespeed-cache' );
		}

		$res = $forword ? $res : sprintf( __( ' %s ago', 'litespeed-cache' ), $res );

		return $res;
	}


	/**
	 * Convert array to string
	 *
	 * @since  1.6
	 * @access public
	 */
	public static function arr2str( $arr ) {
		if ( ! is_array( $arr ) ) {
			return $arr;
		}

		return base64_encode( json_encode( $arr ) );
	}

	/**
	 * Get human readable size
	 *
	 * @since  1.6
	 * @access public
	 */
	public static function real_size( $filesize, $is_1000 = false ) {
		$unit = $is_1000 ? 1000 : 1024;

		if ( $filesize >= pow( $unit, 3 ) ) {
			$filesize = round( $filesize / pow( $unit, 3 ) * 100 ) / 100 . 'G';
		}
		elseif ( $filesize >= pow( $unit, 2 ) ) {
			$filesize = round( $filesize / pow( $unit, 2 ) * 100 ) / 100 . 'M';
		}
		elseif ( $filesize >= $unit ) {
			$filesize = round( $filesize / $unit * 100 ) / 100 . 'K';
		}
		else {
			$filesize = $filesize . 'B';
		}
		return $filesize;
	}

	/**
	 * Parse attributes from string
	 *
	 * @since  1.2.2
	 * @since  1.4 Moved from optimize to utility
	 * @access private
	 * @param  string $str
	 * @return array  All the attributes
	 */
	public static function parse_attr( $str ) {
		$attrs = array();
		preg_match_all( '#([\w-]+)=["\']([^"\']*)["\']#isU', $str, $matches, PREG_SET_ORDER );
		foreach ( $matches as $match ) {
			$attrs[ $match[ 1 ] ] = trim( $match[ 2 ] );
		}
		return $attrs;
	}

	/**
	 * Check if an array has a string
	 *
	 * Support $ exact match
	 *
	 * @since 1.3
	 * @access private
	 * @param string $needle The string to search with
	 * @param array $haystack
	 * @return bool|string False if not found, otherwise return the matched string in haystack.
	 */
	public static function str_hit_array( $needle, $haystack, $has_ttl = false ) {
		/**
		 * Safety check to avoid PHP warning
		 * @see  https://github.com/litespeedtech/lscache_wp/pull/131/commits/45fc03af308c7d6b5583d1664fad68f75fb6d017
		 */
		if ( ! is_array( $haystack ) ) {
			Debug2::debug( "[Util] ❌ bad param in str_hit_array()!" );

			return false;
		}

		$hit = false;
		$this_ttl = 0;
		foreach( $haystack as $item ) {
			if ( ! $item ) {
				continue;
			}

			if ( $has_ttl ) {
				$this_ttl = 0;
				$item = explode( ' ', $item );
				if ( ! empty( $item[ 1 ] ) ) {
					$this_ttl = $item[ 1 ];
				}
				$item = $item[ 0 ];
			}

			if ( substr( $item, -1 ) === '$' ) {
				// do exact match
				if ( substr( $item, 0, -1 ) === $needle ) {
					$hit = $item;
					break;
				}
			}
			elseif ( substr( $item, 0, 1 ) === '^' ) {
				// match beginning
				if ( substr( $item, 1 ) === substr( $needle, 0, strlen( $item ) - 1 ) ) {
					$hit = $item;
					break;
				}
			}
			else {
				if ( strpos( $needle, $item ) !== false ) {
					$hit = $item;
					break;
				}
			}
		}

		if ( $hit ) {
			if ( $has_ttl ) {
				return array( $hit, $this_ttl );
			}

			return $hit;
		}

		return false;
	}

	/**
	 * Improve compatibility to PHP old versions
	 *
	 * @since  1.2.2
	 *
	 */
	public static function compatibility() {
		require_once LSCWP_DIR . 'lib/php-compatibility.func.php';
	}

	/**
	 * Convert URI to URL
	 *
	 * @since  1.3
	 * @access public
	 * @param  string $uri `xx/xx.html` or `/subfolder/xx/xx.html`
	 * @return  string http://www.example.com/subfolder/xx/xx.html
	 */
	public static function uri2url( $uri ) {
		if ( substr( $uri, 0, 1 ) === '/' ) {
			self::domain_const();
			$url = LSCWP_DOMAIN . $uri;
		}
		else {
			$url = home_url( '/' ) . $uri;
		}

		return $url;
	}

	/**
	 * Convert URL to URI
	 *
	 * @since  1.2.2
	 * @since  1.6.2.1 Added 2nd param keep_qs
	 * @access public
	 */
	public static function url2uri( $url, $keep_qs = false ) {
		$url = trim( $url );
		$uri = @parse_url( $url, PHP_URL_PATH );
		$qs = @parse_url( $url, PHP_URL_QUERY );

		if ( ! $keep_qs || ! $qs ) {
			return $uri;
		}

		return $uri . '?' . $qs;
	}

	/**
	 * Get attachment relative path to upload folder
	 *
	 * @since 3.0
	 * @access public
	 * @param  string 	`https://aa.com/bbb/wp-content/upload/2018/08/test.jpg` or `/bbb/wp-content/upload/2018/08/test.jpg`
	 * @return string 	`2018/08/test.jpg`
	 */
	public static function att_short_path( $url ) {
		if ( ! defined( 'LITESPEED_UPLOAD_PATH' ) ) {
			$_wp_upload_dir = wp_upload_dir();

			$upload_path = self::url2uri( $_wp_upload_dir[ 'baseurl' ] );

			define( 'LITESPEED_UPLOAD_PATH', $upload_path );
		}

		$local_file = self::url2uri( $url );

		$short_path = substr( $local_file, strlen( LITESPEED_UPLOAD_PATH ) + 1 );

		return $short_path;
	}

	/**
	 * Make URL to be relative
	 *
	 * NOTE: for subfolder home_url, will keep subfolder part (strip nothing but scheme and host)
	 *
	 * @param  string $url
	 * @return string      Relative URL, start with /
	 */
	public static function make_relative( $url ) {
		// replace home_url if the url is full url
		self::domain_const();
		if ( strpos( $url, LSCWP_DOMAIN ) === 0 ) {
			$url = substr( $url, strlen( LSCWP_DOMAIN ) );
		}
		return trim( $url );
	}

	/**
	 * Convert URL to domain only
	 *
	 * @since  1.7.1
	 */
	public static function parse_domain( $url ) {
		$url = @parse_url( $url );
		if ( empty( $url[ 'host' ] ) ) {
			return '';
		}

		if ( ! empty( $url[ 'scheme' ] ) ) {
			return $url[ 'scheme' ] . '://' . $url[ 'host' ];
		}

		return '//' . $url[ 'host' ];
	}

	/**
	 * Drop protocol `https:` from https://example.com
	 *
	 * @since  3.3
	 */
	public static function noprotocol( $url ) {
		$tmp = parse_url( trim( $url ) );
		if ( ! empty( $tmp[ 'scheme' ] ) ) {
			$url = str_replace( $tmp[ 'scheme' ] . ':', '', $url );
		}

		return $url;
	}

	/**
	 * Generate domain const
	 *
	 * This will generate http://www.example.com even there is a subfolder in home_url setting
	 *
	 * Conf LSCWP_DOMAIN has NO trailing /
	 *
	 * @since  1.3
	 * @access public
	 */
	public static function domain_const() {
		if ( defined( 'LSCWP_DOMAIN' ) ) {
			return;
		}

		self::compatibility();
		$domain = http_build_url( get_home_url(), array(), HTTP_URL_STRIP_ALL );

		define( 'LSCWP_DOMAIN', $domain );
	}

	/**
	 * Array map one textarea to sanitize the url
	 *
	 * @since  1.3
	 * @access public
	 * @param  string $content
	 * @param  bool $type String handler type
	 * @return string
	 */
	public static function sanitize_lines( $arr, $type = null ) {
		if ( ! $arr ) {
			if ( $type === 'string' ) {
				return '';
			}
			return array();
		}

		if ( ! is_array( $arr ) ) {
			$arr = explode( "\n", $arr );
		}

		$arr = array_map( 'trim', $arr );
		$changed = false;
		if ( $type === 'uri' ) {
			$arr = array_map( __CLASS__ . '::url2uri', $arr );
			$changed = true;
		}
		if ( $type === 'relative' ) {
			$arr = array_map( __CLASS__ . '::make_relative', $arr );// Remove domain
			$changed = true;
		}
		if ( $type === 'domain' ) {
			$arr = array_map( __CLASS__ . '::parse_domain', $arr );// Only keep domain
			$changed = true;
		}

		if ( $type === 'noprotocol' ) {
			$arr = array_map( __CLASS__ . '::noprotocol', $arr ); // Drop protocol, `https://example.com` -> `//example.com`
			$changed = true;
		}

		if ( $changed ) {
			$arr = array_map( 'trim', $arr );
		}
		$arr = array_unique( $arr );
		$arr = array_filter( $arr );

		if ( $type === 'string' ) {
			return implode( "\n", $arr );
		}

		return $arr;
	}

	/**
	 * Builds an url with an action and a nonce.
	 *
	 * Assumes user capabilities are already checked.
	 *
	 * @since  1.6 Changed order of 2nd&3rd param, changed 3rd param `append_str` to 2nd `type`
	 * @access public
	 * @return string The built url.
	 */
	public static function build_url( $action, $type = false, $is_ajax = false, $page = null, $append_arr = array() ) {
		$prefix = '?';

		if ( $page === '_ori' ) {
			$page = true;
			$append_arr[ '_litespeed_ori' ] = 1;
		}

		if ( ! $is_ajax ) {
			if ( $page ) {
				// If use admin url
				if ( $page === true ) {
					$page = 'admin.php';
				}
				else {
					if ( strpos( $page, '?' ) !== false ) {
						$prefix = '&';
					}
				}
				$combined = $page . $prefix . Router::ACTION . '=' . $action;
			}
			else {
				// Current page rebuild URL
				$params = $_GET;

				if ( ! empty( $params ) ) {
					if ( isset( $params[ Router::ACTION ] ) ) {
						unset( $params[ Router::ACTION ] );
					}
					if ( isset( $params[ '_wpnonce' ] ) ) {
						unset( $params[ '_wpnonce' ] );
					}
					if ( ! empty( $params ) ) {
						$prefix .= http_build_query( $params ) . '&';
					}
				}
				global $pagenow;
				$combined = $pagenow . $prefix . Router::ACTION . '=' . $action;
			}
		}
		else {
			$combined = 'admin-ajax.php?action=litespeed_ajax&' . Router::ACTION . '=' . $action;
		}

		if ( is_network_admin() ) {
			$prenonce = network_admin_url( $combined );
		}
		else {
			$prenonce = admin_url( $combined );
		}
		$url = wp_nonce_url( $prenonce, $action, Router::NONCE );

		if ( $type ) {
			// Remove potential param `type` from url
			$url = parse_url( htmlspecialchars_decode( $url ) );
			parse_str( $url[ 'query' ], $query );

			$built_arr = array_merge( $query, array( Router::TYPE => $type ) );
			if ( $append_arr ) {
				$built_arr = array_merge( $built_arr, $append_arr );
			}
			$url[ 'query' ] = http_build_query( $built_arr );
			self::compatibility();
			$url = http_build_url( $url );
			$url = htmlspecialchars( $url, ENT_QUOTES, 'UTF-8' );
		}

		return $url;
	}

	/**
	 * Check if the host is the internal host
	 *
	 * @since  1.2.3
	 *
	 */
	public static function internal( $host ) {
		if ( ! defined( 'LITESPEED_FRONTEND_HOST' ) ) {
			if ( defined( 'WP_HOME' ) ) {
				$home_host = WP_HOME;// Also think of `WP_SITEURL`
			}
			else {
				$home_host = get_option( 'home' );
			}
			define( 'LITESPEED_FRONTEND_HOST', parse_url( $home_host, PHP_URL_HOST ) );
		}

		if ( $host === LITESPEED_FRONTEND_HOST ) {
			return true;
		}

		/**
		 * Filter for multiple domains
		 * @since 2.9.4
		 */
		if ( ! isset( self::$_internal_domains ) ) {
			self::$_internal_domains = apply_filters( 'litespeed_internal_domains', array() );
		}

		if ( self::$_internal_domains ) {
			return in_array( $host, self::$_internal_domains );
		}

		return false;
	}

	/**
	 * Check if an URL is a internal existing file
	 *
	 * @since  1.2.2
	 * @since  1.6.2 Moved here from optm.cls due to usage of media.cls
	 * @access public
	 * @return string|bool The real path of file OR false
	 */
	public static function is_internal_file( $url, $addition_postfix = false ) {
		$url_parsed = parse_url( $url );
		if ( isset( $url_parsed[ 'host' ] ) && ! self::internal( $url_parsed[ 'host' ] ) ) {
			// Check if is cdn path
			// Do this to avoid user hardcoded src in tpl
			if ( ! CDN::internal( $url_parsed[ 'host' ] ) ) {
				Debug2::debug2( '[Util] external' );
				return false;
			}
		}

		if ( empty( $url_parsed[ 'path' ] ) ) {
			return false;
		}

		// Need to replace child blog path for assets, ref: .htaccess
		if ( is_multisite() && defined( 'PATH_CURRENT_SITE' ) ) {
			$pattern = '#^' . PATH_CURRENT_SITE . '([_0-9a-zA-Z-]+/)(wp-(content|admin|includes))#U';
			$replacement = PATH_CURRENT_SITE . '$2';
			$url_parsed[ 'path' ] = preg_replace( $pattern, $replacement, $url_parsed[ 'path' ] );
			// $current_blog = (int) get_current_blog_id();
			// $main_blog_id = (int) get_network()->site_id;
			// if ( $current_blog === $main_blog_id ) {
			// 	define( 'LITESPEED_IS_MAIN_BLOG', true );
			// }
			// else {
			// 	define( 'LITESPEED_IS_MAIN_BLOG', false );
			// }
		}

		// Parse file path
		/**
		 * Trying to fix pure /.htaccess rewrite to /wordpress case
		 *
		 * Add `define( 'LITESPEED_WP_REALPATH', '/wordpress' );` in wp-config.php in this case
		 *
		 * @internal #611001 - Combine & Minify not working?
		 * @since  1.6.3
		 */
		if ( substr( $url_parsed[ 'path' ], 0, 1 ) === '/' ) {
			if ( defined( 'LITESPEED_WP_REALPATH' ) ) {
				$file_path_ori = $_SERVER[ 'DOCUMENT_ROOT' ] . LITESPEED_WP_REALPATH . $url_parsed[ 'path' ];
			}
			else {
				$file_path_ori = $_SERVER[ 'DOCUMENT_ROOT' ] . $url_parsed[ 'path' ];
			}
		}
		else {
			$file_path_ori = Router::frontend_path() . '/' . $url_parsed[ 'path' ];
		}

		/**
		 * Added new file postfix to be check if passed in
		 * @since 2.2.4
		 */
		if ( $addition_postfix ) {
			$file_path_ori .= '.' . $addition_postfix;
		}

		/**
		 * Added this filter for those plugins which overwrite the filepath
		 * @see #101091 plugin `Hide My WordPress`
		 * @since 2.2.3
		 */
		$file_path_ori = apply_filters( 'litespeed_realpath', $file_path_ori );

		$file_path = realpath( $file_path_ori );
		if ( ! is_file( $file_path ) ) {
			Debug2::debug2( '[Util] file not exist: ' . $file_path_ori );
			return false;
		}

		return array( $file_path, filesize( $file_path ) );
	}

	/**
	 * Safely parse URL for v5.3 compatibility
	 *
	 * @since  3.4.3
	 */
	public static function parse_url_safe( $url, $component = -1 ) {
		if ( substr( $url, 0, 2 ) == '//' ) {
			$url = 'https:' . $url;
		}

		return parse_url( $url, $component );
	}

	/**
	 * Replace url in srcset to new value
	 *
	 * @since  2.2.3
	 */
	public static function srcset_replace( $content, $callback ) {
		preg_match_all( '# srcset=([\'"])(.+)\g{1}#iU', $content, $matches );
		$srcset_ori = array();
		$srcset_final = array();
		foreach ( $matches[ 2 ] as $k => $urls_ori ) {

			$urls_final = explode( ',', $urls_ori );

			$changed = false;

			foreach ( $urls_final as $k2 => $url_info ) {
				$url_info_arr = explode( ' ', trim( $url_info ) );

				if ( ! $url2 = call_user_func( $callback, $url_info_arr[ 0 ] ) ) {
					continue;
				}

				$changed = true;

				$urls_final[ $k2 ] = str_replace( $url_info_arr[ 0 ], $url2, $url_info );

				Debug2::debug2( '[Util] - srcset replaced to ' . $url2 . ( ! empty( $url_info_arr[ 1 ] ) ? ' ' . $url_info_arr[ 1 ] : '' ) );
			}

			if ( ! $changed ) {
				continue;
			}

			$urls_final = implode( ',', $urls_final );

			$srcset_ori[] = $matches[ 0 ][ $k ];

			$srcset_final[] = str_replace( $urls_ori, $urls_final, $matches[ 0 ][ $k ] );
		}

		if ( $srcset_ori ) {
			$content = str_replace( $srcset_ori, $srcset_final, $content );
			Debug2::debug2( '[Util] - srcset replaced' );
		}

		return $content;

	}

	/**
	 * Generate pagination
	 *
	 * @since 3.0
	 * @access public
	 */
	public static function pagination( $total, $limit, $return_offset = false ) {
		$pagenum = isset( $_GET[ 'pagenum' ] ) ? absint( $_GET[ 'pagenum' ] ) : 1;

		$offset = ( $pagenum - 1 ) * $limit;
		$num_of_pages = ceil( $total / $limit );

		if ( $offset > $total ) {
			$offset = $total - $limit;
		}

		if ( $offset < 0 ) {
			$offset = 0;
		}

		if ( $return_offset ) {
			return $offset;
		}

		$page_links = paginate_links( array(
			'base' => add_query_arg( 'pagenum', '%#%' ),
			'format' => '',
			'prev_text' => __( '&laquo;', 'text-domain' ),
			'next_text' => __( '&raquo;', 'text-domain' ),
			'total' => $num_of_pages,
			'current' => $pagenum,
		) );

		return '<div class="tablenav"><div class="tablenav-pages" style="margin: 1em 0">' . $page_links . '</div></div>';
	}

	/**
	 * Generate placeholder for an array to query
	 *
	 * @since 2.0
	 * @access public
	 */
	public static function chunk_placeholder( $data, $fields ) {
		$division = substr_count( $fields, ',' ) + 1;

		$q = implode( ',', array_map(
			function( $el ) { return '(' . implode( ',', $el ) . ')'; },
			array_chunk( array_fill( 0, count( $data ), '%s' ), $division )
		) );

		return $q;
	}

}
