<?php

require_once __DIR__ . '/dependency-path-mapping.php';
require_once __DIR__ . '/utils.php';

if ( ! defined( 'ALLOW_GZIP_COMPRESSION' ) ) {
	define( 'ALLOW_GZIP_COMPRESSION', true );
}

class Page_Optimize_CSS_Concat extends WP_Styles {
	private $dependency_path_mapping;
	private $old_styles;

	public $allow_gzip_compression;

	function __construct( $styles ) {
		if ( empty( $styles ) || ! ( $styles instanceof WP_Styles ) ) {
			$this->old_styles = new WP_Styles();
		} else {
			$this->old_styles = $styles;
		}

		// Unset all the object properties except our private copy of the styles object.
		// We have to unset everything so that the overload methods talk to $this->old_styles->whatever
		// instead of $this->whatever.
		foreach ( array_keys( get_object_vars( $this ) ) as $key ) {
			if ( 'old_styles' === $key ) {
				continue;
			}
			unset( $this->$key );
		}

		$this->dependency_path_mapping = new Page_Optimize_Dependency_Path_Mapping(
			apply_filters( 'page_optimize_site_url', $this->base_url )
		);
	}

	protected function has_inline_style( $handle ) {
		$after_output = $this->get_data( $handle, 'after' );
		if ( ! empty( $after_output ) ) {
			return true;
		}

		return false;
	}

	protected function css_has_import( $path ) {
		static $cache = array();

		if ( empty( $path ) ) {
			return false;
		}

		$key = $path;

		if ( array_key_exists( $key, $cache ) ) {
			return $cache[ $key ];
		}

		$fh = @fopen( $path, 'rb' );
		if ( ! $fh ) {
			$cache[ $key ] = false;
			return false;
		}

		$found = false;
		$tail  = '';

		// Scan in chunks to keep memory bounded for large stylesheets.
		while ( ! feof( $fh ) ) {
			$chunk = fread( $fh, 32768 );
			if ( false === $chunk || '' === $chunk ) {
				break;
			}

			$haystack = $tail . $chunk;
			if ( false !== stripos( $haystack, '@import' ) ) {
				$found = true;
				break;
			}

			// Keep a short overlap so "@import" across chunk boundaries isn't missed.
			$tail = substr( $haystack, -7 );
		}

		fclose( $fh );

		$cache[ $key ] = $found;
		return $found;
	}

	function do_items( $handles = false, $group = false ) {
		$handles = false === $handles ? $this->queue : (array) $handles;
		$stylesheets = array();
		$siteurl = apply_filters( 'page_optimize_site_url', $this->base_url );

		$this->all_deps( $handles );

		$concat_group = null;

		foreach ( $this->to_do as $key => $handle ) {
			$css_realpath = null;
			// 1a. Skip invalid dependencies.
			if ( ! isset( $this->registered[ $handle ] ) ) {
				unset( $this->to_do[ $key ] );
				continue;
			}

			$obj = $this->registered[ $handle ];

			// 1b. Skip virtual (src-less) dependencies.
			if ( empty( $obj->src ) ) {
				if ( null !== $concat_group ) {
					$stylesheets[] = $concat_group;
					$concat_group = null;
				}

				$stylesheets[] = array(
					'type'   => 'do_item',
					'handle' => $handle,
				);
				unset( $this->to_do[ $key ] );
				continue;
			}

			$obj->src = apply_filters( 'style_loader_src', $obj->src, $obj->handle );

			// Core is kind of broken and returns "true" for src of "colors" handle
			// http://core.trac.wordpress.org/attachment/ticket/16827/colors-hacked-fixed.diff
			// http://core.trac.wordpress.org/ticket/20729
			$css_url = $obj->src;
			if ( 'colors' == $obj->handle && true === $css_url ) {
				$css_url = wp_style_loader_src( $css_url, $obj->handle );
			}

			$css_url_parsed = parse_url( $css_url );
			$css_path = ( is_array( $css_url_parsed ) && isset( $css_url_parsed['path'] ) ) ? $css_url_parsed['path'] : '';
			$extra = $obj->extra;

			// 2. Determine if this stylesheet can be concatenated.

			// Don't concat by default
			$do_concat = false;

			// Only try to concat static css files
			if ( $css_path && false !== strpos( $css_path, '.css' ) ) {
				$do_concat = true;
			} else {
				if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
					echo sprintf( "\n<!-- No Concat CSS %s => Maybe Not Static File %s -->\n", esc_html( $handle ), esc_html( $obj->src ) );
				}
			}

			// Don't try to concat styles which are loaded conditionally (like IE stuff)
			if ( isset( $extra['conditional'] ) ) {
				if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
					echo sprintf( "\n<!-- No Concat CSS %s => Has Conditional -->\n", esc_html( $handle ) );
				}
				$do_concat = false;
			}

