<?php
/**
 * Custom Typography Class
 *
 * @package errigal
 * @since 1.0.0
 */

namespace Errigal\Customiser;

defined( 'ABSPATH' ) || exit;

use Errigal\Constant\{
	Errigal_Custom_Typography_Hook as Typography_Hook,
	Errigal_Editor_Class as Editor_Class,
	Errigal_Html_Attr as Html_Attr,
	Errigal_Html_Option as Html_Option,
	Errigal_Html_Tag as Html_Tag,
	Errigal_Locale as Locale,
	Errigal_Shared_Selector as Selector,
};
use Errigal\Shared\{
	Errigal_Asset as Asset,
	Errigal_Html_Builder as Html_Builder,
	Errigal_Utility as Utility,
	Errigal_Validator as Validator,
};

/**
 * This class is in charge of providing typography settings and functions.
 *
 * @since 1.0.0
 */
class Errigal_Custom_Typography extends Errigal_Custom_Base {
	/**
	 * Setting ID: Base font.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	public const ID_BASE_FONT = 'base_font';

	/**
	 * Setting ID: Heading font.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	public const ID_HEADING_FONT = 'heading_font';

	/**
	 * Setting ID: Font size.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	public const ID_FONT_SIZE = 'font_size';

	/**
	 * Font size value: Small.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	public const FONT_SIZE_SMALL = 'small-font';

	/**
	 * Font size value: Standard.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	public const FONT_SIZE_STANDARD = 'standard-font';

	/**
	 * Font size value: Large.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	public const FONT_SIZE_LARGE = 'large-font';

	/**
	 * Default setting value: Font size.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	public const DEFAULT_FONT_SIZE = self::FONT_SIZE_STANDARD;

	/**
	 * Font property: ID.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	public const FONT_ID = 'id';

	/**
	 * Font property: Name.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	public const FONT_NAME = 'name';

	/**
	 * Font property: Family.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	public const FONT_FAMILY = 'family';

	/**
	 * Stylesheet name: Base google font style.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	public const STYLE_BASE_GOOGLE_FONT = 'base-google-font-style';

	/**
	 * Stylesheet name: Heading google font style.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	public const STYLE_HEADING_GOOGLE_FONT = 'heading-google-font-style';

	/**
	 * Language font sets.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private const LANGUAGE_FONT_SETS = array(
		Locale::ENGLISH  => array(
			'Roboto'     => array(
				self::FONT_ID     => 'roboto',
				self::FONT_NAME   => 'Roboto',
				self::FONT_FAMILY => 'Roboto:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap',
			),
			'Lato'       => array(
				self::FONT_ID     => 'lato',
				self::FONT_NAME   => 'Lato',
				self::FONT_FAMILY => 'Lato:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap',
			),
			'Montserrat' => array(
				self::FONT_ID     => 'montserrat',
				self::FONT_NAME   => 'Montserrat',
				self::FONT_FAMILY => 'Montserrat:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap',
			),
			'Raleway'    => array(
				self::FONT_ID     => 'raleway',
				self::FONT_NAME   => 'Raleway',
				self::FONT_FAMILY => 'Raleway:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap',
			),
			'Inter'      => array(
				self::FONT_ID     => 'inter',
				self::FONT_NAME   => 'Inter',
				self::FONT_FAMILY => 'Inter:wght@300;400;500;600;700&display=swap',
			),
		),
		Locale::JAPANESE => array(
			'Noto Sans Japanese' => array(
				self::FONT_ID     => 'noto_sans_jp',
				self::FONT_NAME   => 'Noto Sans JP',
				self::FONT_FAMILY => 'Noto+Sans+JP:wght@300;400;500;700&display=swap',
			),
			'BIZ UDPGothic'      => array(
				self::FONT_ID     => 'biz_udpgothic',
				self::FONT_NAME   => 'BIZ UDPGothic',
				self::FONT_FAMILY => 'BIZ+UDPGothic:wght@400;700&display=swap',
			),
			'Sawarabi Gothic'    => array(
				self::FONT_ID     => 'sawarabi_gothic',
				self::FONT_NAME   => 'Sawarabi Gothic',
				self::FONT_FAMILY => 'Sawarabi+Gothic&display=swap',
			),
			'Kosugi Maru'        => array(
				self::FONT_ID     => 'kosugi_maru',
				self::FONT_NAME   => 'Kosugi Maru',
				self::FONT_FAMILY => 'Kosugi+Maru&display=swap',
			),
			'Kosugi'             => array(
				self::FONT_ID     => 'kosugi',
				self::FONT_NAME   => 'Kosugi',
				self::FONT_FAMILY => 'Kosugi&display=swap',
			),
		),
	);

	/**
	 * Pixel unit.
	 *
	 * @since 1.2.0
	 *
	 * @var string
	 */
	private const PX_UNIT = 'px';

