<?php
/**
 * Style-Builder Class for sanitizing and building CSS styles to be printed.
 */

/**
 * Wrapper function to create general CSS style with Style-Builder class
 * 
 * $hoot_style_builder->add will sanitizes the property and adds it using 'wp_add_inline_style()'
 *
 * @since 3.0.0
 * @access public
 * @param array $args Arguments array
 *                    'selector' *required => html class/id/tag
 *                    'property' *required => string: name of the css property
 *                                            array: array of $property_name => $value, $idtag, $important, $osn_typo_reset
 *                    'value'              => value of the css property
 *                                            (redundant if $property is an array)
 *                    'idtag'              => setting id used in wp admin (may be used in live preview js)
 *                                            used for fetching background and typography settings
 *                                            (redundant if $property is an array)
 *                    'media'              => media-query
 *                    'important'          => (redundant if $property is an array)
 *                    'osn_typo_reset'   => for 'typography' options only - whether to re-apply all text properties
 * @return void
 */
function hoot_add_css_rule( $args ) {

	global $hoot_style_builder;

	extract( wp_parse_args( $args, array(
		'selector'         => '',
		'property'         => '',
		'value'            => '',
		'idtag'            => '',
		'media'            => '',
		'important'        => false,
		'osn_typo_reset'   => false,
	) ) );

	if ( empty( $selector ) || empty( $property ) )
		return;

	if ( !is_array( $property ) ) {

		$hoot_style_builder->add( $selector, $property, $value, $idtag, $media, $important, $osn_typo_reset );

	} else {

		foreach ( $property as $addproperty => $prop_value ) {
			if ( !is_array( $prop_value ) && !empty( $prop_value ) ) {
				$addvalue = $prop_value;
				$addidtag = $addimportant = $addreset = '';
			} else {
				$addvalue     = ( empty( $prop_value[0] ) ) ? '' : $prop_value[0];
				$addidtag     = ( empty( $prop_value[1] ) ) ? '' : $prop_value[1];
				$addimportant = ( empty( $prop_value[2] ) ) ? '' : $prop_value[2];
				$addreset     = ( empty( $prop_value[3] ) ) ? '' : $prop_value[3];
			}
			$hoot_style_builder->add( $selector, $addproperty, $addvalue, $addidtag, $media, $addimportant, $addreset );
		}

	}
}

/**
 * Wrapper function to add general CSS vars with Style-Builder class
 * $hoot_style_builder->add will sanitizes the property and adds it using 'wp_add_inline_style()'
 * @since 3.0.0
 * @access public
 * @param array $cssvars css vars array
 * @return void
 */
function hoot_add_cssvars( $cssvars ) {
	global $hoot_style_builder;
	$hoot_style_builder->add_cssvars( $cssvars );
}

/**
 * A class of helper functions to create styles
 * 
 * @since 3.0.0
 */
class Hoot_Style_Builder {

	/**
	 * The one instance of Hoot_Customize.
	 *
	 * @since 3.0.0
	 * @access private
	 * @var Hoot_Customize The one instance for the singleton.
	 */
	private static $instance;

	/**
	 * The array for storing css rules.
	 *
	 * @since 3.0.0
	 * @access private
	 * @var array Holds the css rules array.
	 */
	private $cssrules = array();

	/**
	 * The array for storing css variables.
	 *
	 * @since 3.0.0
	 * @access private
	 * @var array Holds the css vars array.
	 */
	private $cssvars = array();

	/**
	 * The array for storing media specific css rules.
	 *
	 * @since 3.0.0
	 * @access private
	 * @var array Holds the media specific css rules array.
	 */
	private $mediarules = array();

	/**
	 * The string with rendered css rules.
	 *
	 * @since 3.0.0
	 * @access private
	 * @var array Holds the media specific css rules array.
	 */
	private $dynamiccss = '';

