<?php
/**
 * FontFace class file
 */

namespace Automattic\WooCommerce\Internal\Font;

// IMPORTANT: We have to switch to the WordPress API to create the FontFace post type when they will be implemented: https://github.com/WordPress/gutenberg/issues/58670!

/**
 * Helper class for font face related functionality.
 *
 * @internal Just for internal use.
 */
class FontFace {

	const POST_TYPE = 'wp_font_face';

	/**
	 * Gets the installed font face by slug.
	 *
	 * @param string $slug The font face slug.
	 * @return \WP_Post|null The font face post or null if not found.
	 */
	public static function get_installed_font_faces_by_slug( $slug ) {
		$query = new \WP_Query(
			array(
				'post_type'              => self::POST_TYPE,
				'update_post_meta_cache' => false,
				'update_post_term_cache' => false,
				'name'                   => $slug,
			)
		);

		if ( ! empty( $query->get_posts() ) ) {
			return $query->get_posts()[0];
		}
		return null;
	}

	/**
	 * Sanitizes a single src value for a font face.
	 *
	 * Copied from Gutenberg: https://github.com/WordPress/gutenberg/blob/8d94c3bd7af977d998466b56bd773f9b19de8d03/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php/#L837-L840
	 *
	 * @param string $value Font face src that is a URL or the key for a $_FILES array item.
	 *
	 * @return string Sanitized value.
	 */
	private static function sanitize_src( $value ) {
		$value = ltrim( $value );
		return false === wp_http_validate_url( $value ) ? (string) $value : esc_url_raw( $value );
	}

	/**
	 * Handles file upload error.
	 *
	 * Copied from Gutenberg: https://github.com/WordPress/gutenberg/blob/b283c47dba96d74dd7589a823d8ab84c9e5a4765/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php/#L859-L883
	 *
	 * @param array  $file    File upload data.
	 * @param string $message Error message from wp_handle_upload().
	 * @return WP_Error WP_Error object.
	 */
	private static function handle_font_file_upload_error( $file, $message ) {
		$status = 500;
		$code   = 'rest_font_upload_unknown_error';

		if ( __( 'Sorry, you are not allowed to upload this file type.', 'woocommerce' ) === $message ) {
			$status = 400;
			$code   = 'rest_font_upload_invalid_file_type';
		}

		return new \WP_Error( $code, $message, array( 'status' => $status ) );
	}