	/**
	 * Font px size: Small.
	 *
	 * @since 1.2.0
	 *
	 * @var int
	 */
	private const FONT_PX_SIZE_SMALL = 16;

	/**
	 * Font px size: Standard.
	 *
	 * @since 1.2.0
	 *
	 * @var int
	 */
	private const FONT_PX_SIZE_STANDARD = 18;

	/**
	 * Font px size: Large.
	 *
	 * @since 1.2.0
	 *
	 * @var int
	 */
	private const FONT_PX_SIZE_LARGE = 20;

	/**
	 * Root em size: Small.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private const ROOT_EM_SIZE_SMALL = self::FONT_PX_SIZE_SMALL . self::PX_UNIT;

	/**
	 * Root em size: Standard.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private const ROOT_EM_SIZE_STANDARD = self::FONT_PX_SIZE_STANDARD . self::PX_UNIT;

	/**
	 * Root em size: Large.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private const ROOT_EM_SIZE_LARGE = self::FONT_PX_SIZE_LARGE . self::PX_UNIT;

	/**
	 * Font px sizes.
	 *
	 * @since 1.2.0
	 *
	 * @var array
	 */
	private const FONT_PX_SIZES = array(
		self::FONT_SIZE_SMALL    => self::FONT_PX_SIZE_SMALL,
		self::FONT_SIZE_STANDARD => self::FONT_PX_SIZE_STANDARD,
		self::FONT_SIZE_LARGE    => self::FONT_PX_SIZE_LARGE,
	);

	/**
	 * Gets base font ID.
	 *
	 * @since 1.1.0
	 *
	 * @return string
	 */
	public static function get_base_font_id() {
		return sanitize_key(
			self::get_theme_mod_string(
				self::ID_BASE_FONT,
				self::get_default_font_id()
			)
		);
	}

	/**
	 * Gets heading font ID.
	 *
	 * @since 1.1.0
	 *
	 * @return string
	 */
	public static function get_heading_font_id() {
		return sanitize_key(
			self::get_theme_mod_string(
				self::ID_HEADING_FONT,
				self::get_default_font_id()
			)
		);
	}

	/**
	 * Gets default font ID.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	public static function get_default_font_id() {
		$fonts = self::get_fonts();

		if ( ! Validator::is_associative_array( $fonts ) ) {
			return '';
		}

		$default_font = array_values( $fonts )[0];

		if ( ! Validator::is_associative_array( $default_font ) ) {
			return '';
		}

		return sanitize_key( $default_font[ self::FONT_ID ] );
	}

	/**
	 * Gets font size.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	public static function get_font_size() {
		return sanitize_key(
			self::get_theme_mod_string(
				self::ID_FONT_SIZE,
				self::DEFAULT_FONT_SIZE
			)
		);
	}

	/**
	 * Gets px font size.
	 *
	 * @since 1.2.0
	 *
	 * @return int
	 */
	public static function get_px_font_size() {
		if ( is_admin() ) {
			return self::FONT_PX_SIZE_SMALL;
		}

		$font_size = self::get_font_size();

		return Validator::has_array_key( self::FONT_PX_SIZES, $font_size )
			? self::FONT_PX_SIZES[ $font_size ]
			: self::FONT_PX_SIZE_STANDARD;
	}