	/**
	 * Protected constructor to prevent creating a new instance of the
	 * Singleton from outside of this class.
	 *
	 * @since 3.0.0
	 * @access protected
	 */
	protected function __construct() {

		/**
		 * Hook into 'wp_enqueue_scripts' as 'wp_add_inline_style()' requires stylesheet $handle to be
		 * already registered. Main stylesheet with handle 'style' is registered by the framework via
		 * 'wp_enqueue_scripts' hook at priority 0
		 */
		add_action( 'wp_enqueue_scripts', array( $this, 'build_css' ), 99 );

	}

	/**
	 * Create Dynamic CSS styles from an array.
	 *
	 * @since 3.0.0
	 * @access public
	 * @return void
	 */
	function build_css(){

		/**
		 * Allow hook for child themes to add dynamic css rules
		 * Recommended priorities:
		 * @2 : Parent lite
		 *      @3 : Child lite (+dark if any)
		 * @4 : Parent premium prepare (rules removed)
		 * @5 : Parent premium
		 *      @6 : Child premium prepare (rules removed)
		 *      @6 : Child premium
		 * @6 : Parent HK lite
		 * @7 : Parent HK premium
		 * @8 : Parent blocks
		 *      @8 : Child HK
		 * @99 : Enqueue gfonts
		 */
		do_action( 'hoot_dynamic_cssrules' );

		/* Build Inline CSS */

		$css = '';
		if ( !empty( $this->cssvars ) ) {
			$css .= $this->build_css_string( array( ':root' => $this->cssvars ) );
		}

		if ( !empty( $this->cssrules ) ) {
			$css .= $this->build_css_string( $this->cssrules );
		}

		if ( !empty( $this->mediarules ) ) {
			foreach ( $this->mediarules as $mediaquery => $mediarules ) {
				$css .= ' @media ' . $mediaquery . ' { ';
				$css .= $this->build_css_string( $mediarules );
				$css .= ' } ';
			}
		}

		/* Allow child themes to add additional conditional css */

		$this->dynamiccss = wp_strip_all_tags( apply_filters( 'hoot_dynamic_css', $css ) );

		/* Print CSS */

		if ( !empty( $this->dynamiccss ) ) {
			$handle = apply_filters( 'hoot_style_builder_inline_style_handle', 'hoot-style' );
			wp_add_inline_style( sanitize_html_class( $handle ), $this->dynamiccss );
		}

	}

	/**
	 * Create Formatted CSS string
	 *
	 * @since 3.0.0
	 * @access public
	 * @param array $cssrules
	 * @param string $n Line ending
	 * @param string $t Starting Tab
	 * @return string
	 */
	function build_css_string( $cssrules ){

		$css = ' ';

		foreach ( $cssrules as $selector => $rules ) {
			if ( is_array( $rules ) ) {
				$css .= $selector . ' { ';
				foreach ( $rules as $property => $value ) {
					if ( is_array( $value ) && !empty( $value['value'] ) ) {
						$important = ( !empty( $value['important'] ) ) ? ' !important' : '';
						$css .= $property . ': ' . $value['value'] . $important . '; ';
					} elseif ( is_scalar( $value ) ) {
						$css .= $property . ': ' . $value . '; ';
					}
				}
				$css .= '} ';
			}
		}

		return $css;
	}