	/**
	 * Handles the upload of a font file using wp_handle_upload().
	 *
	 * Copied from Gutenberg: https://github.com/WordPress/gutenberg/blob/f4889bf58ddeb8470c8d2a765f1b57229c515eda/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php/#L859-L896
	 *
	 * @param array $file Single file item from $_FILES.
	 * @return array Array containing uploaded file attributes on success, or error on failure.
	 */
	private static function handle_font_file_upload( $file ) {
		add_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) );

		/*
		 * Set the upload directory to the fonts directory.
		 *
		 * wp_get_font_dir() contains the 'font_dir' hook, whose callbacks are
		 * likely to call wp_get_upload_dir().
		 *
		 * To avoid an infinite loop, don't hook wp_get_font_dir() to 'upload_dir'.
		 * Instead, just pass its return value to the 'upload_dir' callback.
		 */
		$font_dir       = wp_get_font_dir();
		$set_upload_dir = function () use ( $font_dir ) {
			return $font_dir;
		};
		add_filter( 'upload_dir', $set_upload_dir );

		$overrides = array(
			'upload_error_handler' => array( self::class, 'handle_font_file_upload_error' ),
			// Arbitrary string to avoid the is_uploaded_file() check applied
			// when using 'wp_handle_upload'.
			'action'               => 'wp_handle_font_upload',
			// Not testing a form submission.
			'test_form'            => false,
			// Seems mime type for files that are not images cannot be tested.
			// See wp_check_filetype_and_ext().
			'test_type'            => true,
			// Only allow uploading font files for this request.
			'mimes'                => \WP_Font_Utils::get_allowed_font_mime_types(),
		);

		$uploaded_file = wp_handle_upload( $file, $overrides );
		remove_filter( 'upload_dir', $set_upload_dir );
		remove_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) );

		return $uploaded_file;
	}

	/**
	 * Downloads a file from a URL.
	 *
	 * @param string $file_url The file URL.
	 **/
	private static function download_file( $file_url ) {
		if ( ! function_exists( 'download_url' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}

		$allowed_extensions = array( 'ttf', 'otf', 'woff', 'woff2', 'eot' );

		$allowed_extensions = array_map( 'preg_quote', $allowed_extensions );

		// Set variables for storage, fix file filename for query strings.
		preg_match( '/[^\?]+\.(' . implode( '|', $allowed_extensions ) . ')\b/i', $file_url, $matches );
		$file_array         = array();
		$file_array['name'] = wp_basename( $matches[0] );

		// Download file to temp location.
		$file_array['tmp_name'] = download_url( $file_url );
		return $file_array;
	}

	/**
	 * Inserts a font face.
	 *
	 * @param array $font_face The font face settings.
	 * @param int   $parent_font_family_id The parent font family ID.
	 * @return \WP_Error|\WP_Post The inserted font face post or an error if the font face already exists.
	 */
	public static function insert_font_face( array $font_face, int $parent_font_family_id ) {
		$slug = \WP_Font_Utils::get_font_face_slug( $font_face );

		// Check that the font face slug is unique.
		$query = new \WP_Query(
			array(
				'post_type'              => self::POST_TYPE,
				'posts_per_page'         => 1,
				'name'                   => $slug,
				'update_post_meta_cache' => false,
				'update_post_term_cache' => false,
			)
		);

		if ( ! empty( $query->get_posts() ) ) {
			return new \WP_Error(
				'duplicate_font_face',
				/* translators: %s: Font face slug. */
				sprintf( __( 'A font face with slug "%s" already exists.', 'woocommerce' ), $slug ),
			);
		}

		// Validate the font face settings.

		$validation_error = self::validate_font_face( $font_face );
		if ( is_wp_error( $validation_error ) ) {
			return $validation_error;
		}

		$parsed_font_face['fontFamily'] = addslashes( \WP_Font_Utils::sanitize_font_family( $font_face['fontFamily'] ) );
		$parsed_font_face['fontStyle']  = sanitize_text_field( $font_face['fontStyle'] );
		$parsed_font_face['fontWeight'] = sanitize_text_field( $font_face['fontWeight'] );
		$file                           = self::download_file( $font_face['src'] );

		$uploaded_file = self::handle_font_file_upload( $file );

		$parsed_font_face['src']     = self::sanitize_src( $uploaded_file['url'] );
		$parsed_font_face['preview'] = esc_url_raw( $font_face['preview'] );

		// Insert the font face.
		wp_insert_post(
			array(
				'post_type'    => self::POST_TYPE,
				'post_parent'  => $parent_font_family_id,
				'post_title'   => $slug,
				'post_name'    => sanitize_title( $slug ),
				'post_content' => wp_json_encode( $parsed_font_face ),
				'post_status'  => 'publish',
			)
		);
	}


	/**
	 * Validates a font face.
	 *
	 * @param array $font_face The font face settings.
	 * @return \WP_Error|null The error if the font family is invalid, null otherwise.
	 */
	private static function validate_font_face( $font_face ) {
		// Validate the font face family name.
		if ( empty( $font_face['fontFamily'] ) ) {
			return new \WP_Error(
				'invalid_font_face_font_family',
				__( 'The font face family name is required.', 'woocommerce' ),
			);
		}

		// Validate the font face font style.
		if ( empty( $font_face['fontStyle'] ) ) {
			return new \WP_Error(
				'invalid_font_face_font_style',
				__( 'The font face font style is required.', 'woocommerce' ),
			);
		}

		// Validate the font face weight.
		if ( empty( $font_face['fontWeight'] ) ) {
			return new \WP_Error(
				'invalid_font_face_font_weight',
				__( 'The font face weight is required.', 'woocommerce' ),
			);
		}

		// Validate the font face src.
		if ( empty( $font_face['src'] ) ) {
			return new \WP_Error(
				'invalid_font_face_src',
				__( 'The font face src is required.', 'woocommerce' ),
			);
		}
	}

}