	/**
	 * Gets font size class.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	public static function get_font_size_class() {
		return Utility::get_class( self::get_font_size() );
	}

	/**
	 * Gets font names.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	public static function get_font_names() {
		$fonts = self::get_fonts();

		if ( ! Validator::is_associative_array( $fonts ) ) {
			return array();
		}

		return Utility::sanitise_associative_array(
			array_column(
				$fonts,
				self::FONT_NAME,
				self::FONT_ID
			)
		);
	}

	/**
	 * Gets font families.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	public static function get_font_families() {
		$fonts = self::get_fonts();

		if ( ! Validator::is_associative_array( $fonts ) ) {
			return array();
		}

		return Utility::sanitise_associative_array(
			array_column(
				$fonts,
				self::FONT_FAMILY,
				self::FONT_ID
			)
		);
	}

	/**
	 * Gets font sizes.
	 *
	 * @since 1.1.0
	 *
	 * @return string[]
	 */
	public static function get_font_sizes() {
		return array(
			self::FONT_SIZE_SMALL,
			self::FONT_SIZE_STANDARD,
			self::FONT_SIZE_LARGE,
		);
	}

	/**
	 * Gets base font URI.
	 *
	 * @since 1.2.0
	 *
	 * @param bool $is_editor Optional. Whether to return base font URI for editor. Default false.
	 * @return string
	 */
	public static function get_base_font_uri( $is_editor = false ) {
		return self::get_font_uri( self::get_base_font_id(), $is_editor );
	}

	/**
	 * Gets heading font URI.
	 *
	 * @since 1.2.0
	 *
	 * @param bool $is_editor Optional. Whether to return heading font URI for editor. Default false.
	 * @return string
	 */
	public static function get_heading_font_uri( $is_editor = false ) {
		return self::get_font_uri( self::get_heading_font_id(), $is_editor );
	}

	/**
	 * Gets base inline font style.
	 *
	 * @since 1.2.0
	 *
	 * @param bool $is_editor Optional. Whether to return base inline style for editor. Default false.
	 * @return string
	 */
	public static function get_base_inline_font_style( $is_editor = false ) {
		return Utility::join(
			Utility::SEPARATOR_SPACE,
			Validator::is_true( $is_editor ) && Asset::is_block_editor()
				? self::get_inline_font_size_style( Utility::get_selector( Editor_Class::BLOCK_EDITOR_WRAPPER ) )
				: '',
			self::get_inline_font_family_style(
				self::get_base_inline_font_style_selector( $is_editor ),
				self::get_font_name( self::get_base_font_id() )
			)
		);
	}

	/**
	 * Gets heading inline font style.
	 *
	 * @since 1.2.0
	 *
	 * @param bool $is_editor Optional. Whether to return heading inline style for editor. Default false.
	 * @return string
	 */
	public static function get_heading_inline_font_style( $is_editor = false ) {
		return self::get_inline_font_family_style(
			self::get_heading_inline_font_style_selector( $is_editor ),
			self::get_font_name( self::get_heading_font_id() )
		);
	}

	/**
	 * Determines whether unique font is set for headings.
	 *
	 * @since 1.0.0
	 *
	 * @return bool
	 */
	public static function has_unique_heading_font() {
		return self::get_base_font_id() !== self::get_heading_font_id();
	}