	/**
	 * Sanitize and add CSS styles to cssrules array.
	 * The entire property array is replaced for a selector
	 * Example: Changing only color['value'] is not possible. The entire 'color'
	 *          array (value, important, idtag) will replace the old 'color' array if exists.
	 *
	 * @since 3.0.0
	 * @access public
	 * @param string $selector html class/id/tag
	 * @param string $property name of the css property
	 * @param string $value value of the css property
	 * @param string $idtag setting id used in wp admin (may be used in live preview js)
	 *                      used for fetching background and typography settings
	 * @param string $media media-query
	 * @param bool|string $important
	 * @param bool $osn_typo_reset Used for 'typography' property - whether to re-apply all text properties
	 * @return void
	 */
	function add( $selector, $property, $value = '', $idtag = '', $media = '', $important = false, $osn_typo_reset = false ){

		/* Sanitize the CSS property */
		$merge = $this->sanitize_css_rule( $property, $value, $idtag, $important, $osn_typo_reset );

		/* Add rule if there is one */
		if ( !empty( $merge ) && is_array( $merge ) ) {

			if ( !empty( $media ) ) {
				if ( empty( $this->mediarules[$media][$selector] ) )
					$this->mediarules[$media][$selector] = array();
				$this->mediarules[$media][$selector] = array_merge( $this->mediarules[$media][$selector], $merge );
			} else {
				if ( empty( $this->cssrules[$selector] ) )
					$this->cssrules[$selector] = array();
				$this->cssrules[$selector] = array_merge( $this->cssrules[$selector], $merge );
			}

		}
	}

	/**
	 * Sanitize and add CSS vars to cssvars array.
	 * @since 3.0.0
	 * @access public
	 * @param array $cssvars
	 * @return void
	 */
	function add_cssvars( $cssvars ){
		// Sanitize and add vars
		if ( is_array( $cssvars ) ) {
			foreach ( $cssvars as $key => $value ) {
				$skey = sanitize_key( $key );
				if ( $skey && !empty( $value ) ) {
					if ( is_scalar( $value ) ) {
						$this->cssvars[ $skey ] = wp_strip_all_tags( $value );
					} elseif ( is_array( $value ) ) {
						foreach ( $value as $device => $deviceval ) {
							switch ( $device ) {
								case 'desktop':
									$this->cssvars[ $skey ] = wp_strip_all_tags( $deviceval );
									break;
								case 'tablet':
									if ( apply_filters( 'hoot_dynamiccss_skiptablet', false, $skey, $value ) )
										break;
									$default_breakpoint = 969;
									$breakpoint = apply_filters( 'hoot_dynamiccss_breakpoint', $default_breakpoint, 'tablet', $skey, $value );
									$breakpoint = intval( $breakpoint );
									$breakpoint = ( $breakpoint > 0 ) ? $breakpoint : $default_breakpoint;
									$mquery = 'only screen and (max-width: ' . $breakpoint . 'px)';
									if ( empty( $this->mediarules[ $mquery ] ) )
										$this->mediarules[ $mquery ] = array( ':root' => array() );
									elseif ( empty( $this->mediarules[ $mquery ][':root'] ) )
										$this->mediarules[ $mquery ][':root'] = array();
									$this->mediarules[ $mquery ][':root'][ $skey ] = wp_strip_all_tags( $deviceval );
									break;
								case 'mobile':
									$default_breakpoint = empty( $value['tablet'] ) ? 969 : 600;
									$breakpoint = apply_filters( 'hoot_dynamiccss_breakpoint', $default_breakpoint, 'mobile', $skey, $value );
									$breakpoint = intval( $breakpoint );
									$breakpoint = ( $breakpoint > 0 ) ? $breakpoint : $default_breakpoint;
									$mquery = 'only screen and (max-width: ' . $breakpoint . 'px)';
									if ( empty( $this->mediarules[ $mquery ] ) )
										$this->mediarules[ $mquery ] = array( ':root' => array() );
									elseif ( empty( $this->mediarules[ $mquery ][':root'] ) )
										$this->mediarules[ $mquery ][':root'] = array();
									$this->mediarules[ $mquery ][':root'][ $skey ] = wp_strip_all_tags( $deviceval );
									break;
								default:
									break;
							}
						}
					}
				}
			}
		}
	}

	/**
	 * Get CSS styles from cssrules array.
	 *
	 * @since 3.0.0
	 * @access public
	 * @return array|false
	 */
	function get( $selector = '' ){
		if ( !empty( $selector ) ) {
			if ( isset( $this->cssrules[$selector] ) )
				return $this->cssrules[$selector];
			else
				return false;
		} else
			return $this->cssrules;
	}