			// Don't concat rtl stuff for now until concat supports it correctly
			if ( $do_concat && 'rtl' === $this->text_direction && ! empty( $extra['rtl'] ) ) {
				if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
					echo sprintf( "\n<!-- No Concat CSS %s => Is RTL -->\n", esc_html( $handle ) );
				}
				$do_concat = false;
			}

			// Don't try to concat externally hosted scripts
			$is_internal_uri = $this->dependency_path_mapping->is_internal_uri( $css_url );
			if ( $do_concat && ! $is_internal_uri ) {
				if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
					echo sprintf( "\n<!-- No Concat CSS %s => External URL: %s -->\n", esc_html( $handle ), esc_url( $css_url ) );
				}
				$do_concat = false;
			}

			if ( $do_concat ) {
				// Resolve paths and concat styles that exist in the filesystem
				$css_realpath = $this->dependency_path_mapping->dependency_src_to_fs_path( $css_url );
				if ( false === $css_realpath ) {
					if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
						echo sprintf( "\n<!-- No Concat CSS %s => Invalid Path %s -->\n", esc_html( $handle ), esc_html( $css_realpath ) );
					}
					$do_concat = false;
				}
			}

			// Skip concating CSS from exclusion list
			$exclude_list = page_optimize_css_exclude_list();
			foreach ( $exclude_list as $exclude ) {
				if ( $do_concat && $handle === $exclude ) {
					$do_concat = false;
					if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
						echo sprintf( "\n<!-- No Concat CSS %s => Excluded option -->\n", esc_html( $handle ) );
					}
				}
			}

			// Allow plugins to disable concatenation of certain stylesheets.
			$filtered_concat = apply_filters( 'css_do_concat', $do_concat, $handle );
			if ( $do_concat && ! $filtered_concat ) {
				if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
					echo sprintf( "\n<!-- No Concat CSS %s => Filtered `false` -->\n", esc_html( $handle ) );
				}
			}
			$do_concat = $filtered_concat;

			/**
			 * 3. Add to the stylesheets output list.
			 *
			 * We keep a running $concat_group that accumulates consecutive concat-eligible styles
			 * sharing the same media type. The group is finalized and appended to $stylesheets when:
			 *   - A non-concat style appears
			 *   - The media type changes
			 *   - An inline style is attached to the current style
			 * This makes sure output order matches registration order.
			 **/
			if ( true === $do_concat ) {
				$media = $obj->args;
				if ( empty( $media ) ) {
					$media = 'all';
				}

				// Media type changed - finish the current group.
				if ( null !== $concat_group && $concat_group['media'] !== $media ) {
					$stylesheets[] = $concat_group;
					$concat_group = null;
				}

				// If a non-first stylesheet in a group contains @import, the concat service hoists it,
				// reordering CSS. Split groups so @import stylesheets are always first in their group.
				if (
					null !== $concat_group
					&& ! empty( $css_realpath )
					&& $this->css_has_import( $css_realpath )
				) {
					$stylesheets[] = $concat_group;
					$concat_group = null;
				}

				// Start a new group if needed.
				if ( null === $concat_group ) {
					$concat_group = array(
						'type'    => 'concat',
						'media'   => $media,
						'paths'   => array(),
						'handles' => array(),
					);
				}

				// Add this stylesheet to the current group.
				$concat_group['paths'][] = $css_path;
				$concat_group['handles'][] = $handle;
				$this->done[] = $handle;

				// Inline styles must print right after their <link>, so break the group
				// because we can't concat directly anything after this CSS.
				if ( $this->has_inline_style( $handle ) ) {
					$stylesheets[] = $concat_group;
					$concat_group = null;
				}
			} else {
				// Non-concat item - finish any current open group to preserve order.
				if ( null !== $concat_group ) {
					$stylesheets[] = $concat_group;
					$concat_group = null;
				}
				// Add the non-concat item.
				$stylesheets[] = array(
					'type'   => 'do_item',
					'handle' => $handle,
				);
			}
			unset( $this->to_do[ $key ] );
		}

		if ( null !== $concat_group ) {
			$stylesheets[] = $concat_group;
		}

		foreach ( $stylesheets as $css_array ) {
			if ( 'do_item' === $css_array['type'] ) {
				if ( $this->do_item( $css_array['handle'], $group ) ) {
					$this->done[] = $css_array['handle'];
				}
			} elseif ( 'concat' === $css_array['type'] && isset( $css_array['paths'] ) ) {
				$media = $css_array['media'];
				$css = $css_array['paths'];
				$handles = $css_array['handles'];

				if ( count( $css ) > 1 ) {
					$fs_paths = array();
					foreach ( $css as $css_uri_path ) {
						$fs_paths[] = $this->dependency_path_mapping->uri_path_to_fs_path( $css_uri_path );
					}

					$mtime = max( array_map( 'filemtime', $fs_paths ) );
					if ( page_optimize_use_concat_base_dir() ) {
						$path_str = implode( ',', array_map( 'page_optimize_remove_concat_base_prefix', $fs_paths ) );
					} else {
						$path_str = implode( ',', $css );
					}
					$path_str = "$path_str?m=$mtime";

					if ( $this->allow_gzip_compression ) {
						$path_64 = base64_encode( gzcompress( $path_str ) );
						if ( strlen( $path_str ) > ( strlen( $path_64 ) + 1 ) ) {
							$path_str = '-' . $path_64;
						}
					}

					$href = $siteurl . "/_static/??" . $path_str;
				} else {
					$href = Page_Optimize_Utils::cache_bust_mtime( $css[0], $siteurl );
				}

				$css_id = "$media-css-" . md5( $href );
				if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
					$tag = "<link data-handles='" . esc_attr( implode( ',', $handles ) ) . "' rel='stylesheet' id='$css_id' href='$href' type='text/css' media='$media' />\n";
				} else {
					$tag = "<link rel='stylesheet' id='$css_id' href='$href' type='text/css' media='$media' />\n";
				}

				$tag = apply_filters( 'page_optimize_style_loader_tag', $tag, $handles, $href, $media );

				if ( is_array( $handles ) && count( $handles ) === 1 ) {
					$tag = apply_filters( 'style_loader_tag', $tag, $handles[0], $href, $media );
				}

				echo $tag;
				array_map( array( $this, 'print_inline_style' ), $handles );
			}
		}

		return $this->done;
	}

	function __isset( $key ) {
		return isset( $this->old_styles->$key );
	}

	function __unset( $key ) {
		unset( $this->old_styles->$key );
	}

	function &__get( $key ) {
		return $this->old_styles->$key;
	}

	function __set( $key, $value ) {
		$this->old_styles->$key = $value;
	}
}

function page_optimize_css_concat_init() {
	global $wp_styles;

	$wp_styles = new Page_Optimize_CSS_Concat( $wp_styles );
	$wp_styles->allow_gzip_compression = ALLOW_GZIP_COMPRESSION;
}

if ( page_optimize_should_concat_css() ) {
	add_action( 'init', 'page_optimize_css_concat_init' );
}