	/**
	 * Renders preconnect links.
	 *
	 * @since 1.0.0
	 *
	 * @see Errigal\Hook\Errigal_Asset_Hooks::add_style_hooks()
	 * @link https://developer.wordpress.org/reference/hooks/wp_head/
	 *
	 * @return void
	 */
	public static function render_preconnect_links() {
		$html_builder = Html_Builder::create();

		$html_builder->create_link(
			array(
				Html_Option::ATTRS => array(
					Html_Attr::REL  => Html_Attr::VALUE_PRECONNECT,
					Html_Attr::HREF => 'https://fonts.googleapis.com',
				),
			)
		)->render_element();
		$html_builder->create_link(
			array(
				Html_Option::ATTRS => array(
					Html_Attr::REL         => Html_Attr::VALUE_PRECONNECT,
					Html_Attr::HREF        => 'https://fonts.gstatic.com',
					Html_Attr::CROSSORIGIN => null,
				),
			)
		)->render_element();
	}

	/**
	 * Gets fonts.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	private static function get_fonts() {
		/**
		 * Filters fonts.
		 *
		 * @since 1.1.0
		 *
		 * @param array $fonts Associative array of fonts.
		 */
		$fonts = apply_filters(
			Typography_Hook::FONTS,
			self::get_language_font_set()
		);

		if ( ! Validator::is_associative_array( $fonts ) ) {
			return self::get_language_font_set();
		}