	/**
	 * Get CSS styles from mediarules array.
	 *
	 * @since 3.0.0
	 * @access public
	 * @return array|false
	 */
	function get_media( $media = '', $selector = '' ){
		if ( !empty( $media ) ) {
			if ( isset( $this->mediarules[$media] ) ) {
				if ( !empty( $selector ) ) {
					if ( isset( $this->mediarules[$media][$selector] ) )
						return $this->mediarules[$media][$selector];
					else
						return false;
				} else
					return $this->mediarules[$media];
			} else
				return false;
		} else {
			return $this->mediarules;
		}
	}

	/**
	 * Remove CSS styles from cssrules array.
	 *
	 * @since 3.0.0
	 * @access public
	 * @return string
	 */
	function remove( $selectors ){
		if ( is_array( $selectors ) ) {
			foreach ( $selectors as $selector ) {
				if ( isset( $this->cssrules[$selector] ) )
					unset( $this->cssrules[$selector] );
			}
		} elseif ( isset( $this->cssrules[$selectors] ) ) {
			unset( $this->cssrules[$selectors] );
		}
	}

	/**
	 * Remove CSS styles from mediarules array.
	 *
	 * @since 3.0.0
	 * @access public
	 * @return string
	 */
	function remove_media( $media, $selectors ){
		if ( isset( $this->mediarules[$media] ) ) {
			if ( is_array( $selectors ) ) {
				foreach ( $selectors as $selector ) {
					if ( isset( $this->mediarules[$media][$selector] ) )
						unset( $this->mediarules[$media][$selector] );
				}
			} elseif ( isset( $this->mediarules[$media][$selectors] ) ) {
				unset( $this->mediarules[$media][$selectors] );
			}
		}
	}

	/**
	 * Remove CSS styles from cssrules array.
	 *
	 * @since 3.0.0
	 * @access public
	 * @return string
	 */
	function remove_style( $selector, $property ){
		if ( !empty( $this->cssrules[$selector] ) && isset( $this->cssrules[$selector][$property] ) )
			unset( $this->cssrules[$selector][$property] );
	}

	/**
	 * Remove CSS styles from mediarules array.
	 *
	 * @since 3.0.0
	 * @access public
	 * @return string
	 */
	function remove_media_style( $media, $selector, $property ){
		if ( isset( $this->mediarules[$media] ) && !empty( $this->mediarules[$media][$selector] ) && isset( $this->mediarules[$media][$selector][$property] ) )
			unset( $this->mediarules[$media][$selector][$property] );
	}

	/**
	 * Create general CSS style
	 *
	 * @since 3.0.0
	 * @access public
	 * @param string $property name of the css property
	 * @param string $value value of the css property
	 * @param string $idtag setting id used in wp admin (may be used in live preview js)
	 *                      used for fetching background and typography settings
	 * @param bool|string $important
	 * @param bool $osn_typo_reset Used for 'typography' property
	 * @return mixed empty if sanitization failed, else the sanitized property array
	 */
	function sanitize_css_rule( $property, $value = '', $idtag = '', $important = false, $osn_typo_reset = false ) {

		if ( empty( $property ) )
			return '';
		if ( $property == 'background' || $property == 'font' || $property == 'typography' ) {
			if ( empty( $value ) && empty( $idtag ) ) return '';
		} else {
			if ( empty( $value ) ) return '';
		}

		/** Sanitize CSS values **/
		if ( $value != 'inherit' ) : switch( $property ):

			case 'color':
			case 'background-color':
			case 'border-color':
			case 'border-right-color':
			case 'border-bottom-color':
			case 'border-top-color':
			case 'border-left-color':
				if ( 'none' == $value || 'transparent' == $value )
					$value = 'transparent';
				else
					$value = hoot_sanitize_color_allowocto( $value ); // Now supports alpha hex values (in addition to rgba)
				break;

			case 'background':
				if ( !empty( $value ) ) {
					if ( 'none' == $value || 'transparent' == $value ) {
						$value = 'none';
					} else {
						$value = hoot_sanitize_color_allowocto( $value ); // Now supports alpha hex values (in addition to rgba)
					}
				} elseif ( !empty( $idtag ) ) {
					// use the background function for multiple background properties
					return $this->sanitize_background( $idtag, $important );
				}
				break;

			case 'background-image':
				$check = esc_url( $value );
				$value = ( !empty( $check ) ) ? 'url("' . esc_url( $value ) . '")' : '';
				break;

			case 'background-repeat':
				$value = hoot_sanitize_background_repeat( $value );
				break;

			case 'background-position':
				$value = hoot_sanitize_background_position( $value );
				break;

			case 'background-attachment':
				$value = hoot_sanitize_background_attachment( $value );
				break;

			case 'background-size':
				$value = hoot_sanitize_background_size( $value );
				break;

			case 'box-shadow':
			case '-moz-box-shadow':
			case '-webkit-box-shadow':
				$value = esc_attr( $value );
				break;

			case 'font-family':
				$value = esc_attr( $value );
				break;

			case 'font-style':
				$recognized = array( 'inherit', 'initial', 'italic', 'normal', 'oblique' );
				$value = in_array( $value, $recognized ) ? $value : '';
				break;

			case 'font-weight':
				$value_check = intval( $value );
				if ( !empty( $value_check ) ) {
					// for numerical weights like 300, 600 etc.
					$value = $value_check;
				} else {
					// for strings like 'bold', 'light', 'lighter' etc.
					$recognized = array( 'bold', 'bolder', 'inherit', 'initial', 'lighter', 'normal' );
					$value = in_array( $value, $recognized ) ? $value : '';
				}
				break;

			case 'text-decoration':
				$recognized = array( 'blink', 'inherit', 'initial', 'line-through', 'overline', 'underline' );
				$value = in_array( $value, $recognized ) ? $value : '';
				break;

			case 'text-transform':
				$recognized = array( 'capitalize', 'inherit', 'initial', 'lowercase', 'none', 'uppercase' );
				$value = in_array( $value, $recognized ) ? $value : '';
				break;

			case 'font-size':
			case 'padding':
			case 'padding-right':
			case 'padding-bottom':
			case 'padding-left':
			case 'padding-top':
			case 'margin':
			case 'margin-right':
			case 'margin-bottom':
			case 'margin-left':
			case 'margin-top':
			case 'height':
			case 'max-height':
			case 'min-height':
			case 'width':
			case 'max-width':
			case 'min-width':
				$value_check = preg_replace('/px|em|rem/','', $value);
				$value_check = intval( $value_check );
				$value = ( !empty( $value_check ) || '0' === $value_check || 0 === $value_check ) ? $value : '';
				break;

			case 'opacity':
				$value_check = intval( $value );
				$value = ( !empty( $value_check ) || '0' === $value_check || 0 === $value_check ) ? $value : '';
				break;

		endswitch; endif;

		/* Return */
		if ( empty( $value ) ) {
			// if $value is empty => failed sanitization checks
			return '';
		} else {
			return array(
				$property => array(
					'value'     => $value,
					'important' => $important,
					'idtag'     => $idtag,
				),
			);
		}

	}

	/**
	 * Sanitize and create CSS styles from a background type.
	 *
	 * @since 3.0.0
	 * @access public
	 * @param string $idtag
	 * @param bool|string $important
	 * @return mixed empty if sanitization failed, else array of sanitized properties arrays
	 */
	function sanitize_background( $idtag, $important = false ) {

		if ( empty( $idtag ) || !is_string( $idtag ) )
			return '';

		/* Variables */
		$background = array();
		$properties = array( 'color', 'type', 'pattern', 'image', 'repeat', 'position', 'attachment', 'size' );

		/* Get Background Values */
		foreach ( $properties as $property ) {
			$prop_value = hoot_get_mod( "{$idtag}-{$property}" );
			if ( !empty( $prop_value ) )
				$background[ $property ] = $prop_value ;
		}

		return $this->sanitize_backgroundarray( $background,  $idtag, $important );
	}