		return $fonts;
	}

	/**
	 * Gets language font set.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	private static function get_language_font_set() {
		$locale = get_locale();

		if ( ! Validator::is_non_empty_string( $locale ) ) {
			return array();
		}

		/**
		 * Filters language font sets.
		 *
		 * @since 1.2.0
		 *
		 * @param array $language_font_sets Associative array of language font sets.
		 */
		$language_font_sets = apply_filters(
			Typography_Hook::LANGUAGE_FONT_SETS,
			self::LANGUAGE_FONT_SETS
		);

		if ( Validator::has_array_key( $language_font_sets, $locale ) ) {
			return $language_font_sets[ $locale ];
		}

		if ( Validator::has_array_key( $language_font_sets, Locale::ENGLISH ) ) {
			return $language_font_sets[ Locale::ENGLISH ];
		}

		return array();
	}

	/**
	 * Gets font URI.
	 *
	 * @since 1.0.0
	 *
	 * @param string $font_id   Font ID.
	 * @param bool   $is_editor Optional. Whether to return font URI for editor. Default false.
	 * @return string
	 */
	private static function get_font_uri( $font_id, $is_editor = false ) {
		if ( ! Validator::is_non_empty_string( $font_id ) ) {
			return '';
		}

		$font_families = self::get_font_families();

		if ( ! Validator::is_associative_array( $font_families ) ) {
			return '';
		}

		$base_uri    = Validator::is_true( $is_editor )
			? 'https://fonts.googleapis.com'
			: '//fonts.googleapis.com';
		$font_family = $font_families[ $font_id ] ?? array_values( $font_families )[0];

		return esc_url( $base_uri . '/css?family=' . $font_family );
	}

	/**
	 * Gets inline font size style.
	 *
	 * @since 1.2.0
	 *
	 * @param string $selector Selector.
	 * @return string
	 */
	private static function get_inline_font_size_style( $selector ) {
		if ( ! Validator::is_non_empty_string( $selector ) ) {
			return '';
		}

		$root_em_size = self::get_root_em_size();

		if ( ! Validator::is_non_empty_string( $root_em_size ) ) {
			return '';
		}

		return esc_attr(
			sprintf(
				'%s { font-size: %s !important; }',
				$selector,
				$root_em_size
			)
		);
	}

	/**
	 * Gets inline font family style.
	 *
	 * @since 1.2.0
	 *
	 * @param string $selector  Selector.
	 * @param string $font_name Font name.
	 * @return string
	 */
	private static function get_inline_font_family_style( $selector, $font_name ) {
		if (
			! Validator::is_non_empty_string( $selector ) ||
			! Validator::is_non_empty_string( $font_name )
		) {
			return '';
		}

		return sprintf(
			'%s {font-family: "%s", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;}',
			esc_attr( $selector ),
			esc_attr( $font_name )
		);
	}

	/**
	 * Gets base inline font style selector.
	 *
	 * @since 1.2.0
	 *
	 * @param bool $is_editor Optional. Whether to return base inline font style selector for editor. Default false.
	 * @return string
	 */
	private static function get_base_inline_font_style_selector( $is_editor = false ) {
		if ( ! Validator::is_true( $is_editor ) ) {
			return esc_attr( Html_Tag::HTML );
		}

		if ( Asset::is_block_editor() ) {
			return Utility::get_selector( Editor_Class::BLOCK_EDITOR_WRAPPER, Selector::UNIVERSAL );
		}

		return Utility::get_selector( Editor_Class::CLASSIC_EDITOR_WRAPPER );
	}

	/**
	 * Gets heading inline font style selector.
	 *
	 * @since 1.2.0
	 *
	 * @param bool $is_editor Optional. Whether to return heading inline font style selector for editor. Default false.
	 * @return string
	 */
	private static function get_heading_inline_font_style_selector( $is_editor = false ) {
		if ( ! Validator::is_true( $is_editor ) ) {
			return self::get_heading_selectors();
		}

		if ( Asset::is_block_editor() ) {
			return Utility::combine_selectors(
				self::get_heading_selectors( Utility::get_selector( Editor_Class::BLOCK_EDITOR_WRAPPER ) ),
				Utility::get_selector( Editor_Class::BLOCK_EDITOR_WRAPPER, Editor_Class::BLOCK_EDITOR_POST_TITLE )
			);
		}

		return self::get_heading_selectors( Utility::get_selector( Editor_Class::CLASSIC_EDITOR_WRAPPER ) );
	}

	/**
	 * Gets heading selectors.
	 *
	 * @since 1.2.0
	 *
	 * @param string $wrapper_class Optional. Wrapper class. Default empty string.
	 * @return string
	 */
	private static function get_heading_selectors( $wrapper_class = '' ) {
		if ( ! is_string( $wrapper_class ) ) {
			return '';
		}

		$headings = Html_Tag::HEADINGS;

		if ( ! Validator::is_string_list( $headings ) ) {
			return '';
		}

		$heading_selectors = array();

		foreach ( $headings as $heading ) {
			if ( ! Validator::is_non_empty_string( $heading ) ) {
				continue;
			}

			$heading_selectors[] = Utility::get_selector( $wrapper_class, $heading );
			$heading_selectors[] = Utility::get_selector( $wrapper_class, $heading, Html_Tag::A );
		}

		/**
		 * Filters heading selectors.
		 *
		 * @since 1.2.0
		 *
		 * @param string[] $heading_selectors Heading selectors.
		 */
		$heading_selectors = apply_filters(
			Typography_Hook::HEADING_SELECTORS,
			$heading_selectors
		);

		if ( ! Validator::is_string_list( $heading_selectors ) ) {
			return '';
		}

		return Utility::combine_selectors( ...$heading_selectors );
	}

	/**
	 * Gets font name.
	 *
	 * @since 1.2.0
	 *
	 * @param string $font_id Font ID.
	 * @return string
	 */
	private static function get_font_name( $font_id ) {
		if ( ! Validator::is_non_empty_string( $font_id ) ) {
			return '';
		}

		$font_names = self::get_font_names();

		if ( ! Validator::is_associative_array( $font_names ) ) {
			return '';
		}

		if ( ! Validator::has_array_key( $font_names, $font_id ) ) {
			return esc_html( array_values( $font_names )[0] );
		}

		return esc_html( $font_names[ $font_id ] );
	}

	/**
	 * Gets root em size.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	private static function get_root_em_size() {
		$font_size = self::get_font_size();

		switch ( $font_size ) {
			case self::FONT_SIZE_SMALL:
				return esc_attr( self::ROOT_EM_SIZE_SMALL );
			case self::FONT_SIZE_LARGE:
				return esc_attr( self::ROOT_EM_SIZE_LARGE );
			default:
				return esc_attr( self::ROOT_EM_SIZE_STANDARD );
		}
	}
}