	/**
	 * Sanitize and create CSS styles from a background type.
	 *
	 * @since 3.0.0
	 * @access public
	 * @param string $idtag
	 * @param bool|string $important
	 * @return mixed empty if sanitization failed, else array of sanitized properties arrays
	 */
	function sanitize_backgroundarray( $backgroundarray, $idtag = '', $important = false ) {

		if ( empty( $idtag ) || !is_string( $idtag ) )
			$idtag = '';

		/* Variables */
		$output = array();
		$properties = array( 'color', 'type', 'pattern', 'image', 'repeat', 'position', 'attachment', 'size' );

		$backgroundarray = wp_parse_args( $backgroundarray, array(
			'color' => '',
			'type' => 'custom',
			'pattern' => 0,
			'image' => '',
			'repeat' => '',
			'position' => '',
			'attachment' => '',
			'size' => '',
		) );
		extract( $backgroundarray, EXTR_SKIP );

		/* Create CSS Rules */
		if ( !empty( $color ) ) {
			$output['color'] = $this->sanitize_css_rule( 'background-color', $color, $idtag . '-color', $important );
		}

		if ( 'custom' == $type ) {
			$check = esc_url( $image );
			if ( !empty( $check ) ) {

				$output['image'] = $this->sanitize_css_rule( 'background-image', $image, $idtag . '-image', $important );

				if ( !empty( $repeat ) )
					$output['repeat'] = $this->sanitize_css_rule( 'background-repeat', $repeat, $idtag . '-repeat', $important );

				if ( !empty( $position ) ) 
					$output['position'] = $this->sanitize_css_rule( 'background-position', $position, $idtag . '-position', $important );

				if ( !empty( $attachment ) )
					$output['attachment'] = $this->sanitize_css_rule( 'background-attachment', $attachment, $idtag . '-attachment', $important );

				if ( !empty( $size ) && $size != 'original' )
					$output['size'] = $this->sanitize_css_rule( 'background-size', $size, $idtag . '-size', $important );

			}
		}
		else { // 'predefined' == $type || empty

			$pattern = !empty( $pattern ) && is_string( $pattern ) ? $pattern : '';
			if ( $pattern ) {
				$themeimgdir = hoot_data('template_uri'); if ( ! is_string( $themeimgdir ) ) $themeimgdir = '';
				$pluginimgdir = hoot_data('premium_uri'); if ( ! is_string( $pluginimgdir ) ) $pluginimgdir = '';
				$pattern = str_replace( array( '//theme//', '//plugin//', '//png', '//jpg' ), array( $themeimgdir , $pluginimgdir, '.png', '.jpg' ), $pattern );
				$background_image = esc_url( $pattern );
				$output['image'] = $this->sanitize_css_rule( 'background-image', $background_image, $idtag . '-pattern', $important );
				$output['repeat'] = $this->sanitize_css_rule( 'background-repeat', 'repeat', '', $important );
			}

		}

		$output = apply_filters( 'hoot_style_builder_background', $output, $idtag, $important );

		/* Return */
		if ( is_array( $output ) && !empty( $output ) ) {
			$return = array();
			foreach ( $output as $rule ) {
				if ( !empty( $rule ) && is_array( $rule ) )
					$return = array_merge( $return, $rule );
			}
			return $return;
		}

		/* Failed Sanitization */
		return '';
	}

	/**
	 * Instantiate or return the one Hoot_Customize instance.
	 *
	 * @since 3.0.0
	 * @access public
	 * @return Hoot_Customize
	 */
	public static function get_instance() {
		if ( is_null( self::$instance ) ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

}

/* Initialize class */
global $hoot_style_builder;
$hoot_style_builder = Hoot_Style_Builder::get_instance();